mirror of
https://github.com/mozilla-services/syncstorage-rs.git
synced 2026-05-04 19:56:11 +02:00
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
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:
parent
5513da89cd
commit
af1c5fb68a
9
.github/workflows/main-workflow.yml
vendored
9
.github/workflows/main-workflow.yml
vendored
@ -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
|
||||
|
||||
12
Makefile
12
Makefile
@ -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
|
||||
|
||||
@ -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"
|
||||
@ -0,0 +1 @@
|
||||
"""Tools package for syncstorage-rs utilities and scripts."""
|
||||
@ -0,0 +1 @@
|
||||
"""Hawk authentication utilities package."""
|
||||
46
tools/hawk/poetry.lock
generated
46
tools/hawk/poetry.lock
generated
@ -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]]
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
"""Tests for the make_hawk_token utility script."""
|
||||
|
||||
import argparse
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
@ -0,0 +1 @@
|
||||
"""Integration tests package for syncstorage-rs."""
|
||||
@ -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.
|
||||
"""
|
||||
|
||||
883
tools/integration_tests/poetry.lock
generated
883
tools/integration_tests/poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -0,0 +1 @@
|
||||
"""Tokenserver integration tests package."""
|
||||
@ -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(
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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"))
|
||||
|
||||
204
tools/postgres/poetry.lock
generated
204
tools/postgres/poetry.lock
generated
@ -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]]
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -0,0 +1 @@
|
||||
"""Spanner utilities package for syncstorage-rs."""
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
|
||||
602
tools/spanner/poetry.lock
generated
602
tools/spanner/poetry.lock
generated
@ -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]]
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
"""Tests for the Spanner count_expired_rows module."""
|
||||
|
||||
import logging
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
"""Tests for the Spanner TTL purge utility."""
|
||||
|
||||
import argparse
|
||||
from types import SimpleNamespace
|
||||
from unittest import mock
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
"""Tests for the Spanner utilities module."""
|
||||
|
||||
import pytest
|
||||
|
||||
from tools.spanner.utils import ids_from_env
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 []
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
"""Print a public RSA key as a JSON Web Key (JWK)."""
|
||||
|
||||
import sys
|
||||
|
||||
from authlib.jose import JsonWebKey
|
||||
|
||||
1218
tools/syncstorage-loadtest/poetry.lock
generated
1218
tools/syncstorage-loadtest/poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -0,0 +1 @@
|
||||
"""Tokenserver tools package for syncstorage-rs."""
|
||||
@ -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"
|
||||
|
||||
@ -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]"
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
"""Pytest configuration for tokenserver tests, setting up the module path."""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -0,0 +1 @@
|
||||
"""Load tests package for the tokenserver."""
|
||||
@ -1,3 +1,5 @@
|
||||
"""Print a public RSA key as a JSON Web Key (JWK)."""
|
||||
|
||||
import sys
|
||||
|
||||
from authlib.jose import JsonWebKey
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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:
|
||||
|
||||
867
tools/tokenserver/poetry.lock
generated
867
tools/tokenserver/poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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=[
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user