CI: lint docker tests (#3443)

This commit is contained in:
mmetc 2025-02-17 11:04:26 +01:00 committed by GitHub
parent 5136d928ed
commit b3da6e03ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 155 additions and 152 deletions

View file

@ -70,6 +70,7 @@ jobs:
cd docker/test cd docker/test
uv sync --all-extras --dev --locked uv sync --all-extras --dev --locked
uv run ruff check uv run ruff check
uv run basedpyright
uv run pytest tests -n 1 --durations=0 --color=yes uv run pytest tests -n 1 --durations=0 --color=yes
env: env:
CROWDSEC_TEST_VERSION: test CROWDSEC_TEST_VERSION: test

View file

@ -13,6 +13,7 @@ dependencies = [
[dependency-groups] [dependency-groups]
dev = [ dev = [
"basedpyright>=1.26.0",
"ipdb>=0.13.13", "ipdb>=0.13.13",
"ruff>=0.9.3", "ruff>=0.9.3",
] ]
@ -26,16 +27,34 @@ line-length = 120
[tool.ruff.lint] [tool.ruff.lint]
select = [ select = [
"E", # pycodestyle errors "ALL"
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"C", # flake8-comprehensions
"B", # flake8-bugbear
"UP", # pyupgrade
"C90", # macabe
] ]
ignore = [ ignore = [
"B008", # do not perform function calls in argument defaults "ANN", # Missing type annotations
"COM", # flake8-commas
"D", # pydocstyle
"ERA", # eradicate
"FIX", # flake8-fixme
"TD", # flake8-todos
"INP001", # File `...` is part of an implicit namespace package. Add an `__init__.py`.
"E501", # line too long
# ^ does not ignore comments that can't be moved to their own line, line noqa, pyright
# so we take care of line lenghts only with "ruff format"
"PLR2004", # Magic value used in comparison, consider replacing `...` with a constant variable
"S101", # Use of 'assert' detected
"S603", # `subprocess` call: check for execution of untrusted input
"S607", # Starting a process with a partial executable path
] ]
[tool.basedpyright]
reportUnknownArgumentType = "none"
reportUnknownParameterType = "none"
reportMissingParameterType = "none"
reportMissingTypeStubs = "none"
reportUnknownVariableType = "none"
reportUnknownMemberType = "none"
reportUnreachable = "none"
reportAny = "none"

View file

View file

@ -1,6 +1,8 @@
from _pytest.config import Config
pytest_plugins = ("cs",) pytest_plugins = ("cs",)
def pytest_configure(config): def pytest_configure(config: Config) -> None:
config.addinivalue_line("markers", "docker: mark tests for lone or manually orchestrated containers") config.addinivalue_line("markers", "docker: mark tests for lone or manually orchestrated containers")
config.addinivalue_line("markers", "compose: mark tests for docker compose projects") config.addinivalue_line("markers", "compose: mark tests for docker compose projects")

View file

@ -1,5 +1,3 @@
#!/usr/bin/env python
from http import HTTPStatus from http import HTTPStatus
import pytest import pytest
@ -7,7 +5,7 @@ import pytest
pytestmark = pytest.mark.docker pytestmark = pytest.mark.docker
def test_no_agent(crowdsec, flavor): def test_no_agent(crowdsec, flavor: str) -> None:
"""Test DISABLE_AGENT=true""" """Test DISABLE_AGENT=true"""
env = { env = {
"DISABLE_AGENT": "true", "DISABLE_AGENT": "true",
@ -21,7 +19,7 @@ def test_no_agent(crowdsec, flavor):
assert "You can successfully interact with Local API (LAPI)" in stdout assert "You can successfully interact with Local API (LAPI)" in stdout
def test_machine_register(crowdsec, flavor, tmp_path_factory): def test_machine_register(crowdsec, flavor: str, tmp_path_factory: pytest.TempPathFactory) -> None:
"""A local agent is always registered for use by cscli""" """A local agent is always registered for use by cscli"""
data_dir = tmp_path_factory.mktemp("data") data_dir = tmp_path_factory.mktemp("data")

View file

@ -1,6 +1,4 @@
#!/usr/bin/env python import secrets
import random
from http import HTTPStatus from http import HTTPStatus
import pytest import pytest
@ -8,8 +6,8 @@ import pytest
pytestmark = pytest.mark.docker pytestmark = pytest.mark.docker
def test_split_lapi_agent(crowdsec, flavor): def test_split_lapi_agent(crowdsec, flavor: str) -> None:
rand = str(random.randint(0, 10000)) rand = str(secrets.randbelow(10000))
lapiname = f"lapi-{rand}" lapiname = f"lapi-{rand}"
agentname = f"agent-{rand}" agentname = f"agent-{rand}"

View file

@ -1,10 +1,7 @@
#!/usr/bin/env python
""" """
Test bouncer management: pre-installed, run-time installation and removal. Test bouncer management: pre-installed, run-time installation and removal.
""" """
import hashlib
import json import json
from http import HTTPStatus from http import HTTPStatus
@ -13,12 +10,7 @@ import pytest
pytestmark = pytest.mark.docker pytestmark = pytest.mark.docker
def hex512(s): def test_register_bouncer_env(crowdsec, flavor: str) -> None:
"""Return the sha512 hash of a string as a hex string"""
return hashlib.sha512(s.encode()).hexdigest()
def test_register_bouncer_env(crowdsec, flavor):
"""Test installing bouncers at startup, from envvar""" """Test installing bouncers at startup, from envvar"""
env = {"BOUNCER_KEY_bouncer1name": "bouncer1key", "BOUNCER_KEY_bouncer2name": "bouncer2key"} env = {"BOUNCER_KEY_bouncer1name": "bouncer1key", "BOUNCER_KEY_bouncer2name": "bouncer2key"}

View file

@ -1,5 +1,3 @@
#!/usr/bin/env python
from http import HTTPStatus from http import HTTPStatus
import pytest import pytest
@ -7,7 +5,7 @@ import pytest
pytestmark = pytest.mark.docker pytestmark = pytest.mark.docker
def test_no_capi(crowdsec, flavor): def test_no_capi(crowdsec, flavor: str) -> None:
"""Test no CAPI (disabled by default in tests)""" """Test no CAPI (disabled by default in tests)"""
env = { env = {
@ -26,7 +24,7 @@ def test_no_capi(crowdsec, flavor):
assert not any("Registration to online API done" in line for line in logs) assert not any("Registration to online API done" in line for line in logs)
def test_capi(crowdsec, flavor): def test_capi(crowdsec, flavor: str) -> None:
"""Test CAPI""" """Test CAPI"""
env = { env = {

View file

@ -1,5 +1,3 @@
#!/usr/bin/env python
from http import HTTPStatus from http import HTTPStatus
import pytest import pytest
@ -8,16 +6,12 @@ import yaml
pytestmark = pytest.mark.docker pytestmark = pytest.mark.docker
def test_capi_whitelists( def test_capi_whitelists(crowdsec, tmp_path_factory: pytest.TempPathFactory, flavor: str) -> None:
crowdsec,
tmp_path_factory,
flavor,
):
"""Test CAPI_WHITELISTS_PATH""" """Test CAPI_WHITELISTS_PATH"""
env = {"CAPI_WHITELISTS_PATH": "/path/to/whitelists.yaml"} env = {"CAPI_WHITELISTS_PATH": "/path/to/whitelists.yaml"}
whitelists = tmp_path_factory.mktemp("whitelists") whitelists = tmp_path_factory.mktemp("whitelists")
with open(whitelists / "whitelists.yaml", "w") as f: with (whitelists / "whitelists.yaml").open("w") as f:
yaml.dump({"ips": ["1.2.3.4", "2.3.4.5"], "cidrs": ["1.2.3.0/24"]}, f) yaml.dump({"ips": ["1.2.3.4", "2.3.4.5"], "cidrs": ["1.2.3.0/24"]}, f)
volumes = {whitelists / "whitelists.yaml": {"bind": "/path/to/whitelists.yaml", "mode": "ro"}} volumes = {whitelists / "whitelists.yaml": {"bind": "/path/to/whitelists.yaml", "mode": "ro"}}

View file

@ -1,5 +1,3 @@
#!/usr/bin/env python
import datetime import datetime
import pytest import pytest
@ -8,19 +6,19 @@ from pytest_cs import Status
pytestmark = pytest.mark.docker pytestmark = pytest.mark.docker
def test_cold_logs(crowdsec, tmp_path_factory, flavor): def test_cold_logs(crowdsec, tmp_path_factory: pytest.TempPathFactory, flavor: str) -> None:
env = { env = {
"DSN": "file:///var/log/toto.log", "DSN": "file:///var/log/toto.log",
} }
logs = tmp_path_factory.mktemp("logs") logs = tmp_path_factory.mktemp("logs")
now = datetime.datetime.now() - datetime.timedelta(minutes=1) now = datetime.datetime.now(tz=datetime.UTC) - datetime.timedelta(minutes=1)
with open(logs / "toto.log", "w") as f: with (logs / "toto.log").open("w") as f:
# like date '+%b %d %H:%M:%S' but in python # like date '+%b %d %H:%M:%S' but in python
for i in range(10): for i in range(10):
ts = (now + datetime.timedelta(seconds=i)).strftime("%b %d %H:%M:%S") ts = (now + datetime.timedelta(seconds=i)).strftime("%b %d %H:%M:%S")
f.write(ts + " sd-126005 sshd[12422]: Invalid user netflix from 1.1.1.172 port 35424\n") _ = f.write(ts + " sd-126005 sshd[12422]: Invalid user netflix from 1.1.1.172 port 35424\n")
volumes = { volumes = {
logs / "toto.log": {"bind": "/var/log/toto.log", "mode": "ro"}, logs / "toto.log": {"bind": "/var/log/toto.log", "mode": "ro"},
@ -44,7 +42,7 @@ def test_cold_logs(crowdsec, tmp_path_factory, flavor):
) )
def test_cold_logs_missing_dsn(crowdsec, flavor): def test_cold_logs_missing_dsn(crowdsec, flavor: str) -> None:
env = { env = {
"TYPE": "syslog", "TYPE": "syslog",
} }

View file

@ -1,5 +1,3 @@
#!/usr/bin/env python
""" """
Test basic behavior of all the image variants Test basic behavior of all the image variants
""" """
@ -11,7 +9,7 @@ import pytest
pytestmark = pytest.mark.docker pytestmark = pytest.mark.docker
def test_cscli_lapi(crowdsec, flavor): def test_cscli_lapi(crowdsec, flavor: str) -> None:
"""Test if cscli can talk to lapi""" """Test if cscli can talk to lapi"""
with crowdsec(flavor=flavor) as cs: with crowdsec(flavor=flavor) as cs:
cs.wait_for_log("*Starting processing data*") cs.wait_for_log("*Starting processing data*")
@ -23,7 +21,7 @@ def test_cscli_lapi(crowdsec, flavor):
@pytest.mark.skip(reason="currently broken by hub upgrade") @pytest.mark.skip(reason="currently broken by hub upgrade")
def test_flavor_content(crowdsec, flavor): def test_flavor_content(crowdsec, flavor: str) -> None:
"""Test flavor contents""" """Test flavor contents"""
with crowdsec(flavor=flavor) as cs: with crowdsec(flavor=flavor) as cs:
cs.wait_for_log("*Starting processing data*") cs.wait_for_log("*Starting processing data*")

View file

@ -1,31 +1,30 @@
#!/usr/bin/env python
""" """
Smoke tests in case docker is not set up correctly or has connection issues. Smoke tests in case docker is not set up correctly or has connection issues.
""" """
import subprocess import subprocess
import docker
import pytest import pytest
pytestmark = pytest.mark.docker pytestmark = pytest.mark.docker
def test_docker_cli_run(): def test_docker_cli_run() -> None:
"""Test if docker run works from the command line. Capture stdout too""" """Test if docker run works from the command line. Capture stdout too"""
res = subprocess.run(["docker", "run", "--rm", "hello-world"], capture_output=True, text=True) res = subprocess.run(["docker", "run", "--rm", "hello-world"], capture_output=True, text=True, check=True)
assert 0 == res.returncode assert res.returncode == 0
assert "Hello from Docker!" in res.stdout assert "Hello from Docker!" in res.stdout
def test_docker_run(docker_client): def test_docker_run(docker_client: docker.DockerClient) -> None:
"""Test if docker run works from the python SDK.""" """Test if docker run works from the python SDK."""
output = docker_client.containers.run("hello-world", remove=True) output = docker_client.containers.run("hello-world", remove=True)
lines = output.decode().splitlines() lines = output.decode().splitlines()
assert "Hello from Docker!" in lines assert "Hello from Docker!" in lines
def test_docker_run_detach(docker_client): def test_docker_run_detach(docker_client: docker.DockerClient) -> None:
"""Test with python SDK (async).""" """Test with python SDK (async)."""
cont = docker_client.containers.run("hello-world", detach=True) cont = docker_client.containers.run("hello-world", detach=True)
assert cont.status == "created" assert cont.status == "created"

View file

@ -1,5 +1,3 @@
#!/usr/bin/env python
""" """
Test pre-installed hub items. Test pre-installed hub items.
""" """
@ -12,7 +10,7 @@ import pytest
pytestmark = pytest.mark.docker pytestmark = pytest.mark.docker
def test_preinstalled_hub(crowdsec, flavor): def test_preinstalled_hub(crowdsec, flavor: str) -> None:
"""Test hub objects installed in the entrypoint""" """Test hub objects installed in the entrypoint"""
with crowdsec(flavor=flavor) as cs: with crowdsec(flavor=flavor) as cs:
cs.wait_for_log("*Starting processing data*") cs.wait_for_log("*Starting processing data*")

View file

@ -1,5 +1,3 @@
#!/usr/bin/env python
""" """
Test collection management Test collection management
""" """
@ -12,7 +10,7 @@ import pytest
pytestmark = pytest.mark.docker pytestmark = pytest.mark.docker
def test_install_two_collections(crowdsec, flavor): def test_install_two_collections(crowdsec, flavor: str) -> None:
"""Test installing collections at startup""" """Test installing collections at startup"""
it1 = "crowdsecurity/apache2" it1 = "crowdsecurity/apache2"
it2 = "crowdsecurity/asterisk" it2 = "crowdsecurity/asterisk"
@ -33,7 +31,7 @@ def test_install_two_collections(crowdsec, flavor):
) )
def test_disable_collection(crowdsec, flavor): def test_disable_collection(crowdsec, flavor: str) -> None:
"""Test removing a pre-installed collection at startup""" """Test removing a pre-installed collection at startup"""
it = "crowdsecurity/linux" it = "crowdsecurity/linux"
env = {"DISABLE_COLLECTIONS": it} env = {"DISABLE_COLLECTIONS": it}
@ -52,7 +50,7 @@ def test_disable_collection(crowdsec, flavor):
) )
def test_install_and_disable_collection(crowdsec, flavor): def test_install_and_disable_collection(crowdsec, flavor: str) -> None:
"""Declare a collection to install AND disable: disable wins""" """Declare a collection to install AND disable: disable wins"""
it = "crowdsecurity/apache2" it = "crowdsecurity/apache2"
env = { env = {
@ -73,7 +71,7 @@ def test_install_and_disable_collection(crowdsec, flavor):
# already done in bats, prividing here as example of a somewhat complex test # already done in bats, prividing here as example of a somewhat complex test
def test_taint_bubble_up(crowdsec, tmp_path_factory, flavor): def test_taint_bubble_up(crowdsec, flavor: str) -> None:
coll = "crowdsecurity/nginx" coll = "crowdsecurity/nginx"
env = {"COLLECTIONS": f"{coll}"} env = {"COLLECTIONS": f"{coll}"}

View file

@ -1,5 +1,3 @@
#!/usr/bin/env python
""" """
Test parser management Test parser management
""" """
@ -12,7 +10,7 @@ import pytest
pytestmark = pytest.mark.docker pytestmark = pytest.mark.docker
def test_install_two_parsers(crowdsec, flavor): def test_install_two_parsers(crowdsec, flavor: str) -> None:
"""Test installing parsers at startup""" """Test installing parsers at startup"""
it1 = "crowdsecurity/cpanel-logs" it1 = "crowdsecurity/cpanel-logs"
it2 = "crowdsecurity/cowrie-logs" it2 = "crowdsecurity/cowrie-logs"
@ -29,7 +27,7 @@ def test_install_two_parsers(crowdsec, flavor):
# XXX check that the parser is preinstalled by default # XXX check that the parser is preinstalled by default
def test_disable_parser(crowdsec, flavor): def test_disable_parser(crowdsec, flavor: str) -> None:
"""Test removing a pre-installed parser at startup""" """Test removing a pre-installed parser at startup"""
it = "crowdsecurity/whitelists" it = "crowdsecurity/whitelists"
env = {"DISABLE_PARSERS": it} env = {"DISABLE_PARSERS": it}
@ -48,7 +46,7 @@ def test_disable_parser(crowdsec, flavor):
assert it not in items assert it not in items
def test_install_and_disable_parser(crowdsec, flavor): def test_install_and_disable_parser(crowdsec, flavor: str) -> None:
"""Declare a parser to install AND disable: disable wins""" """Declare a parser to install AND disable: disable wins"""
it = "crowdsecurity/cpanel-logs" it = "crowdsecurity/cpanel-logs"
env = { env = {

View file

@ -1,5 +1,3 @@
#!/usr/bin/env python
""" """
Test postoverflow management Test postoverflow management
""" """
@ -12,7 +10,7 @@ import pytest
pytestmark = pytest.mark.docker pytestmark = pytest.mark.docker
def test_install_two_postoverflows(crowdsec, flavor): def test_install_two_postoverflows(crowdsec, flavor: str) -> None:
"""Test installing postoverflows at startup""" """Test installing postoverflows at startup"""
it1 = "crowdsecurity/cdn-whitelist" it1 = "crowdsecurity/cdn-whitelist"
it2 = "crowdsecurity/ipv6_to_range" it2 = "crowdsecurity/ipv6_to_range"
@ -30,12 +28,12 @@ def test_install_two_postoverflows(crowdsec, flavor):
assert items[it2]["status"] == "enabled" assert items[it2]["status"] == "enabled"
def test_disable_postoverflow(): def test_disable_postoverflow() -> None:
"""Test removing a pre-installed postoverflow at startup""" """Test removing a pre-installed postoverflow at startup"""
pytest.skip("we don't preinstall postoverflows") pytest.skip("we don't preinstall postoverflows")
def test_install_and_disable_postoverflow(crowdsec, flavor): def test_install_and_disable_postoverflow(crowdsec, flavor: str) -> None:
"""Declare a postoverflow to install AND disable: disable wins""" """Declare a postoverflow to install AND disable: disable wins"""
it = "crowdsecurity/cdn-whitelist" it = "crowdsecurity/cdn-whitelist"
env = { env = {

View file

@ -1,5 +1,3 @@
#!/usr/bin/env python
""" """
Test scenario management Test scenario management
""" """
@ -12,7 +10,7 @@ import pytest
pytestmark = pytest.mark.docker pytestmark = pytest.mark.docker
def test_install_two_scenarios(crowdsec, flavor): def test_install_two_scenarios(crowdsec, flavor: str) -> None:
"""Test installing scenarios at startup""" """Test installing scenarios at startup"""
it1 = "crowdsecurity/cpanel-bf-attempt" it1 = "crowdsecurity/cpanel-bf-attempt"
it2 = "crowdsecurity/asterisk_bf" it2 = "crowdsecurity/asterisk_bf"
@ -28,7 +26,7 @@ def test_install_two_scenarios(crowdsec, flavor):
assert items[it2]["status"] == "enabled" assert items[it2]["status"] == "enabled"
def test_disable_scenario(crowdsec, flavor): def test_disable_scenario(crowdsec, flavor: str) -> None:
"""Test removing a pre-installed scenario at startup""" """Test removing a pre-installed scenario at startup"""
it = "crowdsecurity/ssh-bf" it = "crowdsecurity/ssh-bf"
env = {"DISABLE_SCENARIOS": it} env = {"DISABLE_SCENARIOS": it}
@ -42,7 +40,7 @@ def test_disable_scenario(crowdsec, flavor):
assert it not in items assert it not in items
def test_install_and_disable_scenario(crowdsec, flavor): def test_install_and_disable_scenario(crowdsec, flavor: str) -> None:
"""Declare a scenario to install AND disable: disable wins""" """Declare a scenario to install AND disable: disable wins"""
it = "crowdsecurity/asterisk_bf" it = "crowdsecurity/asterisk_bf"
env = { env = {

View file

@ -1,5 +1,3 @@
#!/usr/bin/env python
from http import HTTPStatus from http import HTTPStatus
import pytest import pytest
@ -7,7 +5,7 @@ import pytest
pytestmark = pytest.mark.docker pytestmark = pytest.mark.docker
def test_local_api_url_default(crowdsec, flavor): def test_local_api_url_default(crowdsec, flavor: str) -> None:
"""Test LOCAL_API_URL (default)""" """Test LOCAL_API_URL (default)"""
with crowdsec(flavor=flavor) as cs: with crowdsec(flavor=flavor) as cs:
cs.wait_for_log(["*CrowdSec Local API listening on *:8080*", "*Starting processing data*"]) cs.wait_for_log(["*CrowdSec Local API listening on *:8080*", "*Starting processing data*"])
@ -19,7 +17,7 @@ def test_local_api_url_default(crowdsec, flavor):
assert "You can successfully interact with Local API (LAPI)" in stdout assert "You can successfully interact with Local API (LAPI)" in stdout
def test_local_api_url(crowdsec, flavor): def test_local_api_url(crowdsec, flavor: str) -> None:
"""Test LOCAL_API_URL (custom)""" """Test LOCAL_API_URL (custom)"""
env = {"LOCAL_API_URL": "http://127.0.0.1:8080"} env = {"LOCAL_API_URL": "http://127.0.0.1:8080"}
with crowdsec(flavor=flavor, environment=env) as cs: with crowdsec(flavor=flavor, environment=env) as cs:
@ -32,7 +30,7 @@ def test_local_api_url(crowdsec, flavor):
assert "You can successfully interact with Local API (LAPI)" in stdout assert "You can successfully interact with Local API (LAPI)" in stdout
def test_local_api_url_ipv6(crowdsec, flavor): def test_local_api_url_ipv6(crowdsec, flavor: str) -> None:
"""Test LOCAL_API_URL (custom with ipv6)""" """Test LOCAL_API_URL (custom with ipv6)"""
pytest.skip("ipv6 not supported yet") pytest.skip("ipv6 not supported yet")

View file

@ -1,5 +1,3 @@
#!/usr/bin/env python
""" """
Test bind-mounting local items Test bind-mounting local items
""" """
@ -12,14 +10,14 @@ import pytest
pytestmark = pytest.mark.docker pytestmark = pytest.mark.docker
def test_inject_local_item(crowdsec, tmp_path_factory, flavor): def test_inject_local_item(crowdsec, tmp_path_factory: pytest.TempPathFactory, flavor: str) -> None:
"""Test mounting a custom whitelist at startup""" """Test mounting a custom whitelist at startup"""
localitems = tmp_path_factory.mktemp("localitems") localitems = tmp_path_factory.mktemp("localitems")
custom_whitelists = localitems / "custom_whitelists.yaml" custom_whitelists = localitems / "custom_whitelists.yaml"
with open(custom_whitelists, "w") as f: with custom_whitelists.open("w") as f:
f.write('{"whitelist":{"reason":"Good IPs","ip":["1.2.3.4"]}}') _ = f.write('{"whitelist":{"reason":"Good IPs","ip":["1.2.3.4"]}}')
volumes = {custom_whitelists: {"bind": "/etc/crowdsec/parsers/s02-enrich/custom_whitelists.yaml"}} volumes = {custom_whitelists: {"bind": "/etc/crowdsec/parsers/s02-enrich/custom_whitelists.yaml"}}

View file

@ -1,5 +1,3 @@
#!/usr/bin/env python
from http import HTTPStatus from http import HTTPStatus
import pytest import pytest
@ -7,7 +5,7 @@ import pytest
pytestmark = pytest.mark.docker pytestmark = pytest.mark.docker
def test_metrics_port_default(crowdsec, flavor): def test_metrics_port_default(crowdsec, flavor: str) -> None:
"""Test metrics""" """Test metrics"""
metrics_port = 6060 metrics_port = 6060
with crowdsec(flavor=flavor) as cs: with crowdsec(flavor=flavor) as cs:
@ -23,7 +21,7 @@ def test_metrics_port_default(crowdsec, flavor):
assert "# HELP cs_info Information about Crowdsec." in stdout assert "# HELP cs_info Information about Crowdsec." in stdout
def test_metrics_port_default_ipv6(crowdsec, flavor): def test_metrics_port_default_ipv6(crowdsec, flavor: str) -> None:
"""Test metrics (ipv6)""" """Test metrics (ipv6)"""
pytest.skip("ipv6 not supported yet") pytest.skip("ipv6 not supported yet")
port = 6060 port = 6060
@ -39,7 +37,7 @@ def test_metrics_port_default_ipv6(crowdsec, flavor):
assert "# HELP cs_info Information about Crowdsec." in stdout assert "# HELP cs_info Information about Crowdsec." in stdout
def test_metrics_port(crowdsec, flavor): def test_metrics_port(crowdsec, flavor: str) -> None:
"""Test metrics (custom METRICS_PORT)""" """Test metrics (custom METRICS_PORT)"""
port = 7070 port = 7070
env = {"METRICS_PORT": port} env = {"METRICS_PORT": port}
@ -55,7 +53,7 @@ def test_metrics_port(crowdsec, flavor):
assert "# HELP cs_info Information about Crowdsec." in stdout assert "# HELP cs_info Information about Crowdsec." in stdout
def test_metrics_port_ipv6(crowdsec, flavor): def test_metrics_port_ipv6(crowdsec, flavor: str) -> None:
"""Test metrics (custom METRICS_PORT, ipv6)""" """Test metrics (custom METRICS_PORT, ipv6)"""
pytest.skip("ipv6 not supported yet") pytest.skip("ipv6 not supported yet")
port = 7070 port = 7070

View file

@ -1,12 +1,10 @@
#!/usr/bin/env python
import pytest import pytest
from pytest_cs import Status from pytest_cs import Status
pytestmark = pytest.mark.docker pytestmark = pytest.mark.docker
def test_no_agent(crowdsec, flavor): def test_no_agent(crowdsec, flavor: str) -> None:
"""Test DISABLE_LOCAL_API=true (failing stand-alone container)""" """Test DISABLE_LOCAL_API=true (failing stand-alone container)"""
env = { env = {
"DISABLE_LOCAL_API": "true", "DISABLE_LOCAL_API": "true",

View file

@ -1,16 +1,14 @@
#!/usr/bin/env python
import pytest import pytest
pytestmark = pytest.mark.docker pytestmark = pytest.mark.docker
# XXX this is redundant, already tested in pytest_cs # XXX this is redundant, already tested in pytest_cs
def test_crowdsec(crowdsec, flavor): def test_crowdsec(crowdsec, flavor: str) -> None:
with crowdsec(flavor=flavor) as cs: with crowdsec(flavor=flavor) as cs:
for waiter in cs.log_waiters(): for waiter in cs.log_waiters():
with waiter as matcher: with waiter as matcher:
matcher.fnmatch_lines(["*Starting processing data*"]) matcher.fnmatch_lines(["*Starting processing data*"])
res = cs.cont.exec_run('sh -c "echo $CI_TESTING"') res = cs.cont.exec_run('sh -c "echo $CI_TESTING"')
assert res.exit_code == 0 assert res.exit_code == 0
assert "true" == res.output.decode().strip() assert res.output.decode().strip() == "true"

View file

@ -1,10 +1,10 @@
#!/usr/bin/env python
""" """
Test agent-lapi and cscli-lapi communication via TLS, on the same container. Test agent-lapi and cscli-lapi communication via TLS, on the same container.
""" """
import pathlib
import uuid import uuid
from collections.abc import Callable
import pytest import pytest
from pytest_cs import Status from pytest_cs import Status
@ -12,7 +12,7 @@ from pytest_cs import Status
pytestmark = pytest.mark.docker pytestmark = pytest.mark.docker
def test_missing_key_file(crowdsec, flavor): def test_missing_key_file(crowdsec, flavor: str) -> None:
"""Test that cscli and agent can communicate to LAPI with TLS""" """Test that cscli and agent can communicate to LAPI with TLS"""
env = { env = {
@ -24,7 +24,7 @@ def test_missing_key_file(crowdsec, flavor):
cs.wait_for_log("*local API server stopped with error: missing TLS key file*") cs.wait_for_log("*local API server stopped with error: missing TLS key file*")
def test_missing_cert_file(crowdsec, flavor): def test_missing_cert_file(crowdsec, flavor: str) -> None:
"""Test that cscli and agent can communicate to LAPI with TLS""" """Test that cscli and agent can communicate to LAPI with TLS"""
env = { env = {
@ -36,7 +36,7 @@ def test_missing_cert_file(crowdsec, flavor):
cs.wait_for_log("*local API server stopped with error: missing TLS cert file*") cs.wait_for_log("*local API server stopped with error: missing TLS cert file*")
def test_tls_missing_ca(crowdsec, flavor, certs_dir): def test_tls_missing_ca(crowdsec, flavor: str, certs_dir: Callable[..., pathlib.Path]) -> None:
"""Missing CA cert, unknown authority""" """Missing CA cert, unknown authority"""
env = { env = {
@ -54,7 +54,7 @@ def test_tls_missing_ca(crowdsec, flavor, certs_dir):
cs.wait_for_log("*certificate signed by unknown authority*") cs.wait_for_log("*certificate signed by unknown authority*")
def test_tls_legacy_var(crowdsec, flavor, certs_dir): def test_tls_legacy_var(crowdsec, flavor: str, certs_dir: Callable[..., pathlib.Path]) -> None:
"""Test server-only certificate, legacy variables""" """Test server-only certificate, legacy variables"""
env = { env = {
@ -79,7 +79,7 @@ def test_tls_legacy_var(crowdsec, flavor, certs_dir):
assert "You can successfully interact with Local API (LAPI)" in stdout assert "You can successfully interact with Local API (LAPI)" in stdout
def test_tls_mutual_monolith(crowdsec, flavor, certs_dir): def test_tls_mutual_monolith(crowdsec, flavor: str, certs_dir: Callable[..., pathlib.Path]) -> None:
"""Server and client certificates, on the same container""" """Server and client certificates, on the same container"""
env = { env = {
@ -106,7 +106,7 @@ def test_tls_mutual_monolith(crowdsec, flavor, certs_dir):
assert "You can successfully interact with Local API (LAPI)" in stdout assert "You can successfully interact with Local API (LAPI)" in stdout
def test_tls_lapi_var(crowdsec, flavor, certs_dir): def test_tls_lapi_var(crowdsec, flavor: str, certs_dir: Callable[..., pathlib.Path]) -> None:
"""Test server-only certificate, lapi variables""" """Test server-only certificate, lapi variables"""
env = { env = {
@ -136,7 +136,7 @@ def test_tls_lapi_var(crowdsec, flavor, certs_dir):
# we must set insecure_skip_verify to true to use it # we must set insecure_skip_verify to true to use it
def test_tls_split_lapi_agent(crowdsec, flavor, certs_dir): def test_tls_split_lapi_agent(crowdsec, flavor: str, certs_dir: Callable[..., pathlib.Path]) -> None:
"""Server-only certificate, split containers""" """Server-only certificate, split containers"""
rand = uuid.uuid1() rand = uuid.uuid1()
@ -188,7 +188,7 @@ def test_tls_split_lapi_agent(crowdsec, flavor, certs_dir):
assert "You can successfully interact with Local API (LAPI)" in stdout assert "You can successfully interact with Local API (LAPI)" in stdout
def test_tls_mutual_split_lapi_agent(crowdsec, flavor, certs_dir): def test_tls_mutual_split_lapi_agent(crowdsec, flavor: str, certs_dir: Callable[..., pathlib.Path]) -> None:
"""Server and client certificates, split containers""" """Server and client certificates, split containers"""
rand = uuid.uuid1() rand = uuid.uuid1()
@ -238,7 +238,7 @@ def test_tls_mutual_split_lapi_agent(crowdsec, flavor, certs_dir):
assert "You can successfully interact with Local API (LAPI)" in stdout assert "You can successfully interact with Local API (LAPI)" in stdout
def test_tls_client_ou(crowdsec, flavor, certs_dir): def test_tls_client_ou(crowdsec, flavor: str, certs_dir: Callable[..., pathlib.Path]) -> None:
"""Check behavior of client certificate vs AGENTS_ALLOWED_OU""" """Check behavior of client certificate vs AGENTS_ALLOWED_OU"""
rand = uuid.uuid1() rand = uuid.uuid1()

View file

@ -1,11 +1,9 @@
#!/usr/bin/env python
import pytest import pytest
pytestmark = pytest.mark.docker pytestmark = pytest.mark.docker
def test_version_docker_platform(crowdsec, flavor): def test_version_docker_platform(crowdsec, flavor: str) -> None:
with crowdsec(flavor=flavor) as cs: with crowdsec(flavor=flavor) as cs:
for waiter in cs.log_waiters(): for waiter in cs.log_waiters():
with waiter as matcher: with waiter as matcher:

View file

@ -1,5 +1,3 @@
#!/usr/bin/env python
from http import HTTPStatus from http import HTTPStatus
import pytest import pytest
@ -7,7 +5,7 @@ import pytest
pytestmark = pytest.mark.docker pytestmark = pytest.mark.docker
def test_use_wal_default(crowdsec, flavor): def test_use_wal_default(crowdsec, flavor: str) -> None:
"""Test USE_WAL default""" """Test USE_WAL default"""
with crowdsec(flavor=flavor) as cs: with crowdsec(flavor=flavor) as cs:
cs.wait_for_log("*Starting processing data*") cs.wait_for_log("*Starting processing data*")
@ -18,7 +16,7 @@ def test_use_wal_default(crowdsec, flavor):
assert "false" in stdout assert "false" in stdout
def test_use_wal_true(crowdsec, flavor): def test_use_wal_true(crowdsec, flavor: str) -> None:
"""Test USE_WAL=true""" """Test USE_WAL=true"""
env = { env = {
"USE_WAL": "true", "USE_WAL": "true",
@ -32,7 +30,7 @@ def test_use_wal_true(crowdsec, flavor):
assert "true" in stdout assert "true" in stdout
def test_use_wal_false(crowdsec, flavor): def test_use_wal_false(crowdsec, flavor: str) -> None:
"""Test USE_WAL=false""" """Test USE_WAL=false"""
env = { env = {
"USE_WAL": "false", "USE_WAL": "false",

86
docker/test/uv.lock generated
View file

@ -11,12 +11,24 @@ wheels = [
] ]
[[package]] [[package]]
name = "certifi" name = "basedpyright"
version = "2024.12.14" version = "1.26.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0f/bd/1d41ee578ce09523c81a15426705dd20969f5abf006d1afe8aeff0dd776a/certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db", size = 166010 } dependencies = [
{ name = "nodejs-wheel-binaries" },
]
sdist = { url = "https://files.pythonhosted.org/packages/18/c2/5685d040d4f2598788d42bfd2db5f808e9aa2eaee77fcae3c2fbe4ea0e7c/basedpyright-1.26.0.tar.gz", hash = "sha256:5e01f6eb9290a09ef39672106cf1a02924fdc8970e521838bc502ccf0676f32f", size = 24932771 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/a5/32/8f6669fc4798494966bf446c8c4a162e0b5d893dff088afddf76414f70e1/certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", size = 164927 }, { url = "https://files.pythonhosted.org/packages/8e/72/65308f45bb73efc93075426cac5f37eea937ae364aa675785521cb3512c7/basedpyright-1.26.0-py3-none-any.whl", hash = "sha256:5a6a17f2c389ec313dd2c3644f40e8221bc90252164802e626055341c0a37381", size = 11504579 },
]
[[package]]
name = "certifi"
version = "2025.1.31"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
] ]
[[package]] [[package]]
@ -109,6 +121,7 @@ dependencies = [
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
{ name = "basedpyright" },
{ name = "ipdb" }, { name = "ipdb" },
{ name = "ruff" }, { name = "ruff" },
] ]
@ -123,6 +136,7 @@ requires-dist = [
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
{ name = "basedpyright", specifier = ">=1.26.0" },
{ name = "ipdb", specifier = ">=0.13.13" }, { name = "ipdb", specifier = ">=0.13.13" },
{ name = "ruff", specifier = ">=0.9.3" }, { name = "ruff", specifier = ">=0.9.3" },
] ]
@ -232,7 +246,7 @@ wheels = [
[[package]] [[package]]
name = "ipython" name = "ipython"
version = "8.31.0" version = "8.32.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" }, { name = "colorama", marker = "sys_platform == 'win32'" },
@ -245,9 +259,9 @@ dependencies = [
{ name = "stack-data" }, { name = "stack-data" },
{ name = "traitlets" }, { name = "traitlets" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/01/35/6f90fdddff7a08b7b715fccbd2427b5212c9525cd043d26fdc45bee0708d/ipython-8.31.0.tar.gz", hash = "sha256:b6a2274606bec6166405ff05e54932ed6e5cfecaca1fc05f2cacde7bb074d70b", size = 5501011 } sdist = { url = "https://files.pythonhosted.org/packages/36/80/4d2a072e0db7d250f134bc11676517299264ebe16d62a8619d49a78ced73/ipython-8.32.0.tar.gz", hash = "sha256:be2c91895b0b9ea7ba49d33b23e2040c352b33eb6a519cca7ce6e0c743444251", size = 5507441 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/04/60/d0feb6b6d9fe4ab89fe8fe5b47cbf6cd936bfd9f1e7ffa9d0015425aeed6/ipython-8.31.0-py3-none-any.whl", hash = "sha256:46ec58f8d3d076a61d128fe517a51eb730e3aaf0c184ea8c17d16e366660c6a6", size = 821583 }, { url = "https://files.pythonhosted.org/packages/e7/e1/f4474a7ecdb7745a820f6f6039dc43c66add40f1bcc66485607d93571af6/ipython-8.32.0-py3-none-any.whl", hash = "sha256:cae85b0c61eff1fc48b0a8002de5958b6528fa9c8defb1894da63f42613708aa", size = 825524 },
] ]
[[package]] [[package]]
@ -274,6 +288,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899 }, { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899 },
] ]
[[package]]
name = "nodejs-wheel-binaries"
version = "22.13.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5d/c5/1af2fc54fcc18f4a99426b46f18832a04f755ee340019e1be536187c1e1c/nodejs_wheel_binaries-22.13.1.tar.gz", hash = "sha256:a0c15213c9c3383541be4400a30959883868ce5da9cebb3d63ddc7fe61459308", size = 8053 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/e9/b0dd118e0fd4eabe1ec9c3d9a68df4d811282e8837b811d804f23742e117/nodejs_wheel_binaries-22.13.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:e4f64d0e26600d51cbdd98a6718a19c2d1b8c7538e9e353e95a634a06a8e1a58", size = 51015650 },
{ url = "https://files.pythonhosted.org/packages/cc/a6/9ba835f5d4f3f6b1f01191e7ac0874871f9743de5c42a5a9a54e67c2e2a6/nodejs_wheel_binaries-22.13.1-py2.py3-none-macosx_11_0_x86_64.whl", hash = "sha256:afcb40484bb02f23137f838014724604ae183fd767b30da95b0be1510a40c06d", size = 51814957 },
{ url = "https://files.pythonhosted.org/packages/0d/2e/a430207e5f22bd3dcffb81acbddf57ee4108b9e2b0f99a5578dc2c1ff7fc/nodejs_wheel_binaries-22.13.1-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fc88c98eebabfc36b5270a4ab974a2682746931567ca76a5ca49c54482bbb51", size = 57148437 },
{ url = "https://files.pythonhosted.org/packages/97/f4/5731b6f0c8af434619b4f1b8fd895bc33fca60168cd68133e52841872114/nodejs_wheel_binaries-22.13.1-py2.py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b9f75ea8f5e3e5416256fcb00a98cbe14c8d3b6dcaf17da29c4ade5723026d8", size = 57634451 },
{ url = "https://files.pythonhosted.org/packages/49/28/83166f7e39812e9ef99cfa3e722c54e32dd9de6a1290f3216c2e5d1f4957/nodejs_wheel_binaries-22.13.1-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:94608702ef6c389d32e89ff3b7a925cb5dedaf55b5d98bd0c4fb3450a8b6d1c1", size = 58794510 },
{ url = "https://files.pythonhosted.org/packages/f7/64/4832ec26d0a7ca7a5574df265d85c6832f9a624024511fc34958227ad740/nodejs_wheel_binaries-22.13.1-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:53a40d0269689aa2eaf2e261cbe5ec256644bc56aae0201ef344b7d8f40ccc79", size = 59738596 },
{ url = "https://files.pythonhosted.org/packages/18/cd/def29615dac250cda3d141e1c03b7153b9a027360bde0272a6768c5fae33/nodejs_wheel_binaries-22.13.1-py2.py3-none-win_amd64.whl", hash = "sha256:549371a929a29fbce8d0ab8f1b5410549946d4f1b0376a5ce635b45f6d05298f", size = 40455444 },
{ url = "https://files.pythonhosted.org/packages/15/d7/6de2bc615203bf590ca437a5cac145b2f86d994ce329489125a0a90ba715/nodejs_wheel_binaries-22.13.1-py2.py3-none-win_arm64.whl", hash = "sha256:cf72d50d755f4e5c0709b0449de01768d96b3b1ec7aa531561415b88f179ad8b", size = 36200929 },
]
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "24.2" version = "24.2"
@ -393,8 +423,8 @@ wheels = [
[[package]] [[package]]
name = "pytest-cs" name = "pytest-cs"
version = "0.7.20" version = "0.7.21"
source = { git = "https://github.com/crowdsecurity/pytest-cs#73380b837a80337f361414bebbaf4b914713c4ae" } source = { git = "https://github.com/crowdsecurity/pytest-cs#1eb949d7befa6fe172bf459616b267d4ffc01179" }
dependencies = [ dependencies = [
{ name = "docker" }, { name = "docker" },
{ name = "psutil" }, { name = "psutil" },
@ -509,27 +539,27 @@ wheels = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.9.3" version = "0.9.4"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1e/7f/60fda2eec81f23f8aa7cbbfdf6ec2ca11eb11c273827933fb2541c2ce9d8/ruff-0.9.3.tar.gz", hash = "sha256:8293f89985a090ebc3ed1064df31f3b4b56320cdfcec8b60d3295bddb955c22a", size = 3586740 } sdist = { url = "https://files.pythonhosted.org/packages/c0/17/529e78f49fc6f8076f50d985edd9a2cf011d1dbadb1cdeacc1d12afc1d26/ruff-0.9.4.tar.gz", hash = "sha256:6907ee3529244bb0ed066683e075f09285b38dd5b4039370df6ff06041ca19e7", size = 3599458 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/77/4fb790596d5d52c87fd55b7160c557c400e90f6116a56d82d76e95d9374a/ruff-0.9.3-py3-none-linux_armv6l.whl", hash = "sha256:7f39b879064c7d9670197d91124a75d118d00b0990586549949aae80cdc16624", size = 11656815 }, { url = "https://files.pythonhosted.org/packages/b6/f8/3fafb7804d82e0699a122101b5bee5f0d6e17c3a806dcbc527bb7d3f5b7a/ruff-0.9.4-py3-none-linux_armv6l.whl", hash = "sha256:64e73d25b954f71ff100bb70f39f1ee09e880728efb4250c632ceed4e4cdf706", size = 11668400 },
{ url = "https://files.pythonhosted.org/packages/a2/a8/3338ecb97573eafe74505f28431df3842c1933c5f8eae615427c1de32858/ruff-0.9.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a187171e7c09efa4b4cc30ee5d0d55a8d6c5311b3e1b74ac5cb96cc89bafc43c", size = 11594821 }, { url = "https://files.pythonhosted.org/packages/2e/a6/2efa772d335da48a70ab2c6bb41a096c8517ca43c086ea672d51079e3d1f/ruff-0.9.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6ce6743ed64d9afab4fafeaea70d3631b4d4b28b592db21a5c2d1f0ef52934bf", size = 11628395 },
{ url = "https://files.pythonhosted.org/packages/8e/89/320223c3421962762531a6b2dd58579b858ca9916fb2674874df5e97d628/ruff-0.9.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c59ab92f8e92d6725b7ded9d4a31be3ef42688a115c6d3da9457a5bda140e2b4", size = 11040475 }, { url = "https://files.pythonhosted.org/packages/dc/d7/cd822437561082f1c9d7225cc0d0fbb4bad117ad7ac3c41cd5d7f0fa948c/ruff-0.9.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:54499fb08408e32b57360f6f9de7157a5fec24ad79cb3f42ef2c3f3f728dfe2b", size = 11090052 },
{ url = "https://files.pythonhosted.org/packages/b2/bd/1d775eac5e51409535804a3a888a9623e87a8f4b53e2491580858a083692/ruff-0.9.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dc153c25e715be41bb228bc651c1e9b1a88d5c6e5ed0194fa0dfea02b026439", size = 11856207 }, { url = "https://files.pythonhosted.org/packages/9e/67/3660d58e893d470abb9a13f679223368ff1684a4ef40f254a0157f51b448/ruff-0.9.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37c892540108314a6f01f105040b5106aeb829fa5fb0561d2dcaf71485021137", size = 11882221 },
{ url = "https://files.pythonhosted.org/packages/7f/c6/3e14e09be29587393d188454064a4aa85174910d16644051a80444e4fd88/ruff-0.9.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:646909a1e25e0dc28fbc529eab8eb7bb583079628e8cbe738192853dbbe43af5", size = 11420460 }, { url = "https://files.pythonhosted.org/packages/79/d1/757559995c8ba5f14dfec4459ef2dd3fcea82ac43bc4e7c7bf47484180c0/ruff-0.9.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de9edf2ce4b9ddf43fd93e20ef635a900e25f622f87ed6e3047a664d0e8f810e", size = 11424862 },
{ url = "https://files.pythonhosted.org/packages/ef/42/b7ca38ffd568ae9b128a2fa76353e9a9a3c80ef19746408d4ce99217ecc1/ruff-0.9.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a5a46e09355695fbdbb30ed9889d6cf1c61b77b700a9fafc21b41f097bfbba4", size = 12605472 }, { url = "https://files.pythonhosted.org/packages/c0/96/7915a7c6877bb734caa6a2af424045baf6419f685632469643dbd8eb2958/ruff-0.9.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87c90c32357c74f11deb7fbb065126d91771b207bf9bfaaee01277ca59b574ec", size = 12626735 },
{ url = "https://files.pythonhosted.org/packages/a6/a1/3167023f23e3530fde899497ccfe239e4523854cb874458ac082992d206c/ruff-0.9.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c4bb09d2bbb394e3730d0918c00276e79b2de70ec2a5231cd4ebb51a57df9ba1", size = 13243123 }, { url = "https://files.pythonhosted.org/packages/0e/cc/dadb9b35473d7cb17c7ffe4737b4377aeec519a446ee8514123ff4a26091/ruff-0.9.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56acd6c694da3695a7461cc55775f3a409c3815ac467279dfa126061d84b314b", size = 13255976 },
{ url = "https://files.pythonhosted.org/packages/d0/b4/3c600758e320f5bf7de16858502e849f4216cb0151f819fa0d1154874802/ruff-0.9.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:96a87ec31dc1044d8c2da2ebbed1c456d9b561e7d087734336518181b26b3aa5", size = 12744650 }, { url = "https://files.pythonhosted.org/packages/5f/c3/ad2dd59d3cabbc12df308cced780f9c14367f0321e7800ca0fe52849da4c/ruff-0.9.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0c93e7d47ed951b9394cf352d6695b31498e68fd5782d6cbc282425655f687a", size = 12752262 },
{ url = "https://files.pythonhosted.org/packages/be/38/266fbcbb3d0088862c9bafa8b1b99486691d2945a90b9a7316336a0d9a1b/ruff-0.9.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb7554aca6f842645022fe2d301c264e6925baa708b392867b7a62645304df4", size = 14458585 }, { url = "https://files.pythonhosted.org/packages/c7/17/5f1971e54bd71604da6788efd84d66d789362b1105e17e5ccc53bba0289b/ruff-0.9.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d4c8772670aecf037d1bf7a07c39106574d143b26cfe5ed1787d2f31e800214", size = 14401648 },
{ url = "https://files.pythonhosted.org/packages/63/a6/47fd0e96990ee9b7a4abda62de26d291bd3f7647218d05b7d6d38af47c30/ruff-0.9.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cabc332b7075a914ecea912cd1f3d4370489c8018f2c945a30bcc934e3bc06a6", size = 12419624 }, { url = "https://files.pythonhosted.org/packages/30/24/6200b13ea611b83260501b6955b764bb320e23b2b75884c60ee7d3f0b68e/ruff-0.9.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfc5f1d7afeda8d5d37660eeca6d389b142d7f2b5a1ab659d9214ebd0e025231", size = 12414702 },
{ url = "https://files.pythonhosted.org/packages/84/5d/de0b7652e09f7dda49e1a3825a164a65f4998175b6486603c7601279baad/ruff-0.9.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:33866c3cc2a575cbd546f2cd02bdd466fed65118e4365ee538a3deffd6fcb730", size = 11843238 }, { url = "https://files.pythonhosted.org/packages/34/cb/f5d50d0c4ecdcc7670e348bd0b11878154bc4617f3fdd1e8ad5297c0d0ba/ruff-0.9.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:faa935fc00ae854d8b638c16a5f1ce881bc3f67446957dd6f2af440a5fc8526b", size = 11859608 },
{ url = "https://files.pythonhosted.org/packages/9e/be/3f341ceb1c62b565ec1fb6fd2139cc40b60ae6eff4b6fb8f94b1bb37c7a9/ruff-0.9.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:006e5de2621304c8810bcd2ee101587712fa93b4f955ed0985907a36c427e0c2", size = 11484012 }, { url = "https://files.pythonhosted.org/packages/d6/f4/9c8499ae8426da48363bbb78d081b817b0f64a9305f9b7f87eab2a8fb2c1/ruff-0.9.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a6c634fc6f5a0ceae1ab3e13c58183978185d131a29c425e4eaa9f40afe1e6d6", size = 11485702 },
{ url = "https://files.pythonhosted.org/packages/a3/c8/ff8acbd33addc7e797e702cf00bfde352ab469723720c5607b964491d5cf/ruff-0.9.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ba6eea4459dbd6b1be4e6bfc766079fb9b8dd2e5a35aff6baee4d9b1514ea519", size = 12038494 }, { url = "https://files.pythonhosted.org/packages/18/59/30490e483e804ccaa8147dd78c52e44ff96e1c30b5a95d69a63163cdb15b/ruff-0.9.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:433dedf6ddfdec7f1ac7575ec1eb9844fa60c4c8c2f8887a070672b8d353d34c", size = 12067782 },
{ url = "https://files.pythonhosted.org/packages/73/b1/8d9a2c0efbbabe848b55f877bc10c5001a37ab10aca13c711431673414e5/ruff-0.9.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:90230a6b8055ad47d3325e9ee8f8a9ae7e273078a66401ac66df68943ced029b", size = 12473639 }, { url = "https://files.pythonhosted.org/packages/3d/8c/893fa9551760b2f8eb2a351b603e96f15af167ceaf27e27ad873570bc04c/ruff-0.9.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d612dbd0f3a919a8cc1d12037168bfa536862066808960e0cc901404b77968f0", size = 12483087 },
{ url = "https://files.pythonhosted.org/packages/cb/44/a673647105b1ba6da9824a928634fe23186ab19f9d526d7bdf278cd27bc3/ruff-0.9.3-py3-none-win32.whl", hash = "sha256:eabe5eb2c19a42f4808c03b82bd313fc84d4e395133fb3fc1b1516170a31213c", size = 9834353 }, { url = "https://files.pythonhosted.org/packages/23/15/f6751c07c21ca10e3f4a51ea495ca975ad936d780c347d9808bcedbd7182/ruff-0.9.4-py3-none-win32.whl", hash = "sha256:db1192ddda2200671f9ef61d9597fcef89d934f5d1705e571a93a67fb13a4402", size = 9852302 },
{ url = "https://files.pythonhosted.org/packages/c3/01/65cadb59bf8d4fbe33d1a750103e6883d9ef302f60c28b73b773092fbde5/ruff-0.9.3-py3-none-win_amd64.whl", hash = "sha256:040ceb7f20791dfa0e78b4230ee9dce23da3b64dd5848e40e3bf3ab76468dcf4", size = 10821444 }, { url = "https://files.pythonhosted.org/packages/12/41/2d2d2c6a72e62566f730e49254f602dfed23019c33b5b21ea8f8917315a1/ruff-0.9.4-py3-none-win_amd64.whl", hash = "sha256:05bebf4cdbe3ef75430d26c375773978950bbf4ee3c95ccb5448940dc092408e", size = 10850051 },
{ url = "https://files.pythonhosted.org/packages/69/cb/b3fe58a136a27d981911cba2f18e4b29f15010623b79f0f2510fd0d31fd3/ruff-0.9.3-py3-none-win_arm64.whl", hash = "sha256:800d773f6d4d33b0a3c60e2c6ae8f4c202ea2de056365acfa519aa48acf28e0b", size = 10038168 }, { url = "https://files.pythonhosted.org/packages/c6/e6/3d6ec3bc3d254e7f005c543a661a41c3e788976d0e52a1ada195bd664344/ruff-0.9.4-py3-none-win_arm64.whl", hash = "sha256:585792f1e81509e38ac5123492f8875fbc36f3ede8185af0a26df348e5154f41", size = 10078251 },
] ]
[[package]] [[package]]