fix: split tag string at semicolon instead of comma (#532)

This commit is contained in:
Robert Kaussow 2024-01-24 12:50:39 +01:00 committed by GitHub
parent aa3a82ae08
commit 1c3e4fc7e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 235 additions and 200 deletions

View File

@ -111,6 +111,13 @@ class Discovery:
filtered = [] filtered = []
for item in pve_list: for item in pve_list:
obj = defaultdict(dict, item) obj = defaultdict(dict, item)
tags = []
tags_excl = self.config.config["exclude_tags"]
if isinstance(obj["tags"], str):
tags = obj["tags"].split(";")
self.logger.debug(f"vmid {obj['vmid']}: discovered tags: {tags}")
if ( if (
len(self.config.config["include_vmid"]) > 0 len(self.config.config["include_vmid"]) > 0
and str(obj["vmid"]) not in self.config.config["include_vmid"] and str(obj["vmid"]) not in self.config.config["include_vmid"]
@ -119,7 +126,7 @@ class Discovery:
if len(self.config.config["include_tags"]) > 0 and ( if len(self.config.config["include_tags"]) > 0 and (
bool(obj["tags"]) is False # continue if tags is not set bool(obj["tags"]) is False # continue if tags is not set
or set(obj["tags"].split(",")).isdisjoint(self.config.config["include_tags"]) or set(tags).isdisjoint(self.config.config["include_tags"])
): ):
continue continue
@ -132,9 +139,11 @@ class Discovery:
if str(obj["vmid"]) in self.config.config["exclude_vmid"]: if str(obj["vmid"]) in self.config.config["exclude_vmid"]:
continue continue
if isinstance(obj["tags"], str) and not set(obj["tags"].split(",")).isdisjoint( if isinstance(obj["tags"], str) and not set(tags).isdisjoint(tags_excl):
self.config.config["exclude_tags"] self.logger.debug(
): f"vmid {obj['vmid']}: "
f"excluded by tags: {list(set(tags).intersection(tags_excl))}"
)
continue continue
filtered.append(item.copy()) filtered.append(item.copy())

View File

@ -3,8 +3,7 @@
import environs import environs
import pytest import pytest
from prometheuspvesd.model import Host from prometheuspvesd.model import Host, HostList
from prometheuspvesd.model import HostList
@pytest.fixture @pytest.fixture
@ -13,131 +12,118 @@ def builtins():
"metrics.enabled": { "metrics.enabled": {
"default": True, "default": True,
"env": "METRICS_ENABLED", "env": "METRICS_ENABLED",
"type": environs.Env().bool "type": environs.Env().bool,
}, },
"metrics.address": { "metrics.address": {
"default": "127.0.0.1", "default": "127.0.0.1",
"env": "METRICS_ADDRESS", "env": "METRICS_ADDRESS",
"type": environs.Env().str "type": environs.Env().str,
},
"metrics.port": {
"default": 8000,
"env": "METRICS_PORT",
"type": environs.Env().int
},
"config_file": {
"default": "",
"env": "CONFIG_FILE",
"type": environs.Env().str
}, },
"metrics.port": {"default": 8000, "env": "METRICS_PORT", "type": environs.Env().int},
"config_file": {"default": "", "env": "CONFIG_FILE", "type": environs.Env().str},
"logging.level": { "logging.level": {
"default": "WARNING", "default": "WARNING",
"env": "LOG_LEVEL", "env": "LOG_LEVEL",
"file": True, "file": True,
"type": environs.Env().str "type": environs.Env().str,
}, },
"logging.format": { "logging.format": {
"default": "console", "default": "console",
"env": "LOG_FORMAT", "env": "LOG_FORMAT",
"file": True, "file": True,
"type": environs.Env().str "type": environs.Env().str,
}, },
"output_file": { "output_file": {
"default": "dummy", "default": "dummy",
"env": "OUTPUT_FILE", "env": "OUTPUT_FILE",
"file": True, "file": True,
"type": environs.Env().str "type": environs.Env().str,
}, },
"output_file_mode": { "output_file_mode": {
"default": "0640", "default": "0640",
"env": "OUTPUT_FILE_MODE", "env": "OUTPUT_FILE_MODE",
"file": True, "file": True,
"type": environs.Env().str "type": environs.Env().str,
}, },
"loop_delay": { "loop_delay": {
"default": 300, "default": 300,
"env": "LOOP_DELAY", "env": "LOOP_DELAY",
"file": True, "file": True,
"type": environs.Env().int "type": environs.Env().int,
},
"service": {
"default": False,
"env": "SERVICE",
"file": True,
"type": environs.Env().bool
}, },
"service": {"default": False, "env": "SERVICE", "file": True, "type": environs.Env().bool},
"exclude_state": { "exclude_state": {
"default": [], "default": [],
"env": "EXCLUDE_STATE", "env": "EXCLUDE_STATE",
"file": True, "file": True,
"type": environs.Env().list "type": environs.Env().list,
}, },
"exclude_vmid": { "exclude_vmid": {
"default": [], "default": [],
"env": "EXCLUDE_VMID", "env": "EXCLUDE_VMID",
"file": True, "file": True,
"type": environs.Env().list "type": environs.Env().list,
}, },
"exclude_tags": { "exclude_tags": {
"default": [], "default": [],
"env": "EXCLUDE_TAGS", "env": "EXCLUDE_TAGS",
"file": True, "file": True,
"type": environs.Env().list "type": environs.Env().list,
}, },
"include_vmid": { "include_vmid": {
"default": [], "default": [],
"env": "INCLUDE_VMID", "env": "INCLUDE_VMID",
"file": True, "file": True,
"type": environs.Env().list "type": environs.Env().list,
}, },
"include_tags": { "include_tags": {
"default": [], "default": [],
"env": "INCLUDE_TAGS", "env": "INCLUDE_TAGS",
"file": True, "file": True,
"type": environs.Env().list "type": environs.Env().list,
}, },
"pve.server": { "pve.server": {
"default": "dummyserver", "default": "dummyserver",
"env": "PVE_SERVER", "env": "PVE_SERVER",
"file": True, "file": True,
"type": environs.Env().str "type": environs.Env().str,
}, },
"pve.user": { "pve.user": {
"default": "dummyuser", "default": "dummyuser",
"env": "PVE_USER", "env": "PVE_USER",
"file": True, "file": True,
"type": environs.Env().str "type": environs.Env().str,
}, },
"pve.password": { "pve.password": {
"default": "dummypass", "default": "dummypass",
"env": "PVE_PASSWORD", "env": "PVE_PASSWORD",
"file": True, "file": True,
"type": environs.Env().str "type": environs.Env().str,
}, },
"pve.token_name": { "pve.token_name": {
"default": "dummyname", "default": "dummyname",
"env": "PVE_TOKEN_NAME", "env": "PVE_TOKEN_NAME",
"file": True, "file": True,
"type": environs.Env().str "type": environs.Env().str,
}, },
"pve.token_value": { "pve.token_value": {
"default": "dummyvalue", "default": "dummyvalue",
"env": "PVE_TOKEN_VALUE", "env": "PVE_TOKEN_VALUE",
"file": True, "file": True,
"type": environs.Env().str "type": environs.Env().str,
}, },
"pve.auth_timeout": { "pve.auth_timeout": {
"default": 5, "default": 5,
"env": "PVE_AUTH_TIMEOUT", "env": "PVE_AUTH_TIMEOUT",
"file": True, "file": True,
"type": environs.Env().int "type": environs.Env().int,
}, },
"pve.verify_ssl": { "pve.verify_ssl": {
"default": True, "default": True,
"env": "PVE_VERIFY_SSL", "env": "PVE_VERIFY_SSL",
"file": True, "file": True,
"type": environs.Env().bool "type": environs.Env().bool,
} },
} }
@ -149,16 +135,9 @@ def defaults():
"exclude_vmid": [], "exclude_vmid": [],
"include_tags": [], "include_tags": [],
"include_vmid": [], "include_vmid": [],
"logging": { "logging": {"format": "console", "level": "WARNING"},
"format": "console",
"level": "WARNING"
},
"loop_delay": 300, "loop_delay": 300,
"metrics": { "metrics": {"address": "127.0.0.1", "enabled": True, "port": 8000},
"address": "127.0.0.1",
"enabled": True,
"port": 8000
},
"output_file": "dummy", "output_file": "dummy",
"output_file_mode": "0640", "output_file_mode": "0640",
"pve": { "pve": {
@ -168,7 +147,7 @@ def defaults():
"user": "", "user": "",
"token_name": "", "token_name": "",
"token_value": "", "token_value": "",
"verify_ssl": True "verify_ssl": True,
}, },
"service": True, "service": True,
} }
@ -176,7 +155,8 @@ def defaults():
@pytest.fixture @pytest.fixture
def nodes(): def nodes():
return [{ return [
{
"level": "", "level": "",
"id": "node/example-node", "id": "node/example-node",
"disk": 4783488, "disk": 4783488,
@ -188,8 +168,9 @@ def nodes():
"type": "node", "type": "node",
"status": "online", "status": "online",
"maxdisk": 504209920, "maxdisk": 504209920,
"uptime": 200 "uptime": 200,
}] }
]
@pytest.fixture @pytest.fixture
@ -212,7 +193,7 @@ def qemus():
"status": "running", "status": "running",
"netout": 12159205236, "netout": 12159205236,
"mem": 496179157, "mem": 496179157,
"tags": "unmonitored,excluded,postgres" "tags": "unmonitored;excluded;postgres",
}, },
{ {
"diskwrite": 0, "diskwrite": 0,
@ -230,7 +211,7 @@ def qemus():
"disk": 0, "disk": 0,
"status": "running", "status": "running",
"netout": 12159205236, "netout": 12159205236,
"mem": 496179157 "mem": 496179157,
}, },
{ {
"diskwrite": 0, "diskwrite": 0,
@ -249,7 +230,7 @@ def qemus():
"status": "prelaunch", "status": "prelaunch",
"netout": 12159205236, "netout": 12159205236,
"mem": 496179157, "mem": 496179157,
"tags": "monitored" "tags": "monitored",
}, },
] ]
@ -261,19 +242,17 @@ def instance_config():
"description": '{"groups": "test-group"}', "description": '{"groups": "test-group"}',
"net0": "virtio=D8-85-75-47-2E-8D,bridge=vmbr122,ip=192.0.2.25,ip=2001:db8::666:77:8888", "net0": "virtio=D8-85-75-47-2E-8D,bridge=vmbr122,ip=192.0.2.25,ip=2001:db8::666:77:8888",
"cpu": 2, "cpu": 2,
"cores": 2 "cores": 2,
} }
@pytest.fixture @pytest.fixture
def agent_info(): def agent_info():
return { return {
"supported_commands": [{ "supported_commands": [
"name": "guest-network-get-interfaces", {"name": "guest-network-get-interfaces", "enabled": True, "success-response": True}
"enabled": True, ],
"success-response": True "version": "5.2.0",
}],
"version": "5.2.0"
} }
@ -310,16 +289,8 @@ def networks():
{ {
"hardware-address": "00:00:00:00:00:00", "hardware-address": "00:00:00:00:00:00",
"ip-addresses": [ "ip-addresses": [
{ {"ip-address": "127.0.0.1", "ip-address-type": "ipv4", "prefix": 8},
"ip-address": "127.0.0.1", {"ip-address": "::1", "ip-address-type": "ipv6", "prefix": 128},
"ip-address-type": "ipv4",
"prefix": 8
},
{
"ip-address": "::1",
"ip-address-type": "ipv6",
"prefix": 128
},
], ],
"name": "lo", "name": "lo",
"statistics": { "statistics": {
@ -330,26 +301,18 @@ def networks():
"tx-bytes": 9280, "tx-bytes": 9280,
"tx-dropped": 0, "tx-dropped": 0,
"tx-errs": 0, "tx-errs": 0,
"tx-packets": 92 "tx-packets": 92,
} },
}, },
{ {
"hardware-address": "92:0b:bd:c1:f8:39", "hardware-address": "92:0b:bd:c1:f8:39",
"ip-addresses": [ "ip-addresses": [
{ {"ip-address": "192.0.2.1", "ip-address-type": "ipv4", "prefix": 32},
"ip-address": "192.0.2.1", {"ip-address": "192.0.2.4", "ip-address-type": "ipv4", "prefix": 32},
"ip-address-type": "ipv4",
"prefix": 32
},
{
"ip-address": "192.0.2.4",
"ip-address-type": "ipv4",
"prefix": 32
},
{ {
"ip-address": "2001:db8:3333:4444:5555:6666:7777:8888", "ip-address": "2001:db8:3333:4444:5555:6666:7777:8888",
"ip-address-type": "ipv6", "ip-address-type": "ipv6",
"prefix": 64 "prefix": 64,
}, },
], ],
"name": "eth0", "name": "eth0",
@ -361,13 +324,10 @@ def networks():
"tx-bytes": 12185866619, "tx-bytes": 12185866619,
"tx-dropped": 0, "tx-dropped": 0,
"tx-errs": 0, "tx-errs": 0,
"tx-packets": 14423878 "tx-packets": 14423878,
}
}, },
{
"hardware-address": "ba:97:85:bd:9a:a5",
"name": "eth1"
}, },
{"hardware-address": "ba:97:85:bd:9a:a5", "name": "eth1"},
] ]
@ -383,31 +343,35 @@ def inventory():
@pytest.fixture @pytest.fixture
def labels(): def labels():
return [{ return [
{
"targets": ["100.example.com"], "targets": ["100.example.com"],
"labels": { "labels": {
"__meta_pve_ipv4": "192.0.2.1", "__meta_pve_ipv4": "192.0.2.1",
"__meta_pve_ipv6": "False", "__meta_pve_ipv6": "False",
"__meta_pve_name": "100.example.com", "__meta_pve_name": "100.example.com",
"__meta_pve_type": "qemu", "__meta_pve_type": "qemu",
"__meta_pve_vmid": "100" "__meta_pve_vmid": "100",
} },
}, { },
{
"targets": ["101.example.com"], "targets": ["101.example.com"],
"labels": { "labels": {
"__meta_pve_ipv4": "192.0.2.2", "__meta_pve_ipv4": "192.0.2.2",
"__meta_pve_ipv6": "False", "__meta_pve_ipv6": "False",
"__meta_pve_name": "101.example.com", "__meta_pve_name": "101.example.com",
"__meta_pve_type": "qemu", "__meta_pve_type": "qemu",
"__meta_pve_vmid": "101" "__meta_pve_vmid": "101",
} },
}, { },
{
"targets": ["102.example.com"], "targets": ["102.example.com"],
"labels": { "labels": {
"__meta_pve_ipv4": "192.0.2.3", "__meta_pve_ipv4": "192.0.2.3",
"__meta_pve_ipv6": "False", "__meta_pve_ipv6": "False",
"__meta_pve_name": "102.example.com", "__meta_pve_name": "102.example.com",
"__meta_pve_type": "qemu", "__meta_pve_type": "qemu",
"__meta_pve_vmid": "102" "__meta_pve_vmid": "102",
} },
}] },
]

