From 484b3158e9626af04532db9836455952113738b8 Mon Sep 17 00:00:00 2001 From: Ronan Abhamon Date: Mon, 16 Mar 2026 19:28:27 +0100 Subject: [PATCH 01/11] feat(core): add setup.py and empty project Signed-off-by: Ronan Abhamon --- setup.cfg | 18 ++++++++++++++++++ setup.py | 21 +++++++++++++++++++++ src/xcp_storage/__init__.py | 0 3 files changed, 39 insertions(+) create mode 100644 setup.cfg create mode 100755 setup.py create mode 100644 src/xcp_storage/__init__.py diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..883d452 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,18 @@ +[metadata] +name = xcp-storage +version = 1.0.0 +description = XCP storage layer, scripts and drivers +author = Ronan Abhamon +author_email = ronan.abhamon@vates.tech +url = https://vates.tech +license = GPLv3 + +[options] +packages = find: +package_dir = + =src +# TODO(XCPNG-3032): Change it for modern python. +python_requires = >=3.6 + +[options.packages.find] +where = src diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..8a64343 --- /dev/null +++ b/setup.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2026 Vates SAS +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License + +from setuptools import setup + +# ============================================================================== + +if __name__ == "__main__": + setup() diff --git a/src/xcp_storage/__init__.py b/src/xcp_storage/__init__.py new file mode 100644 index 0000000..e69de29 From ab7af1226d6687c8218ce6ca82e732d0ffa5b4b4 Mon Sep 17 00:00:00 2001 From: Ronan Abhamon Date: Mon, 16 Mar 2026 19:35:02 +0100 Subject: [PATCH 02/11] feat(core): add typing module to support python 3.6 Signed-off-by: Ronan Abhamon --- src/xcp_storage/typing/__init__.py | 51 ++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/xcp_storage/typing/__init__.py diff --git a/src/xcp_storage/typing/__init__.py b/src/xcp_storage/typing/__init__.py new file mode 100644 index 0000000..ae961f4 --- /dev/null +++ b/src/xcp_storage/typing/__init__.py @@ -0,0 +1,51 @@ +# Copyright (C) 2026 Vates SAS +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import contextlib +import typing +from typing import * # noqa: F403 + +# ============================================================================== + +# TODO(XCPNG-3032): Remove all hacks for python 3.11. + +if not hasattr(typing, "override"): + + def override(method): # type: ignore # noqa: ANN001, ANN201 + with contextlib.suppress(AttributeError, TypeError): + # Set internal attr `__override__` like described in PEP 698. + method.__override__ = True + return method + + +if not hasattr(typing, "Never"): + Never = None # type: ignore + +if not hasattr(typing, "Literal"): + from typing_extensions import Literal # noqa: F401, UP035 + +if not hasattr(typing, "ParamSpec"): + + class _SubscriptableListMock(list): + def __getitem__(self, _): # type: ignore # noqa: ANN001, ANN204 + return self + + def __getattr__(self, _): # type: ignore # noqa: ANN001, ANN204 + return self + + def __call__(self, *_args, **_kwargs): # type: ignore # noqa: ANN002, ANN003, ANN204 + return self + + ParamSpec = _SubscriptableListMock() # type: ignore + Concatenate = _SubscriptableListMock() # type: ignore From ce1b2bc802116c5357a1f108dc5431343ea347fd Mon Sep 17 00:00:00 2001 From: Ronan Abhamon Date: Mon, 16 Mar 2026 19:42:33 +0100 Subject: [PATCH 03/11] chore: add initial pyproject.toml Signed-off-by: Ronan Abhamon --- pyproject.toml | 100 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e3bca98 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,100 @@ +[tool.mypy] +mypy_path = "stubs" +files = [ + "src" +] + +disallow_untyped_defs = true +enable_error_code = ["explicit-override"] +ignore_missing_imports = false +implicit_optional = false + +[[tool.mypy.overrides]] +module = "setuptools.*" +ignore_missing_imports = true + +[tool.ruff] +include = [ + "src/**/*.py" +] +line-length = 120 + +[tool.ruff.lint] +exclude = ["stubs/*"] +ignore = [ + "B019", # flake8-bugbear: cached-instance-method + "EM101", # flake8-errmsg: raw-string-in-exception + "EM102", # flake8-errmsg: f-string-in-exception + "S101", # flake8-bandit: assert + "S603", # flake8-bandit: subprocess-without-shell-equals-true + "TD003", # flake8-todos: missing-todo-link + "UP006", # pyupgrade: non-pep585-annotation + "UP007", # pyupgrade: non-pep604-annotation-union + "UP045", # pyupgrade: non-pep604-annotation-optional +] +select = [ + "A", # flake8-builtins + "ANN", # flake8-annotations + "ARG", # flake8-unused-arguments + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "C90", # mccabe + "DTZ", # flake8-datetimez + "E", # pycodestyle: error + "EM", # flake8-errmsg + "ERA", # eradicate + "EXE", # flake8-executable + "F", # Pyflakes + "FBT", # flake8-boolean-trap + "FLY", # flynt + "FURB", # refurb + "G", # flake8-logging-format + "I", # isort + "ICN", # flake8-import-conventions + "INT", # flake8-gettext + "LOG", # flake8-logging + "N", # pep8-naming + "PERF", # perflint + "PIE", # flake8-pie + "PLW", # pylint: warning + "PTH", # flake8-use-pathlib + "Q", # flake8-quotes + "RET", # flake8-return + "RUF013", # ruff: implicit-optional + "RUF059", # ruff: unused-unpacked-variable + "S", # flake8-bandit + "SIM", # flake8-simplify + "SLF", # flake8-self + "T100", # flake8-debugger + "T20", # flake8-print + "TD", # flake8-todos + "TID", # flake8-tidy-imports + "UP", # pyupgrade + "W", # pycodestyle: warning +] +typing-modules = ["xcp_storage.typing"] + +[tool.ruff.lint.isort.sections] +"typing" = ["xcp_storage.typing"] + +[tool.ruff.lint.isort] +case-sensitive = false +combine-as-imports = true +force-sort-within-sections = true +known-first-party = ["xcp_storage"] +lines-after-imports = 1 +order-by-type = false +section-order = [ + "future", + "standard-library", + "third-party", + "first-party", + "local-folder", + "typing" +] + +[tool.ruff.lint.flake8-tidy-imports] +ban-relative-imports = "all" + +[tool.pytest.ini_options] +pythonpath = ["src", "stubs"] From c2450134443a9f7a28ec5b7a85e5618a3339ac2e Mon Sep 17 00:00:00 2001 From: Ronan Abhamon Date: Mon, 16 Mar 2026 19:44:07 +0100 Subject: [PATCH 04/11] chore: add .gitignore Signed-off-by: Ronan Abhamon --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d82b9f --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__ +.mypy_cache +.pytest_cache +.ruff_cache +*.egg-info +*.pyc From b3782b501a41285f054ba05a0d9f9f15cf60ec0d Mon Sep 17 00:00:00 2001 From: Ronan Abhamon Date: Mon, 16 Mar 2026 19:52:14 +0100 Subject: [PATCH 05/11] chore: add LINSTOR stubs Signed-off-by: Ronan Abhamon --- pyproject.toml | 18 + stubs/linstor/__init__.pyi | 10 + stubs/linstor/config.pyi | 11 + stubs/linstor/consts_githash.pyi | 1 + stubs/linstor/errors.pyi | 29 + stubs/linstor/kv.pyi | 43 ++ stubs/linstor/linstorapi.pyi | 315 +++++++++ stubs/linstor/properties.pyi | 3 + stubs/linstor/resource.pyi | 132 ++++ stubs/linstor/resourcegroup.pyi | 87 +++ stubs/linstor/responses.pyi | 1017 ++++++++++++++++++++++++++++++ stubs/linstor/sharedconsts.pyi | 720 +++++++++++++++++++++ stubs/linstor/size_calc.pyi | 38 ++ stubs/linstor/version.pyi | 1 + 14 files changed, 2425 insertions(+) create mode 100644 stubs/linstor/__init__.pyi create mode 100644 stubs/linstor/config.pyi create mode 100644 stubs/linstor/consts_githash.pyi create mode 100644 stubs/linstor/errors.pyi create mode 100644 stubs/linstor/kv.pyi create mode 100644 stubs/linstor/linstorapi.pyi create mode 100644 stubs/linstor/properties.pyi create mode 100644 stubs/linstor/resource.pyi create mode 100644 stubs/linstor/resourcegroup.pyi create mode 100644 stubs/linstor/responses.pyi create mode 100644 stubs/linstor/sharedconsts.pyi create mode 100644 stubs/linstor/size_calc.pyi create mode 100644 stubs/linstor/version.pyi diff --git a/pyproject.toml b/pyproject.toml index e3bca98..a0c69a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,23 @@ implicit_optional = false module = "setuptools.*" ignore_missing_imports = true +# To generate LINSTOR stubs, run the following commands in the LINSTOR source folder: +# ./setup.py build +# stubgen --include-private build/lib/linstor/ -o /stubs/ +# echo "from . import sharedconsts as consts" >> /stubs/linstor/__init__.pyi +[[tool.mypy.overrides]] +module = "linstor.*" + +disable_error_code = [ + "arg-type", + "attr-defined", + "explicit-override", + "name-defined", + "no-redef", + "override" +] +disallow_untyped_defs = false + [tool.ruff] include = [ "src/**/*.py" @@ -82,6 +99,7 @@ case-sensitive = false combine-as-imports = true force-sort-within-sections = true known-first-party = ["xcp_storage"] +known-third-party = ["linstor"] lines-after-imports = 1 order-by-type = false section-order = [ diff --git a/stubs/linstor/__init__.pyi b/stubs/linstor/__init__.pyi new file mode 100644 index 0000000..bc47376 --- /dev/null +++ b/stubs/linstor/__init__.pyi @@ -0,0 +1,10 @@ +from .config import Config as Config +from .errors import LinstorApiCallError as LinstorApiCallError, LinstorArgumentError as LinstorArgumentError, LinstorError as LinstorError, LinstorNetworkError as LinstorNetworkError, LinstorReadOnlyAfterSetError as LinstorReadOnlyAfterSetError, LinstorTimeoutError as LinstorTimeoutError +from .kv import KV as KV +from .linstorapi import ApiCallResponse as ApiCallResponse, ErrorReport as ErrorReport, Linstor as Linstor, LogLevelEnum as LogLevelEnum, MultiLinstor as MultiLinstor, ResourceData as ResourceData +from .resource import Resource as Resource, Volume as Volume +from .resourcegroup import ResourceGroup as ResourceGroup +from .responses import StoragePoolDriver as StoragePoolDriver +from .size_calc import SizeCalc as SizeCalc +from linstor.consts_githash import GITHASH as GITHASH +from . import sharedconsts as consts diff --git a/stubs/linstor/config.pyi b/stubs/linstor/config.pyi new file mode 100644 index 0000000..48fed43 --- /dev/null +++ b/stubs/linstor/config.pyi @@ -0,0 +1,11 @@ +from _typeshed import Incomplete +from linstor.linstorapi import MultiLinstor as MultiLinstor + +class Config: + CONFIG: Incomplete + @staticmethod + def read_config(config_file): ... + @staticmethod + def get_section(section, config_file_name=None): ... + @staticmethod + def get_controllers(section: str = 'global', config_file_name=None, fallback: str = 'linstor://localhost'): ... diff --git a/stubs/linstor/consts_githash.pyi b/stubs/linstor/consts_githash.pyi new file mode 100644 index 0000000..5547b4f --- /dev/null +++ b/stubs/linstor/consts_githash.pyi @@ -0,0 +1 @@ +GITHASH: str diff --git a/stubs/linstor/errors.pyi b/stubs/linstor/errors.pyi new file mode 100644 index 0000000..8cf4e1c --- /dev/null +++ b/stubs/linstor/errors.pyi @@ -0,0 +1,29 @@ +from _typeshed import Incomplete + +class LinstorError(Exception): + _msg: Incomplete + _errors: Incomplete + def __init__(self, msg, all_errors=None) -> None: ... + def all_errors(self): ... + @property + def message(self): ... + def __str__(self) -> str: ... + def __repr__(self) -> str: ... + +class LinstorNetworkError(LinstorError): + def __init__(self, msg, more_errors=None) -> None: ... + +class LinstorTimeoutError(LinstorError): + def __init__(self, msg, more_errors=None) -> None: ... + +class LinstorApiCallError(LinstorError): + _main_error: Incomplete + def __init__(self, apicallresponse, all_errors=None) -> None: ... + @property + def main_error(self): ... + +class LinstorArgumentError(LinstorError): + def __init__(self, msg, more_errors=None) -> None: ... + +class LinstorReadOnlyAfterSetError(LinstorError): + def __init__(self, msg=None, more_errors=None) -> None: ... diff --git a/stubs/linstor/kv.pyi b/stubs/linstor/kv.pyi new file mode 100644 index 0000000..744739f --- /dev/null +++ b/stubs/linstor/kv.pyi @@ -0,0 +1,43 @@ +from _typeshed import Incomplete +from collections.abc import Generator +from linstor.resource import _Client as _Client + +unicode = str + +class KV(dict): + _name: Incomplete + _cached: Incomplete + client: Incomplete + _existing_client: Incomplete + _rw_to_linstor: Incomplete + def __init__(self, name, namespace: str = '/', cached: bool = True, uri: str = 'linstor://localhost', rw_to_linstor: bool = True, existing_client=None) -> None: ... + def _get_connection(self): ... + def _set_linstor_kv(self, k, v) -> None: ... + def _del_linstor_kv(self, k, is_list_like: bool = False) -> None: ... + @classmethod + def _valid_string(cls, s): ... + @classmethod + def _normalize_ns(cls, ns): ... + def _import(self) -> None: ... + _ns: Incomplete + def _set_ns(self, ns) -> None: ... + def _key_ns_add(self, k): ... + def _key_ns_del(self, k): ... + @property + def namespace(self): ... + @namespace.setter + def namespace(self, ns) -> None: ... + def __delitem__(self, k) -> None: ... + def __setitem__(self, k, v) -> None: ... + def __getitem__(self, k): ... + def __contains__(self, k) -> bool: ... + def __iter__(self): ... + def clear(self) -> None: ... + def get(self, k, d=None): ... + def items(self) -> Generator[Incomplete]: ... + def keys(self): ... + def pop(self, *args): ... + def popitem(self): ... + def setdefault(self, k, d=None): ... + def update(self, *args, **kwargs) -> None: ... + def values(self) -> Generator[Incomplete]: ... diff --git a/stubs/linstor/linstorapi.pyi b/stubs/linstor/linstorapi.pyi new file mode 100644 index 0000000..2a268c2 --- /dev/null +++ b/stubs/linstor/linstorapi.pyi @@ -0,0 +1,315 @@ +import types +from _typeshed import Incomplete +from enum import Enum +from http.client import HTTPConnection +from linstor import responses as responses +from linstor.errors import LinstorApiCallError as LinstorApiCallError, LinstorArgumentError as LinstorArgumentError, LinstorError as LinstorError, LinstorNetworkError as LinstorNetworkError, LinstorTimeoutError as LinstorTimeoutError +from linstor.responses import ApiCallResponse as ApiCallResponse, BackupInfoResponse as BackupInfoResponse, BackupListResponse as BackupListResponse, BackupQueues as BackupQueues, CloneStarted as CloneStarted, CloneStatus as CloneStatus, ControllerProperties as ControllerProperties, ControllerVersion as ControllerVersion, ErrorReport as ErrorReport, FileResponse as FileResponse, KeyValueStoresResponse as KeyValueStoresResponse, MaxVolumeSizeResponse as MaxVolumeSizeResponse, NodeConnection as NodeConnection, NodeConnectionsResponse as NodeConnectionsResponse, NodeListResponse as NodeListResponse, PhysicalStorageList as PhysicalStorageList, QuerySizeInfoResponse as QuerySizeInfoResponse, RemoteListResponse as RemoteListResponse, ResourceConnection as ResourceConnection, ResourceConnectionsResponse as ResourceConnectionsResponse, ResourceDefinitionResponse as ResourceDefinitionResponse, ResourceGroupResponse as ResourceGroupResponse, ResourceResponse as ResourceResponse, SnapshotResponse as SnapshotResponse, SpaceReport as SpaceReport, StoragePoolDefinitionResponse as StoragePoolDefinitionResponse, StoragePoolDriver as StoragePoolDriver, StoragePoolListResponse as StoragePoolListResponse, SyncStatus as SyncStatus, VolumeDefinitionResponse as VolumeDefinitionResponse, VolumeGroupResponse as VolumeGroupResponse, VolumeResponse as VolumeResponse +from linstor.size_calc import SizeCalc as SizeCalc +from linstor.version import VERSION as VERSION + +API_VERSION_MIN: str +API_VERSION = API_VERSION_MIN + +def _pquote(pathfmt, *args, **kwargs): ... + +class ResourceData: + _node_name: Incomplete + _rsc_name: Incomplete + _diskless: Incomplete + _storage_pool: Incomplete + _node_id: Incomplete + _layer_list: Incomplete + _drbd_diskless: Incomplete + _nvme_initiator: Incomplete + _ebs_initiator: Incomplete + _active: Incomplete + _drbd_tcp_ports: Incomplete + def __init__(self, node_name, rsc_name, diskless: bool = False, storage_pool=None, node_id=None, layer_list=None, drbd_diskless: bool = False, nvme_initiator: bool = False, ebs_initiator: bool = False, active: bool = True, drbd_tcp_ports=None) -> None: ... + @property + def node_name(self): ... + @property + def rsc_name(self): ... + @property + def diskless(self): ... + @property + def storage_pool(self): ... + @property + def node_id(self): ... + @property + def layer_list(self): ... + @property + def drbd_diskless(self): ... + @property + def nvme_initiator(self): ... + @property + def ebs_initiator(self): ... + @property + def active(self): ... + @property + def drbd_tcp_ports(self): ... + +class LogLevelEnum(Enum): + ERROR = 'ERROR' + WARN = 'WARN' + INFO = 'INFO' + DEBUG = 'DEBUG' + TRACE = 'TRACE' + def __str__(self) -> str: ... + @staticmethod + def check(value): ... + +class Linstor: + _node_types: Incomplete + API_SINGLE_NODE_REQ: str + API_SCHEDULE_BY_RESOURCE_LIST: str + API_SCHEDULE_BY_RESOURCE_LIST_DETAILS: str + API_SINGLE_NODE_CONN_REQ: str + APICALL2RESPONSE: Incomplete + REST_PORT: int + REST_HTTPS_PORT: int + _ctrl_host: Incomplete + _timeout: Incomplete + _keep_alive: Incomplete + _rest_conn: Optional[HTTPConnection] + _connected: bool + _mode_curl: bool + _ctrl_version: Incomplete + _username: Incomplete + _password: Incomplete + _certfile: Incomplete + _keyfile: Incomplete + _cafile: Incomplete + _allow_insecure: bool + _times_entered: int + _http_headers: Incomplete + def __init__(self, ctrl_host, timeout: int = 300, keep_alive: bool = False, agent_info: str = '') -> None: ... + def __del__(self) -> None: ... + def __enter__(self): ... + def __exit__(self, type: type[BaseException] | None, value: BaseException | None, traceback: types.TracebackType | None) -> None: ... + @property + def username(self): ... + @username.setter + def username(self, username) -> None: ... + @property + def password(self): ... + @password.setter + def password(self, password) -> None: ... + @property + def certfile(self): ... + @certfile.setter + def certfile(self, certfile) -> None: ... + @property + def keyfile(self): ... + @keyfile.setter + def keyfile(self, keyfile) -> None: ... + @property + def cafile(self): ... + @cafile.setter + def cafile(self, cafile) -> None: ... + @property + def allow_insecure(self): ... + @allow_insecure.setter + def allow_insecure(self, val) -> None: ... + def __output_curl_command(self, method, path, body) -> None: ... + @classmethod + def _current_milli_time(cls): ... + @classmethod + def parse_volume_size_to_kib(cls, size_str): ... + @classmethod + def _decode_response_data(cls, response): ... + def _require_version(self, required_version, msg: str = 'REST action not supported by server') -> None: ... + def api_version_smaller(self, version): ... + def _rest_request_base(self, apicall, method, path, body=None, reconnect: bool = True): ... + def _handle_response_error(self, response, method, path, raise_error: bool = False): ... + def _rest_request(self, apicall, method, path, body=None, reconnect: bool = True, raise_error: bool = False): ... + def _rest_request_raw(self, apicall, method, path, body=None, reconnect: bool = True): ... + def _rest_request_download(self, apicall, method, path, body=None, reconnect: bool = True, to_file=None): ... + def __convert_rest_response(self, apicall, response, path): ... + @property + def curl(self): ... + @curl.setter + def curl(self, enable) -> None: ... + @classmethod + def all_api_responses_no_error(cls, replies): ... + @classmethod + def all_api_responses_success(cls, replies): ... + @classmethod + def filter_api_call_response(cls, replies): ... + @classmethod + def filter_api_call_response_errors(cls, replies): ... + @classmethod + def return_if_failure(cls, replies_): ... + @classmethod + def return_if_error(cls, replies_): ... + @classmethod + def _modify_props(cls, msg, property_dict, delete_props=None): ... + @classmethod + def has_linstor_https(cls, hostname, port): ... + def connect(self): ... + @property + def connected(self): ... + @property + def is_secure_connection(self): ... + def disconnect(self) -> None: ... + def _require_node_is_active(self, net_interface, value: bool = True) -> None: ... + def node_create(self, node_name, node_type, ip, com_type=..., port=None, netif_name: str = 'default', property_dict=None): ... + def node_create_ebs(self, node_name, ebs_remote_name): ... + def node_modify(self, node_name, node_type=None, property_dict=None, delete_props=None): ... + def node_delete(self, node_name, async_msg: bool = False): ... + def node_lost(self, node_name, async_msg: bool = False): ... + def node_reconnect(self, node_names): ... + def node_restore(self, node_name, delete_resources=None, delete_snapshots=None): ... + def node_evacuate(self, node_name, target=None, do_not_target=None): ... + def netinterface_create(self, node_name, interface_name, ip, port=None, com_type=None, is_active: bool = False): ... + def netinterface_modify(self, node_name, interface_name, ip=None, port=None, com_type=None, is_active: bool = False): ... + def netinterface_delete(self, node_name, interface_name): ... + def net_interface_list(self, node_name): ... + def node_list(self, filter_by_nodes=None, filter_by_props=None): ... + def node_list_raise(self, filter_by_nodes=None, filter_by_props=None): ... + def node_types(self): ... + def storage_pool_dfn_create(self, name): ... + def storage_pool_dfn_modify(self, name, property_dict, delete_props=None): ... + def storage_pool_dfn_delete(self, name): ... + def storage_pool_dfn_list(self): ... + def storage_pool_dfn_max_vlm_sizes(self, place_count, storage_pool_name=None, do_not_place_with=None, do_not_place_with_regex=None, replicas_on_same=None, replicas_on_different=None, x_replicas_on_different=None): ... + @staticmethod + def _filter_props(props, namespace: str = ''): ... + def storage_pool_create(self, node_name, storage_pool_name, storage_driver, driver_pool_name, shared_space=None, property_dict=None, external_locking: bool = False): ... + def storage_pool_modify(self, node_name, storage_pool_name, property_dict, delete_props=None): ... + def storage_pool_delete(self, node_name, storage_pool_name): ... + def storage_pool_list(self, filter_by_nodes=None, filter_by_stor_pools=None, filter_by_props=None): ... + def storage_pool_list_raise(self, filter_by_nodes=None, filter_by_stor_pools=None, filter_by_props=None): ... + @classmethod + def layer_list(cls): ... + @classmethod + def provider_list(cls): ... + def resource_group_create(self, name, description=None, place_count=None, storage_pool=None, do_not_place_with=None, do_not_place_with_regex=None, replicas_on_same=None, replicas_on_different=None, x_replicas_on_different=None, diskless_on_remaining=None, layer_list=None, provider_list=None, property_dict=None, diskless_storage_pool=None, peer_slots=None): ... + def resource_group_modify(self, name, description=None, place_count=None, storage_pool=None, do_not_place_with=None, do_not_place_with_regex=None, replicas_on_same=None, replicas_on_different=None, x_replicas_on_different=None, diskless_on_remaining=None, layer_list=None, provider_list=None, property_dict=None, delete_props=None, diskless_storage_pool=None, peer_slots=None): ... + def resource_group_delete(self, name): ... + def resource_group_list_raise(self, filter_by_resource_groups=None, filter_by_props=None): ... + def resource_group_spawn(self, rsc_grp_name, rsc_dfn_name, vlm_sizes, partial: bool = False, definitions_only: bool = False, external_name=None, place_count=None, storage_pool=None, do_not_place_with=None, do_not_place_with_regex=None, replicas_on_same=None, replicas_on_different=None, x_replicas_on_different=None, diskless_on_remaining=None, layer_list=None, provider_list=None, diskless_storage_pool=None, peer_slots=None, volume_passphrases=None): ... + def resource_group_qmvs(self, rsc_grp_name): ... + def resource_group_query_size_info(self, rsc_grp_name, place_count=None, storage_pool=None, do_not_place_with=None, do_not_place_with_regex=None, replicas_on_same=None, replicas_on_different=None, x_replicas_on_different=None, diskless_on_remaining=None, layer_list=None, provider_list=None, diskless_storage_pool=None): ... + def resource_group_adjust(self, rsc_grp_name): ... + def volume_group_create(self, resource_grp_name, volume_nr=None, property_dict=None, gross: bool = False): ... + def volume_group_modify(self, resource_grp_name, volume_nr, property_dict=None, delete_props=None, gross=None): ... + def volume_group_delete(self, resource_grp_name, volume_nr): ... + def volume_group_list_raise(self, resource_grp_name): ... + def resource_dfn_create(self, name, port=None, external_name=None, layer_list=None, resource_group=None, peer_slots=None): ... + def resource_dfn_clone(self, src_name, clone_name, clone_external_name=None, use_zfs_clone=None, volume_passphrases=None, layer_list=None, resource_group=None, property_dict=None, delete_props=None): ... + def resource_dfn_clone_status(self, src_name, clone_name): ... + def resource_dfn_clone_wait_complete(self, src_name, clone_name, wait_interval: float = 1.0, timeout=None): ... + def resource_dfn_sync_status(self, rsc_name): ... + def resource_dfn_wait_synced(self, rsc_name, wait_interval: float = 1.0, timeout=None): ... + def resource_dfn_modify(self, name, property_dict, delete_props=None, peer_slots=None, resource_group=None, port=None): ... + def resource_dfn_delete(self, name, async_msg: bool = False): ... + def resource_dfn_list(self, query_volume_definitions: bool = True, filter_by_resource_definitions=None, filter_by_props=None): ... + def resource_dfn_list_raise(self, query_volume_definitions: bool = True, filter_by_resource_definitions=None, filter_by_props=None): ... + def resource_dfn_props_list(self, rsc_name, filter_by_namespace: str = ''): ... + def volume_dfn_create(self, rsc_name, size, volume_nr=None, minor_nr=None, encrypt: bool = False, storage_pool=None, gross: bool = False, passphrase=None): ... + def volume_dfn_modify(self, rsc_name, volume_nr, set_properties=None, delete_properties=None, size=None, gross=None): ... + def volume_dfn_delete(self, rsc_name, volume_nr, async_msg: bool = False): ... + def _volume_dfn_size(self, rsc_name, volume_nr): ... + def volume_dfn_modify_passphrase(self, rsc_name, volume_nr, new_passphrase): ... + def resource_create(self, rscs, async_msg: bool = False): ... + def _set_select_filter_body(self, body, place_count, storage_pool, do_not_place_with, do_not_place_with_regex, replicas_on_same, replicas_on_different, x_replicas_on_different, diskless_on_remaining, layer_list, provider_list, additional_place_count, diskless_type, diskless_storage_pool): ... + def resource_auto_place(self, rsc_name, place_count, storage_pool=None, do_not_place_with=None, do_not_place_with_regex=None, replicas_on_same=None, replicas_on_different=None, x_replicas_on_different=None, diskless_on_remaining: bool = False, async_msg: bool = False, layer_list=None, provider_list=None, additional_place_count=None, diskless_type=None, diskless_storage_pool=None): ... + def resource_create_and_auto_place(self, rsc_name, size, place_count, storage_pool=None, diskless_on_remaining: bool = False): ... + def resource_make_available(self, node_name, rsc_name, diskful: bool = False, layer_list=None, drbd_tcp_ports=None): ... + def resource_modify(self, node_name, rsc_name, property_dict, delete_props=None): ... + def resource_delete(self, node_name, rsc_name, async_msg: bool = False, keep_tiebreaker: bool = False): ... + def resource_delete_if_diskless(self, node_name, rsc_name): ... + def resource_activate(self, node_name, rsc_name): ... + def resource_deactivate(self, node_name, rsc_name): ... + def resource_list(self, filter_by_nodes=None, filter_by_resources=None, filter_by_props=None): ... + def resource_list_raise(self, filter_by_nodes=None, filter_by_resources=None, filter_by_props=None): ... + def volume_list(self, filter_by_nodes=None, filter_by_stor_pools=None, filter_by_resources=None, filter_by_props=None): ... + def volume_list_raise(self, filter_by_nodes=None, filter_by_stor_pools=None, filter_by_resources=None, filter_by_props=None): ... + def volume_modify(self, node_name, rsc_name, vlm_nr, property_dict, delete_props=None): ... + def resource_toggle_disk(self, node_name, rsc_name, storage_pool=None, diskless: bool = False, async_msg: bool = False, migrate_from=None): ... + def controller_props(self): ... + @classmethod + def _split_prop_key(cls, fkey): ... + def controller_set_prop(self, key, value): ... + def controller_del_prop(self, key): ... + def controller_backupdb(self, backup_name): ... + def controller_info(self): ... + def controller_version(self): ... + def controller_host(self): ... + def controller_set_log_level(self, level, glob: bool = False, library: bool = False): ... + def crypt_create_passphrase(self, passphrase): ... + def crypt_enter_passphrase(self, passphrase): ... + def crypt_modify_passphrase(self, old_passphrase, new_passphrase): ... + def resource_conn_modify(self, rsc_name, node_a, node_b, property_dict, delete_props): ... + def resource_conn_list(self, rsc_name): ... + def resource_conn_list_raise(self, rsc_name): ... + def resource_conn_node_list_raise(self, rsc_name, node_a, node_b): ... + def node_conn_modify(self, node_a, node_b, property_dict, delete_props): ... + def node_conn_list_specific_pair(self, node_a, node_b): ... + def node_conn_list(self, node_a=None, node_b=None): ... + def _node_conn_list(self, api_call, node_a=None, node_b=None): ... + def node_conn_list_raise(self, node_a=None, node_b=None): ... + def node_set_log_level(self, node_name, level, library: bool = False): ... + def drbd_proxy_enable(self, rsc_name, node_a, node_b, port=None): ... + def drbd_proxy_disable(self, rsc_name, node_a, node_b): ... + def drbd_proxy_modify(self, rsc_name, property_dict=None, delete_props=None, compression_type=None, compression_property_dict=None): ... + def snapshot_create(self, node_names, rsc_name, snapshot_name, async_msg: bool = False): ... + def snapshot_create_multi(self, node_names, rsc_names, snapshot_name): ... + def snapshot_volume_definition_restore(self, from_resource, from_snapshot, to_resource): ... + def snapshot_resource_restore(self, node_names, from_resource, from_snapshot, to_resource, storpool_rename_map=None): ... + def snapshot_delete(self, rsc_name, snapshot_name, node_names=None): ... + def snapshot_rollback(self, rsc_name, snapshot_name, zfs_rollback_strategy=None): ... + def snapshot_dfn_list(self, filter_by_nodes=None, filter_by_resources=None): ... + def snapshot_dfn_list_raise(self, filter_by_nodes=None, filter_by_resources=None): ... + def snapshot_dfn_modify(self, resource_name, snapshot_name, property_dict, delete_props=None): ... + def error_report_list(self, nodes=None, with_content: bool = False, since=None, to=None, ids=None): ... + def error_report_delete(self, nodes=None, since=None, to=None, exception=None, version=None, ids=None): ... + def keyvaluestore_modify(self, instance_name, property_dict=None, delete_props=None): ... + def keyvaluestores(self): ... + def keyvaluestore_list(self, instance_name): ... + def physical_storage_list(self): ... + def physical_storage_create_device_pool(self, node_name, provider_kind, device_paths, pool_name=None, raid_level: str = 'JBOD', vdo_enable: bool = False, vdo_logical_size_kib=None, vdo_slab_size_kib=None, storage_pool_name=None, storage_pool_props=None, sed: bool = False, pv_create_arguments=None, vg_create_arguments=None, lv_create_arguments=None, zpool_create_arguments=None): ... + def sos_report_create(self, since=None, nodes=None, rscs=None, exclude=None, include_ctrl: bool = True): ... + def sos_report_download(self, since=None, to_file=None, nodes=None, rscs=None, exclude=None, include_ctrl: bool = True): ... + def space_reporting_query(self): ... + def backup_list(self, remote_name, resource_name=None, snap_name=None): ... + def backup_create(self, remote_name, resource_name, incremental: bool = True, node_name=None, snap_name=None): ... + def backup_delete(self, remote_name, bak_id=None, bak_id_prefix=None, cascade: bool = False, timestamp=None, resource_name=None, node_name=None, all_linstor: bool = False, all_local_cluster: bool = False, s3_key=None, dryrun=None, keep_snaps=None): ... + def backup_restore(self, remote_name, target_node_name, target_resource_name, resource_name=None, bak_id=None, passphrase=None, stor_pool_map=None, download_only: bool = False, force_restore: bool = False, snap_name=None, dst_rsc_grp=None, force_mv_rsc_grp: bool = False): ... + def backup_abort(self, remote_name, resource_name, restore=None, create=None, snapshot=None): ... + def backup_ship(self, remote_name, src_rsc_name, dst_rsc_name, src_node=None, dst_node=None, dst_net_if=None, dst_stor_pool=None, stor_pool_rename=None, download_only: bool = False, force_restore: bool = False, dst_rsc_grp=None, force_mv_rsc_grp: bool = False, force_full=None, src_snap=None): ... + def backup_info(self, remote_name, resource_name=None, bak_id=None, target_node=None, stor_pool_map=None, snap_name=None): ... + def backup_schedule_enable(self, remote_name, schedule_name, resource_name=None, resource_group_name=None, preferred_node=None, dst_stor_pool=None, storpool_rename_map=None, force_restore: bool = False, dst_rsc_grp=None, force_mv_rsc_grp: bool = False, dst_rsc_name=None): ... + def backup_schedule_disable(self, remote_name, schedule_name, resource_name=None, resource_group_name=None): ... + def backup_schedule_delete(self, remote_name, schedule_name, resource_name=None, resource_group_name=None): ... + def backup_queue_list(self, nodes=None, snaps=None, rscs=None, remotes=None, snap_to_node: bool = False): ... + def remote_list(self): ... + def remote_create_s3(self, remote_name, endpoint, region, bucket, access_key, secret_key, use_path_style: bool = False): ... + def remote_modify_s3(self, remote_name, endpoint=None, region=None, bucket=None, access_key=None, secret_key=None): ... + def remote_create_linstor(self, remote_name, url, passphrase=None, cluster_id=None): ... + def remote_modify_linstor(self, remote_name, url=None, passphrase=None, cluster_id=None): ... + def remote_create_ebs(self, remote_name, availability_zone, access_key, secret_key, endpoint=None, region=None): ... + def remote_modify_ebs(self, remote_name, endpoint=None, region=None, availability_zone=None, access_key=None, secret_key=None): ... + def remote_delete(self, remote_name): ... + def stats(self): ... + def file_list(self): ... + def file_show(self, file_name): ... + def file_modify(self, file_name, new_content): ... + def file_delete(self, file_name): ... + def file_deploy(self, file_name, rsc_name): ... + def file_undeploy(self, file_name, rsc_name): ... + def schedule_list(self): ... + def schedule_list_by_resource(self, filter_by_resource=None, filter_by_remote=None, filter_by_schedule=None, active_only: bool = False): ... + def schedule_list_by_resource_details(self, resource_name): ... + def schedule_create(self, schedule_name, full_cron, keep_local=None, keep_remote=None, on_failure=None, incremental_cron=None, max_retries=None): ... + def schedule_modify(self, schedule_name, full_cron=None, keep_local=None, keep_remote=None, on_failure=None, incremental_cron=None, max_retries=None): ... + def schedule_delete(self, schedule_name): ... + +class MultiLinstor(Linstor): + _ctrl_host_list: list[str] + def __init__(self, ctrl_host_list, timeout: int = 300, keep_alive: bool = False, agent_info: str = '') -> None: ... + _ctrl_host: Incomplete + def connect(self): ... + @classmethod + def controller_uri_list(cls, controller_list): ... diff --git a/stubs/linstor/properties.pyi b/stubs/linstor/properties.pyi new file mode 100644 index 0000000..00f7222 --- /dev/null +++ b/stubs/linstor/properties.pyi @@ -0,0 +1,3 @@ +from _typeshed import Incomplete + +properties: Incomplete diff --git a/stubs/linstor/resource.pyi b/stubs/linstor/resource.pyi new file mode 100644 index 0000000..c05c2f0 --- /dev/null +++ b/stubs/linstor/resource.pyi @@ -0,0 +1,132 @@ +import linstor +from _typeshed import Incomplete +from linstor.linstorapi import Linstor as Linstor +from linstor.responses import ApiCallResponse as ApiCallResponse +from linstor.sharedconsts import FLAG_DISKLESS as FLAG_DISKLESS + +PYTHON2: bool +unicode = str + +class _Utils: + @classmethod + def to_unicode(cls, t): ... + +class _Client: + _uri_list: list[str] + timeout: Incomplete + keep_alive: Incomplete + agent_info: Incomplete + def __init__(self, uris, timeout: int = 300, keep_alive: bool = False, agent_info: str = '') -> None: ... + @property + def uri_list(self): ... + @uri_list.setter + def uri_list(self, uri_list) -> None: ... + +class _Placement: + redundancy: Incomplete + storage_pool: Incomplete + diskless_storage_pool: Incomplete + diskless_on_remaining: bool + def __init__(self, redundancy=None) -> None: ... + +class Volume: + _size: Incomplete + _minor: Incomplete + _device_path: str + _storage_pool_name: str + _volume_id: Incomplete + _rsc_name: Incomplete + _client_ref: Optional[linstor.Linstor] + _assignments: Incomplete + def __init__(self, size) -> None: ... + def __repr__(self) -> str: ... + @property + def storage_pool_name(self): ... + @property + def device_path(self): ... + @device_path.setter + def device_path(self, device_path) -> None: ... + @property + def minor(self): ... + @minor.setter + def minor(self, minor) -> None: ... + @property + def size(self): ... + @classmethod + def _size_to_bytes(cls, size): ... + @size.setter + def size(self, size) -> None: ... + def _delete(self) -> None: ... + +class _VolumeDict(dict): + def __init__(self) -> None: ... + def __setitem__(self, k, v) -> None: ... + def __delitem__(self, k) -> None: ... + +class Resource: + _lin: Incomplete + def _update_volumes(f): ... + _name: Incomplete + _port: Incomplete + _resource_group_name: Optional[str] + client: Incomplete + placement: Incomplete + volumes: dict[int, Volume] + defined: bool + _allow_two_primaries: bool + _assignments: Incomplete + _linstor_name: Incomplete + _existing_client: Incomplete + def __init__(self, name, uri: str = 'linstor://localhost', existing_client=None, resource_group=None) -> None: ... + def __str__(self) -> str: ... + def __repr__(self) -> str: ... + @classmethod + def from_resource_group(cls, uri, resource_group_name, resource_name, vlm_sizes, timeout: int = 300, keep_alive: bool = False, definitions_only: bool = False, existing_client=None, agent_info: str = ''): ... + def _get_connection(self): ... + def _set_properties(self) -> None: ... + def _maybe_create_rd_and_vd(self) -> None: ... + def _create_volume_definitions(self) -> None: ... + def __update_volumes(self): ... + @property + def allow_two_primaries(self): ... + @allow_two_primaries.setter + def allow_two_primaries(self, value) -> None: ... + @property + def name(self): ... + @name.setter + def name(self, name) -> None: ... + @property + def linstor_name(self): ... + @linstor_name.setter + def linstor_name(self, name) -> None: ... + @property + def port(self): ... + @port.setter + def port(self, port_nr) -> None: ... + @property + def resource_group_name(self): ... + @_update_volumes + def autoplace(self): ... + @_update_volumes + def activate(self, node_name, diskful: bool = False): ... + def deactivate(self, node_name): ... + @_update_volumes + def _create_or_toggle(self, node_name, diskless): ... + def snapshot_create(self, name): ... + def snapshot_delete(self, name): ... + def snapshot_rollback(self, name): ... + def restore_from_snapshot(self, snapshot_name, resource_name_to): ... + def clone(self, clone_name, clone_external_name=None, timeout=None, use_zfs_clone=None, volume_passphrases=None, layer_list=None, resource_group=None, property_dict=None, delete_props=None): ... + def diskless(self, node_name): ... + def diskful(self, node_name): ... + def is_diskless(self, node_name): ... + def is_diskful(self, node_name): ... + def is_assigned(self, node_name): ... + def diskless_nodes(self): ... + def diskful_nodes(self): ... + def is_thin(self): ... + _volumes: Incomplete + def _delete(self, node_name=None): ... + def delete(self, node_name=None, snapshots: bool = True): ... + def drbd_proxy_enable(self, node_name_a, node_name_b): ... + def drbd_proxy_disable(self, node_name_a, node_name_b): ... diff --git a/stubs/linstor/resourcegroup.pyi b/stubs/linstor/resourcegroup.pyi new file mode 100644 index 0000000..56d1c82 --- /dev/null +++ b/stubs/linstor/resourcegroup.pyi @@ -0,0 +1,87 @@ +from _typeshed import Incomplete +from linstor.resource import Resource as Resource, _Client as _Client + +class ResourceGroup: + _name: Incomplete + _uri: Incomplete + client: Incomplete + _existing_client: Incomplete + _description: Incomplete + _redundancy: Incomplete + _storage_pool_list: Incomplete + _do_not_place_with: Incomplete + _do_not_place_with_regex: Incomplete + _replicas_on_same: Incomplete + _replicas_on_different: Incomplete + _x_replicas_on_different: Incomplete + _diskless_on_remaining: Incomplete + _layer_list: Incomplete + _provider_list: Incomplete + _property_dict: Incomplete + _nr_volumes: int + _nr_volumes_default: int + def __init__(self, name, uri: str = 'linstor://localhost', existing_client=None, agent_info: str = '') -> None: ... + def _get_connection(self): ... + @property + def name(self): ... + @name.setter + def name(self, name) -> None: ... + @property + def description(self): ... + @description.setter + def description(self, description): ... + @property + def redundancy(self): ... + @redundancy.setter + def redundancy(self, redundancy): ... + @property + def storage_pool(self): ... + @storage_pool.setter + def storage_pool(self, storage_pool): ... + @property + def do_not_place_with(self): ... + @do_not_place_with.setter + def do_not_place_with(self, do_not_place_with): ... + @property + def do_not_place_with_regex(self): ... + @do_not_place_with_regex.setter + def do_not_place_with_regex(self, do_not_place_with_regex): ... + @property + def replicas_on_same(self): ... + @replicas_on_same.setter + def replicas_on_same(self, replicas_on_same): ... + @property + def replicas_on_different(self): ... + @property + def x_replicas_on_different(self): ... + @replicas_on_different.setter + def replicas_on_different(self, replicas_on_different): ... + @x_replicas_on_different.setter + def x_replicas_on_different(self, x_replicas_on_different): ... + @property + def diskless_on_remaining(self): ... + @diskless_on_remaining.setter + def diskless_on_remaining(self, diskless_on_remaining): ... + def _set_nr_volumes(self, nr_volumes): ... + @property + def nr_volumes(self): ... + @nr_volumes.setter + def nr_volumes(self, nr_volumes): ... + @property + def layer_list(self): ... + @layer_list.setter + def layer_list(self, layer_list): ... + @property + def provider_list(self): ... + @provider_list.setter + def provider_list(self, provider_list): ... + @property + def property_dict(self): ... + @property_dict.setter + def property_dict(self, property_dict): ... + def delete(self): ... + def create_resource(self, resource_name, vlm_sizes): ... + def query_max_volume_size(self): ... + def _modify_or_create(self, what: str = 'modify'): ... + def _update(self): ... + def _update_or_create(self): ... diff --git a/stubs/linstor/responses.pyi b/stubs/linstor/responses.pyi new file mode 100644 index 0000000..bbd8aa1 --- /dev/null +++ b/stubs/linstor/responses.pyi @@ -0,0 +1,1017 @@ +from .errors import LinstorError as LinstorError +from _typeshed import Incomplete + +class RESTMessageResponse: + _rest_data: Incomplete + def __init__(self, rest_data) -> None: ... + def data(self, version): ... + @property + def data_v0(self): ... + @property + def data_v1(self): ... + @classmethod + def _unix_epoch_field_value(cls, unix_epoch_val): ... + def __nonzero__(self): ... + def __bool__(self) -> bool: ... + def __str__(self) -> str: ... + def __repr__(self) -> str: ... + +class ApiCallResponse(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @classmethod + def from_json(cls, json_data): ... + @classmethod + def from_str(cls, message, code=None): ... + def is_error(self, code=None): ... + def is_warning(self): ... + def is_info(self): ... + def is_success(self): ... + @property + def ret_code(self): ... + @property + def message(self): ... + @property + def cause(self): ... + @property + def correction(self): ... + @property + def details(self): ... + @property + def object_refs(self): ... + @property + def error_report_ids(self): ... + @property + def data_v0(self): ... + def __eq__(self, other): ... + def __hash__(self): ... + def __str__(self) -> str: ... + +class ErrorReport(RESTMessageResponse): + def __init__(self, data) -> None: ... + @property + def datetime(self): ... + @property + def id(self): ... + @property + def text(self): ... + @property + def node_name(self): ... + @property + def node_names(self): ... + @property + def module(self): ... + @property + def version(self): ... + @property + def peer(self): ... + @property + def exception(self): ... + @property + def exception_message(self): ... + @property + def origin_file(self): ... + @property + def origin_line(self): ... + @property + def data_v0(self): ... + +class NodeType: + CONTROLLER: Incomplete + SATELLITE: Incomplete + COMBINED: Incomplete + AUXILIARY: Incomplete + +class NetInterface(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def name(self): ... + @property + def address(self): ... + @property + def stlt_port(self): ... + @property + def stlt_encryption_type(self): ... + @property + def is_active(self): ... + @property + def data_v0(self): ... + +class Node(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def name(self): ... + @property + def type(self): ... + @property + def connection_status(self): ... + @property + def net_interfaces(self): ... + @property + def storage_providers(self): ... + @property + def resource_layers(self): ... + @property + def unsupported_providers(self): ... + @property + def unsupported_layers(self): ... + @property + def props(self): ... + @property + def properties(self): ... + @property + def flags(self): ... + @property + def eviction_timestamp(self): ... + @property + def data_v0(self): ... + def __str__(self) -> str: ... + +class NodeListResponse(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def nodes(self): ... + def node(self, node_name): ... + @property + def data_v0(self): ... + +class NodeConnection(RESTMessageResponse): + def __init__(self, data) -> None: ... + @property + def node_a(self): ... + @property + def node_b(self): ... + @property + def flags(self): ... + @property + def properties(self): ... + @property + def data_v0(self): ... + +class NodeConnectionsResponse(RESTMessageResponse): + def __init__(self, data) -> None: ... + @property + def node_connections(self): ... + @property + def data_v0(self): ... + +class FreeSpace(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def free_capacity(self): ... + @property + def total_capacity(self): ... + def __str__(self) -> str: ... + @property + def data_v0(self): ... + +class StoragePoolDriver: + LVM: str + LVMThin: str + ZFS: str + ZFSThin: str + Diskless: str + FILE: str + FILEThin: str + SPDK: str + REMOTE_SPDK: str + STORAGE_SPACES: str + STORAGE_SPACES_THIN: str + EBS_TARGET: str + EBS_INIT: str + @staticmethod + def list(): ... + @classmethod + def diskless_driver(cls): ... + @staticmethod + def storage_driver_pool_to_props(storage_driver, driver_pool_name): ... + @classmethod + def storage_props_to_driver_pool(cls, storage_driver, props): ... + +class StoragePool(RESTMessageResponse): + DRIVER_KIND_MAP: Incomplete + def __init__(self, rest_data) -> None: ... + @property + def name(self): ... + @property + def uuid(self): ... + @property + def node_name(self): ... + @property + def driver(self): ... + @property + def provider_kind(self): ... + @property + def properties(self): ... + @property + def static_traits(self): ... + @property + def free_space(self): ... + @property + def free_space_mgr_name(self): ... + def supports_snapshots(self): ... + def is_thin(self): ... + def is_fat(self): ... + def is_diskless(self): ... + @property + def reports(self): ... + @property + def data_v0(self): ... + +class StoragePoolListResponse(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def storage_pools(self): ... + @property + def data_v0(self): ... + +class KeyValueStoresResponse(RESTMessageResponse): + def __init__(self, data) -> None: ... + def instances(self): ... + def instance(self, name): ... + +class KeyValueStore: + _instance_name: Incomplete + _props: Incomplete + def __init__(self, instance_name, props) -> None: ... + @property + def properties(self): ... + def __str__(self) -> str: ... + +class DrbdVolumeDefinitionData(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def resource_name_suffix(self): ... + @property + def minor(self): ... + @property + def number(self): ... + +class VolumeDefinition(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def number(self): ... + @property + def uuid(self): ... + @property + def size(self): ... + @property + def flags(self): ... + @property + def properties(self): ... + @property + def drbd_data(self): ... + @property + def data_v0(self): ... + +class VolumeDefinitionResponse(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def volume_definitions(self): ... + @property + def rest_data(self): ... + +class DrbdLayer(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def port(self): ... + @property + def secret(self): ... + @property + def peer_slots(self): ... + +class ResourceDfnLayerData(RESTMessageResponse): + def __init__(self, data) -> None: ... + @property + def type(self): ... + @property + def drbd_resource(self): ... + +class ResourceDefinition(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def name(self): ... + @property + def uuid(self): ... + @property + def external_name(self): ... + @property + def flags(self): ... + @property + def properties(self): ... + @property + def layer_data(self): ... + @property + def drbd_data(self): ... + @property + def volume_definitions(self): ... + @property + def resource_group_name(self): ... + +class ResourceDefinitionResponse(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def resource_definitions(self): ... + @property + def data_v0(self): ... + +class SelectFilter(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def place_count(self): ... + @property + def diskless_on_remaining(self): ... + @property + def storage_pool(self): ... + @property + def storage_pool_list(self): ... + @property + def not_place_with_rsc(self): ... + @property + def not_place_with_rsc_regex(self): ... + @property + def replicas_on_same(self): ... + @property + def replicas_on_different(self): ... + @property + def x_replicas_on_different(self): ... + @property + def layer_stack(self): ... + @property + def provider_list(self): ... + def __str__(self) -> str: ... + +class ResourceGroup(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def name(self): ... + @property + def description(self): ... + @property + def properties(self): ... + @property + def select_filter(self): ... + +class ResourceGroupResponse(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def resource_groups(self): ... + +class VolumeGroup(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def number(self): ... + @property + def properties(self): ... + @property + def flags(self): ... + +class VolumeGroupResponse(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def volume_groups(self): ... + +class ReplicationState(RESTMessageResponse): + def __init__(self, data) -> None: ... + @property + def replication_state(self): ... + @property + def done_percentage(self): ... + +class VolumeState(RESTMessageResponse): + def __init__(self, data) -> None: ... + @property + def number(self): ... + @property + def disk_state(self): ... + @property + def replication_states(self): ... + @property + def data_v0(self): ... + +class ResourceState(RESTMessageResponse): + def __init__(self, data) -> None: ... + @property + def name(self): ... + @property + def rsc_name(self): ... + @property + def node_name(self): ... + @property + def in_use(self): ... + @property + def volume_states(self): ... + @property + def data_v0(self): ... + +class DrbdConnection(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def connected(self): ... + @property + def message(self): ... + +class DrbdResource(RESTMessageResponse): + def __init__(self, data) -> None: ... + @property + def node_id(self): ... + @property + def peer_slots(self): ... + @property + def al_stripes(self): ... + @property + def al_size(self): ... + @property + def connections(self): ... + @property + def tcp_ports(self): ... + +class ResourceLayerData(RESTMessageResponse): + def __init__(self, data) -> None: ... + @property + def name_suffix(self): ... + @property + def children(self): ... + @property + def layer_stack(self): ... + @property + def type(self): ... + @property + def drbd_resource(self): ... + +class VolumeLayerData(RESTMessageResponse): + def __init__(self, data) -> None: ... + @property + def layer_type(self): ... + +class DrbdVolumeDefinition(RESTMessageResponse): + def __init__(self, data) -> None: ... + @property + def number(self): ... + @property + def minor(self): ... + @property + def resource_name_suffix(self): ... + +class DrbdVolumeData(RESTMessageResponse): + def __init__(self, data) -> None: ... + @property + def drbd_volume_definition(self): ... + @property + def device_path(self): ... + @property + def backing_device(self): ... + @property + def meta_disk(self): ... + @property + def allocated_size(self): ... + @property + def usable_size(self): ... + +class StorageVolumeData(RESTMessageResponse): + def __init__(self, data) -> None: ... + +class LUKSVolumeData(RESTMessageResponse): + def __init__(self, data) -> None: ... + +class Volume(RESTMessageResponse): + def __init__(self, data) -> None: ... + @property + def number(self): ... + @property + def uuid(self): ... + @property + def storage_pool_name(self): ... + @property + def storage_pool_driver_name(self): ... + @property + def device_path(self): ... + @property + def allocated_size(self): ... + @property + def usable_size(self): ... + @property + def flags(self): ... + @property + def properties(self): ... + @property + def layer_data(self): ... + @property + def drbd_data(self): ... + @property + def storage_data(self): ... + @property + def luks_data(self): ... + @property + def state(self): ... + @property + def reports(self): ... + @property + def data_v0(self): ... + +class VolumeResponse(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def volumes(self): ... + +class Resource(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def name(self): ... + @property + def uuid(self): ... + @property + def node_name(self): ... + @property + def volumes(self): ... + @property + def flags(self): ... + @property + def properties(self): ... + @property + def effective_properties(self): ... + @property + def layer_data(self): ... + @property + def create_datetime(self): ... + @property + def data_v0(self): ... + +class ResourceResponse(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def resources(self): ... + @property + def nodes(self): ... + @property + def resource_states(self): ... + @property + def data_v0(self): ... + +class ResourceConnection(RESTMessageResponse): + def __init__(self, data) -> None: ... + @property + def node_a(self): ... + @property + def node_b(self): ... + @property + def flags(self): ... + @property + def properties(self): ... + @property + def port(self): ... + @property + def data_v0(self): ... + +class ResourceConnectionsResponse(RESTMessageResponse): + def __init__(self, data) -> None: ... + @property + def resource_connections(self): ... + @property + def data_v0(self): ... + +class SnapshotVolumeDefinition(RESTMessageResponse): + def __init__(self, data) -> None: ... + @property + def number(self): ... + @property + def size(self): ... + @property + def data_v0(self): ... + +class Snapshot(RESTMessageResponse): + def __init__(self, data) -> None: ... + @property + def name(self): ... + @property + def node_name(self): ... + @property + def flags(self): ... + @property + def create_datetime(self): ... + @property + def uuid(self): ... + @property + def snapshot_volumes(self): ... + +class SnapshotVolume(RESTMessageResponse): + def __init__(self, data) -> None: ... + @property + def uuid(self): ... + @property + def vlm_nr(self): ... + @property + def state(self): ... + @property + def properties(self): ... + +class SnapshotDefinition(RESTMessageResponse): + def __init__(self, data) -> None: ... + @property + def name(self): ... + @property + def uuid(self): ... + @property + def snapshot_name(self): ... + @property + def resource_name(self): ... + @property + def rsc_name(self): ... + @property + def nodes(self): ... + @property + def flags(self): ... + @property + def properties(self): ... + @property + def snapshot_volume_definitions(self): ... + @property + def snapshots(self): ... + @property + def data_v0(self): ... + +class SnapshotResponse(RESTMessageResponse): + def __init__(self, data) -> None: ... + @property + def snapshots(self): ... + @property + def data_v0(self): ... + +class ControllerProperties(RESTMessageResponse): + def __init__(self, data) -> None: ... + @property + def properties(self): ... + +class StoragePoolDefinition(RESTMessageResponse): + def __init__(self, data) -> None: ... + @property + def name(self): ... + @property + def properties(self): ... + @property + def data_v0(self): ... + +class StoragePoolDefinitionResponse(RESTMessageResponse): + def __init__(self, data) -> None: ... + @property + def storage_pool_definitions(self): ... + @property + def data_v0(self): ... + +class Candidate(RESTMessageResponse): + def __init__(self, data) -> None: ... + @property + def max_volume_size(self): ... + @property + def storage_pool(self): ... + @property + def node_names(self): ... + @property + def all_thin(self): ... + +class MaxVolumeSizeResponse(RESTMessageResponse): + def __init__(self, data) -> None: ... + @property + def candidates(self): ... + @property + def default_max_oversubscription_ratio(self): ... + +class ControllerVersion(RESTMessageResponse): + def __init__(self, data) -> None: ... + @property + def version(self): ... + @property + def git_hash(self): ... + @property + def build_time(self): ... + @property + def rest_api_version(self): ... + +class NodeStorageEntry(RESTMessageResponse): + def __init__(self, data) -> None: ... + @property + def device(self): ... + @property + def model(self): ... + @property + def serial(self): ... + @property + def wwn(self): ... + +class PhysicalDevice(RESTMessageResponse): + def __init__(self, data) -> None: ... + @property + def size(self): ... + @property + def rotational(self): ... + @property + def nodes(self): ... + +class PhysicalStorageList(RESTMessageResponse): + def __init__(self, data) -> None: ... + @property + def physical_devices(self): ... + +class SpaceReport(RESTMessageResponse): + def __init__(self, data) -> None: ... + @property + def report(self): ... + +class CloneStarted(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def location(self): ... + @property + def source_name(self): ... + @property + def clone_name(self): ... + @property + def messages(self): ... + +class CloneStatus(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def status(self): ... + +class SyncStatus(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def synced_on_all(self): ... + +class S3Remote(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def remote_name(self): ... + @property + def endpoint(self): ... + @property + def bucket(self): ... + @property + def region(self): ... + @property + def access_key(self): ... + @property + def secret_key(self): ... + +class LinstorRemote(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def remote_name(self): ... + @property + def url(self): ... + @property + def passphrase(self): ... + +class EbsRemote(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def remote_name(self): ... + @property + def endpoint(self): ... + @property + def region(self): ... + @property + def availability_zone(self): ... + +class RemoteListResponse(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def s3_remotes(self): ... + @property + def linstor_remotes(self): ... + @property + def ebs_remotes(self): ... + +class Backup(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def id(self): ... + @property + def start_time(self): ... + @property + def start_timestamp(self): ... + @property + def finished_time(self): ... + @property + def finished_timestamp(self): ... + @property + def origin_rsc_name(self): ... + @property + def origin_snap_name(self): ... + @property + def based_on(self): ... + @property + def success(self): ... + @property + def shipping(self): ... + @property + def restorable(self): ... + +class BackupOther(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def files(self): ... + +class BackupListResponse(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def linstor(self): ... + @property + def other(self): ... + +class BackupInfoResponse(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def rsc(self): ... + @property + def full(self): ... + @property + def snap(self): ... + @property + def latest(self): ... + @property + def count(self): ... + @property + def dl_size(self): ... + @property + def alloc_size(self): ... + @property + def storpools(self): ... + +class BackupInfoStorPool(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def name(self): ... + @property + def provider_kind(self): ... + @property + def target_name(self): ... + @property + def remaining_space(self): ... + @property + def volumes(self): ... + +class BackupInfoStorPoolVolume(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def name(self): ... + @property + def layer_type(self): ... + @property + def dl_size(self): ... + @property + def alloc_size(self): ... + @property + def usable_size(self): ... + +class BackupQueues(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def node_queues(self): ... + @property + def snap_queues(self): ... + +class NodeQueue(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def node_name(self): ... + @property + def queue(self): ... + +class SnapQueue(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def resource_name(self): ... + @property + def snapshot_name(self): ... + @property + def remote_name(self): ... + @property + def incremental(self): ... + @property + def based_on(self): ... + @property + def start_timestamp(self): ... + @property + def pref_node(self): ... + @property + def queue(self): ... + +class ExternalFile(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def path(self): ... + @property + def content(self): ... + +class FileResponse(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def files(self): ... + +class Schedule(RESTMessageResponse): + def __int__(self, rest_data) -> int: ... + @property + def schedule_name(self): ... + @property + def full_cron(self): ... + @property + def inc_cron(self): ... + @property + def keep_local(self): ... + @property + def keep_remote(self): ... + @property + def on_failure(self): ... + @property + def max_retries(self): ... + +class ScheduleListResponse(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def schedules(self): ... + +class ScheduleResource(RESTMessageResponse): + def __int__(self, rest_data) -> int: ... + @property + def resource_name(self): ... + @property + def remote_name(self): ... + @property + def schedule_name(self): ... + @property + def reason(self): ... + @property + def last_snap_time(self): ... + @property + def last_snap_inc(self): ... + @property + def next_exec_time(self): ... + @property + def next_exec_inc(self): ... + @property + def next_planned_full(self): ... + @property + def next_planned_inc(self): ... + +class ScheduleResourceListResponse(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def schedule_resources(self): ... + +class ScheduleResourceDetails(RESTMessageResponse): + def __int__(self, rest_data) -> int: ... + @property + def remote_name(self): ... + @property + def schedule_name(self): ... + @property + def controller(self): ... + @property + def resource_definition(self): ... + @property + def resource_group(self): ... + +class ScheduleResourceDetailsListResponse(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def schedule_resources(self): ... + +class QuerySizeInfoSpawnResult(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def node_name(self): ... + @property + def stor_pool_name(self): ... + @property + def stor_pool_oversubscription_ratio(self): ... + +class QuerySizeInfoResponseSpaceInfo(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def max_vlm_size_in_kib(self): ... + @property + def available_size_in_kib(self): ... + @property + def capacity_in_kib(self): ... + @property + def default_max_oversubscription_ratio(self): ... + @property + def next_spawn_result(self): ... + +class QuerySizeInfoResponse(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def space_info(self): ... + @property + def reports(self): ... + +class EffectiveProp(RESTMessageResponse): + def __init__(self, rest_data) -> None: ... + @property + def type(self): ... + @property + def value(self): ... + @property + def descr(self): ... + @property + def other(self): ... diff --git a/stubs/linstor/sharedconsts.pyi b/stubs/linstor/sharedconsts.pyi new file mode 100644 index 0000000..5c44553 --- /dev/null +++ b/stubs/linstor/sharedconsts.pyi @@ -0,0 +1,720 @@ +from _typeshed import Incomplete +from enum import Enum + +long = int +MASK_BITS_TYPE: Incomplete +MASK_ERROR: Incomplete +MASK_WARN: Incomplete +MASK_INFO: Incomplete +MASK_SUCCESS: Incomplete +MASK_BITS_OP: Incomplete +MASK_CRT: Incomplete +MASK_MOD: Incomplete +MASK_DEL: Incomplete +MASK_BITS_OBJ: Incomplete +MASK_SCHEDULE: Incomplete +MASK_EXT_FILES: Incomplete +MASK_PHYSICAL_DEVICE: Incomplete +MASK_VLM_GRP: Incomplete +MASK_RSC_GRP: Incomplete +MASK_KVS: Incomplete +MASK_NODE: Incomplete +MASK_RSC_DFN: Incomplete +MASK_RSC: Incomplete +MASK_VLM_DFN: Incomplete +MASK_VLM: Incomplete +MASK_NODE_CONN: Incomplete +MASK_RSC_CONN: Incomplete +MASK_VLM_CONN: Incomplete +MASK_NET_IF: Incomplete +MASK_STOR_POOL_DFN: Incomplete +MASK_STOR_POOL: Incomplete +MASK_CTRL_CONF: Incomplete +MASK_SNAPSHOT: Incomplete +MASK_BACKUP: Incomplete +MASK_REMOTE: Incomplete +MASK_BITS_CODE: Incomplete +CREATED: Incomplete +DELETED: Incomplete +MODIFIED: Incomplete +PASSPHRASE_ACCEPTED: Incomplete +FAIL_SQL: Incomplete +FAIL_SQL_ROLLBACK: Incomplete +FAIL_INVLD_NODE_NAME: Incomplete +FAIL_INVLD_NODE_TYPE: Incomplete +FAIL_INVLD_RSC_NAME: Incomplete +FAIL_INVLD_RSC_PORT: Incomplete +FAIL_INVLD_NODE_ID: Incomplete +FAIL_INVLD_VLM_NR: Incomplete +FAIL_INVLD_VLM_SIZE: Incomplete +FAIL_INVLD_MINOR_NR: Incomplete +FAIL_INVLD_STOR_POOL_NAME: Incomplete +FAIL_INVLD_NET_NAME: Incomplete +FAIL_INVLD_NET_ADDR: Incomplete +FAIL_INVLD_NET_PORT: Incomplete +FAIL_INVLD_NET_TYPE: Incomplete +FAIL_INVLD_PROP: Incomplete +FAIL_INVLD_TRANSPORT_TYPE: Incomplete +FAIL_INVLD_TCP_PORT: Incomplete +FAIL_INVLD_CRYPT_PASSPHRASE: Incomplete +FAIL_INVLD_ENCRYPT_TYPE: Incomplete +FAIL_INVLD_SNAPSHOT_NAME: Incomplete +FAIL_INVLD_PLACE_COUNT: Incomplete +FAIL_INVLD_FREE_SPACE_MGR_NAME: Incomplete +FAIL_INVLD_STOR_DRIVER: Incomplete +FAIL_INVLD_DRBD_PROXY_COMPRESSION_TYPE: Incomplete +FAIL_INVLD_KVS_NAME: Incomplete +FAIL_INVLD_LAYER_KIND: Incomplete +FAIL_INVLD_LAYER_STACK: Incomplete +FAIL_INVLD_EXT_NAME: Incomplete +FAIL_INVLD_PROVIDER: Incomplete +FAIL_INVLD_VLM_SIZES: Incomplete +FAIL_INVLD_VLM_COUNT: Incomplete +FAIL_INVLD_CONF: Incomplete +FAIL_INVLD_SNAPSHOT_SHIPPING_SOURCE: Incomplete +FAIL_INVLD_SNAPSHOT_SHIPPING_TARGET: Incomplete +FAIL_NODE_HAS_USED_RSC: Incomplete +FAIL_INVLD_REQUEST: Incomplete +FAIL_INVLD_EXT_FILE_NAME: Incomplete +FAIL_INVLD_EXT_FILE: Incomplete +FAIL_INVLD_RSC_GRP_NAME: Incomplete +FAIL_INVLD_BACKUP_CONFIG: Incomplete +FAIL_INVLD_REMOTE_NAME: Incomplete +FAIL_INVLD_TIME_PARAM: Incomplete +FAIL_INVLD_SCHEDULE_NAME: Incomplete +FAIL_INVLD_DB_EXPORT_FILE: Incomplete +FAIL_INVLD_RSC_STATE: Incomplete +FAIL_NOT_FOUND_NODE: Incomplete +FAIL_NOT_FOUND_RSC_DFN: Incomplete +FAIL_NOT_FOUND_RSC: Incomplete +FAIL_NOT_FOUND_VLM_DFN: Incomplete +FAIL_NOT_FOUND_VLM: Incomplete +FAIL_NOT_FOUND_NET_IF: Incomplete +FAIL_NOT_FOUND_NODE_CONN: Incomplete +FAIL_NOT_FOUND_RSC_CONN: Incomplete +FAIL_NOT_FOUND_VLM_CONN: Incomplete +FAIL_NOT_FOUND_STOR_POOL_DFN: Incomplete +FAIL_NOT_FOUND_STOR_POOL: Incomplete +FAIL_NOT_FOUND_DFLT_STOR_POOL: Incomplete +FAIL_NOT_FOUND_CRYPT_KEY: Incomplete +FAIL_NOT_FOUND_SNAPSHOT_DFN: Incomplete +FAIL_NOT_FOUND_SNAPSHOT_VLM_DFN: Incomplete +FAIL_NOT_FOUND_SNAPSHOT: Incomplete +FAIL_NOT_FOUND_KVS: Incomplete +FAIL_NOT_FOUND_RSC_GRP: Incomplete +FAIL_NOT_FOUND_VLM_GRP: Incomplete +FAIL_NOT_FOUND_EXOS_ENCLOSURE: Incomplete +FAIL_NOT_FOUND_EXT_FILE: Incomplete +FAIL_NOT_FOUND_REMOTE: Incomplete +FAIL_NOT_FOUND_BACKUP: Incomplete +FAIL_NOT_FOUND_SCHEDULE: Incomplete +FAIL_ACC_DENIED_NODE: Incomplete +FAIL_ACC_DENIED_RSC_DFN: Incomplete +FAIL_ACC_DENIED_RSC: Incomplete +FAIL_ACC_DENIED_VLM_DFN: Incomplete +FAIL_ACC_DENIED_VLM: Incomplete +FAIL_ACC_DENIED_STOR_POOL_DFN: Incomplete +FAIL_ACC_DENIED_STOR_POOL: Incomplete +FAIL_ACC_DENIED_NODE_CONN: Incomplete +FAIL_ACC_DENIED_RSC_CONN: Incomplete +FAIL_ACC_DENIED_VLM_CONN: Incomplete +FAIL_ACC_DENIED_STLT_CONN: Incomplete +FAIL_ACC_DENIED_CTRL_CFG: Incomplete +FAIL_ACC_DENIED_COMMAND: Incomplete +FAIL_ACC_DENIED_WATCH: Incomplete +FAIL_ACC_DENIED_SNAPSHOT_DFN: Incomplete +FAIL_ACC_DENIED_SNAPSHOT: Incomplete +FAIL_ACC_DENIED_SNAPSHOT_VLM_DFN: Incomplete +FAIL_ACC_DENIED_FREE_SPACE_MGR: Incomplete +FAIL_ACC_DENIED_KVS: Incomplete +FAIL_ACC_DENIED_RSC_GRP: Incomplete +FAIL_ACC_DENIED_VLM_GRP: Incomplete +FAIL_ACC_DENIED_SNAP_DFN: Incomplete +FAIL_ACC_DENIED_EXT_FILE: Incomplete +FAIL_ACC_DENIED_REMOTE: Incomplete +FAIL_ACC_DENIED_SCHEDULE: Incomplete +FAIL_EXISTS_NODE: Incomplete +FAIL_EXISTS_RSC_DFN: Incomplete +FAIL_EXISTS_RSC: Incomplete +FAIL_EXISTS_VLM_DFN: Incomplete +FAIL_EXISTS_VLM: Incomplete +FAIL_EXISTS_NET_IF: Incomplete +FAIL_EXISTS_NODE_CONN: Incomplete +FAIL_EXISTS_RSC_CONN: Incomplete +FAIL_EXISTS_VLM_CONN: Incomplete +FAIL_EXISTS_STOR_POOL_DFN: Incomplete +FAIL_EXISTS_STOR_POOL: Incomplete +FAIL_EXISTS_STLT_CONN: Incomplete +FAIL_EXISTS_CRYPT_PASSPHRASE: Incomplete +FAIL_EXISTS_WATCH: Incomplete +FAIL_EXISTS_SNAPSHOT_DFN: Incomplete +FAIL_EXISTS_SNAPSHOT: Incomplete +FAIL_EXISTS_EXT_NAME: Incomplete +FAIL_EXISTS_NVME_TARGET_PER_RSC_DFN: Incomplete +FAIL_EXISTS_NVME_INITIATOR_PER_RSC_DFN: Incomplete +FAIL_LOST_STOR_POOL: Incomplete +FAIL_EXISTS_RSC_GRP: Incomplete +FAIL_EXISTS_VLM_GRP: Incomplete +FAIL_EXISTS_SNAPSHOT_SHIPPING: Incomplete +FAIL_EXISTS_EXOS_ENCLOSURE: Incomplete +FAIL_EXISTS_REMOTE: Incomplete +FAIL_EXISTS_SCHEDULE: Incomplete +FAIL_MISSING_PROPS: Incomplete +FAIL_MISSING_PROPS_NETCOM_TYPE: Incomplete +FAIL_MISSING_PROPS_NETCOM_PORT: Incomplete +FAIL_MISSING_NETCOM: Incomplete +FAIL_MISSING_PROPS_NETIF_NAME: Incomplete +FAIL_MISSING_STLT_CONN: Incomplete +FAIL_MISSING_EXT_NAME: Incomplete +FAIL_MISSING_NVME_TARGET: Incomplete +FAIL_NO_STLT_CONN_DEFINED: Incomplete +FAIL_MISSING_EBS_TARGET: Incomplete +FAIL_UUID_NODE: Incomplete +FAIL_UUID_RSC_DFN: Incomplete +FAIL_UUID_RSC: Incomplete +FAIL_UUID_VLM_DFN: Incomplete +FAIL_UUID_VLM: Incomplete +FAIL_UUID_NET_IF: Incomplete +FAIL_UUID_NODE_CONN: Incomplete +FAIL_UUID_RSC_CONN: Incomplete +FAIL_UUID_VLM_CONN: Incomplete +FAIL_UUID_STOR_POOL_DFN: Incomplete +FAIL_UUID_STOR_POOL: Incomplete +FAIL_UUID_KVS: Incomplete +FAIL_POOL_EXHAUSTED_VLM_NR: Incomplete +FAIL_POOL_EXHAUSTED_MINOR_NR: Incomplete +FAIL_POOL_EXHAUSTED_TCP_PORT: Incomplete +FAIL_POOL_EXHAUSTED_NODE_ID: Incomplete +FAIL_POOL_EXHAUSTED_RSC_LAYER_ID: Incomplete +FAIL_POOL_EXHAUSTED_SPECIAL_SATELLTE_TCP_PORT: Incomplete +FAIL_POOL_EXHAUSTED_SNAPSHOT_SHIPPING_TCP_PORT: Incomplete +FAIL_POOL_EXHAUSTED_BACKUP_SHIPPING_TCP_PORT: Incomplete +FAIL_SNAPSHOT_ROLLBACK_IN_PROGRESS: Incomplete +FAIL_SP_MIXING_NOT_ALLOWED: Incomplete +FAIL_EBS_COOLDOWN: Incomplete +FAIL_NOT_ALL_UPTODATE: Incomplete +FAIL_EVACUATING: Incomplete +FAIL_BACKUP_UNKNOWN_CLUSTER: Incomplete +FAIL_DEPENDEND_BACKUP: Incomplete +FAIL_BACKUP_INCOMPATIBLE_VERSION: Incomplete +FAIL_SNAPSHOT_NOT_UPTODATE: Incomplete +FAIL_NOT_ENOUGH_FREE_SPACE: Incomplete +FAIL_ONLY_ONE_ACT_RSC_PER_SHARED_STOR_POOL_ALLOWED: Incomplete +FAIL_CRYPT_INIT: Incomplete +FAIL_SNAPSHOT_SHIPPING_NOT_SUPPORTED: Incomplete +FAIL_SNAPSHOT_SHIPPING_IN_PROGRESS: Incomplete +FAIL_UNDECIDABLE_AUTOPLACMENT: Incomplete +FAIL_PRE_SELECT_SCRIPT_DID_NOT_TERMINATE: Incomplete +FAIL_LINSTOR_MANAGED_SATELLITE_DID_NOT_START_PROPERLY: Incomplete +FAIL_STLT_DOES_NOT_SUPPORT_LAYER: Incomplete +FAIL_STLT_DOES_NOT_SUPPORT_PROVIDER: Incomplete +FAIL_STOR_POOL_CONFIGURATION_ERROR: Incomplete +FAIL_INSUFFICIENT_REPLICA_COUNT: Incomplete +FAIL_RSC_BUSY: Incomplete +FAIL_INSUFFICIENT_PEER_SLOTS: Incomplete +FAIL_SNAPSHOTS_NOT_SUPPORTED: Incomplete +FAIL_NOT_CONNECTED: Incomplete +FAIL_NOT_ENOUGH_NODES: Incomplete +FAIL_IN_USE: Incomplete +FAIL_UNKNOWN_ERROR: Incomplete +FAIL_IMPL_ERROR: Incomplete +WARN_INVLD_OPT_PROP_NETCOM_ENABLED: Incomplete +WARN_NOT_CONNECTED: Incomplete +WARN_STLT_NOT_UPDATED: Incomplete +WARN_NO_STLT_CONN_DEFINED: Incomplete +WARN_DEL_UNSET_PROP: Incomplete +WARN_RSC_ALREADY_DEPLOYED: Incomplete +WARN_RSC_ALREADY_HAS_DISK: Incomplete +WARN_RSC_ALREADY_DISKLESS: Incomplete +WARN_ALL_DISKLESS: Incomplete +WARN_STORAGE_ERROR: Incomplete +WARN_NOT_FOUND_CRYPT_KEY: Incomplete +WARN_STORAGE_KIND_ADDED: Incomplete +WARN_NOT_ENOUGH_NODES_FOR_TIE_BREAKER: Incomplete +WARN_MIXED_PMEM_AND_NON_PMEM: Incomplete +WARN_UNEFFECTIVE_PROP: Incomplete +WARN_INVLD_SNAPSHOT_SHIPPING_PREFIX: Incomplete +WARN_NODE_EVICTED: Incomplete +WARN_RSC_DEACTIVATED: Incomplete +WARN_BACKUP_DL_ONLY: Incomplete +WARN_NOT_EVACUATING: Incomplete +WARN_INVLD_CONF: Incomplete +WARN_STORPOOL_RENAME_NOT_ALLOWED: Incomplete +WARN_VLMDFN_RESIZE_SAME_SIZE: Incomplete +WARN_NOT_FOUND: Incomplete +WARN_DEPRECATED: Incomplete +INFO_NO_RSC_SPAWNED: Incomplete +INFO_NODE_NAME_MISMATCH: Incomplete +INFO_PROP_SET: Incomplete +INFO_TIE_BREAKER_CREATED: Incomplete +INFO_TIE_BREAKER_DELETING: Incomplete +INFO_TIE_BREAKER_TAKEOVER: Incomplete +INFO_PROP_REMOVED: Incomplete +INFO_AUTO_DRBD_PROXY_CREATED: Incomplete +INFO_NOOP: Incomplete +INFO_RSC_ALREADY_EXISTS: Incomplete +INFO_ABORTED_FAILED_SNAPSHOT_ROLLBACK: Incomplete +INFO_RECOVERING_FAILED_SNAPSHOT_ROLLBACK: Incomplete +UNKNOWN_API_CALL: Incomplete +API_CALL_AUTH_REQ: Incomplete +API_CALL_PARSE_ERROR: Incomplete +SUCCESS_SIGN_IN: Incomplete +FAIL_SIGN_IN: Incomplete +FAIL_SIGN_IN_MISSING_CREDENTIALS: Incomplete +API_REPLY: Incomplete +API_END_OF_IMMEDIATE_ANSWERS: Incomplete +API_CRT_NODE: Incomplete +API_CRT_RSC: Incomplete +API_CRT_RSC_DFN: Incomplete +API_CRT_NET_IF: Incomplete +API_CRT_VLM_DFN: Incomplete +API_CRT_SNAPSHOT: Incomplete +API_CRT_SNAPSHOT_MULTI: Incomplete +API_CRT_BACKUP: Incomplete +API_CRT_STOR_POOL_DFN: Incomplete +API_CRT_STOR_POOL: Incomplete +API_CRT_NODE_CONN: Incomplete +API_CRT_RSC_CONN: Incomplete +API_CRT_VLM_CONN: Incomplete +API_AUTO_PLACE_RSC: Incomplete +API_CLONE_RSCDFN: Incomplete +API_CLONE_RSCDFN_STATUS: Incomplete +API_CRT_CRYPT_PASS: Incomplete +API_STATUS_CRYPT_PASS: Incomplete +API_RESTORE_VLM_DFN: Incomplete +API_RESTORE_SNAPSHOT: Incomplete +API_CRT_RSC_GRP: Incomplete +API_CRT_VLM_GRP: Incomplete +API_SPAWN_RSC_DFN: Incomplete +API_ADJUST_RSC_GRP: Incomplete +API_CREATE_DEVICE_POOL: Incomplete +API_MAKE_RSC_AVAIL: Incomplete +API_SET_REMOTE: Incomplete +API_RESTORE_BACKUP: Incomplete +API_ABORT_BACKUP: Incomplete +API_SHIP_BACKUP: Incomplete +API_CRT_SCHEDULE: Incomplete +API_MOD_NODE: Incomplete +API_MOD_NODE_CONN: Incomplete +API_MOD_RSC: Incomplete +API_TOGGLE_DISK: Incomplete +API_MOD_RSC_CONN: Incomplete +API_MOD_RSC_DFN: Incomplete +API_MOD_NET_IF: Incomplete +API_MOD_STOR_POOL: Incomplete +API_MOD_STOR_POOL_DFN: Incomplete +API_MOD_VLM_DFN: Incomplete +API_MOD_VLM_DFN_PASS: Incomplete +API_MOD_VLM: Incomplete +API_MOD_VLM_CONN: Incomplete +API_MOD_SNAPSHOT: Incomplete +API_MOD_CRYPT_PASS: Incomplete +API_ENABLE_DRBD_PROXY: Incomplete +API_DISABLE_DRBD_PROXY: Incomplete +API_MOD_DRBD_PROXY: Incomplete +API_ROLLBACK_SNAPSHOT: Incomplete +API_MOD_KVS: Incomplete +API_MOD_RSC_GRP: Incomplete +API_MOD_VLM_GRP: Incomplete +API_ACTIVATE_RSC: Incomplete +API_DEACTIVATE_RSC: Incomplete +API_MOD_SCHEDULE: Incomplete +API_DEL_NODE: Incomplete +API_DEL_RSC: Incomplete +API_DEL_RSC_DFN: Incomplete +API_DEL_NET_IF: Incomplete +API_DEL_VLM_DFN: Incomplete +API_DEL_STOR_POOL_DFN: Incomplete +API_DEL_STOR_POOL: Incomplete +API_DEL_NODE_CONN: Incomplete +API_DEL_RSC_CONN: Incomplete +API_DEL_VLM_CONN: Incomplete +API_DEL_SNAPSHOT: Incomplete +API_DEL_KVS: Incomplete +API_DEL_RSC_GRP: Incomplete +API_DEL_VLM_GRP: Incomplete +API_DEL_BACKUP: Incomplete +API_LOST_NODE: Incomplete +API_LOST_STOR_POOL: Incomplete +API_DEL_SCHEDULE: Incomplete +API_SIGN_IN: Incomplete +API_VERSION: Incomplete +API_CRT_DBG_CNSL: Incomplete +API_DSTR_DBG_CNSL: Incomplete +API_CONTROL_CTRL: Incomplete +API_CMD_SHUTDOWN: Incomplete +API_NODE_RECONNECT: Incomplete +API_NODE_RESTORE: Incomplete +API_NODE_EVICT: Incomplete +API_NODE_EVACUATE: Incomplete +API_LST_NODE: Incomplete +API_LST_NODE_CONN: Incomplete +API_LST_RSC: Incomplete +API_LST_RSC_DFN: Incomplete +API_LST_NET_IF: Incomplete +API_LST_VLM_DFN: Incomplete +API_LST_VLM: Incomplete +API_LST_SNAPSHOT_DFN: Incomplete +API_LST_STOR_POOL: Incomplete +API_LST_STOR_POOL_DFN: Incomplete +API_LST_ERROR_REPORTS: Incomplete +API_REQ_ERROR_REPORTS: Incomplete +API_DEL_ERROR_REPORT: Incomplete +API_DEL_ERROR_REPORTS: Incomplete +API_REQ_SOS_REPORT: Incomplete +API_REQ_RSC_CONN_LIST: Incomplete +API_LST_RSC_CONN: Incomplete +API_LST_KVS: Incomplete +API_LST_RSC_GRP: Incomplete +API_LST_VLM_GRP: Incomplete +API_LST_PHYS_STOR: Incomplete +API_LST_BACKUPS: Incomplete +API_BACKUP_INFO: Incomplete +API_LST_PROPS_INFO: Incomplete +API_LST_REMOTE: Incomplete +API_LST_EXT_FILES: Incomplete +API_NODE_STATS: Incomplete +API_RSC_GRP_STATS: Incomplete +API_RSC_DFN_STATS: Incomplete +API_RSC_STATS: Incomplete +API_STOR_POOL_STATS: Incomplete +API_ERR_REPORT_STATS: Incomplete +API_LST_SCHEDULE: Incomplete +API_LST_QUEUE: Incomplete +API_QRY_MAX_VLM_SIZE: Incomplete +API_RSP_MAX_VLM_SIZE: Incomplete +API_QRY_SIZE_INFO: Incomplete +API_QRY_ALL_SIZE_INFO: Incomplete +API_RSCDFN_SYNC_STATUS: Incomplete +API_CHECK_EXT_FILE: Incomplete +API_CRT_WATCH: Incomplete +API_DEL_WATCH: Incomplete +API_EVENT: Incomplete +API_RPT_SPC: Incomplete +API_PING: Incomplete +API_PONG: Incomplete +API_MOD_INF: Incomplete +API_VSN_INF: Incomplete +API_SET_CTRL_PROP: Incomplete +API_DEL_CTRL_PROP: Incomplete +API_LST_CTRL_PROPS: Incomplete +API_ENTER_CRYPT_PASS: Incomplete +API_SET_EXT_FILE: Incomplete +API_DEL_EXT_FILE: Incomplete +API_DEPLOY_EXT_FILE: Incomplete +API_UNDEPLOY_EXT_FILE: Incomplete +API_DB_EXPORT: Incomplete +KEY_UUID: Incomplete +KEY_DRBD_CURRENT_GI: Incomplete +KEY_DMSTATS: Incomplete +KEY_QUORUM_SET_BY: Incomplete +KEY_DRBD_AUTO_QUORUM: Incomplete +KEY_DRBD_AUTO_ADD_QUORUM_TIEBREAKER: Incomplete +KEY_MINOR_NR_AUTO_RANGE: Incomplete +KEY_DRBD_AUTO_DISKFUL: Incomplete +KEY_DRBD_AUTO_DISKFUL_ALLOW_CLEANUP: Incomplete +KEY_DRBD_DISABLE_AUTO_RESYNC_AFTER: Incomplete +KEY_DRBD_DISABLE_AUTO_VERIFY_ALGO: Incomplete +KEY_DRBD_AUTO_VERIFY_ALGO_ALLOWED_USER: Incomplete +KEY_DRBD_AUTO_RS_DISCARD_GRANULARITY: Incomplete +KEY_FORCE_INITIAL_SYNC: Incomplete +KEY_DRBD_SKIP_DISK: Incomplete +KEY_DRBD_EXACT_SIZE: Incomplete +KEY_NODE: Incomplete +KEY_1ST_NODE: Incomplete +KEY_2ND_NODE: Incomplete +KEY_CUR_STLT_CONN_NAME: Incomplete +KEY_PASSPHRASE: Incomplete +KEY_RSC_DFN: Incomplete +KEY_RSC_GRP: Incomplete +KEY_TCP_PORT_AUTO_RANGE: Incomplete +KEY_PEER_SLOTS_NEW_RESOURCE: Incomplete +KEY_PEER_SLOTS: Incomplete +KEY_RSC_ROLLBACK_TARGET: Incomplete +KEY_RSC_MIGRATE_FROM: Incomplete +KEY_RSC_ALLOW_MIXING_DEVICE_KIND: Incomplete +KEY_RSC_DISKFUL_BY: Incomplete +KEY_BALANCE_RESOURCES_ENABLED: Incomplete +KEY_BALANCE_RESOURCES_INTERVAL: Incomplete +KEY_BALANCE_RESOURCES_GRACE_PERIOD: Incomplete +KEY_VLM_GRP: Incomplete +KEY_VLM_NR: Incomplete +KEY_VLM_RESTORE_FROM_RESOURCE: Incomplete +KEY_VLM_RESTORE_FROM_SNAPSHOT: Incomplete +KEY_SEARCH_DOMAIN: Incomplete +KEY_TR_TYPE: Incomplete +KEY_SNAPSHOT: Incomplete +KEY_SNAPSHOT_DFN_SEQUENCE_NUMBER: Incomplete +KEY_PORT: Incomplete +KEY_DISABLE_HTTP_METRICS: Incomplete +KEY_WRITECACHE_BLOCKSIZE: Incomplete +KEY_WRITECACHE_POOL_NAME: Incomplete +KEY_WRITECACHE_SIZE: Incomplete +KEY_WRITECACHE_OPTION_HIGH_WATERMARK: Incomplete +KEY_WRITECACHE_OPTION_LOW_WATERMARK: Incomplete +KEY_WRITECACHE_OPTION_START_SECTOR: Incomplete +KEY_WRITECACHE_OPTION_WRITEBACK_JOBS: Incomplete +KEY_WRITECACHE_OPTION_AUTOCOMMIT_BLOCKS: Incomplete +KEY_WRITECACHE_OPTION_AUTOCOMMIT_TIME: Incomplete +KEY_WRITECACHE_OPTION_FUA: Incomplete +KEY_WRITECACHE_OPTION_ADDITIONAL: Incomplete +KEY_CACHE_OPERATING_MODE: Incomplete +KEY_CACHE_META_POOL_NAME: Incomplete +KEY_CACHE_CACHE_POOL_NAME: Incomplete +KEY_CACHE_META_SIZE: Incomplete +KEY_CACHE_CACHE_SIZE: Incomplete +KEY_CACHE_BLOCK_SIZE: Incomplete +KEY_CACHE_POLICY: Incomplete +KEY_UPDATE_CACHE_INTERVAL: Incomplete +KEY_BCACHE_POOL_NAME: Incomplete +KEY_BCACHE_SIZE: Incomplete +KEY_BCACHE_BLOCKSIZE: Incomplete +KEY_BCACHE_BUCKETSIZE: Incomplete +KEY_BCACHE_DATA_OFFSET: Incomplete +KEY_BCACHE_WRITEBACK: Incomplete +KEY_BCACHE_DISCARD: Incomplete +KEY_BCACHE_CACHE_REPLACEMENT_POLICY: Incomplete +KEY_AUTOPLACE_STRAT_WEIGHT_MAX_FREESPACE: Incomplete +KEY_AUTOPLACE_STRAT_WEIGHT_MIN_RESERVED_SPACE: Incomplete +KEY_AUTOPLACE_STRAT_WEIGHT_MIN_RSC_COUNT: Incomplete +KEY_AUTOPLACE_PRE_SELECT_FILE_NAME: Incomplete +KEY_AUTOPLACE_PRE_SELECT_SCRIPT_TIMEOUT: Incomplete +KEY_AUTOPLACE_MAX_THROUGHPUT: Incomplete +KEY_SITE: Incomplete +KEY_AUTOPLACE_ALLOW_TARGET: Incomplete +KEY_AUTO_EVICT_MIN_REPLICA_COUNT: Incomplete +KEY_AUTO_EVICT_AFTER_TIME: Incomplete +KEY_AUTO_EVICT_MAX_DISCONNECTED_NODES: Incomplete +KEY_AUTO_EVICT_ALLOW_EVICTION: Incomplete +KEY_TARGET_NODE: Incomplete +KEY_SOURCE_NODE: Incomplete +KEY_RUN_EVERY: Incomplete +KEY_AUTO_SNAPSHOT_PREFIX: Incomplete +KEY_KEEP: Incomplete +KEY_AUTO_SNAPSHOT_NEXT_ID: Incomplete +KEY_TCP_PORT_RANGE: Incomplete +KEY_BACKUP_TIMEOUT: Incomplete +KEY_BACKUP_S3_SUFFIX: Incomplete +KEY_REMOTE: Incomplete +KEY_MAX_CONCURRENT_BACKUPS_PER_NODE: Incomplete +VAL_NODE_UNDECIDED: Incomplete +KEY_ALLOW_FORCE_RESTORE: Incomplete +KEY_RECV_TIMEOUT_IN_MS: Incomplete +KEY_BACKUP_L2L_SKIP_WAIT_FOR_START: Incomplete +KEY_USE_ZFS_CLONE: Incomplete +KEY_NETCOM_ENABLED: Incomplete +KEY_NETCOM_BIND_ADDRESS: Incomplete +KEY_NETCOM_KEY_PASSWD: Incomplete +KEY_NETCOM_KEY_STORE: Incomplete +KEY_NETCOM_KEY_STORE_PASSWD: Incomplete +KEY_NETCOM_PORT: Incomplete +KEY_NETCOM_SSL_PROTOCOL: Incomplete +KEY_NETCOM_TRUST_STORE: Incomplete +KEY_NETCOM_TRUST_STORE_PASSWD: Incomplete +KEY_NETCOM_TYPE: Incomplete +NAMESPC_NETCOM: Incomplete +NAMESPC_DFLT: Incomplete +NAMESPC_LOGGING: Incomplete +NAMESPC_ALLOC: Incomplete +NAMESPC_NETIF: Incomplete +NAMESPC_STLT: Incomplete +NAMESPC_NODE: Incomplete +NAMESPC_STORAGE_DRIVER: Incomplete +NAMESPC_DRBD_PROXY: Incomplete +NAMESPC_AUXILIARY: Incomplete +NAMESPC_DRBD_OPTIONS: Incomplete +NAMESPC_DRBD_NET_OPTIONS: Incomplete +NAMESPC_DRBD_DISK_OPTIONS: Incomplete +NAMESPC_DRBD_RESOURCE_OPTIONS: Incomplete +NAMESPC_DRBD_PEER_DEVICE_OPTIONS: Incomplete +NAMESPC_DRBD_PROXY_OPTIONS: Incomplete +NAMESPC_DRBD_PROXY_COMPRESSION_OPTIONS: Incomplete +NAMESPC_DRBD_HANDLER_OPTIONS: Incomplete +NAMESPC_CONNECTION_PATHS: Incomplete +NAMESPC_INTERNAL_DRBD: Incomplete +NAMESPC_REST: Incomplete +NAMESPC_FILESYSTEM: Incomplete +NAMESPC_NVME: Incomplete +NAMESPC_SYS_FS: Incomplete +NAMESPC_WRITECACHE: Incomplete +NAMESPC_WRITECACHE_OPTIONS: Incomplete +NAMESPC_CACHE: Incomplete +NAMESPC_CACHE_FEATURES: Incomplete +NAMESPC_CACHE_POLICY_ARGS: Incomplete +NAMESPC_BCACHE: Incomplete +NAMESPC_AUTOPLACER: Incomplete +NAMESPC_AUTOPLACER_WEIGHTS: Incomplete +NAMESPC_SNAPSHOT_SHIPPING: Incomplete +NAMESPC_AUTO_SNAPSHOT: Incomplete +NAMESPC_STLT_DEV_SYMLINKS: Incomplete +NAMESPC_BACKUP_SHIPPING: Incomplete +NAMESPC_CLUSTER: Incomplete +NAMESPC_CLUSTER_REMOTE: Incomplete +NAMESPC_SED: Incomplete +NAMESPC_EBS: Incomplete +NAMESPC_TAGS: Incomplete +NAMESPC_ENCRYPTION: Incomplete +NAMESPC_ZFS: Incomplete +KEY_STOR_POOL_DFN: Incomplete +KEY_STOR_POOL_NAME: Incomplete +KEY_STOR_POOL_DRBD_META_NAME: Incomplete +KEY_STOR_POOL_VOLUME_GROUP: Incomplete +KEY_STOR_POOL_LVCREATE_TYPE: Incomplete +KEY_STOR_POOL_LVCREATE_OPTIONS: Incomplete +KEY_STOR_POOL_LVCREATE_SNAPSHOT_OPTIONS: Incomplete +KEY_STOR_POOL_THIN_POOL: Incomplete +KEY_STOR_POOL_LVM_SIZES_CACHE_TIME: Incomplete +KEY_STOR_POOL_ZPOOL: Incomplete +KEY_STOR_POOL_ZPOOLTHIN: Incomplete +KEY_STOR_POOL_ZFS_CREATE_OPTIONS: Incomplete +KEY_STOR_POOL_ZFS_SNAPSHOT_OPTIONS: Incomplete +KEY_STOR_POOL_FILE_DIRECTORY: Incomplete +KEY_STOR_POOL_PREF_NIC: Incomplete +KEY_STOR_POOL_CRYPT_PASSWD: Incomplete +KEY_STOR_POOL_OVERRIDE_VLM_ID: Incomplete +KEY_STOR_POOL_MAX_OVERSUBSCRIPTION_RATIO: Incomplete +KEY_STOR_POOL_MAX_FREE_CAPACITY_OVERSUBSCRIPTION_RATIO: Incomplete +KEY_STOR_POOL_MAX_TOTAL_CAPACITY_OVERSUBSCRIPTION_RATIO: Incomplete +KEY_STOR_POOL_WAIT_TIMEOUT_AFTER_CREATE: Incomplete +KEY_SPEC_STLT_PORT_AUTO_RANGE: Incomplete +KEY_STOR_POOL_REMOTE_SPDK_API_HOST: Incomplete +KEY_STOR_POOL_REMOTE_SPDK_API_PORT: Incomplete +KEY_STOR_POOL_REMOTE_SPDK_API_USER_NAME: Incomplete +KEY_STOR_POOL_REMOTE_SPDK_API_USER_PW: Incomplete +KEY_STOR_POOL_REMOTE_SPDK_API_USER_NAME_ENV: Incomplete +KEY_STOR_POOL_REMOTE_SPDK_API_USER_PW_ENV: Incomplete +KEY_EBS_VOLUME_TYPE: Incomplete +KEY_PREF_NIC: Incomplete +KEY_ZFS_ROLLBACK_STRATEGY: Incomplete +KEY_ZFS_DELETE_STRATEGY: Incomplete +KEY_STOR_POOL_SUPPORTS_SNAPSHOTS: Incomplete +KEY_STOR_POOL_PROVISIONING: Incomplete +KEY_STOR_POOL_ALLOCATION_UNIT: Incomplete +VAL_STOR_POOL_PROVISIONING_FAT: Incomplete +VAL_STOR_POOL_PROVISIONING_THIN: Incomplete +VAL_STOR_POOL_DRBD_META_INTERNAL: Incomplete +VAL_STOR_POOL_SPACE_NOT_FOUND: Incomplete +VAL_STOR_POOL_SPACE_ENOUGH: Incomplete +VAL_STOR_POOL_ZFS_ROLLBACK_STRAT_ROLLBACK: Incomplete +VAL_STOR_POOL_ZFS_ROLLBACK_STRAT_CLONE: Incomplete +VAL_STOR_POOL_ZFS_ROLLBACK_STRAT_DYNAMIC: Incomplete +VAL_STOR_POOL_ZFS_DELETE_STRAT_DESTROY: Incomplete +VAL_STOR_POOL_ZFS_DELETE_STRAT_RENAME: Incomplete +VAL_STOR_POOL_ZFS_DELETE_STRAT_DYNAMIC: Incomplete +KEY_DRBD_PROXY_COMPRESSION_TYPE: Incomplete +KEY_DRBD_PROXY_AUTO_ENABLE: Incomplete +KEY_FS_TYPE: Incomplete +KEY_FS_MKFSPARAMETERS: Incomplete +KEY_FS_USER: Incomplete +KEY_FS_GROUP: Incomplete +VAL_FS_TYPE_EXT4: Incomplete +VAL_FS_TYPE_XFS: Incomplete +KEY_SYS_FS_BLKIO_THROTTLE_READ: Incomplete +KEY_SYS_FS_BLKIO_THROTTLE_WRITE: Incomplete +KEY_SYS_FS_BLKIO_THROTTLE_READ_IOPS: Incomplete +KEY_SYS_FS_BLKIO_THROTTLE_WRITE_IOPS: Incomplete +VAL_NETCOM_TYPE_SSL: Incomplete +VAL_NETCOM_TYPE_PLAIN: Incomplete +VAL_SSL_PROTO_TLSv1: Incomplete +VAL_DRBD_PROXY_COMPRESSION_NONE: Incomplete +VAL_DRBD_PROXY_COMPRESSION_ZSTD: Incomplete +VAL_DRBD_PROXY_COMPRESSION_ZLIB: Incomplete +VAL_DRBD_PROXY_COMPRESSION_LZMA: Incomplete +VAL_DRBD_PROXY_COMPRESSION_LZ4: Incomplete +VAL_DRBD_AUTO_QUORUM_DISABLED: Incomplete +VAL_DRBD_AUTO_QUORUM_IO_ERROR: Incomplete +VAL_DRBD_AUTO_QUORUM_SUSPEND_IO: Incomplete +VAL_NODE_TYPE_CTRL: Incomplete +VAL_NODE_TYPE_STLT: Incomplete +VAL_NODE_TYPE_CMBD: Incomplete +VAL_NODE_TYPE_AUX: Incomplete +VAL_NODE_TYPE_REMOTE_SPDK: Incomplete +VAL_WRITECACHE_FUA_ON: Incomplete +VAL_WRITECACHE_FUA_OFF: Incomplete +VAL_NETIF_TYPE_IP: Incomplete +VAL_NETIF_TYPE_RDMA: Incomplete +VAL_NETIF_TYPE_ROCE: Incomplete +KEY_SEC_IDENTITY: Incomplete +KEY_SEC_ROLE: Incomplete +KEY_SEC_TYPE: Incomplete +KEY_SEC_DOMAIN: Incomplete +KEY_SEC_PASSWORD: Incomplete +KEY_POOL_NAME: Incomplete +KEY_EXT_CMD_WAIT_TO: Incomplete +KEY_EXT_FILE: Incomplete +DFLT_CTRL_PORT_SSL: Incomplete +DFLT_CTRL_PORT_PLAIN: Incomplete +DFLT_STLT_PORT_SSL: Incomplete +DFLT_STLT_PORT_PLAIN: Incomplete +VAL_TRUE: Incomplete +VAL_FALSE: Incomplete +VAL_YES: Incomplete +VAL_NO: Incomplete +FLAG_CLEAN: Incomplete +FLAG_EVICTED: Incomplete +FLAG_EVACUATE: Incomplete +FLAG_DELETE: Incomplete +FLAG_DRBD_DELETE: Incomplete +FLAG_DISKLESS: Incomplete +FLAG_QIGNORE: Incomplete +FLAG_ENCRYPTED: Incomplete +FLAG_GROSS_SIZE: Incomplete +FLAG_SUCCESSFUL: Incomplete +FLAG_SHIPPING: Incomplete +FLAG_SHIPPING_CLEANUP: Incomplete +FLAG_SHIPPING_ABORT: Incomplete +FLAG_SHIPPED: Incomplete +FLAG_AUTO_SNAPSHOT: Incomplete +FLAG_BACKUP: Incomplete +FLAG_RESTORE_BACKUP_ON_SUCCESS: Incomplete +FLAG_BACKUP_TARGET: Incomplete +FLAG_BACKUP_SOURCE: Incomplete +FLAG_FAILED_DEPLOYMENT: Incomplete +FLAG_FAILED_DISCONNECT: Incomplete +FLAG_RESIZE: Incomplete +FLAG_DISK_ADDING: Incomplete +FLAG_DISK_ADD_REQUESTED: Incomplete +FLAG_DISK_REMOVING: Incomplete +FLAG_DISK_REMOVE_REQUESTED: Incomplete +FLAG_TIE_BREAKER: Incomplete +FLAG_DRBD_DISKLESS: Incomplete +FLAG_NVME_INITIATOR: Incomplete +FLAG_RSC_INACTIVE: Incomplete +FLAG_EBS_INITIATOR: Incomplete +FLAG_CLONING: Incomplete +FLAG_FAILED: Incomplete +FLAG_RESTORE_TARGET: Incomplete + +class DeviceLayerKind(Enum): + DRBD = 'DRBD' + LUKS = 'LUKS' + STORAGE = 'STORAGE' + NVME = 'NVME' + WRITECACHE = 'WRITECACHE' + CACHE = 'CACHE' + BCACHE = 'BCACHE' + +class ConnectionStatus(Enum): + OFFLINE = 0 + CONNECTED = 1 + ONLINE = 2 + VERSION_MISMATCH = 3 + FULL_SYNC_FAILED = 4 + AUTHENTICATION_ERROR = 5 + UNKNOWN = 6 + HOSTNAME_MISMATCH = 7 + OTHER_CONTROLLER = 8 + AUTHENTICATED = 9 + NO_STLT_CONN = 10 + MISSING_EXT_TOOLS = 11 + DUPLICATE_UNAME = 12 + +class CloneStatus(Enum): + FAILED = 'FAILED' + CLONING = 'CLONING' + COMPLETE = 'COMPLETE' + +DEFAULT_NETIF: Incomplete +DFLT_SNAPSHOT_SHIPPING_PREFIX: Incomplete +DFLT_AUTO_SNAPSHOT_KEEP: Incomplete +DFLT_SHIPPED_SNAPSHOT_KEEP: Incomplete diff --git a/stubs/linstor/size_calc.pyi b/stubs/linstor/size_calc.pyi new file mode 100644 index 0000000..0642eda --- /dev/null +++ b/stubs/linstor/size_calc.pyi @@ -0,0 +1,38 @@ +from .errors import LinstorError as LinstorError +from _typeshed import Incomplete + +class SizeCalc: + _base_2: int + _base_10: int + UNIT_B: Incomplete + UNIT_S: Incomplete + UNIT_KiB: Incomplete + UNIT_MiB: Incomplete + UNIT_GiB: Incomplete + UNIT_TiB: Incomplete + UNIT_PiB: Incomplete + UNIT_EiB: Incomplete + UNIT_ZiB: Incomplete + UNIT_YiB: Incomplete + UNIT_kB: Incomplete + UNIT_MB: Incomplete + UNIT_GB: Incomplete + UNIT_TB: Incomplete + UNIT_PB: Incomplete + UNIT_EB: Incomplete + UNIT_ZB: Incomplete + UNIT_YB: Incomplete + UNITS_MAP: Incomplete + UNITS_LIST_STR: Incomplete + @classmethod + def unit_to_str(cls, unit): ... + @classmethod + def parse_unit(cls, value): ... + @classmethod + def auto_convert(cls, value, unit_to): ... + @classmethod + def convert(cls, size, unit_in, unit_out): ... + @classmethod + def convert_round_up(cls, size, unit_in, unit_out): ... + @classmethod + def approximate_size_string(cls, size_kib): ... diff --git a/stubs/linstor/version.pyi b/stubs/linstor/version.pyi new file mode 100644 index 0000000..3acee93 --- /dev/null +++ b/stubs/linstor/version.pyi @@ -0,0 +1 @@ +VERSION: str From 7a054e5d141f6a5fd136467c2ecb192ef39d253b Mon Sep 17 00:00:00 2001 From: Ronan Abhamon Date: Mon, 16 Mar 2026 22:52:29 +0100 Subject: [PATCH 06/11] chore: add github workflow Signed-off-by: Ronan Abhamon --- .github/actions/setup-python-env/action.yml | 14 ++++++++++ .github/workflows/code-checkers.yml | 30 +++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 .github/actions/setup-python-env/action.yml create mode 100644 .github/workflows/code-checkers.yml diff --git a/.github/actions/setup-python-env/action.yml b/.github/actions/setup-python-env/action.yml new file mode 100644 index 0000000..cbfe506 --- /dev/null +++ b/.github/actions/setup-python-env/action.yml @@ -0,0 +1,14 @@ +name: Setup Python env +description: Setup Python and pip +runs: + using: "composite" + steps: + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: "3.14" + cache: "pip" + + - name: Install dependencies + shell: bash + run: pip install --upgrade pip diff --git a/.github/workflows/code-checkers.yml b/.github/workflows/code-checkers.yml new file mode 100644 index 0000000..19e5d41 --- /dev/null +++ b/.github/workflows/code-checkers.yml @@ -0,0 +1,30 @@ +name: Code checkers + +on: [push, pull_request] + +jobs: + linting: + name: Linting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup-python-env + + - name: Install Ruff + run: pip install ruff + + - name: Ruff check + run: ruff check + + static-type-checking: + name: Static type checking + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup-python-env + + - name: Install Mypy + run: pip install mypy + + - name: Mypy check + run: mypy From 2a633ebe2d1522ce9a3e9011028c37695bad16f8 Mon Sep 17 00:00:00 2001 From: Ronan Abhamon Date: Mon, 16 Mar 2026 23:53:30 +0100 Subject: [PATCH 07/11] feat(core): add JSON-RPC helpers Signed-off-by: Ronan Abhamon --- .github/workflows/code-checkers.yml | 13 + pyproject.toml | 7 +- src/xcp_storage/utils/__init__.py | 0 src/xcp_storage/utils/exception.py | 24 + src/xcp_storage/utils/json/__init__.py | 34 ++ src/xcp_storage/utils/json/rpc/__init__.py | 597 +++++++++++++++++++++ src/xcp_storage/utils/reflection.py | 26 + tests/utils/test_json_rpc.py | 229 ++++++++ 8 files changed, 928 insertions(+), 2 deletions(-) create mode 100644 src/xcp_storage/utils/__init__.py create mode 100644 src/xcp_storage/utils/exception.py create mode 100644 src/xcp_storage/utils/json/__init__.py create mode 100644 src/xcp_storage/utils/json/rpc/__init__.py create mode 100644 src/xcp_storage/utils/reflection.py create mode 100644 tests/utils/test_json_rpc.py diff --git a/.github/workflows/code-checkers.yml b/.github/workflows/code-checkers.yml index 19e5d41..d0a35bf 100644 --- a/.github/workflows/code-checkers.yml +++ b/.github/workflows/code-checkers.yml @@ -28,3 +28,16 @@ jobs: - name: Mypy check run: mypy + + tests: + name: Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup-python-env + + - name: Install Pytest + run: pip install pytest + + - name: Pytest check + run: pytest diff --git a/pyproject.toml b/pyproject.toml index a0c69a7..debb7b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,8 @@ [tool.mypy] mypy_path = "stubs" files = [ - "src" + "src", + "tests" ] disallow_untyped_defs = true @@ -32,7 +33,8 @@ disallow_untyped_defs = false [tool.ruff] include = [ - "src/**/*.py" + "src/**/*.py", + "tests/**/*.py" ] line-length = 120 @@ -116,3 +118,4 @@ ban-relative-imports = "all" [tool.pytest.ini_options] pythonpath = ["src", "stubs"] +testpaths = ["tests"] diff --git a/src/xcp_storage/utils/__init__.py b/src/xcp_storage/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/xcp_storage/utils/exception.py b/src/xcp_storage/utils/exception.py new file mode 100644 index 0000000..ff65269 --- /dev/null +++ b/src/xcp_storage/utils/exception.py @@ -0,0 +1,24 @@ +# Copyright (C) 2026 Vates SAS +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# ============================================================================== + +def stringify_exception(e: Exception) -> str: + try: + return str(e) + except Exception: + try: + return f"Exception of type `{e.__class__.__name__}`." + except Exception: + return "Unknown Exception." diff --git a/src/xcp_storage/utils/json/__init__.py b/src/xcp_storage/utils/json/__init__.py new file mode 100644 index 0000000..7621a5e --- /dev/null +++ b/src/xcp_storage/utils/json/__init__.py @@ -0,0 +1,34 @@ +# Copyright (C) 2026 Vates SAS +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from xcp_storage.typing import ( + Dict, + List, + Union, +) + +# ============================================================================== + +JsonValue = Union[ + str, + int, + float, + bool, + None, + List["JsonValue"], + Dict[str, "JsonValue"] +] + +JsonDict = Dict[str, JsonValue] +JsonList = List[JsonValue] diff --git a/src/xcp_storage/utils/json/rpc/__init__.py b/src/xcp_storage/utils/json/rpc/__init__.py new file mode 100644 index 0000000..09ce08e --- /dev/null +++ b/src/xcp_storage/utils/json/rpc/__init__.py @@ -0,0 +1,597 @@ +# Copyright (C) 2026 Vates SAS +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from abc import ABC, abstractmethod +import contextlib +import inspect +import json +from pathlib import PurePath + +from xcp_storage.utils.exception import stringify_exception +from xcp_storage.utils.json import JsonDict, JsonList, JsonValue +from xcp_storage.utils.reflection import is_callable_with + +from xcp_storage.typing import ( + Any, + Callable, + cast, + Dict, + List, + Optional, + override, + ParamSpec, + TypeVar, + Union, +) + +P = ParamSpec("P") +T = TypeVar("T") + +# ============================================================================== + +JSON_RPC_VERSION = "2.0" + +# ------------------------------------------------------------------------------ +# Base error. +# ------------------------------------------------------------------------------ + +class JsonRpcError(Exception): + def __init__(self, message: str) -> None: + super().__init__(message) + +# ------------------------------------------------------------------------------ +# Request errors. +# ------------------------------------------------------------------------------ + +class JsonRpcRequestError(JsonRpcError): + pass + +# ------------------------------------------------------------------------------ +# Response errors. +# ------------------------------------------------------------------------------ + +class JsonRpcResponseError(JsonRpcError): + def __init__(self, code: int, message: str, data: JsonValue = None) -> None: + super().__init__(message) + self._payload: Optional[JsonDict] = None + self.code = code + self.message = message + self.data = data + + @property + def payload(self) -> JsonDict: + if self._payload is None: + self._payload = { + "code": self.code, + "message": self.message + } + if self.data: + self._payload["data"] = self.data + return self._payload + + @classmethod + def from_payload(cls, payload: JsonValue) -> "JsonRpcResponseError": + if not isinstance(payload, dict): + raise JsonRpcResponseClientError("Invalid response error, not a dict.") + + try: + code = payload["code"] + if not isinstance(code, int): + raise JsonRpcResponseClientError("Invalid response error. `code` is not an integer.") + + message = payload["message"] + if not isinstance(message, str): + raise JsonRpcResponseClientError("Invalid response error. `message` is not a string.") + + data = payload.get("data") + except KeyError as e: + raise JsonRpcResponseClientError(f"Invalid response error. Missing member: `{e}`.") from None + + error_type = _CODE_TO_ERROR_TYPE.get(code) + if not error_type: + return JsonRpcResponseError(code, message, data) + + error = error_type(data) + error.message = message + return error + +# ------------------------------------------------------------------------------ + +class JsonRpcResponseParseError(JsonRpcResponseError): + def __init__(self, data: JsonValue = None) -> None: + super().__init__(-32700, "Parse error", data) + +class JsonRpcResponseInvalidRequestError(JsonRpcResponseError): + def __init__(self, data: JsonValue = None) -> None: + super().__init__(-32600, "Invalid Request", data) + +class JsonRpcResponseMethodNotFoundError(JsonRpcResponseError): + def __init__(self, data: JsonValue = None) -> None: + super().__init__(-32601, "Method not found", data) + +class JsonRpcResponseInvalidParamsError(JsonRpcResponseError): + def __init__(self, data: JsonValue = None) -> None: + super().__init__(-32602, "Invalid params", data) + +class JsonRpcResponseInternalError(JsonRpcResponseError): + def __init__(self, data: JsonValue = None) -> None: + super().__init__(-32603, "Internal error", data) + +class JsonRpcResponseServerError(JsonRpcResponseError): + def __init__(self, data: JsonValue = None) -> None: + super().__init__(-32000, "Server error", data) + +# ------------------------------------------------------------------------------ + +class JsonRpcResponseClientError(JsonRpcResponseError): + def __init__(self, message: str) -> None: + super().__init__(-33000, message) + +# ------------------------------------------------------------------------------ + +_CODE_TO_ERROR_TYPE = { + -32700: JsonRpcResponseParseError, + -32600: JsonRpcResponseInvalidRequestError, + -32601: JsonRpcResponseMethodNotFoundError, + -32602: JsonRpcResponseInvalidParamsError, + -32603: JsonRpcResponseInternalError, + -32000: JsonRpcResponseServerError +} + +# ------------------------------------------------------------------------------ +# Base Response/Request object. +# ------------------------------------------------------------------------------ + +class JsonRpcObject(ABC): + @abstractmethod + def to_json(self) -> str: + pass + +# ------------------------------------------------------------------------------ +# Request. +# ------------------------------------------------------------------------------ + +class JsonRpcRequest(JsonRpcObject): + PAYLOAD_MEMBERS = {"jsonrpc", "method", "params", "id"} + PAYLOAD_REQUIRED_MEMBERS = {"jsonrpc", "method"} + + def __init__( + self, + identifier: Union[str, int, None] = None, + method: str = "", + params: Union[JsonList, JsonDict, None] = None + ) -> None: + self._payload: JsonDict = {} + self._json_payload: Optional[str] = None + self.identifier = identifier + self.method = method + self.params = params + + def _invalidate(self) -> None: + self._modified = True + self._json_payload = None + + @property + def identifier(self) -> Union[str, int, None]: + return self._identifier + + @identifier.setter + def identifier(self, value: Union[str, int, None]) -> None: + self._identifier = value + self._invalidate() + + @property + def method(self) -> str: + return self._method + + @method.setter + def method(self, value: str) -> None: + if value.startswith("rpc."): + raise JsonRpcRequestError("Cannot use reserved RPC prefix as method name.") + self._method = value + self._invalidate() + + @property + def params(self) -> Union[JsonList, JsonDict, None]: + return self._params + + @params.setter + def params(self, value: Union[JsonList, JsonDict, None]) -> None: + self._params = value + self._invalidate() + + @property + def args(self) -> List: + return self._params if isinstance(self._params, list) else [] + + @args.setter + def args(self, value: List[JsonValue]) -> None: + self._params = value + self._invalidate() + + @property + def kwargs(self) -> JsonDict: + return self._params if isinstance(self._params, dict) else {} + + @kwargs.setter + def kwargs(self, value: JsonDict) -> None: + self._params = value + self._invalidate() + + @property + def payload(self) -> JsonDict: + if not self._modified: + return self._payload + + if not self._method: + raise JsonRpcRequestError("Method is missing.") + + self._payload = { + "jsonrpc": JSON_RPC_VERSION, + "method": self._method + } + if self._params: + self._payload["params"] = self._params + if self._identifier: + self._payload["id"] = self._identifier + + self._modified = False + return self._payload + + @override + def to_json(self) -> str: + if self._json_payload is None: + self._json_payload = json.dumps(self.payload) + return self._json_payload + + @classmethod + def from_json(cls, request_str: str) -> Union["JsonRpcRequest", "JsonRpcBatchRequest"]: + try: + payload = json.loads(request_str) + except (TypeError, ValueError): + raise JsonRpcRequestError("Invalid request. Parse error.") from None + return cls.from_payload(payload) + + @classmethod + def from_payload(cls, payload: JsonValue) -> Union["JsonRpcRequest", "JsonRpcBatchRequest"]: + if not isinstance(payload, list): + return cls._from_request_payload(payload) + + if not payload: + raise JsonRpcRequestError("Invalid request. Empty batch.") + + payloads = payload + return JsonRpcBatchRequest([cls._from_request_payload(payload) for payload in payloads]) + + @classmethod + def _from_request_payload(cls, payload: JsonValue) -> "JsonRpcRequest": + if not isinstance(payload, dict): + raise JsonRpcRequestError("Invalid request. Payload must be an object or a list of objects.") + + payload = cast(JsonDict, payload) + payload_keys = set(payload.keys()) + + missing_members = cls.PAYLOAD_REQUIRED_MEMBERS - payload_keys + unknown_members = payload_keys - cls.PAYLOAD_MEMBERS + if missing_members or unknown_members: + raise JsonRpcRequestError( + f"Invalid request. Missing members: `{missing_members or '{}'}`, " + f"unknown members: `{unknown_members or '{}'}`." + ) + + request = JsonRpcRequest() + + try: + identifier = payload.get("id") + if identifier is not None and not isinstance(identifier, (int, str)): + raise JsonRpcRequestError("Invalid request. `id` is not an integer or string.") + + method = payload["method"] + if not isinstance(method, str): + raise JsonRpcRequestError("Invalid request. `method` is not a string.") + + params = payload.get("params") + if params is not None and not isinstance(params, (list, dict)): + raise JsonRpcRequestError("Invalid request. `params` is not a list or dict.") + + request.identifier = identifier + request.method = method + request.params = params + except KeyError as e: + raise JsonRpcRequestError(f"Invalid request. Missing member: `{e}`.") from None + + request._modified = False + request._payload = payload + + return request + +class JsonRpcBatchRequest(JsonRpcObject): + def __init__(self, requests: List[JsonRpcRequest]) -> None: + self.requests = requests + + @override + def to_json(self) -> str: + return "[" + ", ".join([request.to_json() for request in self.requests]) + "]" + +# ------------------------------------------------------------------------------ +# Response. +# ------------------------------------------------------------------------------ + +class JsonRpcResponse(JsonRpcObject): + def __init__( + self, + *, + identifier: Union[str, int, None] = None, + error: Optional[JsonDict] = None, + result: Any = None # noqa: ANN401 + ) -> None: + if error is not None and result is not None: + raise JsonRpcResponseClientError("Invalid response. Only error or result can be set, but not both.") + + self._payload: JsonDict = {} + self._json_payload: Optional[str] = None + self.identifier = identifier + self.error = error + self.result = result + + def _invalidate(self) -> None: + self._modified = True + self._json_payload = None + + @property + def identifier(self) -> Union[str, int, None]: + return self._identifier + + @identifier.setter + def identifier(self, value: Union[str, int, None]) -> None: + self._identifier = value + self._invalidate() + + @property + def error(self) -> Optional[JsonDict]: + return self._error + + @error.setter + def error(self, value: Optional[JsonDict]) -> None: + if value is not None: + try: + _code = value["code"] + _message = value["message"] + except KeyError as e: + raise JsonRpcResponseClientError(f"Invalid response. Missing error member: `{e}`.") from None + + self._error = value + self._invalidate() + + @property + def result(self) -> Any: # noqa: ANN401 + return self._result + + @result.setter + def result(self, value: Any) -> None: # noqa: ANN401 + self._result = value + self._invalidate() + + @property + def payload(self) -> JsonDict: + if not self._modified: + return self._payload + + self._payload = { + "jsonrpc": JSON_RPC_VERSION, + "id": self._identifier + } + if self._error: + self._payload["error"] = self._error + else: + self._payload["result"] = self._result + + self._modified = False + return self._payload + + @override + def to_json(self) -> str: + if self._json_payload is None: + self._json_payload = json.dumps(self.payload) + return self._json_payload + + @classmethod + def from_json(cls, response_str: str) -> Union["JsonRpcResponse", "JsonRpcBatchResponse"]: + try: + payload = json.loads(response_str) + except (TypeError, ValueError): + raise JsonRpcResponseClientError("Invalid reponse. Parse error.") from None + return cls.from_payload(payload) + + @classmethod + def from_payload(cls, payload: JsonValue) -> Union["JsonRpcResponse", "JsonRpcBatchResponse"]: + if not isinstance(payload, list): + return cls._from_response_payload(payload) + + if not payload: + raise JsonRpcResponseClientError("Invalid reponse. Empty batch.") + + payloads = payload + return JsonRpcBatchResponse([cls._from_response_payload(payload) for payload in payloads]) + + @classmethod + def _from_response_payload(cls, payload: JsonValue) -> "JsonRpcResponse": + if not isinstance(payload, dict): + raise JsonRpcResponseClientError("Invalid response. Payload must be an object or a list of objects.") + + payload = cast(JsonDict, payload) + + response = JsonRpcResponse() + + try: + identifier = payload["id"] + if identifier is not None and not isinstance(identifier, (int, str)): + raise JsonRpcResponseClientError("Invalid response. `id` is not an integer or string.") + + error: JsonValue = None + result: Any = None + has_result = False + + with contextlib.suppress(KeyError): + error = payload["error"] + if not isinstance(error, dict): + raise JsonRpcResponseClientError("Invalid response. `error` is not an object.") + + with contextlib.suppress(KeyError): + # We don't use `get` here to differentiate between: `"result": None` and the absence of field. + result = payload["result"] + has_result = True + if error is not None: + raise JsonRpcResponseClientError("Invalid response. `error` and `result` are set.") + + if error is None and not has_result: + raise JsonRpcResponseClientError("Invalid response. `error` and `result` are not set.") + + response.identifier = identifier + response.error = cast(JsonDict, error) + response.result = result + except KeyError as e: + raise JsonRpcResponseClientError(f"Invalid response. Missing member: `{e}`.") from None + + response._modified = False + response._payload = payload + + return response + +class JsonRpcBatchResponse(JsonRpcObject): + def __init__(self, responses: List[JsonRpcResponse]) -> None: + assert responses, "Response list must have at least one item." + self.responses = responses + + @override + def to_json(self) -> str: + return "[" + ", ".join([response.to_json() for response in self.responses]) + "]" + +# ------------------------------------------------------------------------------ +# Dispatcher. +# ------------------------------------------------------------------------------ + +JsonRpcCallable = Callable[..., Any] + +class JsonRpcCallResult: + def __init__(self, *, result: Any = None, error: Optional[JsonRpcResponseError] = None) -> None: # noqa: ANN401 + assert error is None or result is None, "Only error or result can be set, but not both." + self.result = result + self.error = error + + def is_success(self) -> bool: + return self.error is None + +class JsonRpcDispatcher: + def __init__(self, *, use_module_name: bool = False) -> None: + self._name_to_method: Dict[str, JsonRpcCallable] = {} + self._use_module_name = use_module_name + + def call_method(self, method: str, *args: Any, **kwargs: Any) -> Any: # noqa: ANN401 + try: + target = self._name_to_method[method] + except KeyError: + return JsonRpcCallResult(error=JsonRpcResponseMethodNotFoundError()) + + try: + result = target(*args, **kwargs) + except Exception as e: + data: JsonDict = {"message": stringify_exception(e)} + + if isinstance(e, TypeError) and not is_callable_with(target, *args, **kwargs): + return JsonRpcCallResult(error=JsonRpcResponseInvalidParamsError(data=data)) + return JsonRpcCallResult(error=JsonRpcResponseServerError(data=data)) + + return JsonRpcCallResult(result=result) + + def method(self, func: Callable[P, T]) -> Callable[P, T]: + name = func.__name__ + if self._use_module_name: + name = PurePath(inspect.getfile(func)).stem + "." + name + self._name_to_method[name] = func + + cast(Any, func)._rpc_name = name # noqa: SLF001 + return func + +# ------------------------------------------------------------------------------ +# Request processor. +# ------------------------------------------------------------------------------ + +class JsonRpcRequestProcessor: + def __init__(self, dispatcher: JsonRpcDispatcher) -> None: + self._dispatcher = dispatcher + + def process(self, request_str: str) -> Union[JsonRpcResponse, JsonRpcBatchResponse, None]: + # 1. Get request payload. + try: + payload = json.loads(request_str) + except (TypeError, ValueError): + return JsonRpcResponse(error=JsonRpcResponseParseError().payload) + + # 2. Get request. + try: + request = JsonRpcRequest.from_payload(payload) + except JsonRpcRequestError as e: + return JsonRpcResponse( + error=JsonRpcResponseInvalidRequestError(data={ + "message": stringify_exception(e) + }).payload + ) + + # 3. Process request. + if isinstance(request, JsonRpcRequest): + return self._process_request(request) + + responses = self._process_requests(request.requests) + if responses: + return JsonRpcBatchResponse(responses) + return None + + def _process_request(self, request: JsonRpcRequest) -> Optional[JsonRpcResponse]: + identifier = request.identifier + try: + call_result = self._dispatcher.call_method(request.method, *request.args, **request.kwargs) + except Exception as e: + return JsonRpcResponse( + identifier=identifier, + error=JsonRpcResponseInternalError(data={ + "message": stringify_exception(e) + }).payload + ) + + if not call_result.is_success(): + return JsonRpcResponse(identifier=identifier, error=call_result.error.payload) + + if identifier is None: + return None # Notification. + + response = JsonRpcResponse(identifier=identifier, result=call_result.result) + try: + response.to_json() + except Exception as e: + # It's possible that the value cannot be converted to JSON... + return JsonRpcResponse( + identifier=identifier, + error=JsonRpcResponseServerError(data={ + "message": stringify_exception(e) + }).payload) + return response + + def _process_requests(self, requests: List[JsonRpcRequest]) -> List[JsonRpcResponse]: + responses = [] + for request in requests: + response = self._process_request(request) + if response: + responses.append(response) + return responses diff --git a/src/xcp_storage/utils/reflection.py b/src/xcp_storage/utils/reflection.py new file mode 100644 index 0000000..54af056 --- /dev/null +++ b/src/xcp_storage/utils/reflection.py @@ -0,0 +1,26 @@ +# Copyright (C) 2026 Vates SAS +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import inspect + +from xcp_storage.typing import Any, Callable + +# ============================================================================== + +def is_callable_with(func: Callable[..., Any], *args: Any, **kwargs: Any) -> bool: # noqa: ANN401 + try: + inspect.signature(func).bind(*args, **kwargs) + return True + except (TypeError, ValueError): + return False diff --git a/tests/utils/test_json_rpc.py b/tests/utils/test_json_rpc.py new file mode 100644 index 0000000..b596adf --- /dev/null +++ b/tests/utils/test_json_rpc.py @@ -0,0 +1,229 @@ +# Copyright (C) 2026 Vates SAS +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from xcp_storage.utils.json import JsonDict, JsonValue +from xcp_storage.utils.json.rpc import ( + JSON_RPC_VERSION, + JsonRpcBatchRequest, + JsonRpcBatchResponse, + JsonRpcDispatcher, + JsonRpcRequest, + JsonRpcRequestProcessor, + JsonRpcResponse, +) + +from xcp_storage.typing import Union + +# ============================================================================== + +def assert_response_result_impl( + response: Union[JsonRpcResponse, JsonRpcBatchResponse], + expected_identifier: Union[int, None], + expected_result: JsonValue +) -> None: + assert isinstance(response, JsonRpcResponse) + + payload = response.payload + assert payload["jsonrpc"] == JSON_RPC_VERSION + assert payload["id"] == expected_identifier + assert payload["result"] == expected_result + +def assert_response_result( + response: Union[JsonRpcResponse, JsonRpcBatchResponse, None], + expected_identifier: Union[int, None], + expected_result: JsonValue +) -> None: + assert isinstance(response, JsonRpcResponse) + assert_response_result_impl(response, expected_identifier, expected_result) + assert_response_result_impl(JsonRpcResponse.from_json(response.to_json()), expected_identifier, expected_result) + +# ------------------------------------------------------------------------------ + +def assert_response_error_impl( + response: Union[JsonRpcResponse, JsonRpcBatchResponse], + expected_identifier: Union[int, None], + expected_error: JsonDict +) -> None: + assert isinstance(response, JsonRpcResponse) + + payload = response.payload + assert payload["jsonrpc"] == JSON_RPC_VERSION + assert payload["id"] == expected_identifier + assert payload["error"] == expected_error + +def assert_response_error( + response: Union[JsonRpcResponse, JsonRpcBatchResponse, None], + expected_identifier: Union[int, None], + expected_error: JsonDict +) -> None: + assert isinstance(response, JsonRpcResponse) + assert_response_error_impl(response, expected_identifier, expected_error) + assert_response_error_impl(JsonRpcResponse.from_json(response.to_json()), expected_identifier, expected_error) + +# ------------------------------------------------------------------------------ + +class TestJsonRpcRequest: + def test_positional_params(self) -> None: + dispatcher = JsonRpcDispatcher() + + @dispatcher.method + def substract(a: int, b: int) -> int: + return a - b + + request_processor = JsonRpcRequestProcessor(dispatcher) + request = JsonRpcRequest(1, "substract", [42, 23]) + response = request_processor.process(request.to_json()) + assert_response_result(response, 1, 19) + + def test_named_params(self) -> None: + dispatcher = JsonRpcDispatcher() + + @dispatcher.method + def substract(a: int, b: int) -> int: + return a - b + + request_processor = JsonRpcRequestProcessor(dispatcher) + request = JsonRpcRequest(2, "substract", {"a": 23, "b": 42}) + response = request_processor.process(request.to_json()) + assert_response_result(response, 2, -19) + + def test_notification(self) -> None: + dispatcher = JsonRpcDispatcher() + + @dispatcher.method + def hello(message: str) -> str: + return message + + request_processor = JsonRpcRequestProcessor(dispatcher) + request = JsonRpcRequest(None, "hello", {"message": "Hi!"}) + response = request_processor.process(request.to_json()) + assert response is None + + def test_parse_error(self) -> None: + dispatcher = JsonRpcDispatcher() + + request_processor = JsonRpcRequestProcessor(dispatcher) + response = request_processor.process('{"jsonrpc": "2.0", "method": "foobar, "params": "bar", "baz]') + assert_response_error(response, None, {"code": -32700, "message": "Parse error"}) + + def test_invalid_request(self) -> None: + dispatcher = JsonRpcDispatcher() + + request_processor = JsonRpcRequestProcessor(dispatcher) + response = request_processor.process('{"jsonrpc": "2.0", "method": 1, "params": "bar"}') + assert_response_error(response, None, { + "code": -32600, + "message": "Invalid Request", + "data": {"message": "Invalid request. `method` is not a string."} + }) + + def test_method_not_found(self) -> None: + dispatcher = JsonRpcDispatcher() + + request = JsonRpcRequest(1, "foobar") + request_processor = JsonRpcRequestProcessor(dispatcher) + response = request_processor.process(request.to_json()) + assert_response_error(response, 1, {"code": -32601, "message": "Method not found"}) + + def test_invalid_params(self) -> None: + dispatcher = JsonRpcDispatcher() + + @dispatcher.method + def multiply(a: int, b: int) -> int: + return a * b + + request_processor = JsonRpcRequestProcessor(dispatcher) + request = JsonRpcRequest(1, "multiply", {"a": 23, "c": 42}) + response = request_processor.process(request.to_json()) + assert_response_error(response, 1, { + "code": -32602, + "message": "Invalid params", + "data": { + "message": ( + "TestJsonRpcRequest.test_invalid_params..multiply() " + "got an unexpected keyword argument 'c'" + ) + } + }) + +# ------------------------------------------------------------------------------ + +class TestJsonRpcBatchRequest: + def test_batch(self) -> None: + dispatcher = JsonRpcDispatcher() + + @dispatcher.method + def add(*args: int) -> int: + total = 0 + for i in args: + total += i + return total + + @dispatcher.method + def substract(a: int, b: int) -> int: + return a - b + + @dispatcher.method + def hello(count: int) -> None: + pass + + request_processor = JsonRpcRequestProcessor(dispatcher) + + request = JsonRpcBatchRequest([ + JsonRpcRequest(1, "add", [1, 2, 4]), + JsonRpcRequest(None, "hello", [7]), + JsonRpcRequest(5, "get", {"name": "myself"}), + JsonRpcRequest(2, "substract", [42, 23]), + ]) + response = request_processor.process(request.to_json()) + assert isinstance(response, JsonRpcBatchResponse) + + responses = response.responses + assert len(responses) == 3 + + assert_response_result(responses[0], 1, 7) + assert_response_error(responses[1], 5, {"code": -32601, "message": "Method not found"}) + assert_response_result(responses[2], 2, 19) + + def test_parse_error(self) -> None: + dispatcher = JsonRpcDispatcher() + + request_processor = JsonRpcRequestProcessor(dispatcher) + response = request_processor.process("[" + '{"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},' + '{"jsonrpc": "2.0", "method"' + "]") + assert_response_error(response, None, {"code": -32700, "message": "Parse error"}) + + def test_no_requests(self) -> None: + dispatcher = JsonRpcDispatcher() + + request_processor = JsonRpcRequestProcessor(dispatcher) + response = request_processor.process("[]") + assert_response_error(response, None, { + "code": -32600, + "message": "Invalid Request", + "data": {"message": "Invalid request. Empty batch."} + }) + + def test_invalid_request(self) -> None: + dispatcher = JsonRpcDispatcher() + + request_processor = JsonRpcRequestProcessor(dispatcher) + response = request_processor.process("[1]") + assert_response_error(response, None, { + "code": -32600, + "message": "Invalid Request", + "data": {"message": "Invalid request. Payload must be an object or a list of objects."} + }) From f3a538c219e4d086cb7ef9ce226a4fbe34c45a19 Mon Sep 17 00:00:00 2001 From: Ronan Abhamon Date: Tue, 17 Mar 2026 23:36:03 +0100 Subject: [PATCH 08/11] fix(tests/core): use hack for JSON-RPC with python 3.6 Signed-off-by: Ronan Abhamon --- tests/utils/test_json_rpc.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/utils/test_json_rpc.py b/tests/utils/test_json_rpc.py index b596adf..4b3c11e 100644 --- a/tests/utils/test_json_rpc.py +++ b/tests/utils/test_json_rpc.py @@ -143,18 +143,17 @@ def test_invalid_params(self) -> None: def multiply(a: int, b: int) -> int: return a * b + # TODO(XCPNG-3032): Remove it for python 3.11. + # Small hack to use basic function name in python 3.6 because exception messages use it. + multiply.__qualname__ = multiply.__name__ + request_processor = JsonRpcRequestProcessor(dispatcher) request = JsonRpcRequest(1, "multiply", {"a": 23, "c": 42}) response = request_processor.process(request.to_json()) assert_response_error(response, 1, { "code": -32602, "message": "Invalid params", - "data": { - "message": ( - "TestJsonRpcRequest.test_invalid_params..multiply() " - "got an unexpected keyword argument 'c'" - ) - } + "data": {"message": f"{multiply.__qualname__}() got an unexpected keyword argument 'c'"} }) # ------------------------------------------------------------------------------ From 4f7a785c8ddb2840b2a78a6864a94726dc226a27 Mon Sep 17 00:00:00 2001 From: Ronan Abhamon Date: Tue, 17 Mar 2026 11:09:55 +0100 Subject: [PATCH 09/11] chore: use uv workflow Signed-off-by: Ronan Abhamon --- .github/actions/setup-python-env/action.yml | 22 +- .github/workflows/code-checkers.yml | 56 +- pyproject.toml | 21 + uv.lock | 732 ++++++++++++++++++++ 4 files changed, 808 insertions(+), 23 deletions(-) create mode 100644 uv.lock diff --git a/.github/actions/setup-python-env/action.yml b/.github/actions/setup-python-env/action.yml index cbfe506..2da55cc 100644 --- a/.github/actions/setup-python-env/action.yml +++ b/.github/actions/setup-python-env/action.yml @@ -1,14 +1,24 @@ name: Setup Python env -description: Setup Python and pip +description: Setup Python and dependencies +inputs: + python-version: + description: Python version to use + required: true runs: - using: "composite" + using: composite steps: - - name: Setup Python + - name: Set up uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + cache-dependency-glob: uv.lock + version: "0.10.11" + + - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.14" - cache: "pip" + python-version: ${{ inputs.python-version }} - name: Install dependencies shell: bash - run: pip install --upgrade pip + run: uv sync --all-extras --dev diff --git a/.github/workflows/code-checkers.yml b/.github/workflows/code-checkers.yml index d0a35bf..72e28d1 100644 --- a/.github/workflows/code-checkers.yml +++ b/.github/workflows/code-checkers.yml @@ -2,42 +2,64 @@ name: Code checkers on: [push, pull_request] +env: + PYTHON_VERSIONS: '["3.8", "3.14"]' + jobs: + setup: + name: Setup + runs-on: ubuntu-latest + outputs: + python-versions: ${{ env.PYTHON_VERSIONS }} + python-latest: ${{ steps.set-vars.outputs.python-latest }} + steps: + - id: set-vars + run: | + LATEST=$(echo '${{ env.PYTHON_VERSIONS }}' | jq -c '.[-1:]') + echo "python-latest=$LATEST" >> $GITHUB_OUTPUT + linting: name: Linting runs-on: ubuntu-latest + needs: setup + strategy: + matrix: + python-version: ${{ fromJSON(needs.setup.outputs.python-versions) }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: ./.github/actions/setup-python-env - - - name: Install Ruff - run: pip install ruff - - - name: Ruff check - run: ruff check + with: + python-version: ${{ matrix.python-version }} + - run: uv run ruff check static-type-checking: name: Static type checking runs-on: ubuntu-latest + needs: setup + strategy: + matrix: + python-version: ${{ fromJSON(needs.setup.outputs.python-latest) }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: ./.github/actions/setup-python-env - - - name: Install Mypy - run: pip install mypy + with: + python-version: ${{ matrix.python-version }} - name: Mypy check - run: mypy + run: uv run mypy tests: name: Tests runs-on: ubuntu-latest + needs: setup + strategy: + matrix: + python-version: ${{ fromJSON(needs.setup.outputs.python-versions) }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: ./.github/actions/setup-python-env - - - name: Install Pytest - run: pip install pytest + with: + python-version: ${{ matrix.python-version }} - name: Pytest check - run: pytest + run: uv run pytest diff --git a/pyproject.toml b/pyproject.toml index debb7b3..7de9ae4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,24 @@ +[project] +name = "xcp-storage" +dynamic = [ + "authors", + "description", + "license", + "version" +] +requires-python = ">=3.7" # TODO(XCPNG-3032): Change it for modern python. + +[build-system] +requires = ["setuptools>=40"] +build-backend = "setuptools.build_meta" + +[dependency-groups] +dev = [ + "mypy>=1.4.1", + "pytest>=7.4.4", + "ruff>=0.15.6", +] + [tool.mypy] mypy_path = "stubs" files = [ diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..76397e6 --- /dev/null +++ b/uv.lock @@ -0,0 +1,732 @@ +version = 1 +revision = 3 +requires-python = ">=3.7" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", + "python_full_version < '3.8'", +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", version = "4.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "6.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", version = "4.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "zipp", marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/82/f6e29c8d5c098b6be61460371c2c5591f4a335923639edec43b3830650a4/importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4", size = 53569, upload-time = "2023-06-18T21:44:35.024Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/94/64287b38c7de4c90683630338cf28f129decbba0a44f0c6db35a873c73c4/importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5", size = 22934, upload-time = "2023-06-18T21:44:33.441Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "librt" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/5f/63f5fa395c7a8a93558c0904ba8f1c8d1b997ca6a3de61bc7659970d66bf/librt-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81fd938344fecb9373ba1b155968c8a329491d2ce38e7ddb76f30ffb938f12dc", size = 65697, upload-time = "2026-02-17T16:11:06.903Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e0/0472cf37267b5920eff2f292ccfaede1886288ce35b7f3203d8de00abfe6/librt-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5db05697c82b3a2ec53f6e72b2ed373132b0c2e05135f0696784e97d7f5d48e7", size = 68376, upload-time = "2026-02-17T16:11:08.395Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8bd1359fdcd27ab897cd5963294fa4a7c83b20a8564678e4fd12157e56a5/librt-0.8.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d56bc4011975f7460bea7b33e1ff425d2f1adf419935ff6707273c77f8a4ada6", size = 197084, upload-time = "2026-02-17T16:11:09.774Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fe/163e33fdd091d0c2b102f8a60cc0a61fd730ad44e32617cd161e7cd67a01/librt-0.8.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdc0f588ff4b663ea96c26d2a230c525c6fc62b28314edaaaca8ed5af931ad0", size = 207337, upload-time = "2026-02-17T16:11:11.311Z" }, + { url = "https://files.pythonhosted.org/packages/01/99/f85130582f05dcf0c8902f3d629270231d2f4afdfc567f8305a952ac7f14/librt-0.8.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c2b54ff6717a7a563b72627990bec60d8029df17df423f0ed37d56a17a176b", size = 219980, upload-time = "2026-02-17T16:11:12.499Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/cb5e4d03659e043a26c74e08206412ac9a3742f0477d96f9761a55313b5f/librt-0.8.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f1125e6bbf2f1657d9a2f3ccc4a2c9b0c8b176965bb565dd4d86be67eddb4b6", size = 212921, upload-time = "2026-02-17T16:11:14.484Z" }, + { url = "https://files.pythonhosted.org/packages/b1/81/a3a01e4240579c30f3487f6fed01eb4bc8ef0616da5b4ebac27ca19775f3/librt-0.8.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8f4bb453f408137d7581be309b2fbc6868a80e7ef60c88e689078ee3a296ae71", size = 221381, upload-time = "2026-02-17T16:11:17.459Z" }, + { url = "https://files.pythonhosted.org/packages/08/b0/fc2d54b4b1c6fb81e77288ff31ff25a2c1e62eaef4424a984f228839717b/librt-0.8.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c336d61d2fe74a3195edc1646d53ff1cddd3a9600b09fa6ab75e5514ba4862a7", size = 216714, upload-time = "2026-02-17T16:11:19.197Z" }, + { url = "https://files.pythonhosted.org/packages/96/96/85daa73ffbd87e1fb287d7af6553ada66bf25a2a6b0de4764344a05469f6/librt-0.8.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eb5656019db7c4deacf0c1a55a898c5bb8f989be904597fcb5232a2f4828fa05", size = 214777, upload-time = "2026-02-17T16:11:20.443Z" }, + { url = "https://files.pythonhosted.org/packages/12/9c/c3aa7a2360383f4bf4f04d98195f2739a579128720c603f4807f006a4225/librt-0.8.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c25d9e338d5bed46c1632f851babf3d13c78f49a225462017cf5e11e845c5891", size = 237398, upload-time = "2026-02-17T16:11:22.083Z" }, + { url = "https://files.pythonhosted.org/packages/61/19/d350ea89e5274665185dabc4bbb9c3536c3411f862881d316c8b8e00eb66/librt-0.8.1-cp310-cp310-win32.whl", hash = "sha256:aaab0e307e344cb28d800957ef3ec16605146ef0e59e059a60a176d19543d1b7", size = 54285, upload-time = "2026-02-17T16:11:23.27Z" }, + { url = "https://files.pythonhosted.org/packages/4f/d6/45d587d3d41c112e9543a0093d883eb57a24a03e41561c127818aa2a6bcc/librt-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:56e04c14b696300d47b3bc5f1d10a00e86ae978886d0cee14e5714fafb5df5d2", size = 61352, upload-time = "2026-02-17T16:11:24.207Z" }, + { url = "https://files.pythonhosted.org/packages/1d/01/0e748af5e4fee180cf7cd12bd12b0513ad23b045dccb2a83191bde82d168/librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd", size = 65315, upload-time = "2026-02-17T16:11:25.152Z" }, + { url = "https://files.pythonhosted.org/packages/9d/4d/7184806efda571887c798d573ca4134c80ac8642dcdd32f12c31b939c595/librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965", size = 68021, upload-time = "2026-02-17T16:11:26.129Z" }, + { url = "https://files.pythonhosted.org/packages/ae/88/c3c52d2a5d5101f28d3dc89298444626e7874aa904eed498464c2af17627/librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da", size = 194500, upload-time = "2026-02-17T16:11:27.177Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5d/6fb0a25b6a8906e85b2c3b87bee1d6ed31510be7605b06772f9374ca5cb3/librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0", size = 205622, upload-time = "2026-02-17T16:11:28.242Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a6/8006ae81227105476a45691f5831499e4d936b1c049b0c1feb17c11b02d1/librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e", size = 218304, upload-time = "2026-02-17T16:11:29.344Z" }, + { url = "https://files.pythonhosted.org/packages/ee/19/60e07886ad16670aae57ef44dada41912c90906a6fe9f2b9abac21374748/librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3", size = 211493, upload-time = "2026-02-17T16:11:30.445Z" }, + { url = "https://files.pythonhosted.org/packages/9c/cf/f666c89d0e861d05600438213feeb818c7514d3315bae3648b1fc145d2b6/librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac", size = 219129, upload-time = "2026-02-17T16:11:32.021Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ef/f1bea01e40b4a879364c031476c82a0dc69ce068daad67ab96302fed2d45/librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596", size = 213113, upload-time = "2026-02-17T16:11:33.192Z" }, + { url = "https://files.pythonhosted.org/packages/9b/80/cdab544370cc6bc1b72ea369525f547a59e6938ef6863a11ab3cd24759af/librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99", size = 212269, upload-time = "2026-02-17T16:11:34.373Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9c/48d6ed8dac595654f15eceab2035131c136d1ae9a1e3548e777bb6dbb95d/librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe", size = 234673, upload-time = "2026-02-17T16:11:36.063Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/35b68b1db517f27a01be4467593292eb5315def8900afad29fabf56304ba/librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb", size = 54597, upload-time = "2026-02-17T16:11:37.544Z" }, + { url = "https://files.pythonhosted.org/packages/71/02/796fe8f02822235966693f257bf2c79f40e11337337a657a8cfebba5febc/librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b", size = 61733, upload-time = "2026-02-17T16:11:38.691Z" }, + { url = "https://files.pythonhosted.org/packages/28/ad/232e13d61f879a42a4e7117d65e4984bb28371a34bb6fb9ca54ec2c8f54e/librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9", size = 52273, upload-time = "2026-02-17T16:11:40.308Z" }, + { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" }, + { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" }, + { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" }, + { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" }, + { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" }, + { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" }, + { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" }, + { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" }, + { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" }, + { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" }, + { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" }, + { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" }, + { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" }, + { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" }, + { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" }, + { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" }, + { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" }, + { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" }, + { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" }, + { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" }, + { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" }, + { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, + { url = "https://files.pythonhosted.org/packages/01/1f/c7d8b66a3ca3ca3ed8ded4b32c96ee58a45920ebbbaa934355c74adcc33e/librt-0.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3dff3d3ca8db20e783b1bc7de49c0a2ab0b8387f31236d6a026597d07fcd68ac", size = 65990, upload-time = "2026-02-17T16:12:48.972Z" }, + { url = "https://files.pythonhosted.org/packages/56/be/ee9ba1730052313d08457f19beaa1b878619978863fba09b40aed5b5c123/librt-0.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08eec3a1fc435f0d09c87b6bf1ec798986a3544f446b864e4099633a56fcd9ed", size = 68640, upload-time = "2026-02-17T16:12:50.24Z" }, + { url = "https://files.pythonhosted.org/packages/81/27/b7309298b96f7690cec3ceee38004c1a7f60fcd96d952d3ac344a1e3e8b3/librt-0.8.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e3f0a41487fd5fad7e760b9e8a90e251e27c2816fbc2cff36a22a0e6bcbbd9dd", size = 196099, upload-time = "2026-02-17T16:12:52.788Z" }, + { url = "https://files.pythonhosted.org/packages/10/48/160a5aacdcb21824b10a52378c39e88c46a29bb31efdaf3910dd1f9b670e/librt-0.8.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bacdb58d9939d95cc557b4dbaa86527c9db2ac1ed76a18bc8d26f6dc8647d851", size = 206663, upload-time = "2026-02-17T16:12:55.017Z" }, + { url = "https://files.pythonhosted.org/packages/ee/65/33dd1d8caabb7c6805d87d095b143417dc96b0277c06ffa0508361422c82/librt-0.8.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d7ab1f01aa753188605b09a51faa44a3327400b00b8cce424c71910fc0a128", size = 219318, upload-time = "2026-02-17T16:12:56.145Z" }, + { url = "https://files.pythonhosted.org/packages/09/d4/353805aa6181c7950a2462bd6e855366eeca21a501f375228d72a51547df/librt-0.8.1-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4998009e7cb9e896569f4be7004f09d0ed70d386fa99d42b6d363f6d200501ac", size = 212191, upload-time = "2026-02-17T16:12:57.326Z" }, + { url = "https://files.pythonhosted.org/packages/06/08/725b3f304d61eba56c713c251fb833a06d84bf93381caad5152366f5d2bb/librt-0.8.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2cc68eeeef5e906839c7bb0815748b5b0a974ec27125beefc0f942715785b551", size = 220672, upload-time = "2026-02-17T16:12:58.497Z" }, + { url = "https://files.pythonhosted.org/packages/0e/55/e8cdf04145872b3b97cb9b68287b22d1c08348227063f305aec11a3e6ce7/librt-0.8.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0bf69d79a23f4f40b8673a947a234baeeb133b5078b483b7297c5916539cf5d5", size = 216172, upload-time = "2026-02-17T16:12:59.751Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d8/23b1c6592d2422dd6829c672f45b1f1c257f219926b0d216fedb572d0184/librt-0.8.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:22b46eabd76c1986ee7d231b0765ad387d7673bbd996aa0d0d054b38ac65d8f6", size = 214116, upload-time = "2026-02-17T16:13:01.056Z" }, + { url = "https://files.pythonhosted.org/packages/c9/92/2b44fd3cc3313f44e43bdbb41343735b568fa675fa351642b408ee48d418/librt-0.8.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:237796479f4d0637d6b9cbcb926ff424a97735e68ade6facf402df4ec93375ed", size = 236664, upload-time = "2026-02-17T16:13:02.314Z" }, + { url = "https://files.pythonhosted.org/packages/00/23/92313ecdab80e142d8ea10e8dfa6297694359dbaacc9e81679bdc8cbceb6/librt-0.8.1-cp39-cp39-win32.whl", hash = "sha256:4beb04b8c66c6ae62f8c1e0b2f097c1ebad9295c929a8d5286c05eae7c2fc7dc", size = 54368, upload-time = "2026-02-17T16:13:03.549Z" }, + { url = "https://files.pythonhosted.org/packages/68/36/18f6e768afad6b55a690d38427c53251b69b7ba8795512730fd2508b31a9/librt-0.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:64548cde61b692dc0dc379f4b5f59a2f582c2ebe7890d09c1ae3b9e66fa015b7", size = 61507, upload-time = "2026-02-17T16:13:04.556Z" }, +] + +[[package]] +name = "mypy" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "mypy-extensions", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "tomli", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "typed-ast", marker = "python_full_version < '3.8'" }, + { name = "typing-extensions", version = "4.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/28/d8a8233ff167d06108e53b7aefb4a8d7350adbbf9d7abd980f17fdb7a3a6/mypy-1.4.1.tar.gz", hash = "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b", size = 2855162, upload-time = "2023-06-25T23:22:54.364Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/3b/1c7363863b56c059f60a1dfdca9ac774a22ba64b7a4da0ee58ee53e5243f/mypy-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8", size = 10451043, upload-time = "2023-06-25T23:22:02.502Z" }, + { url = "https://files.pythonhosted.org/packages/a7/24/6f0df1874118839db1155fed62a4bd7e80c181367ff8ea07d40fbaffcfb4/mypy-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878", size = 9542079, upload-time = "2023-06-25T23:22:37.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5c/deeac94fcccd11aa621e6b350df333e1b809b11443774ea67582cc0205da/mypy-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd", size = 11974913, upload-time = "2023-06-25T23:21:14.603Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2f/de3c455c54e8cf5e37ea38705c1920f2df470389f8fc051084d2dd8c9c59/mypy-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc", size = 12044492, upload-time = "2023-06-25T23:22:17.551Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d3/6f65357dcb68109946de70cd55bd2e60f10114f387471302f48d54ff5dae/mypy-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1", size = 8831655, upload-time = "2023-06-25T23:21:40.201Z" }, + { url = "https://files.pythonhosted.org/packages/94/01/e34e37a044325af4d4af9825c15e8a0d26d89b5a9624b4d0908449d3411b/mypy-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462", size = 10338636, upload-time = "2023-06-25T23:22:43.45Z" }, + { url = "https://files.pythonhosted.org/packages/92/58/ccc0b714ecbd1a64b34d8ce1c38763ff6431de1d82551904ecc3711fbe05/mypy-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258", size = 9444172, upload-time = "2023-06-25T23:21:25.502Z" }, + { url = "https://files.pythonhosted.org/packages/73/72/dfc0b46e6905eafd598e7c48c0c4f2e232647e4e36547425c64e6c850495/mypy-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2", size = 11855450, upload-time = "2023-06-25T23:21:37.234Z" }, + { url = "https://files.pythonhosted.org/packages/66/f4/60739a2d336f3adf5628e7c9b920d16e8af6dc078550d615e4ba2a1d7759/mypy-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7", size = 11928679, upload-time = "2023-06-25T23:22:40.757Z" }, + { url = "https://files.pythonhosted.org/packages/8c/26/6ff2b55bf8b605a4cc898883654c2ca4dd4feedf0bb04ecaacf60d165cde/mypy-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01", size = 8831134, upload-time = "2023-06-25T23:22:09.178Z" }, + { url = "https://files.pythonhosted.org/packages/95/47/fb69dad9634af9f1dab69f8b4031d674592384b59c7171852b1fbed6de15/mypy-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:470c969bb3f9a9efcedbadcd19a74ffb34a25f8e6b0e02dae7c0e71f8372f97b", size = 10101278, upload-time = "2023-06-25T23:22:51.525Z" }, + { url = "https://files.pythonhosted.org/packages/65/f7/77339904a3415cadca5551f2ea0c74feefc9b7187636a292690788f4d4b3/mypy-1.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5952d2d18b79f7dc25e62e014fe5a23eb1a3d2bc66318df8988a01b1a037c5b", size = 11643877, upload-time = "2023-06-25T23:22:20.963Z" }, + { url = "https://files.pythonhosted.org/packages/f5/93/ae39163ae84266d24d1fcf8ee1e2db1e0346e09de97570dd101a07ccf876/mypy-1.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:190b6bab0302cec4e9e6767d3eb66085aef2a1cc98fe04936d8a42ed2ba77bb7", size = 11702718, upload-time = "2023-06-25T23:22:32.056Z" }, + { url = "https://files.pythonhosted.org/packages/13/3b/3b7de921626547b36c34b91c74cfbda260210df7c49bd3d315015cfd6005/mypy-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9d40652cc4fe33871ad3338581dca3297ff5f2213d0df345bcfbde5162abf0c9", size = 8551181, upload-time = "2023-06-25T23:22:27.656Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/63bab763e4d44e1a7c341fb64496ddf20970780935596ffed9ed2d85eae7/mypy-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:01fd2e9f85622d981fd9063bfaef1aed6e336eaacca00892cd2d82801ab7c042", size = 10390236, upload-time = "2023-06-25T23:21:30.367Z" }, + { url = "https://files.pythonhosted.org/packages/23/3f/54a87d933440416a1efd7a42b45f8cf22e353efe889eb3903cc34177ab44/mypy-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2460a58faeea905aeb1b9b36f5065f2dc9a9c6e4c992a6499a2360c6c74ceca3", size = 9496760, upload-time = "2023-06-25T23:21:33.753Z" }, + { url = "https://files.pythonhosted.org/packages/4e/89/26230b46e27724bd54f76cd73a2759eaaf35292b32ba64f36c7c47836d4b/mypy-1.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2746d69a8196698146a3dbe29104f9eb6a2a4d8a27878d92169a6c0b74435b6", size = 11927489, upload-time = "2023-06-25T23:21:58.934Z" }, + { url = "https://files.pythonhosted.org/packages/64/7d/156e721376951c449554942eedf4d53e9ca2a57e94bf0833ad2821d59bfa/mypy-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae704dcfaa180ff7c4cfbad23e74321a2b774f92ca77fd94ce1049175a21c97f", size = 11990009, upload-time = "2023-06-25T23:21:55.909Z" }, + { url = "https://files.pythonhosted.org/packages/27/ab/21230851e8137c9ef9a095cc8cb70d8ff8cac21014e4b249ac7a9eae7df9/mypy-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:43d24f6437925ce50139a310a64b2ab048cb2d3694c84c71c3f2a1626d8101dc", size = 8816535, upload-time = "2023-06-25T23:21:45.703Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1b/9050b5c444ef82c3d59bdbf21f91b259cf20b2ac1df37d55bc6b91d609a1/mypy-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828", size = 10447897, upload-time = "2023-06-25T23:21:22.06Z" }, + { url = "https://files.pythonhosted.org/packages/da/00/ac2b58b321d85cac25be0dcd1bc2427dfc6cf403283fc205a0031576f14b/mypy-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3", size = 9534091, upload-time = "2023-06-25T23:22:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/26240f14e854a95af87d577b288d607ebe0ccb75cb37052f6386402f022d/mypy-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816", size = 11970165, upload-time = "2023-06-25T23:22:05.673Z" }, + { url = "https://files.pythonhosted.org/packages/b7/34/a3edaec8762181bfe97439c7e094f4c2f411ed9b79ac8f4d72156e88d5ce/mypy-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c", size = 12040792, upload-time = "2023-06-25T23:21:49.878Z" }, + { url = "https://files.pythonhosted.org/packages/d1/f3/0d0622d5a83859a992b01741a7b97949d6fb9efc9f05f20a09f0df10dc1e/mypy-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f", size = 8831367, upload-time = "2023-06-25T23:21:43.065Z" }, + { url = "https://files.pythonhosted.org/packages/3d/9a/e13addb8d652cb068f835ac2746d9d42f85b730092f581bb17e2059c28f1/mypy-1.4.1-py3-none-any.whl", hash = "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4", size = 2451741, upload-time = "2023-06-25T23:22:49.033Z" }, +] + +[[package]] +name = "mypy" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "mypy-extensions", version = "1.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "tomli", version = "2.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051, upload-time = "2024-12-30T16:39:07.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/7a/87ae2adb31d68402da6da1e5f30c07ea6063e9f09b5e7cfc9dfa44075e74/mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb", size = 11211002, upload-time = "2024-12-30T16:37:22.435Z" }, + { url = "https://files.pythonhosted.org/packages/e1/23/eada4c38608b444618a132be0d199b280049ded278b24cbb9d3fc59658e4/mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0", size = 10358400, upload-time = "2024-12-30T16:37:53.526Z" }, + { url = "https://files.pythonhosted.org/packages/43/c9/d6785c6f66241c62fd2992b05057f404237deaad1566545e9f144ced07f5/mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d", size = 12095172, upload-time = "2024-12-30T16:37:50.332Z" }, + { url = "https://files.pythonhosted.org/packages/c3/62/daa7e787770c83c52ce2aaf1a111eae5893de9e004743f51bfcad9e487ec/mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b", size = 12828732, upload-time = "2024-12-30T16:37:29.96Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a2/5fb18318a3637f29f16f4e41340b795da14f4751ef4f51c99ff39ab62e52/mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427", size = 13012197, upload-time = "2024-12-30T16:38:05.037Z" }, + { url = "https://files.pythonhosted.org/packages/28/99/e153ce39105d164b5f02c06c35c7ba958aaff50a2babba7d080988b03fe7/mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f", size = 9780836, upload-time = "2024-12-30T16:37:19.726Z" }, + { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432, upload-time = "2024-12-30T16:37:11.533Z" }, + { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515, upload-time = "2024-12-30T16:37:40.724Z" }, + { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791, upload-time = "2024-12-30T16:36:58.73Z" }, + { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203, upload-time = "2024-12-30T16:37:03.741Z" }, + { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900, upload-time = "2024-12-30T16:37:57.948Z" }, + { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869, upload-time = "2024-12-30T16:37:33.428Z" }, + { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668, upload-time = "2024-12-30T16:38:02.211Z" }, + { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060, upload-time = "2024-12-30T16:37:46.131Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167, upload-time = "2024-12-30T16:37:43.534Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341, upload-time = "2024-12-30T16:37:36.249Z" }, + { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991, upload-time = "2024-12-30T16:37:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016, upload-time = "2024-12-30T16:37:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097, upload-time = "2024-12-30T16:37:25.144Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728, upload-time = "2024-12-30T16:38:08.634Z" }, + { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965, upload-time = "2024-12-30T16:38:12.132Z" }, + { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660, upload-time = "2024-12-30T16:38:17.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198, upload-time = "2024-12-30T16:38:32.839Z" }, + { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276, upload-time = "2024-12-30T16:38:20.828Z" }, + { url = "https://files.pythonhosted.org/packages/39/02/1817328c1372be57c16148ce7d2bfcfa4a796bedaed897381b1aad9b267c/mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31", size = 11143050, upload-time = "2024-12-30T16:38:29.743Z" }, + { url = "https://files.pythonhosted.org/packages/b9/07/99db9a95ece5e58eee1dd87ca456a7e7b5ced6798fd78182c59c35a7587b/mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6", size = 10321087, upload-time = "2024-12-30T16:38:14.739Z" }, + { url = "https://files.pythonhosted.org/packages/9a/eb/85ea6086227b84bce79b3baf7f465b4732e0785830726ce4a51528173b71/mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319", size = 12066766, upload-time = "2024-12-30T16:38:47.038Z" }, + { url = "https://files.pythonhosted.org/packages/4b/bb/f01bebf76811475d66359c259eabe40766d2f8ac8b8250d4e224bb6df379/mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac", size = 12787111, upload-time = "2024-12-30T16:39:02.444Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c9/84837ff891edcb6dcc3c27d85ea52aab0c4a34740ff5f0ccc0eb87c56139/mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b", size = 12974331, upload-time = "2024-12-30T16:38:23.849Z" }, + { url = "https://files.pythonhosted.org/packages/84/5f/901e18464e6a13f8949b4909535be3fa7f823291b8ab4e4b36cfe57d6769/mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837", size = 9763210, upload-time = "2024-12-30T16:38:36.299Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1f/186d133ae2514633f8558e78cd658070ba686c0e9275c5a5c24a1e1f0d67/mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35", size = 11200493, upload-time = "2024-12-30T16:38:26.935Z" }, + { url = "https://files.pythonhosted.org/packages/af/fc/4842485d034e38a4646cccd1369f6b1ccd7bc86989c52770d75d719a9941/mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc", size = 10357702, upload-time = "2024-12-30T16:38:50.623Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e6/457b83f2d701e23869cfec013a48a12638f75b9d37612a9ddf99072c1051/mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9", size = 12091104, upload-time = "2024-12-30T16:38:53.735Z" }, + { url = "https://files.pythonhosted.org/packages/f1/bf/76a569158db678fee59f4fd30b8e7a0d75bcbaeef49edd882a0d63af6d66/mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb", size = 12830167, upload-time = "2024-12-30T16:38:56.437Z" }, + { url = "https://files.pythonhosted.org/packages/43/bc/0bc6b694b3103de9fed61867f1c8bd33336b913d16831431e7cb48ef1c92/mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60", size = 13013834, upload-time = "2024-12-30T16:38:59.204Z" }, + { url = "https://files.pythonhosted.org/packages/b0/79/5f5ec47849b6df1e6943d5fd8e6632fbfc04b4fd4acfa5a5a9535d11b4e2/mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c", size = 9781231, upload-time = "2024-12-30T16:39:05.124Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905, upload-time = "2024-12-30T16:38:42.021Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "librt", marker = "python_full_version >= '3.9' and platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions", version = "1.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pathspec", marker = "python_full_version >= '3.9'" }, + { name = "tomli", version = "2.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f7/88436084550ca9af5e610fa45286be04c3b63374df3e021c762fe8c4369f/mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3", size = 13102606, upload-time = "2025-12-15T05:02:46.833Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a5/43dfad311a734b48a752790571fd9e12d61893849a01bff346a54011957f/mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a", size = 12164496, upload-time = "2025-12-15T05:03:41.947Z" }, + { url = "https://files.pythonhosted.org/packages/88/f0/efbfa391395cce2f2771f937e0620cfd185ec88f2b9cd88711028a768e96/mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67", size = 12772068, upload-time = "2025-12-15T05:02:53.689Z" }, + { url = "https://files.pythonhosted.org/packages/25/05/58b3ba28f5aed10479e899a12d2120d582ba9fa6288851b20bf1c32cbb4f/mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e", size = 13520385, upload-time = "2025-12-15T05:02:38.328Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a0/c006ccaff50b31e542ae69b92fe7e2f55d99fba3a55e01067dd564325f85/mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376", size = 13796221, upload-time = "2025-12-15T05:03:22.147Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ff/8bdb051cd710f01b880472241bd36b3f817a8e1c5d5540d0b761675b6de2/mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24", size = 10055456, upload-time = "2025-12-15T05:03:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433, upload-time = "2023-02-04T12:11:27.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695, upload-time = "2023-02-04T12:11:25.002Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "packaging" +version = "24.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/b5/b43a27ac7472e1818c4bafd44430e69605baefe1f34440593e0332ec8b4d/packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9", size = 147882, upload-time = "2024-03-10T09:39:28.33Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", size = 53488, upload-time = "2024-03-10T09:39:25.947Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + +[[package]] +name = "pluggy" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/42/8f2833655a29c4e9cb52ee8a2be04ceac61bcff4a680fb338cbd3d1e322d/pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3", size = 61613, upload-time = "2023-06-21T09:12:28.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/32/4a79112b8b87b21450b066e102d6608907f4c885ed7b04c3fdb085d4d6ae/pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849", size = 17695, upload-time = "2023-06-21T09:12:27.397Z" }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "7.4.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.8' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.8'" }, + { name = "importlib-metadata", marker = "python_full_version < '3.8'" }, + { name = "iniconfig", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "packaging", version = "24.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "pluggy", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "tomli", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/1f/9d8e98e4133ffb16c90f3b405c43e38d3abb715bb5d7a63a5a684f7e46a3/pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", size = 1357116, upload-time = "2023-12-31T12:00:18.035Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8", size = 325287, upload-time = "2023-12-31T12:00:13.963Z" }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version == '3.8.*' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.8.*'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "packaging", version = "26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "tomli", version = "2.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version == '3.9.*' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.9.*'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "packaging", version = "26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pygments", marker = "python_full_version == '3.9.*'" }, + { name = "tomli", version = "2.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", version = "26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pygments", marker = "python_full_version >= '3.10'" }, + { name = "tomli", version = "2.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/df/f8629c19c5318601d3121e230f74cbee7a3732339c52b21daa2b82ef9c7d/ruff-0.15.6.tar.gz", hash = "sha256:8394c7bb153a4e3811a4ecdacd4a8e6a4fa8097028119160dffecdcdf9b56ae4", size = 4597916, upload-time = "2026-03-12T23:05:47.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/2f/4e03a7e5ce99b517e98d3b4951f411de2b0fa8348d39cf446671adcce9a2/ruff-0.15.6-py3-none-linux_armv6l.whl", hash = "sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff", size = 10508953, upload-time = "2026-03-12T23:05:17.246Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/55bcdc3e9f80bcf39edf0cd272da6fa511a3d94d5a0dd9e0adf76ceebdb4/ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3", size = 10942257, upload-time = "2026-03-12T23:05:23.076Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/005c29bd1726c0f492bfa215e95154cf480574140cb5f867c797c18c790b/ruff-0.15.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3bd9967851a25f038fc8b9ae88a7fbd1b609f30349231dffaa37b6804923c4bb", size = 10322683, upload-time = "2026-03-12T23:05:33.738Z" }, + { url = "https://files.pythonhosted.org/packages/5f/74/2f861f5fd7cbb2146bddb5501450300ce41562da36d21868c69b7a828169/ruff-0.15.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13f4594b04e42cd24a41da653886b04d2ff87adbf57497ed4f728b0e8a4866f8", size = 10660986, upload-time = "2026-03-12T23:05:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/c1/a1/309f2364a424eccb763cdafc49df843c282609f47fe53aa83f38272389e0/ruff-0.15.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2ed8aea2f3fe57886d3f00ea5b8aae5bf68d5e195f487f037a955ff9fbaac9e", size = 10332177, upload-time = "2026-03-12T23:05:56.145Z" }, + { url = "https://files.pythonhosted.org/packages/30/41/7ebf1d32658b4bab20f8ac80972fb19cd4e2c6b78552be263a680edc55ac/ruff-0.15.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70789d3e7830b848b548aae96766431c0dc01a6c78c13381f423bf7076c66d15", size = 11170783, upload-time = "2026-03-12T23:06:01.742Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/6d488f6adca047df82cd62c304638bcb00821c36bd4881cfca221561fdfc/ruff-0.15.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:542aaf1de3154cea088ced5a819ce872611256ffe2498e750bbae5247a8114e9", size = 12044201, upload-time = "2026-03-12T23:05:28.697Z" }, + { url = "https://files.pythonhosted.org/packages/71/68/e6f125df4af7e6d0b498f8d373274794bc5156b324e8ab4bf5c1b4fc0ec7/ruff-0.15.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c22e6f02c16cfac3888aa636e9eba857254d15bbacc9906c9689fdecb1953ab", size = 11421561, upload-time = "2026-03-12T23:05:31.236Z" }, + { url = "https://files.pythonhosted.org/packages/f1/9f/f85ef5fd01a52e0b472b26dc1b4bd228b8f6f0435975442ffa4741278703/ruff-0.15.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98893c4c0aadc8e448cfa315bd0cc343a5323d740fe5f28ef8a3f9e21b381f7e", size = 11310928, upload-time = "2026-03-12T23:05:45.288Z" }, + { url = "https://files.pythonhosted.org/packages/8c/26/b75f8c421f5654304b89471ed384ae8c7f42b4dff58fa6ce1626d7f2b59a/ruff-0.15.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:70d263770d234912374493e8cc1e7385c5d49376e41dfa51c5c3453169dc581c", size = 11235186, upload-time = "2026-03-12T23:05:50.677Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d4/d5a6d065962ff7a68a86c9b4f5500f7d101a0792078de636526c0edd40da/ruff-0.15.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:55a1ad63c5a6e54b1f21b7514dfadc0c7fb40093fa22e95143cf3f64ebdcd512", size = 10635231, upload-time = "2026-03-12T23:05:37.044Z" }, + { url = "https://files.pythonhosted.org/packages/d6/56/7c3acf3d50910375349016cf33de24be021532042afbed87942858992491/ruff-0.15.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8dc473ba093c5ec238bb1e7429ee676dca24643c471e11fbaa8a857925b061c0", size = 10340357, upload-time = "2026-03-12T23:06:04.748Z" }, + { url = "https://files.pythonhosted.org/packages/06/54/6faa39e9c1033ff6a3b6e76b5df536931cd30caf64988e112bbf91ef5ce5/ruff-0.15.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:85b042377c2a5561131767974617006f99f7e13c63c111b998f29fc1e58a4cfb", size = 10860583, upload-time = "2026-03-12T23:05:58.978Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/509a201b843b4dfb0b32acdedf68d951d3377988cae43949ba4c4133a96a/ruff-0.15.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cef49e30bc5a86a6a92098a7fbf6e467a234d90b63305d6f3ec01225a9d092e0", size = 11410976, upload-time = "2026-03-12T23:05:39.955Z" }, + { url = "https://files.pythonhosted.org/packages/6c/25/3fc9114abf979a41673ce877c08016f8e660ad6cf508c3957f537d2e9fa9/ruff-0.15.6-py3-none-win32.whl", hash = "sha256:bbf67d39832404812a2d23020dda68fee7f18ce15654e96fb1d3ad21a5fe436c", size = 10616872, upload-time = "2026-03-12T23:05:42.451Z" }, + { url = "https://files.pythonhosted.org/packages/89/7a/09ece68445ceac348df06e08bf75db72d0e8427765b96c9c0ffabc1be1d9/ruff-0.15.6-py3-none-win_amd64.whl", hash = "sha256:aee25bc84c2f1007ecb5037dff75cef00414fdf17c23f07dc13e577883dca406", size = 11787271, upload-time = "2026-03-12T23:05:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d0/578c47dd68152ddddddf31cd7fc67dc30b7cdf639a86275fda821b0d9d98/ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837", size = 11060497, upload-time = "2026-03-12T23:05:25.968Z" }, +] + +[[package]] +name = "tomli" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f", size = 15164, upload-time = "2022-02-08T10:54:04.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", size = 12757, upload-time = "2022-02-08T10:54:02.017Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "typed-ast" +version = "1.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/7e/a424029f350aa8078b75fd0d360a787a273ca753a678d1104c5fa4f3072a/typed_ast-1.5.5.tar.gz", hash = "sha256:94282f7a354f36ef5dbce0ef3467ebf6a258e370ab33d5b40c249fa996e590dd", size = 252841, upload-time = "2023-07-04T18:38:08.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/07/5defe18d4fc16281cd18c4374270abc430c3d852d8ac29b5db6599d45cfe/typed_ast-1.5.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4bc1efe0ce3ffb74784e06460f01a223ac1f6ab31c6bc0376a21184bf5aabe3b", size = 223267, upload-time = "2023-07-04T18:37:00.129Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5c/e379b00028680bfcd267d845cf46b60e76d8ac6f7009fd440d6ce030cc92/typed_ast-1.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f7a8c46a8b333f71abd61d7ab9255440d4a588f34a21f126bbfc95f6049e686", size = 208260, upload-time = "2023-07-04T18:37:03.069Z" }, + { url = "https://files.pythonhosted.org/packages/3b/99/5cc31ef4f3c80e1ceb03ed2690c7085571e3fbf119cbd67a111ec0b6622f/typed_ast-1.5.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597fc66b4162f959ee6a96b978c0435bd63791e31e4f410622d19f1686d5e769", size = 842272, upload-time = "2023-07-04T18:37:04.916Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ed/b9b8b794b37b55c9247b1e8d38b0361e8158795c181636d34d6c11b506e7/typed_ast-1.5.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d41b7a686ce653e06c2609075d397ebd5b969d821b9797d029fccd71fdec8e04", size = 824651, upload-time = "2023-07-04T18:37:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/ca/59/dbbbe5a0e91c15d14a0896b539a5ed01326b0d468e75c1a33274d128d2d1/typed_ast-1.5.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5fe83a9a44c4ce67c796a1b466c270c1272e176603d5e06f6afbc101a572859d", size = 854960, upload-time = "2023-07-04T18:37:08.474Z" }, + { url = "https://files.pythonhosted.org/packages/90/f0/0956d925f87bd81f6e0f8cf119eac5e5c8f4da50ca25bb9f5904148d4611/typed_ast-1.5.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d5c0c112a74c0e5db2c75882a0adf3133adedcdbfd8cf7c9d6ed77365ab90a1d", size = 839321, upload-time = "2023-07-04T18:37:10.417Z" }, + { url = "https://files.pythonhosted.org/packages/43/17/4bdece9795da6f3345c4da5667ac64bc25863617f19c28d81f350f515be6/typed_ast-1.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:e1a976ed4cc2d71bb073e1b2a250892a6e968ff02aa14c1f40eba4f365ffec02", size = 139380, upload-time = "2023-07-04T18:37:12.157Z" }, + { url = "https://files.pythonhosted.org/packages/75/53/b685e10da535c7b3572735f8bea0d4abb35a04722a7d44ca9c163a0cf822/typed_ast-1.5.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c631da9710271cb67b08bd3f3813b7af7f4c69c319b75475436fcab8c3d21bee", size = 223264, upload-time = "2023-07-04T18:37:13.637Z" }, + { url = "https://files.pythonhosted.org/packages/96/fd/fc8ccf19fc16a40a23e7c7802d0abc78c1f38f1abb6e2447c474f8a076d8/typed_ast-1.5.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b445c2abfecab89a932b20bd8261488d574591173d07827c1eda32c457358b18", size = 208158, upload-time = "2023-07-04T18:37:15.141Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9a/598e47f2c3ecd19d7f1bb66854d0d3ba23ffd93c846448790a92524b0a8d/typed_ast-1.5.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc95ffaaab2be3b25eb938779e43f513e0e538a84dd14a5d844b8f2932593d88", size = 878366, upload-time = "2023-07-04T18:37:16.614Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/765e8bf8b24d0ed7b9fc669f6826c5bc3eb7412fc765691f59b83ae195b2/typed_ast-1.5.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61443214d9b4c660dcf4b5307f15c12cb30bdfe9588ce6158f4a005baeb167b2", size = 860314, upload-time = "2023-07-04T18:37:18.215Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3c/4af750e6c673a0dd6c7b9f5b5e5ed58ec51a2e4e744081781c664d369dfa/typed_ast-1.5.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6eb936d107e4d474940469e8ec5b380c9b329b5f08b78282d46baeebd3692dc9", size = 898108, upload-time = "2023-07-04T18:37:20.095Z" }, + { url = "https://files.pythonhosted.org/packages/03/8d/d0a4d1e060e1e8dda2408131a0cc7633fc4bc99fca5941dcb86c461dfe01/typed_ast-1.5.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e48bf27022897577d8479eaed64701ecaf0467182448bd95759883300ca818c8", size = 881971, upload-time = "2023-07-04T18:37:21.912Z" }, + { url = "https://files.pythonhosted.org/packages/90/83/f28d2c912cd010a09b3677ac69d23181045eb17e358914ab739b7fdee530/typed_ast-1.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:83509f9324011c9a39faaef0922c6f720f9623afe3fe220b6d0b15638247206b", size = 139286, upload-time = "2023-07-04T18:37:23.625Z" }, + { url = "https://files.pythonhosted.org/packages/d5/00/635353c31b71ed307ab020eff6baed9987da59a1b2ba489f885ecbe293b8/typed_ast-1.5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2188bc33d85951ea4ddad55d2b35598b2709d122c11c75cffd529fbc9965508e", size = 222315, upload-time = "2023-07-04T18:37:36.008Z" }, + { url = "https://files.pythonhosted.org/packages/01/95/11be104446bb20212a741d30d40eab52a9cfc05ea34efa074ff4f7c16983/typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0635900d16ae133cab3b26c607586131269f88266954eb04ec31535c9a12ef1e", size = 793541, upload-time = "2023-07-04T18:37:37.614Z" }, + { url = "https://files.pythonhosted.org/packages/32/f1/75bd58fb1410cb72fbc6e8adf163015720db2c38844b46a9149c5ff6bf38/typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57bfc3cf35a0f2fdf0a88a3044aafaec1d2f24d8ae8cd87c4f58d615fb5b6311", size = 778348, upload-time = "2023-07-04T18:37:39.332Z" }, + { url = "https://files.pythonhosted.org/packages/47/97/0bb4dba688a58ff9c08e63b39653e4bcaa340ce1bb9c1d58163e5c2c66f1/typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fe58ef6a764de7b4b36edfc8592641f56e69b7163bba9f9c8089838ee596bfb2", size = 809447, upload-time = "2023-07-04T18:37:41.017Z" }, + { url = "https://files.pythonhosted.org/packages/a8/cd/9a867f5a96d83a9742c43914e10d3a2083d8fe894ab9bf60fd467c6c497f/typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d09d930c2d1d621f717bb217bf1fe2584616febb5138d9b3e8cdd26506c3f6d4", size = 796707, upload-time = "2023-07-04T18:37:42.625Z" }, + { url = "https://files.pythonhosted.org/packages/eb/06/73ca55ee5303b41d08920de775f02d2a3e1e59430371f5adf7fbb1a21127/typed_ast-1.5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:d40c10326893ecab8a80a53039164a224984339b2c32a6baf55ecbd5b1df6431", size = 138403, upload-time = "2023-07-04T18:37:44.399Z" }, + { url = "https://files.pythonhosted.org/packages/19/e3/88b65e46643006592f39e0fdef3e29454244a9fdaa52acfb047dc68cae6a/typed_ast-1.5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fd946abf3c31fb50eee07451a6aedbfff912fcd13cf357363f5b4e834cc5e71a", size = 222951, upload-time = "2023-07-04T18:37:45.745Z" }, + { url = "https://files.pythonhosted.org/packages/15/e0/182bdd9edb6c6a1c068cecaa87f58924a817f2807a0b0d940f578b3328df/typed_ast-1.5.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ed4a1a42df8a3dfb6b40c3d2de109e935949f2f66b19703eafade03173f8f437", size = 208247, upload-time = "2023-07-04T18:37:47.28Z" }, + { url = "https://files.pythonhosted.org/packages/8d/09/bba083f2c11746288eaf1859e512130420405033de84189375fe65d839ba/typed_ast-1.5.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:045f9930a1550d9352464e5149710d56a2aed23a2ffe78946478f7b5416f1ede", size = 861010, upload-time = "2023-07-04T18:37:48.847Z" }, + { url = "https://files.pythonhosted.org/packages/31/f3/38839df509b04fb54205e388fc04b47627377e0ad628870112086864a441/typed_ast-1.5.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:381eed9c95484ceef5ced626355fdc0765ab51d8553fec08661dce654a935db4", size = 840026, upload-time = "2023-07-04T18:37:50.631Z" }, + { url = "https://files.pythonhosted.org/packages/45/1e/aa5f1dae4b92bc665ae9a655787bb2fe007a881fa2866b0408ce548bb24c/typed_ast-1.5.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bfd39a41c0ef6f31684daff53befddae608f9daf6957140228a08e51f312d7e6", size = 875615, upload-time = "2023-07-04T18:37:52.27Z" }, + { url = "https://files.pythonhosted.org/packages/94/88/71a1c249c01fbbd66f9f28648f8249e737a7fe19056c1a78e7b3b9250eb1/typed_ast-1.5.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8c524eb3024edcc04e288db9541fe1f438f82d281e591c548903d5b77ad1ddd4", size = 858320, upload-time = "2023-07-04T18:37:54.23Z" }, + { url = "https://files.pythonhosted.org/packages/12/1e/19f53aad3984e351e6730e4265fde4b949a66c451e10828fdbc4dfb050f1/typed_ast-1.5.5-cp38-cp38-win_amd64.whl", hash = "sha256:7f58fabdde8dcbe764cef5e1a7fcb440f2463c1bbbec1cf2a86ca7bc1f95184b", size = 139414, upload-time = "2023-07-04T18:37:55.912Z" }, + { url = "https://files.pythonhosted.org/packages/b1/88/6e7f36f5fab6fbf0586a2dd866ac337924b7d4796a4d1b2b04443a864faf/typed_ast-1.5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:042eb665ff6bf020dd2243307d11ed626306b82812aba21836096d229fdc6a10", size = 223329, upload-time = "2023-07-04T18:37:57.344Z" }, + { url = "https://files.pythonhosted.org/packages/71/30/09d27e13824495547bcc665bd07afc593b22b9484f143b27565eae4ccaac/typed_ast-1.5.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:622e4a006472b05cf6ef7f9f2636edc51bda670b7bbffa18d26b255269d3d814", size = 208314, upload-time = "2023-07-04T18:37:59.073Z" }, + { url = "https://files.pythonhosted.org/packages/07/3d/564308b7a432acb1f5399933cbb1b376a1a64d2544b90f6ba91894674260/typed_ast-1.5.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1efebbbf4604ad1283e963e8915daa240cb4bf5067053cf2f0baadc4d4fb51b8", size = 840900, upload-time = "2023-07-04T18:38:00.562Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f4/262512d14f777ea3666a089e2675a9b1500a85b8329a36de85d63433fb0e/typed_ast-1.5.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0aefdd66f1784c58f65b502b6cf8b121544680456d1cebbd300c2c813899274", size = 823435, upload-time = "2023-07-04T18:38:02.532Z" }, + { url = "https://files.pythonhosted.org/packages/a1/25/b3ccb948166d309ab75296ac9863ebe2ff209fbc063f1122a2d3979e47c3/typed_ast-1.5.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:48074261a842acf825af1968cd912f6f21357316080ebaca5f19abbb11690c8a", size = 853125, upload-time = "2023-07-04T18:38:04.128Z" }, + { url = "https://files.pythonhosted.org/packages/1c/09/012da182242f168bb5c42284297dcc08dc0a1b3668db5b3852aec467f56f/typed_ast-1.5.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:429ae404f69dc94b9361bb62291885894b7c6fb4640d561179548c849f8492ba", size = 837280, upload-time = "2023-07-04T18:38:05.968Z" }, + { url = "https://files.pythonhosted.org/packages/30/bd/c815051404c4293265634d9d3e292f04fcf681d0502a9484c38b8f224d04/typed_ast-1.5.5-cp39-cp39-win_amd64.whl", hash = "sha256:335f22ccb244da2b5c296e6f96b06ee9bed46526db0de38d2f0e5a6597b81155", size = 139486, upload-time = "2023-07-04T18:38:07.249Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.7.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/8b/0111dd7d6c1478bf83baa1cab85c686426c7a6274119aceb2bd9d35395ad/typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2", size = 72876, upload-time = "2023-07-02T14:20:55.045Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/6b/63cc3df74987c36fe26157ee12e09e8f9db4de771e0f3404263117e75b95/typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36", size = 33232, upload-time = "2023-07-02T14:20:53.275Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "xcp-storage" +source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "mypy", version = "1.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "mypy", version = "1.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "mypy", version = "1.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pytest", version = "7.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "ruff" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = ">=1.4.1" }, + { name = "pytest", specifier = ">=7.4.4" }, + { name = "ruff", specifier = ">=0.15.6" }, +] + +[[package]] +name = "zipp" +version = "3.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/27/f0ac6b846684cecce1ee93d32450c45ab607f65c2e0255f0092032d91f07/zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b", size = 18454, upload-time = "2023-02-25T02:17:22.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/fa/c9e82bbe1af6266adf08afb563905eb87cab83fde00a0a08963510621047/zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556", size = 6758, upload-time = "2023-02-25T02:17:20.807Z" }, +] From 1db51d48209756cf8b909b6900e163cefa1f6860 Mon Sep 17 00:00:00 2001 From: Mathieu Labourier Date: Mon, 20 Apr 2026 15:08:12 +0200 Subject: [PATCH 10/11] cmd: add linstor witness script Signed-off-by: Mathieu Labourier --- src/linstor_witness/README.md | 288 ++++++++ src/linstor_witness/images/topology.png | Bin 0 -> 55615 bytes src/linstor_witness/provision-witness | 634 ++++++++++++++++++ .../src/scripts/drbd_install.bash | 20 + .../src/scripts/linstor_install.bash | 47 ++ .../src/scripts/prepare_template.bash | 14 + src/linstor_witness/src/scripts/setup.bash | 7 + 7 files changed, 1010 insertions(+) create mode 100644 src/linstor_witness/README.md create mode 100644 src/linstor_witness/images/topology.png create mode 100755 src/linstor_witness/provision-witness create mode 100644 src/linstor_witness/src/scripts/drbd_install.bash create mode 100644 src/linstor_witness/src/scripts/linstor_install.bash create mode 100644 src/linstor_witness/src/scripts/prepare_template.bash create mode 100644 src/linstor_witness/src/scripts/setup.bash diff --git a/src/linstor_witness/README.md b/src/linstor_witness/README.md new file mode 100644 index 0000000..3304aee --- /dev/null +++ b/src/linstor_witness/README.md @@ -0,0 +1,288 @@ +# XOSTOR Witness scripts + +In any case, the witness VM should not run on a node of the LINSTOR pool, but on a separate host. +It is recommended to use a dedicated, possibly a remote host for the witness VM. + +The witness VM should have access to the LINSTOR pool network and be able to communicate with the LINSTOR nodes. +The witness VM will act as a TieBreaker and will help to keep the quorum of the LINSTOR pool in the case of a pool with only two nodes. + +If the ssh keys are not provided, ssh will ask for the root password to connect to each remote host (witness VM and LINSTOR node). +The script will first ask for the current password of the root user on the remote hosts when copying the ssh key if provided. +Then the ssh key should be used. + +The script should be run from the `./provision` directory. + +## Table of contents + +- [Deploying the witness VM](#deploying-the-witness-vm) + * [On an existing VM](#on-an-existing-vm) + * [From a template VM](#from-a-template-vm) +- [Creating a template VM](#creating-a-template-vm) +- [Updating the witness VM](#updating-the-witness-vm) + * [Updating in-place](#updating-in-place) + * [Using a new VM](#using-a-new-vm) + + [On an existing VM](#on-an-existing-vm-1) + + [From a template VM](#from-a-template-vm-1) +- [Setting up paths and interfaces](#setting-up-paths-and-interfaces) + * [Preparing the network path with a `PrefNIC` set on the nodes](#preparing-the-network-path-with-a-prefnic-set-on-the-nodes) + * [Alternative: Preparing the network path removing the `PrefNIC` from the nodes](#alternative-preparing-the-network-path-removing-the-prefnic-from-the-nodes) + * [If there is no `PrefNIC` set on any node](#if-there-is-no-prefnic-set-on-any-node) + +## Deploying the witness VM + +### On an existing VM + +This script will provision a witness VM on an existing VM. +It requires an existing LINSTOR pool and an AlmaLinux >=9 VM. + +```bash +./provision-witness from-existing \ + --witness-ip \ + --cluster-ip \ + --cluster-ssh-key \ + --set-hostname \ + --set-ssh-key-path \ + --set-password \ + --skip-setup \ + --skip-drbd \ + --skip-linstor +``` + +### From a template VM + +This script will create a witness VM from a template VM. +It requires an existing LINSTOR pool and a template VM that has been provisioned with the LINSTOR client installed. + +To create a template VM, please refer to the [Creating a template VM](#creating-a-template-vm) section below. + +```bash +./provision-witness from-template \ + --template-xva-path \ + --vm-name \ + --vm-network \ + --vm-sr \ + --cluster-ip \ + --cluster-ssh-key \ + --host-ip \ + --host-ssh-key-path \ + --set-hostname \ + --set-ssh-key-path \ + --set-password +``` + +If `--host-ip` is not provided, the script will assume the current host is the one that will host the witness VM. + + +## Creating a template VM + +To create a template VM, use the `make-template` command on an existing AlmaLinux >=9 VM +and then use the newly provisioned VM as a template for future witness VMs. + +The command will follow the same steps as for provisioning the witness VM from an existing VM, but will also +strip identifying information from the VM to make it reusable as a template. Then it will shut the vm down. +It will also skip the step of adding the VM to the LINSTOR pool as it is not necessary for a template VM. + +```bash +./provision-witness make-template \ + --witness-ip \ + --set-hostname \ + --set-ssh-key-path \ + --set-password \ + --skip-setup \ + --skip-drbd \ + --skip-linstor +``` + +The resulting VM is now ready to be converted as a template or exported as an XVA to be used as a template for future witness VMs. + +## Updating the witness VM + +Updating the witness VM can be done in two ways: +- Updating the witness VM in place, which will keep the same VM and update its configuration. +- Using a new VM, which will provision a new witness VM and migrate the witness resource to +the new VM. + +The update command can be used to update the witness VM or to change its configuration, +for example to change the ssh key or the password. + +### Updating in-place + +The script will follow the same steps as for provisioning the witness VM from an existing VM +except for those differences: + +- Unless any configuration has changed, the `--set-*` options +can be omitted to keep the current configuration of the witness VM. +- Since it is done in place, the step were the node is added to the LINSTOR pool will be skipped +and the `--cluster-*` options are not used. + +To update in place: + +```bash +./provision-witness update-in-place \ + --witness-ip + # The other options are available, but wouldn't make sense in most cases +``` + +### Using a new VM + +The script will follow the same steps as either the provisioning the witness VM from an existing VM +or from a template VM. + +The two following flags are added to the command, +to respectively specify the name of the old witness VM and if it should be deleted. + +It is important that the new witness node doesn't have the same name +as the old witness node to avoid conflicts in the LINSTOR pool. + +The process is as follows: +- A new witness VM is provisioned/created +- The new witness VM is added to the LINSTOR pool +- The old witness VM is evacuated, which will migrate the witness resource to the new witness VM. +- Optionally, the old witness VM is deleted. + +#### On an existing VM +```bash +./provision-witness update-from-new from-existing \ + --witness-ip \ + --evacuate-node \ + --cluster-ip \ + --cluster-ssh-key \ + --remove-node \ + --set-hostname \ + --set-ssh-key-path \ + --set-password \ + --skip-setup \ + --skip-drbd \ + --skip-linstor +``` + +#### From a template VM +```bash +./provision-witness update-from-new from-template \ + --template-xva-path \ + --vm-name \ + --vm-network \ + --vm-sr \ + --cluster-ip \ + --evacuate-node \ + --cluster-ssh-key \ + --remove-node \ + --host-ip \ + --host-ssh-key-path \ + --set-hostname \ + --set-ssh-key-path \ + --set-password +``` + + +## Setting up paths and interfaces + +As it is recommended to have a dedicated 10Gbps network for the LINSTOR nodes, +and that the witness VM should not run on the same pool as the LINSTOR nodes, +it is likely that the witness VM doesn't have access to the 10Gbps network. +Therefore, it is necessary to set up the paths and interfaces +for the witness VM to be able to communicate with the LINSTOR nodes. + +Let's imagine a setup with the following network topology: + +![topology.png](images/topology.png) + +In a typical setup before adding the witness, there are two likely situations: +- Either a `PrefNic` is set +- Either a node-connection path is set + +To check which situation is in place: + +- Check for existing interfaces on each node: +```bash +linstor node interface list lin-1 +╭──────────────────────────────────────────────────────────────────╮ +┊ r620-s1 ┊ NetInterface ┊ IP ┊ Port ┊ EncryptionType ┊ +╞══════════════════════════════════════════════════════════════════╡ +┊ + StltCon ┊ default ┊ 10.0.0.11 ┊ 3366 ┊ PLAIN ┊ +┊ + ┊ 10g ┊ 192.168.1.11 ┊ ┊ ┊ +╰──────────────────────────────────────────────────────────────────╯ +``` +```bash +linstor node interface list lin-1 +╭──────────────────────────────────────────────────────────────────╮ +┊ r620-s1 ┊ NetInterface ┊ IP ┊ Port ┊ EncryptionType ┊ +╞══════════════════════════════════════════════════════════════════╡ +┊ + StltCon ┊ default ┊ 10.0.0.12 ┊ 3366 ┊ PLAIN ┊ +┊ + ┊ 10g ┊ 192.168.1.12 ┊ ┊ ┊ +╰──────────────────────────────────────────────────────────────────╯ +``` + +- Check for a `PrefNIC` set on each node: +```bash +linstor node list-properties lin-1 +╭───────────────────────────╮ +┊ Key ┊ Value ┊ +╞═══════════════════════════╡ +┊ CurStltConnName ┊ default ┊ +┊ NodeUname ┊ lin-1 ┊ +┊ PrefNIC ┊ 10g ┊ +╰───────────────────────────╯ +``` +```bash +linstor node list-properties lin-2 +╭───────────────────────────╮ +┊ Key ┊ Value ┊ +╞═══════════════════════════╡ +┊ CurStltConnName ┊ default ┊ +┊ NodeUname ┊ lin-1 ┊ +┊ PrefNIC ┊ 10g ┊ +╰───────────────────────────╯ +``` + +If there is a `PrefNIC` set on any node, the witness VM will use it to communicate with the LINSTOR nodes. +It will then be necessary to set up the corresponding path on the witness VM to use the default interface. + +If there is no `PrefNIC` set on any node, the witness VM will use the default interface to communicate with the LINSTOR nodes. + +- Check for existing paths on each node: +```bash +linstor node-connection path list lin-1 lin-2 +╭────────────────────────────────────╮ +┊ Key ┊ Value ┊ +╞════════════════════════════════════╡ +┊ Paths/drbd/lin-1 ┊ 10g ┊ +┊ Paths/drbd/lin-2 ┊ 10g ┊ +╰────────────────────────────────────╯ +``` + +If there is a path set between the two nodes, the two nodes will use the specified interface for DRBD traffic. +If there is no path set between the two nodes, the two nodes will use the PrefNic/default interface for DRBD traffic. + +### Preparing the network path with a `PrefNIC` set on the nodes + +These steps must be done after adding the witness node to the pool. +Assuming the witness VM is called `witness`. + +Set up the corresponding path on the witness VM to use the default interface: +```bash +linstor node-connection path create lin-1 witness default default +``` +This step is to be repeated for each node with a `PrefNIC` set. + +### Alternative: Preparing the network path removing the `PrefNIC` from the nodes + +It is also possible to remove the `PrefNIC` from the nodes and set up the paths on the nodes. +This will allow the witness VM to use the default interface to communicate with the LINSTOR nodes. + +Create the path between the two hosts: +```bash +linstor node-connection path create lin-1 lin-2 default default +``` + +Remove the `PrefNIC` from the nodes: +```bash +linstor node set-property lin-1 PrefNIC +# This step is to be repeated for each node with a `PrefNIC` set. +``` + +This order is important to avoid using the default interface with the nodes when removing the `PrefNIC`. + +### If there is no `PrefNIC` set on any node + +Nothing needs to be done on the witness VM as it will use the default interface to communicate with the LINSTOR nodes. diff --git a/src/linstor_witness/images/topology.png b/src/linstor_witness/images/topology.png new file mode 100644 index 0000000000000000000000000000000000000000..571e256e242e3a7751a051d4f546ab5c7b8edea3 GIT binary patch literal 55615 zcmc$`cR1B={4jo`sEkNvR>Lamph#Jv!m)SuF-k(RvX#gt#Ia=@dqrd`j+K>@k@?9! z)=Bn$?ze`%-|Kq*`(3~1kFM%|-{W<^_PS4iiqc)O(+sC!Fc_KKJsC9^?6?{D6LgXY zj9@LSB499Yn4FB1#xui(k)1*V^Tf?%ypaIUeF5_(7C!_##gpn_L6ggt{4M-4YNp*v zmp+neoRrb<6L>)@nU^XRbnVo6?H?arF*qYIqlBBf*2rO9B+(Cvd^bh0GIEy#_j*NkH*1EwL43rP7B4&>t%JDL3OABEpb0y8>8KETI=h2?=r?XutgaX z8|=lH3C=pA>Y}y!32J$Oe06I)rR+8_#0O?EJ&j_uQLSuI#ytmA=6f*E>*}!TdN4qq^2^ z-?CxpmpTTE)NG1g9eYu!h$28SgWW0GsgZ4Cl7##t(^L;-=)G~QEii7?m(i=3Qrcda z5Tyl-!VDIjvV2p!Rtc2mhU_EyZoboQa=M(H`2K&j@0u#!r{c*zkdR9`|WXuWJ)>$(cL)3ipvyUW_dOCNBQqF@V zM15fdDb_jI_!p#yTM%`AAx;U&nQwuGb4U@TJg2RKm8!0lvcY~{2K)U$(t2M9+x+Bc zfqmud`r_;<4zpJ073FJzO)7*FWL;>1b!+wddJwckvJ$E^uoV(KyedV+>IF6@lOpoI ze$^-VD#5Wyx!+SQ!1yQtf?CO33cb6VT04{Aep-rLkU_hlu(5<2E>T1%K|> zT*KL8-4^?ahd5GcATfXqwx`y@T!0)r0)#G&{e_&AJM?Dy>kPl31=#%x_~jvx4p!Sg zuz&>++Q0f!T!Ntf1Ii0hm^B!lRQ%sC{y+I(9=;6jodn_Nf1j^q7Z>sSu70e9l6rtc zwjd-#Nlg!!hAG+}^A7k!Dd_J1(xL4C@`3KXwxLVLv#qHZtZ+jDI22;#Yscx0qmo?b zvd2?=$mjMFDz@ixvGH$TgR=owr+~AAduldDZ`w;T4V83>$1fdw|L;oABOH_kZ1lU*0l&3#aTA_rE6R5PvNmc{UM(<5dCd)RGLC$n?HX`+Q-Y;jn#|7=|rI zUoF97{TrrPV&AT=!uG8Q-Oc4js-Tii(cFP5XIoc6`hj=m3P#}-``hzagOupsKsJ@0 zh#lKsFUOpq)mLG3|MAg-IACh}3jd;-VIoJ_d7BRxYGO$rk5*UU-MIHjSAgh10-6OU zlbXiR8ZGQ;d9Ka%mK;M=VlERcs*(w0COX}R&MLK3AKMsMEFVky=OUGiX5)`|Ig;If z=FRq$8rWf|aU=Xvy{z`1PI06QM?U||S62G4e0NlqwC$#e$k%1znVGjHnwB9 z{5s6tOzW9kxqau2%z+B~2d=cGD%V=7=MC`kw(NvP+dVPcwFgO_+c*c>tUotZtsUsN z(bAYL=NPmaJv0PxHJVLDY14A|T1(@4BbD;GF!Ak&cJuk=G-I1h!hu1&m06o@+O=x> zuDA@$wSIl|-TjR^W&Dd%SJP}OYSss>{-IzRi4@v05f$dSnpX39smZ*c^)6#+Gj(xK z;f($ewP7Na}^9Ffy; zPLBo?`o4-MZ-33Ss1<+3`1GEupwM4T-VVR9vrzJY3Dvxwv{C)wvbMuFz!=5WLuKQ4 zCAL*{Tvn9t5kn%gtDZab^$ayQQ)hITKYthr&n$> zZDsxf9SctlAMVzp_Gtjw z*pGWb!pkYXUK~c<%;c<(w|D5%eLx*!;G~OpDWjuo9S=08oa-wv7}}w?8xP>1nMrc9 zDYnF-$17%%6voU`=57xy4;WwgBk7eCX15t-nqA>I0w>3yvCf14h$VEsx(ZY|<9Wq5 zKc4BV>~g5Wq2qg;6Q_E$jd-p<9^jw%_{>A@YI@Q9?YPxefUTI=4bys*A2_B#7s7@2Dva6R{w0vJK;`j9YjWxp=VQJ-sgeVJ09`u zR@%>361#7mM$$GJWzNOf$^KH`W@j+%ig`fYPjJH1tZtttL;ulslIr1XA zN3xV&W6+j=K-d;zlo2G}Rh|BF{&Tf<;bQ#+TN`x;d$l_{a})m8ypSQU{YBt!mM2&a zK5QKHZWsg(*;HBgnPv|m?^~Wj9(yGIoohWH_f>DejHvp25+qFgq+x>1<;g3fu9kucTx-1hv2-&+DDGtQ|aa!fwzO}zOQIaZ)rD5?3 z*Rjg1G;)pW0a|r=v=QMh?zvGlwzj{d+2G2Ut?adp^}v_wSUw5lVHZZ8<+|8dUb~jB zTOM+I#0)V)6LypOd#8Qgw54bKD&s}zzE4XCX4Vq3Kz-0LM8+6 zpi=$sfy9ok?U!LGlA*r*-k;slXF4kD29WYo5emFG#iCvO!M^R7rVa9ZDJ#A+@#cdO zr)l}QjTf7RaMi!ALLQYY^f zl~sLuc{7bse2z{X+XzS5vrz4R`9Q8Xojehx=L#DkGr9@UTGU^7M)=A^c(NIuAkVVX zlropvsLRBzuH5&qX~gLiQT#D;24@5^KAcv|i&S$CSP+@INN${q%#T@>6fY>ype9bn zdGzIH+wHCOZ!FTzTc#5i!c8bL9W^$Bn!}Zt8#V5VzCNo?=_Bssn!@y*gL=bgBy)Hi zmW4a5*5JvQ?U0{{;dXWJ)zn@ax$8cPty5=ExKrFGx9u3B9;!wu*Mo~bc}3F)op&)# z$bfCGxS2VgTIHgoCRyBQK-aeXgfPEznUlXQ$5~;^wFXJ6X<-q5Cq%V<(+> zqnNjYgs-~0T-m=RYTsbKz(E~A4s_RRhjQ_>XN;P~(F$uGaeWf>mv3WJveR@pru@#^ z%-Y%9yQ8M^t&tb z$tybt%}82yC5d^IV`M0}U;Z)nLPW6?^C{``CK{O+k)rHk&l0=(N-3-u5gHqcnL;_^ zLMd~#z0Y1Nl{8M|-Q452sJ=~SdcM@l^}cpOAJ$AGEN#xpXy^XezE7X{DavAI&&}_; zuT51n;;n+N`NW}b(_BhY?hT`);!zwIs+!xURFKC}7muI)Z-v^VaJ#m5EwuKBFW>4& z->SY+1_Asw!e61Me^1O<{Ex1DN-kj6%}Bg$*PDb%XgizvO5+U8rF&7x#gI8`oaJR4 z4VJ}DGlkvqKYEAz%9@U;7sWlfY8>ou^+_SOFYKYFuFuNf#oaEpX9^FSv(vyV_U;jT zbY66df^+pvX6Ga+(HNx(iDu|kd&#^s2~zjVVc`&GQOu;1&B|Hw8g-fJ7^cDM_6{Kv z)3!t>f(5rnHv%}4P!)>{C0zwI`Eu?oS{#^hyq4=y#mx57N=fqG<0&x{n?Fo_gE)4z zepLZ@5qXZragE!wy&Hwh_s~IGcd7$?CO=fFA#xjQi!qd;lxiV_v%oMpj69TWA zYi}mAs0`_urPe_r62a}R}3-fU*1zUCdRw$sW zWrhP!DheGNwyKdikdxV;k(rEX{NkRjj##ed!SvTH;R#m$I2o_)!vk^;G|sFeg9sNy zeuU=Mlg3H<6E=#7gudtQ+1Q)pkhoDY79u=4;H<}ZoKF_@;_o zHJ`*PFhsZUh|}T*WiRvQGM|&!bTr`W)bJYX3Yh&(B@KW8uzHYfN z(`x6`m%A*9Yw%n>M1A9ZR!YWkwJ(;X{za+`Le`YTmGI&SLphGJr`9nmT@rQp#a^uO zZANcx+rk1qftD=hyPEpQ>PlOEhRQ4KF*!y@v4f$N6(eq{1cTTYYnj%hle0?L-T3OfSp0#&saePM*LGUbgFvbEtWp z7Al&vel9Wxn5?iGF2A{S$ziXx+VO}6+{DAd9J|ZtThsTk=;0i%atEf!ft=S|UVr$owGhZj^X|L(q&PLYpjt z_tVXOn#>?fsY2wYyUa7hSh;18`f(Nz9<3Hw$iQ-zSga&Iy8nqQCz;K1_JNr#EDx1? z`5KiFK)8DHG9~GF`6|4FGSd$@z6H*wlbMrz8y6T*SPQXrPs58V*k$BMWjf2A3*bUISijYHSre%&UUB_CW~fVaK5sBf z@SaR3>SExGxxwS07Mh@~&i6m;@0psp4$=#Ow9_xoLoJ;CjfjZinR_!Ovca6nWZ%lH z^~S92L=l?$k8X@BCUmMBe6|oDPuWazTf*hj3nUXxQm~kIXMCD~Nw9Mv-wyjSOiyG5 zdd$6$KJFt@Ul{-NiWrDGYKkIi5A^kpS8v8~_h!Tx4m#Im179?KsMz%GB2LCWV8_3n z_5u&P*7SCD=Uv3l#_^is7KdS5gnMtIVc%sXwxH}R9g&-T70>@hh7@Mg9OO~`g+>zd z-Zho7LjIP%Rs|IneIK7x`XR7f%_Rmxl$8wxQzN zv!l5CcBj=Ve=H(3`@^#h;Uo{CE_C zrq0KH-H%dkTnMeB%%)@)q;odtpvghG0^MG8N}x#Ok1=@B{!GR@n<#dU8Z}#cwXpgf ze*R2**x$r}AhR8H@*Key4kaa2YXA!l6Z& za<~$#ZXB)b6hpW(s3X>@re;*U0BaE(E1-pez)HSrZ2@hk<8- zcK-c0qlj0)w@cm`p6N00CitTtkH;?Gz*nzqled*y4@EECex=D*&ZK?cb*)z$YvBFI zZSZ3i6fYljwK5970nB2L!E6x}K;!3GVg`xg{s@7F4`Np$ZRHaQ-zrmhbD7t{zJsvn zm4ZCYy}{%spHAU;C2t=AUg$XAx5gBOAE^=^@eK`hG$X$;86I1>XV=UWvvln8Ki~!C zE;epoSl$Y~CW~l6#6UsK^bSwoxnqA1J}EFktIFPYrZ{Wfp%MgRfjoL;>9#~ff1K%m zQ#^!Fs_l39DFl7E^c&m%Uw;TZ2~uZJrtBd|&vxJW%@#yZrV8XgiwVM6pcEoVPM!aa zKL#j6_a}4o|NDn1K8LzZ3?EvUpuQ0R0%gnoUjUS5zcJ#xQ6^HveTH|UZh3Fojrac84GP#6&{_;pe9xH9ed-|%Mbc##CGHSWzr0o#{7%vkkVuXBA8Y5qW@pl13R7xI_tbRP5ig&rD zR0rq5!@eZhe}t2wCzg`*eH+TIb*ZFSP+EXQv0F0yxoh)v z07I+|o|PW_DPsI5`3SB?#~s4jU|8+^wx@Z*ExO>h+ylNgE)()Zh&Dy_G+%4;^)>1F zEl*&{tab?V^4^zdI(7N}QcCmLD@Xi48r6ilp39t9wpaWKd350)ehEl~T-4jo|7;Ql zukhfTq>K3Lxr3e22Ay#9<+g6wBVNbmksJ@r|b zhX6W3FLCYC#J^ON);>u}GGl)y{+|<{7Zp7dcP@Q%G->I$Z^p`v^pHQdJWl_V{W57- z7n?PtM1Z~rN+kR$fyt)3k|ot&g84COHawqMdJ-T|>U*@8O(W_HmH9)U&kkz~gi?M9 z{_u%QZTyx;z*kV1HYrD$MEp6gjgXi4Luo9X;Ehz$$culE>~=!7PX-7eUL5TnuPN_J7CH|aMddMHX$kE16 zeguy7#iq$L*P?~_M$RLYm-^C0vZ(s09#QWhA)MW4N~$FJ0x75g{obx2JpjH%WlneE z2oAZkrj^oBCIf$NcbuMGHWSuDa7wR}1T_r>BC;L}`O@pan}6a5$N20 zbPb&T0vi8}M1he2fv6)i(A*9-Va(|goIk7h52RZH74;$k0!RC|zoKin>@R=?*73LY zG~q>(MqX}1{IiN3Novj7h5(heKYT0-M%2VaXwA|8(1H~Vf`zn4)1v64<2BFp;I1DH zU9oZu_xUL~(v@#>q#rnMFuhFh7Z?Ga(r;#^Xrz03xy|8-xqRrPJ7r>*j@SkrPba-J z?xH9DYL9WA=I;R(t^sfdUrwyzU#zw}oHdZX^ z;;Je;RJ?;qNMNKKjNBX}{NpGCCa(PaoU{7l#%*ZH@4aktn7y|LZy#*P=rhN!AEBi( z)0JXk{NznpXJ4MK`mMFE>NrKSECzFTe5vyG3fXTnA6p#& zZ&>av^nAZ@W4F_}7Q3?oLUyBSQ0!_olm(e+?bY?F+0>w(_?9U3n8DvAGyr~UjL#d_ zGh(YMcJ1)ybHc+)M>ID!O{HU9fAx?dI0gIW@N}dg4Ez z7tTEo#GyQ)vKM~k>9AdMhH%GEkf!f)XJ!GlsBx;~&BpfRF0qZ_T941HD<98z7CoDe zeGs^_R8{z4BJ^gU8@{Ie*>_*s?R7SmUP3Q*fyLiB7@+hgTc699fEdnYREiyqM2;O6 ziX=e#8!IR^1Jwd*cwQ(Zbe_9_-gYUpnMrguT(h~m^zA`1k4@d)I&!GY8mCyYFi@NZ zmHzN`q*|U#n2z-y_T;~1ie0&vkeWEW#A8l9dCtS8hc%LSc&F`Z;W=U^2Nl~fsP)04 z{LRijlwP4WYFh-wA2&OYjHH38A)vabyjtTm2g-olh8}CZs~v*KkvLngouR{Gcx+>nmxg9`#t!!LnkhGEt)?2s@^eVl zrf4>p!fqTR^U>^&c*}GoA-8Vavs?$euZjzU3MHugb~Og54Kv$bEiBO3{?_HGrSClA zDCiZ+NoCbB49bx;d&(eR*EU*`_-s6&W)E38O=7fpd#rzc-sr$}thOAaF50b&b)MYn ze3~;=Ubkm$o9r*zyYpeC^X3D^ohYP1{L{LV*N4h&m7U+-*>WifMch2nnOk<6s0xfI zzj^pcN}<}(9S$t*v=M_b--3b;qul?>< z;zA|LH$GAgsorJMth)CEvFN^*YqY3m&#v^EVRd731n=4JOphNB->B&7h}->6Hq|99 zh5na4-qf{jIOb}TF&2B^-Nxd#4xXOQU{J`ZU4LJ=W)r3!nltW@@>9Gmm->yiNlw94 z^`?lEg1xy622Vbos3{+FozJh4VDVV}QX_Hj4OBj-LJ)~aJ4B;j^ev}=F9 zi4CJZ2sxERu~G7%D=F5)lqXKGQ^a(-_-+(K>4zVsf)}v7M=Cl=pVGpn*HNSM#!SNK z5OObij$jn;b+9v8Gx`K7rr1c#=ap=aGG|J>!JpTDSP8U`tKFr2cM$A%EoeJ)ErQ@W zV`Kg2X?e<*<6XrEEc-uR)p}f-Y`m=e_yP@KsK>j;z#>%@O{MNdsebztSeXYDf5bq?4@J+X?y zZ1`m~s2Q)VS1i2y+!Jr1B$#6Z$ElIAP$G$HY!|3Vj>XLP$-Lz1e;L3@l}U@?l3j+k zWvLh3P`{J}9B;ZDw)Ob`%zo8qChrKN_BOx$AJz{+BUN6>X`kga=v{pi5YcjG1V|Kx)hm09BjNAmK`K<(-dfUuiJ;AT_K-pUQL)4b>q?+KjueRd4QtD% z>-e!TJ!K2j#njz}2NG9pYPY6G-$eF=Yfw|&Dj)N7vmNv2FSclBwPS($2oB4kKaXM>hZuFi|HzJ+ z|Lc}enWLPLh}t8Vm%xY?uWce|e)?^-Ab%VYzZz&ddG{|CV##qIvpJ>vHIwnY2T4a> zqlIg_YLnCz1I@&fE4R90e!Cn}TQB+>8yzUjXGP)Yn@4BiF12-QA`#_BdKU{KU#mvc z(ouAXj!`&xnBwnV^fL5M*_s^mTpzLh%2S*aIIik9><|CP;Z!+m z-XeRgY4cmJ!C%~wYNnD}b2K5&{JpMU6sNRa033_yy7=39qbIUsl>u=Swz=!VpP@O8 zGu}+3a5*G!J%8o@fD`rbB8dBt?W;#4CsAH~!XhK$^i1LvVd%s^XLT z9~w0V=}Y;U*rjyn-@5xh0wnu-MW!w?n%`MR;Od#2V|*6*Gr{=n5s~g-2t+bgEB>1y z`%|QNs6s?f{5klxE9;WWb&sAIZ@LEeBe@8^?<%=eab2vZu!gST_8$;V3It2L8ZT}2 zdGz{N!u>G+#ALLp7OwvAf1^hb3XO0y@pB(hd;++-ZcC1Lx8GBP;OvO*a`(;*iyi!o z@l?dnMav%{PmQtO`BfoX+7V^=<(8|jCi{?2u3T>7zjCxQh@d9rf=&Jj(FSivwFc2O zWF1|pG=MXb8183x1oXprkq^!(V}?l5i9cbAO6>r(*+VK-bYdf2Q}(v|iasIK#|V)6 zHeV*zDfVhpHRx=@kO>c~>~44+I2QxAy>`gBcrM8nHVJGv@MXI#QV}=XsZBpEjZ_5z zcjO!%v{D2C8&oh*?P6a^!mWd~C-;UT5s*nKP2?2RT?v4jfD$&QDdfD{4!2Ql_2!J% z{sUD%w)lA$V#%t*n27!<2ojszO%VfiUS7raGo9mNc$YBFhVJ&63Svn&Xigh#Rku@W z`!k-)O;I`O0_IU$nO@tO$Ob<$Tme5HiOW7gw>UuAVWvc2q8LR-g_H-4M_Yj zd7D*rJ2zGRWOZllDZHJajN?gx$vaW>`IshD4~Q|wHui}bpGYo;9c+eS#`YJ-XgHFd zrC#MPW)1RSK6dEWf~^??1k#GX(osRZ0I`l{NWEgD>@w8Zvr*NM3I=OHC)xNOu;lG~jiLf~8SP~3fHz7Dtnm8*K9YA|XD-1ap_Wz( zvzAy+LB@ta`1h$8BVMRYrgkTYD;5BD!08FR8z5pW{7I281duyGx(m*!?LDM|O)r5B z%s*_{^Jw~1`-;W$*)nJ%fhHFv#lR4tjS-?ViA*d;&J?lhl5OaDljWHUcvlt-^# zz(J)h06N9^KtZ#g)?A+^o*iIw6v!CJb{kx1<8J3fBC-s(xA_i!@&$#Q7hd15ONa=_ z<qB7NB{Gq1@M6Ou3At1Cf2@$6#&6lcII>#p?m3a$5 zFXO5O+8?O!!@guzY4SN_`)wZt3e73lN=MbEVAT93x}b6{XLM-#CL0xUteM5D`Wfh* z=!r*N{LLbub=SK8oGA?z=#d+66l()X>z6-!`iYsJ8QU#mXuGOCYSjfO{@~|0{a_w% zQ{x-A{q4eo%8dhJ7WXH1Aaq9KMKd2o;l`gEp5M1|l(M*>E}$YFQ^lZ6P^olS&g<54 zgtM2S0$qTkKD2YtKz{S_7pq{0Yi&`P=8D@pqP2d2sM~q0REXBYUR;QKA<({_GewF{ zGvS}zU52<mgUGc2vaMfZxctHRNZ|Z5ThQzc5x;2Rd0Zx#$_*FS?M` ze*!k?TAR-Y#xs(*XrZRYj0muUGwmy8*V%Rg-Ri|CbtNvRg#q{~E7j8w0qbD+g%_U- z460m>O7v`NuGLgDN0HiFPS~IKK5Uo4$*|`pRV7Zxv z4`gHjZ`=fZYETc968`Czr$GYNA@B^HtL(LgP~`5a9>zsK-7`HS46_HS9rU3i22<$l zp&SAz^%xmF=FBwxM0n;Fzx|txO?)BHQj>-;>@l3`d%LAucAXibZ1(j8#OHFg3zeKr z0)ddY{N6?FT2o_har=HUJ&O{ypxl~?kc ztJ*a2!nR7zrt<&nAP;s}{S2i8#^c9!E%=%mt&8a%yRd2{hMoeEeSf?4VSwDlsgZU! zo~Cw5Etm_q&(VBFqdS$iA#3?d@=-4Vnz^n_ttvc>3q94#x>9(sT_|YF;`?a#VS|(j ztto<^O*--CxleLlc!I+MF`1N~9H);$9<4oSA6>^pSD;OA)0>S8Wrn#=Uh)9VgL
5 zX-_Qj^X&!&6L~+We){4g zeyUUdMcHR>74+I4h_XjXS?H5&Uzba3A=h0zj(-UphR$} zz`y(QTJw+LP(MIAjMl0zkA7zx?&wEv-m??eM5ivmCY`#m%k4kz(A-~|hy&9IyZU6R<)Q)eW0pDaH=rA*(Xbh9PsdEGEM=qD+zToBMHBJ5Rzp5Uq%# zpa*O=>ws8llcYLud~!nqZFc0p(^esF_h*{YF{=w;b43lN$2h%jL5@WAfcaUpwWuFP zkWH|rE&0rT$gvg>0d=SBl1-*u{aDY53N?8P4#|8JPH+eVP)r|s&Q2gj{d@ztVnJX| z4SbAiY=Ue(pIZojJ$kvy16M+u>f{0cjvDlHLmzXbn)pZ$AJw$Kb^kKY=y9jRfmET` zZNo-gKs{BIX(H;rH{?Rw7lo2e7>oL42(Sq<^0IK^KJvI;Iu`0}NJH70O2tVb^$)%nAxI@2F56@nvhbbhT-2DVtxb18G_j$fzJEnnKWKqL&jx ziwo!v)8{T%j7Z%m+d=BtL<5BGMhn9g_7?Kfz#rlP{p<-#ckbkNDed{N(H%Al4Yj!a)GytTf!Hr4&3sfKt0u%Cq8tN(rYA7zEwL67wBIX!u z1bVimQc+*19;0&_3l9cYuh#TB#o7dO0D1Ky$zIwPL%I?=v?(2}y1*6#FhqnYP^2oG z6&fBSz#WC7YXZ0T%z>j5+E(#ozn*ekZYzRT2&)UYWXA_DRx{t?d#YRF-v~?w6RU<{8zAgE?{sOv|>X z>m7i`^E2)avgAvZiw!l20G{;cXr8O&KrYk;sAw`ddKEE^LlaK0N|0NpOReOekiCXB zMG1j?w#pq;C53tu9jp?RGU=X6nV{WYGnL0C#fsw`+W#64XP?)AG3XT|`EhlW$HePb z!GU+Oc-zzY>T}%Z;m3BZ5o7lRU(H^mU=HSN}L8BENjWyf-Y1=^1_V%c+ z_WDp(`Lj{XORX*aTa)3^7K?)Y(c%a8-+Gcw8;0A$RQGOWNZ{wo285Rgy>TGC36@&( zb=|%_Y*F&+e77ZO6}9eiJ{~>VkAj>a4!bEaKdC3c?2K;I)_U~djfQ&4pGFWtPX%GYDwR&{ zCa;*Z^xYQU<81h~`XUItTLD`d%McmpT7q{pypA1>72{`Nh$L1**d-u`*9F-K51sq^ zlc>P&Ag$nsCIVNwK0G$`0*Bki5|1ZHgQLI+ z&^t(De?)V5+DMf%6g6A}Q%ymQww?k^y}Pr%c(u%CcX819}Ila~VuIyZ{f_j^?8SDYiZekOD+NupppVEW?#r8G$vx_>rCO8G(7r0b7}f_&(e8tzgJ{bfC_#75rUux z&_JcZf%GfHOA3HRB+0Pw?sl*{L4Kj07_X+M2ToDKy?8# z_})qpYVz#VSVA7`s3jbU%(&;h5rox(&5-YgO9*WArLMB zhwha49P|AZiw(Hj zK4|+feiR$}{0l^oHE?Y-;7+Xot2G9O}JQKg~_2g?r8?{Gh9?Ld&lJ9eJcPfOLZep3qLF-45+@ zy}3NoB&JAk)+pc~{%Zh#X3z{_z;%cdfXw88GXIf5^8q`R`T*v52CC$P4JJI^qI)R^tHdF%*`)eS7Ojs z*ykgg`0Kvw-~@ms0%MRhf|+%J?La&PgThi)D*$dhfUDaP?4t`c!WP?%hywgeK^GPP z7k-R!XpiR6+g8#BSRDZz<;@FNq6MISRvaMQ0kjZFMgx*N00knc4X9!U92*40hYp~V zP|^wd1c)q23w>$^pID$zA>fl1^a*h9VP~5e27Ka!K7sgE@;dYhs0zs#2x5RPV{mQ( z;Ek3SI678vm6WRhGw;B?IRP`IkbwW(2lrAjhrk2V9*SY%l5GIP6#&+UHQ;StqXTI- zM`FZ{|GWtUdvmoR}8mD|NRx9M-FGa2Q%!y{bDvP0KXZi2c$rQE5E$YUjuKT0SG!q=ujZ3 z#uotA!_NZ*_raM6xivtH32LS^pnP@Dnb5lrx&spc3JDYuz`hnEc;$it$t^(g6l&-;C=|;7#c@G%e0_nreN7MXc=IY`o?rD{1uav zjvYuMpsNVD0{|xu0+Vlikw~#Q&#-0?Ctu$U(KH0ylKqndDLAeftCUMBI2NBAPaB&7%a1f?5*qUTg-Gv zJV2;%IhKBuK&OMvm$K&a>#fP~RPN`(r(-jqoL#dpyO4+-dG3+4FCs4#$ZcJ zGXM zBhAY5uy&v)j7PyK5Hdu-t)>^j+v6*|HLE@24WNA8zV6HCeqn?x1q%UB3fy|STL-%< z80ay6Z7g|Ep$lUv!3YFX27u|#EO?l3k}YP9sB9jj*}<=yt(_0>|Wl7e&W>BHQ@6rWecCg^EN94%#~)CR2#8Vf_^d^Op9 zK=nNkKbUfwe0`UOWoMw-weDMi8=|mo-<5B27jzDALnVdP=^K0Fv~{ShXLp$#HSqi1 zPCHNoL%gerE6VIRJ0J$yu$nFi_vUHpcc^cOsWjQKPd6yZ#-H#9ct3A3owA#gTW|#k zQvA~OvuAGokSw<7h#Pg~v!=L^vwKS>WgGNVse|kh3C<>@%yvxNo&`$2@@46R!!aNf z!W!%}9&6-Ccyim;`jt?7)KC>@QW>=^&mEUn>-FdqnFuyC)Ad&dF^M3aMe6j!Ow^-* zTDEjiE)+{yoddMjkSuSy#Yce#%eXVH#TfjqFQZK|CP^c;P| zBiRU)I&i$|-=S*9-q08D9#*{RMk}3!`ozp8cy;$ai%TXlSn~Wyf%>l>*823^s2JdI zRn5et&DBf^gJJM<4VX9DhBxesnDHgyw2ziA7FXASH!AEkE|YDn_Mi<6Ev%>^^ZFSA zM}h~Ikv2?hU8mA58FT~{mgAwuoWq|jz-y$UM7g|j4Q%X+)`9QOnNQF_K@oCEi4;Bvc|Bz!9NwmGKNuPPExNGQ9nL4={q#U6B@f1I^4&J-! zxldg+;)2{BhYgni9sSV>0=-Amcpu~%2-;1%&*! zYioz%qO?c+Y11NAh+^p)Ji(=1!}dU_WL7badsXADTD5T~EpB7X%gYt}LS{Cw`S4Zu zCg_zI+K9*bv-xyRh2^Th*&&U~3K&ajDpjmdMogsFNrib+m19Jud}Ks|pxa(E5U?WY$6*Q_02iC679yNWr(qk;@cg%zaTdCH-9E!)h9xKy8h4)R<@5 z8r9LM&$zCQVonBXs~k6!GOxLM@wCHS|0I3aE!z#mhJICMU2=|k+mKj!5=f6IA9Pw8 zv(6$inS!$^D3iXazZ{IVY-t7Vr`2Z%{=B+m=mlM!Zywc?j%)p!vH=cH9z0;UCN4%o zeFivF^fjRKs-o+jpMzxnmhS5I9zo8vnk21R$@}Hn9H~rNn?*j4*i=4+MhEKDrl1#1 zKWpm9rM1-LZ-5_L=;J97%$ApA`v&BuTAz4QE>GInHwo&e8;x1G?uzem2NlToGrjXl zEGpOKW3XAlKVS^pWvk$BOH-iUS6l0^v2Z75)z*}^_8=A-eQ z7MMU!HhKy0&VEyPP~rhK3VP#O=@=PqcD!5poEdEO4fzk7imB)i4TZVOW^YT7W9}=8 zBV!J%^quNYfyQX`>VF=c!Ee!(!~|4v{*sfZRi?S+DZ2TVt6yIG_MBa3jOby$ljuP0 z^C`fqx1efCwC)kRZS`_fk&mjCD5vHN8R9tIE(cew509Kr_JQBBpd1Se^Rue@=4J;v zILYF@>Q7%3HP&vEmE}kjyXI=Ncq3=);SOlZVL32}TyY&_?;}t>%^9#L{k^e$?()MA zFU2g<xmPxq={2Le`Ln7mFW`X= zF0?+sXEV6oyODBwOl~{Tth6+@2i|Yw<*~RSW4*nKQ!GCvcL%n2TZ%|>Y*unmIc|=M zRGv30&01Tsn;?~pWtnO(Tb()S8`k67-aM>>M4i$S6A75~x*^BBE`rUVhny~ioN;fy zPb!6-8Y0);9K7RoieM#3%>pp^T1Y)V#f&4*Oh!c>R&h)PhXHfZpkFZtfHCEPM`l|ktDxq zn%Pq09GUOfp03~fd-M^(PRWbxor7F7O5!tw; z{9vwUpqE7mZbIXPL8lYmNt?0mQuKY5uH<>o{@cq3J9(CF*&j?bqP|FF7l^!_^87h| zYXMtpt|;WS!@LLvo0yMOhO0 zU}VHJ336M! z|5V$xd%5{L^{L1*DT&D7E2$4-55)Rwb6*BW;|3%x(}gb66RZ-RpofWN3R*h-hgKj7 z>J%B&?fk*o-HF1%zK{0?r9@0!TSf!i!>%wBR9T&R&`KH;fF->N&=|4^ceQPazf!|cFStqBL z%*2s?TO#C)1T=GeBgmt$lSFg1=TXlh_v5U$wRd}>*jlrizXqI4Y$+N$T_a8n8i@NB zs^9?`CPFXUNcgB1spR$$0)zM@HnZEVj91%cWn}MPjJ|xy+cSlgFlENUqJzA^!8^Ak z-pP@8PxuxILqRT@(3VO6o357Q@!O*Rtr2^Q(^%&{i@09h(iD~3kn9AT`4*mWC+w>0 z!jn{Krb!M(@rRgGOssyyh2O3B+zA77=kbAi<n4EtH7eXDu5P~fi z%;HFNP}{Y&c5}=BK3Q>P`rDWzMUocc^^Z9g(fNF(@7>)h)g29=lO7rUSYhyARJT-h zPxO877;*NvXq8nML6j_IMDV)U0*C9hD{V782}2vYX;;6$xzL)G{?@BH-NN@>-s9QG z2w%C!y!;+Uesv1q?)TqvE?y314=qHrQ_wVAJ6bz z%PH}Srs#Q|xhGa#>B~D~>7|XOp9~KvIYXkT)u^`omRjqw-1&&eH;%WzYEYcA?2(Nx zx!WQ7&_wFa0~Kk}ljR%wg~B)w8aBNJ@rVcoQE6)?nT&V06wKLbJQbU=J+wbNvL-tm ze2=wDAq|W$D-}wZPL@yYN*+-j3};u4dD^!vXL(xP?{eB%*>%PS^h|0$I<-3Dg6FJ` zU>u$HM%q<2wlAI(Xp>+uPl#ZEWW4|H5`tRv); zjx?HyXQ``&9)C)xznvq5(g1wZKBY|`VeTs`9Wh$`EO(BLiBqrN5 zV^3u#d3?3+#{1|#%aYC)>w>>fGA`Hi<07@|!iG%4%XgaL*Exm+Vu@z2$Qx0!qO zZ}Cnou{|XLKUsYGn8BBoo}Z<7lZas0_VX8{-bv~L zcZhF^vi3`v-6JlMzoFA((iBX;Tp8zfIo(HeZ^W;5Hiz1YHN!;u&V#uH{FQ>2F+9;v ze%@zF*KIabByMx*;tn)elv4CP;pH^YIh*r@iv7jvV8dt5d2d0U_55^ya*f5Y@BH*) zh98oq3A(?@?N#+EROh6mA!P>Xv3E+vRx5=Nk{z`0Pv}oUyu|ZC##)b8Vm@ELGOELI zo9Tq=cbUX{*J`@75>}Y5DE_2N$}bl@n0p9pW}xRBYIBC+dI#-W4NZo57<(cYabOmw z+?}g_?0be#NB#Su?M8!3COLPq1Maj%nU&&O)1!=-qx})`@toFK-_zGCA2Y`SP1BHC zWrSX2==4oDeSY~!UHUw_DS{=4=yacZbbbytKoFVuvFDot)51=Pi2(3V?n^b1w@3P& z^8K=sUXt})lO+%5UGfGQTr3k17dH4xI^*lt=c+k(eP8~_D>keW<@-)8?!eFBWb^p! z?V2VPwF`HdSd3<8PgEU~SHsTt&SQ0-7=$#?wg*p#b#IGOCEO`gl%=Qs*yyLaiS|&y zT(@=}I+OqHxx3DS{G&My&YFx)OWNJhD!n{XcBIcRber z_dkBAG>FQ`N+`rdWLB4~tX%dUCCMg9A;~JEva_<+W$zGW79nJ>B1C16ME3Ze$Mt%> zKkv`ycl-6%>*e{p9^*XE<2=see$Kf+JnSv2$r8IB`if`x^C*r6n1e-JQyz2^Gsu2# z-=RWko6ex~wm6s4@OsfL>EU_gJM5^F-W$HD=g{pz)vlMeCZ6x25OzXJQazNUlMZ)@ z{Mo9H3iAwaxX_H(W#y#x{Trd__j|)8e7c@QcOS>jr7`FIO4;A~`~7g!l1Q`Z;OxMM z;8=-vKFlRBYa)_T_a{4=g8HeN`{*dn7A zt5A0CR~(QLO?qyq`KKTd9LWfgJ9rt-Ki%6mu{X0^GA5oy_^LEiq?u?`s|wnT2(%Fh z;i7osLHBba)u$7XOX`3Ci=~U*p%FWtwu`-~79TEb|2DK$SW+%! zK*Qaf#il;PzNK1*>#T7JkMy9fW6Qy3`s75Jt>{&g#f?~|JC|1Pn17am0cH&M$5P2) zC$hgs3Lj`W3mys!cqmykPJ)KwUiQ9c=AG|lDSb-C)v?jeeEeF)_)q&=>VwL1Y_z|b zIGBsO6)}&mIBo7+h{19&L>|^mQ6AEpX5(-!6%X^j6J&Ot)9DQCpxyWMXvv*)B<5Lg zCsNlw*w0#A6WljNM<$5rG(TCtdT5LO&J636yCg8SJ$Pv2b%x<5J7HqIIQVVY6~)3J zcGI<&W(U-Dz?fn%>hv&fKk5*zQ`skxNeKcB27|#WaUW@WLhw=MCzqZAL;Rn=V=z)7 z8bm%-3}#9I`Xl}mmxrJZ`j^}U(5jW*FzWY zz19JDNnQHU)Z`lpAqL@1NQBwJW&<*Dni;~cOLl#L+)n6`U>rXIJV2)se@UQXp<;GB zRU7DWlpP0*9?Aj#+G+p>!yyM_KlpxYvDTO;3-KR1IB*solUQd}{iLrkkruA%4|T|5 zwTzf#3j5j}=hDTRt;W$3s5}WZj-QEt(Eh?+$ZoLo62%b=mn6tT{XQfOAkT!Lk)RFCa2RF%sj-l%;&cZAGrToY8PAVt%=j|8h9v)N&W^ClX}5j zclva){YcfN{R30!drW0PUx6-5L5t)$)RYnzy6C6rW7krLj z@PY8%B6^{&K}U>>MaV)~SXlU{-izs5M$Av3E=srp(sK#vO0OFCssX3?QS!!GxarBT zCy(U=63pXo9$WKo7Q4B%GGrnA^gGvV6-vnDdNS1tc@_ZQq4N;INF70+C=54ORBvF{}_roHgqr;l^alU_hzxnG(=o<6T)WO*mv2T z9&k$Sb$`WkzQlHbuNI3*evf9Xm@P$hQV%7oel(H8#ku}zKBu6-;_fJH*&c7TG~Tdu zMIg3x?ajc=<=iGmj*#CqlO0K;npLMUIdkIy`~~hIr=5B=mT2e0rWWz%`Sgoeckd4D zT~A%AKPI`7JAk+`oksic;}}efHxf|<89ORo8+O~bxff?&^X}L93;&C;{EZ7CitZ%I zB9Jta%M(?#(yinX8H>0x?+zC+10j#c-`IPn1w?8JNs@1%Q8&Bq?W{Ryi}QX~j=sRQ zc8V0c;B#m-mf+QTbHCJVhH8T0TD2?Gt8t$E9I2~4X8z}AvxCC~XlY2h1ukC9@S zWEIF%w7y~{%-a&kA}Ns=QLPhuZwIiKyx}CyzdI1e+OffSQGvUR*)MC&(598#>{7ci z!BqP;1=3T`N{Ch3^kHicV>%C}-`Flnd-q^+vyZXcr+PD(a`6X%OJ-+`F5HLP}Z8t$9@!+t`i_w|tVb4@XBCXAE-r~|A~qp z-d zZbOd3cgWao)O{@|_|o|EDFAu{P|~Coga>yyU&Cws732OqhN?G=;H34Y=?WO?o!iO9 z5&%wH^<=2*cDO5qy>?wm1o&}aw*%9u1%ipF1AS#sNx+G0E+_5H(e>#|PoNUa07h2; z)oc>}hO6SZ86`d3909|SgY3#AV-cZBH4$2jpP%10)`&3P6o2`_xOba}Rud2J^LXH` z?E<`DLeCc8KwlYx%qR}n$mTTL1mY{So~oz``^3>8tft#2g1 zLG{`@%5R`oaZ`{Pm^<7A6gT5~B-m-B;gZxrHLZ_fo7v-f=K`@NdB~>JTUn@FY?KLk zY>u4M#a+n$+dhMtVgN>!Vtx;1tZK2Hrr3-yAwf0mW;}1}i+$mkH_sQt#)O}2lgd#X@F_C z^`+&Tp9?S6?s-%N`?DHMf6ihK<$jdn-3|6kSheNL=-Q7l(zu%sgx3sN~?HUR&yiHCK`U0fG=5AgEA3twzEZbVX$)BzXi-#=u zrTe2tv}fexPF7a?yRz+E-@36*Z-Wc%=yvEa7%km8cu*zEaYRSYh}QG;3PZa$Z1|4Y zW_nKYbE@(5o(j48BV0yVi)&CycosJiZR$t6P@mxhNu?zfLuGMhZcs;T6Oz4WAuf%< zB*^|du|sw^@^YKL92bcb>9lj0Ie<_72QN5|h2`YI20;hSRb zwYDo*@>mJMArsfS?#0mEbYb&w4uZ)$_1@E^`<`|4mW@Cd zM7{hDq}e-U;z^mVD>Trl9a=Vw5*{nT$?AIbd=nh8D<3||-}BXQ_~938{kQi_;*0Tw zEnNk*S?tUGM~iubT&R07C83e=;#Vl>f(%itu$lQ9p4k_H?>Q9 zM&~57JC99nOv{D|@U|FvZ2a;Nz&J9)^7qZ~#wPsLWNTz`H0Uw7q>&=Or`ydz&g?bh zzAj{+>V5V4$P}w!TFDbbW6QfGo3Q_kKO%RG%+k&NQG2_=|B}b3RZT?NQ`wUh`IHeu z<}ac^7bJGesd#Wx4?0D6lg;CM2*E$YwT!;pn$~AUm{R z4X6D2%UZG;mT_2jcD?EATL;s8*38L?0Lyt!21uGRdYn)lUw;te16H-B=R-{66G^Df zeiK{)z#daQWl>)CRC38JRQ$dIn|0$X&t{&v0l8qvl|R?2JO~E@nWHVlWwMU=zNtyn zJIlBwflf~Xb%Q`Yy`b0jk+VhF2KPy)Lhevg>>W{l=HwXHs+s`1)GjSo#^ahE_Gg4s<)0WwJh7<>t*_~7kyxk3HK z`f$<^$=$cc`DIt8*K`&2x2oMBy1zYM78Yn?h?i-`B)@_!Xo|^6J5xDEQ_QN^;+3pQ z?3|iqa|D-S>h0kEB~k`)RsAUeXJ-++krT``Lq9yA`qRVVjF^iG?v}9Ogz+m%;d3gn zH0}VZdi;&1RJUs

>bwJe(9xF-;cd_ldoFA+K~;`+ZmEg-SIJ<%o6lrVmN!pX(OR zaciWWiHgd;M0HZTZi6C%oy?ff|5souZLNp%w@#BPwTx{71b_HPCY;3*n*YkAe&pP_ zQFQBxF^!}ePm2|oJR?(C+Sa~6}P>sKL=}mhP<@boI zolin{-CMXbd;7ICGiT4ePKODOY2+;`_o!Y(FQa$l5eeiIg=$UFw$|GbW+3d9r^w4QLP zM1}{(dg*x%JS<5!ud|!ntG@rM<@?RGq8;TSl?KV3DC?`M_x6;AjtxT8qE7exd^F}C zF(hhDUcMrwbxwp=(<<*>W~-Ksf#&DSW^1-D4)0t)|D@r}n%05*>yvs688eFVOoC}Uvf)cxlO`&p`K8TY|K8ZyG*M#2 zn&V!YmHM=Irz`D%pTY=!ifcQiv(qh>b~Zxrda7Ii)w$80biOyLmn4js@1_hZ@%v<657h!>g$Fgi%%#;ufI!u=FhsSzxvJ4yuPF7|S*K#V>gi)c>!9Pmd87QBzJ`Jx`TmwV zzRP(}U$`F|?EAdkdAvESy`+>-836DmEt7{+M&` zd}!m+Ej`+|`>?;{wy|N)5AGM?Y1E4HVy6|A#xPPB5w!HcnZY!m+E#)gotBX~k3;2~ zm7Ym)uQa`p9(nkA=SDu>sV~2t_uZj9!R+|VI-#`nL54<4!`<9qC8~7g2&p4``L8#0 zqq{sK4;L#{=Y*#2za=+1@1;>#j}!FRrDocsb>6k(W8S&Q@5DPNlAe&>j<-DRBsp_! zO8KoHKi9s+V;}zxszi4x#)?TT<}rCu zXC|+tJ?C-z%=5H_l=AkggfU8~msS#-L*;)H(%SQ=ELJa4nExqGTaFOz!eOhvbjrA| zYM*+&MX4pYgBQdyQ>l{WW&~I9e-1VK77jqQ9fG2S*)g+4oM_72`!DU#4|EsjYsco7`<~=H!W|F|fJT)^U;0nlbd`ZCN_*HuiYV zwTyxM337Rp9*=ZN8N$<4C*iR6i2ZVAqgvQRzXY54MUxN05gZA0l*}4h3PWLb{nL~I z{D4!>g!PX7z9wQ+JXE7_O>p5WWtob*wCw1pX!g5UKE(uKy-=>yUB)<~D3`m5Wlpd5;VzN_%E7trSQ`&MhS^>}mGU>h3syUbj|iw1aa1urUp`^{|?7J3(3BnSmy9Hfj6di zs;n##$6N9AdH2mW+*9oDFLphCMi@C$>XN}wby}5`N2-jPHlR4#TojT@W5=Z_4Ejdv z&&bt=1!6z?yttTf?Jzm{CS}(mtSj-kHJ$T{E|F=!cPI?-_;Vs*Q(_|v^_eHcUYhk5 z>E-_j5yvFQ5uFp}6?T>JOmF=Jwq->KWWM?9bSq0>diOovvd)d<<5XTeWoN|uBh$&7 zKx`TmAdBs{I;>N9=c$YcE%i#(Ra__q`2`cL6sbcI@*UR`qDtv2MpaX3UN%fQspqSz z)K0nUR!jgFOvvPYjhBkt9G+#BC!F@h(Mqt-@eWoAw8fT%CmLTmUg?PJ-}XJFMEL+a1Iz0LJS!@<=vnlL@JZ zA8WqOz^_PHC25|+lhV?9mnK>7vN>^uhiNCIZj~vD_k~S}WBl>Hx0JqTXZPf3N8^5s zndNWZi23>!)kjS(XE~8+g)jl-W@~YZvpTUH$_|6u{Ok+_mpZ5FbBcO7Z}WEbw4^I&+#NfR#duO0 zchH>B>*W{jM;cvZnzs^{bc`LG*zhGk(pRBmS@?kU4yD5IKvxe&i*Vfw>Q6a&Rl&w+ zxwV{evyV%6$jiP!a2Keed6*d%I9Pt?>9jl`=w5dbyknHMUxxYZY#x7(++ro}gn#pX z^9;(;2|Y~`X+4~+@T+iGny(<2g2b>4h`op8qQr-NWX7@<28oN%kd~C`TDhHfYkt8_u9wGydN{gsR?NE75D_n z9oL$`DLc>2A!JhLV+F;;9yi%M(_ErPgVvN7O_TlE_LuB+#WoZdy20 z6-Xc`-(3ocLk)n^voHa71pRLkCy<0Du!kmadU-k;-Ft+CL#85(-PIqazJjJcC6E-% z0O)k-tq=P^^cW*K7{_J+8b_#FQx>oT1S47S6PrRJKK*B!US&eewx1WhE-sB;e}-N^ zjD~&UC5&npDV{j`Nb0iq<~)NEP6>1n&(ZLXqu~jnFC&P)Of`xNR0N{Y97UtCM-$^a1M=pghA-18hQ3Sy`Z6s(=;xH^ z=TyX>pFuwdKnPRegMJ?9hPNoJUiIfxOPm9ZEPVmPcO*f>ih*LCH(dk;h1OU6Yq=zt zL4=Z4+(LL&Z?+ba9KJczOg{J&0|YR-U@=&s9dhbIx~bzy4buhmLJJbDBT=+PP!}!- zJW-rzj3Q`^Mre%Q5y1!&o#9;OKo5A(199R5H28x=k(@>%VM8MEMzU}`gt|G7CLR9xzz6Y_fRKV2Cr{hi00C z)Oj8)_{(VR+&YFH<6yd>)G#T`syZGrVnvqh*+7RrA=Ubc7J`>6L`Fb#0aHs8~0%`u%gaX&`?>KzyMsnZzk+8F8iMc6vPKf7dQx_t}9k( z6kaf7VPd;(s9jMs3Q{yPLm3!)ELz&!Znz0x-S7u)*Sl!B-9s-vKrc%F*N(BCMS?-$ zy=%nQR@u|dIF+N11H#Z=NCvV7F`q)?b@T)4!zhA{y=b3kOo727y}yJ$$J9|6ufT0t zVdpu0XzMr<0FL+oeUeZV$vQxkhXLpTD+Bbj?n!)136C$F`v3X==fv0TklLJv$N&B> zz8RI1#ERN6f?S`2-XQJ7xSIVhwLya={#NE+dxo|PS_SlBN6<2U3^qvJn%KEGbPnrN z%6G#tFqW@L5xuj3-U0b&N2@hiz3m3uvkluoGC9aC3#ktF1^Vhc=@Fk}MQq1MjiHI0 z=YTQb(Q1&|`(IWGiB0MkQFZ8{aDkbEf&y%tx*}?v5)IqmJL0HU4gzd6(SX%?VY1q^ zFxp6D`Mk#2IQA#|!yL1YBH7bW!<2PUo94te-@4%^-tRva2o*F7di%YeAh?=?_~Z*% zr=Oo*c71$74O`fS3#f?$#3qcR!!(%0U4@~g{jf&8o)E=yAB)IuGJJNQETP(gubnkp4svvse_!1kL5W6j7iXAhHXe z&>)CCgJ}QWQNEG40LogL@e>4@M%s#-HeV+^>y_JkaaqD*|{2b{CJw5v3^veH7 zf%e0037w(Iet<%~0xKw5KEWv#B?|GV1bp&?N6_?5{xwEf z81yG35xD#0KgLLk6!pyoeRGK;;cJtlp~8~&(nPjO>MweH7#?#&w!fSq$soEyp(w$j zkzvU|K#Y|Mw$v-{3XMbL2~M>e9sEK4vwk)k0Oh6z@Yh6xkh-zT(v5q`tEn6Nn?1@= z@uP4Xu2z^aNS@GTC2YtaD|uQ+3}UQdFPa&XWZf;z3Q)` z+)oqf%<6(}vFPuaD-mc@Hdsy?ddBWaA7PrNXUx}c^YpMTy?Jr(x+<~Fl8n)i-!2)i zTs1>ANHY3Z6!&D}`nTt;O4BCR;!;IM-!b@1@N_5GKqhB|JX3Gjrnyu`MwL5%% zO>$1|N0M=7+}`U2i)AtE!dTBy>wl+MDs8u`?-CljXdF?!QDYFp)csYoc+F_5c<&ph z$k1eEIuC%pCVN-6Ur~!4-TdHbO`E^L@?6?#SD}j9t~*V!YjkILw68`_*uo9szAR-U ztQYr&@dOI|@)jCP>M&`3oiRwKzg_<#vE)G0km^;~*r^v%tLvd8^C>9v-kZ$aThGKW3UmBqm&P zNQM3dt+k^yf(TkT%}~Y1sR;$ANr>h!6ucL&yVr6bI-YUqc9!OxZgwiZeE{Q!7V9;% zSj*Y_#BmxC*14?VD{0pnFjFUyf+n8;Ev}Gjpp&$`TWJyzMJ}k=5bd0F6qCjV`ihA~ z=4i<6d|HA!UWqg&?T8z9Be=9l!I!6!y;GqqV2C)r_Do``fz?=@FRXE^7`2vC!wBna z)*mc4C@>Wq+{{9c{+RjkkYKz_z)XF7exmEkYf}PLmTu^fB%9apb+@<)z9ScuNbFG+ zIgBYrelzCJZ|)ckg1j80IWOk|guVSKMOU8U7vCrKK$1=+w6LXa!n$2~dMxi}WG-}t zLd|#4D#I9pdnv{5BlGGH<#n4}RvCeVQWw!aipd8%pG;SmqV(w5`Cr&iJ`MCDFdL#A zm{D6-bGc(alQO^`T!|LniVl`8ikuBU0ta=3$1x zHIlk$FrP*sjk&-aF5BtavzNFqpS+O{O>rPaNy#N;;9Mc?q#(|iA5I(KOsyX{4g(`b z7_Ete*(v&0-_ZnVkcC9%7S165b*6AFn<54U>ItBYzbHvqd5Duv`oFu|=Rdt__i4cw z2qy~N;}3Vt%d-md)aO)JKp`wD=&7J-UwPCtjr4yvx%1!xCI2S=w;m8^b6FayD zR(*;F3_Y10MCZGJy$ku?URTj;TIe<2bW?N>Y{eL!IKf)jh&qqJE8mW7L}APBDqDI$v_wbVJdWbd~s({gG4s&)y~q)Hw;pv=}O<{D0<(4r&;F{1Fy!I_p4LxZ6qj#(MTY=s;IU!oF3AV)tR`41O}QxrrhO-p0uMTWwd+eu`| z1IKV7=gtsz)U0Tzqhj9;RE6voxYf3Bd%T9Yuv_31>s1B4(8ALEIXLOKyS?GzF#693 zkmB8hCjBo%RM{p;i`LTr_P@KHR5n~JCFHs}ooU-71kwfv%cjQzF`e@ zhi@k3ka!~cK#5b~M$LvM;eTI^{0&+3a;QAY&2Jp8>IAa zx(@aOomoT_aK}j3*@g9MCwGRV^VMz-KOEhtWH@Zv7?@o>0z_q>`4_nVnI6)qKw2{> zZq%%`imszGNrn1!+%q+HT^vZ9-fXq0AQ*42b zIO)!REyX2;CNWS2pMx2!u)p~-w+-3X4i|Q-yKsLms)m_+^wlksGPF@c3Q&@)ea(i& z3NZG_L4u*bmXMy2lMWPcU2JH638np8l?<1_am^Xx!`5x&@>)xFi)!9yw>$G45*jK> zmslA6C4plwvy>;?pVr5?9u@MYt$a*V4EJ-Yfy=3TbPtqx0`I9L~G7;p@!Ov!)T@*Tcv2 zEauC=gWX^4=W*jn09x)@j7edaf4|>P_|^Jqs}H07S&GJN+hEU^oNiNxOr_pp#n%l& z-viZ@qfo(l)U)T(!qhg-`52y_^R_c^!byX% z>ETNF;O7{joi+lhPW|Gls3I5owZyXNK_j9KSo>ItO}qkWE)E(pLJhV}F%Y12gliug zte^4MZg-EG(YFb*NSvGXv7HLJ8RywCuF zHv@kwnoQ4-4Vy4qTERtLxr|b$N+T!YyL2iY4V=?s{@i^`c;uyhB~HjvH9oeRy}v@X zk}MQ}Aj6FD$S{d9TXdQD4%Uch^0amSXCF!)hlx^C>S;O<;(AlGn>OJ#;^)+-*xEG4 zpHW!v8y2qc*Lr)mr0o7LyeFN5!neXnbW`-0eN;ffRmZ86XOxx3quOsfed?^;q~#mf zX(6j7xhOld_ekv6$Y}X0Er$rYR?>yam{xVg#iP13V_&4_Q6tyk#r-wb@BE&ds|;|F zO!SC@=s0QbK<0yjC!L5ZfBlfKYi$K_kI2Hp9#PdzXSUrJNi40yD{${nNhE4u&*h&j zDD@q%%6i#N6FVxK@Cz`3M&|pTBFUj)mt5~qNLOtCX?>azP;5zixC$sH2KCF^sf=W@UZp$B$&hqzOx2yZ%doos*~{jc;^_lwvATz#qYKlkpcSW`>#2ep&= z%&w;x;$)Ibef&tc#f!lUsu(J-r_25$N_#y*v&&=b*(qN+zqAZ@f(F^O7d&c1_y-3! zoa!^?2e`TjV;YIIj4_-W^R&cWc?Ndn!;uxN1pZQ>Q$8al=J>+25+W<|+o8ueOoe}P zY{%JSOV@n@!j5@}i#To8f^n;Gx}L)CT&YH@3Y?||R{Gt*Rb(^EeT2{}x9-xv;;}xE zp!>6{LUH-EV~hDKBhfhl_nw-x@t6rSjj^jTAGN>tsHe$y`=5#3l+BNoloA&qb z_SL~g4`z{hB`ld=eVF#!SV_;_BB#FXq8!8me|Lk<*iECn>N?sZFmvD+dvQBSH_|B# zR{PlNhiWKck5A1=j^SdpMYhXhHN_-$mb#VrH!A(hU)JWw;^r!jC4KyKHvYY?bxf6Z z^zRNOUw?V(CJ9J$toO4UhIC`6S$2xqF1zCP%kZ@Woh)vZ)FF}BG?5^xbIy`8 zpC;4CmDX>xLyCAU{)}XEQIfiTedhM8@{ni(4QIA?+VAEBXP*Xsk+!|?2DGQ%rSq^@ z+1_>@1P0@Kvt|{HQ3Z1}BYdY${tUl5PqR~u$2U!3H4yrb(fOq%2&PBPZJQ$55Pv=^`z zd$4pp>ssg0oZk72U(|8AuCj6mr65No+KLfBZts85+MtkD5^V)*azT8Q53`cdLb;Jg z_gBbA>L{dYOAsr+%WPXw97=c6Kv>#V2sXa1%mqBhJD|pQaPfS#!l5&bDWBnnv8XHca zu2ZxWITkqiOfj3A?WH!mhG0sUd%A|ELLt8$SNKA%!IO{1Dd)J4I3S22YRe(lgD>o! zN4R`J&E`zS5c7l20#2JafaeuR1Nfe2-Y?|8noz2y@c-yg& z{YJ(PAGuL_t8tfN{ii_xz%jw}62p|q!hNYDi@{5jLF6f4y?>UmA?dbfUeT3z67|5R}&?1)*{ zC9QW~-m`kiAHP0idrhBk)q7%SN$%8}iz6F}Sszn2F~1v6F8dZdms z*;jQHYEA<2b1W99ZW9W5Q zZ9XB{tB}wVTASJqIg6U*SAITATUR=MJw?j?YPXw6BbCtZo1EB%q}nClV|!mJ$kg9n ziMNz>%aK)iRneY^yb8p^M%uy>VEp;UX9&Xdlgz{ zKErx0{pMAZiU#BCPjm*_UpnJYSxZ2Hy2+8}X~9)o=T8)5nmxsJA%;=WyldyVr5}I~ zbL=|5E&1g6Bf~I4r&;dw9=q5(UnOgKt#k6UOP3dw`ZM+=shx;H_dI;rG>C)CgkMqV zo#DPw*mv12mX>%lz?i|&Ad8Ro_R2ecBgh{Pg(_P+eTCAUs z`b+@dlVRocUo(Xf1ey>}z6yJlg&g)5iLb(huo6a)E8Ye7RbRT}ZsgAWVq)$0Y{r|7 z<;ktMriiCYGH+u=(!*E2SVvGD8PL0*Jp=>+m**ap3B}k^sf5N0m-6OY8`1t0b}Kkh zTOBKsVASRD&V9?-Q%@jiuzNFK{i)4NZ+12j;=o|O0vefzTRl%64SVrPVK{ zT1mN@GWcOD7geE(S}5ijxEm*&m)*0nS?A8)N?T}{a;5{U?6Wp*YSHsruG}3Qpa1U9 zzZ_nddwC-x#Tm$Kuhu{E2=q(zEw@|Q&Fre#dM3He_L}t{Yr&!^C?yhE!+I8o!Tp~t zTS-uG+z`Go0o6PK&vLBV!-0vg;fNpLW|D~mGP6@Ql6b$Y2Xon0$w#!sB2$4=*>WbH z1ACDB#1FAA2C);|)@fa#bhH{Mjo)R+Jl*N9o5+d0h3njhB^4A;!bL6`Zqw6n1f0Ms zm&*9^(2}mi_Dl>=L6!9PH=`S^36|*{ z&)UDS|Emx`kwRoV;reuRU;5o{PM{;u#2PNbv10k*%WI_EN@-jisgLeNEHsCuVY<13 z2<|O(X2lt%jU8BqFhF2?$lc^&zCIPnk+-alF&?w5Ac-n}}lE3K*W9TEQ;lC_lKQ8Yg;Q zA{cOkqY?mx+~0s)e_PY|`lkVEO>+?L?E<{{=Bwb>w*x8pXqx{vL;aT*wx?N-g5n(5 z6co99XGO`lHGUWXJLVriDl9OeSG%d>*l%Fj7X|1MinvKR>DOYFc2J%&s)ImC%a4u? zbW^~3@f=`HCpmA`-42|H?2`6dI0VD`3+_}O3TE(C-0p>#>5qjut-Gul)PCbxt82>m0!&g#B?GPzTHgtc8OtXmW@hXUX!0 zo(67nitsGmRJg0KMxUz@-YHe(j)5BwJ~o>&Yr!f0;4yeT##%L+mPhwvl7o?)HPAgc z0ltQ=E_zd|5eUMt8iw<5Din8pY|?=te}GaKDT^_V!umQTO#(gOft`u}BF*rW1Y*_1 zry^$4Akw~~tAVk*5~hv7=nlQx0se&;whe3C-WgL!9f1%_sDXYcgSe}s-F;)@Ku}uM zDn58|5CV*66@mi`9gie$1H|)*m-o|p#>c?+F94kal9zXZ!}J^qO8$aSs+0xt%c0OI zRzHvdicVHW_jX2U&=!K@Rl`rfju^NAQYAvWC%m3QT$Qy1iF^}ynbh?HyM9)~)yGMx z2#)E4#yi;zh>X1fzzlP#;M0K!sR1%U8_Yo^O(;CKiLRmp$hopO!>nd+cWVkxpJV>Y z#FDhoudut#a{kf5S%9hnhzxM2bW}v3A1|Qcyr(rK%v&NV%rXAHpi!T1KKd0|p=1<} zfX}G}UhWuF#p4QF@wk#13y|l4rqhrUzfIi;RMLsm#1aEeUL&?qVH#Z)P=B;k417=_ zkIcOG0a(aNXJm~&9a4N%>b1G-?RG~|imWtiihA$dIx!*loj2t5J;KMiH zaO{%1p>qUYR&&EQs(s&r`ld!wroD%l26$sC9IfHIo5yGNM`uop)_mQ8p(cM57FYya zh`B9V;H<-^muk&cHPOEX2LO380-m9r7cbnOJcaQ%#?a#PqNzOOD~7|!F45ctS@_!l zb!itAj}TBK0ymRI@H0|WWK1`k>mC50?l$_>^K>J(KMiz>{{3NOaCXN`B?0rZoPG6Q zU16wfbP0|IhV-?1f59M4LFZ}Pg3<#tOr#9SDp4LwzQN9MEEy3H)xmwOyB$PpGp8ie z#t+IVWogFUl1A)hbEwdBXSQMp!$nCZC`CTVU8m2M9w+mBPNKe4^-G1p=hscpr_|$E zTJvsAK?JAdv(~2xS0AkjNtj_<`O8Sig^)nLVHd!mu-f*gNEX>P*PAlgjmBQ(Tc5UMxtIzmNb6L)m& zMqF(+t$9S)S8(2M6mHkYjjcKjzNW2yPhyT2UuIFn@U<+`>yKCCS$@A(yh14jxzZoQ#i*+YYw=k z?yInk+1lON^*zsxR%3aDC03rp$U$T|GD_DcyXAP=O&jg_lnWrm&>;Snx!S$mu0RH% zaW)kbeB&S#=}@0HEd4Y};k7TB)w@NJr|!$4Cufu&`j9d(jr=T^BRaAC*9fqzR&@Xkd&#S zTnyAkVGYaf7z{h~0cypp5+m3!dP<5!J@kW=67;C5#+LCgR^@3tM3e}`k@9A9EjLN- z3NT*0dt+W7@ND%PG-ekY>tO z>d&+N(ozE%t&yCQvbjniLNo=7vY>L1sy@o=b}Fm4X6kV*6&efpg0VwDS-JvSM4!K4 z#mZllo3`19S%EZz2r^&#G+OiGz{&#vjgj%RC?`pXqZ}pzlqPi+?n~}~$W4lOpelam zxz_XSg0vm!pi-FH`_tH#DzHmkKxMlP40r^)_L{T|Q#+Bphn37;t7wGQl7ABJ;zmMC zKhkW021|+aRO!ELV3B+?)a`$f=AM@$jhVXD7$u{}*AjcSh%!_ZtsO)wS)t=%UA~b@ z(eyV9!q{BwVaKfGG)?^6p^2k8sx-!PWs1IGr4uZpvBPBvlUH9J)BzjfOW5I@ZjISF zVxS%Y*2BBS|8zdhl{uF|I-QE_;WI(QC+W4JJ1)nJt|OZZOOl(L7hyyO`eW=$n;gn- zeMn!Wo>nW@Ok)%GmWJ?xZ)pD$3zn;5Zcjo|VX|B~MVKD}C&peY_t*I_AEM2t4`)~I zULT0>zvG_#nd?*XVpxsK^d*8;*eZWb95j5wOyzynuk#XL++S&o>{A)B+fU=W>NFG* zm%{2Dth5>Si=1}&`9{US}c5OG-&)`bvJ^*53~qcAc;7`h|}+ zeaN@fjb{__K0{#jPF$A6n==pj-zRXv3aemKI?)*XWSZYeH){N-Iy-bFq$sKC#+{nP z)dISt+Uamtla19nn#wnN#7aAU?Cmi*@HWIxELgdnNWHdwOl0&rcnY~`)WQ930)AJO zANeN{tZ*v)r;&P z80Bp`vc>big*Z1zG0FO%G}@4Fu$FR3-Upm26$eogE57Z(-#dpOJ!qY6^;A=8lB4nz z`BN|WX~uR4qt}?*SQrZ^oh#4WI%os6^5}t`p8?);A;-xCt+=+wlf91!Pk35!-`a=i zWHQNY1W|~~Ypk2fZMMROp}kAKU+Tf`gZedAi2eQw5ZI<9eA<3}hP2)W7KgT*1}1;$ zg!R!3NW_$fB3s|R2WmTT4($RgDiw^X9FQCMLv8K5Dd1;#0SF3D?TpkW0QLlkW{SCj zFSC>S8H#^s%TeLuCJ+@6kk97|cl4yfk>g8YY%6kWA!Tq&k|4Wsd9eC%`JND2<1vPb zH9B)Cje~HnF_o_|m76qXW8GUuLdJUs$6r@a-5 zA(9AIx{nN;z|EKT%&b1N-F$p8n~>LZ847`SQk7LDHh!9PT)u;s{)K46U#r|4qYTUU_iVK9PgUr?s>cG9fAw1t(`Cw_eT+OOwlgP+@Hj=!Md zz<;&+qQYZ21`)v1qk6f%$I=b&ut(Kd`RX4&p04fNP*uIM^l^O^aIPrhUZ)xQnb(n&_{}Yd z^3CifMpBTrhBJ~$E)Et{Cq8Cxl?V_oUSJV>xR6Jy{>Hsh{6OKS6$5=ZK?}RCOil5_ zRZ#@E1ZdBHqCJ7hz7=>pJH%Dm*GF|la7A2V#u3vE0%!dpU)E-uj>K#A@Aw0AxoFVP zqA0%&xaQ4R@45BE@t&O5?RJD0M!P)HEIrVHr{dGqAhTUM0eLw76lMOUsZzx&C+ znBp|=oPP-%dz%i0gdE*3Wv;s$lRK#eB_`WA#DY93iD-1PHR|MT1BI8h+yZlShn?7A&ZnYvuy)fH5 zt7-KJ+w8qu>u6tz{n&Wx9ScQPqj&i7<@#jCM?k4DK9uD>+L|WC)hZG9tYE|mIATzk z@s+W*>!Y=h0KN*Yn*--w=Uk(H!sGr3?s1Ys_uuJX42kp?ii(|X4CV9}$lB-m)p>R4 z1D!{?2zjY`D8sFzI{PY`SY$=FidDjOeY)KJtm)f~93aZ4{=9h_7Ev&|x_T3YnJi;#R(Z0W~Jkj9e<~cc&$f(ULfd(Ng^>Zr4s6m$}w(Xt>6hS;r~$ z?>avHxdaoIbt~&ssUfFRRHz3B){k{-6jGDEavfJp zhf8Qo04*7n+gnlFlV#-G-$s&BQW-$U9}JFsb%l9Fpwqe$y{Jk}?vd*vBzY5&$Hi=S zQ4nodjZuk^#&HWB+FdqK5SvaU?a>9tO5KU_BJBpiR(4YL3G?YGr$nCxP289!6-Sgl zhFfb8H)oWr@i)bU_tAH|l7X z9CsWg8B+xh4-QM-;bU+=t**AUxVO8w*RLt$aZO7<-ulgjdUZ6K6Y7Fj@)Rh zv7{rEBBVH1mfJmzvYg*B%UZHkrbDf~=T~k?(IdBB;m_xS)Xc4hut6yUvGUU~K#GHLl=KV#`$fs4#I6A$YT74)&G(3q;r(U|9WKUHo3NXhlw zcAW}tt3dVn_-}lr{<1H#XJqOq;tR4Ms&Sv!_ zJoC$*b`HW_)C01XBKJ>sGbbk7J{4C3h3JJjiTvy@$@`w>^ORAz^^{TaAIl&5B1q9J zfHnI%YIl=HoPXzf1Fwd(?DD%W>ex3`ch*C2!3I9Q`K4pL*ehRfgA4dgVbiTw_G9TE zmlsNcvM#*TF8Y1m_Nh`wE17$2m0@|`gWk!j2@Xnu8*HwwDvG|G)sq$4Rb;BETLwTH zaigOFS=Cx{Xu^1c9s#sd~8N3Oe06^!oy}i?S z-mxX%)9D>KL@4=gXpvCKj_0#Z0K7{c#So%6HD15xdYM|Ao?xVB`SC6bWpi`g-B%Hx zPgEa#tUA+aLrEe2kmq!EzHx0$nMdM$`hi#CHGYm+$7!8s@-eih?{IOarZ~;8+uk?b zIUoPHVZwK5dC*7h%;TD<@x$eBUPBr%aQhWMk0q}>RdMsN{wZRZw(H)WM)?SmbYq)?hgR&WPJH5gZy4@j`Q2O z^jI1#$7FMGi1#rXr8l2Da7k5;b|Y1}`-pf16KMK9Ac zqS{0O!hp`1eQRJyL`?5ho7n%E9NDiF%0S|*HLMV2fBt1>_T!>WRPM_+6|8b9P&%oF z@<8iQrdcQb`$W?|BOm{KXeWa6U|W+n->~rR7;P^Xb*Z{16>xn4xN+Xh?3dp!?8$AL z^)fo1w$ zpvI-1PYeq@c0P9=IgjzY9ulqS(#&bQ%ABYx_>D+vB}#Nj3**V<5}xAf);0W##9B)S z)6TTwyGGc@0vtUO#^IAs14#}M{uu>Tu@bW;UkZ5Xi`ohtKL&&-5Kv={tVETo=Sc)9 z_TaexrfQ>coc!Z+2w1}2hrsOwXwFmnxhXs<_0kCK|Az4sGYb+^TL5~71Jsl&39FDr zmw_5(Yf9s+ssb1P{hPbHT$+lgi%aWo=d;xOs`j&ZdHR|k1B6LrAzZdB7mxz(q`Fp?fX0W&D0 zo&dH3N!jP_-)(uj@7|N*%+D(^w2NgmMt6CNeh;3&@Yr1U6TyG^p{^%HYk*(G<&oD3 znUqvlab&>aO!lm4YZZD;TRVuuyUHpQ_RQ-B}a9fAP+ zY+1>y_?|@FGIlW@Sh-c@6&#wTILoVM7(M&|X5lgz=BXc7H5l4Y2*4>i=aDE}lw3v$ zEe$}-C)Z=(e%BML8h9!si=!>EG4`NOdHK$=U#1syO`PkY3V3->e+OufwvVNBso=S? z@94f7snPvEC%(?zz3JEx1$(Sm8J`&Sxkt?5r-)o@~2zU`RT z(DY+>e39%jjyA{TXViLmc_B4b62?5{!DFYxnyD|ozF*^Objp35EI}ap?Nd#Q`^0zB zLXF5DKwfok;b^K8^?T|vIP`|QV&#Ahd8K+0(wKhp)y(1)AbDF2rc{4-?bv3%? zUmOkAH~dUw`ME(*Cx_}6$KutcvY&bG24zT7A7+u02tyb9mA>R1tpN2ME`{9LXX(}M za{77Nt1w>EZ(S(&b@#jdtGU?WVJb6X-$`4J8F}?Ta#5>8Zi_lnvfGrrvzl!M7R!Mq z?D4G1dV2Mt-ksyqA?dmltQT=Q|3eX}?HuP&%(&Ss+}Et`LcsXJD7P*A;PWwAVp!M7CTiOIh-E68h>>Jh)b z$&-Vf{|<%SiH(HCqZM%Rh`E;ahhI0G3J9l)yG=;@Y`Y!X*lw>XbL^h)%!=f_6+jd92KiO4;I!AhgdJZbHS&56165U2&_EA4~#Pshr#JF7M~C zYJ;d4Kth*$&Nw<4g}h&cb!$Oy<1~gTT!0x0ODdy{tDX|;_U8b1)`1Fwr`J(nldG4jq zZR+c&zY)H6{n36N=_4o1v(obH!jZ`ro{?D`HGWJ$W`OFOF0RC$ROeOVqm);C%CWb* zJZae)Bg;9DvXn9}$8GeUmHak2In;hJH zOHQ(vB;uj}S48A-L??3IE;3tkcuk;VHJ??I9AAMp0p+;Oes3&9eIvEkM-7t{BzgRv zfSJv%=S@+g{7>oyD+)Qg9M6Z+ecYjg?|nKwa7QVn{3{eY(W30~&i4#(5cRB=vU{t))Ncm(piz&P;J7>T1i*6HNJbA~e~ z?^r!rrAk?MtTuu{1@t?@=FMz0k_@qZWF8`fbP10Khw@!+c0Ui@55(K3=nkbd@3VAD zZ?+k+a_1K1T#*ht&BN&7_?-l==gvWH=%7-Hzt(j<;~yct*)4(q8=8O4B(u)euxUE%nF3Cm)vV`{&CqLFa#^ey%tpPNa6laLa>20Rg@y!@;;dh>XrKq<0KnbL;3Z4sL z_r^1uZNxen=3NO!+iUv&!r6E_MFFwOPlpu8i|u^Dnq`TmtE=W0*~ub#qziN~cWDiy zTwj|x?$;AqEB3u$w;N>Rw1L>Y6~!(ILBbu4@^-*`5dNH&^PPAbA0myVW+AjVO;+Yj zhcWh`Iqmy6T>A<5A)dEKBWb!J;EVb~oB&;l3@2*zcxF+i{?#=4QO=c{k zw6j<NZs9#-#_H4o@}$rc`aA)YOk3}V3nx*gk)4Ii zSTg}JvO-fxYJMUD)8sgMw;HT!`wx405Y5y|&j<;%PyN=lI z+EcfitT)7}2S6*{g)>(VzNHMjwa$p+KCD{cBv%_QTR^_f_L2OCqi}^Fgr5h8&b;a{ zrYKd`)*ASt7tJlrAW!n7AV)n>s_wn@1lEe3!OcRXr;MFmoOY0p52qy+{irs~r?i_; zPLx52J18g3EGLd*ysUdRxnv*TSg0P+KUtvZ7O`Bz$J+z5n15OywGq4g&gULMDV8`o}RY5fcwtgxRp^XlW?Vv4_I;cO*44FKFG{%D@aDAAa@5 z#RY*ggzY(>O=9yxLJ-&d2<1c^guof`6xx} zZe4X2oW(mptGjK;(5Nhs@a>5BhEbq(O&UfaK0&>J(OCVK)ml?%4f~Uhy6F3tm z$2@kOO|o23eEC&et~Lm~+zT!4Y^5CnazaXGkr*1|Qz`_ZHRPk7xQQ|&)yxOxhK=M@ z;lm5==%O^)0x3ZdUvAhw0^a*Xjc;+FB~RjmphJv`xODQ70#L5Gmg|#%>ssDoOyY&w{9dFd;v^o!uj@+~Pu>mD%ylItau2Xm^@0-X ziCNh(AX#+kE*dgxVMTH?$dP6`y8I~=Saf8- zWVpu@!dn#Z$YW9w&q!teWnUk3v;A?sQfN)1#mg9qhJbN0C*Vg zxnKLoL7(Z~`Yv4f5hbQAjXUcD64gbwYw{TD{N-AFFun2E(F>fv z8XS+%2FBd5g0+Ui%>X_9Eh(7lt72e~*A!Co5m}d;| z;gQQn3^OR2Ze4-vomEGK=x)#rrnMS9DD#YJ;|}1iN*UmF172DUpW%zufUBo_8^1=H zU84uhr7-VMyhGY~M<(IO!?-2e@fBuX?fTDmS|6|xggRo}qw37z)X~z%6;a!3J+3|M zi7(4A40QFs6iz(`78rv1LF9f%|WP3__f-%&Z+aTxa|g#fv0_O#C0DxM9!e4-hUVAKkV>`*+}_EJJMLwhQk(+60d}RO{sc3>1v9bn(NShc>ULmfs2VvD zh_+87wlZ!8(sLfOJPDcVs-|6xi?8@HCx3o+_H#Iy;*{PCUAJQ8Pi@K+zli}-t~Wor zgc@1$S?=%MU(LT1?+>A7C$qNXvhg-eTg(6e#&tK?k2MgTTFVIkrGCB#^zndw13<2Z z(s)o!(fKjSY`;_3=Ycxpx+K_8z_*j1a@5Ok+kSmLDNC$=sAccHb6T@N{WkdM?SkX3 zz)In77Su=P|L4ul>dtCYi5Zdg?aAsLfE%@_73`|p4dX3c8}NeFIXy_Y0WYecX%jj3 zJt{Wa&WOqoKF8>0dh7zjrI@NsZqf6-j0K`k3~NzmfOz>UGBbPO^m|ZWBbjv_E_^#Q zTW%Qdng%RvhZSa5`-ze}_`B@h6M1?Gx2VCYG%F1j5+RO?ABSMElDYIGDgbb+iFGR& ztJee3;v0yR8xH#B!h-}Vri<1YPE@*2r6=e&V7VSQHyhzYJ+4O^9KZ2WRP99G(W^#CQMKoDcV z6$OmG(vyCW-+m7Re6OF(|G5O5P}?i3VNBu}iJgynv?nFQ8622Ap;_X(_Ni$N}&t#^M|O9<^PkzEN7 zu-thJW`Yp)XxBeu=G+pQ0?0~z_mc;G3yjmb`O5$yez!r{CNBuCUWmjgQB6w|@;yUu=0z@sgmsHKpMwXOh&a}h|5ocQf$zS2)xbfZ%tQ1Sk;-?rwgms5Z z5U<>+SPMD`BJ(vrdlg3(@VBkOpV`i|Im)4WocEUVJI6R|=cKNRCc6LXBKl~s76j0S zyhIRN)251uJI9$28IUqcR4?wWrN?}aPa0(pt+TZ?J#W5P7Sza8mCLg9PyXw<0gF{_ za<+7Dh(?KZP|T5EERF2a6)P>XJw~slg#s$$ju(7}@gVk7PfXOZA7Az|_IQwfzZQDh z{CQZv>}B&7ac#DS|8pe^sgV=Y2@4hMWc;NY(Ywr@tc;^zq2bU^*(^T97^w}VHVfzT zC-r*qzblds#WG98c3BT*CYx)Fn|462EN~R2d(0fV&F?4FBFq8?#8+QNbp$t@rY{nB zjI8?F(MJ?w5KtIqY```n#~7o6HpEeia?(Rf|TVI*V_uu7c09g}7z0 zkw(frC()urTWN0JN|I>vL-VfJwkJ4P?(Z((QFMM2kFTf)`7u7cen1dnD3Ztm(VcqT z;Cz%n=(po0iPTAWB^~ulBsJur%>T5^sI)3MsuKy-maNa9!z4y z5+phBBRe(eC0>B@hX!+q&Dq7O%z~P1y2kU%!ho~Fx$;H=+nm+D%J0b+Q`!q91~W94 zuk7Br=}DM*q?lO6z)i|5RmgQ8pv?(3JM(o1+Qi1+4QRJ# zxG)z38U0MJ(bRd7(<+(ssvhR+J!TF6&02p4*X4MFl=Ii|p{;@|4QrHAHkR!9hTL~~ z&M+ASr!i7efYpAkg7W={(eC?(jy=hCU(sdPN!Emk$ia83a*fjUizj|aXnI(DdQKuj z!s&oQ%1(d$=4%sNVOXd;(bI%!hXSazzL%rr^>5#V8m)KPY3mE*15&_A+aXCR;IxvL zsda39kr;5AxbF7vT=+mqYD5$aQP8G0R4S9mnqCumH>hTrb(80!E8s#&%*+0e>u;>g zX{Z=y*y_xc~en z{)GK>lP$Ww;LJaLB+PwShrQj4du1>TQtEH32))g7G`;lRbdy*`-AS zL#)&PG77}Wri*VK)YLY8K98^)d(?|Y3ftVLT)hIHMVWDtec20+7J(v%iA?|!+O5%m zCiju7r&FwstY-L>`u>%*{HuK-T`{O77}RI`pRTZAXnnQRbpY=pwzT}M!wTgOJGHVj zizM{SWu+M5QLQ*U^6!|N{~KZK-1NnlDCHh+nitTBo%uXQHTlMHU&?zRPStLcbrdxw z_XnI>`7&o%QFIsy2U7^;jz>v`9QRh%h8R#2ei$uKUoS(PtokvcyPzZlv|xY^pyL0k1C@4o_Afq$h$i> zG3O+8oHbp6uRmX}Z(6P`8$C@QC-Dh8uzYZ`t`Xqv3a|^?)0-9ql;H?~;7XorSO0{R$ z)dDMWk4eHCZ;Lz*?Q{V2?sNPr3cXOeoTZ0t4^>LwG^>L)aRhBvG7s!R>zXY`j%?zR zzDlh`ajVX|Rq~M!$ZJ#w01Izn?<<&C7%>f}bNQi!yz$82_mxLBUSKjb3PFnSczb3= zS#VNB;}7P23f`5Y)8Qz85mOt1@6DK5x2*axLe^Gzq(Wr(-)sZN7gm{s5R;p^7O@G% z>a2atSHGCm-y~6eZ@+*HzZhmf!NRf?V9&WDQ~w@-<%P?vvfS=t=W55?vfc(U=QAj6 z&KNCRKABpC?}=r6-TeT8+@;w3T9cd)SKH134U(PvUE?R4FAw^~JZCvqzNuJJfBi4> z;MgGG$Uw5@>0t^#CvGIuJmtWffNfd=gp-+#2kE<8`j%UiL7E#b>=78h zjxZ`i0}ta%k!8WzM&l$m4HTZnjGwxWuQzFhM((U(5J;SuBTFg1nNh+xq@;Al2;^(P8eZ1jW}INIr0b35!7C2rXa*aVw7uu)uAgmiHh?4^Rk!W`bZ^p#^M z!)5*&jWneg2B)vtK3gExktBU zVOO9SlGyN!lLP;3*abkpd7zuklkmiHopeWf@o)GEHI7ogvN;JIz`IQuR# zc=j_;viHLh{4aJ!zitce90EW@R7E}2A8|9fR7t=nUfzFMWdw<`H&ENNX zE(kN2#B3D~W?_Txe`&%_5PW>m8aLH|Un})LtI%VAp=r{g0IFPMw`0F?E1*e6U$e6*ync3>20g^lOBc* z>}1jQyG<2-IUy=UK~_GURw0ZYQ-|){(V^uWiQc0a{S-;2Om>t={A9GzGDcM!)uNTD zd(syoP~p8kmE?(8Q|`7HNI?L%#_7+5i~H=Kc8r|5 zd=T$M%c3MYL?eVaYP2K@JG|f=)9*91l+zL|D}k(%%s@B{>i&-Er+nyOl$%?yH_Sw0 zr8E?fusN5A$dI^$XosSNzzW6+==^HSrnAo0}TiCB*97D@cNO&uE#tsH|o;FRzPCpkG+(np~}YM z2ZP*&X>h*)TFsxQ!89RkYLnfo#W#1^jD$sRv|>`yFE-nQ*}76!Anv&%{o zjFOUvDyM(>t^Lk#3+QmA`2xjnv%f!jylE?|>DJFPFjfY=m9cEYKJo98@L&H9Fo`V} z%cd3&RWCVIb9{8&+kx$W(a%X2&(Xv(R+HH>;rov%f}Hn0`|@Q$9fc!bGTpny2McY zZi`$RF#cXiKT0y8Pl=mn4j5Dr%%tTws!VKp-ZdC%#fOwE`;dCFwa&l;IVYzqP|Qlc z0kZ+4a{dMvrrf+xZ1w#WfwidB+NmzfXjV0U#PypRz5|n#5MfM|pn+A9J1vw+xXi2H zpGj6h1tYISgG8M$e55XoV7Ly*q{Cli z0bhL_rRz9u%j3oc{J9ZOUU`!v@%V`AMw24eQTJwMER09jS6*(7YeN?#o>MkqTu3&K=7-4S7HBkdTF43LN=|QnIP$}h;GUo4h|iDpMj;e z6wBXIQnrS=M7)-dM;-v_Hm{Qz*O=Kg8w#n%zw2l{3`+Cj{1u^6VMu|Bw3mH2vGRjy zfI`N>`$2r{VP|{H-Ydj_aaiZ`PouX1@6YmR`pd>8N;5^AzrbUfeAU;)QHpTuF#&$X zWu0=t;XAJXDz;GEtd6y*hYv4NI%{72G6{CgFvsYc5DV2Ilsg{0u5uN@G|;|l{#)x7 zIm!2GvfX<{P87Tr9S*>;-M3%V15|~iNo*_?l1x(CmRC}pGp-uB?76hA3=b>089q%S zeH>3rf};R3S*hXFgsnAH&wk|mcU{t-S`BZtX&=t^ryXwO^c~mP8ixd7lkJ0V5|)lG z-AxMv$9v0CwG5*U{RVqAaN+OKoKLYgYS$0!m@j3e_u#ht$A<(YSp8Ulx z7-IfY-Bkb`%Yp~Qo41^3nnyDB&+mYQ}i|;*8vF~&n9WXy=34UEJN3-48Ez#J>7It_{HKqch2D-=_MwpU|PrwwiNtHD=A+R@@3e z=tuZ4nD3L6B&e9O%p%Yg+eMA11Gn60m!;*L;Uhmmo57T3itN4;CV};P6=N?l38H?V z5}2Up#eYdIoDLicXR&)}sEb{gKiJ>QwDR#A@suw63>z!ELeu(5H&95`JB;3BqD|lo&E6}i2xxNTf>IJ!;z5v#n(Jbm+*PbQ}%v(*GF$^2P z=xR=)THPa^$89;bYCN~4-i|m}9#v}h>BRcuSGPyMqAh$0By1;n{Z_kB3XZ(V8R;WUW>`qgI0ueR#3qp)W^Y z6cFfGcI#xWO~***I@|9UK9TI#q2!&6!w7us)HSmz!?t$+xdx*&3w95z>~{_ive`k1 zDD&cLWDZlk^nbfSY#U@5_KCBt;uDso^epd{HMPIUrSfg}6{>*i^qo`mYOQv)gJqz} zs-x^MBk4`SAqYUcRAQlUyA!{kt#|rEq|USDd~WUlw(iWmVidM&iT5$-?@5BO(>lk2 znuRV;1(9whK_nQ3sH}UZ(xlZIz#|9BHaGzuNj{m@+a{WYjh+!i0-wWsUgUbJ5Z(3% z*2DIzD_~#mCWJ&Tf69$xRYbDAIt7mXRnvp=JSLuH*J|M<-M^D|_~*JK^` z{TS`cjL60l%z%rsPPV|szG3YNkvlv9YDFm}HzLibD%mBJdl&mdH~4 z&<*YR_!n2!d~D{*a)~*8Pd?bqSyui8bW!E0#vCiIp;iJ{5!t#GzegHF-<|Mc7;6U8 z0fkB~{*ycd&Ir}{NKdO5O{j@$JouexXXY%X^{+2|;flit?EKdkS0^nkzTU-o%MChr zj)4`(ZHBG(BA2X_y)1;tdcwiZpDmgWYxc%Qtidx=T)N`5P*)HEITB!QQnQ@ip@ma_ z(=FM@{mfHlDm%NsXe8+Zh`M$`eDj4E&V~-D0e0Z6uO8oC-Omtv`tMDIvjiJ4W)Qr5 zNcE$1;EYl`@2B@>qVA3e#v$F%I*tS+kAeD2?kY$Ab_PghJsQ)N?A777ICLBA(LoYR z77+&=5Pp|fvd)JrRYo9=p2tb~JYyaPsTLUL#)A))HcF$@t3yR@qa$P*BZwoCn`k}7t1nZ;+-^w~tCkXorpxyAsVm0X)I!0$ zyC6gGwuCIxja=|b5Ywc~zQC5S=7f*})Cr`MqfF!3*hAyn4d6k3@E|uR&kvN>K^6#m ze5CBIY^UU(8{H31t96x$&~ke4IwAcTkTkC==KkhI6)t>h%8B%`VRNNqtrbZi3>F2g zRyr>ul^aY4{XMFRudj+JMZk0cYW(_)u2h#S2+9bGxVKM?>7ocd|EAQ*a)_vEdNr`J zQn8ai@cQ)h&9Vks>@T8E$pb6Un=#%!vU4L9JkisQ+1t)ReYeVgQ}7x1pL}~u7FaQQ zD-iQmnjol-cTcP9GFG7weqe=go#5Eui~M@1)Kek=o5)DoV8=O6%p^;v!J4|Yepbjt z=&mjJIb0THx{trq$cQt=PbRA4LW3RXn3V_};~m$+D>$O{Zl>yfP99tN?J8dGIv56d z&T#bjW30vD5qb9z??S?X2kB1*6c1MUb~Ry^dSvez)X|ODx+Ve%FaO%eU?<`KBM4$X zyL9w;^5MrfJ-(ZFqb&-2nV>fWwmzrbG)^LH6&)1_tiR8+De|%bU_0WnRfP}|>ifTw zu*{9u*>R4BBnyyo2b4j53GkPMD^;H;AX^2F5O+=q6EG%UVX4C4OuIENRXQCiMT}7 zu;RkkJcyE;vsJvKuv(o!7uwo2j61K@xn#>Kl7;P|1p%?$^WtWq&bOoxTeqg*BZ}Y} zi#1V-ZlUF$wyB_aa~G$LZ)ddALy_6r&w`+mQxngN;SnxSNUP9BR5_DVR;|^V2Ss;z z_*lH7j{5ISlN*q>WzX%Sn4t0QHrnm#GeYYrG1FgD9Lt*QtKtwt6Mr`xL+S0R8mJUA zk@XsTkPSneMPGC*2c$fe!Mj`n#q;Iq+A%0URucu=B({24g&%m?TlTrMsQmVfwIe8r z%Iidj6Vl1n5?=V50MZN4Hofta2KHS;jvUbz0@+Y2wg{}iao9`31f|_14?=6hluE2k zYuR2f>93%WUYj+F+a}_T88y&35WB=DM+@P5ISZb{ASIE#a*9>Zk?PiCzxuSoyqGLmd`BOPSdG$-#5e%=&W3yENpu*yFFnsJ0V771{DF~bk zhAo1bx1ENV%5z2D@mzpp2_$?+rK68ykB>nqQ}3E%!%(o|gk8@89f+o2HuOT#OBfdB zF?xS#)`KYssygZE>hjG-Z=Y3VKntSTa-kR|!8WVnW8a7i*UjGIU%cA}kN=i=Ner3! zu%J228|WCjd%aNs>XA)v!?hUP29GA*;MRD90hp8?!|B9r8M5Jn>D%OifXKK4ZorEo)y63PB^WsBlFxQ7Vr;+OqM3+#N zvM8Rz^ztpY*<8VQp=|H40=Hu)iO2EmwajKan;QXWK%{tg$;Ll|$8(AxWH>YX*$b^he`v<1V#{vM8+?-lPiW z3FVtO5WY8>s3%9`?&EH?ppjas)O}M*n=syYq1%htc#tgvWXp>;A!;dl3tu$YqAhp{ zY&YPWIeC@15I!BF126Wh9|FSA?9=F`j9iG&5_BS z^6L*^hJ&t;wqN*iEf94RA2LH|%S?*?B?; zDKR$l(@1@5vsrwjfxL$go38uN^kZ*$jh|2L%U|4r((6mV*>C|At*o>#<9lV_OpE!g zAn4)aZ>nak(P+^S;iAOel3gVYT1b|ypL>$y=gErOeijjaVP3d!Az30bbnJ5`+4ch~ zD2sMETrkVklMrIpQ&OMq0S!-bkJWBU0t;Mu7V9s0iI3XRqu&YZph(okb+8abwl^4l zTmBg$(cPLBSQIxz%1#houhq*m9`;M%ygxk9rlV+FYVjd5pE#Aoft1XY)TfD(RK=Y~ zFvM?#$l@pP9l@2Q)X>D|jWMsI;EhIkOPN~!SB){(&wmj^%K3A4bSA=c+83h_kHRXV z%FVFp3}YXBRL)lRf}1@hZv;euBD`^-Iet}h+PnLe(w|BD9}-w+VRPS9&fnV8#uxWv zmr87@xiEo1Rw(6Jo2d8iB6MidaPkFnNHY>v!&zK&D9f?khr2I(RpMsa4g!NxN?q}uSfV1MkM3~#vv{{5- zz8roO6cOqN<_g3^b-G6zCpm8a-#~)@*7azKwQDi&Reyf2t%%DLhl~T!&@!jfA^z@RWbcL<#A)3k@6O)^`k4fx0s%8 z7432RaCsO>cur*fNcyFHi_C3s6*vNDs}|;A77Ia$n}Ey$0-0sY>|{qjT$zy4WYM`| zdxWWlc|<~b&~2`09>+&=lk-U0Ch`tD$OoYAo$W>%E*_&r2ioU>5=JvQ=YiR~k@4HgEMWgXedxIj>+ zv*bG(JNfQgq2&NBGP{KIe|lhj6yowk=4X$RsM<^5WrkiXv3b|ePnpvUMiO(m!gBum zpX-qJ3aJf#1^OmOZH;U}9CnJ4>1!$lt^fT`Ur$>$R&l3<_nornt?n&$KXNNU?r3dM zGjzLdQCS?P#(&*g^d-)HIz@F_bjF%{wy&7)-bZ*emiYxbJSXRNbzHoJf>zv{g#8D( ke6_7L|6fz{f6VG@G6wij)ApUKf1iM|g1UU^BlFk)3vZj%_5c6? literal 0 HcmV?d00001 diff --git a/src/linstor_witness/provision-witness b/src/linstor_witness/provision-witness new file mode 100755 index 0000000..985e30d --- /dev/null +++ b/src/linstor_witness/provision-witness @@ -0,0 +1,634 @@ +#!/usr/bin/env python3 + +# ============================================================================== +# TODO(Mathieu Labourier): Remove all hacks for python 3.11. + +import contextlib +import typing +from typing import * # noqa: F403 + +if typing.TYPE_CHECKING: + from typing_extensions import override, ParamSpec + +if not hasattr(typing, "Never"): + Never = None # type: ignore + +if not hasattr(typing, "ParamSpec"): + + class _SubscriptableListMock(list): + def __getitem__(self, _): # type: ignore # noqa: ANN001, ANN204 + return self + + def __getattr__(self, _): # type: ignore # noqa: ANN001, ANN204 + return self + + def __call__(self, *_args, **_kwargs): # type: ignore # noqa: ANN002, ANN003, ANN204 + return self + + ParamSpec = _SubscriptableListMock() # type: ignore + Concatenate = _SubscriptableListMock() # type: ignore + +if not hasattr(typing, "Literal"): + from typing_extensions import Literal # noqa: F401, UP035 + +if not hasattr(typing, "override"): + + def override(method): # type: ignore # noqa: ANN001, ANN201 + with contextlib.suppress(AttributeError, TypeError): + # Set internal attr `__override__` like described in PEP 698. + method.__override__ = True + return method + +# ============================================================================== + +import argparse +import logging +from pathlib import Path +import shlex +import shutil +import subprocess +import sys +import time +from typing import Callable, cast, Dict, List, Optional, Tuple, Type, TypeVar +import uuid + +T = TypeVar("T") +P = ParamSpec("P") + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) + + +SCRIPT_NAME = "provision-witness" + +FROM_EXISTING_COMMAND = "from-existing" +FROM_EXISTING_ALIASES = ["from_existing", "existing", "e"] +FROM_EXISTING: set[str] = {*FROM_EXISTING_ALIASES, FROM_EXISTING_COMMAND} + +FROM_TEMPLATE_COMMAND = "from-template" +FROM_TEMPLATE_ALIASES = ["from_template", "template", "t"] +FROM_TEMPLATE: set[str] = {*FROM_TEMPLATE_ALIASES, FROM_TEMPLATE_COMMAND} + +MAKE_TEMPLATE_COMMAND = "make-template" +MAKE_TEMPLATE_ALIASES = ["make_template", "make", "mt", "m"] +MAKE_TEMPLATE: set[str] = {*MAKE_TEMPLATE_ALIASES, MAKE_TEMPLATE_COMMAND} + +UPDATE_IN_PLACE_COMMAND = "update-in-place" +UPDATE_IN_PLACE_ALIASES = ["update_in_place", "ui"] +UPDATE_IN_PLACE: set[str] = {*UPDATE_IN_PLACE_ALIASES, UPDATE_IN_PLACE_COMMAND} + +UPDATE_FROM_NEW_COMMAND = "update-from-new" +UPDATE_FROM_NEW_ALIASES = ["update_from_new", "un"] +UPDATE_FROM_NEW: set[str] = {*UPDATE_FROM_NEW_ALIASES, UPDATE_FROM_NEW_COMMAND} + +PROVISION_COMMANDS = {*FROM_EXISTING, *MAKE_TEMPLATE, *UPDATE_IN_PLACE} +SPAWN_COMMANDS = {*FROM_TEMPLATE} +NO_POOL_COMMANDS = {*UPDATE_IN_PLACE, *MAKE_TEMPLATE} + +XE_COMMAND_PATH = "/opt/xensource/bin/xe" +LINSTOR_COMMAND_PATH = "/usr/bin/linstor" + + +class MultitonMeta(type): + """ + Metaclass for implementing the Multiton design pattern. + + This metaclass ensures that instances of a class using it are created only once + for each unique combination of arguments passed to the class. This allows + distinct objects to exist for different parameter combinations but prevents + the creation of more than one object for the same set of parameters. + + This metaclass can be used for classes that could act as globals, + or that don't need to be created more than once. + + Attributes: + _instances: A dictionary storing unique class instances + based on their class type and constructor arguments as keys. + """ + + _instances: Dict[Tuple[Type[object], object, object], object] = {} + + @override + def __call__(cls: Type[T], *args: object, **kwargs: object) -> T: + key = (cls, args, tuple(kwargs.items())) + if key not in MultitonMeta._instances: + MultitonMeta._instances[key] = type.__call__(cls, *args, **kwargs) + return cast(T, MultitonMeta._instances[key]) + + +def _run_command(cmd: List[str], *, print_error: bool = True) -> str: + try: + return subprocess.run( # noqa: S603, UP022 + cmd, + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ).stdout.strip() + except subprocess.CalledProcessError as e: + if print_error: + logger.error("Command failed [%d]: '%s'\nstderr: `%s`", e.returncode, cmd, e.stderr) + raise + + +def _stream_output(process: "subprocess.Popen[str]", cmd: List[str]) -> None: + if not process.stdout: + raise ValueError("process.stdout is None.") + try: + for line in iter(process.stdout.readline, ""): + logger.info(line.strip()) + + if process.wait() != 0: + raise subprocess.CalledProcessError(process.returncode, cmd, stderr="streamed output") + finally: + process.stdout.close() + if process.stderr: + process.stderr.close() + + +class CommandRunner(metaclass=MultitonMeta): + def __init__(self, *, dry_run: bool = False, ssh_key: Optional[str] = None) -> None: + self.dry_run = dry_run + self.ssh_key = ssh_key + + def run_command( + self, + cmd: List[str], + *, + ignore_dry_run: bool = False, + remote_host: str = "", + print_error: bool = True, + stream: bool = False, + ) -> str: + with_key = ["-i", shlex.quote(self.ssh_key)] if self.ssh_key else [] + if remote_host: + cmd = ["ssh", f"root@{shlex.quote(remote_host)}"] + with_key + ["-C" + shlex.quote(shlex.join(cmd))] + + logger.debug("Running: '%s'.", cmd) + + if self.dry_run and not ignore_dry_run: + return "" + + if not stream: + return _run_command(cmd, print_error=print_error) + + try: + with subprocess.Popen( # noqa: S603 + cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, bufsize=1 + ) as process: + _stream_output(process, cmd) + except subprocess.CalledProcessError as e: + if print_error: + logger.error("Command failed [%d]: '%s'\noutput: `%s`", e.returncode, cmd, e.stdout) + raise + return "" + + def run_script( + self, path: str, *, remote_host: str = "", print_error: bool = True, remove_script: bool = True + ) -> None: + if not remote_host: + self.run_write_command(["bash", shlex.quote(path)], print_error=print_error, stream=True) + return + logger.debug("Creating remote temp directory on '%s'.", remote_host) + remote_dir = self.run_write_command(["mktemp", "-d"], remote_host=remote_host, print_error=print_error) + if not remote_dir: + err_msg = "Could not create remote temp directory." + logger.error(err_msg) + raise RuntimeError(err_msg) + remote_path = Path.joinpath(Path(remote_dir), uuid.uuid4().hex).absolute().as_posix() + logger.debug("Copying '%s' to '%s:%s'.", path, remote_host, remote_path) + with_key = ["-i", shlex.quote(self.ssh_key)] if self.ssh_key else [] + self.run_write_command( + ["scp"] + with_key + [shlex.quote(path), f"root@{shlex.quote(remote_host)}:{shlex.quote(remote_path)}"], + print_error=print_error, + ) + try: + logger.debug("Running '%s' on '%s'", path, remote_host) + self.run_write_command( + ["bash", shlex.quote(remote_path)], remote_host=remote_host, print_error=print_error, stream=True + ) + finally: + if remove_script: + logger.debug("Removing '%s' from '%s'", remote_path, remote_host) + self.run_write_command( + ["rm", shlex.quote(remote_path)], remote_host=remote_host, print_error=print_error, stream=True + ) + + def run_read_command( + self, cmd: List[str], *, remote_host: str = "", print_error: bool = True, stream: bool = False + ) -> str: + return self.run_command( + cmd, ignore_dry_run=True, remote_host=remote_host, print_error=print_error, stream=stream + ) + + def run_write_command( + self, cmd: List[str], *, remote_host: str = "", print_error: bool = True, stream: bool = False + ) -> str: + return self.run_command( + cmd, ignore_dry_run=False, remote_host=remote_host, print_error=print_error, stream=stream + ) + + +g_witness_ip = "" + + +def set_witness_ip(ip: str) -> None: + global g_witness_ip # noqa: PLW0603 + g_witness_ip = ip + + +def wait_for_condition(function: Callable[[], T], timeout: float, interval: float) -> T: + if timeout <= 0: + return function() + + start_time = time.time() + while True: + result = function() + if result or time.time() - start_time >= timeout: + return result + time.sleep(interval) + + +def is_remote_reachable(runner: CommandRunner) -> bool: + try: + runner.run_read_command(["hostname"], remote_host=g_witness_ip, print_error=False) + return True + except subprocess.CalledProcessError: + return False + + +def reboot_remote(runner: CommandRunner) -> None: + logger.info("Rebooting remote host '%s'", g_witness_ip) + runner.run_write_command(["reboot"], remote_host=g_witness_ip) + logger.info("Waiting for VM to go down...") + wait_for_condition(lambda: not is_remote_reachable(runner), timeout=300, interval=30) + logger.info("Waiting for VM to come back up...") + wait_for_condition(lambda: is_remote_reachable(runner), timeout=300, interval=30) + logger.error("VM is not reachable at IP '%s' after reboot.", g_witness_ip) + raise RuntimeError("VM is not reachable after reboot.") + + +def provision_witness() -> None: + script_dir = Path("./scripts").absolute() + + def script_path(script_name: str) -> str: + return Path.joinpath(script_dir, script_name).as_posix() + + runner = CommandRunner(dry_run=g_args.dry_run, ssh_key=g_args.witness_ssh_key_path) + if not g_args.skip_setup: + logger.info("Starting provisioning of LINSTOR witness node.") + logger.info("Setting up node.") + runner.run_script(script_path("setup.bash"), remote_host=g_witness_ip) + logger.info("Rebooting node.") + reboot_remote(runner) + if not g_args.skip_drbd: + logger.info("Installing DRBD.") + runner.run_script(script_path("drbd_install.bash"), remote_host=g_witness_ip) + if not g_args.skip_linstor: + logger.info("Installing LINSTOR.") + runner.run_script(script_path("linstor_install.bash"), remote_host=g_witness_ip) + if g_args.command in UPDATE_IN_PLACE: + logger.info("Rebooting node.") + reboot_remote(runner) + if g_args.command in MAKE_TEMPLATE: + logger.info("Preparing template.") + runner.run_script(script_path("prepare_template.bash"), remote_host=g_witness_ip, remove_script=False) + + +def spawn_witness() -> None: + input("You will have to enter the password for the witness node when prompted to. Press Enter when ready.") + + local_xva_path = Path(g_args.template_xva_path).absolute().as_posix() + xva_path = local_xva_path + if g_args.host_ip: + xva_path = "witness.xva" + with_key = ["-i", shlex.quote(g_args.host_ssh_key_path)] if g_args.host_ssh_key_path else [] + CommandRunner(dry_run=g_args.dry_run).run_write_command( + ["scp"] + + with_key + + [shlex.quote(local_xva_path), f"root@{shlex.quote(g_args.host_ip)}:{shlex.quote(xva_path)}"] + ) + + runner = CommandRunner(dry_run=g_args.dry_run, ssh_key=g_args.host_ssh_key_path) + logger.info("Spawning witness node from template.") + vm_uuid = runner.run_write_command( + [XE_COMMAND_PATH, "vm-import", f"filename={shlex.quote(xva_path)}", f"sr-uuid={shlex.quote(g_args.vm_sr)}"], + remote_host=g_args.host_ip, + ).strip() + if not vm_uuid: + logger.error("Could not spawn witness node") + sys.exit(1) + logger.info("Spawned witness node with UUID '%s'.", vm_uuid) + + runner.run_write_command( + [XE_COMMAND_PATH, "vm-param-set", f"uuid={shlex.quote(vm_uuid)}", f"name-label={shlex.quote(g_args.vm_name)}"], + remote_host=g_args.host_ip, + ) + + old_vifs = runner.run_read_command( + [XE_COMMAND_PATH, "vif-list", f"vm-uuid={shlex.quote(vm_uuid)}", "--minimal"], remote_host=g_args.host_ip + ) + if old_vifs: + for vif in old_vifs.split(","): + logger.info("Destroying old VIF '%s'.", vif.strip()) + runner.run_write_command( + [XE_COMMAND_PATH, "vif-destroy", f"uuid={shlex.quote(vif.strip())}"], remote_host=g_args.host_ip + ) + + vif_uuid = runner.run_write_command( + [ + XE_COMMAND_PATH, + "vif-create", + f"vm-uuid={shlex.quote(vm_uuid)}", + f"network-uuid={shlex.quote(g_args.vm_network)}", + "device=0", + ], + remote_host=g_args.host_ip, + ).strip() + if not vif_uuid: + logger.error("Could not create VIF.") + sys.exit(1) + logger.info("Created VIF with UUID '%s'.", vif_uuid) + + logger.info("Starting witness node.") + runner.run_write_command([XE_COMMAND_PATH, "vm-start", f"uuid={shlex.quote(vm_uuid)}"], remote_host=g_args.host_ip) + + def get_vm_ip() -> Optional[str]: + networks = runner.run_read_command( + [XE_COMMAND_PATH, "vm-list", f"uuid={shlex.quote(vm_uuid)}", "params=networks", "--minimal"], + remote_host=g_args.host_ip, + ) + if networks: + try: + return networks.split(";")[0].split(":")[1].strip() + except IndexError: + return None + return None + + wait_for_condition(lambda: get_vm_ip() is not None, timeout=300, interval=30) + vm_ip = get_vm_ip() + if not vm_ip: + logger.error("Could not get IP address of VM.") + vm_ip = input("Please enter the IP address of the VM: ") + else: + logger.info("VM IP address is '%s'.", vm_ip) + logger.info("Waiting for VM to be reachable at '%s'.", vm_ip) + + set_witness_ip(vm_ip) + + +def prepare_witness() -> None: + runner = CommandRunner(dry_run=g_args.dry_run, ssh_key=g_args.witness_ssh_key_path) + if g_args.set_ssh_key_path: + logger.info("Setting SSH key.") + runner.run_write_command( + ["ssh-copy-id", "-i", shlex.quote(g_args.set_ssh_key_path), f"root@{shlex.quote(g_witness_ip)}"] + ) + if g_args.witness_ssh_key_path is None: + g_args.witness_ssh_key_path = g_args.set_ssh_key_path + runner = CommandRunner(dry_run=g_args.dry_run, ssh_key=g_args.witness_ssh_key_path) + if g_args.set_hostname: + logger.info("Setting hostname to '%s'.", g_args.set_hostname) + runner.run_write_command( + ["hostnamectl", "set-hostname", shlex.quote(g_args.set_hostname)], remote_host=g_witness_ip + ) + if g_args.set_password: + logger.info("Setting password.") + runner.run_write_command( + ["echo", f"root:{shlex.quote(g_args.set_password)}", "|", "chpasswd"], remote_host=g_witness_ip + ) + + +def setup_pool() -> None: + runner = CommandRunner(dry_run=g_args.dry_run, ssh_key=g_args.cluster_ssh_key_path) + logger.info("Adding witness LINSTOR node to pool.") + ips = runner.run_read_command( + [XE_COMMAND_PATH, "host-list", "params=address", "--minimal"], remote_host=g_args.cluster_ip + ) + + if getattr(g_args, "evacuate_node", None): + logger.info("Evacuating node '%s'.", g_args.evacuate_node) + runner.run_write_command( + [ + LINSTOR_COMMAND_PATH, + f"--controller={shlex.quote(ips)}", + "node", + "evacuate", + shlex.quote(g_args.evacuate_node), + ], + remote_host=g_args.cluster_ip, + ) + if g_args.remove_node: + logger.info("Removing node '%s'.", g_args.evacuate_node) + runner.run_write_command( + [ + LINSTOR_COMMAND_PATH, + f"--controller={shlex.quote(ips)}", + "node", + "delete", + shlex.quote(g_args.evacuate_node), + ], + remote_host=g_args.cluster_ip, + ) + + cmd = [ + LINSTOR_COMMAND_PATH, + f"--controller={shlex.quote(ips)}", + "node", + "create", + shlex.quote(g_args.set_hostname or "witness"), + shlex.quote(g_witness_ip), + "--node-type", + "satellite", + ] + runner.run_write_command(cmd, remote_host=g_args.cluster_ip) + + +def main() -> None: + commands = {g_args.command, getattr(g_args, "update_command", None)} + # Whitelist + should_provision = not commands.isdisjoint(PROVISION_COMMANDS) + should_spawn = not commands.isdisjoint(SPAWN_COMMANDS) + # Blacklist + should_setup_pool = commands.isdisjoint(NO_POOL_COMMANDS) + + if should_provision: + logger.debug("IN PROVISION") + set_witness_ip(g_args.witness_ip) + prepare_witness() + provision_witness() + elif should_spawn: + logger.debug("IN SPAWN") + spawn_witness() + if not g_witness_ip and not g_args.dry_run: + logger.error("Could not determine IP address of the VM.") + sys.exit(1) + prepare_witness() + else: + argparser.error("Please specify a valid command.") + if should_setup_pool: + logger.debug("IN SETUP POOL") + setup_pool() + + +if __name__ == "__main__": + + def has_xe() -> bool: + return shutil.which(XE_COMMAND_PATH) is not None + + def generate_common_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(add_help=False) + + common_settings = parser.add_argument_group("Witness VM settings") + common_settings.add_argument("--set-hostname", help="Hostname to set on the target VM") + common_settings.add_argument("--set-password", help="Password to set on the target VM") + common_settings.add_argument("--set-ssh-key-path", help="SSH key path to copy on the target VM") + + common_group = parser.add_argument_group("Common arguments") + common_group.add_argument("--dry-run", action="store_true", help="Dry run") + common_group.add_argument( + "--log-level", + type=lambda x: x.upper(), + choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], + default="INFO", + help="Logging level (default: INFO)", + ) + + return parser + + def generate_common_linstor_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(add_help=False) + + common_linstor = parser.add_argument_group("Linstor pool") + common_linstor.add_argument("--cluster-ip", required=True, help="IP address of an existing node") + common_linstor.add_argument("--cluster-ssh-key-path", help="SSH key path for the linstor cluster") + + return parser + + def generate_from_existing_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(add_help=False) + + witness_group = parser.add_argument_group("Witness node") + witness_group.add_argument("--witness-ip", required=True, help="IP address of the target VM") + witness_group.add_argument("--witness-ssh-key-path", help="SSH key path for the target VM") + + skip_group = parser.add_argument_group("Skip") + skip_group.add_argument("--skip-setup", action="store_true", help="Skip setup step of the VM") + skip_group.add_argument("--skip-drbd", action="store_true", help="Skip DRBD installation") + skip_group.add_argument("--skip-linstor", action="store_true", help="Skip LINSTOR installation") + + return parser + + def generate_from_template_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(add_help=False) + + template_group = parser.add_argument_group("Template VM settings") + template_group.add_argument("--template-xva-path", type=Path, required=True, help="Path to the template XVA") + template_group.add_argument("--vm-name", required=True, help="Name of the target VM") + template_group.add_argument("--vm-sr", required=True, help="SR UUID to create the target VM in") + template_group.add_argument("--vm-network", required=True, help="UUID of the network to use") + + xcp_group = parser.add_argument_group("xcp-ng pool") + xcp_group.add_argument("--host-ip", required=has_xe(), help="IP address of the witness VM host") + xcp_group.add_argument("--host-ssh-key-path", help="SSH key path for the witness VM host") + + return parser + + def generate_update_from_new_common_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(add_help=False) + + parser.add_argument("--evacuate-node", required=True, help="Name of the node to be evacuated") + parser.add_argument("--remove-node", action="store_true", help="Remove the evacuated node") + + return parser + + argparser = argparse.ArgumentParser( + description="Spawn a template VM or provision an existing VM to be a LINSTOR witness node", prog=SCRIPT_NAME + ) + subparsers = argparser.add_subparsers(title="command", dest="command") + subparsers.required = True + + common_parser = generate_common_parser() + common_linstor_parser = generate_common_linstor_parser() + existing_parser = generate_from_existing_parser() + template_parser = generate_from_template_parser() + update_common_parser = generate_update_from_new_common_parser() + + # --- From existing --- + existing_subparser = subparsers.add_parser( + FROM_EXISTING_COMMAND, + aliases=FROM_EXISTING_ALIASES, + parents=[common_parser, common_linstor_parser, existing_parser], + help="Provision an existing VM.", + description="Provision an existing VM to be a LINSTOR witness node.", + ) + + # --- From template --- + template_subparser = subparsers.add_parser( + FROM_TEMPLATE_COMMAND, + aliases=FROM_TEMPLATE_ALIASES, + parents=[common_parser, common_linstor_parser, template_parser], + help="Spawn a witness VM from a template.", + description="Spawn a witness VM from a template.", + ) + + # --- Make template --- + make_template_subparser = subparsers.add_parser( + MAKE_TEMPLATE_COMMAND, + aliases=MAKE_TEMPLATE_ALIASES, + parents=[common_parser, existing_parser], + help="Prepare a VM to be used as a template.", + description="Prepare a VM to be used as a template.", + ) + + # --- Update in place -- + update_in_place_subparser = subparsers.add_parser( + UPDATE_IN_PLACE_COMMAND, + aliases=UPDATE_IN_PLACE_ALIASES, + parents=[common_parser, existing_parser], + help="Update an existing VM.", + description="Update an existing VM.", + ) + + # --- Update from new -- + update_from_new_subparser = subparsers.add_parser( + UPDATE_FROM_NEW_COMMAND, + aliases=UPDATE_FROM_NEW_ALIASES, + help="Spawn a template VM or provision an existing VM and update the pool to use it.", + description="Spawn a template VM or provision an existing VM and update the pool to use it.", + ) + update_from_new_subparsers = update_from_new_subparser.add_subparsers(title="update-command", dest="update_command") + update_from_new_subparsers.required = True + + # - From existing - + update_from_new_from_existing_subparser = update_from_new_subparsers.add_parser( + FROM_EXISTING_COMMAND, + aliases=FROM_EXISTING_ALIASES, + parents=[common_linstor_parser, common_parser, update_common_parser, existing_parser], + help="Provision an existing VM and update the pool to use it.", + description="Provision an existing VM and update the pool to use it.", + ) + + # - From template - + # TODO(Mathieu Labourier): Test update from template + update_from_new_from_template_subparser = update_from_new_subparsers.add_parser( + FROM_TEMPLATE_COMMAND, + aliases=FROM_TEMPLATE_ALIASES, + parents=[common_linstor_parser, common_parser, update_common_parser, template_parser], + help="Spawn a witness VM from a template and update the pool to use it.", + description="Spawn a witness VM from a template and update the pool to use it.", + ) + + g_args = argparser.parse_args() + if not hasattr(g_args, "witness_ssh_key_path"): + # Prevent errors in template mode + g_args.witness_ssh_key_path = None + + logging.basicConfig(level=getattr(logging, g_args.log_level.upper(), "INFO")) + + evacuate_node = getattr(g_args, "evacuate_node", None) + if evacuate_node and evacuate_node == g_args.set_hostname: + logger.error("Cannot remove the same node that is being added as witness.") + sys.exit(1) + + main() diff --git a/src/linstor_witness/src/scripts/drbd_install.bash b/src/linstor_witness/src/scripts/drbd_install.bash new file mode 100644 index 0000000..f0750d7 --- /dev/null +++ b/src/linstor_witness/src/scripts/drbd_install.bash @@ -0,0 +1,20 @@ +#!/bin/bash +set -e +set -v + +DRBD_SRC_DIR=/usr/src/drbd +DRBD_VERSION=9.2.16 +DRBD_UTILS_VERSION=9.21.1 + +mkdir -p $DRBD_SRC_DIR +rm -vrf $DRBD_SRC_DIR/drbd-* +yum group install -y Development\ Tools +yum install -y kernel-devel-$(uname -r) kernel-headers-$(uname -r) +cd $DRBD_SRC_DIR; curl -LO https://pkg.linbit.com//downloads/drbd/9/drbd-${DRBD_VERSION}.tar.gz; tar -xvf drbd-${DRBD_VERSION}.tar.gz; cd drbd-${DRBD_VERSION} +make && make install +cd $DRBD_SRC_DIR; curl -LO https://pkg.linbit.com//downloads/drbd/utils/drbd-utils-${DRBD_UTILS_VERSION}.tar.gz; tar xf drbd-utils-${DRBD_UTILS_VERSION}.tar.gz; cd drbd-utils-${DRBD_UTILS_VERSION} +./configure --prefix=/usr --localstatedir=/var --sysconfdir=/etc +make tools && make install +yum group remove -y Development\ Tools +yum clean all -y +modprobe drbd > /dev/null 2>&1 < /dev/null \ No newline at end of file diff --git a/src/linstor_witness/src/scripts/linstor_install.bash b/src/linstor_witness/src/scripts/linstor_install.bash new file mode 100644 index 0000000..f3fa7e1 --- /dev/null +++ b/src/linstor_witness/src/scripts/linstor_install.bash @@ -0,0 +1,47 @@ +#!/bin/bash +set -e +set -v + +cat < Date: Tue, 12 May 2026 09:57:34 +0200 Subject: [PATCH 11/11] doc: modify warning sentence Co-authored-by: Ronan Abhamon Signed-off-by: Mathieu Labourier --- src/linstor_witness/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linstor_witness/README.md b/src/linstor_witness/README.md index 3304aee..f7e7078 100644 --- a/src/linstor_witness/README.md +++ b/src/linstor_witness/README.md @@ -1,7 +1,7 @@ # XOSTOR Witness scripts In any case, the witness VM should not run on a node of the LINSTOR pool, but on a separate host. -It is recommended to use a dedicated, possibly a remote host for the witness VM. +It is required to use a dedicated, possibly a distant host for the witness VM. The witness VM should have access to the LINSTOR pool network and be able to communicate with the LINSTOR nodes. The witness VM will act as a TieBreaker and will help to keep the quorum of the LINSTOR pool in the case of a pool with only two nodes.