From 2d5bfdf76c6fb660da7495eb1fe22a6a1570d414 Mon Sep 17 00:00:00 2001 From: slampy Date: Wed, 22 Oct 2025 03:04:39 +0200 Subject: [PATCH 01/19] first step --- .gitmodules | 3 ++ ydb/coordination/__init__.py | 0 ydb/coordination/client.py | 0 ydb/coordination/coordination_lock.py | 0 ydb/coordination/exceptions.py | 0 ydb/coordination/tests/__init__.py | 0 .../tests/test_coordination_minimal.py | 0 ydb/coordination/ydb-protos | 1 + .../\321\201oordination_session.py" | 41 +++++++++++++++++++ 9 files changed, 45 insertions(+) create mode 100644 ydb/coordination/__init__.py create mode 100644 ydb/coordination/client.py create mode 100644 ydb/coordination/coordination_lock.py create mode 100644 ydb/coordination/exceptions.py create mode 100644 ydb/coordination/tests/__init__.py create mode 100644 ydb/coordination/tests/test_coordination_minimal.py create mode 160000 ydb/coordination/ydb-protos create mode 100644 "ydb/coordination/\321\201oordination_session.py" diff --git a/.gitmodules b/.gitmodules index 29daa459..fed941be 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "ydb-api-protos"] path = ydb-api-protos url = https://github.com/ydb-platform/ydb-api-protos.git +[submodule "ydb/coordination/ydb-protos"] + path = ydb/coordination/ydb-protos + url = https://github.com/ydb-platform/ydb-api-protos.git diff --git a/ydb/coordination/__init__.py b/ydb/coordination/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ydb/coordination/client.py b/ydb/coordination/client.py new file mode 100644 index 00000000..e69de29b diff --git a/ydb/coordination/coordination_lock.py b/ydb/coordination/coordination_lock.py new file mode 100644 index 00000000..e69de29b diff --git a/ydb/coordination/exceptions.py b/ydb/coordination/exceptions.py new file mode 100644 index 00000000..e69de29b diff --git a/ydb/coordination/tests/__init__.py b/ydb/coordination/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ydb/coordination/tests/test_coordination_minimal.py b/ydb/coordination/tests/test_coordination_minimal.py new file mode 100644 index 00000000..e69de29b diff --git a/ydb/coordination/ydb-protos b/ydb/coordination/ydb-protos new file mode 160000 index 00000000..a0c108c3 --- /dev/null +++ b/ydb/coordination/ydb-protos @@ -0,0 +1 @@ +Subproject commit a0c108c3525a7fd602705257cc716d9e7086393e diff --git "a/ydb/coordination/\321\201oordination_session.py" "b/ydb/coordination/\321\201oordination_session.py" new file mode 100644 index 00000000..d2beb9b1 --- /dev/null +++ "b/ydb/coordination/\321\201oordination_session.py" @@ -0,0 +1,41 @@ +import time + +from ydb._grpc.v5.protos.ydb_coordination_pb2 import SessionRequest +from ydb._grpc.v5.ydb_coordination_v1_pb2_grpc import CoordinationServiceStub + + +class CoordinationSession: + def __init__(self, driver: "ydb.Driver"): + self._driver = driver + + def acquire_semaphore(self, name: str, count: int = 1, timeout: int = 5000): + req = SessionRequest( + acquire_semaphore=SessionRequest.AcquireSemaphore( + name=name, + count=count, + timeout_millis=timeout, + req_id=int(time.time() * 1000), + data=b"", + ephemeral=True, + ), + session_start=SessionRequest.SessionStart() + ) + + res_iter = self._driver(req, CoordinationServiceStub, "Session") + res = next(res_iter) + acquire_result = getattr(res, "acquire_semaphore_result", None) + + if not acquire_result or not acquire_result.acquired: + raise RuntimeError(f"Failed to acquire semaphore {name}") + + return res.session_started.session_id + + def release_semaphore(self, name: str, session_id: int): + req = SessionRequest( + release_semaphore=SessionRequest.ReleaseSemaphore( + name=name, + req_id=int(time.time() * 1000), + ), + session_stop=SessionRequest.SessionStop() + ) + self._driver(req, CoordinationServiceStub, "Session") \ No newline at end of file From 96c0ff19635c0bd9b0087228d846adf6327bd8e9 Mon Sep 17 00:00:00 2001 From: slampy Date: Wed, 22 Oct 2025 03:05:14 +0200 Subject: [PATCH 02/19] also driver --- ydb/driver.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ydb/driver.py b/ydb/driver.py index 09d531d0..cfabb3ce 100644 --- a/ydb/driver.py +++ b/ydb/driver.py @@ -8,6 +8,8 @@ from . import tracing from . import iam from . import _utilities +from .coordination.client import CoordinationClient + logger = logging.getLogger(__name__) @@ -248,7 +250,7 @@ def get_config( class Driver(pool.ConnectionPool): - __slots__ = ("scheme_client", "table_client") + __slots__ = ("scheme_client", "table_client", "coordination_client") def __init__( self, @@ -287,10 +289,16 @@ def __init__( self._credentials = driver_config.credentials self.scheme_client = scheme.SchemeClient(self) + self.coordination_client = CoordinationClient(self) self.table_client = table.TableClient(self, driver_config.table_client_settings) self.topic_client = topic.TopicClient(self, driver_config.topic_client_settings) def stop(self, timeout=10): self.table_client._stop_pool_if_needed(timeout=timeout) self.topic_client.close() + if hasattr(self, "coordination_client"): + try: + self.coordination_client.close() + except Exception as e: + logger.warning(f"Failed to close coordination client: {e}") super().stop(timeout=timeout) From a4a292e00aad83c1997c1903899fd13ddb7dd159 Mon Sep 17 00:00:00 2001 From: slampy Date: Wed, 22 Oct 2025 03:05:39 +0200 Subject: [PATCH 03/19] working coord client plus not working lock/ conenction problemsg --- ydb/coordination/client.py | 56 +++++++++++ ydb/coordination/coordination_lock.py | 22 +++++ ydb/coordination/exceptions.py | 19 ++++ .../tests/test_coordination_minimal.py | 93 +++++++++++++++++++ .../\321\201oordination_session.py" | 58 ++++++++---- 5 files changed, 228 insertions(+), 20 deletions(-) diff --git a/ydb/coordination/client.py b/ydb/coordination/client.py index e69de29b..a454eba7 100644 --- a/ydb/coordination/client.py +++ b/ydb/coordination/client.py @@ -0,0 +1,56 @@ +import ydb +from ydb import operation + +from ydb._grpc.v5.ydb_coordination_v1_pb2_grpc import CoordinationServiceStub +from ydb._grpc.v5.protos import ydb_coordination_pb2 +from ydb.coordination.coordination_lock import CoordinationLock +from ydb.coordination.сoordination_session import CoordinationSession + + +class CoordinationClient: + def __init__(self, driver: "ydb.Driver"): + self._driver = driver + + def session(self): + return CoordinationSession(self._driver) + + + def create_node(self, path: str, config=None, operation_params=None): + request = ydb_coordination_pb2.CreateNodeRequest( + path=path, + config=config, + operation_params=operation_params, + ) + return self._driver( + request, + CoordinationServiceStub, + "CreateNode", + operation.Operation, + ) + + def describe_node(self, path: str, operation_params=None): + request = ydb_coordination_pb2.DescribeNodeRequest( + path=path, + operation_params=operation_params, + ) + return self._driver( + request, + CoordinationServiceStub, + "DescribeNode", + operation.Operation, + ) + + def delete_node(self, path: str, operation_params=None): + request = ydb_coordination_pb2.DropNodeRequest( + path=path, + operation_params=operation_params, + ) + return self._driver( + request, + CoordinationServiceStub, + "DropNode", + operation.Operation, + ) + + def lock(self, path: str, timeout: int = 5000, count: int = 1): + return CoordinationLock(self.session(), path, timeout, count) diff --git a/ydb/coordination/coordination_lock.py b/ydb/coordination/coordination_lock.py index e69de29b..1965bc23 100644 --- a/ydb/coordination/coordination_lock.py +++ b/ydb/coordination/coordination_lock.py @@ -0,0 +1,22 @@ +from ydb.coordination.сoordination_session import CoordinationSession + + +class CoordinationLock: + def __init__(self, session: CoordinationSession, path: str, timeout: int = 5000, count: int = 1): + self._session = session + self._path = path + self._timeout = timeout + self._count = count + + def acquire(self): + self._session.acquire_semaphore(self._path, self._count, self._timeout) + + def release(self): + self._session.release_semaphore(self._path) + + def __enter__(self): + self.acquire() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.release() diff --git a/ydb/coordination/exceptions.py b/ydb/coordination/exceptions.py index e69de29b..e2288dc0 100644 --- a/ydb/coordination/exceptions.py +++ b/ydb/coordination/exceptions.py @@ -0,0 +1,19 @@ + +class CoordinationError(Exception): + """Базовое исключение для всех ошибок координации.""" + + +class NodeAlreadyExists(CoordinationError): + """Узел координации уже существует.""" + + +class NodeNotFound(CoordinationError): + """Узел координации не найден.""" + + +class NodeLocked(CoordinationError): + """Узел координации уже захвачен.""" + + +class NodeTimeout(CoordinationError): + """Истекло время ожидания при захвате узла.""" \ No newline at end of file diff --git a/ydb/coordination/tests/test_coordination_minimal.py b/ydb/coordination/tests/test_coordination_minimal.py index e69de29b..b5122f86 100644 --- a/ydb/coordination/tests/test_coordination_minimal.py +++ b/ydb/coordination/tests/test_coordination_minimal.py @@ -0,0 +1,93 @@ +import pytest +import ydb +from ydb.coordination.client import CoordinationClient +import time + + + +@pytest.mark.integration +def test_coordination_client_local(): + driver_config = ydb.DriverConfig( + endpoint="grpc://localhost:2136", + database="/local", + ) + + with ydb.Driver(driver_config) as driver: + for _ in range(10): + try: + driver.wait(timeout=5) + break + except Exception: + time.sleep(1) + + + scheme = ydb.SchemeClient(driver) + base_path = "/local/coordination" + + try: + scheme.describe_path(base_path) + except ydb.issues.SchemeError: + scheme.make_directory(base_path) + desc = scheme.describe_path(base_path) + assert desc is not None, f"Directory {base_path} was not created" + + + node_path = f"{base_path}/test_node" + + client = CoordinationClient(driver) + + + create_future = client.create_node(path=node_path) + assert create_future is not None + + + res_desc = client.describe_node(path=node_path) + assert res_desc is not None + + + res_delete = client.delete_node(path=node_path) + assert res_delete is not None + +@pytest.mark.integration +def test_coordination_lock_lifecycle(): + driver_config = ydb.DriverConfig( + endpoint="grpc://localhost:2136", + database="/local", + ) + + with ydb.Driver(driver_config) as driver: + for _ in range(10): + try: + driver.wait(timeout=5) + break + except Exception: + time.sleep(1) + + scheme = driver.scheme_client + base_path = "/local/coordination" + try: + scheme.describe_path(base_path) + except ydb.SchemeError: + scheme.make_directory(base_path) + desc = scheme.describe_path(base_path) + assert desc is not None, f"Directory {base_path} was not created" + + # создаём client + client = CoordinationClient(driver) + + lock_path = f"{base_path}/test_lock" + + + with client.lock(lock_path, timeout=2000, count=1) as lock: + assert lock._session_id is not None, "Lock должен иметь session_id после acquire" + + + sem_state = client.describe_node(lock_path) + assert sem_state is not None, "Семафор должен существовать после acquire" + + + assert lock._session_id is None, "Lock должен быть освобождён после выхода из with" + + + sem_state_after = client.describe_node(lock_path) + assert sem_state_after is not None, "Семафор всё ещё существует" \ No newline at end of file diff --git "a/ydb/coordination/\321\201oordination_session.py" "b/ydb/coordination/\321\201oordination_session.py" index d2beb9b1..b2d95df3 100644 --- "a/ydb/coordination/\321\201oordination_session.py" +++ "b/ydb/coordination/\321\201oordination_session.py" @@ -1,41 +1,59 @@ import time +import ydb from ydb._grpc.v5.protos.ydb_coordination_pb2 import SessionRequest from ydb._grpc.v5.ydb_coordination_v1_pb2_grpc import CoordinationServiceStub +from ydb._utilities import SyncResponseIterator class CoordinationSession: def __init__(self, driver: "ydb.Driver"): self._driver = driver - - def acquire_semaphore(self, name: str, count: int = 1, timeout: int = 5000): - req = SessionRequest( + self._session_id = None + + def _ensure_session(self): + if self._session_id is None: + req = SessionRequest(session_start=SessionRequest.SessionStart()) + stream_it = self._driver( + req, + CoordinationServiceStub, + "Session", + ) + iterator = SyncResponseIterator(stream_it, lambda resp: resp) + first_resp = next(iterator) + self._session_id = first_resp.session_started.session_id + return self._session_id + + def acquire_semaphore(self, path: str, count: int = 1, timeout_millis: int = 5000): + session_id = self._ensure_session() + acquire_req = SessionRequest( acquire_semaphore=SessionRequest.AcquireSemaphore( - name=name, + name=path, count=count, - timeout_millis=timeout, + timeout_millis=timeout_millis, req_id=int(time.time() * 1000), data=b"", ephemeral=True, ), - session_start=SessionRequest.SessionStart() + session_start=SessionRequest.SessionStart(session_id=session_id) ) - - res_iter = self._driver(req, CoordinationServiceStub, "Session") - res = next(res_iter) - acquire_result = getattr(res, "acquire_semaphore_result", None) - - if not acquire_result or not acquire_result.acquired: - raise RuntimeError(f"Failed to acquire semaphore {name}") - - return res.session_started.session_id - - def release_semaphore(self, name: str, session_id: int): - req = SessionRequest( + stream_it = self._driver(acquire_req, CoordinationServiceStub, "Session") + iterator = SyncResponseIterator(stream_it, lambda resp: resp) + resp = next(iterator) + result = getattr(resp, "acquire_semaphore_result", None) + if not result or not result.acquired: + raise RuntimeError(f"Failed to acquire semaphore {path}") + + def release_semaphore(self, path: str): + if self._session_id is None: + return + release_req = SessionRequest( release_semaphore=SessionRequest.ReleaseSemaphore( - name=name, + name=path, req_id=int(time.time() * 1000), ), session_stop=SessionRequest.SessionStop() ) - self._driver(req, CoordinationServiceStub, "Session") \ No newline at end of file + stream_it = self._driver(release_req, CoordinationServiceStub, "Session") + SyncResponseIterator(stream_it, lambda resp: resp) + self._session_id = None \ No newline at end of file From d9de575a9d4db9c82b942afd273804b50bc81874 Mon Sep 17 00:00:00 2001 From: slampy Date: Wed, 29 Oct 2025 04:58:34 +0100 Subject: [PATCH 04/19] add wrappers, simple test is working, but problem to got structure back --- tests/coordination/coordination_client.py | 33 +++++ ydb/_apis.py | 21 ++++ ydb/_grpc/grpcwrapper/ydb_coordination.py | 93 ++++++++++++++ .../ydb_coordination_public_types.py | 47 ++++++++ ydb/coordination/client.py | 56 --------- ydb/coordination/coordination_client.py | 114 ++++++++++++++++++ .../tests/test_coordination_minimal.py | 2 +- ydb/driver.py | 2 +- 8 files changed, 310 insertions(+), 58 deletions(-) create mode 100644 tests/coordination/coordination_client.py create mode 100644 ydb/_grpc/grpcwrapper/ydb_coordination.py create mode 100644 ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py delete mode 100644 ydb/coordination/client.py create mode 100644 ydb/coordination/coordination_client.py diff --git a/tests/coordination/coordination_client.py b/tests/coordination/coordination_client.py new file mode 100644 index 00000000..19c60553 --- /dev/null +++ b/tests/coordination/coordination_client.py @@ -0,0 +1,33 @@ +import ydb +from ydb._grpc.v4.protos import ydb_coordination_pb2 + + +def test_coordination_nodes(driver_sync: ydb.Driver): + client = driver_sync.coordination_client + node_path = "/local/test_node" + + try: + drop_res = client.delete_node(node_path) + print(f"Node deleted (pre-clean), operation id: {drop_res.operation.id}") + except ydb.SchemeError: + pass + + create_res = client.create_node(node_path) + assert create_res.operation.id is not None + print(f"Node created, operation id: {create_res.operation.id}") + + describe_res = client.describe_node(node_path) + assert describe_res.operation.id is not None + print(f"Node described, operation id: {describe_res.operation.id}") + + describe_result_proto = ydb_coordination_pb2.DescribeNodeResult() + describe_res.operation.result.Unpack(describe_result_proto) + + print(f"Node path: {describe_result_proto.path}") + if describe_result_proto.HasField("config"): + print(f"Node config: {describe_result_proto.config}") + + # --- Удаляем узел --- + drop_res = client.delete_node(node_path) + assert drop_res.operation.id is not None + print(f"Node deleted, operation id: {drop_res.operation.id}") diff --git a/ydb/_apis.py b/ydb/_apis.py index 827a71a4..db5d50f0 100644 --- a/ydb/_apis.py +++ b/ydb/_apis.py @@ -11,6 +11,7 @@ ydb_operation_v1_pb2_grpc, ydb_topic_v1_pb2_grpc, ydb_query_v1_pb2_grpc, + ydb_coordination_v1_pb2_grpc, ) from ._grpc.v4.protos import ( @@ -22,6 +23,7 @@ ydb_operation_pb2, ydb_common_pb2, ydb_query_pb2, + ydb_coordination_pb2, ) else: @@ -33,6 +35,7 @@ ydb_operation_v1_pb2_grpc, ydb_topic_v1_pb2_grpc, ydb_query_v1_pb2_grpc, + ydb_coordination_v1_pb2_grpc, ) from ._grpc.common.protos import ( @@ -44,6 +47,7 @@ ydb_operation_pb2, ydb_common_pb2, ydb_query_pb2, + ydb_coordination_pb2, ) @@ -56,6 +60,7 @@ ydb_discovery = ydb_discovery_pb2 ydb_operation = ydb_operation_pb2 ydb_query = ydb_query_pb2 +ydb_coordination = ydb_coordination_pb2 class CmsService(object): @@ -134,3 +139,19 @@ class QueryService(object): ExecuteQuery = "ExecuteQuery" ExecuteScript = "ExecuteScript" FetchScriptResults = "FetchScriptResults" + +class CoordinationService(object): + Stub = ydb_coordination_v1_pb2_grpc.CoordinationServiceStub + + Session = "Session" + CreateNode = "CreateNode" + AlterNode = "AlterNode" + DropNode = "DropNode" + DescribeNode = "DescribeNode" + + Request = ydb_coordination.CreateNodeRequest + Response = ydb_coordination.CreateNodeResponse + DescribeRequest = ydb_coordination.DescribeNodeRequest + DescribeResponse = ydb_coordination.DescribeNodeResponse + DropRequest = ydb_coordination.DropNodeRequest + DropResponse = ydb_coordination.DropNodeResponse \ No newline at end of file diff --git a/ydb/_grpc/grpcwrapper/ydb_coordination.py b/ydb/_grpc/grpcwrapper/ydb_coordination.py new file mode 100644 index 00000000..315bdcfa --- /dev/null +++ b/ydb/_grpc/grpcwrapper/ydb_coordination.py @@ -0,0 +1,93 @@ +import typing +from dataclasses import dataclass + +import ydb + +if typing.TYPE_CHECKING: + from ..v4.protos import ydb_coordination_pb2 +else: + from ..common.protos import ydb_coordination_pb2 + +from .common_utils import IToProto, IFromProto, ServerStatus +from . import ydb_coordination_public_types as public_types + + +# -------------------- Requests -------------------- + +@dataclass +class CreateNodeRequest(IToProto): + path: str + config: typing.Optional[public_types.NodeConfig] = None + operation_params: typing.Any = None + + def to_proto(self) -> ydb_coordination_pb2.CreateNodeRequest: + cfg_proto = self.config.to_proto() if self.config else None + return ydb_coordination_pb2.CreateNodeRequest( + path=self.path, + config=cfg_proto, + operation_params=self.operation_params, + ) + + +@dataclass +class DescribeNodeRequest(IToProto): + path: str + operation_params: typing.Any = None + + def to_proto(self) -> ydb_coordination_pb2.DescribeNodeRequest: + return ydb_coordination_pb2.DescribeNodeRequest( + path=self.path, + operation_params=self.operation_params, + ) + + +@dataclass +class DropNodeRequest(IToProto): + path: str + operation_params: typing.Any = None + + def to_proto(self) -> ydb_coordination_pb2.DropNodeRequest: + return ydb_coordination_pb2.DropNodeRequest( + path=self.path, + operation_params=self.operation_params, + ) + + +@dataclass +class CreateNodeResponse(IFromProto): + operation : ydb.Operation + OPERATION_FIELD_NUMBER : int + + @staticmethod + def from_proto(msg: ydb_coordination_pb2.CreateNodeResponse) -> "CreateNodeResponse": + return CreateNodeResponse( + operation=msg.operation, + OPERATION_FIELD_NUMBER=msg.OPERATION_FIELD_NUMBER + ) + + +@dataclass +class DescribeNodeResponse(IFromProto): + operation : ydb.Operation + OPERATION_FIELD_NUMBER : int + + @staticmethod + def from_proto(msg: "ydb_coordination_pb2.DescribeNodeResponse") -> "DescribeNodeResponse": + return DescribeNodeResponse( + operation=msg.operation, + OPERATION_FIELD_NUMBER=msg.OPERATION_FIELD_NUMBER + ) + + +@dataclass +class DropNodeResponse(IFromProto): + operation : ydb.Operation + OPERATION_FIELD_NUMBER : int + + @staticmethod + def from_proto(msg: ydb_coordination_pb2.DropNodeResponse) -> "DropNodeResponse": + return DropNodeResponse( + operation=msg.operation, + OPERATION_FIELD_NUMBER=msg.OPERATION_FIELD_NUMBER + ) + diff --git a/ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py b/ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py new file mode 100644 index 00000000..7732c1f3 --- /dev/null +++ b/ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py @@ -0,0 +1,47 @@ +from dataclasses import dataclass +from enum import Enum +import typing + +if typing.TYPE_CHECKING: + from ..v4.protos import ydb_coordination_pb2 +else: + from ..common.protos import ydb_coordination_pb2 + +class ConsistencyMode(Enum): + STRICT = 0 + RELAXED = 1 + +class RateLimiterCountersMode(Enum): + NONE = 0 + BASIC = 1 + FULL = 2 + +@dataclass +class NodeConfig: + attach_consistency_mode: ConsistencyMode + path: str + rate_limiter_counters_mode: RateLimiterCountersMode + read_consistency_mode: ConsistencyMode + self_check_period_millis: int + session_grace_period_millis: int + + @staticmethod + def from_proto(msg: ydb_coordination_pb2.Config) -> "NodeConfig": + return NodeConfig( + attach_consistency_mode=ConsistencyMode(msg.attach_consistency_mode), + path=msg.path, + rate_limiter_counters_mode=RateLimiterCountersMode(msg.rate_limiter_counters_mode), + read_consistency_mode=ConsistencyMode(msg.read_consistency_mode), + self_check_period_millis=msg.self_check_period_millis, + session_grace_period_millis=msg.session_grace_period_millis, + ) + + def to_proto(self) -> ydb_coordination_pb2.Config: + return ydb_coordination_pb2.Config( + attach_consistency_mode=self.attach_consistency_mode.value, + path=self.path, + rate_limiter_counters_mode=self.rate_limiter_counters_mode.value, + read_consistency_mode=self.read_consistency_mode.value, + self_check_period_millis=self.self_check_period_millis, + session_grace_period_millis=self.session_grace_period_millis, + ) diff --git a/ydb/coordination/client.py b/ydb/coordination/client.py deleted file mode 100644 index a454eba7..00000000 --- a/ydb/coordination/client.py +++ /dev/null @@ -1,56 +0,0 @@ -import ydb -from ydb import operation - -from ydb._grpc.v5.ydb_coordination_v1_pb2_grpc import CoordinationServiceStub -from ydb._grpc.v5.protos import ydb_coordination_pb2 -from ydb.coordination.coordination_lock import CoordinationLock -from ydb.coordination.сoordination_session import CoordinationSession - - -class CoordinationClient: - def __init__(self, driver: "ydb.Driver"): - self._driver = driver - - def session(self): - return CoordinationSession(self._driver) - - - def create_node(self, path: str, config=None, operation_params=None): - request = ydb_coordination_pb2.CreateNodeRequest( - path=path, - config=config, - operation_params=operation_params, - ) - return self._driver( - request, - CoordinationServiceStub, - "CreateNode", - operation.Operation, - ) - - def describe_node(self, path: str, operation_params=None): - request = ydb_coordination_pb2.DescribeNodeRequest( - path=path, - operation_params=operation_params, - ) - return self._driver( - request, - CoordinationServiceStub, - "DescribeNode", - operation.Operation, - ) - - def delete_node(self, path: str, operation_params=None): - request = ydb_coordination_pb2.DropNodeRequest( - path=path, - operation_params=operation_params, - ) - return self._driver( - request, - CoordinationServiceStub, - "DropNode", - operation.Operation, - ) - - def lock(self, path: str, timeout: int = 5000, count: int = 1): - return CoordinationLock(self.session(), path, timeout, count) diff --git a/ydb/coordination/coordination_client.py b/ydb/coordination/coordination_client.py new file mode 100644 index 00000000..d43a3719 --- /dev/null +++ b/ydb/coordination/coordination_client.py @@ -0,0 +1,114 @@ +import typing +from typing import Optional + +import ydb +from ydb import _apis, issues + +from .coordination_lock import CoordinationLock +from .сoordination_session import CoordinationSession + + +def wrapper_create_node(rpc_state, response_pb, *_args, **_kwargs): + from .._grpc.grpcwrapper.ydb_coordination import CreateNodeResponse + + issues._process_response(response_pb.operation) + return CreateNodeResponse.from_proto(response_pb) + + +def wrapper_describe_node(rpc_state, response_pb, *_args, **_kwargs): + from .._grpc.grpcwrapper.ydb_coordination import DescribeNodeResponse + + issues._process_response(response_pb.operation) + return DescribeNodeResponse.from_proto(response_pb) + + +def wrapper_delete_node(rpc_state, response_pb, *_args, **_kwargs): + from .._grpc.grpcwrapper.ydb_coordination import DropNodeResponse + + issues._process_response(response_pb.operation) + return DropNodeResponse.from_proto(response_pb) + + +class CoordinationClient: + def __init__(self, driver: "ydb.Driver"): + self._driver = driver + + def session(self) -> "CoordinationSession": + return CoordinationSession(self._driver) + + def _call_node( + self, + request, + rpc_method, + wrapper_fn, + settings: Optional["ydb.BaseRequestSettings"] = None, + ): + return self._driver( + request, + _apis.CoordinationService.Stub, + rpc_method, + wrap_result=wrapper_fn, + wrap_args=(), + settings=settings, + ) + + def create_node( + self, + path: str, + config: typing.Optional[typing.Any] = None, + operation_params: typing.Optional[typing.Any] = None, + settings: Optional["ydb.BaseRequestSettings"] = None, + ) -> _apis.ydb_coordination.CreateNodeResponse: + request = _apis.ydb_coordination.CreateNodeRequest( + path=path, + config=config, + operation_params=operation_params, + ) + return self._call_node( + request, + _apis.CoordinationService.CreateNode, + wrapper_create_node, + settings, + ) + + def describe_node( + self, + path: str, + operation_params: typing.Optional[typing.Any] = None, + settings: Optional["ydb.BaseRequestSettings"] = None, + ) -> _apis.ydb_coordination.DescribeNodeResponse: + request = _apis.ydb_coordination.DescribeNodeRequest( + path=path, + operation_params=operation_params, + ) + return self._call_node( + request, + _apis.CoordinationService.DescribeNode, + wrapper_describe_node, + settings, + ) + + def delete_node( + self, + path: str, + operation_params: typing.Optional[typing.Any] = None, + settings: Optional["ydb.BaseRequestSettings"] = None, + ) -> _apis.ydb_coordination.DropNodeResponse: + request = _apis.ydb_coordination.DropNodeRequest( + path=path, + operation_params=operation_params, + ) + return self._call_node( + request, + _apis.CoordinationService.DropNode, + wrapper_delete_node, + settings, + ) + + def lock( + self, + path: str, + timeout: int = 5000, + count: int = 1, + ) -> "CoordinationLock": + return CoordinationLock(self.session(), path, timeout, count) diff --git a/ydb/coordination/tests/test_coordination_minimal.py b/ydb/coordination/tests/test_coordination_minimal.py index b5122f86..a7543ae5 100644 --- a/ydb/coordination/tests/test_coordination_minimal.py +++ b/ydb/coordination/tests/test_coordination_minimal.py @@ -1,6 +1,6 @@ import pytest import ydb -from ydb.coordination.client import CoordinationClient +from ydb.coordination.coordination_client import CoordinationClient import time diff --git a/ydb/driver.py b/ydb/driver.py index cfabb3ce..661771c6 100644 --- a/ydb/driver.py +++ b/ydb/driver.py @@ -8,7 +8,7 @@ from . import tracing from . import iam from . import _utilities -from .coordination.client import CoordinationClient +from .coordination.coordination_client import CoordinationClient From 7701a056f9258635d6accbbc96f13e8dd7f1f97b Mon Sep 17 00:00:00 2001 From: slampy Date: Wed, 29 Oct 2025 14:31:34 +0100 Subject: [PATCH 05/19] several fixes plus wrappers --- ...k_on_api_and_examples_from_zookeeper.patch | 942 ++++++++++++++++++ .../tests => tests/coordination}/__init__.py | 0 tests/coordination/coordination_client.py | 27 +- ydb/_apis.py | 9 +- ydb/coordination/coordination_client.py | 51 +- ydb/coordination/coordination_lock.py | 22 - ydb/coordination/exceptions.py | 19 - ydb/coordination/operations.py | 50 + .../tests/test_coordination_minimal.py | 93 -- ydb/coordination/ydb-protos | 1 - .../\321\201oordination_session.py" | 59 -- 11 files changed, 1023 insertions(+), 250 deletions(-) create mode 100644 First_look_on_api_and_examples_from_zookeeper.patch rename {ydb/coordination/tests => tests/coordination}/__init__.py (100%) delete mode 100644 ydb/coordination/coordination_lock.py delete mode 100644 ydb/coordination/exceptions.py create mode 100644 ydb/coordination/operations.py delete mode 100644 ydb/coordination/tests/test_coordination_minimal.py delete mode 160000 ydb/coordination/ydb-protos delete mode 100644 "ydb/coordination/\321\201oordination_session.py" diff --git a/First_look_on_api_and_examples_from_zookeeper.patch b/First_look_on_api_and_examples_from_zookeeper.patch new file mode 100644 index 00000000..c29311ce --- /dev/null +++ b/First_look_on_api_and_examples_from_zookeeper.patch @@ -0,0 +1,942 @@ +Subject: [PATCH] First look on api and examples from zookeeper +--- +Index: .gitmodules +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/.gitmodules b/.gitmodules +--- a/.gitmodules (revision 40ac6927bb0d93f3d8e647f0596fe9cef35066e3) ++++ b/.gitmodules (date 1759873809817) +@@ -1,3 +1,6 @@ + [submodule "ydb-api-protos"] + path = ydb-api-protos + url = https://github.com/ydb-platform/ydb-api-protos.git ++[submodule "ydb/coordination/ydb-protos"] ++ path = ydb/coordination/ydb-protos ++ url = https://github.com/ydb-platform/ydb-api-protos.git +Index: ydb/coordination/exceptions.py +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/ydb/coordination/exceptions.py b/ydb/coordination/exceptions.py +new file mode 100644 +--- /dev/null (date 1760451514839) ++++ b/ydb/coordination/exceptions.py (date 1760451514839) +@@ -0,0 +1,19 @@ ++ ++class CoordinationError(Exception): ++ """Базовое исключение для всех ошибок координации.""" ++ ++ ++class NodeAlreadyExists(CoordinationError): ++ """Узел координации уже существует.""" ++ ++ ++class NodeNotFound(CoordinationError): ++ """Узел координации не найден.""" ++ ++ ++class NodeLocked(CoordinationError): ++ """Узел координации уже захвачен.""" ++ ++ ++class NodeTimeout(CoordinationError): ++ """Истекло время ожидания при захвате узла.""" +\ No newline at end of file +Index: ydb/coordination/client.py +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/ydb/coordination/client.py b/ydb/coordination/client.py +new file mode 100644 +--- /dev/null (date 1760520160132) ++++ b/ydb/coordination/client.py (date 1760520160132) +@@ -0,0 +1,170 @@ ++from lib2to3.fixes.fix_input import context ++from typing import Optional ++ ++ ++class CoordinationClient: ++ def __init__(self, driver): ++ self.driver = driver ++ # driver должен быть инициализирован и готов к работе ++ ++ # ------------------ Создание узла ------------------ ++ def create(self, path: str, ttl: int = 60): ++ pass ++ ++ def lock(self): ++ # ---Создание сессии и так далее ( а тут в локе инкапсулирована вся логика)------------------ ++ return CoordinationLock() ++ ++ ++ # ------------------ Получение содержимого узла ------------------ ++ def describe(self, path: str): ++ pass ++ ++ # ------------------ Удаление узла ------------------ ++ def delete(self, path: str): ++ pass ++ ++ ++class CoordinationLock: ++ def enter , exit - context manager ++ ++ # ------------------ Захват узла ------------------ ++ def acquire(self, path: str, timeout: Optional[int] = None): ++ pass ++ ++ ++ # ------------------ Освобождение узла ------------------ ++ def release(self, path: str): ++ pass ++ ++#USE CASES: ++ ++""" ++coord_client = CoordinationClient(driver) ++ ++# Синхронный вызов ++coord_client.create("/my_app/lock_node", ttl=120) ++ ++# Асинхронный вызов ++await coord_client.create_async("/my_app/lock_node", ttl=120) ++ ++как опциональный параметр ++settings = NodeSettings( ++ read_consistency_mode=ReadConsistencyMode.STRICT, ++ attach_consistency_mode=AttachConsistencyMode.RELAXED, ++ self_check_period=1, ++ session_grace_period=5 ++) ++ ++coord_client.create("/path/to/mynode", ttl=60, node_settings=settings) ++ ++что то такое в статусе ошибки, чтобы не кидать самому исключения и не вызывать isSuccess ++def check_status(status): ++ if not status.is_success(): ++ raise RuntimeError(f"Operation failed: {status}") ++ ++# Использование ++status = coord_client.create("/path/to/mynode") ++check_status(status) ++ ++аналоги kazoo и zkpython ++from kazoo.client import KazooClient ++ ++zk = KazooClient(hosts='127.0.0.1:2181') ++zk.start() ++ ++# Создание persistent узла ++zk.create("/my_node", b"some_data") ++ ++# Создание ephemeral узла (аналог TTL/сессионного узла) ++zk.create("/my_ephemeral_node", b"data", ephemeral=True) ++ ++Если узел уже существует — выбрасывается NodeExistsError. ++ ++Исключения — основной способ проверки успеха. ++ ++TTL напрямую не задаётся, ephemeral узел живёт столько, сколько сессия клиента. ++ ++from kazoo.recipe.lock import Lock ++ ++lock = Lock(zk, "/my_lock") ++with lock: # контекстный менеджер автоматически acquire/release ++ # критическая секция ++ do_work() ++ ++Асинхронный код через kazoo официально не поддерживается, но есть сторонние async-обёртки. ++ ++Проверка успеха встроена в методы — если acquire не удаётся, можно поймать исключение или заблокироваться до тайм-аута. ++ ++data, stat = zk.get("/my_node") ++print("data:", data) ++print("stat:", stat) ++ ++stat содержит информацию о версии, времени изменения и количестве детей. ++ ++Можно подписаться на watch, чтобы получать уведомления об изменениях. ++ ++zk.delete("/my_node") ++Исключение NoNodeError если узел не существует. ++ ++В стиле zookeper - тогда будет чет такое . ++ ++coord_client.create("/my_app/lock_node", ttl=60) ++ ++with coord_client.lock("/my_app/lock_node", timeout=5): ++ # критическая секция ++ print("Lock acquired, doing work") ++ # lock автоматически отпустится после выхода из блока ++ ++await coord_client.create_async("/my_app/lock_node", ttl=60) ++ ++async with coord_client.lock_async("/my_app/lock_node", timeout=5): ++ # критическая секция ++ print("Async lock acquired, doing work") ++ # lock автоматически отпустится после выхода из блока ++ ++ ++CoordinationClient ++ ├─ create_session / create_session_async ++ └─ возвращает CoordinationSession ++CoordinationSession ++ ├─ create / create_async ++ ├─ delete / delete_async ++ ├─ describe / describe_async ++ ├─ lock / lock_async # контекстный менеджер для acquire/release ++ ++ ++ session.create_semaphore("my-semaphore", 10, "my-data") ++ ++# синхронно ++with session.semaphore("my-semaphore"): ++ # критическая секция ++ do_work() ++ ++# асинхронно ++async with session.semaphore_async("my-semaphore"): ++ await do_work_async() ++ ++def on_semaphore_changed(changed): ++ if changed: ++ print("Semaphore state changed!") ++ else: ++ print("Watch expired, need to resubscribe") ++ ++settings = DescribeSemaphoreSettings( ++ on_changed=on_semaphore_changed, ++ watch_data=True, ++ watch_owners=True, ++ include_owners=True, ++ include_waiters=True ++) ++ ++desc = session.describe_semaphore("my-semaphore", settings=settings) ++ ++print(f"Semaphore {desc.name}: {desc.count}/{desc.limit}") ++for owner in desc.owners: ++ print(f"Owner {owner.session_id} with {owner.count} tokens") ++ ++на синхронном драйвере написать базу -> compose файлик, 4 метода реализовать + написать обертки (toProto, fromProto) +++ в идеале тесты написать (без локов) -> все теститься ++""" +\ No newline at end of file +diff --git a/ydb/coordination/ydb-protos/img.png b/ydb/coordination/ydb-protos/img.png +new file mode 100644 +index 0000000000000000000000000000000000000000..1e0be898b5ebe820a3d0b87fcd92ebe9c7a04c64 +GIT binary patch +literal 40330 +zc%0XTNIX5d(m~nH>V`e;xB7n +zZ%>Uc&L%kMJID`>D`?&O)hJhM5}Q!XJ=FhhUGR{_+9@CFK(x;21;Bf@7byIk<3-NRVo~`$6n(&MoUbyC0q?X#BSOe&~7cvEBE}=OrEf +zo%-tkrxSw1B(Z4WkAVlQRAjg}Z{k@#X8Dg1I3(#cc&H#{z3-RZSu?YqO4Y)jY=@Cb +zHNm~8b<{KRg#u#B;tpE4T1}o6ZVr*bK0JR$wYqRPWB=|I4V4|Ytm9DMi^(sM26)5o +zUNyzQlojdv=QSxms>yD`BnL)K0r6Yk-okf#_aD|O(FqN13nwm=>c<3Y(f&Ri7O2*- +zltZS4rzA#^vUKq>xl!2Ly4nEVxBVqJ)OPF0=g)tQiDK^K#o?R2oHs@;x4TL&uS +zI*fDxJ~W-QWq->W5we%m%~BxcixxFC^ln~1d6wR@kU_t>cWOwjtD%Dqp%dnKWL<#x +z+vBVc7^7kwTEHApPu}>$=QoFS7c|EyO2DQ-7chwnjPj9b7av-NR_bB7V4d)Vxw$px +z_Fxcqb}T#Ryt2cFfbz97CWJ8@0BUAW7NE2)5@@r{0kitgyB3&;%Ba99i}6eF+^Sx! +z>I)#j29zL^D@E^E+vwLtO$3NS_3m-9q6WVno4y(%tOxT9Y;`CoI%UXL_`0O{8C+}R +zc1_eM+y|#s;04CM_b=ai4A-?4VBorA3n`Ns^sQy}6mVX2{Eee8_u~pxi97we#e{|J +zwo2BY# +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 +z3jrQ~B0n{^-PRt8+Q!5s?|$)j|`A +z3Ew_GR;E`jWRw}iWv{iKFo(*AwuQ~(f8bk(0&e#*aX!TEC1v$fWvr%fiNvp8ibIe+ +zv06TS7)RYWHBKaT-zJ_IT1z;npUvvT^_VSFIq?%2-cyE~FQKwt;jYHXsDy5j;ag5K +zZ>T-!%|R9HWU~nlg%@l64G(vl9Rmn=Fe60)_qcWy*;r4{*6+>UtI2eA +z7vd|Czudh`htLzvadn9Km0=``vqmmwbq6r=Y7G6O=glHxcYzx`mbifB?Na&k*V8{G +z${*NyU*Me(6oux_hqsirIaM;JZO*gp48I?=4J;QqAUj7|S-kv+^>dLZ_m=89;Mr5^rV2PWYBK=fYs}`9AeM}m)r|cju0t$;n(hOzp0;W +zr4!mhPmBM;Jx{~ENM9A#of|h4Y(N1t96<0Lpu^p!oi0OD$`ivFl>mNVi}j&i_;yQ8 +z);4!pJ%zoPstP*7y55D5E^F6MHQxCokzL)#%-g?x5-s8vLfIZ4J&^(@%#n54Pbu43 +zR`=J9B^qa-IMYa-xK3@qrq!QQM0)NY(g~b_!Js0!SY)-EO9h?DvmTBq!;jex= +zW~Kc@pQ=bdkhMpyly79(r6T^$Tsl^$7J=J39QVN_YUk(oiIxu@oY}GD*e!f+^Pjw( +z4YEkI^#(rLth0r@?QB@@557i(r3HH6t8>Nyf}g3N#LHEDHwk@mFfjkn;p_AXT6hXq +z86a=!TTFc;A)!Uv98&iwovCi|F*#T5!^Pu@i2@o?UYXF^brEz#!e{5Rv>7GBW|;9l +zqGt7hZrAMlE*jH7x7r +zu2|ptP%bYwQ8QDu=cUA-NvFi~TC(Er4^j$dUV6INZSoodj6wz?@u|C7(n_9hzw`08 +z!keAVbqqJ$s%R-a6Jq-7FiVff6Hj0t%B_ap4jT~Jqm>ekmRgU-uT^Z_YGMGdSV9km +z{JF`&p_4Zfrim`yi^nTh&mt1%#k)iT{e!EIQMF9ml<(#0`+8)qpf5y~ksa6V=Lnmb +zPK{zuj6jo?6f2*e!ylF--ujsTYB~0%MM+M({V%vyA%se5fyZ?b=FQ^DZq)VgHVe?* +z@tzAJaLFQ5!>`8vaAhQ-S&5aOzKbl~n&I1*<-O}GV=+bzkZ!K7u9vK!F(Jmj{QaUZ +ztL)M{J2=t7*zG(xW^*1Dpth=}C9W%KIGy(KsD>3N|HE0QzxeL@{CvL5#BWyItNPHq7IV@ZY^_=R4 +zyP>AIR;^Q?<;#e)&h?_?5fg88pR`WLi!i|*(RDdyewi3sVSPrzEI?)$* +zZdxbk&dFt}hON=x4W_+Pb}5Z9TUTq>juEhDE(a2Vg`(IA*G}QKrx*m=MhDzrPTaK( +z&6t@e>xQQANR@kvfYp#~bU5Vw&fT9PJ-eCHb*O6F9y%CQELdBIj}~<=P(ZAX39P1m +ztQYrq%+z}c@INMILw#}{%CBTf7fAA~f7{GEJTm0*|Y>fZMdGBsgF4-sM-Gtr6+0TQPjAu-->)i{6JT9kR4cOAjK +zZ>y9D2;O>}v28&CuQbCaXZn{h4J6&yFTF5uY`s#__uq3F7dp>eI +z7I*u2r(dCQothV^6}+K8XuopQPW+y$37 +z)u+W8AXYz+x!G&D0>!9V_;zY}&wi*h79>q?1|3+jXa8)r&1fpQOmOmbmzGuNw3Tt1cOiu|SR0X*` +z#1NY84H{?^fac^QcS>dsCtk*EAh>%BvOQEYy+)<7srVKdc0|#xKPsh-J6oIG>T_0H9~yIMGx0@%lTWpQN0rZ`Iv^8I3?!k +zqErY@Ha;5vu)1D#KwQc|oC#=l8(heh_t8nN{3s;aqx?Driza{e6a?E^a#|!Jdh+B? +zPrIH)q30+)>R!!mIG(+iWB9Z&9yzIrptsI#%VFF2Kd}8lN!282Vxl-H$@VF|@E6h* +z5j6|ETxV`Hb*A?5^KqovTH-6V%N6D1t0vH-ty4!t-cpxJj}Vjx4Uffnk&b%ujUpC% +zb0Qfv@#Tg6kIu{3NIDExB1E1izDcmZ4j`|XD9>N0S3M2gTuTVz3a>JtlPh>aM-=6~ +z5TlhM220_YKY6nw+=|p=r)u%OG}-&gh%T)TIZ_3fn@Fy0IC&SGLqYb{^nG)nl_rbb +ziqqMYxM<-`-es<42WxRvH|$LpF>hr`tLhcyl%?lWO{L +zb+?D2b#DS-!6`9eD~D?Z@FAWYDo{ugYD|MG4F>U0j3HUWO40_ +zUiacrZ~nXNepx|E@!A(Uie{qZBQP=0J4IzIVC3dL=xiZhxtwQ8kJdWtP@cpW#>y}tQp`h)5sZ?dv=7IJYj2g456duGe#GcIBB93f9IJ3+`Lq6-gQ+zMRTJ+bL~YH4iG +zD5V$%XkD8~Qmub{$Fpk@ki$-?Q)tz(B&uf#Bd@1uf1K8+Q?3xWCl%vgoV4y2Qx{5W +zRXld${S}%-Y|6VILqmsm>5N?HaGa0y_!0W>Ht({9pwn!n%cm+x^bCsmlRJ&|e5O#Q +zqQxf2y_(5Pp^yswdKB|TBN^_th;KvH8jRSmzWS)-jveO`411KV+A8b-pFEVbSQ&_cS<}O3WH&_ +z^Qje`A%GV3izY*cyzde|)}tKnp^Xwi72sG1K}6qdZ11e#+WGuDy)pU-mJ(h;0C2c# +zNIs`QQwLd50`UEKWCc9qskY;*>q*?fHGYYAn@~RBh@A +zI54^xr^V#Q8uh#sc3lyxZ$B%EMW*nnm9|hTXt4D!=@-N<)c|kjU1NskGQAMGJ*&5e +zkLin)k@a54Sl#`@C&;j +zdXHH1b?6c5OMSjEl5;b5-;(=K2qsRc!lyn0T)Vx2OdM0i5kk{Dhv1jXZA2kB*$#6ZOinh +z9}-bLvn@eb-<8(ty@jBKjw8Qn0GcLt1X90h%zoq*t@dt>zcv{mkCc=cC2n6TO8)*# +zTE`qJ@1-gJG5TlrvomSzw5)y>{7l-wKJwV`xoS(p_ON-p?A1R%2J8_rlcr%4S;P2u +zvu&XsyE@jN(SPcq4`E_EY2igvI1$wcbdFu0>v$;8db$=faZ_5g1Z4G^#m~GlTDX$D +z(nI5xhiRdNj3egRvO%LD?vDxXI-_4MxAR!&1J)M7>ofcQ#Af{*US4cc9P2jzBZh^7poJ*JC&`5A*~bcFY}gr%=~Ejfl4Z*0*?fK{RW! +z5RtZu3R^J4C3cCdr~LGjrx#_n<275ws~GsPXGx=+Zk{*0W1Rsxg@y$myzk;M<6M)J +zb#sY&b9sp|Lv4Zm45jFEyRY_#;sP6}m{}+;_zSc$+yR4*hL{81kX-8>Rv}Q9_DsfR +z)NE+X+FCAGwx&&|Di)M*S1=sy>uv?RK})Qyx^Fp9pjK|b36pBJ_r-4qHo(TD158yl +zvbqW`UXgT2iPlM_d;AlpX~iv}{_x+~6tN2)h$&VYcj@TbtKhx4CoeDUF;#jx@rgXD +zh29>IjSQ$+wv0RFD|QOYZ@sacr$4hY&N%a`B`XaTvl+>V_^Ih46}tDCdl?6TF23WY +z@q+pIxqu^f8_9U3i@PbzW?+cADf1^Z&Zv~tB(JkWZW1)?je>EHK0~joIne&J9^STuu)@2d~?r4p2 +ze$7C{LXgTW{3JQH9=x)J7&=4=4Ee)ZQUKyolS&t3P$+HnHEL)Y8nB_ifIDyHA2=T$ +z-g8)>-&R7k3jqXU>Zn)aMqEVRyYG9k3S;XYF5|^ +z>uF$)yREeqaW75MKcTD?U`3>q7Mk5SjU;3%-&vH)s0ew7oms8B!jyOz36z)nvz)&7 +z{bXAuiTR%KalnAzraAqozq7tE*Or9~$!dFi4IbLsh%WwT@8uL41|;#&DrSVbMiijh +zl%MJ?_NXY!dr>Mx`NV!TS*a7x(qx2$5~Q%*Sy#uRa(sRtVeIL~n31@E@08GNd}8fN +zaFp-7p*osg#Xz-{BkN-<2Tba$8*%M6pvIz}#cPUHil80nF5F(tH_zl1P3nb;&U{mW235)sQie>0W +za_>~Zaob;v4yxY9hA6-8KX!vWYse>29-&pzwcY$9u73;I3Q+j3sPoLkKbtZ7?4;rhnTLIks +zbJbOkH!T$;I28?uD7%T+%of21DF;9)Y+u^%x9gSXG~m6_7l^_G#^F&@3nn*RN-8xO +z?C+YaxIa44l{pj%Q<5=Y(7rTkNgZcPqS +zwlmm1n(XPEQ#iB0>L&h|6F|tZh_`Z-G!X5=zfP!7vXE;8-ehCITF5Pu`Ag5B4nCKq +zK00J^_rmIC@1=s{L)^-4h@=C}+$FM*LPerdi-%%fKH`#w@x&z76uppaI_1NiC*Tvs +zrs6S@uMgMG9Guo~E0l(wWp|%^V_G9WM7BX`y|C*Qs}6nTNWBVqM|$$Cb?QO^70Kvq +z{q>D1Ub51?EOKg}j3|%>-->%WK|Q^bX&Bd(X`5x7N~4vkBMe;-im9cYY%ilc$0vTn +z7@-kmm969Ny-^h*uikzY6%a+<+^}7u#Bs0C?1hqeb-!8OLR)_0axld!uZHa%5bnp5 +zrM-2yg$qgt-#wCiN=|AUX=pWTk8}!>15svQUv!#WOm{Y +zdMbLRv#N86rH#)pY^=9ImB~V~b3YSQp>u6Bzc2!-r5w`Dtr&`X_39**Z-bLP5ZnCAmP7 +zoRw+xhlsG{?1n}u$KG#>3UR&Q^1&&nQG5VpXo*I~6m>^6POIoMU6EQp-ENm{emvVo +zO0T<7))U@ryOTIxV^b{u1S1Rj<|-O{)Uc%=jvE@Kdo@t}mpL5NOa;x8dIe2ANlEC_ +z*osl`{c~+^H?OlLjF4C`H1M5pzE6~RbWHu0H`w?4#0+@}%&7;IZ?N9PX2C|Ec&57d +zbmHHGVmAGKA*vMI8y|@#V^EK3WLc|ifoJ4Ej{YjK$(Zx5kA9KU^gNUs-AYI(BM4pl +zxz7=SiNq)f-!&evgzOppT@E`J*V{_tTX>Wu8R#XIB$x8=cXsp~3;;%#Qyzdcywoj+ +z^G?F#;Y@R%nj&ES$MJ5zBIbGV>h34-=-Mkjs~-b<{A8#tM{i(^fEbN$jHotMPWhM! +zUxf{+aFypMIzQp2QjyoeL9J&9RpacCbbG)Rx}CnX)nm4aue*jsdeExIgS&wul9iCR +z&&EWYT%m@|6SMIv)3M(<(-Dn|y-9q$A@YT>#A}v5u}9{uszYzt=ls%o6SKK{KXUIh +zDsjA17?LV_l*=i%tSNU47p(1{2SxLCHvG$m8dCMgdHk;Y=!V`cQ>9QrVbN#6z0&-j +zn6+anr> +zVp^7DXL3i#brE&V{T*K_qALHdGjr|c^woh};K9Gn9QgN@c4N3$#4Z0lP-Hy*UwfE& +zIqliAXXKp?(nymZ=76u$Ylb=HsMvVTu**0q+6dpA6E(x( +zqM=K>LzeJI2^ftsn{W|A3d29(hE6eYJ6xJm5u=eiBs7@%}F_T7U*R8QB+ng3V +zlxg0W#yn8w^6?D~-&#N0900Wq%a%97Kb-H(Z~8}nvI)1*vyBfaXx-nN)TfQ}ypn)q +zmiwVZGCpC(Eg(xKCGOkDJ^Fj^T7pwej*x!8I~kl_SIs!f<_wSw!}q7L4`s2m*Ks=l +zMn>uj@aPP(l6c54F3O-rGda#gz6D_V|KKq8lS{(I}BvbVM?5-HtxN +zIp&aJ1@G*wDLp#z)55Sgmy)Q5uWs{7cyo1zh)xma_(#^9pki9$8=1a7QF0J&QFVwB +zoGw=<@WKbcz45>S<^VHOb8}OGVgv7PcrR;OVtIC9NQa=Jm4A{_j=inHDHpsP+uM*8 +zr^ArlFCqVM^eQQnT#=#Xt^;+IH;7yJjBLo3)tfAh8r7>Ltqj2`-e|o@Hy4X4`A$m= +zi{ii@XJNS5xm2#JDbFy2y)bsuCx2)=1_Gu7d(bM=&075f*lvL8bWLOCq{f|7-Y$Uf +z9sQyiEYFolc+5NyUNUoKn?}&p-^ZacbRyHBXEjoEtbNzS%c_^(sL8Av?)-eyd)jki +zvnd;x4>Fz^BV+}Yr}c*bc!BIc)Vu%O3g75eXS_G1QkI4c4%|jBK36$G0ndU|={`Xy +z^fqgR>Ol4E*}MI84w{ROD@Mi97y6bVT~$F5yy*2MmTU;A>^AWZ5VAcR0L?iL#~qis +z6L|MK7H4qFnwy$!x5}T^nENkfM~%;`6(0~#^HvH7rcted5NICRoH(Hu$ +z)%0*HY9ZxWTU5)GEg9EDn*+XW=oa@eBaDXtkiAJMUd3e0neE0#1S*@sUO0SyD!bNb +z_Pzg*FQF-?t=CUEy$&{F5sNT^yN3F9;z636I!_9Ut3Pn0Oc50OC296@dL2-6;=K9( +z@8W*{?3E1vQ#sBOk$*hvE=(^G(wm+SN4R-yPXr(jSmitR-Pvv*(Zxra-+% +z0$hv^|1`Am=`Y5zh)S*-#Qt#IEcXlTZeBfIPm4ZoqFil?UXi)H4G~&ehJKK5nu_rX +zsqZOU^Vp|TK8G`%bg~-U1TR&<9Zpq6{MAxc3E>P(x+=0lBf>B(_Qvj=C-8&+V9vCG +zcfa{BfFaa=ta03HEFqi2bp+&>(RP1I17Ch;E!nxm|4rJvO5qFy%=TigXT!oe7CU_VOo0n^;COeg>=iq +zIg5uL)v?(yjt#v%5>6-W8Gr-~vow!~`Y8}&5mA}@5uIyqCy1Ub!xID4ym$pF?U4fh +z&2BG$3boSOP7kG(DYSb$pEFw}YV=GkK0QvdOoNTHT#z>+?^WOX@-#BeD`xHa$y@QKCW^S5kcvhd +z6kOmgM97)syU8575N+7vEi0We-)3CJQZzr5WJwr#y=nENM^<^FjS~AN&yfuJNT@^k +z-zB9-m^RWkQ9UySfeA|03-rI+qO`)%ZK}vh;N-XQbBM34;_^0eKOa&s**2XUm>}AB7Qq9cm7A>l5sqQ}Ahmc4CLP?`B2G&uE +z*DMOxwZ$qXEP%O*e@`<;&nbW&BVxKk%fbX(`2D|VpdAqshj=kAWu2c5HquTWh&SSz +zig=eXgNnZktO&hx@y|<3imquA|)nGW4;( +zl3@m|emnLPguy73A9ye^);Hff85v5cWtB}(0u}(6LM22H-yr48g0s(ste_-!*}YyW +z;7e6O9q_Rj28o=6YTx^OZu{;0M5+&fzhW*;hzqyf+^lNTY=?5%+fzHjgF~#2L-3|V +zR7GQ%)7Gk@75{HmqFyk>)fA@_f^kK5W#^{m^@kOzpFXEC{aev#*YH=0ML||`-JkM3 +zZR!p+N*OyJqf$n{w12nA7m$~<;*K>n03+4Z?jyTu8b_0}PXz*4Zp=Srw+L9bu(?>7HHiDbQuh)islD(U#q4KOn(2EAq<|IG +zR63I~UGF^&&gO75R9TO`_^w-z!FuY7blV^>`~;P&`O>xjpv5K~@n7~5yDp)4EUcf9ZbBee;JN@WIDB-OYBUl!E=&s8|DWp{6U}|Ddk9WsI +zzQNeglcPTB;mx5vX^1_R#K$s^vd#jLqPk}I`eN&J$NXj&bmN|9OMu|9`?~J2P=?|6 +z5nAO;Mkr;w%MsZY+txxWq|;EJrSJ%PVFFQl0#@SMYrpG{Ya#8^skFbFW#Fb +z<#l(myZLNdo9&O|GTnLm?#YYkWXDSmi)f+XMmmz{hDlP;D0^iAtll_(#NtBrzy=9Z +z5TpO=>!s$cfkc-{nb(ZpKlYozHHFu?sECJYBD%TdD*BmOJ%Y}jRCGEj_)0VUd;K}t +zyg%>FHd|14gL3>)Rw~F}@MIf8IwRuU5kQ+#*O`qYy}>3FsYh8EApd01kl$I+&;iIV +zgNm-b?l&kB@Hcyph;Ywz!kW+R@FFg +zad@*M+!J9-P4x724l_Tc#LVZ`RImw?fFdmJVUa=TOzAAH?>WvB_0wX3_t$rBVl +zCgqSqOGid4SC`-4E}~<*%d1F>F^ri1as7SE^dia2$~iCp3@V2W#A%z@j2ePRhI)09 +z67`NUmoA%@Rm>rJ9sLuGHbHr9B+(rp*%`>aB4g`!rK6)w^)X9Hri+EQ(%tft4_+3Q +zgULnH{*k{>21<Q{^(l+cqC@+7hNhyja^fA2B$?>&?p60~%Q +z8e6?nQlDN(Veu>#hMcwSf@Pd_m!w0A9Cf+31*ViilvBM;jq>!>o9@f?5y?b1Gm$#2 +zP}tM;?XN#zN{M=V7^atp;>7d2^I_O%+Z~a33GC9lmuFv5w;ZWitGQX(U6t^w?F}m| +zM^<_u&H?g2oH!WAi~O7~bs4RAp$cI9oEfP=87M|?D^2<&vbEWE3-%DF(Pz@$AMPi+ +z_~Ka~;x(;ymFok!-D{u!It-X*8axs74BwJBipX4!mS!M@9ArUKxPu{>VX9TVOozlO +zSLH}K45U{X`%(}32f4#Zi*afHeF6Uy4z~U;P8fOUo3OcL@n5?8K*f>f2M?tF6Z}bM +z{?~q+_>k|td$0A9dGS#{zH0bd-k_&0NQ?1rTY2;||F}CNGOh4~6wv0Pfc<6;Ou@kA~*0+>TmlQ^jhxONnKI&@d6m&VMvUBQumAuZyDT7DDzx06vOsiAyd +z&#mk?l8q9^Oa5{gC|;pj7hl_0)%eMb{NLtv*jV!;h`fJ_1ZDf3ni^(sWsi1|1L2b3 +z_`x|g$m?dPn2V*7wkon7H(HR+-`@1?aI^ZVflm6VCBA#MHxW~T28GKuMNvloqnIIc +zuR{YpKt_w-K0O)Hqm*K0^&;=jcH6humAC|#Jj(ivEMOjPwGDtnwu_NO{7zqA{JDOK +zjZUrDw^=bs){EGrOC&!Tb>ntDcQdA4&tj`Oi5lYLrzg=!;a&obw3o2E?aP$<8Bg%ge(dZ97-{=vv4_ijrCuioA +zs;m0L=?gs>(oc0S3G^l2CwUK#NjrGVz8?EHLZuyy$}1b1Kr{O457wO49$&2l4fQ=jw|EBCNIFFFE5*0imn^1dL0cnzm$}EO?X>zC +z22ycH#}d04TQ(*t`}YTG0$Tkov5>ChA4$nUBFxYPY_)C4AH!GB-U>6MP8X3Gv@2={ +z!Izev@NYTxTmn?)<&00Z*ylLB4GBwBXi9I&Rrhc=W_0#z0fcNV+geR{4G#bJHZB)v +zT#cZ2Zg}s+tJBfAp&I4|lTEH;!k8(wcVjRm3O*5#$f3rJ(r*%@-q9m^qYGDiwXo?F +z=$PxEBnK6*FK{CM;UEak6i;<{Ov{V^lG!> +z_4eOlh5JD97oY8OmwD9pNJY{X@UhQOd{jY?as4$gRiNKi6Jwbkq&nc_wWSYZ;$#dj +zT5M@nmX_XutsVPL{3EoBbSgb$xIW*5-~*LxH5V28+P3Q-&A#KTp0;1tiw-60^byp( +znp-8Z6f}A^2UX0edH6@Nh`hAc^rmc4N%eg13`4AI55Bjdk`%L2yh?etb`oJ6ux<8p1dF!wzQdjTqauq@)9@ +z1p>C_B)xTCP5BL1QYZa&)hMEwV8JEJ5ZPsa<|8^B3 +zy$JiyjqJAtkLMd}GYuZ=tqy%>B;8pVC_+0=Nw!YD2bZTzmh8_f_vChVc-jW@M+4>m +ziB+!lwn>(ofN4?AuVN8uQl1&824ccHzOB)R;8g2rj}|CSLgU&B^ice)LeGfY=_j4?oHvleVv +zwEF=*J>H;3+f&KimUOqg$~y|Rn@rv6KGgQC_p0GQOl|&m=8r^K4Y>!J8d)yywe}Q;!0^d{W6Yma0t4;S6*cn+GCm+MSsXzQtiT%yu +z^&U6W-fw$b-2ZYaraZY}`wKrNC^2-146a(bT+mV6J4JuBBREu4n2;**@P2ZS>5g`Z +zUt@Y5tY<05${p}M>->bP0+B!E`g#lsNbi>~k7k!S+Ma`CI+QqDMkaXzIdR>i=}uGH +z;RGs$>mEFo1h*0)7z8DPu&=m6Y1`5zu(FC9j^(rojZWyX#C8sjb74>ycJ90ux%Xt2wHi@<#eLQ- +zs8~gN92NdHcW7t{!rE*Jn>xB1^7iqX)sH<;D&H7UOXehnPfSd>qF#o<;qr$w`<|9x +zBad&jeY)qdSun&jy+XIWL?zTs4@udj{JY(M2HOT&J@R2tl`B*=m{fufWqVLj(xLTK +zXIluAXExRQ;Y@{&&BoJ9{Wjp?@BU!!H@_UbmTlx;1$0u0p-x6bsk#FJ*Q!+AG3<%J +zT>ygYY`kZnCT){3j1N=9Kaeh=?v01ws= +zzPH=w`~Cgny3L-?>v>&|$K!c_-0#oFbv*f&o>AC3FcKgqsTE%s|H#^oSFuoDVfBzj0%F|1rd!?KJ2E(7@f{rd4uMgxqNiK-jJ;AGTnFduS3%N9jtb35>*4_`r4Zl{ +zH(CL1sdQuDf>hgB$=5Bsn>05WRAoX!f^U3fXh)qQYM0`Dj`);aQdO$80DF7Xgdusi +zi51*4yx+Z5UX#srq-{O4B +zbyTYcEb{h$X`|X6Z&yL`Ht)#z4Fyq=yPOL`^8X991S(g$r5gd}TAIbCJ{*jIYLb@BdIaOublRLcKxBUCdo8B_sR05(G +zWu1314l#alk9^`=BCuIMZ1VFP*u;~Mi4wX(_7EtpVE8Vm(1Ze_<%gr0M#3@v{zEVe +zt*lI+<_Jz^Yz_YQraVe(L@6LC*u@*&Z>U=4izpNm5y|n`tN?YV@AQr_Nh;g_IaFyT +z@UqnbX^#l|w?c~MJNTcSO-`CdsUBZ3Ai6AvbsWAeMdKNcEhV21vh-ViBTSv%!qgdi +z|D~@wVeOYa%(I64jaHvPCa2@x#nXA{R@k@7xWc$`7|n@Z-47(qxnDWr1U9YK-GsG=bn81A11LZF*ED3K4JZAEn{JlQ&u6Ex$Wki+u&jeuM65t +zOUK$&T0AfNwTv|27)~)p2=H^eE%y};RWkZaVq($P_cY7-}JQMi$@R4lB`dd7Zyi< +z$oxoy?Vsu@Z&ia}A>RzK8SSi{m^`{WB^7n5>-0P|)5Wv#cOM#%d|46Y!86^ILQPNq +znZ1;y+496YOXFpEhoU0W%eIEWvO8-)NZHE{-!oZTE3*EE-On@~I7Rn~;J2I|*&EI?-f!gMCf~hVu +z8^d3kc8cCe-F^V8H_6u11@tmKz4%hbIoq8X)16JvK2L{*o4g9UlH}^&>{)^P%qyLz +zizNp@RrtyduNXDCV=zXcWZ& +z`|qIKy?b}h^T(kVsb;rYJ^()T+Q*!UQrwsJakVk_T~@#RAU>rewQA$@RksOW=C)JemVJ%`sEn^pHbf1YhS-4|0N +zA~F~+2*+Ol=^}0cHH{)>?B2qd(|>l<*4`#6fP;RC%js!oLtk7p0J=2YwX+2|DzgsD6)#Wh`KCX40`_R0`1~K` +zOYc0t%{xZa7&IVOAQZkUrq^`GFG>bVSY-?IX69D3WItHC%irc2nNsCYkvmUmu#L!j +z8}vPC@?FnI?vl+7Oz7n2XVbsm5)}|fB813yPE10<9rvaG|EXe4I;Zo)`wq*WEppi< +z!k_s#!{@Qjiob7K)`?LpA>8Q6QvFBjRhzpMeLcuqop*yj53TpjFquLV8I&~WHdK$$ +z^G$A}kuF!6qzlS7_6SBoUIC&!!=t}F``rPzy+5>%vuZioodj!?qo#fJ_!S-W&N-@x +zLC;!Mv^^~!*s{upP*RzYN39@Fr2mV`>}+Z1Py4yqiGmOBObX_45DH37)2S2KXLD|U +zD6|iVB_1gyYV8wgs-oJ*mA8)Hz5>5f!0Vhj9>}!x5mJCER+N2YOTm8e&Y +zv+~c)F5={z>DZ49Vn$!`oe72#uJ4;j7`^r~!t`h=v~arfB>8AonuMbFi_utnm5-g; +zy_7B#tp7PL4)e3u>)7P-M_wzL;p=j2Gm^KtGTE6{;P-mCL#rn{qd=O*T +zDl+i(+V2qENgbz^dhK;F6)e75Es8c3uJmJP9wU#aZhbm)c5gLyf)YxA`&03Q%!zF3 +zNB2VY#MxiM$$HC3I*`nv%lF>9dSl`r;aEl@5j}{TRg7e$QQCF*_kDwgNvB!2fx*hG +zuKHOOb*}X(#nkecwYsa{2v4Fpg3+{iM&4ZPrvol$+=e^%6y{8uz1BJS$afiTYjuU; +zF`jsBoL=f*YvXb_1_b{-(Wx0NYn19om*lSe#|*8kuB0RlnionscuY3hf5zW9gQ2H9 +zAYWj%Y#FNi{(kk8MX7zVK_CH`!dy08*t6Vv#rF#Epnl12plbGE=;pJ;T>0~nPjCab +z&#Q%$4f!QUokMgdU$_9#-$Oi~umkEY4TdkxNO}~|1zj};4Qu_Kv{R$|9Wv$VF^O5U +zQ(?4MZ&sr3d|~$0SKGJ}R(6Dt3Bz+44bsm5;6EZZO((W-AL4wZ$UJ^SXpD0IMld9A +z`1beW*16N#n2aBjODTg*z*fh!n$WxIl3{#AkYZyI8%X^WeJKg8L?;BFspfJ3x{OMWx8ysY+qipEuI$WXX_9wx~%MxuOo`c^m*Fz#O7kBuE9$nB~)J%(*okGk+OE?)}*3`E_gLXSNvUK +zqa3BCU&Tx%gHHz}Rds*vDrxQeNo^a_!M}a7Xs|{%)L4YpyDyw$5@MnYnt)@&P-$ks +z)NpF1*xktTt;dxV_am;mP;+>$v*&{!wOb^4FKlRAM@<->T1HZ7u|{xLf4Lt$53V`G +zoAuABr|q_MBuvL%RdH3?Ke%?+HLe~4)>*l1Rd0>0KDO|byL@z95>i&ZXMh@=Pwwf) +zr#Mdnk8#h`FU={1y>aFlhshx<-7 +zmH$8p4QT&I)kPaE*Ny5K!;4A<`kE|6pCz=*F>!|0g4Dd&ulc#NNmcVeFVWZh{vrC> +zT0f}+1Oqp9-|?*YLh&okbBZ=NZzRqgsJ6D^t(=0`yIKr}0dnYeb3NgH +zhdq1{lEj+#5u01v;*L$@OO&Czu2&6wPC-^ZTNE!FD&}e-x}W(Ss^?d_88-dm{h;*9 +zTauAis>&AqUBLymYNw+vA#fARIstUr&AR{bH~^WiX@qxpw0N_ZmTAx{SKjyP^cJSA +zKdWJ-pnqmZc*VF=1?eQp11i&bq?G>7Or-KOi+YsJE*Dg<+z4D~H7yD?RS@&L-U8fM +zXIl8uVC4|*Arnj1jEvY;Om;dm-wc;Rc^Qa&k~l;RZ2Y=3D?blgvEV*_wBLcx!o?NtcTkP(Qp`RR`yfjD-t6Yv17JG)j4EzM9HHRl{`Y%CE3ETh2TOc8sG?Y1mEe`PPq +z*5O6K>y|>nWq(R!en?pzA?h>13Mml|s}C0$Z2JQ;_3e&>%?8D;w^3nN3TjQ{^Fcr% +zaaGFofCgLMdJX7 +z2J=jtIc>C4g;XZYB<;LVKBd#f^5jH4k*s4W9IsD#zSp!5DKp0au<=Q=hga;YhF|=k +z*u%SKi)CIb$L!%hyabe3&+#3 +zte(ABt3GhToVm+;bSlWuGe3fX&R>URT3HyofHsWTGD(;&#Fv&m|=-hN#H`j3&s-W6>@ +z4ZX|saWpsxyfwI7KH(@U;m;Wf!8Ultav+A9y@Qs}$1sCb*kS4|x5L25W%hBVj)BQM +z{6ypcXj0;QIA>SL26?n1!=#5B+43r9;qps%d|~@Ex7rF^)E*Jp3-@lauNt_T_(MJ~ +z+G4AfBtvRie_iwY4(QAB2hjaLqx>j;iP*VV-Xy0t4787m4kolmTzr<}?F_5XVA#hD +z`F(DWP%MfT6`8wabsMDN@n@OmpK|LSrh?^GrhIqFeuuF(7C7Kl!nl%%@z_hr!IY7> +zn%nN +zrrI8~Ey`GA^iAgW`R#YrHfvIPcZ$C?K?B2AW(GV5Z)dJf{8vqR^6lfg{49X?279wc +zHI9eX*2J_Rc&pPxRSvilI$8>_Jbr)dh;KPWS|iY}$N3;)bgzi4Y|?CoLs&x~S*^%j +z`=+-rX6%2(AMn;G>#?M$c@3{=Ux*OX*nKXTwC1F54z9i~*7GdKrk0dHc>+I+&)cjw!QoxdCQ6g`>v=Ji#!);;VLItqGu+Gy_P0BwrE6LAUn9AH=ZHS53-=S0Q +zK8!2K1ztvF4NSaMafxFuj5P-p;GY~LtFD$r1XiF|9n7G5!9x_vVNx>5mRq*Ttz)r?AFnt<9V)7BXd%v#i1 +z=E~$nb6~F|eWdxl6Q?$0?q{cWssIH8)ODwaU!w;)!35i@GUvH5z}9y15}z4*4Tuc= +zeZ^-iXBvBDaQ%>PIZPJEhkO;6yyp8K-|R+$Mq +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 +zDbMMU%&yC(H*NS7|K+Q*kdK +zO)MgFgeoWQgtz6h>_Fz{U3ruAX-Hs9a2eaJI<7i~50%=C$>y|wqNj3FHR7z6sjz@wF0Yz=j)+%68AJ) +zxJF&>4VDj1O4v8HIhkAH{l +z+*D8Y?vi_N2wJPpzLy@Uu +zr=bUpR58F?hzuOobs2D7M#X^kuX(<6!FsAb5Zg7S_qb16*b!gYQB%Le8Q}00{hHG7 +zWTkQuk>BF~-y!3FsjV*9i^76iQFh8wyAn)FgpH+rWN+X0e;#EZkNoqbpy}BE{&wb< +z(6(=dINwKDU%t})@G;koPq2}*=)R_($_&oCvp0zFo2S1`PDKcoDY2|M(g)-CH;ttN +zuvqGG-xqIc{v5NS%8RzylK5mZd{-QT0q%A>;8bqiy@^ZL%jH;j%1L)oiv8+r{$Ju_ +zHroevL&m?TxxdqB7K#aCt`U6gP9%3__L~s-5C`w7zPA|1mCfiqNs7fRRHttCV5QH9 +z+(v#MnSn5P0ATnzTh>aJ@(@ett!X+%If0Px8=&=;Qwz@WWuMzrX!n=P;SfrFaQoPF-J9GOlVB +zJq8RZ@U?7pC%|Em8v=g9tkdDEV|iNGY88i3Szp=IiJHurw^f}yU`KHr`uC>bj61uFZKpdq%S-;&rx8_JWmLy5a(+wfS +zVmN4GJgxjn_VgfJ)@jF=pChJz+8C#b@eJ?7A=%`ghOe&}a{8M_X+NMnc15$SvF0eM +zsvBQJJ(qg_wc6M +zD&nimb~J}hbeJ4UmwRP+vvE!WG5KTW=o>D-8lR$K%sq!ayJ~SmFRKb@qkGP8<@GG( +zGa7guMMMKd9ZTTJ;G?g&x?Z&EXkWqksBui5 +z7|Yd`*ASHF4G?0+cu;zGzn4aK)?8DRr;8NhEGQM@^Nfs5EhEeF<_F6r1*^VjVBi6e +zLZfY2k&h?PMj+)urEy#tbEN8C9e+;kymP0^JP9VvJ0Ah0!8qK<&(jK~!T9Kr6I}pu+pWJ}nIjSzcn%sc94aOOP=!L@bb+y%dP8@6~g0W4|fOcR*W^jbAY!-8{H5 +zP3q1}BrxZ(iSuc>sfrZf$k)Jx&pBWt#m`4EyHJ?<#~^tO%Df;A079x;#b~gUP(F1v +zbCs*7dT!`V9-$*pv#vl-X1i3+@|J|h5|b>0L`iF+sAQZ6M$$Y!*HC$djA5NK{%UN9 +z;?bSYm)gwR?x}O7JSsIL1oZ&C;SEr@tZyp>e!+%6Fv~;+F-CSnkpe^Nh^!qX!vmXO!ea5Ojj0{ +z;J67N=JwP*@;vzO^?FOX-Wq2bTOFwM7!VleQGXnz9ZrU$PmQ+%Ttg2?9dsdii +z&5#*Av@M#c@;Iw!i@r2iw@8en`I!%r5_i1qIsAvtbPI2Q$9y(Z7a+;P6eSBx9}S1v +z84-f%F+Fsr&apHEJs&+c6f=NBFI3SS7qaJ|!5>TV9p0BvI9>)TLto=7#2h5DTs?K2 +zBvLeundE(Q%Q|&d>!C*_N=S9l1r4Va#QE5jIo^6xsX+{ZG{ghMg?=vRjqov1ENSQ1bC!+7%L13cjvt* +zhq#eR=qDYw&d~1KMX2{^hR;um=dCekA?+#{IEZkf!0)~yyY`7*lydAX-P%9o&4p+Ftow2Dx5qk#UmCIjz*ttm{9(o +z=iKEbv*q%Q8tuD}pI*;gR|iXtddIA9Dc^y}%_?e-(Ma*apXiS@VfPn<_u7^3~s{p+w*duR2t&z44**nDiF-|<*o6OYa +zej-xwNHqn$b*R+5>47_8oA9iHM9{0p)%h{(z-IKv?8D6xxM7EZTA8L*S6W6<9}h6bcspXdy74}(INNzjUxe%$On99 +z^u*k9p!dWhuvJ$u_0Euu&bvx%L%?H8vFg?loKDGZIx`&+&(!uC`809xX1A>6o@UM^$hbipUxJpPJ=sZ*$cE@6sS>-J7+ +zBNPGX$`~K_g0wsp2Of}<3{I#xG)Q=XenT{j963IH53UB4{ZZ8_k&W$= +z3F|L!%z4e}bq_)4dUBr#@|_dP21+Pkfk(OW18(}u!nTNRTw2UHBprd@zMh{O1JbVv +z^V>w<{~@oFJ=A6Drl|Mk^m7J{`#`BK2jmmWMiM6~3EcpQ9QV;QE>To_i>#)IS}++m2t;Cow%EAN{M}1yZ%k?)cr^(~r4v@_MNmzTHiI&5#e; +z{SCh-guFs)Q#Y?TEFjjEzw1b|*iA)hcjQj_OzHhO=r!jJL)cN}j(@6PHUHghq)rY2 +z$%l=awW^d>|rbRmcdx +zWuA69IBnjD;H}~nR3)kE6TNx5Z+Ak|72pwho;`R9`s2>NKNf;R!zD<@*Lpe)LfdG0Fe|34qi2V@ +zJHVY|tsQJhdVe-+$)Iyofyqy~M5XW5bfll55J%!*#$T_tB<;1v&U|P3Ce|PThMf9? +zWak=(!DZ;ro^}HF{SCkK0bry%YbXJaR0PekScdf=@|;d-qMZcDLi2N64msFQJNuYXQo(KIGF +zKO9xS8eOXt{E-$Omr~ES#bSng*-EPE +z;8@v;g^$;oKc!WS9+=PwNeqed8}oA*CrtalIEDB@Sw`ev6%&_jtIHYRuY0;R=_YdE +zawKx}Dx@C>h@DlE?YEN6!!P6w!^2E&k +z00+YOIbxjN1PxYiML1azq(qf$Z#atF$O{86MmmKVAsL|`P +zZ{TXyFUCu!poOWBw@s_=P)L#l7yiN!(jB=T^J)~R^B(HdT&lY7ks_n#np-u8#7DbQ +zKNhIJ{Viw=kcaqmOHfQmw|_wZ=#Tzm->Ddna-+fer0gu&)`d +zK~24%@_8LlU^S@j(R2=s4e&WgPGVamk7}$w_-Z~jw|k!a?d@%8;L)Q4!<}gWVs;8( +zho9@_l!MuSX3Rr!4MEaxN}y#a7-%Qgs~ea~+Omm^nI*~kOxg6nv2#PIhbkR=*6M2S +zS(C)6U}{A`!KTQNZDlkoqr&SkFLXNn4&hAb36D~SC}3M7rB?s}4Y +zl&gVqVqYc!<{z;$uhkgX)D~&^0&czaNytQ3#ow~ +zQJ7iN2v2&&Pt%ax(k8yrG$$?04+%e`vgY-wNdO-miNyDzTh|2*-H0{@s5TC1-H_^_ +zO|5X6U*ZnC8V)B#nl&$LyJ9AWq?@E^bi0Q&>9v7YXuhmWtRJ#& +zjMvXQxgZJyvz&U`169*$ywLnN5?aH-0|p%YSKKlHBoh;|#DR^WQ#fgW6xM}$4y(sy`Ij+V$|= +zzE8auc?Ti>7}54d%?J;FPpCDwkwFXiM17Jt1Mh{>w%l-cGa&j9z=0%|i?ys!-!7sB +z=!Lj(T_5(guK7o8UhZ#6ElSnXE`!U650nF86d%6tYRIm7Qz(t;RoQo{Gxn7@sILci +zm~y(N2M(XWr@@xghrN+V(5vy|)&8c8UJ?t%;2SbE-`i0>F)lo(l<3SfU0Y4^57MPF +zZvHm#_wX4w2_9$bwy!2~`?HfEJcNxf2gULn;Q++pkhgWC^Y|KkDtqeM`Q;`gHWga2 +za>27^w6jW>1n1!zV-Err8&q03TSlG)6ytW09JWdRR!Y@%cG_k&1iC3Iax}-LSogq& +z{~&q?>izU5XhZ6jV;81g-@(_a-1oQUw`p%HHqjWBzNGFl`16Cv( +ziPGHZdBD|r4|-r+{pL#{5`|YRPOkNe5wa!N-zyxacW$5Fv^i1?=OU|dlA9D*$dU~X +z??&a;9H9aHF&_%z+Ht#LV22ScvFrsC4OUDYrkGJ3b9SRL>6SZYHVI~{A}^z=zZmcq&EyR($ax$5ES`~IFp-e=R> +zfrqK$A|mf`M{@p}`4B(Yzp}pdT;lmS^x-gh%wn#o&zW?PvuydU@LD)B;05SpUb6bz +z0FB;xwm%F8KFK}FRRNiA6A>9XxYa?#Bl?a^ZlP$U*x>rjbB=984G!BJB(Ofq=_4v~ +zO%r=io{WCv=`%b@XQAFG+$x<2egZmSg>{%%1Ioepfs%&VL3_NZ_NXt30mI4K07%aL^dhWETCZ&|sQMU*Qm} +zwpwmPX?ogcy=S|_5(E^z_@f26f;gV>m&jd+iN?0O +zMw5Ws+l3amCA7eZX$*G`AiMd|dGlkaKy!S()=M~e4`Yo#%PS+y|2MbwZ`)Q_bn?II +zCBo)Zcg?qN|J-UL5?lY2PK;gt540Y51TS)Rd8+4IHW+!i^2c(%mU +z|E;B*;gsB`BqH*fvMH!7ZQmJQvs1QEHPqiG;?*DoI|OsO_xFc{-ESq8Z1AUJFtd4g +zc&qc@y3&;Exvu A30u?#~E8_k0&jE{DVa(|d +zT@8DO`XEMC;S?z={$58WTxOW|^nR7tu3f$!+eXffH8by$PrBqC2ooh8>XateVUcdbR0G*Uz*GL`&rr +zC0f6hraEZVWbo4UJ%S$d7^e{zzC!4?AG=2^{&2*etqA= +z{`Kc4feu|DvqJ_a$18q;9xbw%$PR84W4wUqaK7v&3Ahb=d~V6-I^V0EO-Z{qNl3OhfZ~vzD=kD&`|`@{;I|iNZ{JE&Fmlb& +zEf+DGYzq-b#muu8G$JL~Y(3Hx-E1H>vDKo3zCJ!T@!(MB149t*+b^Hj(bOKE=89)d +zr>^PX%Ydjec@5IM-}Kn?!1cwC37Rh~fzIP(0vLF)dk`Mmm>K|guueu)jSjnBq>u@m +zNT(r`^9F7YE3|N$7ElAqN?UYMJr2Zm^XV)X#K2V+#7@=`jd&jLq;e{;X1Cl}-}b?P +zBjACgXcAj_~kV}@F$h)SKCRz8YHJi(;YXv*=^dsSTtm|MM +zUQ8C-oIBkKhJ)}RbK0ea5Re60Ab*t=3M97kI%u7AI5L`29XX}>bhU@53c7V5B$?R5 +zcOcDn64?hjeU#$;p|y_abdU81E4zWHPnXg2YkQR@WxKC6uWz4GYMI2DBVh8>i%u4c +z$)DNH+SX9o-5()(B%N?0r-B=EnoD^P2x-t|c3WC@!?zH-@w7gwayw-bvMLbgtoy5` +zv;AMiRdbr#KR$RYwm(5FLEf6}o8}s(kz!ZwPxy|@g6u&_t9#~qw8TKib>vN16{vky +zp_d{gh>QXOP!Yo}tYyFZLXGuy#drGihjSFyc#lE`J5Sp%>-Q!YO4wuAjq9G*QntZ! +z8#I~7C+y#W|lsG0=prP_=l +z-RG2pJuW?FaZ}7=jtFTq#Q9;3C=~?|wU=E^A05_CVU5mu8ARS++a|T{T^+0pa~ +z|4Ls@zfSkp$Er_H$tDf=D#6uqUFIunt49^kzWj^R(iOJJ23h_M?8`c=oJoV_y*gRU +zd9+!c8-=LL3ut|d7H>VwO=)$tSl)}Px2lPK6&DV8;vq`;{KrNJC$BGC%x`FM+f1wP +z+bt+bK!Bk8RavqzZz|^LJJDF_dHX}jN-g6j!McfHNJFk?FOkxV`1@1QB940*T#pTS +zOQwtluau4EP!8YoWY$9x`l%ZNHj}~l-gs7BKWAFNMlM%O5kRo95})$FR`#kf +zK}U)#lZ;I3o7kxy1Mq9HxFr~u9IP7`D!x1h1lYe?d&Ltgqkc0N%QDKaq$AW?rNvva +z)z1x-0FiXQviM@CDLCt06lLXqobyQ)u`C(vqx!Qmw-I!&ZukPqOAdNVSfjB{8`N +zr7sc&>Vx3{d1}S*U%b7q>w_zUfxupR#OT9m9#E{8>Cbt16xo(qPazXg3h8+F8X~*( +zYe3Q5*UoCkL~4e+QzCYP_jQy34-ZtG5RxB{qzSDE=GaArlgIKWz0mJ7OdtLARIFL0 +zzsk(6yF^*Nni1YFi&jdUkn8aF@R-oYPZd{?6`r;@Qe2uzf?O8`{zZt-@3lw6OEJP22e>d8u-vqSH;0t=s7j*4+j%bN$lHw}ihWQ> +zGn!b>JqyUI)zJ${mko5O>xzF}#3U_Ma}Usk48&0>!RA#-dKol~o8;snMXJ-#He0(q +zulv3hQ8~&Oe27!eRnXOFeUC?!G`*k+4h6FN;URI0KF0=#+}w}=AKh~2B0tJ5)PS@T +z&&0_BxbO=SJRh|{3^>e44|s7D=?OV6A-V6;vcuUpEtJ78#3-Iz7w}jUvUNUqi{v_fjmPv3jXNK=cV=yKD@@|?Nj@(#n +zXjOgiTP`g8Jxk{kk}GiJAJ{fhcWAQ1XoM;q5!ZLGpTQ0eBCP3nyKWW1rq2p4? +z+(bYT+*P|{$>*ACx)beEeFy;}O>_Dl$3h@2f#vd{p0^sQoufaNVUAU)=EJU3V)opc +z6aO)5^qZH80VkqlZD-L&TGceiR)TMWw)thKy78+XJi@tuwJXm3y*e)7e1JtE3fnX= +zR~>SCn&!V`z_1leSe3%Kr_fZx^@nokWSwBO+Od*I7bdB&7KEIH48ZFtA$xKc0D!OT +zrBR-d11}LcCxtc`BEZj}WiuEJ)Qswh1GLZ@3cZV;$JWkb5h%A5P8vQnyVSeogxFDa +z!OgQ`012?Yi+Hxe{W3Z2oavY_<1b8C`Xoo8qvO86w#O&l2QUp$ZTaBy`nhu`9`M!a +z9Bz|OdDF>z6Z_fWtu&r~B`+Kasu~w%>V2eDFZc@9o~`LHd1iGK%H%|52|LrUFo``` +z!qx}@*WDm3S$|=&Z4jWLRKtVM0zfSrihv?Kqa+tKg;@W^cek$Y(DRI||DqDN{+TX6 +z(ODZ{p-|SoOo)pqAclbYTy;7Ixygja)`uO2o(A| +zpp>un)R1ahI``qrAe`?WM<|(;^77T9wg>af;oC@`E#Ljw?Njne>zzbwpsPB_buOb9^78ZeNz!aZMkj<+pv@unMWc-(& +z_!4q~kxAm2d?zw_#S3hXV1XSPmnIv_NLpQ6JBoYJ3CKOzy1RN-Q$55d=>7Zz$}hz8 +z_DstizcWIPWdO%d@v%@QSL@tC|9M*f(h?%Xx_ZHaG$K^k45nvPy;ZOeG&LGLEy(XC +zo?QypZqUvma}Lsq9;YQQbE>o=Fph|$BmsvY;1J^q<5;0?pF0P54WqcUu{#mx0alte +zx+1MPX#d3Gn6me`U^#Y6PbcqPH9xA@dZFEL<8y`Jovrvi^jX0U`>jpxArA(xfyBz9 +zM-MB4QdaCzTVmE-a6|Fc!-= +zRfTn{KP5dzMqfB7;>AQCX;wdn3^w}FHn#&6Tf@CvDfi13_N&5wk2`N!(O9u%LV_*1E3ZBp+qwg`35QAaym!(`f^b1S(?)tq)fsLeb!7e=z@NYx2m +z7fSxGUe5fjiE9tzqe8{h0$MCpk*M^RfXY@>ppdADmKX^TLMYV)Yg++Dff68rNl-3D +z7QreCD6%O55*Dj01tAexQUOJXAyvv6*%Cq^Awc$<=zZG1;NG9+hncg?cb@Y-?|aVY +zOjYpvpY9AFb1;0BQW&}T8@}5w{bi9Q8cEg2r{%F*uCi7qGTxjk0uLycIOu7wQC9ro +z_scRKp^sWNQC>umaMu8b^xJq(4MwS2wfn|BNwzt*1)7C+6KV0brB+(l2#x^mSE{6a +z)4Q6FCw&Bj<713xs|) +zuWgGp(+KV2w*X+_uIKd(4Zkt;_ED}E+%zOmb{UU^>5Vq<(a|opPQ_bm(W#_`wjIHXZ-6;dA+}P}!4fR>0 +z1fBom>8aoXc-PF~%SUGYV(fkrYVHjLnd;@CTZOl~{gNwZtU6UxOeRV1KnKcBJhny`uI>f?Bz?eHla9Rkr8Ik>MsfV= +z;3kyp!E$GYIecrpiSB#DKj5PVL&1e`(CzD=r+Vl5?;rnIN;a>b)zfUl&BXI(MoK=D +zrDf%2-R-;UzvMWA)ovEMU8!7%!e{Q%l|J{Ok)6596>;DCRqWV6iyn9`^hjuM5W5_C +zm*Zx&lm5cCdb#>Z)ixCOK)oM0DX9-)&xBPW_Qf>l;7dY<|q}|9(6XArU?X6jRa5cRrXbTDR +ze>IVI1b_K9M)K~toUFLB?$K1`#R#j=C!`{_pda^QWvsfP6W$@DqZwyS>rRTldQQYj +zAJes6gK6MPu$U;(bkxD*L8}KCAm%hlZGuoeyl@VW11;Fa;eD^7Af7~kvGaY%44Y2r +zWV{}zexnPg`Pz{NLyl4Z&}IoOM$MWgn05M^wxZ6QRl>w*!8;D^c)yPbGg+FVq7KFe&~T +zU|{}_U7hWh<%$|864 +zoKM&UX|-#8+16sq^clG*t0P_T*dmsy+}4IH-GqMJx_tevPIxfHY8Rn*m>IY1$I@!c +z?)!FPeqv&ankzh2x6h;6;(oa)OF1n5OhcIUbx^lMHNHNzGuq0{sJP=BU+V_T6Vu2< +z)1t{Wpusbh=RFu?Jw6*h9dai**!ZSvX!vl8#`j6I!(!x&5=J6O-0slC2*u2OInM&c +z7Cc8!NZ@HqG6JhEv(9vVSUJD_;KddkuFi4Uag}+_k2*3RWSViE2PwO;!h*yxNBfz9 +z)SvK#dJ~NBTC7jyx*$V+<|o0fhFo8V+49hR8wRc?SW>?NoLxW}WKXRLZO*B^m2CmU%8UE>==QEF$i4TQW$OxzZU^Hzw>QuhgL@R84h$;Vlsk{)ET0f{ +zgR__Sbx;Dv=>pbo2f`=@96BD+HDU)A3HwCb!_O}$=wM_uC<$>3SpN{BN1r9WrX{jyR2xF +z5BX+~88WFRfm$KL=p=X1 +zT?HJEcJ(>?PZqAej!oH-axK1;E(Ya!xjgBtqKD<0=T(vReOEjxU6rfW#l7E@I?c6m<+JU~4*j!|xvDO>1vRDmc_ZX@h~ +zv*E{}+Np!Ae6@nd!v9G%xgobmXG$q)(wgvWp7}()*K?wehtEGVkU@^xUQ_q;_8uOA +z>Y?R(_WUs(kn9B3)-wL>pE6-?k6-;^%wyZqbhf1Cu0X~j@3E{2tVel3Fx9{{etmD$ +z+QBq^8pJabn@k}vQtLE91@)3|1HjUE5jPzfP{(8&$+B&r|1{hmug+zEqq}fNX*jooupAYx>JpK^#(+P>4`1B%|R6-1iv*V^lpgrb3K@E5q)v8p*FC*qIIc +zv`@?cKrj5n@~OL*0yQA#M_7(z6CMCGsXE=6)Gscyb+N>`V23>@MO%QR2>ljCaWQrD +z(Y$pz{lw(}7GKe0&sx2p>%nJwLHLncQ%;!MasyMVQIZaEF@3A&n-x4K2z^ta49hV)CQG0P2Wksx +z3Z63njA?aB40vde(q~jihA4F6`Ng`5bS6)Ggi9C#rBk11i*$9R{l5sj|NV6;mq5u^ +z>lFTDF;jVQSs@d?Vc=?iBIu;jT!uX}uT^guCaP${P^X$H>&b23h(#ZTT7*=(hH@TrB$1uGLM{SUo@(|eUzdU +x&FK{&GzM(gu79c_#VP$!x>LrZ45!{*t1LbzJ;=&B++Tmo+so%@&5@8R{{ba(5Lo~K + diff --git a/ydb/coordination/tests/__init__.py b/tests/coordination/__init__.py similarity index 100% rename from ydb/coordination/tests/__init__.py rename to tests/coordination/__init__.py diff --git a/tests/coordination/coordination_client.py b/tests/coordination/coordination_client.py index 19c60553..ef51d173 100644 --- a/tests/coordination/coordination_client.py +++ b/tests/coordination/coordination_client.py @@ -1,5 +1,4 @@ import ydb -from ydb._grpc.v4.protos import ydb_coordination_pb2 def test_coordination_nodes(driver_sync: ydb.Driver): @@ -7,27 +6,19 @@ def test_coordination_nodes(driver_sync: ydb.Driver): node_path = "/local/test_node" try: - drop_res = client.delete_node(node_path) - print(f"Node deleted (pre-clean), operation id: {drop_res.operation.id}") + client.delete_node(node_path) except ydb.SchemeError: pass - create_res = client.create_node(node_path) - assert create_res.operation.id is not None - print(f"Node created, operation id: {create_res.operation.id}") + client.create_node(node_path) - describe_res = client.describe_node(node_path) - assert describe_res.operation.id is not None - print(f"Node described, operation id: {describe_res.operation.id}") + node = client.describe_node(node_path) - describe_result_proto = ydb_coordination_pb2.DescribeNodeResult() - describe_res.operation.result.Unpack(describe_result_proto) + assert node.status == ydb.StatusCode.SUCCESS, f"Unexpected operation status: {node.status}" - print(f"Node path: {describe_result_proto.path}") - if describe_result_proto.HasField("config"): - print(f"Node config: {describe_result_proto.config}") + assert node.path.split("/")[-1] == "test_node", "Node name mismatch" - # --- Удаляем узел --- - drop_res = client.delete_node(node_path) - assert drop_res.operation.id is not None - print(f"Node deleted, operation id: {drop_res.operation.id}") + + assert node.config is not None, "Node config is missing" + + client.delete_node(node_path) diff --git a/ydb/_apis.py b/ydb/_apis.py index db5d50f0..c6e3a153 100644 --- a/ydb/_apis.py +++ b/ydb/_apis.py @@ -147,11 +147,4 @@ class CoordinationService(object): CreateNode = "CreateNode" AlterNode = "AlterNode" DropNode = "DropNode" - DescribeNode = "DescribeNode" - - Request = ydb_coordination.CreateNodeRequest - Response = ydb_coordination.CreateNodeResponse - DescribeRequest = ydb_coordination.DescribeNodeRequest - DescribeResponse = ydb_coordination.DescribeNodeResponse - DropRequest = ydb_coordination.DropNodeRequest - DropResponse = ydb_coordination.DropNodeResponse \ No newline at end of file + DescribeNode = "DescribeNode" \ No newline at end of file diff --git a/ydb/coordination/coordination_client.py b/ydb/coordination/coordination_client.py index d43a3719..f0bd03fe 100644 --- a/ydb/coordination/coordination_client.py +++ b/ydb/coordination/coordination_client.py @@ -1,46 +1,41 @@ import typing from typing import Optional -import ydb from ydb import _apis, issues -from .coordination_lock import CoordinationLock -from .сoordination_session import CoordinationSession +from .operations import DescribeNodeOperation, CreateNodeOperation, DropNodeOperation -def wrapper_create_node(rpc_state, response_pb, *_args, **_kwargs): - from .._grpc.grpcwrapper.ydb_coordination import CreateNodeResponse +if typing.TYPE_CHECKING: + import ydb + + +def wrapper_create_node(rpc_state, response_pb, path, *_args, **_kwargs): issues._process_response(response_pb.operation) - return CreateNodeResponse.from_proto(response_pb) + return CreateNodeOperation(rpc_state, response_pb, path) def wrapper_describe_node(rpc_state, response_pb, *_args, **_kwargs): - from .._grpc.grpcwrapper.ydb_coordination import DescribeNodeResponse - issues._process_response(response_pb.operation) - return DescribeNodeResponse.from_proto(response_pb) - + return DescribeNodeOperation(rpc_state, response_pb) -def wrapper_delete_node(rpc_state, response_pb, *_args, **_kwargs): - from .._grpc.grpcwrapper.ydb_coordination import DropNodeResponse +def wrapper_delete_node(rpc_state, response_pb, path, *_args, **_kwargs): issues._process_response(response_pb.operation) - return DropNodeResponse.from_proto(response_pb) + return DropNodeOperation(rpc_state, response_pb, path) class CoordinationClient: def __init__(self, driver: "ydb.Driver"): self._driver = driver - def session(self) -> "CoordinationSession": - return CoordinationSession(self._driver) - def _call_node( self, request, rpc_method, wrapper_fn, + wrap_args=(), settings: Optional["ydb.BaseRequestSettings"] = None, ): return self._driver( @@ -48,7 +43,7 @@ def _call_node( _apis.CoordinationService.Stub, rpc_method, wrap_result=wrapper_fn, - wrap_args=(), + wrap_args=wrap_args, settings=settings, ) @@ -58,7 +53,7 @@ def create_node( config: typing.Optional[typing.Any] = None, operation_params: typing.Optional[typing.Any] = None, settings: Optional["ydb.BaseRequestSettings"] = None, - ) -> _apis.ydb_coordination.CreateNodeResponse: + ) -> CreateNodeOperation: request = _apis.ydb_coordination.CreateNodeRequest( path=path, config=config, @@ -68,7 +63,8 @@ def create_node( request, _apis.CoordinationService.CreateNode, wrapper_create_node, - settings, + wrap_args=(path,), + settings=settings, ) def describe_node( @@ -76,7 +72,7 @@ def describe_node( path: str, operation_params: typing.Optional[typing.Any] = None, settings: Optional["ydb.BaseRequestSettings"] = None, - ) -> _apis.ydb_coordination.DescribeNodeResponse: + ) -> DescribeNodeOperation: request = _apis.ydb_coordination.DescribeNodeRequest( path=path, operation_params=operation_params, @@ -85,7 +81,8 @@ def describe_node( request, _apis.CoordinationService.DescribeNode, wrapper_describe_node, - settings, + wrap_args=(path,), + settings=settings, ) def delete_node( @@ -93,7 +90,7 @@ def delete_node( path: str, operation_params: typing.Optional[typing.Any] = None, settings: Optional["ydb.BaseRequestSettings"] = None, - ) -> _apis.ydb_coordination.DropNodeResponse: + ): request = _apis.ydb_coordination.DropNodeRequest( path=path, operation_params=operation_params, @@ -102,13 +99,7 @@ def delete_node( request, _apis.CoordinationService.DropNode, wrapper_delete_node, - settings, + wrap_args=(path,), + settings=settings, ) - def lock( - self, - path: str, - timeout: int = 5000, - count: int = 1, - ) -> "CoordinationLock": - return CoordinationLock(self.session(), path, timeout, count) diff --git a/ydb/coordination/coordination_lock.py b/ydb/coordination/coordination_lock.py deleted file mode 100644 index 1965bc23..00000000 --- a/ydb/coordination/coordination_lock.py +++ /dev/null @@ -1,22 +0,0 @@ -from ydb.coordination.сoordination_session import CoordinationSession - - -class CoordinationLock: - def __init__(self, session: CoordinationSession, path: str, timeout: int = 5000, count: int = 1): - self._session = session - self._path = path - self._timeout = timeout - self._count = count - - def acquire(self): - self._session.acquire_semaphore(self._path, self._count, self._timeout) - - def release(self): - self._session.release_semaphore(self._path) - - def __enter__(self): - self.acquire() - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.release() diff --git a/ydb/coordination/exceptions.py b/ydb/coordination/exceptions.py deleted file mode 100644 index e2288dc0..00000000 --- a/ydb/coordination/exceptions.py +++ /dev/null @@ -1,19 +0,0 @@ - -class CoordinationError(Exception): - """Базовое исключение для всех ошибок координации.""" - - -class NodeAlreadyExists(CoordinationError): - """Узел координации уже существует.""" - - -class NodeNotFound(CoordinationError): - """Узел координации не найден.""" - - -class NodeLocked(CoordinationError): - """Узел координации уже захвачен.""" - - -class NodeTimeout(CoordinationError): - """Истекло время ожидания при захвате узла.""" \ No newline at end of file diff --git a/ydb/coordination/operations.py b/ydb/coordination/operations.py new file mode 100644 index 00000000..47cf120e --- /dev/null +++ b/ydb/coordination/operations.py @@ -0,0 +1,50 @@ +from ydb import operation as ydb_op +from ydb import _apis + +class DescribeNodeOperation(ydb_op.Operation): + def __init__(self, rpc_state, response, driver=None): + super().__init__(rpc_state, response, driver) + + self.status = response.operation.status + + result = _apis.ydb_coordination.DescribeNodeResult() + response.operation.result.Unpack(result) + + node_info = result.self + self.path = node_info.name + self.node_owner = node_info.owner + self.effective_permissions = node_info.effective_permissions + self.config = result.config + + if self.config: + self.session_grace_period_millis = self.config.session_grace_period_millis + self.attach_consistency_mode = self.config.attach_consistency_mode + self.read_consistency_mode = self.config.read_consistency_mode + else: + self.session_grace_period_millis = None + self.attach_consistency_mode = None + self.read_consistency_mode = None + + def __repr__(self): + return f"DescribeNodeOperation" + + __str__ = __repr__ + + +class CreateNodeOperation(ydb_op.Operation): + def __init__(self, rpc_state, response, path, driver=None): + super().__init__(rpc_state, response, driver) + self.path = path + self.status = response.operation.status + + def __repr__(self): + return f"CreateNodeOperation" + +class DropNodeOperation(ydb_op.Operation): + def __init__(self, rpc_state, response, path, driver=None): + super().__init__(rpc_state, response, driver) + self.path = path + self.status = response.operation.status + + def __repr__(self): + return f"DropNodeOperation" diff --git a/ydb/coordination/tests/test_coordination_minimal.py b/ydb/coordination/tests/test_coordination_minimal.py deleted file mode 100644 index a7543ae5..00000000 --- a/ydb/coordination/tests/test_coordination_minimal.py +++ /dev/null @@ -1,93 +0,0 @@ -import pytest -import ydb -from ydb.coordination.coordination_client import CoordinationClient -import time - - - -@pytest.mark.integration -def test_coordination_client_local(): - driver_config = ydb.DriverConfig( - endpoint="grpc://localhost:2136", - database="/local", - ) - - with ydb.Driver(driver_config) as driver: - for _ in range(10): - try: - driver.wait(timeout=5) - break - except Exception: - time.sleep(1) - - - scheme = ydb.SchemeClient(driver) - base_path = "/local/coordination" - - try: - scheme.describe_path(base_path) - except ydb.issues.SchemeError: - scheme.make_directory(base_path) - desc = scheme.describe_path(base_path) - assert desc is not None, f"Directory {base_path} was not created" - - - node_path = f"{base_path}/test_node" - - client = CoordinationClient(driver) - - - create_future = client.create_node(path=node_path) - assert create_future is not None - - - res_desc = client.describe_node(path=node_path) - assert res_desc is not None - - - res_delete = client.delete_node(path=node_path) - assert res_delete is not None - -@pytest.mark.integration -def test_coordination_lock_lifecycle(): - driver_config = ydb.DriverConfig( - endpoint="grpc://localhost:2136", - database="/local", - ) - - with ydb.Driver(driver_config) as driver: - for _ in range(10): - try: - driver.wait(timeout=5) - break - except Exception: - time.sleep(1) - - scheme = driver.scheme_client - base_path = "/local/coordination" - try: - scheme.describe_path(base_path) - except ydb.SchemeError: - scheme.make_directory(base_path) - desc = scheme.describe_path(base_path) - assert desc is not None, f"Directory {base_path} was not created" - - # создаём client - client = CoordinationClient(driver) - - lock_path = f"{base_path}/test_lock" - - - with client.lock(lock_path, timeout=2000, count=1) as lock: - assert lock._session_id is not None, "Lock должен иметь session_id после acquire" - - - sem_state = client.describe_node(lock_path) - assert sem_state is not None, "Семафор должен существовать после acquire" - - - assert lock._session_id is None, "Lock должен быть освобождён после выхода из with" - - - sem_state_after = client.describe_node(lock_path) - assert sem_state_after is not None, "Семафор всё ещё существует" \ No newline at end of file diff --git a/ydb/coordination/ydb-protos b/ydb/coordination/ydb-protos deleted file mode 160000 index a0c108c3..00000000 --- a/ydb/coordination/ydb-protos +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a0c108c3525a7fd602705257cc716d9e7086393e diff --git "a/ydb/coordination/\321\201oordination_session.py" "b/ydb/coordination/\321\201oordination_session.py" deleted file mode 100644 index b2d95df3..00000000 --- "a/ydb/coordination/\321\201oordination_session.py" +++ /dev/null @@ -1,59 +0,0 @@ -import time - -import ydb -from ydb._grpc.v5.protos.ydb_coordination_pb2 import SessionRequest -from ydb._grpc.v5.ydb_coordination_v1_pb2_grpc import CoordinationServiceStub -from ydb._utilities import SyncResponseIterator - - -class CoordinationSession: - def __init__(self, driver: "ydb.Driver"): - self._driver = driver - self._session_id = None - - def _ensure_session(self): - if self._session_id is None: - req = SessionRequest(session_start=SessionRequest.SessionStart()) - stream_it = self._driver( - req, - CoordinationServiceStub, - "Session", - ) - iterator = SyncResponseIterator(stream_it, lambda resp: resp) - first_resp = next(iterator) - self._session_id = first_resp.session_started.session_id - return self._session_id - - def acquire_semaphore(self, path: str, count: int = 1, timeout_millis: int = 5000): - session_id = self._ensure_session() - acquire_req = SessionRequest( - acquire_semaphore=SessionRequest.AcquireSemaphore( - name=path, - count=count, - timeout_millis=timeout_millis, - req_id=int(time.time() * 1000), - data=b"", - ephemeral=True, - ), - session_start=SessionRequest.SessionStart(session_id=session_id) - ) - stream_it = self._driver(acquire_req, CoordinationServiceStub, "Session") - iterator = SyncResponseIterator(stream_it, lambda resp: resp) - resp = next(iterator) - result = getattr(resp, "acquire_semaphore_result", None) - if not result or not result.acquired: - raise RuntimeError(f"Failed to acquire semaphore {path}") - - def release_semaphore(self, path: str): - if self._session_id is None: - return - release_req = SessionRequest( - release_semaphore=SessionRequest.ReleaseSemaphore( - name=path, - req_id=int(time.time() * 1000), - ), - session_stop=SessionRequest.SessionStop() - ) - stream_it = self._driver(release_req, CoordinationServiceStub, "Session") - SyncResponseIterator(stream_it, lambda resp: resp) - self._session_id = None \ No newline at end of file From 82669fc1eb2839a92b4af3b5c378c1754601dcea Mon Sep 17 00:00:00 2001 From: slampy Date: Wed, 29 Oct 2025 14:34:54 +0100 Subject: [PATCH 06/19] delete path from pr --- ...k_on_api_and_examples_from_zookeeper.patch | 942 ------------------ 1 file changed, 942 deletions(-) delete mode 100644 First_look_on_api_and_examples_from_zookeeper.patch diff --git a/First_look_on_api_and_examples_from_zookeeper.patch b/First_look_on_api_and_examples_from_zookeeper.patch deleted file mode 100644 index c29311ce..00000000 --- a/First_look_on_api_and_examples_from_zookeeper.patch +++ /dev/null @@ -1,942 +0,0 @@ -Subject: [PATCH] First look on api and examples from zookeeper ---- -Index: .gitmodules -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== -diff --git a/.gitmodules b/.gitmodules ---- a/.gitmodules (revision 40ac6927bb0d93f3d8e647f0596fe9cef35066e3) -+++ b/.gitmodules (date 1759873809817) -@@ -1,3 +1,6 @@ - [submodule "ydb-api-protos"] - path = ydb-api-protos - url = https://github.com/ydb-platform/ydb-api-protos.git -+[submodule "ydb/coordination/ydb-protos"] -+ path = ydb/coordination/ydb-protos -+ url = https://github.com/ydb-platform/ydb-api-protos.git -Index: ydb/coordination/exceptions.py -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== -diff --git a/ydb/coordination/exceptions.py b/ydb/coordination/exceptions.py -new file mode 100644 ---- /dev/null (date 1760451514839) -+++ b/ydb/coordination/exceptions.py (date 1760451514839) -@@ -0,0 +1,19 @@ -+ -+class CoordinationError(Exception): -+ """Базовое исключение для всех ошибок координации.""" -+ -+ -+class NodeAlreadyExists(CoordinationError): -+ """Узел координации уже существует.""" -+ -+ -+class NodeNotFound(CoordinationError): -+ """Узел координации не найден.""" -+ -+ -+class NodeLocked(CoordinationError): -+ """Узел координации уже захвачен.""" -+ -+ -+class NodeTimeout(CoordinationError): -+ """Истекло время ожидания при захвате узла.""" -\ No newline at end of file -Index: ydb/coordination/client.py -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== -diff --git a/ydb/coordination/client.py b/ydb/coordination/client.py -new file mode 100644 ---- /dev/null (date 1760520160132) -+++ b/ydb/coordination/client.py (date 1760520160132) -@@ -0,0 +1,170 @@ -+from lib2to3.fixes.fix_input import context -+from typing import Optional -+ -+ -+class CoordinationClient: -+ def __init__(self, driver): -+ self.driver = driver -+ # driver должен быть инициализирован и готов к работе -+ -+ # ------------------ Создание узла ------------------ -+ def create(self, path: str, ttl: int = 60): -+ pass -+ -+ def lock(self): -+ # ---Создание сессии и так далее ( а тут в локе инкапсулирована вся логика)------------------ -+ return CoordinationLock() -+ -+ -+ # ------------------ Получение содержимого узла ------------------ -+ def describe(self, path: str): -+ pass -+ -+ # ------------------ Удаление узла ------------------ -+ def delete(self, path: str): -+ pass -+ -+ -+class CoordinationLock: -+ def enter , exit - context manager -+ -+ # ------------------ Захват узла ------------------ -+ def acquire(self, path: str, timeout: Optional[int] = None): -+ pass -+ -+ -+ # ------------------ Освобождение узла ------------------ -+ def release(self, path: str): -+ pass -+ -+#USE CASES: -+ -+""" -+coord_client = CoordinationClient(driver) -+ -+# Синхронный вызов -+coord_client.create("/my_app/lock_node", ttl=120) -+ -+# Асинхронный вызов -+await coord_client.create_async("/my_app/lock_node", ttl=120) -+ -+как опциональный параметр -+settings = NodeSettings( -+ read_consistency_mode=ReadConsistencyMode.STRICT, -+ attach_consistency_mode=AttachConsistencyMode.RELAXED, -+ self_check_period=1, -+ session_grace_period=5 -+) -+ -+coord_client.create("/path/to/mynode", ttl=60, node_settings=settings) -+ -+что то такое в статусе ошибки, чтобы не кидать самому исключения и не вызывать isSuccess -+def check_status(status): -+ if not status.is_success(): -+ raise RuntimeError(f"Operation failed: {status}") -+ -+# Использование -+status = coord_client.create("/path/to/mynode") -+check_status(status) -+ -+аналоги kazoo и zkpython -+from kazoo.client import KazooClient -+ -+zk = KazooClient(hosts='127.0.0.1:2181') -+zk.start() -+ -+# Создание persistent узла -+zk.create("/my_node", b"some_data") -+ -+# Создание ephemeral узла (аналог TTL/сессионного узла) -+zk.create("/my_ephemeral_node", b"data", ephemeral=True) -+ -+Если узел уже существует — выбрасывается NodeExistsError. -+ -+Исключения — основной способ проверки успеха. -+ -+TTL напрямую не задаётся, ephemeral узел живёт столько, сколько сессия клиента. -+ -+from kazoo.recipe.lock import Lock -+ -+lock = Lock(zk, "/my_lock") -+with lock: # контекстный менеджер автоматически acquire/release -+ # критическая секция -+ do_work() -+ -+Асинхронный код через kazoo официально не поддерживается, но есть сторонние async-обёртки. -+ -+Проверка успеха встроена в методы — если acquire не удаётся, можно поймать исключение или заблокироваться до тайм-аута. -+ -+data, stat = zk.get("/my_node") -+print("data:", data) -+print("stat:", stat) -+ -+stat содержит информацию о версии, времени изменения и количестве детей. -+ -+Можно подписаться на watch, чтобы получать уведомления об изменениях. -+ -+zk.delete("/my_node") -+Исключение NoNodeError если узел не существует. -+ -+В стиле zookeper - тогда будет чет такое . -+ -+coord_client.create("/my_app/lock_node", ttl=60) -+ -+with coord_client.lock("/my_app/lock_node", timeout=5): -+ # критическая секция -+ print("Lock acquired, doing work") -+ # lock автоматически отпустится после выхода из блока -+ -+await coord_client.create_async("/my_app/lock_node", ttl=60) -+ -+async with coord_client.lock_async("/my_app/lock_node", timeout=5): -+ # критическая секция -+ print("Async lock acquired, doing work") -+ # lock автоматически отпустится после выхода из блока -+ -+ -+CoordinationClient -+ ├─ create_session / create_session_async -+ └─ возвращает CoordinationSession -+CoordinationSession -+ ├─ create / create_async -+ ├─ delete / delete_async -+ ├─ describe / describe_async -+ ├─ lock / lock_async # контекстный менеджер для acquire/release -+ -+ -+ session.create_semaphore("my-semaphore", 10, "my-data") -+ -+# синхронно -+with session.semaphore("my-semaphore"): -+ # критическая секция -+ do_work() -+ -+# асинхронно -+async with session.semaphore_async("my-semaphore"): -+ await do_work_async() -+ -+def on_semaphore_changed(changed): -+ if changed: -+ print("Semaphore state changed!") -+ else: -+ print("Watch expired, need to resubscribe") -+ -+settings = DescribeSemaphoreSettings( -+ on_changed=on_semaphore_changed, -+ watch_data=True, -+ watch_owners=True, -+ include_owners=True, -+ include_waiters=True -+) -+ -+desc = session.describe_semaphore("my-semaphore", settings=settings) -+ -+print(f"Semaphore {desc.name}: {desc.count}/{desc.limit}") -+for owner in desc.owners: -+ print(f"Owner {owner.session_id} with {owner.count} tokens") -+ -+на синхронном драйвере написать базу -> compose файлик, 4 метода реализовать + написать обертки (toProto, fromProto) -++ в идеале тесты написать (без локов) -> все теститься -+""" -\ No newline at end of file -diff --git a/ydb/coordination/ydb-protos/img.png b/ydb/coordination/ydb-protos/img.png -new file mode 100644 -index 0000000000000000000000000000000000000000..1e0be898b5ebe820a3d0b87fcd92ebe9c7a04c64 -GIT binary patch -literal 40330 -zc%0XTNIX5d(m~nH>V`e;xB7n -zZ%>Uc&L%kMJID`>D`?&O)hJhM5}Q!XJ=FhhUGR{_+9@CFK(x;21;Bf@7byIk<3-NRVo~`$6n(&MoUbyC0q?X#BSOe&~7cvEBE}=OrEf -zo%-tkrxSw1B(Z4WkAVlQRAjg}Z{k@#X8Dg1I3(#cc&H#{z3-RZSu?YqO4Y)jY=@Cb -zHNm~8b<{KRg#u#B;tpE4T1}o6ZVr*bK0JR$wYqRPWB=|I4V4|Ytm9DMi^(sM26)5o -zUNyzQlojdv=QSxms>yD`BnL)K0r6Yk-okf#_aD|O(FqN13nwm=>c<3Y(f&Ri7O2*- -zltZS4rzA#^vUKq>xl!2Ly4nEVxBVqJ)OPF0=g)tQiDK^K#o?R2oHs@;x4TL&uS -zI*fDxJ~W-QWq->W5we%m%~BxcixxFC^ln~1d6wR@kU_t>cWOwjtD%Dqp%dnKWL<#x -z+vBVc7^7kwTEHApPu}>$=QoFS7c|EyO2DQ-7chwnjPj9b7av-NR_bB7V4d)Vxw$px -z_Fxcqb}T#Ryt2cFfbz97CWJ8@0BUAW7NE2)5@@r{0kitgyB3&;%Ba99i}6eF+^Sx! -z>I)#j29zL^D@E^E+vwLtO$3NS_3m-9q6WVno4y(%tOxT9Y;`CoI%UXL_`0O{8C+}R -zc1_eM+y|#s;04CM_b=ai4A-?4VBorA3n`Ns^sQy}6mVX2{Eee8_u~pxi97we#e{|J -zwo2BY# -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 -z3jrQ~B0n{^-PRt8+Q!5s?|$)j|`A -z3Ew_GR;E`jWRw}iWv{iKFo(*AwuQ~(f8bk(0&e#*aX!TEC1v$fWvr%fiNvp8ibIe+ -zv06TS7)RYWHBKaT-zJ_IT1z;npUvvT^_VSFIq?%2-cyE~FQKwt;jYHXsDy5j;ag5K -zZ>T-!%|R9HWU~nlg%@l64G(vl9Rmn=Fe60)_qcWy*;r4{*6+>UtI2eA -z7vd|Czudh`htLzvadn9Km0=``vqmmwbq6r=Y7G6O=glHxcYzx`mbifB?Na&k*V8{G -z${*NyU*Me(6oux_hqsirIaM;JZO*gp48I?=4J;QqAUj7|S-kv+^>dLZ_m=89;Mr5^rV2PWYBK=fYs}`9AeM}m)r|cju0t$;n(hOzp0;W -zr4!mhPmBM;Jx{~ENM9A#of|h4Y(N1t96<0Lpu^p!oi0OD$`ivFl>mNVi}j&i_;yQ8 -z);4!pJ%zoPstP*7y55D5E^F6MHQxCokzL)#%-g?x5-s8vLfIZ4J&^(@%#n54Pbu43 -zR`=J9B^qa-IMYa-xK3@qrq!QQM0)NY(g~b_!Js0!SY)-EO9h?DvmTBq!;jex= -zW~Kc@pQ=bdkhMpyly79(r6T^$Tsl^$7J=J39QVN_YUk(oiIxu@oY}GD*e!f+^Pjw( -z4YEkI^#(rLth0r@?QB@@557i(r3HH6t8>Nyf}g3N#LHEDHwk@mFfjkn;p_AXT6hXq -z86a=!TTFc;A)!Uv98&iwovCi|F*#T5!^Pu@i2@o?UYXF^brEz#!e{5Rv>7GBW|;9l -zqGt7hZrAMlE*jH7x7r -zu2|ptP%bYwQ8QDu=cUA-NvFi~TC(Er4^j$dUV6INZSoodj6wz?@u|C7(n_9hzw`08 -z!keAVbqqJ$s%R-a6Jq-7FiVff6Hj0t%B_ap4jT~Jqm>ekmRgU-uT^Z_YGMGdSV9km -z{JF`&p_4Zfrim`yi^nTh&mt1%#k)iT{e!EIQMF9ml<(#0`+8)qpf5y~ksa6V=Lnmb -zPK{zuj6jo?6f2*e!ylF--ujsTYB~0%MM+M({V%vyA%se5fyZ?b=FQ^DZq)VgHVe?* -z@tzAJaLFQ5!>`8vaAhQ-S&5aOzKbl~n&I1*<-O}GV=+bzkZ!K7u9vK!F(Jmj{QaUZ -ztL)M{J2=t7*zG(xW^*1Dpth=}C9W%KIGy(KsD>3N|HE0QzxeL@{CvL5#BWyItNPHq7IV@ZY^_=R4 -zyP>AIR;^Q?<;#e)&h?_?5fg88pR`WLi!i|*(RDdyewi3sVSPrzEI?)$* -zZdxbk&dFt}hON=x4W_+Pb}5Z9TUTq>juEhDE(a2Vg`(IA*G}QKrx*m=MhDzrPTaK( -z&6t@e>xQQANR@kvfYp#~bU5Vw&fT9PJ-eCHb*O6F9y%CQELdBIj}~<=P(ZAX39P1m -ztQYrq%+z}c@INMILw#}{%CBTf7fAA~f7{GEJTm0*|Y>fZMdGBsgF4-sM-Gtr6+0TQPjAu-->)i{6JT9kR4cOAjK -zZ>y9D2;O>}v28&CuQbCaXZn{h4J6&yFTF5uY`s#__uq3F7dp>eI -z7I*u2r(dCQothV^6}+K8XuopQPW+y$37 -z)u+W8AXYz+x!G&D0>!9V_;zY}&wi*h79>q?1|3+jXa8)r&1fpQOmOmbmzGuNw3Tt1cOiu|SR0X*` -z#1NY84H{?^fac^QcS>dsCtk*EAh>%BvOQEYy+)<7srVKdc0|#xKPsh-J6oIG>T_0H9~yIMGx0@%lTWpQN0rZ`Iv^8I3?!k -zqErY@Ha;5vu)1D#KwQc|oC#=l8(heh_t8nN{3s;aqx?Driza{e6a?E^a#|!Jdh+B? -zPrIH)q30+)>R!!mIG(+iWB9Z&9yzIrptsI#%VFF2Kd}8lN!282Vxl-H$@VF|@E6h* -z5j6|ETxV`Hb*A?5^KqovTH-6V%N6D1t0vH-ty4!t-cpxJj}Vjx4Uffnk&b%ujUpC% -zb0Qfv@#Tg6kIu{3NIDExB1E1izDcmZ4j`|XD9>N0S3M2gTuTVz3a>JtlPh>aM-=6~ -z5TlhM220_YKY6nw+=|p=r)u%OG}-&gh%T)TIZ_3fn@Fy0IC&SGLqYb{^nG)nl_rbb -ziqqMYxM<-`-es<42WxRvH|$LpF>hr`tLhcyl%?lWO{L -zb+?D2b#DS-!6`9eD~D?Z@FAWYDo{ugYD|MG4F>U0j3HUWO40_ -zUiacrZ~nXNepx|E@!A(Uie{qZBQP=0J4IzIVC3dL=xiZhxtwQ8kJdWtP@cpW#>y}tQp`h)5sZ?dv=7IJYj2g456duGe#GcIBB93f9IJ3+`Lq6-gQ+zMRTJ+bL~YH4iG -zD5V$%XkD8~Qmub{$Fpk@ki$-?Q)tz(B&uf#Bd@1uf1K8+Q?3xWCl%vgoV4y2Qx{5W -zRXld${S}%-Y|6VILqmsm>5N?HaGa0y_!0W>Ht({9pwn!n%cm+x^bCsmlRJ&|e5O#Q -zqQxf2y_(5Pp^yswdKB|TBN^_th;KvH8jRSmzWS)-jveO`411KV+A8b-pFEVbSQ&_cS<}O3WH&_ -z^Qje`A%GV3izY*cyzde|)}tKnp^Xwi72sG1K}6qdZ11e#+WGuDy)pU-mJ(h;0C2c# -zNIs`QQwLd50`UEKWCc9qskY;*>q*?fHGYYAn@~RBh@A -zI54^xr^V#Q8uh#sc3lyxZ$B%EMW*nnm9|hTXt4D!=@-N<)c|kjU1NskGQAMGJ*&5e -zkLin)k@a54Sl#`@C&;j -zdXHH1b?6c5OMSjEl5;b5-;(=K2qsRc!lyn0T)Vx2OdM0i5kk{Dhv1jXZA2kB*$#6ZOinh -z9}-bLvn@eb-<8(ty@jBKjw8Qn0GcLt1X90h%zoq*t@dt>zcv{mkCc=cC2n6TO8)*# -zTE`qJ@1-gJG5TlrvomSzw5)y>{7l-wKJwV`xoS(p_ON-p?A1R%2J8_rlcr%4S;P2u -zvu&XsyE@jN(SPcq4`E_EY2igvI1$wcbdFu0>v$;8db$=faZ_5g1Z4G^#m~GlTDX$D -z(nI5xhiRdNj3egRvO%LD?vDxXI-_4MxAR!&1J)M7>ofcQ#Af{*US4cc9P2jzBZh^7poJ*JC&`5A*~bcFY}gr%=~Ejfl4Z*0*?fK{RW! -z5RtZu3R^J4C3cCdr~LGjrx#_n<275ws~GsPXGx=+Zk{*0W1Rsxg@y$myzk;M<6M)J -zb#sY&b9sp|Lv4Zm45jFEyRY_#;sP6}m{}+;_zSc$+yR4*hL{81kX-8>Rv}Q9_DsfR -z)NE+X+FCAGwx&&|Di)M*S1=sy>uv?RK})Qyx^Fp9pjK|b36pBJ_r-4qHo(TD158yl -zvbqW`UXgT2iPlM_d;AlpX~iv}{_x+~6tN2)h$&VYcj@TbtKhx4CoeDUF;#jx@rgXD -zh29>IjSQ$+wv0RFD|QOYZ@sacr$4hY&N%a`B`XaTvl+>V_^Ih46}tDCdl?6TF23WY -z@q+pIxqu^f8_9U3i@PbzW?+cADf1^Z&Zv~tB(JkWZW1)?je>EHK0~joIne&J9^STuu)@2d~?r4p2 -ze$7C{LXgTW{3JQH9=x)J7&=4=4Ee)ZQUKyolS&t3P$+HnHEL)Y8nB_ifIDyHA2=T$ -z-g8)>-&R7k3jqXU>Zn)aMqEVRyYG9k3S;XYF5|^ -z>uF$)yREeqaW75MKcTD?U`3>q7Mk5SjU;3%-&vH)s0ew7oms8B!jyOz36z)nvz)&7 -z{bXAuiTR%KalnAzraAqozq7tE*Or9~$!dFi4IbLsh%WwT@8uL41|;#&DrSVbMiijh -zl%MJ?_NXY!dr>Mx`NV!TS*a7x(qx2$5~Q%*Sy#uRa(sRtVeIL~n31@E@08GNd}8fN -zaFp-7p*osg#Xz-{BkN-<2Tba$8*%M6pvIz}#cPUHil80nF5F(tH_zl1P3nb;&U{mW235)sQie>0W -za_>~Zaob;v4yxY9hA6-8KX!vWYse>29-&pzwcY$9u73;I3Q+j3sPoLkKbtZ7?4;rhnTLIks -zbJbOkH!T$;I28?uD7%T+%of21DF;9)Y+u^%x9gSXG~m6_7l^_G#^F&@3nn*RN-8xO -z?C+YaxIa44l{pj%Q<5=Y(7rTkNgZcPqS -zwlmm1n(XPEQ#iB0>L&h|6F|tZh_`Z-G!X5=zfP!7vXE;8-ehCITF5Pu`Ag5B4nCKq -zK00J^_rmIC@1=s{L)^-4h@=C}+$FM*LPerdi-%%fKH`#w@x&z76uppaI_1NiC*Tvs -zrs6S@uMgMG9Guo~E0l(wWp|%^V_G9WM7BX`y|C*Qs}6nTNWBVqM|$$Cb?QO^70Kvq -z{q>D1Ub51?EOKg}j3|%>-->%WK|Q^bX&Bd(X`5x7N~4vkBMe;-im9cYY%ilc$0vTn -z7@-kmm969Ny-^h*uikzY6%a+<+^}7u#Bs0C?1hqeb-!8OLR)_0axld!uZHa%5bnp5 -zrM-2yg$qgt-#wCiN=|AUX=pWTk8}!>15svQUv!#WOm{Y -zdMbLRv#N86rH#)pY^=9ImB~V~b3YSQp>u6Bzc2!-r5w`Dtr&`X_39**Z-bLP5ZnCAmP7 -zoRw+xhlsG{?1n}u$KG#>3UR&Q^1&&nQG5VpXo*I~6m>^6POIoMU6EQp-ENm{emvVo -zO0T<7))U@ryOTIxV^b{u1S1Rj<|-O{)Uc%=jvE@Kdo@t}mpL5NOa;x8dIe2ANlEC_ -z*osl`{c~+^H?OlLjF4C`H1M5pzE6~RbWHu0H`w?4#0+@}%&7;IZ?N9PX2C|Ec&57d -zbmHHGVmAGKA*vMI8y|@#V^EK3WLc|ifoJ4Ej{YjK$(Zx5kA9KU^gNUs-AYI(BM4pl -zxz7=SiNq)f-!&evgzOppT@E`J*V{_tTX>Wu8R#XIB$x8=cXsp~3;;%#Qyzdcywoj+ -z^G?F#;Y@R%nj&ES$MJ5zBIbGV>h34-=-Mkjs~-b<{A8#tM{i(^fEbN$jHotMPWhM! -zUxf{+aFypMIzQp2QjyoeL9J&9RpacCbbG)Rx}CnX)nm4aue*jsdeExIgS&wul9iCR -z&&EWYT%m@|6SMIv)3M(<(-Dn|y-9q$A@YT>#A}v5u}9{uszYzt=ls%o6SKK{KXUIh -zDsjA17?LV_l*=i%tSNU47p(1{2SxLCHvG$m8dCMgdHk;Y=!V`cQ>9QrVbN#6z0&-j -zn6+anr> -zVp^7DXL3i#brE&V{T*K_qALHdGjr|c^woh};K9Gn9QgN@c4N3$#4Z0lP-Hy*UwfE& -zIqliAXXKp?(nymZ=76u$Ylb=HsMvVTu**0q+6dpA6E(x( -zqM=K>LzeJI2^ftsn{W|A3d29(hE6eYJ6xJm5u=eiBs7@%}F_T7U*R8QB+ng3V -zlxg0W#yn8w^6?D~-&#N0900Wq%a%97Kb-H(Z~8}nvI)1*vyBfaXx-nN)TfQ}ypn)q -zmiwVZGCpC(Eg(xKCGOkDJ^Fj^T7pwej*x!8I~kl_SIs!f<_wSw!}q7L4`s2m*Ks=l -zMn>uj@aPP(l6c54F3O-rGda#gz6D_V|KKq8lS{(I}BvbVM?5-HtxN -zIp&aJ1@G*wDLp#z)55Sgmy)Q5uWs{7cyo1zh)xma_(#^9pki9$8=1a7QF0J&QFVwB -zoGw=<@WKbcz45>S<^VHOb8}OGVgv7PcrR;OVtIC9NQa=Jm4A{_j=inHDHpsP+uM*8 -zr^ArlFCqVM^eQQnT#=#Xt^;+IH;7yJjBLo3)tfAh8r7>Ltqj2`-e|o@Hy4X4`A$m= -zi{ii@XJNS5xm2#JDbFy2y)bsuCx2)=1_Gu7d(bM=&075f*lvL8bWLOCq{f|7-Y$Uf -z9sQyiEYFolc+5NyUNUoKn?}&p-^ZacbRyHBXEjoEtbNzS%c_^(sL8Av?)-eyd)jki -zvnd;x4>Fz^BV+}Yr}c*bc!BIc)Vu%O3g75eXS_G1QkI4c4%|jBK36$G0ndU|={`Xy -z^fqgR>Ol4E*}MI84w{ROD@Mi97y6bVT~$F5yy*2MmTU;A>^AWZ5VAcR0L?iL#~qis -z6L|MK7H4qFnwy$!x5}T^nENkfM~%;`6(0~#^HvH7rcted5NICRoH(Hu$ -z)%0*HY9ZxWTU5)GEg9EDn*+XW=oa@eBaDXtkiAJMUd3e0neE0#1S*@sUO0SyD!bNb -z_Pzg*FQF-?t=CUEy$&{F5sNT^yN3F9;z636I!_9Ut3Pn0Oc50OC296@dL2-6;=K9( -z@8W*{?3E1vQ#sBOk$*hvE=(^G(wm+SN4R-yPXr(jSmitR-Pvv*(Zxra-+% -z0$hv^|1`Am=`Y5zh)S*-#Qt#IEcXlTZeBfIPm4ZoqFil?UXi)H4G~&ehJKK5nu_rX -zsqZOU^Vp|TK8G`%bg~-U1TR&<9Zpq6{MAxc3E>P(x+=0lBf>B(_Qvj=C-8&+V9vCG -zcfa{BfFaa=ta03HEFqi2bp+&>(RP1I17Ch;E!nxm|4rJvO5qFy%=TigXT!oe7CU_VOo0n^;COeg>=iq -zIg5uL)v?(yjt#v%5>6-W8Gr-~vow!~`Y8}&5mA}@5uIyqCy1Ub!xID4ym$pF?U4fh -z&2BG$3boSOP7kG(DYSb$pEFw}YV=GkK0QvdOoNTHT#z>+?^WOX@-#BeD`xHa$y@QKCW^S5kcvhd -z6kOmgM97)syU8575N+7vEi0We-)3CJQZzr5WJwr#y=nENM^<^FjS~AN&yfuJNT@^k -z-zB9-m^RWkQ9UySfeA|03-rI+qO`)%ZK}vh;N-XQbBM34;_^0eKOa&s**2XUm>}AB7Qq9cm7A>l5sqQ}Ahmc4CLP?`B2G&uE -z*DMOxwZ$qXEP%O*e@`<;&nbW&BVxKk%fbX(`2D|VpdAqshj=kAWu2c5HquTWh&SSz -zig=eXgNnZktO&hx@y|<3imquA|)nGW4;( -zl3@m|emnLPguy73A9ye^);Hff85v5cWtB}(0u}(6LM22H-yr48g0s(ste_-!*}YyW -z;7e6O9q_Rj28o=6YTx^OZu{;0M5+&fzhW*;hzqyf+^lNTY=?5%+fzHjgF~#2L-3|V -zR7GQ%)7Gk@75{HmqFyk>)fA@_f^kK5W#^{m^@kOzpFXEC{aev#*YH=0ML||`-JkM3 -zZR!p+N*OyJqf$n{w12nA7m$~<;*K>n03+4Z?jyTu8b_0}PXz*4Zp=Srw+L9bu(?>7HHiDbQuh)islD(U#q4KOn(2EAq<|IG -zR63I~UGF^&&gO75R9TO`_^w-z!FuY7blV^>`~;P&`O>xjpv5K~@n7~5yDp)4EUcf9ZbBee;JN@WIDB-OYBUl!E=&s8|DWp{6U}|Ddk9WsI -zzQNeglcPTB;mx5vX^1_R#K$s^vd#jLqPk}I`eN&J$NXj&bmN|9OMu|9`?~J2P=?|6 -z5nAO;Mkr;w%MsZY+txxWq|;EJrSJ%PVFFQl0#@SMYrpG{Ya#8^skFbFW#Fb -z<#l(myZLNdo9&O|GTnLm?#YYkWXDSmi)f+XMmmz{hDlP;D0^iAtll_(#NtBrzy=9Z -z5TpO=>!s$cfkc-{nb(ZpKlYozHHFu?sECJYBD%TdD*BmOJ%Y}jRCGEj_)0VUd;K}t -zyg%>FHd|14gL3>)Rw~F}@MIf8IwRuU5kQ+#*O`qYy}>3FsYh8EApd01kl$I+&;iIV -zgNm-b?l&kB@Hcyph;Ywz!kW+R@FFg -zad@*M+!J9-P4x724l_Tc#LVZ`RImw?fFdmJVUa=TOzAAH?>WvB_0wX3_t$rBVl -zCgqSqOGid4SC`-4E}~<*%d1F>F^ri1as7SE^dia2$~iCp3@V2W#A%z@j2ePRhI)09 -z67`NUmoA%@Rm>rJ9sLuGHbHr9B+(rp*%`>aB4g`!rK6)w^)X9Hri+EQ(%tft4_+3Q -zgULnH{*k{>21<Q{^(l+cqC@+7hNhyja^fA2B$?>&?p60~%Q -z8e6?nQlDN(Veu>#hMcwSf@Pd_m!w0A9Cf+31*ViilvBM;jq>!>o9@f?5y?b1Gm$#2 -zP}tM;?XN#zN{M=V7^atp;>7d2^I_O%+Z~a33GC9lmuFv5w;ZWitGQX(U6t^w?F}m| -zM^<_u&H?g2oH!WAi~O7~bs4RAp$cI9oEfP=87M|?D^2<&vbEWE3-%DF(Pz@$AMPi+ -z_~Ka~;x(;ymFok!-D{u!It-X*8axs74BwJBipX4!mS!M@9ArUKxPu{>VX9TVOozlO -zSLH}K45U{X`%(}32f4#Zi*afHeF6Uy4z~U;P8fOUo3OcL@n5?8K*f>f2M?tF6Z}bM -z{?~q+_>k|td$0A9dGS#{zH0bd-k_&0NQ?1rTY2;||F}CNGOh4~6wv0Pfc<6;Ou@kA~*0+>TmlQ^jhxONnKI&@d6m&VMvUBQumAuZyDT7DDzx06vOsiAyd -z&#mk?l8q9^Oa5{gC|;pj7hl_0)%eMb{NLtv*jV!;h`fJ_1ZDf3ni^(sWsi1|1L2b3 -z_`x|g$m?dPn2V*7wkon7H(HR+-`@1?aI^ZVflm6VCBA#MHxW~T28GKuMNvloqnIIc -zuR{YpKt_w-K0O)Hqm*K0^&;=jcH6humAC|#Jj(ivEMOjPwGDtnwu_NO{7zqA{JDOK -zjZUrDw^=bs){EGrOC&!Tb>ntDcQdA4&tj`Oi5lYLrzg=!;a&obw3o2E?aP$<8Bg%ge(dZ97-{=vv4_ijrCuioA -zs;m0L=?gs>(oc0S3G^l2CwUK#NjrGVz8?EHLZuyy$}1b1Kr{O457wO49$&2l4fQ=jw|EBCNIFFFE5*0imn^1dL0cnzm$}EO?X>zC -z22ycH#}d04TQ(*t`}YTG0$Tkov5>ChA4$nUBFxYPY_)C4AH!GB-U>6MP8X3Gv@2={ -z!Izev@NYTxTmn?)<&00Z*ylLB4GBwBXi9I&Rrhc=W_0#z0fcNV+geR{4G#bJHZB)v -zT#cZ2Zg}s+tJBfAp&I4|lTEH;!k8(wcVjRm3O*5#$f3rJ(r*%@-q9m^qYGDiwXo?F -z=$PxEBnK6*FK{CM;UEak6i;<{Ov{V^lG!> -z_4eOlh5JD97oY8OmwD9pNJY{X@UhQOd{jY?as4$gRiNKi6Jwbkq&nc_wWSYZ;$#dj -zT5M@nmX_XutsVPL{3EoBbSgb$xIW*5-~*LxH5V28+P3Q-&A#KTp0;1tiw-60^byp( -znp-8Z6f}A^2UX0edH6@Nh`hAc^rmc4N%eg13`4AI55Bjdk`%L2yh?etb`oJ6ux<8p1dF!wzQdjTqauq@)9@ -z1p>C_B)xTCP5BL1QYZa&)hMEwV8JEJ5ZPsa<|8^B3 -zy$JiyjqJAtkLMd}GYuZ=tqy%>B;8pVC_+0=Nw!YD2bZTzmh8_f_vChVc-jW@M+4>m -ziB+!lwn>(ofN4?AuVN8uQl1&824ccHzOB)R;8g2rj}|CSLgU&B^ice)LeGfY=_j4?oHvleVv -zwEF=*J>H;3+f&KimUOqg$~y|Rn@rv6KGgQC_p0GQOl|&m=8r^K4Y>!J8d)yywe}Q;!0^d{W6Yma0t4;S6*cn+GCm+MSsXzQtiT%yu -z^&U6W-fw$b-2ZYaraZY}`wKrNC^2-146a(bT+mV6J4JuBBREu4n2;**@P2ZS>5g`Z -zUt@Y5tY<05${p}M>->bP0+B!E`g#lsNbi>~k7k!S+Ma`CI+QqDMkaXzIdR>i=}uGH -z;RGs$>mEFo1h*0)7z8DPu&=m6Y1`5zu(FC9j^(rojZWyX#C8sjb74>ycJ90ux%Xt2wHi@<#eLQ- -zs8~gN92NdHcW7t{!rE*Jn>xB1^7iqX)sH<;D&H7UOXehnPfSd>qF#o<;qr$w`<|9x -zBad&jeY)qdSun&jy+XIWL?zTs4@udj{JY(M2HOT&J@R2tl`B*=m{fufWqVLj(xLTK -zXIluAXExRQ;Y@{&&BoJ9{Wjp?@BU!!H@_UbmTlx;1$0u0p-x6bsk#FJ*Q!+AG3<%J -zT>ygYY`kZnCT){3j1N=9Kaeh=?v01ws= -zzPH=w`~Cgny3L-?>v>&|$K!c_-0#oFbv*f&o>AC3FcKgqsTE%s|H#^oSFuoDVfBzj0%F|1rd!?KJ2E(7@f{rd4uMgxqNiK-jJ;AGTnFduS3%N9jtb35>*4_`r4Zl{ -zH(CL1sdQuDf>hgB$=5Bsn>05WRAoX!f^U3fXh)qQYM0`Dj`);aQdO$80DF7Xgdusi -zi51*4yx+Z5UX#srq-{O4B -zbyTYcEb{h$X`|X6Z&yL`Ht)#z4Fyq=yPOL`^8X991S(g$r5gd}TAIbCJ{*jIYLb@BdIaOublRLcKxBUCdo8B_sR05(G -zWu1314l#alk9^`=BCuIMZ1VFP*u;~Mi4wX(_7EtpVE8Vm(1Ze_<%gr0M#3@v{zEVe -zt*lI+<_Jz^Yz_YQraVe(L@6LC*u@*&Z>U=4izpNm5y|n`tN?YV@AQr_Nh;g_IaFyT -z@UqnbX^#l|w?c~MJNTcSO-`CdsUBZ3Ai6AvbsWAeMdKNcEhV21vh-ViBTSv%!qgdi -z|D~@wVeOYa%(I64jaHvPCa2@x#nXA{R@k@7xWc$`7|n@Z-47(qxnDWr1U9YK-GsG=bn81A11LZF*ED3K4JZAEn{JlQ&u6Ex$Wki+u&jeuM65t -zOUK$&T0AfNwTv|27)~)p2=H^eE%y};RWkZaVq($P_cY7-}JQMi$@R4lB`dd7Zyi< -z$oxoy?Vsu@Z&ia}A>RzK8SSi{m^`{WB^7n5>-0P|)5Wv#cOM#%d|46Y!86^ILQPNq -znZ1;y+496YOXFpEhoU0W%eIEWvO8-)NZHE{-!oZTE3*EE-On@~I7Rn~;J2I|*&EI?-f!gMCf~hVu -z8^d3kc8cCe-F^V8H_6u11@tmKz4%hbIoq8X)16JvK2L{*o4g9UlH}^&>{)^P%qyLz -zizNp@RrtyduNXDCV=zXcWZ& -z`|qIKy?b}h^T(kVsb;rYJ^()T+Q*!UQrwsJakVk_T~@#RAU>rewQA$@RksOW=C)JemVJ%`sEn^pHbf1YhS-4|0N -zA~F~+2*+Ol=^}0cHH{)>?B2qd(|>l<*4`#6fP;RC%js!oLtk7p0J=2YwX+2|DzgsD6)#Wh`KCX40`_R0`1~K` -zOYc0t%{xZa7&IVOAQZkUrq^`GFG>bVSY-?IX69D3WItHC%irc2nNsCYkvmUmu#L!j -z8}vPC@?FnI?vl+7Oz7n2XVbsm5)}|fB813yPE10<9rvaG|EXe4I;Zo)`wq*WEppi< -z!k_s#!{@Qjiob7K)`?LpA>8Q6QvFBjRhzpMeLcuqop*yj53TpjFquLV8I&~WHdK$$ -z^G$A}kuF!6qzlS7_6SBoUIC&!!=t}F``rPzy+5>%vuZioodj!?qo#fJ_!S-W&N-@x -zLC;!Mv^^~!*s{upP*RzYN39@Fr2mV`>}+Z1Py4yqiGmOBObX_45DH37)2S2KXLD|U -zD6|iVB_1gyYV8wgs-oJ*mA8)Hz5>5f!0Vhj9>}!x5mJCER+N2YOTm8e&Y -zv+~c)F5={z>DZ49Vn$!`oe72#uJ4;j7`^r~!t`h=v~arfB>8AonuMbFi_utnm5-g; -zy_7B#tp7PL4)e3u>)7P-M_wzL;p=j2Gm^KtGTE6{;P-mCL#rn{qd=O*T -zDl+i(+V2qENgbz^dhK;F6)e75Es8c3uJmJP9wU#aZhbm)c5gLyf)YxA`&03Q%!zF3 -zNB2VY#MxiM$$HC3I*`nv%lF>9dSl`r;aEl@5j}{TRg7e$QQCF*_kDwgNvB!2fx*hG -zuKHOOb*}X(#nkecwYsa{2v4Fpg3+{iM&4ZPrvol$+=e^%6y{8uz1BJS$afiTYjuU; -zF`jsBoL=f*YvXb_1_b{-(Wx0NYn19om*lSe#|*8kuB0RlnionscuY3hf5zW9gQ2H9 -zAYWj%Y#FNi{(kk8MX7zVK_CH`!dy08*t6Vv#rF#Epnl12plbGE=;pJ;T>0~nPjCab -z&#Q%$4f!QUokMgdU$_9#-$Oi~umkEY4TdkxNO}~|1zj};4Qu_Kv{R$|9Wv$VF^O5U -zQ(?4MZ&sr3d|~$0SKGJ}R(6Dt3Bz+44bsm5;6EZZO((W-AL4wZ$UJ^SXpD0IMld9A -z`1beW*16N#n2aBjODTg*z*fh!n$WxIl3{#AkYZyI8%X^WeJKg8L?;BFspfJ3x{OMWx8ysY+qipEuI$WXX_9wx~%MxuOo`c^m*Fz#O7kBuE9$nB~)J%(*okGk+OE?)}*3`E_gLXSNvUK -zqa3BCU&Tx%gHHz}Rds*vDrxQeNo^a_!M}a7Xs|{%)L4YpyDyw$5@MnYnt)@&P-$ks -z)NpF1*xktTt;dxV_am;mP;+>$v*&{!wOb^4FKlRAM@<->T1HZ7u|{xLf4Lt$53V`G -zoAuABr|q_MBuvL%RdH3?Ke%?+HLe~4)>*l1Rd0>0KDO|byL@z95>i&ZXMh@=Pwwf) -zr#Mdnk8#h`FU={1y>aFlhshx<-7 -zmH$8p4QT&I)kPaE*Ny5K!;4A<`kE|6pCz=*F>!|0g4Dd&ulc#NNmcVeFVWZh{vrC> -zT0f}+1Oqp9-|?*YLh&okbBZ=NZzRqgsJ6D^t(=0`yIKr}0dnYeb3NgH -zhdq1{lEj+#5u01v;*L$@OO&Czu2&6wPC-^ZTNE!FD&}e-x}W(Ss^?d_88-dm{h;*9 -zTauAis>&AqUBLymYNw+vA#fARIstUr&AR{bH~^WiX@qxpw0N_ZmTAx{SKjyP^cJSA -zKdWJ-pnqmZc*VF=1?eQp11i&bq?G>7Or-KOi+YsJE*Dg<+z4D~H7yD?RS@&L-U8fM -zXIl8uVC4|*Arnj1jEvY;Om;dm-wc;Rc^Qa&k~l;RZ2Y=3D?blgvEV*_wBLcx!o?NtcTkP(Qp`RR`yfjD-t6Yv17JG)j4EzM9HHRl{`Y%CE3ETh2TOc8sG?Y1mEe`PPq -z*5O6K>y|>nWq(R!en?pzA?h>13Mml|s}C0$Z2JQ;_3e&>%?8D;w^3nN3TjQ{^Fcr% -zaaGFofCgLMdJX7 -z2J=jtIc>C4g;XZYB<;LVKBd#f^5jH4k*s4W9IsD#zSp!5DKp0au<=Q=hga;YhF|=k -z*u%SKi)CIb$L!%hyabe3&+#3 -zte(ABt3GhToVm+;bSlWuGe3fX&R>URT3HyofHsWTGD(;&#Fv&m|=-hN#H`j3&s-W6>@ -z4ZX|saWpsxyfwI7KH(@U;m;Wf!8Ultav+A9y@Qs}$1sCb*kS4|x5L25W%hBVj)BQM -z{6ypcXj0;QIA>SL26?n1!=#5B+43r9;qps%d|~@Ex7rF^)E*Jp3-@lauNt_T_(MJ~ -z+G4AfBtvRie_iwY4(QAB2hjaLqx>j;iP*VV-Xy0t4787m4kolmTzr<}?F_5XVA#hD -z`F(DWP%MfT6`8wabsMDN@n@OmpK|LSrh?^GrhIqFeuuF(7C7Kl!nl%%@z_hr!IY7> -zn%nN -zrrI8~Ey`GA^iAgW`R#YrHfvIPcZ$C?K?B2AW(GV5Z)dJf{8vqR^6lfg{49X?279wc -zHI9eX*2J_Rc&pPxRSvilI$8>_Jbr)dh;KPWS|iY}$N3;)bgzi4Y|?CoLs&x~S*^%j -z`=+-rX6%2(AMn;G>#?M$c@3{=Ux*OX*nKXTwC1F54z9i~*7GdKrk0dHc>+I+&)cjw!QoxdCQ6g`>v=Ji#!);;VLItqGu+Gy_P0BwrE6LAUn9AH=ZHS53-=S0Q -zK8!2K1ztvF4NSaMafxFuj5P-p;GY~LtFD$r1XiF|9n7G5!9x_vVNx>5mRq*Ttz)r?AFnt<9V)7BXd%v#i1 -z=E~$nb6~F|eWdxl6Q?$0?q{cWssIH8)ODwaU!w;)!35i@GUvH5z}9y15}z4*4Tuc= -zeZ^-iXBvBDaQ%>PIZPJEhkO;6yyp8K-|R+$Mq -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 -zDbMMU%&yC(H*NS7|K+Q*kdK -zO)MgFgeoWQgtz6h>_Fz{U3ruAX-Hs9a2eaJI<7i~50%=C$>y|wqNj3FHR7z6sjz@wF0Yz=j)+%68AJ) -zxJF&>4VDj1O4v8HIhkAH{l -z+*D8Y?vi_N2wJPpzLy@Uu -zr=bUpR58F?hzuOobs2D7M#X^kuX(<6!FsAb5Zg7S_qb16*b!gYQB%Le8Q}00{hHG7 -zWTkQuk>BF~-y!3FsjV*9i^76iQFh8wyAn)FgpH+rWN+X0e;#EZkNoqbpy}BE{&wb< -z(6(=dINwKDU%t})@G;koPq2}*=)R_($_&oCvp0zFo2S1`PDKcoDY2|M(g)-CH;ttN -zuvqGG-xqIc{v5NS%8RzylK5mZd{-QT0q%A>;8bqiy@^ZL%jH;j%1L)oiv8+r{$Ju_ -zHroevL&m?TxxdqB7K#aCt`U6gP9%3__L~s-5C`w7zPA|1mCfiqNs7fRRHttCV5QH9 -z+(v#MnSn5P0ATnzTh>aJ@(@ett!X+%If0Px8=&=;Qwz@WWuMzrX!n=P;SfrFaQoPF-J9GOlVB -zJq8RZ@U?7pC%|Em8v=g9tkdDEV|iNGY88i3Szp=IiJHurw^f}yU`KHr`uC>bj61uFZKpdq%S-;&rx8_JWmLy5a(+wfS -zVmN4GJgxjn_VgfJ)@jF=pChJz+8C#b@eJ?7A=%`ghOe&}a{8M_X+NMnc15$SvF0eM -zsvBQJJ(qg_wc6M -zD&nimb~J}hbeJ4UmwRP+vvE!WG5KTW=o>D-8lR$K%sq!ayJ~SmFRKb@qkGP8<@GG( -zGa7guMMMKd9ZTTJ;G?g&x?Z&EXkWqksBui5 -z7|Yd`*ASHF4G?0+cu;zGzn4aK)?8DRr;8NhEGQM@^Nfs5EhEeF<_F6r1*^VjVBi6e -zLZfY2k&h?PMj+)urEy#tbEN8C9e+;kymP0^JP9VvJ0Ah0!8qK<&(jK~!T9Kr6I}pu+pWJ}nIjSzcn%sc94aOOP=!L@bb+y%dP8@6~g0W4|fOcR*W^jbAY!-8{H5 -zP3q1}BrxZ(iSuc>sfrZf$k)Jx&pBWt#m`4EyHJ?<#~^tO%Df;A079x;#b~gUP(F1v -zbCs*7dT!`V9-$*pv#vl-X1i3+@|J|h5|b>0L`iF+sAQZ6M$$Y!*HC$djA5NK{%UN9 -z;?bSYm)gwR?x}O7JSsIL1oZ&C;SEr@tZyp>e!+%6Fv~;+F-CSnkpe^Nh^!qX!vmXO!ea5Ojj0{ -z;J67N=JwP*@;vzO^?FOX-Wq2bTOFwM7!VleQGXnz9ZrU$PmQ+%Ttg2?9dsdii -z&5#*Av@M#c@;Iw!i@r2iw@8en`I!%r5_i1qIsAvtbPI2Q$9y(Z7a+;P6eSBx9}S1v -z84-f%F+Fsr&apHEJs&+c6f=NBFI3SS7qaJ|!5>TV9p0BvI9>)TLto=7#2h5DTs?K2 -zBvLeundE(Q%Q|&d>!C*_N=S9l1r4Va#QE5jIo^6xsX+{ZG{ghMg?=vRjqov1ENSQ1bC!+7%L13cjvt* -zhq#eR=qDYw&d~1KMX2{^hR;um=dCekA?+#{IEZkf!0)~yyY`7*lydAX-P%9o&4p+Ftow2Dx5qk#UmCIjz*ttm{9(o -z=iKEbv*q%Q8tuD}pI*;gR|iXtddIA9Dc^y}%_?e-(Ma*apXiS@VfPn<_u7^3~s{p+w*duR2t&z44**nDiF-|<*o6OYa -zej-xwNHqn$b*R+5>47_8oA9iHM9{0p)%h{(z-IKv?8D6xxM7EZTA8L*S6W6<9}h6bcspXdy74}(INNzjUxe%$On99 -z^u*k9p!dWhuvJ$u_0Euu&bvx%L%?H8vFg?loKDGZIx`&+&(!uC`809xX1A>6o@UM^$hbipUxJpPJ=sZ*$cE@6sS>-J7+ -zBNPGX$`~K_g0wsp2Of}<3{I#xG)Q=XenT{j963IH53UB4{ZZ8_k&W$= -z3F|L!%z4e}bq_)4dUBr#@|_dP21+Pkfk(OW18(}u!nTNRTw2UHBprd@zMh{O1JbVv -z^V>w<{~@oFJ=A6Drl|Mk^m7J{`#`BK2jmmWMiM6~3EcpQ9QV;QE>To_i>#)IS}++m2t;Cow%EAN{M}1yZ%k?)cr^(~r4v@_MNmzTHiI&5#e; -z{SCh-guFs)Q#Y?TEFjjEzw1b|*iA)hcjQj_OzHhO=r!jJL)cN}j(@6PHUHghq)rY2 -z$%l=awW^d>|rbRmcdx -zWuA69IBnjD;H}~nR3)kE6TNx5Z+Ak|72pwho;`R9`s2>NKNf;R!zD<@*Lpe)LfdG0Fe|34qi2V@ -zJHVY|tsQJhdVe-+$)Iyofyqy~M5XW5bfll55J%!*#$T_tB<;1v&U|P3Ce|PThMf9? -zWak=(!DZ;ro^}HF{SCkK0bry%YbXJaR0PekScdf=@|;d-qMZcDLi2N64msFQJNuYXQo(KIGF -zKO9xS8eOXt{E-$Omr~ES#bSng*-EPE -z;8@v;g^$;oKc!WS9+=PwNeqed8}oA*CrtalIEDB@Sw`ev6%&_jtIHYRuY0;R=_YdE -zawKx}Dx@C>h@DlE?YEN6!!P6w!^2E&k -z00+YOIbxjN1PxYiML1azq(qf$Z#atF$O{86MmmKVAsL|`P -zZ{TXyFUCu!poOWBw@s_=P)L#l7yiN!(jB=T^J)~R^B(HdT&lY7ks_n#np-u8#7DbQ -zKNhIJ{Viw=kcaqmOHfQmw|_wZ=#Tzm->Ddna-+fer0gu&)`d -zK~24%@_8LlU^S@j(R2=s4e&WgPGVamk7}$w_-Z~jw|k!a?d@%8;L)Q4!<}gWVs;8( -zho9@_l!MuSX3Rr!4MEaxN}y#a7-%Qgs~ea~+Omm^nI*~kOxg6nv2#PIhbkR=*6M2S -zS(C)6U}{A`!KTQNZDlkoqr&SkFLXNn4&hAb36D~SC}3M7rB?s}4Y -zl&gVqVqYc!<{z;$uhkgX)D~&^0&czaNytQ3#ow~ -zQJ7iN2v2&&Pt%ax(k8yrG$$?04+%e`vgY-wNdO-miNyDzTh|2*-H0{@s5TC1-H_^_ -zO|5X6U*ZnC8V)B#nl&$LyJ9AWq?@E^bi0Q&>9v7YXuhmWtRJ#& -zjMvXQxgZJyvz&U`169*$ywLnN5?aH-0|p%YSKKlHBoh;|#DR^WQ#fgW6xM}$4y(sy`Ij+V$|= -zzE8auc?Ti>7}54d%?J;FPpCDwkwFXiM17Jt1Mh{>w%l-cGa&j9z=0%|i?ys!-!7sB -z=!Lj(T_5(guK7o8UhZ#6ElSnXE`!U650nF86d%6tYRIm7Qz(t;RoQo{Gxn7@sILci -zm~y(N2M(XWr@@xghrN+V(5vy|)&8c8UJ?t%;2SbE-`i0>F)lo(l<3SfU0Y4^57MPF -zZvHm#_wX4w2_9$bwy!2~`?HfEJcNxf2gULn;Q++pkhgWC^Y|KkDtqeM`Q;`gHWga2 -za>27^w6jW>1n1!zV-Err8&q03TSlG)6ytW09JWdRR!Y@%cG_k&1iC3Iax}-LSogq& -z{~&q?>izU5XhZ6jV;81g-@(_a-1oQUw`p%HHqjWBzNGFl`16Cv( -ziPGHZdBD|r4|-r+{pL#{5`|YRPOkNe5wa!N-zyxacW$5Fv^i1?=OU|dlA9D*$dU~X -z??&a;9H9aHF&_%z+Ht#LV22ScvFrsC4OUDYrkGJ3b9SRL>6SZYHVI~{A}^z=zZmcq&EyR($ax$5ES`~IFp-e=R> -zfrqK$A|mf`M{@p}`4B(Yzp}pdT;lmS^x-gh%wn#o&zW?PvuydU@LD)B;05SpUb6bz -z0FB;xwm%F8KFK}FRRNiA6A>9XxYa?#Bl?a^ZlP$U*x>rjbB=984G!BJB(Ofq=_4v~ -zO%r=io{WCv=`%b@XQAFG+$x<2egZmSg>{%%1Ioepfs%&VL3_NZ_NXt30mI4K07%aL^dhWETCZ&|sQMU*Qm} -zwpwmPX?ogcy=S|_5(E^z_@f26f;gV>m&jd+iN?0O -zMw5Ws+l3amCA7eZX$*G`AiMd|dGlkaKy!S()=M~e4`Yo#%PS+y|2MbwZ`)Q_bn?II -zCBo)Zcg?qN|J-UL5?lY2PK;gt540Y51TS)Rd8+4IHW+!i^2c(%mU -z|E;B*;gsB`BqH*fvMH!7ZQmJQvs1QEHPqiG;?*DoI|OsO_xFc{-ESq8Z1AUJFtd4g -zc&qc@y3&;Exvu A30u?#~E8_k0&jE{DVa(|d -zT@8DO`XEMC;S?z={$58WTxOW|^nR7tu3f$!+eXffH8by$PrBqC2ooh8>XateVUcdbR0G*Uz*GL`&rr -zC0f6hraEZVWbo4UJ%S$d7^e{zzC!4?AG=2^{&2*etqA= -z{`Kc4feu|DvqJ_a$18q;9xbw%$PR84W4wUqaK7v&3Ahb=d~V6-I^V0EO-Z{qNl3OhfZ~vzD=kD&`|`@{;I|iNZ{JE&Fmlb& -zEf+DGYzq-b#muu8G$JL~Y(3Hx-E1H>vDKo3zCJ!T@!(MB149t*+b^Hj(bOKE=89)d -zr>^PX%Ydjec@5IM-}Kn?!1cwC37Rh~fzIP(0vLF)dk`Mmm>K|guueu)jSjnBq>u@m -zNT(r`^9F7YE3|N$7ElAqN?UYMJr2Zm^XV)X#K2V+#7@=`jd&jLq;e{;X1Cl}-}b?P -zBjACgXcAj_~kV}@F$h)SKCRz8YHJi(;YXv*=^dsSTtm|MM -zUQ8C-oIBkKhJ)}RbK0ea5Re60Ab*t=3M97kI%u7AI5L`29XX}>bhU@53c7V5B$?R5 -zcOcDn64?hjeU#$;p|y_abdU81E4zWHPnXg2YkQR@WxKC6uWz4GYMI2DBVh8>i%u4c -z$)DNH+SX9o-5()(B%N?0r-B=EnoD^P2x-t|c3WC@!?zH-@w7gwayw-bvMLbgtoy5` -zv;AMiRdbr#KR$RYwm(5FLEf6}o8}s(kz!ZwPxy|@g6u&_t9#~qw8TKib>vN16{vky -zp_d{gh>QXOP!Yo}tYyFZLXGuy#drGihjSFyc#lE`J5Sp%>-Q!YO4wuAjq9G*QntZ! -z8#I~7C+y#W|lsG0=prP_=l -z-RG2pJuW?FaZ}7=jtFTq#Q9;3C=~?|wU=E^A05_CVU5mu8ARS++a|T{T^+0pa~ -z|4Ls@zfSkp$Er_H$tDf=D#6uqUFIunt49^kzWj^R(iOJJ23h_M?8`c=oJoV_y*gRU -zd9+!c8-=LL3ut|d7H>VwO=)$tSl)}Px2lPK6&DV8;vq`;{KrNJC$BGC%x`FM+f1wP -z+bt+bK!Bk8RavqzZz|^LJJDF_dHX}jN-g6j!McfHNJFk?FOkxV`1@1QB940*T#pTS -zOQwtluau4EP!8YoWY$9x`l%ZNHj}~l-gs7BKWAFNMlM%O5kRo95})$FR`#kf -zK}U)#lZ;I3o7kxy1Mq9HxFr~u9IP7`D!x1h1lYe?d&Ltgqkc0N%QDKaq$AW?rNvva -z)z1x-0FiXQviM@CDLCt06lLXqobyQ)u`C(vqx!Qmw-I!&ZukPqOAdNVSfjB{8`N -zr7sc&>Vx3{d1}S*U%b7q>w_zUfxupR#OT9m9#E{8>Cbt16xo(qPazXg3h8+F8X~*( -zYe3Q5*UoCkL~4e+QzCYP_jQy34-ZtG5RxB{qzSDE=GaArlgIKWz0mJ7OdtLARIFL0 -zzsk(6yF^*Nni1YFi&jdUkn8aF@R-oYPZd{?6`r;@Qe2uzf?O8`{zZt-@3lw6OEJP22e>d8u-vqSH;0t=s7j*4+j%bN$lHw}ihWQ> -zGn!b>JqyUI)zJ${mko5O>xzF}#3U_Ma}Usk48&0>!RA#-dKol~o8;snMXJ-#He0(q -zulv3hQ8~&Oe27!eRnXOFeUC?!G`*k+4h6FN;URI0KF0=#+}w}=AKh~2B0tJ5)PS@T -z&&0_BxbO=SJRh|{3^>e44|s7D=?OV6A-V6;vcuUpEtJ78#3-Iz7w}jUvUNUqi{v_fjmPv3jXNK=cV=yKD@@|?Nj@(#n -zXjOgiTP`g8Jxk{kk}GiJAJ{fhcWAQ1XoM;q5!ZLGpTQ0eBCP3nyKWW1rq2p4? -z+(bYT+*P|{$>*ACx)beEeFy;}O>_Dl$3h@2f#vd{p0^sQoufaNVUAU)=EJU3V)opc -z6aO)5^qZH80VkqlZD-L&TGceiR)TMWw)thKy78+XJi@tuwJXm3y*e)7e1JtE3fnX= -zR~>SCn&!V`z_1leSe3%Kr_fZx^@nokWSwBO+Od*I7bdB&7KEIH48ZFtA$xKc0D!OT -zrBR-d11}LcCxtc`BEZj}WiuEJ)Qswh1GLZ@3cZV;$JWkb5h%A5P8vQnyVSeogxFDa -z!OgQ`012?Yi+Hxe{W3Z2oavY_<1b8C`Xoo8qvO86w#O&l2QUp$ZTaBy`nhu`9`M!a -z9Bz|OdDF>z6Z_fWtu&r~B`+Kasu~w%>V2eDFZc@9o~`LHd1iGK%H%|52|LrUFo``` -z!qx}@*WDm3S$|=&Z4jWLRKtVM0zfSrihv?Kqa+tKg;@W^cek$Y(DRI||DqDN{+TX6 -z(ODZ{p-|SoOo)pqAclbYTy;7Ixygja)`uO2o(A| -zpp>un)R1ahI``qrAe`?WM<|(;^77T9wg>af;oC@`E#Ljw?Njne>zzbwpsPB_buOb9^78ZeNz!aZMkj<+pv@unMWc-(& -z_!4q~kxAm2d?zw_#S3hXV1XSPmnIv_NLpQ6JBoYJ3CKOzy1RN-Q$55d=>7Zz$}hz8 -z_DstizcWIPWdO%d@v%@QSL@tC|9M*f(h?%Xx_ZHaG$K^k45nvPy;ZOeG&LGLEy(XC -zo?QypZqUvma}Lsq9;YQQbE>o=Fph|$BmsvY;1J^q<5;0?pF0P54WqcUu{#mx0alte -zx+1MPX#d3Gn6me`U^#Y6PbcqPH9xA@dZFEL<8y`Jovrvi^jX0U`>jpxArA(xfyBz9 -zM-MB4QdaCzTVmE-a6|Fc!-= -zRfTn{KP5dzMqfB7;>AQCX;wdn3^w}FHn#&6Tf@CvDfi13_N&5wk2`N!(O9u%LV_*1E3ZBp+qwg`35QAaym!(`f^b1S(?)tq)fsLeb!7e=z@NYx2m -z7fSxGUe5fjiE9tzqe8{h0$MCpk*M^RfXY@>ppdADmKX^TLMYV)Yg++Dff68rNl-3D -z7QreCD6%O55*Dj01tAexQUOJXAyvv6*%Cq^Awc$<=zZG1;NG9+hncg?cb@Y-?|aVY -zOjYpvpY9AFb1;0BQW&}T8@}5w{bi9Q8cEg2r{%F*uCi7qGTxjk0uLycIOu7wQC9ro -z_scRKp^sWNQC>umaMu8b^xJq(4MwS2wfn|BNwzt*1)7C+6KV0brB+(l2#x^mSE{6a -z)4Q6FCw&Bj<713xs|) -zuWgGp(+KV2w*X+_uIKd(4Zkt;_ED}E+%zOmb{UU^>5Vq<(a|opPQ_bm(W#_`wjIHXZ-6;dA+}P}!4fR>0 -z1fBom>8aoXc-PF~%SUGYV(fkrYVHjLnd;@CTZOl~{gNwZtU6UxOeRV1KnKcBJhny`uI>f?Bz?eHla9Rkr8Ik>MsfV= -z;3kyp!E$GYIecrpiSB#DKj5PVL&1e`(CzD=r+Vl5?;rnIN;a>b)zfUl&BXI(MoK=D -zrDf%2-R-;UzvMWA)ovEMU8!7%!e{Q%l|J{Ok)6596>;DCRqWV6iyn9`^hjuM5W5_C -zm*Zx&lm5cCdb#>Z)ixCOK)oM0DX9-)&xBPW_Qf>l;7dY<|q}|9(6XArU?X6jRa5cRrXbTDR -ze>IVI1b_K9M)K~toUFLB?$K1`#R#j=C!`{_pda^QWvsfP6W$@DqZwyS>rRTldQQYj -zAJes6gK6MPu$U;(bkxD*L8}KCAm%hlZGuoeyl@VW11;Fa;eD^7Af7~kvGaY%44Y2r -zWV{}zexnPg`Pz{NLyl4Z&}IoOM$MWgn05M^wxZ6QRl>w*!8;D^c)yPbGg+FVq7KFe&~T -zU|{}_U7hWh<%$|864 -zoKM&UX|-#8+16sq^clG*t0P_T*dmsy+}4IH-GqMJx_tevPIxfHY8Rn*m>IY1$I@!c -z?)!FPeqv&ankzh2x6h;6;(oa)OF1n5OhcIUbx^lMHNHNzGuq0{sJP=BU+V_T6Vu2< -z)1t{Wpusbh=RFu?Jw6*h9dai**!ZSvX!vl8#`j6I!(!x&5=J6O-0slC2*u2OInM&c -z7Cc8!NZ@HqG6JhEv(9vVSUJD_;KddkuFi4Uag}+_k2*3RWSViE2PwO;!h*yxNBfz9 -z)SvK#dJ~NBTC7jyx*$V+<|o0fhFo8V+49hR8wRc?SW>?NoLxW}WKXRLZO*B^m2CmU%8UE>==QEF$i4TQW$OxzZU^Hzw>QuhgL@R84h$;Vlsk{)ET0f{ -zgR__Sbx;Dv=>pbo2f`=@96BD+HDU)A3HwCb!_O}$=wM_uC<$>3SpN{BN1r9WrX{jyR2xF -z5BX+~88WFRfm$KL=p=X1 -zT?HJEcJ(>?PZqAej!oH-axK1;E(Ya!xjgBtqKD<0=T(vReOEjxU6rfW#l7E@I?c6m<+JU~4*j!|xvDO>1vRDmc_ZX@h~ -zv*E{}+Np!Ae6@nd!v9G%xgobmXG$q)(wgvWp7}()*K?wehtEGVkU@^xUQ_q;_8uOA -z>Y?R(_WUs(kn9B3)-wL>pE6-?k6-;^%wyZqbhf1Cu0X~j@3E{2tVel3Fx9{{etmD$ -z+QBq^8pJabn@k}vQtLE91@)3|1HjUE5jPzfP{(8&$+B&r|1{hmug+zEqq}fNX*jooupAYx>JpK^#(+P>4`1B%|R6-1iv*V^lpgrb3K@E5q)v8p*FC*qIIc -zv`@?cKrj5n@~OL*0yQA#M_7(z6CMCGsXE=6)Gscyb+N>`V23>@MO%QR2>ljCaWQrD -z(Y$pz{lw(}7GKe0&sx2p>%nJwLHLncQ%;!MasyMVQIZaEF@3A&n-x4K2z^ta49hV)CQG0P2Wksx -z3Z63njA?aB40vde(q~jihA4F6`Ng`5bS6)Ggi9C#rBk11i*$9R{l5sj|NV6;mq5u^ -z>lFTDF;jVQSs@d?Vc=?iBIu;jT!uX}uT^guCaP${P^X$H>&b23h(#ZTT7*=(hH@TrB$1uGLM{SUo@(|eUzdU -x&FK{&GzM(gu79c_#VP$!x>LrZ45!{*t1LbzJ;=&B++Tmo+so%@&5@8R{{ba(5Lo~K - From 9fd707326b36bd48b6af453fb6a0fda7d66d59f3 Mon Sep 17 00:00:00 2001 From: slampy Date: Wed, 29 Oct 2025 14:37:31 +0100 Subject: [PATCH 07/19] fix add gitmodules --- .gitmodules | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitmodules b/.gitmodules index fed941be..29daa459 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ [submodule "ydb-api-protos"] path = ydb-api-protos url = https://github.com/ydb-platform/ydb-api-protos.git -[submodule "ydb/coordination/ydb-protos"] - path = ydb/coordination/ydb-protos - url = https://github.com/ydb-platform/ydb-api-protos.git From 44a20a6e9d3b1a4adc3fd1a88ec34492559fd8f6 Mon Sep 17 00:00:00 2001 From: slampy Date: Wed, 29 Oct 2025 15:23:34 +0100 Subject: [PATCH 08/19] add alter node --- .../test_coordination_alter_node.py | 34 ++++++++++++ ....py => test_coordination_describe_node.py} | 0 ydb/_grpc/grpcwrapper/ydb_coordination.py | 53 ++++++------------- ydb/coordination/__init__.py | 5 ++ ydb/coordination/coordination_client.py | 34 +++++++++++- ydb/coordination/operations.py | 13 +++++ ydb/driver.py | 10 ++-- 7 files changed, 102 insertions(+), 47 deletions(-) create mode 100644 tests/coordination/test_coordination_alter_node.py rename tests/coordination/{coordination_client.py => test_coordination_describe_node.py} (100%) diff --git a/tests/coordination/test_coordination_alter_node.py b/tests/coordination/test_coordination_alter_node.py new file mode 100644 index 00000000..2bd0dd80 --- /dev/null +++ b/tests/coordination/test_coordination_alter_node.py @@ -0,0 +1,34 @@ +import ydb +from ydb import _apis + + +def test_coordination_alter_node(driver_sync: ydb.Driver): + client = driver_sync.coordination_client + node_path = "/local/test_alter_node" + + try: + client.delete_node(node_path) + except ydb.SchemeError: + pass + + client.create_node(node_path) + + new_config = _apis.ydb_coordination.Config( + session_grace_period_millis=12345, + attach_consistency_mode=_apis.ydb_coordination.ConsistencyMode.CONSISTENCY_MODE_STRICT, + read_consistency_mode=_apis.ydb_coordination.ConsistencyMode.CONSISTENCY_MODE_RELAXED, + ) + + alter_res = client.alter_node(node_path, new_config) + + assert alter_res.status == ydb.StatusCode.SUCCESS, f"Alter operation failed: {alter_res.status}" + + + node = client.describe_node(node_path) + assert node.config.session_grace_period_millis == 12345, "Session grace period not updated" + assert node.config.attach_consistency_mode == _apis.ydb_coordination.ConsistencyMode.CONSISTENCY_MODE_STRICT, \ + "Attach consistency mode not updated" + assert node.config.read_consistency_mode == _apis.ydb_coordination.ConsistencyMode.CONSISTENCY_MODE_RELAXED, \ + "Read consistency mode not updated" + + client.delete_node(node_path) diff --git a/tests/coordination/coordination_client.py b/tests/coordination/test_coordination_describe_node.py similarity index 100% rename from tests/coordination/coordination_client.py rename to tests/coordination/test_coordination_describe_node.py diff --git a/ydb/_grpc/grpcwrapper/ydb_coordination.py b/ydb/_grpc/grpcwrapper/ydb_coordination.py index 315bdcfa..2a7ac33a 100644 --- a/ydb/_grpc/grpcwrapper/ydb_coordination.py +++ b/ydb/_grpc/grpcwrapper/ydb_coordination.py @@ -28,6 +28,21 @@ def to_proto(self) -> ydb_coordination_pb2.CreateNodeRequest: operation_params=self.operation_params, ) +@dataclass +class AlterNodeRequest(IToProto): + path: str + config: typing.Optional[public_types.NodeConfig] = None + operation_params: typing.Any = None + + def to_proto(self) -> ydb_coordination_pb2.AlterNodeRequest: + cfg_proto = self.config.to_proto() if self.config else None + return ydb_coordination_pb2.AlterNodeRequest( + path=self.path, + config=cfg_proto, + operation_params=self.operation_params, + ) + + @dataclass class DescribeNodeRequest(IToProto): @@ -53,41 +68,3 @@ def to_proto(self) -> ydb_coordination_pb2.DropNodeRequest: ) -@dataclass -class CreateNodeResponse(IFromProto): - operation : ydb.Operation - OPERATION_FIELD_NUMBER : int - - @staticmethod - def from_proto(msg: ydb_coordination_pb2.CreateNodeResponse) -> "CreateNodeResponse": - return CreateNodeResponse( - operation=msg.operation, - OPERATION_FIELD_NUMBER=msg.OPERATION_FIELD_NUMBER - ) - - -@dataclass -class DescribeNodeResponse(IFromProto): - operation : ydb.Operation - OPERATION_FIELD_NUMBER : int - - @staticmethod - def from_proto(msg: "ydb_coordination_pb2.DescribeNodeResponse") -> "DescribeNodeResponse": - return DescribeNodeResponse( - operation=msg.operation, - OPERATION_FIELD_NUMBER=msg.OPERATION_FIELD_NUMBER - ) - - -@dataclass -class DropNodeResponse(IFromProto): - operation : ydb.Operation - OPERATION_FIELD_NUMBER : int - - @staticmethod - def from_proto(msg: ydb_coordination_pb2.DropNodeResponse) -> "DropNodeResponse": - return DropNodeResponse( - operation=msg.operation, - OPERATION_FIELD_NUMBER=msg.OPERATION_FIELD_NUMBER - ) - diff --git a/ydb/coordination/__init__.py b/ydb/coordination/__init__.py index e69de29b..bedb18d0 100644 --- a/ydb/coordination/__init__.py +++ b/ydb/coordination/__init__.py @@ -0,0 +1,5 @@ +from .coordination_client import CoordinationClient + +__all__ = [ + "CoordinationClient", +] \ No newline at end of file diff --git a/ydb/coordination/coordination_client.py b/ydb/coordination/coordination_client.py index f0bd03fe..86a2da55 100644 --- a/ydb/coordination/coordination_client.py +++ b/ydb/coordination/coordination_client.py @@ -4,8 +4,7 @@ from ydb import _apis, issues -from .operations import DescribeNodeOperation, CreateNodeOperation, DropNodeOperation - +from .operations import DescribeNodeOperation, CreateNodeOperation, DropNodeOperation, AlterNodeOperation if typing.TYPE_CHECKING: import ydb @@ -25,6 +24,10 @@ def wrapper_delete_node(rpc_state, response_pb, path, *_args, **_kwargs): issues._process_response(response_pb.operation) return DropNodeOperation(rpc_state, response_pb, path) +def wrapper_alter_node(rpc_state, response_pb, path, *_args, **_kwargs): + issues._process_response(response_pb.operation) + return AlterNodeOperation(rpc_state, response_pb, path) + class CoordinationClient: def __init__(self, driver: "ydb.Driver"): @@ -103,3 +106,30 @@ def delete_node( settings=settings, ) + def alter_node( + self, + path: str, + new_config: typing.Optional[typing.Any] = None, + operation_params: typing.Optional[typing.Any] = None, + settings: Optional["ydb.BaseRequestSettings"] = None, + ): + """ + Alter node configuration. + """ + request = _apis.ydb_coordination.AlterNodeRequest( + path=path, + config=new_config, + operation_params=operation_params, + ) + + return self._call_node( + request, + _apis.CoordinationService.AlterNode, + wrapper_alter_node, + wrap_args=(path,), + settings=settings, + ) + + def close(self): + pass + diff --git a/ydb/coordination/operations.py b/ydb/coordination/operations.py index 47cf120e..8422ed1b 100644 --- a/ydb/coordination/operations.py +++ b/ydb/coordination/operations.py @@ -48,3 +48,16 @@ def __init__(self, rpc_state, response, path, driver=None): def __repr__(self): return f"DropNodeOperation" + +class AlterNodeOperation(ydb_op.Operation): + def __init__(self, rpc_state, response, path, driver=None): + super().__init__(rpc_state, response, driver) + self.path = path + self.status = response.operation.status + + def __repr__(self): + return f"AlterNodeOperation" + + __str__ = __repr__ + + diff --git a/ydb/driver.py b/ydb/driver.py index 661771c6..9fffc2eb 100644 --- a/ydb/driver.py +++ b/ydb/driver.py @@ -8,7 +8,7 @@ from . import tracing from . import iam from . import _utilities -from .coordination.coordination_client import CoordinationClient +from . import coordination @@ -289,16 +289,12 @@ def __init__( self._credentials = driver_config.credentials self.scheme_client = scheme.SchemeClient(self) - self.coordination_client = CoordinationClient(self) + self.coordination_client = coordination.CoordinationClient(self) self.table_client = table.TableClient(self, driver_config.table_client_settings) self.topic_client = topic.TopicClient(self, driver_config.topic_client_settings) def stop(self, timeout=10): self.table_client._stop_pool_if_needed(timeout=timeout) self.topic_client.close() - if hasattr(self, "coordination_client"): - try: - self.coordination_client.close() - except Exception as e: - logger.warning(f"Failed to close coordination client: {e}") + self.coordination_client.close() super().stop(timeout=timeout) From d749eb01c017e991e42cac3338a5581d2750d27c Mon Sep 17 00:00:00 2001 From: slampy Date: Wed, 29 Oct 2025 15:27:25 +0100 Subject: [PATCH 09/19] fix --- ydb/coordination/coordination_client.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/ydb/coordination/coordination_client.py b/ydb/coordination/coordination_client.py index 86a2da55..fe0fb889 100644 --- a/ydb/coordination/coordination_client.py +++ b/ydb/coordination/coordination_client.py @@ -113,9 +113,6 @@ def alter_node( operation_params: typing.Optional[typing.Any] = None, settings: Optional["ydb.BaseRequestSettings"] = None, ): - """ - Alter node configuration. - """ request = _apis.ydb_coordination.AlterNodeRequest( path=path, config=new_config, From a5a36f6e1c75ec4ed5e287a7510096321686bfef Mon Sep 17 00:00:00 2001 From: slampy Date: Wed, 29 Oct 2025 15:34:55 +0100 Subject: [PATCH 10/19] fix config --- .../ydb_coordination_public_types.py | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py b/ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py index 7732c1f3..a45f4b32 100644 --- a/ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py +++ b/ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py @@ -7,31 +7,23 @@ else: from ..common.protos import ydb_coordination_pb2 -class ConsistencyMode(Enum): - STRICT = 0 - RELAXED = 1 - -class RateLimiterCountersMode(Enum): - NONE = 0 - BASIC = 1 - FULL = 2 @dataclass class NodeConfig: - attach_consistency_mode: ConsistencyMode + attach_consistency_mode: ydb_coordination_pb2.ConsistencyMode path: str - rate_limiter_counters_mode: RateLimiterCountersMode - read_consistency_mode: ConsistencyMode + rate_limiter_counters_mode: ydb_coordination_pb2.RateLimiterCountersMode + read_consistency_mode: ydb_coordination_pb2.ConsistencyMode self_check_period_millis: int session_grace_period_millis: int @staticmethod def from_proto(msg: ydb_coordination_pb2.Config) -> "NodeConfig": return NodeConfig( - attach_consistency_mode=ConsistencyMode(msg.attach_consistency_mode), + attach_consistency_mode=ydb_coordination_pb2.ConsistencyMode(msg.attach_consistency_mode), path=msg.path, - rate_limiter_counters_mode=RateLimiterCountersMode(msg.rate_limiter_counters_mode), - read_consistency_mode=ConsistencyMode(msg.read_consistency_mode), + rate_limiter_counters_mode=ydb_coordination_pb2.RateLimiterCountersMode(msg.rate_limiter_counters_mode), + read_consistency_mode=ydb_coordination_pb2.ConsistencyMode(msg.read_consistency_mode), self_check_period_millis=msg.self_check_period_millis, session_grace_period_millis=msg.session_grace_period_millis, ) From 53ebed958dc9c46cdadb6bf9c84745c24cbae10a Mon Sep 17 00:00:00 2001 From: slampy Date: Wed, 29 Oct 2025 16:41:44 +0100 Subject: [PATCH 11/19] erase wrappers --- .../test_coordination_alter_node.py | 12 +- .../test_coordination_describe_node.py | 10 +- .../ydb_coordination_public_types.py | 6 +- ydb/coordination/coordination_client.py | 121 +++++------------- ydb/coordination/operations.py | 63 --------- 5 files changed, 42 insertions(+), 170 deletions(-) delete mode 100644 ydb/coordination/operations.py diff --git a/tests/coordination/test_coordination_alter_node.py b/tests/coordination/test_coordination_alter_node.py index 2bd0dd80..47de88c6 100644 --- a/tests/coordination/test_coordination_alter_node.py +++ b/tests/coordination/test_coordination_alter_node.py @@ -19,16 +19,14 @@ def test_coordination_alter_node(driver_sync: ydb.Driver): read_consistency_mode=_apis.ydb_coordination.ConsistencyMode.CONSISTENCY_MODE_RELAXED, ) - alter_res = client.alter_node(node_path, new_config) - assert alter_res.status == ydb.StatusCode.SUCCESS, f"Alter operation failed: {alter_res.status}" + client.alter_node(node_path, new_config) - - node = client.describe_node(node_path) - assert node.config.session_grace_period_millis == 12345, "Session grace period not updated" - assert node.config.attach_consistency_mode == _apis.ydb_coordination.ConsistencyMode.CONSISTENCY_MODE_STRICT, \ + node_config = client.describe_node(node_path) + assert node_config.session_grace_period_millis == 12345, "Session grace period not updated" + assert node_config.attach_consistency_mode == _apis.ydb_coordination.ConsistencyMode.CONSISTENCY_MODE_STRICT, \ "Attach consistency mode not updated" - assert node.config.read_consistency_mode == _apis.ydb_coordination.ConsistencyMode.CONSISTENCY_MODE_RELAXED, \ + assert node_config.read_consistency_mode == _apis.ydb_coordination.ConsistencyMode.CONSISTENCY_MODE_RELAXED, \ "Read consistency mode not updated" client.delete_node(node_path) diff --git a/tests/coordination/test_coordination_describe_node.py b/tests/coordination/test_coordination_describe_node.py index ef51d173..5718bad0 100644 --- a/tests/coordination/test_coordination_describe_node.py +++ b/tests/coordination/test_coordination_describe_node.py @@ -1,6 +1,5 @@ import ydb - def test_coordination_nodes(driver_sync: ydb.Driver): client = driver_sync.coordination_client node_path = "/local/test_node" @@ -12,13 +11,8 @@ def test_coordination_nodes(driver_sync: ydb.Driver): client.create_node(node_path) - node = client.describe_node(node_path) - - assert node.status == ydb.StatusCode.SUCCESS, f"Unexpected operation status: {node.status}" - - assert node.path.split("/")[-1] == "test_node", "Node name mismatch" - + node_config = client.describe_node(node_path) - assert node.config is not None, "Node config is missing" + assert node_config.path == "/local/test_node" client.delete_node(node_path) diff --git a/ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py b/ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py index a45f4b32..37bf97cc 100644 --- a/ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py +++ b/ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py @@ -20,10 +20,10 @@ class NodeConfig: @staticmethod def from_proto(msg: ydb_coordination_pb2.Config) -> "NodeConfig": return NodeConfig( - attach_consistency_mode=ydb_coordination_pb2.ConsistencyMode(msg.attach_consistency_mode), + attach_consistency_mode=msg.attach_consistency_mode, path=msg.path, - rate_limiter_counters_mode=ydb_coordination_pb2.RateLimiterCountersMode(msg.rate_limiter_counters_mode), - read_consistency_mode=ydb_coordination_pb2.ConsistencyMode(msg.read_consistency_mode), + rate_limiter_counters_mode=msg.rate_limiter_counters_mode, + read_consistency_mode=msg.read_consistency_mode, self_check_period_millis=msg.self_check_period_millis, session_grace_period_millis=msg.session_grace_period_millis, ) diff --git a/ydb/coordination/coordination_client.py b/ydb/coordination/coordination_client.py index fe0fb889..a198b20b 100644 --- a/ydb/coordination/coordination_client.py +++ b/ydb/coordination/coordination_client.py @@ -2,131 +2,74 @@ from typing import Optional from ydb import _apis, issues - - -from .operations import DescribeNodeOperation, CreateNodeOperation, DropNodeOperation, AlterNodeOperation +from ydb._grpc.grpcwrapper.ydb_coordination_public_types import NodeConfig if typing.TYPE_CHECKING: import ydb -def wrapper_create_node(rpc_state, response_pb, path, *_args, **_kwargs): - issues._process_response(response_pb.operation) - return CreateNodeOperation(rpc_state, response_pb, path) - - -def wrapper_describe_node(rpc_state, response_pb, *_args, **_kwargs): - issues._process_response(response_pb.operation) - return DescribeNodeOperation(rpc_state, response_pb) - - -def wrapper_delete_node(rpc_state, response_pb, path, *_args, **_kwargs): - issues._process_response(response_pb.operation) - return DropNodeOperation(rpc_state, response_pb, path) - -def wrapper_alter_node(rpc_state, response_pb, path, *_args, **_kwargs): - issues._process_response(response_pb.operation) - return AlterNodeOperation(rpc_state, response_pb, path) - - class CoordinationClient: def __init__(self, driver: "ydb.Driver"): self._driver = driver def _call_node( - self, - request, - rpc_method, - wrapper_fn, - wrap_args=(), - settings: Optional["ydb.BaseRequestSettings"] = None, + self, + request, + rpc_method, + settings: Optional["ydb.BaseRequestSettings"] = None, ): - return self._driver( + response = self._driver( request, _apis.CoordinationService.Stub, rpc_method, - wrap_result=wrapper_fn, - wrap_args=wrap_args, settings=settings, ) + issues._process_response(response.operation) + return response def create_node( - self, - path: str, - config: typing.Optional[typing.Any] = None, - operation_params: typing.Optional[typing.Any] = None, - settings: Optional["ydb.BaseRequestSettings"] = None, - ) -> CreateNodeOperation: + self, + path: str, + config: Optional[_apis.ydb_coordination.Config] = None, + settings: Optional["ydb.BaseRequestSettings"] = None, + ): request = _apis.ydb_coordination.CreateNodeRequest( path=path, config=config, - operation_params=operation_params, - ) - return self._call_node( - request, - _apis.CoordinationService.CreateNode, - wrapper_create_node, - wrap_args=(path,), - settings=settings, ) + self._call_node(request, _apis.CoordinationService.CreateNode, settings) def describe_node( self, path: str, - operation_params: typing.Optional[typing.Any] = None, - settings: Optional["ydb.BaseRequestSettings"] = None, - ) -> DescribeNodeOperation: - request = _apis.ydb_coordination.DescribeNodeRequest( - path=path, - operation_params=operation_params, - ) - return self._call_node( - request, - _apis.CoordinationService.DescribeNode, - wrapper_describe_node, - wrap_args=(path,), - settings=settings, - ) + settings: Optional["_apis.ydb_coordination.Config"] = None, + ) -> Optional[NodeConfig]: + request = _apis.ydb_coordination.DescribeNodeRequest(path=path) + response = self._call_node(request, _apis.CoordinationService.DescribeNode, settings) + result = _apis.ydb_coordination.DescribeNodeResult() + response.operation.result.Unpack(result) + result.config.path = path + return NodeConfig.from_proto(result.config) def delete_node( - self, - path: str, - operation_params: typing.Optional[typing.Any] = None, - settings: Optional["ydb.BaseRequestSettings"] = None, + self, + path: str, + settings: Optional["ydb.BaseRequestSettings"] = None, ): - request = _apis.ydb_coordination.DropNodeRequest( - path=path, - operation_params=operation_params, - ) - return self._call_node( - request, - _apis.CoordinationService.DropNode, - wrapper_delete_node, - wrap_args=(path,), - settings=settings, - ) + request = _apis.ydb_coordination.DropNodeRequest(path=path) + self._call_node(request, _apis.CoordinationService.DropNode, settings) def alter_node( - self, - path: str, - new_config: typing.Optional[typing.Any] = None, - operation_params: typing.Optional[typing.Any] = None, - settings: Optional["ydb.BaseRequestSettings"] = None, + self, + path: str, + new_config: _apis.ydb_coordination.Config, + settings: Optional["ydb.BaseRequestSettings"] = None, ): request = _apis.ydb_coordination.AlterNodeRequest( path=path, config=new_config, - operation_params=operation_params, - ) - - return self._call_node( - request, - _apis.CoordinationService.AlterNode, - wrapper_alter_node, - wrap_args=(path,), - settings=settings, ) + self._call_node(request, _apis.CoordinationService.AlterNode, settings) def close(self): pass - diff --git a/ydb/coordination/operations.py b/ydb/coordination/operations.py deleted file mode 100644 index 8422ed1b..00000000 --- a/ydb/coordination/operations.py +++ /dev/null @@ -1,63 +0,0 @@ -from ydb import operation as ydb_op -from ydb import _apis - -class DescribeNodeOperation(ydb_op.Operation): - def __init__(self, rpc_state, response, driver=None): - super().__init__(rpc_state, response, driver) - - self.status = response.operation.status - - result = _apis.ydb_coordination.DescribeNodeResult() - response.operation.result.Unpack(result) - - node_info = result.self - self.path = node_info.name - self.node_owner = node_info.owner - self.effective_permissions = node_info.effective_permissions - self.config = result.config - - if self.config: - self.session_grace_period_millis = self.config.session_grace_period_millis - self.attach_consistency_mode = self.config.attach_consistency_mode - self.read_consistency_mode = self.config.read_consistency_mode - else: - self.session_grace_period_millis = None - self.attach_consistency_mode = None - self.read_consistency_mode = None - - def __repr__(self): - return f"DescribeNodeOperation" - - __str__ = __repr__ - - -class CreateNodeOperation(ydb_op.Operation): - def __init__(self, rpc_state, response, path, driver=None): - super().__init__(rpc_state, response, driver) - self.path = path - self.status = response.operation.status - - def __repr__(self): - return f"CreateNodeOperation" - -class DropNodeOperation(ydb_op.Operation): - def __init__(self, rpc_state, response, path, driver=None): - super().__init__(rpc_state, response, driver) - self.path = path - self.status = response.operation.status - - def __repr__(self): - return f"DropNodeOperation" - -class AlterNodeOperation(ydb_op.Operation): - def __init__(self, rpc_state, response, path, driver=None): - super().__init__(rpc_state, response, driver) - self.path = path - self.status = response.operation.status - - def __repr__(self): - return f"AlterNodeOperation" - - __str__ = __repr__ - - From 678598b5ecea22cd291ca59fd8a66e4243a592bc Mon Sep 17 00:00:00 2001 From: slampy Date: Fri, 31 Oct 2025 17:34:15 +0100 Subject: [PATCH 12/19] fix public wrappers --- .../test_coordination_alter_node.py | 32 ----- .../coordination/test_coordination_client.py | 61 ++++++++ .../test_coordination_describe_node.py | 18 --- ydb/_grpc/grpcwrapper/ydb_coordination.py | 17 +-- .../ydb_coordination_public_types.py | 132 ++++++++++++++++-- ydb/coordination/__init__.py | 13 ++ ydb/coordination/coordination_client.py | 85 +++++++---- ydb/driver.py | 4 +- 8 files changed, 253 insertions(+), 109 deletions(-) delete mode 100644 tests/coordination/test_coordination_alter_node.py create mode 100644 tests/coordination/test_coordination_client.py delete mode 100644 tests/coordination/test_coordination_describe_node.py diff --git a/tests/coordination/test_coordination_alter_node.py b/tests/coordination/test_coordination_alter_node.py deleted file mode 100644 index 47de88c6..00000000 --- a/tests/coordination/test_coordination_alter_node.py +++ /dev/null @@ -1,32 +0,0 @@ -import ydb -from ydb import _apis - - -def test_coordination_alter_node(driver_sync: ydb.Driver): - client = driver_sync.coordination_client - node_path = "/local/test_alter_node" - - try: - client.delete_node(node_path) - except ydb.SchemeError: - pass - - client.create_node(node_path) - - new_config = _apis.ydb_coordination.Config( - session_grace_period_millis=12345, - attach_consistency_mode=_apis.ydb_coordination.ConsistencyMode.CONSISTENCY_MODE_STRICT, - read_consistency_mode=_apis.ydb_coordination.ConsistencyMode.CONSISTENCY_MODE_RELAXED, - ) - - - client.alter_node(node_path, new_config) - - node_config = client.describe_node(node_path) - assert node_config.session_grace_period_millis == 12345, "Session grace period not updated" - assert node_config.attach_consistency_mode == _apis.ydb_coordination.ConsistencyMode.CONSISTENCY_MODE_STRICT, \ - "Attach consistency mode not updated" - assert node_config.read_consistency_mode == _apis.ydb_coordination.ConsistencyMode.CONSISTENCY_MODE_RELAXED, \ - "Read consistency mode not updated" - - client.delete_node(node_path) diff --git a/tests/coordination/test_coordination_client.py b/tests/coordination/test_coordination_client.py new file mode 100644 index 00000000..dae195ea --- /dev/null +++ b/tests/coordination/test_coordination_client.py @@ -0,0 +1,61 @@ +import ydb + +from ydb.coordination import ( + NodeConfig, + ConsistencyMode, + RateLimiterCountersMode, + CoordinationClient +) + +class TestCoordination: + + def test_coordination_alter_node(self, driver_sync: ydb.Driver): + client = CoordinationClient(driver_sync) + node_path = "/local/test_alter_node" + + try: + client.delete_node(node_path) + except ydb.SchemeError: + pass + + client.create_node(node_path) + + new_config = NodeConfig( + session_grace_period_millis=12345, + attach_consistency_mode=ConsistencyMode.STRICT, + read_consistency_mode=ConsistencyMode.RELAXED, + rate_limiter_counters_mode=RateLimiterCountersMode.UNSET, + self_check_period_millis=0, + ) + + client.alter_node(node_path, new_config) + + node_desc = client.describe_node(node_path) + node_config = node_desc.config + path = node_desc.path + + assert node_path == path + assert node_config.session_grace_period_millis == 12345 + assert node_config.attach_consistency_mode == ConsistencyMode.STRICT + assert node_config.read_consistency_mode == ConsistencyMode.RELAXED + + client.delete_node(node_path) + + def test_coordination_nodes(self, driver_sync: ydb.Driver): + client = CoordinationClient(driver_sync) + node_path = "/local/test_node" + + try: + client.delete_node(node_path) + except ydb.SchemeError: + pass + + client.create_node(node_path) + + node_descr = client.describe_node(node_path) + + node_descr_path = node_descr.path + + assert node_descr_path == node_path + + client.delete_node(node_path) diff --git a/tests/coordination/test_coordination_describe_node.py b/tests/coordination/test_coordination_describe_node.py deleted file mode 100644 index 5718bad0..00000000 --- a/tests/coordination/test_coordination_describe_node.py +++ /dev/null @@ -1,18 +0,0 @@ -import ydb - -def test_coordination_nodes(driver_sync: ydb.Driver): - client = driver_sync.coordination_client - node_path = "/local/test_node" - - try: - client.delete_node(node_path) - except ydb.SchemeError: - pass - - client.create_node(node_path) - - node_config = client.describe_node(node_path) - - assert node_config.path == "/local/test_node" - - client.delete_node(node_path) diff --git a/ydb/_grpc/grpcwrapper/ydb_coordination.py b/ydb/_grpc/grpcwrapper/ydb_coordination.py index 2a7ac33a..32f2d3ba 100644 --- a/ydb/_grpc/grpcwrapper/ydb_coordination.py +++ b/ydb/_grpc/grpcwrapper/ydb_coordination.py @@ -1,7 +1,6 @@ import typing from dataclasses import dataclass -import ydb if typing.TYPE_CHECKING: from ..v4.protos import ydb_coordination_pb2 @@ -9,37 +8,31 @@ from ..common.protos import ydb_coordination_pb2 from .common_utils import IToProto, IFromProto, ServerStatus -from . import ydb_coordination_public_types as public_types +from ydb.coordination import NodeConfig -# -------------------- Requests -------------------- - @dataclass class CreateNodeRequest(IToProto): path: str - config: typing.Optional[public_types.NodeConfig] = None - operation_params: typing.Any = None + config: typing.Optional[NodeConfig] = None def to_proto(self) -> ydb_coordination_pb2.CreateNodeRequest: cfg_proto = self.config.to_proto() if self.config else None return ydb_coordination_pb2.CreateNodeRequest( path=self.path, config=cfg_proto, - operation_params=self.operation_params, ) @dataclass class AlterNodeRequest(IToProto): path: str - config: typing.Optional[public_types.NodeConfig] = None - operation_params: typing.Any = None + config: typing.Optional[NodeConfig] = None def to_proto(self) -> ydb_coordination_pb2.AlterNodeRequest: cfg_proto = self.config.to_proto() if self.config else None return ydb_coordination_pb2.AlterNodeRequest( path=self.path, config=cfg_proto, - operation_params=self.operation_params, ) @@ -47,24 +40,20 @@ def to_proto(self) -> ydb_coordination_pb2.AlterNodeRequest: @dataclass class DescribeNodeRequest(IToProto): path: str - operation_params: typing.Any = None def to_proto(self) -> ydb_coordination_pb2.DescribeNodeRequest: return ydb_coordination_pb2.DescribeNodeRequest( path=self.path, - operation_params=self.operation_params, ) @dataclass class DropNodeRequest(IToProto): path: str - operation_params: typing.Any = None def to_proto(self) -> ydb_coordination_pb2.DropNodeRequest: return ydb_coordination_pb2.DropNodeRequest( path=self.path, - operation_params=self.operation_params, ) diff --git a/ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py b/ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py index 37bf97cc..611aacf6 100644 --- a/ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py +++ b/ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py @@ -1,6 +1,7 @@ from dataclasses import dataclass from enum import Enum import typing +import ydb if typing.TYPE_CHECKING: from ..v4.protos import ydb_coordination_pb2 @@ -8,32 +9,139 @@ from ..common.protos import ydb_coordination_pb2 + +class ConsistencyMode(Enum): + UNSET = 0 + STRICT = 1 + RELAXED = 2 + + @classmethod + def from_proto(cls, proto_val: int) -> "ConsistencyMode": + mapping = { + ydb_coordination_pb2.ConsistencyMode.CONSISTENCY_MODE_UNSET: cls.UNSET, + ydb_coordination_pb2.ConsistencyMode.CONSISTENCY_MODE_STRICT: cls.STRICT, + ydb_coordination_pb2.ConsistencyMode.CONSISTENCY_MODE_RELAXED: cls.RELAXED, + } + return mapping[proto_val] + + def to_proto(self) -> int: + mapping = { + self.UNSET: ydb_coordination_pb2.ConsistencyMode.CONSISTENCY_MODE_UNSET, + self.STRICT: ydb_coordination_pb2.ConsistencyMode.CONSISTENCY_MODE_STRICT, + self.RELAXED: ydb_coordination_pb2.ConsistencyMode.CONSISTENCY_MODE_RELAXED, + } + return mapping[self] + + +class RateLimiterCountersMode(Enum): + UNSET = 0 + AGGREGATED = 1 + DETAILED = 2 + + @classmethod + def from_proto(cls, proto_val: int) -> "RateLimiterCountersMode": + mapping = { + ydb_coordination_pb2.RateLimiterCountersMode.RATE_LIMITER_COUNTERS_MODE_UNSET: cls.UNSET, + ydb_coordination_pb2.RateLimiterCountersMode.RATE_LIMITER_COUNTERS_MODE_AGGREGATED: cls.AGGREGATED, + ydb_coordination_pb2.RateLimiterCountersMode.RATE_LIMITER_COUNTERS_MODE_DETAILED: cls.DETAILED, + } + return mapping[proto_val] + + def to_proto(self) -> int: + mapping = { + self.UNSET: ydb_coordination_pb2.RateLimiterCountersMode.RATE_LIMITER_COUNTERS_MODE_UNSET, + self.AGGREGATED: ydb_coordination_pb2.RateLimiterCountersMode.RATE_LIMITER_COUNTERS_MODE_AGGREGATED, + self.DETAILED: ydb_coordination_pb2.RateLimiterCountersMode.RATE_LIMITER_COUNTERS_MODE_DETAILED, + } + return mapping[self] + + @dataclass class NodeConfig: - attach_consistency_mode: ydb_coordination_pb2.ConsistencyMode - path: str - rate_limiter_counters_mode: ydb_coordination_pb2.RateLimiterCountersMode - read_consistency_mode: ydb_coordination_pb2.ConsistencyMode + attach_consistency_mode: ConsistencyMode + rate_limiter_counters_mode: RateLimiterCountersMode + read_consistency_mode: ConsistencyMode self_check_period_millis: int session_grace_period_millis: int @staticmethod def from_proto(msg: ydb_coordination_pb2.Config) -> "NodeConfig": return NodeConfig( - attach_consistency_mode=msg.attach_consistency_mode, - path=msg.path, - rate_limiter_counters_mode=msg.rate_limiter_counters_mode, - read_consistency_mode=msg.read_consistency_mode, + attach_consistency_mode=ConsistencyMode.from_proto(msg.attach_consistency_mode), + rate_limiter_counters_mode=RateLimiterCountersMode.from_proto(msg.rate_limiter_counters_mode), + read_consistency_mode=ConsistencyMode.from_proto(msg.read_consistency_mode), self_check_period_millis=msg.self_check_period_millis, session_grace_period_millis=msg.session_grace_period_millis, ) def to_proto(self) -> ydb_coordination_pb2.Config: return ydb_coordination_pb2.Config( - attach_consistency_mode=self.attach_consistency_mode.value, - path=self.path, - rate_limiter_counters_mode=self.rate_limiter_counters_mode.value, - read_consistency_mode=self.read_consistency_mode.value, + attach_consistency_mode=self.attach_consistency_mode.to_proto(), + rate_limiter_counters_mode=self.rate_limiter_counters_mode.to_proto(), + read_consistency_mode=self.read_consistency_mode.to_proto(), self_check_period_millis=self.self_check_period_millis, session_grace_period_millis=self.session_grace_period_millis, ) + + +@dataclass +class NodeDescription: + path: str + config: NodeConfig + + +class CoordinationClientSettings: + def __init__(self): + self._trace_id = None + self._request_type = None + self._timeout = None + self._cancel_after = None + self._operation_timeout = None + self._compression = None + self._need_rpc_auth = True + self._headers = [] + + def with_trace_id(self, trace_id: str) -> "CoordinationClientSettings": + self._trace_id = trace_id + return self + + def with_request_type(self, request_type: str) -> "CoordinationClientSettings": + self._request_type = request_type + return self + + def with_timeout(self, timeout: float) -> "CoordinationClientSettings": + self._timeout = timeout + return self + + def with_cancel_after(self, timeout: float) -> "CoordinationClientSettings": + self._cancel_after = timeout + return self + + def with_operation_timeout(self, timeout: float) -> "CoordinationClientSettings": + self._operation_timeout = timeout + return self + + def with_compression(self, compression) -> "CoordinationClientSettings": + self._compression = compression + return self + + def with_need_rpc_auth(self, need_rpc_auth: bool) -> "CoordinationClientSettings": + self._need_rpc_auth = need_rpc_auth + return self + + def with_header(self, key: str, value: str) -> "CoordinationClientSettings": + self._headers.append((key, value)) + return self + + def to_base_request_settings(self) -> "ydb.BaseRequestSettings": + brs = ydb.BaseRequestSettings() + brs.trace_id = self._trace_id + brs.request_type = self._request_type + brs.timeout = self._timeout + brs.cancel_after = self._cancel_after + brs.operation_timeout = self._operation_timeout + brs.compression = self._compression + brs.need_rpc_auth = self._need_rpc_auth + brs.headers.extend(self._headers) + return brs + diff --git a/ydb/coordination/__init__.py b/ydb/coordination/__init__.py index bedb18d0..73b5470d 100644 --- a/ydb/coordination/__init__.py +++ b/ydb/coordination/__init__.py @@ -1,5 +1,18 @@ from .coordination_client import CoordinationClient +from ydb._grpc.grpcwrapper.ydb_coordination_public_types import ( + NodeConfig, + NodeDescription, + ConsistencyMode, + RateLimiterCountersMode, + CoordinationClientSettings, +) + __all__ = [ "CoordinationClient", + "NodeConfig", + "NodeDescription", + "ConsistencyMode", + "RateLimiterCountersMode", + "CoordinationClientSettings", ] \ No newline at end of file diff --git a/ydb/coordination/coordination_client.py b/ydb/coordination/coordination_client.py index a198b20b..112c1a28 100644 --- a/ydb/coordination/coordination_client.py +++ b/ydb/coordination/coordination_client.py @@ -2,7 +2,7 @@ from typing import Optional from ydb import _apis, issues -from ydb._grpc.grpcwrapper.ydb_coordination_public_types import NodeConfig +from ydb._grpc.grpcwrapper.ydb_coordination_public_types import NodeConfig, NodeDescription, CoordinationClientSettings if typing.TYPE_CHECKING: import ydb @@ -12,64 +12,89 @@ class CoordinationClient: def __init__(self, driver: "ydb.Driver"): self._driver = driver - def _call_node( + def create_node( self, - request, - rpc_method, - settings: Optional["ydb.BaseRequestSettings"] = None, + path: str, + config: Optional[NodeConfig] = None, + settings: Optional[CoordinationClientSettings] = None, ): + proto_config = config.to_proto() if config else None + base_driver_settings = settings.to_base_request_settings() if settings is not None else None + request = _apis.ydb_coordination.CreateNodeRequest( + path=path, + config=proto_config, + ) + response = self._driver( request, _apis.CoordinationService.Stub, - rpc_method, - settings=settings, + _apis.CoordinationService.CreateNode, + settings=base_driver_settings, ) issues._process_response(response.operation) return response - def create_node( - self, - path: str, - config: Optional[_apis.ydb_coordination.Config] = None, - settings: Optional["ydb.BaseRequestSettings"] = None, - ): - request = _apis.ydb_coordination.CreateNodeRequest( - path=path, - config=config, - ) - self._call_node(request, _apis.CoordinationService.CreateNode, settings) - def describe_node( self, path: str, - settings: Optional["_apis.ydb_coordination.Config"] = None, - ) -> Optional[NodeConfig]: + settings: Optional[CoordinationClientSettings] = None, + ) -> NodeDescription: request = _apis.ydb_coordination.DescribeNodeRequest(path=path) - response = self._call_node(request, _apis.CoordinationService.DescribeNode, settings) + base_driver_settings = settings.to_base_request_settings() if settings is not None else None + response = self._driver( + request, + _apis.CoordinationService.Stub, + _apis.CoordinationService.DescribeNode, + settings=base_driver_settings + ) + issues._process_response(response.operation) + result = _apis.ydb_coordination.DescribeNodeResult() response.operation.result.Unpack(result) - result.config.path = path - return NodeConfig.from_proto(result.config) + + return NodeDescription( + path=path, + config=NodeConfig.from_proto(result.config), + ) def delete_node( self, path: str, - settings: Optional["ydb.BaseRequestSettings"] = None, + settings: Optional[CoordinationClientSettings] = None, ): + base_driver_settings = settings.to_base_request_settings() if settings is not None else None request = _apis.ydb_coordination.DropNodeRequest(path=path) - self._call_node(request, _apis.CoordinationService.DropNode, settings) + response = self._driver( + request, + _apis.CoordinationService.Stub, + _apis.CoordinationService.DropNode, + settings=base_driver_settings, + ) + issues._process_response(response.operation) + return response def alter_node( self, path: str, - new_config: _apis.ydb_coordination.Config, - settings: Optional["ydb.BaseRequestSettings"] = None, + new_config: NodeConfig, + settings: Optional[CoordinationClientSettings] = None, ): + proto_config = new_config.to_proto() if new_config else None + base_driver_settings = settings.to_base_request_settings() if settings is not None else None + request = _apis.ydb_coordination.AlterNodeRequest( path=path, - config=new_config, + config=proto_config, ) - self._call_node(request, _apis.CoordinationService.AlterNode, settings) + + response = self._driver( + request, + _apis.CoordinationService.Stub, + _apis.CoordinationService.AlterNode, + settings=base_driver_settings, + ) + issues._process_response(response.operation) + return response def close(self): pass diff --git a/ydb/driver.py b/ydb/driver.py index 9fffc2eb..78e7eddd 100644 --- a/ydb/driver.py +++ b/ydb/driver.py @@ -250,7 +250,7 @@ def get_config( class Driver(pool.ConnectionPool): - __slots__ = ("scheme_client", "table_client", "coordination_client") + __slots__ = ("scheme_client", "table_client") def __init__( self, @@ -289,12 +289,10 @@ def __init__( self._credentials = driver_config.credentials self.scheme_client = scheme.SchemeClient(self) - self.coordination_client = coordination.CoordinationClient(self) self.table_client = table.TableClient(self, driver_config.table_client_settings) self.topic_client = topic.TopicClient(self, driver_config.topic_client_settings) def stop(self, timeout=10): self.table_client._stop_pool_if_needed(timeout=timeout) self.topic_client.close() - self.coordination_client.close() super().stop(timeout=timeout) From 1a6e1663df5c1a599acf8663e849b3398b50f557 Mon Sep 17 00:00:00 2001 From: slampy Date: Fri, 31 Oct 2025 17:37:50 +0100 Subject: [PATCH 13/19] fix last drivers coordination client inclusion --- ydb/driver.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ydb/driver.py b/ydb/driver.py index 78e7eddd..58aae2f0 100644 --- a/ydb/driver.py +++ b/ydb/driver.py @@ -8,7 +8,6 @@ from . import tracing from . import iam from . import _utilities -from . import coordination From aa8eaf9d84f3737935adfefc93970ee029a05c89 Mon Sep 17 00:00:00 2001 From: slampy Date: Fri, 31 Oct 2025 17:54:34 +0100 Subject: [PATCH 14/19] fix tox -e style --- tests/coordination/test_coordination_client.py | 1 + ydb/_apis.py | 3 ++- ydb/_grpc/grpcwrapper/ydb_coordination.py | 8 +++----- ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py | 2 -- ydb/coordination/__init__.py | 2 +- ydb/driver.py | 1 - 6 files changed, 7 insertions(+), 10 deletions(-) diff --git a/tests/coordination/test_coordination_client.py b/tests/coordination/test_coordination_client.py index dae195ea..cc48a5cc 100644 --- a/tests/coordination/test_coordination_client.py +++ b/tests/coordination/test_coordination_client.py @@ -7,6 +7,7 @@ CoordinationClient ) + class TestCoordination: def test_coordination_alter_node(self, driver_sync: ydb.Driver): diff --git a/ydb/_apis.py b/ydb/_apis.py index c6e3a153..97f64b90 100644 --- a/ydb/_apis.py +++ b/ydb/_apis.py @@ -140,6 +140,7 @@ class QueryService(object): ExecuteScript = "ExecuteScript" FetchScriptResults = "FetchScriptResults" + class CoordinationService(object): Stub = ydb_coordination_v1_pb2_grpc.CoordinationServiceStub @@ -147,4 +148,4 @@ class CoordinationService(object): CreateNode = "CreateNode" AlterNode = "AlterNode" DropNode = "DropNode" - DescribeNode = "DescribeNode" \ No newline at end of file + DescribeNode = "DescribeNode" diff --git a/ydb/_grpc/grpcwrapper/ydb_coordination.py b/ydb/_grpc/grpcwrapper/ydb_coordination.py index 32f2d3ba..c54b21d9 100644 --- a/ydb/_grpc/grpcwrapper/ydb_coordination.py +++ b/ydb/_grpc/grpcwrapper/ydb_coordination.py @@ -7,8 +7,8 @@ else: from ..common.protos import ydb_coordination_pb2 -from .common_utils import IToProto, IFromProto, ServerStatus -from ydb.coordination import NodeConfig +from .common_utils import IToProto +from ydb.coordination import NodeConfig @dataclass @@ -23,6 +23,7 @@ def to_proto(self) -> ydb_coordination_pb2.CreateNodeRequest: config=cfg_proto, ) + @dataclass class AlterNodeRequest(IToProto): path: str @@ -36,7 +37,6 @@ def to_proto(self) -> ydb_coordination_pb2.AlterNodeRequest: ) - @dataclass class DescribeNodeRequest(IToProto): path: str @@ -55,5 +55,3 @@ def to_proto(self) -> ydb_coordination_pb2.DropNodeRequest: return ydb_coordination_pb2.DropNodeRequest( path=self.path, ) - - diff --git a/ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py b/ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py index 611aacf6..c9461ce4 100644 --- a/ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py +++ b/ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py @@ -9,7 +9,6 @@ from ..common.protos import ydb_coordination_pb2 - class ConsistencyMode(Enum): UNSET = 0 STRICT = 1 @@ -144,4 +143,3 @@ def to_base_request_settings(self) -> "ydb.BaseRequestSettings": brs.need_rpc_auth = self._need_rpc_auth brs.headers.extend(self._headers) return brs - diff --git a/ydb/coordination/__init__.py b/ydb/coordination/__init__.py index 73b5470d..a2ed199b 100644 --- a/ydb/coordination/__init__.py +++ b/ydb/coordination/__init__.py @@ -15,4 +15,4 @@ "ConsistencyMode", "RateLimiterCountersMode", "CoordinationClientSettings", -] \ No newline at end of file +] diff --git a/ydb/driver.py b/ydb/driver.py index 58aae2f0..09d531d0 100644 --- a/ydb/driver.py +++ b/ydb/driver.py @@ -10,7 +10,6 @@ from . import _utilities - logger = logging.getLogger(__name__) From bd96246b57d73dbb62cc6b9614eee9c34c65cbae Mon Sep 17 00:00:00 2001 From: slampy Date: Fri, 31 Oct 2025 17:56:22 +0100 Subject: [PATCH 15/19] tox -e black-format --- tests/coordination/test_coordination_client.py | 8 +------- ydb/coordination/coordination_client.py | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/coordination/test_coordination_client.py b/tests/coordination/test_coordination_client.py index cc48a5cc..c220731c 100644 --- a/tests/coordination/test_coordination_client.py +++ b/tests/coordination/test_coordination_client.py @@ -1,15 +1,9 @@ import ydb -from ydb.coordination import ( - NodeConfig, - ConsistencyMode, - RateLimiterCountersMode, - CoordinationClient -) +from ydb.coordination import NodeConfig, ConsistencyMode, RateLimiterCountersMode, CoordinationClient class TestCoordination: - def test_coordination_alter_node(self, driver_sync: ydb.Driver): client = CoordinationClient(driver_sync) node_path = "/local/test_alter_node" diff --git a/ydb/coordination/coordination_client.py b/ydb/coordination/coordination_client.py index 112c1a28..2fe3d89d 100644 --- a/ydb/coordination/coordination_client.py +++ b/ydb/coordination/coordination_client.py @@ -45,7 +45,7 @@ def describe_node( request, _apis.CoordinationService.Stub, _apis.CoordinationService.DescribeNode, - settings=base_driver_settings + settings=base_driver_settings, ) issues._process_response(response.operation) From c3324cfe74af13fbab8c40cfefd1c22cb238e1e9 Mon Sep 17 00:00:00 2001 From: slampy Date: Tue, 4 Nov 2025 21:58:08 +0100 Subject: [PATCH 16/19] fix review remarks --- .../coordination/test_coordination_client.py | 91 +++++++++---- ydb/_grpc/grpcwrapper/ydb_coordination.py | 6 +- ydb/coordination/__init__.py | 4 +- ydb/coordination/coordination_client.py | 123 +++++------------- ydb/coordination/coordination_client_async.py | 43 ++++++ 5 files changed, 148 insertions(+), 119 deletions(-) create mode 100644 ydb/coordination/coordination_client_async.py diff --git a/tests/coordination/test_coordination_client.py b/tests/coordination/test_coordination_client.py index c220731c..11913915 100644 --- a/tests/coordination/test_coordination_client.py +++ b/tests/coordination/test_coordination_client.py @@ -1,56 +1,99 @@ +import pytest + import ydb -from ydb.coordination import NodeConfig, ConsistencyMode, RateLimiterCountersMode, CoordinationClient +from ydb.coordination import ( + NodeConfig, + ConsistencyMode, + RateLimiterCountersMode, + CoordinationClient, + AsyncCoordinationClient, +) class TestCoordination: - def test_coordination_alter_node(self, driver_sync: ydb.Driver): + def test_coordination_node_lifecycle(self, driver_sync: ydb.Driver): client = CoordinationClient(driver_sync) - node_path = "/local/test_alter_node" + node_path = "/local/test_node_lifecycle" try: client.delete_node(node_path) except ydb.SchemeError: pass - client.create_node(node_path) + with pytest.raises(ydb.SchemeError): + client.describe_node(node_path) - new_config = NodeConfig( - session_grace_period_millis=12345, + initial_config = NodeConfig( + session_grace_period_millis=1000, attach_consistency_mode=ConsistencyMode.STRICT, - read_consistency_mode=ConsistencyMode.RELAXED, + read_consistency_mode=ConsistencyMode.STRICT, rate_limiter_counters_mode=RateLimiterCountersMode.UNSET, self_check_period_millis=0, ) + client.create_node(node_path, initial_config) - client.alter_node(node_path, new_config) + node_descr = client.describe_node(node_path) + assert node_descr.path == node_path + assert node_descr.config == initial_config - node_desc = client.describe_node(node_path) - node_config = node_desc.config - path = node_desc.path + updated_config = NodeConfig( + session_grace_period_millis=12345, + attach_consistency_mode=ConsistencyMode.STRICT, + read_consistency_mode=ConsistencyMode.RELAXED, + rate_limiter_counters_mode=RateLimiterCountersMode.DETAILED, + self_check_period_millis=10, + ) + client.alter_node(node_path, updated_config) - assert node_path == path - assert node_config.session_grace_period_millis == 12345 - assert node_config.attach_consistency_mode == ConsistencyMode.STRICT - assert node_config.read_consistency_mode == ConsistencyMode.RELAXED + node_descr = client.describe_node(node_path) + assert node_descr.path == node_path + assert node_descr.config == updated_config client.delete_node(node_path) - def test_coordination_nodes(self, driver_sync: ydb.Driver): - client = CoordinationClient(driver_sync) - node_path = "/local/test_node" + with pytest.raises(ydb.SchemeError): + client.describe_node(node_path) + + async def test_coordination_node_lifecycle_async(self, aio_connection): + client = AsyncCoordinationClient(aio_connection) + node_path = "/local/test_node_lifecycle" try: - client.delete_node(node_path) + await client.delete_node(node_path) except ydb.SchemeError: pass - client.create_node(node_path) + with pytest.raises(ydb.SchemeError): + await client.describe_node(node_path) - node_descr = client.describe_node(node_path) + initial_config = NodeConfig( + session_grace_period_millis=1000, + attach_consistency_mode=ConsistencyMode.STRICT, + read_consistency_mode=ConsistencyMode.STRICT, + rate_limiter_counters_mode=RateLimiterCountersMode.UNSET, + self_check_period_millis=0, + ) + await client.create_node(node_path, initial_config) + + node_descr = await client.describe_node(node_path) + assert node_descr.path == node_path + assert node_descr.config == initial_config - node_descr_path = node_descr.path + updated_config = NodeConfig( + session_grace_period_millis=12345, + attach_consistency_mode=ConsistencyMode.STRICT, + read_consistency_mode=ConsistencyMode.RELAXED, + rate_limiter_counters_mode=RateLimiterCountersMode.DETAILED, + self_check_period_millis=10, + ) + await client.alter_node(node_path, updated_config) - assert node_descr_path == node_path + node_descr = await client.describe_node(node_path) + assert node_descr.path == node_path + assert node_descr.config == updated_config - client.delete_node(node_path) + await client.delete_node(node_path) + + with pytest.raises(ydb.SchemeError): + await client.describe_node(node_path) diff --git a/ydb/_grpc/grpcwrapper/ydb_coordination.py b/ydb/_grpc/grpcwrapper/ydb_coordination.py index c54b21d9..176e4e02 100644 --- a/ydb/_grpc/grpcwrapper/ydb_coordination.py +++ b/ydb/_grpc/grpcwrapper/ydb_coordination.py @@ -1,6 +1,7 @@ import typing from dataclasses import dataclass +from .ydb_coordination_public_types import NodeConfig if typing.TYPE_CHECKING: from ..v4.protos import ydb_coordination_pb2 @@ -8,13 +9,12 @@ from ..common.protos import ydb_coordination_pb2 from .common_utils import IToProto -from ydb.coordination import NodeConfig @dataclass class CreateNodeRequest(IToProto): path: str - config: typing.Optional[NodeConfig] = None + config: typing.Optional[NodeConfig] def to_proto(self) -> ydb_coordination_pb2.CreateNodeRequest: cfg_proto = self.config.to_proto() if self.config else None @@ -27,7 +27,7 @@ def to_proto(self) -> ydb_coordination_pb2.CreateNodeRequest: @dataclass class AlterNodeRequest(IToProto): path: str - config: typing.Optional[NodeConfig] = None + config: NodeConfig def to_proto(self) -> ydb_coordination_pb2.AlterNodeRequest: cfg_proto = self.config.to_proto() if self.config else None diff --git a/ydb/coordination/__init__.py b/ydb/coordination/__init__.py index a2ed199b..15966311 100644 --- a/ydb/coordination/__init__.py +++ b/ydb/coordination/__init__.py @@ -1,11 +1,11 @@ from .coordination_client import CoordinationClient +from .coordination_client_async import AsyncCoordinationClient from ydb._grpc.grpcwrapper.ydb_coordination_public_types import ( NodeConfig, NodeDescription, ConsistencyMode, RateLimiterCountersMode, - CoordinationClientSettings, ) __all__ = [ @@ -14,5 +14,5 @@ "NodeDescription", "ConsistencyMode", "RateLimiterCountersMode", - "CoordinationClientSettings", + "AsyncCoordinationClient", ] diff --git a/ydb/coordination/coordination_client.py b/ydb/coordination/coordination_client.py index 2fe3d89d..0c5c5d9d 100644 --- a/ydb/coordination/coordination_client.py +++ b/ydb/coordination/coordination_client.py @@ -1,100 +1,43 @@ -import typing from typing import Optional -from ydb import _apis, issues -from ydb._grpc.grpcwrapper.ydb_coordination_public_types import NodeConfig, NodeDescription, CoordinationClientSettings - -if typing.TYPE_CHECKING: - import ydb - - -class CoordinationClient: - def __init__(self, driver: "ydb.Driver"): - self._driver = driver - - def create_node( - self, - path: str, - config: Optional[NodeConfig] = None, - settings: Optional[CoordinationClientSettings] = None, - ): - proto_config = config.to_proto() if config else None - base_driver_settings = settings.to_base_request_settings() if settings is not None else None - request = _apis.ydb_coordination.CreateNodeRequest( - path=path, - config=proto_config, - ) - - response = self._driver( - request, - _apis.CoordinationService.Stub, - _apis.CoordinationService.CreateNode, - settings=base_driver_settings, +from ydb._grpc.grpcwrapper.ydb_coordination import ( + CreateNodeRequest, + DescribeNodeRequest, + AlterNodeRequest, + DropNodeRequest, +) +from ydb._grpc.grpcwrapper.ydb_coordination_public_types import NodeConfig, NodeDescription +from ydb.coordination.base_coordination_client import BaseCoordinationClient + + +class CoordinationClient(BaseCoordinationClient): + def create_node(self, path: str, config: Optional[NodeConfig], settings=None): + return self._call_create( + CreateNodeRequest(path=path, config=config).to_proto(), + settings=settings, + wrap_args=(self,), ) - issues._process_response(response.operation) - return response - def describe_node( - self, - path: str, - settings: Optional[CoordinationClientSettings] = None, - ) -> NodeDescription: - request = _apis.ydb_coordination.DescribeNodeRequest(path=path) - base_driver_settings = settings.to_base_request_settings() if settings is not None else None - response = self._driver( - request, - _apis.CoordinationService.Stub, - _apis.CoordinationService.DescribeNode, - settings=base_driver_settings, + def describe_node(self, path: str, settings=None) -> NodeDescription: + return self._call_describe( + DescribeNodeRequest(path=path).to_proto(), + settings=settings, + wrap_args=(self, path), ) - issues._process_response(response.operation) - - result = _apis.ydb_coordination.DescribeNodeResult() - response.operation.result.Unpack(result) - - return NodeDescription( - path=path, - config=NodeConfig.from_proto(result.config), - ) - - def delete_node( - self, - path: str, - settings: Optional[CoordinationClientSettings] = None, - ): - base_driver_settings = settings.to_base_request_settings() if settings is not None else None - request = _apis.ydb_coordination.DropNodeRequest(path=path) - response = self._driver( - request, - _apis.CoordinationService.Stub, - _apis.CoordinationService.DropNode, - settings=base_driver_settings, - ) - issues._process_response(response.operation) - return response - - def alter_node( - self, - path: str, - new_config: NodeConfig, - settings: Optional[CoordinationClientSettings] = None, - ): - proto_config = new_config.to_proto() if new_config else None - base_driver_settings = settings.to_base_request_settings() if settings is not None else None - request = _apis.ydb_coordination.AlterNodeRequest( - path=path, - config=proto_config, + def alter_node(self, path: str, new_config: NodeConfig, settings=None): + return self._call_alter( + AlterNodeRequest(path=path, config=new_config).to_proto(), + settings=settings, + wrap_args=(self,), ) - response = self._driver( - request, - _apis.CoordinationService.Stub, - _apis.CoordinationService.AlterNode, - settings=base_driver_settings, + def delete_node(self, path: str, settings=None): + return self._call_delete( + DropNodeRequest(path=path).to_proto(), + settings=settings, + wrap_args=(self,), ) - issues._process_response(response.operation) - return response - def close(self): - pass + def lock(self): + raise NotImplementedError("Will be implemented in future release") diff --git a/ydb/coordination/coordination_client_async.py b/ydb/coordination/coordination_client_async.py new file mode 100644 index 00000000..47964f05 --- /dev/null +++ b/ydb/coordination/coordination_client_async.py @@ -0,0 +1,43 @@ +from typing import Optional + +from ydb._grpc.grpcwrapper.ydb_coordination import ( + CreateNodeRequest, + DescribeNodeRequest, + AlterNodeRequest, + DropNodeRequest, +) +from ydb._grpc.grpcwrapper.ydb_coordination_public_types import NodeConfig, NodeDescription +from ydb.coordination.base_coordination_client import BaseCoordinationClient + + +class AsyncCoordinationClient(BaseCoordinationClient): + async def create_node(self, path: str, config: Optional[NodeConfig] = None, settings=None): + return await self._call_create( + CreateNodeRequest(path=path, config=config).to_proto(), + settings=settings, + wrap_args=(self,), + ) + + async def describe_node(self, path: str, settings=None) -> NodeDescription: + return await self._call_describe( + DescribeNodeRequest(path=path).to_proto(), + settings=settings, + wrap_args=(self, path), + ) + + async def alter_node(self, path: str, new_config: NodeConfig, settings=None): + return await self._call_alter( + AlterNodeRequest(path=path, config=new_config).to_proto(), + settings=settings, + wrap_args=(self,), + ) + + async def delete_node(self, path: str, settings=None): + return await self._call_delete( + DropNodeRequest(path=path).to_proto(), + settings=settings, + wrap_args=(self,), + ) + + async def lock(self): + raise NotImplementedError("Will be implemented in future release") From 41323e6bd0270908949eedca5c0b5a80f0d0e9e8 Mon Sep 17 00:00:00 2001 From: slampy Date: Tue, 4 Nov 2025 21:59:19 +0100 Subject: [PATCH 17/19] fix review remarks --- .../ydb_coordination_public_types.py | 124 +++--------------- ydb/coordination/base_coordination_client.py | 67 ++++++++++ 2 files changed, 87 insertions(+), 104 deletions(-) create mode 100644 ydb/coordination/base_coordination_client.py diff --git a/ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py b/ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py index c9461ce4..d1832ef8 100644 --- a/ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py +++ b/ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from enum import Enum +from enum import IntEnum import typing import ydb @@ -9,50 +9,16 @@ from ..common.protos import ydb_coordination_pb2 -class ConsistencyMode(Enum): - UNSET = 0 - STRICT = 1 - RELAXED = 2 +class ConsistencyMode(IntEnum): + UNSET = ydb_coordination_pb2.ConsistencyMode.CONSISTENCY_MODE_UNSET + STRICT = ydb_coordination_pb2.ConsistencyMode.CONSISTENCY_MODE_STRICT + RELAXED = ydb_coordination_pb2.ConsistencyMode.CONSISTENCY_MODE_RELAXED - @classmethod - def from_proto(cls, proto_val: int) -> "ConsistencyMode": - mapping = { - ydb_coordination_pb2.ConsistencyMode.CONSISTENCY_MODE_UNSET: cls.UNSET, - ydb_coordination_pb2.ConsistencyMode.CONSISTENCY_MODE_STRICT: cls.STRICT, - ydb_coordination_pb2.ConsistencyMode.CONSISTENCY_MODE_RELAXED: cls.RELAXED, - } - return mapping[proto_val] - def to_proto(self) -> int: - mapping = { - self.UNSET: ydb_coordination_pb2.ConsistencyMode.CONSISTENCY_MODE_UNSET, - self.STRICT: ydb_coordination_pb2.ConsistencyMode.CONSISTENCY_MODE_STRICT, - self.RELAXED: ydb_coordination_pb2.ConsistencyMode.CONSISTENCY_MODE_RELAXED, - } - return mapping[self] - - -class RateLimiterCountersMode(Enum): - UNSET = 0 - AGGREGATED = 1 - DETAILED = 2 - - @classmethod - def from_proto(cls, proto_val: int) -> "RateLimiterCountersMode": - mapping = { - ydb_coordination_pb2.RateLimiterCountersMode.RATE_LIMITER_COUNTERS_MODE_UNSET: cls.UNSET, - ydb_coordination_pb2.RateLimiterCountersMode.RATE_LIMITER_COUNTERS_MODE_AGGREGATED: cls.AGGREGATED, - ydb_coordination_pb2.RateLimiterCountersMode.RATE_LIMITER_COUNTERS_MODE_DETAILED: cls.DETAILED, - } - return mapping[proto_val] - - def to_proto(self) -> int: - mapping = { - self.UNSET: ydb_coordination_pb2.RateLimiterCountersMode.RATE_LIMITER_COUNTERS_MODE_UNSET, - self.AGGREGATED: ydb_coordination_pb2.RateLimiterCountersMode.RATE_LIMITER_COUNTERS_MODE_AGGREGATED, - self.DETAILED: ydb_coordination_pb2.RateLimiterCountersMode.RATE_LIMITER_COUNTERS_MODE_DETAILED, - } - return mapping[self] +class RateLimiterCountersMode(IntEnum): + UNSET = ydb_coordination_pb2.RateLimiterCountersMode.RATE_LIMITER_COUNTERS_MODE_UNSET + AGGREGATED = ydb_coordination_pb2.RateLimiterCountersMode.RATE_LIMITER_COUNTERS_MODE_AGGREGATED + DETAILED = ydb_coordination_pb2.RateLimiterCountersMode.RATE_LIMITER_COUNTERS_MODE_DETAILED @dataclass @@ -66,18 +32,18 @@ class NodeConfig: @staticmethod def from_proto(msg: ydb_coordination_pb2.Config) -> "NodeConfig": return NodeConfig( - attach_consistency_mode=ConsistencyMode.from_proto(msg.attach_consistency_mode), - rate_limiter_counters_mode=RateLimiterCountersMode.from_proto(msg.rate_limiter_counters_mode), - read_consistency_mode=ConsistencyMode.from_proto(msg.read_consistency_mode), + attach_consistency_mode=msg.attach_consistency_mode, + rate_limiter_counters_mode=msg.rate_limiter_counters_mode, + read_consistency_mode=msg.read_consistency_mode, self_check_period_millis=msg.self_check_period_millis, session_grace_period_millis=msg.session_grace_period_millis, ) def to_proto(self) -> ydb_coordination_pb2.Config: return ydb_coordination_pb2.Config( - attach_consistency_mode=self.attach_consistency_mode.to_proto(), - rate_limiter_counters_mode=self.rate_limiter_counters_mode.to_proto(), - read_consistency_mode=self.read_consistency_mode.to_proto(), + attach_consistency_mode=self.attach_consistency_mode, + rate_limiter_counters_mode=self.rate_limiter_counters_mode, + read_consistency_mode=self.read_consistency_mode, self_check_period_millis=self.self_check_period_millis, session_grace_period_millis=self.session_grace_period_millis, ) @@ -88,58 +54,8 @@ class NodeDescription: path: str config: NodeConfig - -class CoordinationClientSettings: - def __init__(self): - self._trace_id = None - self._request_type = None - self._timeout = None - self._cancel_after = None - self._operation_timeout = None - self._compression = None - self._need_rpc_auth = True - self._headers = [] - - def with_trace_id(self, trace_id: str) -> "CoordinationClientSettings": - self._trace_id = trace_id - return self - - def with_request_type(self, request_type: str) -> "CoordinationClientSettings": - self._request_type = request_type - return self - - def with_timeout(self, timeout: float) -> "CoordinationClientSettings": - self._timeout = timeout - return self - - def with_cancel_after(self, timeout: float) -> "CoordinationClientSettings": - self._cancel_after = timeout - return self - - def with_operation_timeout(self, timeout: float) -> "CoordinationClientSettings": - self._operation_timeout = timeout - return self - - def with_compression(self, compression) -> "CoordinationClientSettings": - self._compression = compression - return self - - def with_need_rpc_auth(self, need_rpc_auth: bool) -> "CoordinationClientSettings": - self._need_rpc_auth = need_rpc_auth - return self - - def with_header(self, key: str, value: str) -> "CoordinationClientSettings": - self._headers.append((key, value)) - return self - - def to_base_request_settings(self) -> "ydb.BaseRequestSettings": - brs = ydb.BaseRequestSettings() - brs.trace_id = self._trace_id - brs.request_type = self._request_type - brs.timeout = self._timeout - brs.cancel_after = self._cancel_after - brs.operation_timeout = self._operation_timeout - brs.compression = self._compression - brs.need_rpc_auth = self._need_rpc_auth - brs.headers.extend(self._headers) - return brs + @staticmethod + def from_proto(path: str, response_pb: ydb_coordination_pb2.DescribeNodeResponse) -> "NodeDescription": + result = ydb_coordination_pb2.DescribeNodeResult() + response_pb.operation.result.Unpack(result) + return NodeDescription(path=path, config=NodeConfig.from_proto(result.config)) diff --git a/ydb/coordination/base_coordination_client.py b/ydb/coordination/base_coordination_client.py new file mode 100644 index 00000000..7521f086 --- /dev/null +++ b/ydb/coordination/base_coordination_client.py @@ -0,0 +1,67 @@ +import typing +from typing import Optional + +from ydb import _apis, issues +from ydb._grpc.grpcwrapper.ydb_coordination_public_types import NodeConfig, NodeDescription + + +def wrapper_create_node(rpc_state, response_pb, client: "BaseCoordinationClient"): + issues._process_response(response_pb.operation) + + +def wrapper_describe_node(rpc_state, response_pb, client: "BaseCoordinationClient", path: str) -> NodeDescription: + issues._process_response(response_pb.operation) + return NodeDescription.from_proto(path, response_pb) + + +def wrapper_delete_node(rpc_state, response_pb, client: "BaseCoordinationClient"): + issues._process_response(response_pb.operation) + + +def wrapper_alter_node(rpc_state, response_pb, client: "BaseCoordinationClient"): + issues._process_response(response_pb.operation) + + +class BaseCoordinationClient: + def __init__(self, driver): + self._driver = driver + + def _call_create(self, request, settings=None, wrap_args=None): + return self._driver( + request, + _apis.CoordinationService.Stub, + _apis.CoordinationService.CreateNode, + wrap_result=wrapper_create_node, + wrap_args=wrap_args, + settings=settings, + ) + + def _call_describe(self, request, settings=None, wrap_args=None): + return self._driver( + request, + _apis.CoordinationService.Stub, + _apis.CoordinationService.DescribeNode, + wrap_result=wrapper_describe_node, + wrap_args=wrap_args, + settings=settings, + ) + + def _call_alter(self, request, settings=None, wrap_args=None): + return self._driver( + request, + _apis.CoordinationService.Stub, + _apis.CoordinationService.AlterNode, + wrap_result=wrapper_alter_node, + wrap_args=wrap_args, + settings=settings, + ) + + def _call_delete(self, request, settings=None, wrap_args=None): + return self._driver( + request, + _apis.CoordinationService.Stub, + _apis.CoordinationService.DropNode, + wrap_result=wrapper_delete_node, + wrap_args=wrap_args, + settings=settings, + ) From 65edbea82bb807d52bdac5b8730970709528c4d2 Mon Sep 17 00:00:00 2001 From: slampy Date: Wed, 5 Nov 2025 00:21:58 +0100 Subject: [PATCH 18/19] fix flake8 mistakes --- ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py | 2 +- ydb/coordination/base_coordination_client.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py b/ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py index d1832ef8..e98909f1 100644 --- a/ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py +++ b/ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from enum import IntEnum import typing -import ydb + if typing.TYPE_CHECKING: from ..v4.protos import ydb_coordination_pb2 diff --git a/ydb/coordination/base_coordination_client.py b/ydb/coordination/base_coordination_client.py index 7521f086..689cb8ee 100644 --- a/ydb/coordination/base_coordination_client.py +++ b/ydb/coordination/base_coordination_client.py @@ -1,6 +1,3 @@ -import typing -from typing import Optional - from ydb import _apis, issues from ydb._grpc.grpcwrapper.ydb_coordination_public_types import NodeConfig, NodeDescription From d2d25d4489acc6164c8ddbf5263f85070cb37b94 Mon Sep 17 00:00:00 2001 From: slampy Date: Wed, 5 Nov 2025 01:43:16 +0100 Subject: [PATCH 19/19] fix review remarks plus styles checks --- .../coordination/test_coordination_client.py | 24 +++++++-------- .../ydb_coordination_public_types.py | 12 +++----- ydb/aio/__init__.py | 1 + .../coordination_client.py} | 10 ++----- ydb/coordination/__init__.py | 12 ++------ ydb/coordination/base_coordination_client.py | 29 ++++++++++--------- ydb/coordination/coordination_client.py | 8 ++--- 7 files changed, 37 insertions(+), 59 deletions(-) rename ydb/{coordination/coordination_client_async.py => aio/coordination_client.py} (85%) diff --git a/tests/coordination/test_coordination_client.py b/tests/coordination/test_coordination_client.py index 11913915..98fb6768 100644 --- a/tests/coordination/test_coordination_client.py +++ b/tests/coordination/test_coordination_client.py @@ -1,13 +1,13 @@ import pytest import ydb +from ydb import aio from ydb.coordination import ( NodeConfig, ConsistencyMode, RateLimiterCountersMode, CoordinationClient, - AsyncCoordinationClient, ) @@ -33,9 +33,8 @@ def test_coordination_node_lifecycle(self, driver_sync: ydb.Driver): ) client.create_node(node_path, initial_config) - node_descr = client.describe_node(node_path) - assert node_descr.path == node_path - assert node_descr.config == initial_config + node_conf = client.describe_node(node_path) + assert node_conf == initial_config updated_config = NodeConfig( session_grace_period_millis=12345, @@ -46,9 +45,8 @@ def test_coordination_node_lifecycle(self, driver_sync: ydb.Driver): ) client.alter_node(node_path, updated_config) - node_descr = client.describe_node(node_path) - assert node_descr.path == node_path - assert node_descr.config == updated_config + node_conf = client.describe_node(node_path) + assert node_conf == updated_config client.delete_node(node_path) @@ -56,7 +54,7 @@ def test_coordination_node_lifecycle(self, driver_sync: ydb.Driver): client.describe_node(node_path) async def test_coordination_node_lifecycle_async(self, aio_connection): - client = AsyncCoordinationClient(aio_connection) + client = aio.CoordinationClient(aio_connection) node_path = "/local/test_node_lifecycle" try: @@ -76,9 +74,8 @@ async def test_coordination_node_lifecycle_async(self, aio_connection): ) await client.create_node(node_path, initial_config) - node_descr = await client.describe_node(node_path) - assert node_descr.path == node_path - assert node_descr.config == initial_config + node_conf = await client.describe_node(node_path) + assert node_conf == initial_config updated_config = NodeConfig( session_grace_period_millis=12345, @@ -89,9 +86,8 @@ async def test_coordination_node_lifecycle_async(self, aio_connection): ) await client.alter_node(node_path, updated_config) - node_descr = await client.describe_node(node_path) - assert node_descr.path == node_path - assert node_descr.config == updated_config + node_conf = await client.describe_node(node_path) + assert node_conf == updated_config await client.delete_node(node_path) diff --git a/ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py b/ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py index e98909f1..a3580974 100644 --- a/ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py +++ b/ydb/_grpc/grpcwrapper/ydb_coordination_public_types.py @@ -49,13 +49,9 @@ def to_proto(self) -> ydb_coordination_pb2.Config: ) -@dataclass -class NodeDescription: - path: str - config: NodeConfig - +class DescribeResult: @staticmethod - def from_proto(path: str, response_pb: ydb_coordination_pb2.DescribeNodeResponse) -> "NodeDescription": + def from_proto(msg: ydb_coordination_pb2.DescribeNodeResponse) -> "NodeConfig": result = ydb_coordination_pb2.DescribeNodeResult() - response_pb.operation.result.Unpack(result) - return NodeDescription(path=path, config=NodeConfig.from_proto(result.config)) + msg.operation.result.Unpack(result) + return NodeConfig.from_proto(result.config) diff --git a/ydb/aio/__init__.py b/ydb/aio/__init__.py index 1c9c887c..d38d9e73 100644 --- a/ydb/aio/__init__.py +++ b/ydb/aio/__init__.py @@ -1,3 +1,4 @@ from .driver import Driver # noqa from .table import SessionPool, retry_operation # noqa from .query import QuerySessionPool, QuerySession, QueryTxContext # noqa +from .coordination_client import CoordinationClient # noqa diff --git a/ydb/coordination/coordination_client_async.py b/ydb/aio/coordination_client.py similarity index 85% rename from ydb/coordination/coordination_client_async.py rename to ydb/aio/coordination_client.py index 47964f05..9aab6785 100644 --- a/ydb/coordination/coordination_client_async.py +++ b/ydb/aio/coordination_client.py @@ -6,37 +6,33 @@ AlterNodeRequest, DropNodeRequest, ) -from ydb._grpc.grpcwrapper.ydb_coordination_public_types import NodeConfig, NodeDescription +from ydb._grpc.grpcwrapper.ydb_coordination_public_types import NodeConfig from ydb.coordination.base_coordination_client import BaseCoordinationClient -class AsyncCoordinationClient(BaseCoordinationClient): +class CoordinationClient(BaseCoordinationClient): async def create_node(self, path: str, config: Optional[NodeConfig] = None, settings=None): return await self._call_create( CreateNodeRequest(path=path, config=config).to_proto(), settings=settings, - wrap_args=(self,), ) - async def describe_node(self, path: str, settings=None) -> NodeDescription: + async def describe_node(self, path: str, settings=None) -> NodeConfig: return await self._call_describe( DescribeNodeRequest(path=path).to_proto(), settings=settings, - wrap_args=(self, path), ) async def alter_node(self, path: str, new_config: NodeConfig, settings=None): return await self._call_alter( AlterNodeRequest(path=path, config=new_config).to_proto(), settings=settings, - wrap_args=(self,), ) async def delete_node(self, path: str, settings=None): return await self._call_delete( DropNodeRequest(path=path).to_proto(), settings=settings, - wrap_args=(self,), ) async def lock(self): diff --git a/ydb/coordination/__init__.py b/ydb/coordination/__init__.py index 15966311..55834e89 100644 --- a/ydb/coordination/__init__.py +++ b/ydb/coordination/__init__.py @@ -1,18 +1,10 @@ from .coordination_client import CoordinationClient -from .coordination_client_async import AsyncCoordinationClient from ydb._grpc.grpcwrapper.ydb_coordination_public_types import ( NodeConfig, - NodeDescription, ConsistencyMode, RateLimiterCountersMode, + DescribeResult, ) -__all__ = [ - "CoordinationClient", - "NodeConfig", - "NodeDescription", - "ConsistencyMode", - "RateLimiterCountersMode", - "AsyncCoordinationClient", -] +__all__ = ["CoordinationClient", "NodeConfig", "ConsistencyMode", "RateLimiterCountersMode", "DescribeResult"] diff --git a/ydb/coordination/base_coordination_client.py b/ydb/coordination/base_coordination_client.py index 689cb8ee..d2b5bf8c 100644 --- a/ydb/coordination/base_coordination_client.py +++ b/ydb/coordination/base_coordination_client.py @@ -1,64 +1,65 @@ from ydb import _apis, issues -from ydb._grpc.grpcwrapper.ydb_coordination_public_types import NodeConfig, NodeDescription +from ydb._grpc.grpcwrapper.ydb_coordination_public_types import NodeConfig, DescribeResult +import logging -def wrapper_create_node(rpc_state, response_pb, client: "BaseCoordinationClient"): +logger = logging.getLogger(__name__) + + +def wrapper_create_node(rpc_state, response_pb): issues._process_response(response_pb.operation) -def wrapper_describe_node(rpc_state, response_pb, client: "BaseCoordinationClient", path: str) -> NodeDescription: +def wrapper_describe_node(rpc_state, response_pb) -> NodeConfig: issues._process_response(response_pb.operation) - return NodeDescription.from_proto(path, response_pb) + return DescribeResult.from_proto(response_pb) -def wrapper_delete_node(rpc_state, response_pb, client: "BaseCoordinationClient"): +def wrapper_delete_node(rpc_state, response_pb): issues._process_response(response_pb.operation) -def wrapper_alter_node(rpc_state, response_pb, client: "BaseCoordinationClient"): +def wrapper_alter_node(rpc_state, response_pb): issues._process_response(response_pb.operation) class BaseCoordinationClient: def __init__(self, driver): + logger.warning("Experimental API: interface may change in future releases.") self._driver = driver - def _call_create(self, request, settings=None, wrap_args=None): + def _call_create(self, request, settings=None): return self._driver( request, _apis.CoordinationService.Stub, _apis.CoordinationService.CreateNode, wrap_result=wrapper_create_node, - wrap_args=wrap_args, settings=settings, ) - def _call_describe(self, request, settings=None, wrap_args=None): + def _call_describe(self, request, settings=None): return self._driver( request, _apis.CoordinationService.Stub, _apis.CoordinationService.DescribeNode, wrap_result=wrapper_describe_node, - wrap_args=wrap_args, settings=settings, ) - def _call_alter(self, request, settings=None, wrap_args=None): + def _call_alter(self, request, settings=None): return self._driver( request, _apis.CoordinationService.Stub, _apis.CoordinationService.AlterNode, wrap_result=wrapper_alter_node, - wrap_args=wrap_args, settings=settings, ) - def _call_delete(self, request, settings=None, wrap_args=None): + def _call_delete(self, request, settings=None): return self._driver( request, _apis.CoordinationService.Stub, _apis.CoordinationService.DropNode, wrap_result=wrapper_delete_node, - wrap_args=wrap_args, settings=settings, ) diff --git a/ydb/coordination/coordination_client.py b/ydb/coordination/coordination_client.py index 0c5c5d9d..24dd999d 100644 --- a/ydb/coordination/coordination_client.py +++ b/ydb/coordination/coordination_client.py @@ -6,7 +6,7 @@ AlterNodeRequest, DropNodeRequest, ) -from ydb._grpc.grpcwrapper.ydb_coordination_public_types import NodeConfig, NodeDescription +from ydb._grpc.grpcwrapper.ydb_coordination_public_types import NodeConfig from ydb.coordination.base_coordination_client import BaseCoordinationClient @@ -15,28 +15,24 @@ def create_node(self, path: str, config: Optional[NodeConfig], settings=None): return self._call_create( CreateNodeRequest(path=path, config=config).to_proto(), settings=settings, - wrap_args=(self,), ) - def describe_node(self, path: str, settings=None) -> NodeDescription: + def describe_node(self, path: str, settings=None) -> NodeConfig: return self._call_describe( DescribeNodeRequest(path=path).to_proto(), settings=settings, - wrap_args=(self, path), ) def alter_node(self, path: str, new_config: NodeConfig, settings=None): return self._call_alter( AlterNodeRequest(path=path, config=new_config).to_proto(), settings=settings, - wrap_args=(self,), ) def delete_node(self, path: str, settings=None): return self._call_delete( DropNodeRequest(path=path).to_proto(), settings=settings, - wrap_args=(self,), ) def lock(self):