View File

@ -1,8 +1,11 @@
"""Pytest conftest fixtures.""" """Pytest conftest fixtures."""
import logging
import os import os
import sys import sys
from contextlib import contextmanager
import pytest import pytest
from _pytest.logging import LogCaptureHandler
from prometheuspvesd.utils import Singleton from prometheuspvesd.utils import Singleton
@ -14,9 +17,43 @@ def reset_singletons():
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def reset_os_environment(): def reset_os_environment():
os.environ = {} os.environ.clear()
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def reset_sys_argv(): def reset_sys_argv():
sys.argv = ["prometheus-pve-sd"] sys.argv = ["prometheus-pve-sd"]
@contextmanager
def local_caplog_fn(level=logging.INFO, name="prometheuspvesd"):
"""
Context manager that captures records from non-propagating loggers.
After the end of the 'with' statement, the log level is restored to its original
value. Code adapted from https://github.com/pytest-dev/pytest/issues/3697#issuecomment-790925527.
:param int level: The level.
:param logging.Logger logger: The logger to update.
"""
logger = logging.getLogger(name)
old_level = logger.level
logger.setLevel(level)
handler = LogCaptureHandler()
logger.addHandler(handler)
try:
yield handler
finally:
logger.setLevel(old_level)
logger.removeHandler(handler)
@pytest.fixture
def local_caplog():
"""Fixture that yields a context manager for capturing records from non-propagating loggers."""
yield local_caplog_fn

View File

@ -30,22 +30,22 @@ def test_cli_required_error(mocker, capsys):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"testinput", [{ "testinput",
"pve.user": "dummy", [
"pve.password": "", {"pve.user": "dummy", "pve.password": "", "pve.token_name": "", "pve.token_value": ""},
"pve.token_name": "", {
"pve.token_value": ""
}, {
"pve.user": "dummy", "pve.user": "dummy",
"pve.password": "", "pve.password": "",
"pve.token_name": "dummy", "pve.token_name": "dummy",
"pve.token_value": "" "pve.token_value": "",
}, { },
{
"pve.user": "dummy", "pve.user": "dummy",
"pve.password": "", "pve.password": "",
"pve.token_name": "", "pve.token_name": "",
"pve.token_value": "dummy" "pve.token_value": "dummy",
}] },
],
) )
def test_cli_auth_required_error(mocker, capsys, builtins, testinput): def test_cli_auth_required_error(mocker, capsys, builtins, testinput):
for key, value in testinput.items(): for key, value in testinput.items():
@ -59,22 +59,21 @@ def test_cli_auth_required_error(mocker, capsys, builtins, testinput):
PrometheusSD() PrometheusSD()
stdout, stderr = capsys.readouterr() stdout, stderr = capsys.readouterr()
assert "Either 'pve.password' or 'pve.token_name' and 'pve.token_value' are required but not set" in stderr assert (
"Either 'pve.password' or 'pve.token_name' and 'pve.token_value' are required but not set"
in stderr
)
assert e.value.code == 1 assert e.value.code == 1
@pytest.mark.parametrize( @pytest.mark.parametrize(
"testinput", [{ "testinput",
"pve.password": "dummy", [
"pve.token_name": "", {"pve.password": "dummy", "pve.token_name": "", "pve.token_value": ""},
"pve.token_value": "" {"pve.password": "", "pve.token_name": "dummy", "pve.token_value": "dummy"},
}, { ],
"pve.password": "",
"pve.token_name": "dummy",
"pve.token_value": "dummy"
}]
) )
def test_cli_auth_no_error(mocker, capsys, builtins, testinput): def test_cli_auth_no_error(mocker, builtins, testinput):
for key, value in testinput.items(): for key, value in testinput.items():
builtins[key]["default"] = value builtins[key]["default"] = value
@ -91,7 +90,7 @@ def test_cli_auth_no_error(mocker, capsys, builtins, testinput):
def test_cli_config_error(mocker, capsys): def test_cli_config_error(mocker, capsys):
mocker.patch( mocker.patch(
"prometheuspvesd.config.SingleConfig.__init__", "prometheuspvesd.config.SingleConfig.__init__",
side_effect=prometheuspvesd.exception.ConfigError("Dummy Config Exception") side_effect=prometheuspvesd.exception.ConfigError("Dummy Config Exception"),
) )
mocker.patch.object(ProxmoxClient, "_auth", return_value=mocker.create_autospec(ProxmoxAPI)) mocker.patch.object(ProxmoxClient, "_auth", return_value=mocker.create_autospec(ProxmoxAPI))
mocker.patch.object(PrometheusSD, "_fetch", return_value=True) mocker.patch.object(PrometheusSD, "_fetch", return_value=True)

View File

@ -25,7 +25,7 @@ def test_yaml_config(mocker, defaults):
assert config.config == defaults assert config.config == defaults
def test_yaml_config_error(mocker, capsys): def test_yaml_config_error(mocker):
mocker.patch( mocker.patch(
"prometheuspvesd.config.default_config_file", "./prometheuspvesd/test/data/config.yml" "prometheuspvesd.config.default_config_file", "./prometheuspvesd/test/data/config.yml"
) )

View File

@ -1,5 +1,7 @@
"""Test Discovery class.""" """Test Discovery class."""
import logging
import pytest import pytest
from proxmoxer import ProxmoxAPI from proxmoxer import ProxmoxAPI
@ -11,6 +13,10 @@ pytest_plugins = [
] ]
def records_to_messages(records):
return [r.getMessage() for r in records]
@pytest.fixture @pytest.fixture
def discovery(mocker): def discovery(mocker):
mocker.patch.object(ProxmoxClient, "_auth", return_value=mocker.create_autospec(ProxmoxAPI)) mocker.patch.object(ProxmoxClient, "_auth", return_value=mocker.create_autospec(ProxmoxAPI))
@ -32,19 +38,27 @@ def test_exclude_state(discovery, qemus):
assert len(filtered) == 2 assert len(filtered) == 2
def test_exclude_tags(discovery, qemus): def test_exclude_tags(discovery, qemus, local_caplog):
discovery.config.config["exclude_tags"] = ["unmonitored"] discovery.config.config["exclude_tags"] = ["unmonitored"]
with local_caplog(level=logging.DEBUG) as caplog:
filtered = discovery._filter(qemus) filtered = discovery._filter(qemus)
assert (
"vmid 100: discovered tags: ['unmonitored', 'excluded', 'postgres']"
in records_to_messages(caplog.records)
)
assert "vmid 100: excluded by tags: ['unmonitored']"
assert len(filtered) == 2 assert len(filtered) == 2
@pytest.mark.parametrize( @pytest.mark.parametrize(
"testinput,expected", [ "testinput,expected",
[
(["monitored"], 1), (["monitored"], 1),
(["monitored", "postgres"], 2), (["monitored", "postgres"], 2),
([], 3), ([], 3),
] ],
) )
def test_include_tags(discovery, qemus, testinput, expected): def test_include_tags(discovery, qemus, testinput, expected):
discovery.config.config["include_tags"] = testinput discovery.config.config["include_tags"] = testinput
@ -53,10 +67,13 @@ def test_include_tags(discovery, qemus, testinput, expected):
assert len(filtered) == expected assert len(filtered) == expected
@pytest.mark.parametrize("testinput,expected", [ @pytest.mark.parametrize(
"testinput,expected",
[
(["101", "100"], 2), (["101", "100"], 2),
([], 3), ([], 3),
]) ],
)
def test_include_vmid(discovery, qemus, testinput, expected): def test_include_vmid(discovery, qemus, testinput, expected):
discovery.config.config["include_vmid"] = testinput discovery.config.config["include_vmid"] = testinput
filtered = discovery._filter(qemus) filtered = discovery._filter(qemus)

View File

@ -9,34 +9,41 @@ pytest_plugins = [
@pytest.mark.parametrize( @pytest.mark.parametrize(
"testinput,expected", [ "testinput,expected",
({ [
(
{
"vmid": 101, "vmid": 101,
"hostname": "host1", "hostname": "host1",
"ipv4_address": False, "ipv4_address": False,
"ipv6_address": False, "ipv6_address": False,
"pve_type": "qemu", "pve_type": "qemu",
}, { },
{
"__meta_pve_vmid": "101", "__meta_pve_vmid": "101",
"__meta_pve_name": "host1", "__meta_pve_name": "host1",
"__meta_pve_ipv4": "False", "__meta_pve_ipv4": "False",
"__meta_pve_ipv6": "False", "__meta_pve_ipv6": "False",
"__meta_pve_type": "qemu", "__meta_pve_type": "qemu",
}), },
({ ),
(
{
"vmid": "202", "vmid": "202",
"hostname": "host2", "hostname": "host2",
"ipv4_address": "129.168.0.1", "ipv4_address": "129.168.0.1",
"ipv6_address": "2001:db8:3333:4444:5555:6666:7777:8888", "ipv6_address": "2001:db8:3333:4444:5555:6666:7777:8888",
"pve_type": "qemu", "pve_type": "qemu",
}, { },
{
"__meta_pve_vmid": "202", "__meta_pve_vmid": "202",
"__meta_pve_name": "host2", "__meta_pve_name": "host2",
"__meta_pve_ipv4": "129.168.0.1", "__meta_pve_ipv4": "129.168.0.1",
"__meta_pve_ipv6": "2001:db8:3333:4444:5555:6666:7777:8888", "__meta_pve_ipv6": "2001:db8:3333:4444:5555:6666:7777:8888",
"__meta_pve_type": "qemu", "__meta_pve_type": "qemu",
}), },
] ),
],
) )
def test_host(testinput, expected): def test_host(testinput, expected):
host = Host( host = Host(

View File

@ -78,7 +78,6 @@ exclude = [
"__pycache__", "__pycache__",
"build", "build",
"dist", "dist",
"test",
"*.pyc", "*.pyc",
"*.egg-info", "*.egg-info",
".cache", ".cache",
@ -131,6 +130,9 @@ select = [
"RUF", "RUF",
] ]
[tool.ruff.per-file-ignores]
"test_*.py" = ["S"]
[tool.ruff.format] [tool.ruff.format]
quote-style = "double" quote-style = "double"
indent-style = "space" indent-style = "space"