From fba6eaaa9cbc276d94967dc49adbee6f77352c12 Mon Sep 17 00:00:00 2001 From: Ilyas Niyazov Date: Thu, 13 Mar 2025 14:26:01 +0300 Subject: [PATCH] [#3] Added generate proto script create container method Signed-off-by: Ilyas Niyazov --- frostfs_sdk/__init__.py | 2 + frostfs_sdk/client/__init__.py | 10 +++ frostfs_sdk/client/frostfs_client.py | 9 ++- .../client/models/client_environment.py | 18 ++++- .../models/{ecdsa.py => ecdsa_model.py} | 0 .../client/parameters/call_context_param.py | 21 ++++++ ...container_create.py => container_param.py} | 10 +-- .../parameters/{wait.py => wait_param.py} | 2 +- frostfs_sdk/client/services/container.py | 37 ++++++---- .../client/services/context_accessor.py | 12 +++ frostfs_sdk/client/services/session.py | 37 ++++++++++ frostfs_sdk/client/utils/message_helper.py | 30 ++++++++ frostfs_sdk/cryptography/key_extension.py | 8 ++ frostfs_sdk/cryptography/signer.py | 26 +++++++ frostfs_sdk/models/__init__.py | 9 +++ frostfs_sdk/models/dto/container.py | 12 ++- frostfs_sdk/models/dto/owner_id.py | 20 +++++ frostfs_sdk/models/dto/placement_policy.py | 5 ++ frostfs_sdk/models/dto/session_token.py | 5 -- frostfs_sdk/models/dto/version.py | 19 +++++ frostfs_sdk/models/enums/filter_operation.py | 2 +- frostfs_sdk/models/enums/selector_clause.py | 30 +++----- .../models/mappers/container_mapper.py | 30 ++++---- frostfs_sdk/models/mappers/filter_mapper.py | 2 +- frostfs_sdk/models/mappers/owner_id_mapper.py | 32 ++++++++ .../models/mappers/placement_policy_mapper.py | 17 +++-- frostfs_sdk/models/mappers/replica_mapper.py | 56 ++++++++++++++ frostfs_sdk/models/mappers/selector_mapper.py | 73 +++++++++++++++++++ frostfs_sdk/models/mappers/uuid_extension.py | 31 ++++++++ frostfs_sdk/models/mappers/version_mapper.py | 31 ++++++++ generate_proto.sh | 2 +- tests/client/test_create_container.py | 23 +++--- tests/conftest.py | 18 ++--- tests/helpers/params_container.py | 16 ++-- 34 files changed, 547 insertions(+), 108 deletions(-) create mode 100644 frostfs_sdk/client/__init__.py rename frostfs_sdk/client/models/{ecdsa.py => ecdsa_model.py} (100%) create mode 100644 frostfs_sdk/client/parameters/call_context_param.py rename frostfs_sdk/client/parameters/{container_create.py => container_param.py} (57%) rename frostfs_sdk/client/parameters/{wait.py => wait_param.py} (97%) create mode 100644 frostfs_sdk/client/services/context_accessor.py create mode 100644 frostfs_sdk/client/services/session.py create mode 100644 frostfs_sdk/client/utils/message_helper.py create mode 100644 frostfs_sdk/models/__init__.py create mode 100644 frostfs_sdk/models/dto/owner_id.py delete mode 100644 frostfs_sdk/models/dto/session_token.py create mode 100644 frostfs_sdk/models/dto/version.py create mode 100644 frostfs_sdk/models/mappers/uuid_extension.py create mode 100644 frostfs_sdk/models/mappers/version_mapper.py diff --git a/frostfs_sdk/__init__.py b/frostfs_sdk/__init__.py index e69de29..4934e5f 100644 --- a/frostfs_sdk/__init__.py +++ b/frostfs_sdk/__init__.py @@ -0,0 +1,2 @@ +from frostfs_sdk.client import * +from frostfs_sdk.models import * diff --git a/frostfs_sdk/client/__init__.py b/frostfs_sdk/client/__init__.py new file mode 100644 index 0000000..f9d922d --- /dev/null +++ b/frostfs_sdk/client/__init__.py @@ -0,0 +1,10 @@ +from frostfs_sdk.client.frostfs_client import FrostfsClient + +from frostfs_sdk.client.models.client_environment import ClientEnvironment +from frostfs_sdk.client.models.client_settings import ClientSettings +from frostfs_sdk.client.models.ecdsa_model import ECDSA + +from frostfs_sdk.client.parameters.container_param import ContainerCreateParam +from frostfs_sdk.client.parameters.wait_param import WaitParam + +from frostfs_sdk.client.services.container import ContainerClient diff --git a/frostfs_sdk/client/frostfs_client.py b/frostfs_sdk/client/frostfs_client.py index 02f79f4..d5738fa 100644 --- a/frostfs_sdk/client/frostfs_client.py +++ b/frostfs_sdk/client/frostfs_client.py @@ -1,17 +1,20 @@ # Create channel and Stubs import grpc + +from frostfs_sdk.client.services.session import SessionCache from frostfs_sdk.client.models.client_environment import ClientEnvironment from frostfs_sdk.client.models.client_settings import ClientSettings -from frostfs_sdk.client.models.ecdsa import ECDSA +from frostfs_sdk.client.models.ecdsa_model import ECDSA from frostfs_sdk.client.services.container import ContainerClient +from frostfs_sdk.models.dto.version import Version class FrostfsClient: def __init__(self, client_settings: ClientSettings): - self.channel = grpc.insecure_channel(f"{client_settings.host}:{client_settings.port}") + self.channel = grpc.insecure_channel(client_settings.address) self.ecdsa: ECDSA = ECDSA(wif=client_settings.wif) - client_environment = ClientEnvironment(self.ecdsa, self.channel) + client_environment = ClientEnvironment(self.ecdsa, self.channel, client_settings.address, Version(), SessionCache(0)) self.container = ContainerClient(client_environment) def close(self): diff --git a/frostfs_sdk/client/models/client_environment.py b/frostfs_sdk/client/models/client_environment.py index 8d6d33a..00f5aa1 100644 --- a/frostfs_sdk/client/models/client_environment.py +++ b/frostfs_sdk/client/models/client_environment.py @@ -1,8 +1,22 @@ import grpc -from frostfs_sdk.client.models.ecdsa import ECDSA +from frostfs_sdk.cryptography.key_extension import KeyExtension +from frostfs_sdk.client.models.ecdsa_model import ECDSA +from frostfs_sdk.models.dto.version import Version +from frostfs_sdk.models.dto.owner_id import OwnerId +from frostfs_sdk.client.services.session import SessionCache class ClientEnvironment: - def __init__(self, ecdsa: ECDSA, channel: grpc.Channel): + def __init__(self, ecdsa: ECDSA, channel: grpc.Channel, address: str, version: Version, session_cache: SessionCache): self.ecdsa = ecdsa self.channel = channel + self.version = version + # self.owner_id = OwnerId(KeyExtension.get_owner_id_by_wif(ecdsa.wif)) + self.owner_id = "11" + self.session_cache = session_cache + self.address = address + + def get_session_key(self): + if not self._session_key: + self._session_key = SessionCache.form_cache_key(self.address, KeyExtension.get_hex_string(self.ecdsa.public_key)) + return self._session_key diff --git a/frostfs_sdk/client/models/ecdsa.py b/frostfs_sdk/client/models/ecdsa_model.py similarity index 100% rename from frostfs_sdk/client/models/ecdsa.py rename to frostfs_sdk/client/models/ecdsa_model.py diff --git a/frostfs_sdk/client/parameters/call_context_param.py b/frostfs_sdk/client/parameters/call_context_param.py new file mode 100644 index 0000000..4b15546 --- /dev/null +++ b/frostfs_sdk/client/parameters/call_context_param.py @@ -0,0 +1,21 @@ +from dataclasses import dataclass, field +from enum import Enum +from typing import Optional + + +DEFAULT_GRPC_TIMEOUT = 5 + + +class TimeUnit(Enum): + MINUTES = "MINUTES" + SECONDS = "SECONDS" + MILLISECONDS = "MILLISECONDS" + +@dataclass +class CallContextParam: + timeout: int = DEFAULT_GRPC_TIMEOUT + time_unit: TimeUnit = TimeUnit.SECONDS + + @classmethod + def default(cls): + return cls() diff --git a/frostfs_sdk/client/parameters/container_create.py b/frostfs_sdk/client/parameters/container_param.py similarity index 57% rename from frostfs_sdk/client/parameters/container_create.py rename to frostfs_sdk/client/parameters/container_param.py index 827c5bd..ed4f987 100644 --- a/frostfs_sdk/client/parameters/container_create.py +++ b/frostfs_sdk/client/parameters/container_param.py @@ -2,17 +2,17 @@ from dataclasses import dataclass, field from typing import Optional, Dict from frostfs_sdk.models.dto.container import Container -from frostfs_sdk.models.dto.session_token import SessionToken -from frostfs_sdk.client.parameters.wait import PrmWait +from frostfs_sdk.client.services.session import SessionToken +from frostfs_sdk.client.parameters.wait_param import WaitParam @dataclass(frozen=True) -class PrmContainerCreate: +class ContainerCreateParam: container: Container - wait_params: Optional[PrmWait] = None + wait_params: Optional[WaitParam] = None session_token: Optional[SessionToken] = None x_headers: Dict[str, str] = field(default_factory=dict) def __post_init__(self): if self.wait_params is None: - object.__setattr__(self, 'wait_params', PrmWait()) + object.__setattr__(self, 'wait_params', WaitParam()) diff --git a/frostfs_sdk/client/parameters/wait.py b/frostfs_sdk/client/parameters/wait_param.py similarity index 97% rename from frostfs_sdk/client/parameters/wait.py rename to frostfs_sdk/client/parameters/wait_param.py index a77e561..e05fcb7 100644 --- a/frostfs_sdk/client/parameters/wait.py +++ b/frostfs_sdk/client/parameters/wait_param.py @@ -4,7 +4,7 @@ from typing import Optional @dataclass(frozen=True) -class PrmWait: +class WaitParam: DEFAULT_TIMEOUT: timedelta = field(default=timedelta(seconds=120), init=False) DEFAULT_POLL_INTERVAL: timedelta = field(default=timedelta(seconds=5), init=False) diff --git a/frostfs_sdk/client/services/container.py b/frostfs_sdk/client/services/container.py index 694dc9f..62fadbd 100644 --- a/frostfs_sdk/client/services/container.py +++ b/frostfs_sdk/client/services/container.py @@ -1,30 +1,41 @@ # implementation Conainer methods from frostfs_sdk.client.models.client_environment import ClientEnvironment +from frostfs_sdk.client.services.context_accessor import ContextAccessor +from frostfs_sdk.client.parameters.container_param import ContainerCreateParam +from frostfs_sdk.client.parameters.call_context_param import CallContextParam from frostfs_sdk.cryptography.signer import Signer -from frostfs_sdk.models.dto.container import ContainerId -import protos.models.container.service_pb2_grpc as service_pb2_grpc_container -import protos.models.container.service_pb2 as service_pb2_container - -from frostfs_sdk.client.parameters.container_create import PrmContainerCreate +from frostfs_sdk.models.dto.container import ContainerId, Container from frostfs_sdk.models.mappers.container_mapper import ContainerMapper +from frostfs_sdk.models.mappers.owner_id_mapper import OwnerIdMapper +from frostfs_sdk.models.mappers.version_mapper import VersionMapper +from frostfs_sdk.protos.models.container import service_pb2 as service_pb2_container +from frostfs_sdk.protos.models.container import service_pb2_grpc as service_pb2_grpc_container - -class ContainerClient: +class ContainerClient(ContextAccessor): def __init__(self, client_environment: ClientEnvironment): + super().__init__(client_environment) self.container_stub = service_pb2_grpc_container.ContainerServiceStub(client_environment.channel) self.ecdsa = client_environment.ecdsa - def create(self, prm_container_create: PrmContainerCreate) -> ContainerId: - request = self.create_put_request(prm_container_create) + def create_container(self, container_create_param: ContainerCreateParam, ctx: CallContextParam) -> ContainerId: + request = self._create_put_request(container_create_param, ctx) response: service_pb2_container.PutResponse = self.container_stub.Put(request) - return ContainerId(value=response.body.container_id) + return ContainerId(value=response.body.container_id.value) - def create_put_request(self, prm: PrmContainerCreate): - grpc_container=ContainerMapper().to_grpc_message(prm.container) + def _create_put_request(self, param: ContainerCreateParam, ctx: CallContextParam) -> service_pb2_container.PutRequest: + """ + Creates a PUT request for creating a container. + """ + grpc_container=ContainerMapper().to_grpc_message(param.container) + if not grpc_container.owner_id: + grpc_container.owner_id = OwnerIdMapper.to_grpc_message(self.get_context.owner_id) + if not grpc_container.version: + grpc_container.version = VersionMapper.to_grpc_message(self.get_context.version) + body = service_pb2_container.PutRequest.Body( container=grpc_container, - signature=Signer.sign_rfc6979(self.ecdsa.private_key, grpc_container) + signature=Signer.sign_message_rfc_6979(self.get_context.ecdsa, grpc_container) ) request = service_pb2_container.PutRequest(body=body) diff --git a/frostfs_sdk/client/services/context_accessor.py b/frostfs_sdk/client/services/context_accessor.py new file mode 100644 index 0000000..913bcaa --- /dev/null +++ b/frostfs_sdk/client/services/context_accessor.py @@ -0,0 +1,12 @@ +from frostfs_sdk.client.models.client_environment import ClientEnvironment + +class ContextAccessor: + def __init__(self, context: ClientEnvironment): + """ + Initializes a ContextAccessor with a given ClientEnvironment. + """ + self.context: ClientEnvironment = context + + @property + def get_context(self) -> ClientEnvironment: + return self.context diff --git a/frostfs_sdk/client/services/session.py b/frostfs_sdk/client/services/session.py new file mode 100644 index 0000000..5b3ec5a --- /dev/null +++ b/frostfs_sdk/client/services/session.py @@ -0,0 +1,37 @@ +from dataclasses import dataclass + + +@dataclass(frozen=True) +class SessionToken: + token: bytes + + +class SessionCache: + def __init__(self, session_expiration_duration): + self.cache = {} + self.token_duration = session_expiration_duration + self.current_epoch = 0 + + + def contains(self, key): + return key in self.cache + + def try_get_value(self, key): + if not key: + return None + return self.cache.get(key) + + + def set_value(self, key, value): + if key is not None: + self.cache[key] = value + + def delete_by_prefix(self, prefix): + # Collect keys to avoid modifying dictionary during iteration + keys_to_delete = [key for key in self.cache if key.startswith(prefix)] + for key in keys_to_delete: + del self.cache[key] + + @staticmethod + def form_cache_key(address: str, key: str): + return address + key diff --git a/frostfs_sdk/client/utils/message_helper.py b/frostfs_sdk/client/utils/message_helper.py new file mode 100644 index 0000000..ca1a12a --- /dev/null +++ b/frostfs_sdk/client/utils/message_helper.py @@ -0,0 +1,30 @@ +from typing import Any +from google.protobuf.message import Message + +class MessageHelper: + @staticmethod + def get_field(message: Message, field_name: str): + """ + Retrieves the value of a field from a Protobuf message. + + :param message: A Protobuf Message object. + :param field_name: The name of the field to retrieve. + :return: The value of the specified field. + :raises ValueError: If the input parameters are invalid. + """ + if not message or not field_name or not isinstance(field_name, str) or not field_name.strip(): + raise ValueError("Some parameter is missing") + + descriptor = message.DESCRIPTOR + field_descriptor = descriptor.fields[field_name] + if not field_descriptor: + raise ValueError(f"Field '{field_name}' not found in message descriptor") + + return message.GetField(field_descriptor[field_name]) + + @staticmethod + def set_field(message: Message, field_name: str, value: Any) -> None: + if message is None or not field_name.strip() or value is None: + raise ValueError("Some parameter is missing") + + message.SetField(message.DESCRIPTOR.fields[field_name], value) diff --git a/frostfs_sdk/cryptography/key_extension.py b/frostfs_sdk/cryptography/key_extension.py index 5844e89..a6e5f13 100644 --- a/frostfs_sdk/cryptography/key_extension.py +++ b/frostfs_sdk/cryptography/key_extension.py @@ -42,3 +42,11 @@ class KeyExtension: if len(sequence_symbols) == 0 or sequence_symbols is None: raise ValueError(f"Empty sequence symbols of key: {sequence_symbols}") return False + + @staticmethod + def get_hex_string(value): + if value is None or len(value) == 0: + raise ValueError("Input parameter is missing") + + # Convert byte array to hexadecimal string + return f"{int.from_bytes(value, byteorder='big'):0{len(value) * 2}x}" diff --git a/frostfs_sdk/cryptography/signer.py b/frostfs_sdk/cryptography/signer.py index 81c3df4..72d849f 100644 --- a/frostfs_sdk/cryptography/signer.py +++ b/frostfs_sdk/cryptography/signer.py @@ -1,8 +1,20 @@ import ecdsa from hashlib import sha256, sha512 +from google.protobuf.message import Message + +from frostfs_sdk.client.models.ecdsa_model import ECDSA +from frostfs_sdk.protos.models.refs.types_pb2 import SignatureRFC6979, Signature + class Signer: + @staticmethod + def sign_message_rfc_6979(key: ECDSA, message: Message) -> SignatureRFC6979: + return SignatureRFC6979( + key=key.public_key, + sign=Signer.sign_rfc6979(key.private_key, message.SerializeToString()) + ) + @staticmethod def sign_rfc6979(private_key: bytes, message: bytes) -> bytes: if len(private_key) == 0 or private_key is None: @@ -14,6 +26,13 @@ class Signer: return signature + @staticmethod + def sign_message(key: ECDSA, message: Message) -> SignatureRFC6979: + return SignatureRFC6979( + key=key.public_key, + sign=Signer.sign(key.private_key, message.SerializeToString()) + ) + @staticmethod def sign(private_key: bytes, message: bytes) -> bytes: if len(private_key) == 0 or private_key is None: @@ -26,3 +45,10 @@ class Signer: signature_with_marker = bytes([0x04]) + signature return signature_with_marker + + @staticmethod + def _sign_message_part(key: ECDSA, data: Message) -> Signature: + return Signature( + key=key.public_key, + sign=Signature.sign(key.private_key, data.SerializeToString()) + ) diff --git a/frostfs_sdk/models/__init__.py b/frostfs_sdk/models/__init__.py new file mode 100644 index 0000000..aa4cd69 --- /dev/null +++ b/frostfs_sdk/models/__init__.py @@ -0,0 +1,9 @@ +from frostfs_sdk.models.dto.container import Container, ContainerId +from frostfs_sdk.models.dto.filter import Filter +from frostfs_sdk.models.dto.placement_policy import PlacementPolicy +from frostfs_sdk.models.dto.replica import Replica +from frostfs_sdk.models.dto.selector import Selector + +from frostfs_sdk.models.enums.basic_acl import BasicAcl +from frostfs_sdk.models.enums.filter_operation import FilterOperation +from frostfs_sdk.models.enums.selector_clause import SelectorClause diff --git a/frostfs_sdk/models/dto/container.py b/frostfs_sdk/models/dto/container.py index a21f8d8..e369b35 100644 --- a/frostfs_sdk/models/dto/container.py +++ b/frostfs_sdk/models/dto/container.py @@ -2,6 +2,8 @@ from dataclasses import dataclass, field from typing import Dict, Optional import uuid +from frostfs_sdk.models.dto.owner_id import OwnerId +from frostfs_sdk.models.dto.version import Version from frostfs_sdk.models.enums.basic_acl import BasicAcl from frostfs_sdk.models.dto.placement_policy import PlacementPolicy @@ -9,15 +11,17 @@ from frostfs_sdk.models.dto.placement_policy import PlacementPolicy @dataclass class Container: - basicAcl: BasicAcl + # basicAcl: BasicAcl # TODO: will remove it? placementPolicy: PlacementPolicy nonce: uuid.UUID = field(default_factory=uuid.uuid4) - version: Optional[str] = None + version: Optional[Version] = None + owner_id: Optional[OwnerId] = None attributes: Dict[str, str] = field(default_factory=dict) - def __init__(self, basicAcl: BasicAcl, placementPolicy: PlacementPolicy): - self.basicAcl = basicAcl + def __init__(self, placementPolicy: PlacementPolicy): + self.nonce = uuid.uuid4() self.placementPolicy = placementPolicy + self.attributes = {} @dataclass diff --git a/frostfs_sdk/models/dto/owner_id.py b/frostfs_sdk/models/dto/owner_id.py new file mode 100644 index 0000000..47283a4 --- /dev/null +++ b/frostfs_sdk/models/dto/owner_id.py @@ -0,0 +1,20 @@ +from base58 import b58decode +from dataclasses import dataclass + +@dataclass(frozen=True) +class OwnerId: + value: str + + def __post_init__(self): + if not self.value or self.value.strip() == "": + raise ValueError(f"{self.__class__.__name__} value is not present") + + def to_hash(self) -> bytes: + """Decodes the Base58-encoded value into a byte array.""" + try: + return b58decode(self.value) + except Exception as e: + raise ValueError(f"Failed to decode Base58 value: {self.value}") from e + + def __str__(self) -> str: + return self.value diff --git a/frostfs_sdk/models/dto/placement_policy.py b/frostfs_sdk/models/dto/placement_policy.py index a0234e0..f3c95f0 100644 --- a/frostfs_sdk/models/dto/placement_policy.py +++ b/frostfs_sdk/models/dto/placement_policy.py @@ -2,8 +2,13 @@ from dataclasses import dataclass from typing import List from frostfs_sdk.models.dto.replica import Replica +from frostfs_sdk.models.dto.selector import Selector +from frostfs_sdk.models.dto.filter import Filter @dataclass class PlacementPolicy: replicas: List[Replica] unique: bool + backup_factory: int + filters: List[Filter] = None + selectors: List[Selector] = None diff --git a/frostfs_sdk/models/dto/session_token.py b/frostfs_sdk/models/dto/session_token.py deleted file mode 100644 index 3b4aa76..0000000 --- a/frostfs_sdk/models/dto/session_token.py +++ /dev/null @@ -1,5 +0,0 @@ -from dataclasses import dataclass - -@dataclass(frozen=True) -class SessionToken: - token: bytes diff --git a/frostfs_sdk/models/dto/version.py b/frostfs_sdk/models/dto/version.py new file mode 100644 index 0000000..b492109 --- /dev/null +++ b/frostfs_sdk/models/dto/version.py @@ -0,0 +1,19 @@ +from dataclasses import dataclass + +DEFAULT_MAJOR_VERSION = 2 +DEFAULT_MINOR_VERSION = 13 + + +@dataclass(frozen=True) +class Version: + major: int = DEFAULT_MAJOR_VERSION + minor: int = DEFAULT_MINOR_VERSION + + def __str__(self) -> str: + return f"v{self.major}.{self.minor}" + + def is_supported(self, other): + if not isinstance(other, Version): + return False + return self.major == other.major + diff --git a/frostfs_sdk/models/enums/filter_operation.py b/frostfs_sdk/models/enums/filter_operation.py index 68a1958..e447962 100644 --- a/frostfs_sdk/models/enums/filter_operation.py +++ b/frostfs_sdk/models/enums/filter_operation.py @@ -39,4 +39,4 @@ class FilterOperation: Returns: Corresponding FilterOperation instance """ - return cls._value_map.get(value) \ No newline at end of file + return cls._value_map.get(value) diff --git a/frostfs_sdk/models/enums/selector_clause.py b/frostfs_sdk/models/enums/selector_clause.py index d294887..880a0f5 100644 --- a/frostfs_sdk/models/enums/selector_clause.py +++ b/frostfs_sdk/models/enums/selector_clause.py @@ -1,26 +1,14 @@ -class SelectorClause: - """ - Enum for selector clauses with integer value mapping - """ +from enum import Enum, unique + +@unique +class SelectorClause(Enum): CLAUSE_UNSPECIFIED = 0 SAME = 1 DISTINCT = 2 - _value_map = { - 0: CLAUSE_UNSPECIFIED, - 1: SAME, - 2: DISTINCT - } - @classmethod - def get(cls, value: int) -> 'SelectorClause': - """ - Get enum instance by integer value - - Args: - value: Integer value of the clause - - Returns: - Corresponding SelectorClause instance - """ - return cls._value_map.get(value) + def get(cls, value: int): + try: + return cls(value) + except ValueError: + raise KeyError(f"Unknown enum value: {value}") diff --git a/frostfs_sdk/models/mappers/container_mapper.py b/frostfs_sdk/models/mappers/container_mapper.py index 637e493..1992c16 100644 --- a/frostfs_sdk/models/mappers/container_mapper.py +++ b/frostfs_sdk/models/mappers/container_mapper.py @@ -1,8 +1,13 @@ -from typing import ByteString, Optional +from typing import Optional + +import grpc from frostfs_sdk.models.mappers.placement_policy_mapper import PlacementPolicyMapper -import protos.models.container.types_pb2 as types_pb2_container +from frostfs_sdk.models.mappers.owner_id_mapper import OwnerIdMapper +from frostfs_sdk.models.mappers.version_mapper import VersionMapper +from frostfs_sdk.models.mappers.uuid_extension import UuidExtension from frostfs_sdk.models.dto.container import Container +from frostfs_sdk.protos.models.container import types_pb2 as types_pb2_container class ContainerMapper: @@ -19,24 +24,23 @@ class ContainerMapper: """ if not container: return None - + attributes = [ types_pb2_container.Container.Attribute(key=k, value=v) for k, v in container.attributes.items() ] - + grpc_container = types_pb2_container.Container( - # nonce=ByteString.copy(container.nonce), + nonce=container.nonce.bytes, placement_policy=PlacementPolicyMapper.to_grpc_message(container.placementPolicy), attributes=attributes - ) - # if container.owner_id: - # grpc_container.owner_id = OwnerIdMapper.to_grpc_message(container.owner_id) + if container.owner_id: + grpc_container.owner_id = OwnerIdMapper.to_grpc_message(container.owner_id) - # if container.version: - # grpc_container.version = VersionMapper.to_grpc_message(container.version) + if container.version: + grpc_container.version = VersionMapper.to_grpc_message(container.version) return grpc_container @@ -57,9 +61,9 @@ class ContainerMapper: attributes = {attr.key: attr.value for attr in container_grpc.attributes} return Container( - # nonce=UuidUtils.as_uuid(container_grpc.nonce.to_bytes()), + nonce=UuidExtension.to_uuid(container_grpc.nonce), placement_policy=PlacementPolicyMapper.to_model(container_grpc.placement_policy), - # version=VersionMapper.to_model(container_grpc.version), - # owner_id=OwnerIdMapper.to_model(container_grpc.owner_id), + version=VersionMapper.to_model(container_grpc.version), + owner_id=OwnerIdMapper.to_model(container_grpc.owner_id), attributes=attributes ) diff --git a/frostfs_sdk/models/mappers/filter_mapper.py b/frostfs_sdk/models/mappers/filter_mapper.py index 2848a32..a3f2f11 100644 --- a/frostfs_sdk/models/mappers/filter_mapper.py +++ b/frostfs_sdk/models/mappers/filter_mapper.py @@ -2,7 +2,7 @@ from typing import List, Optional from frostfs_sdk.models.enums.filter_operation import FilterOperation from frostfs_sdk.models.dto.filter import Filter -import protos.models.netmap.types_pb2 as types_pb2_netmap +from frostfs_sdk.protos.models.netmap import types_pb2 as types_pb2_netmap class FilterMapper: diff --git a/frostfs_sdk/models/mappers/owner_id_mapper.py b/frostfs_sdk/models/mappers/owner_id_mapper.py index e69de29..8606673 100644 --- a/frostfs_sdk/models/mappers/owner_id_mapper.py +++ b/frostfs_sdk/models/mappers/owner_id_mapper.py @@ -0,0 +1,32 @@ +from base58 import b58encode + +from frostfs_sdk.models.dto.owner_id import OwnerId +from frostfs_sdk.protos.models.refs import types_pb2 as types_pb2_refs + +class OwnerIdMapper: + @staticmethod + def to_grpc_message(owner_id: OwnerId) -> types_pb2_refs.OwnerID: + """ + Converts OwnerId DTO to gRPC message + """ + if not owner_id: + return None + + return types_pb2_refs.OwnerID( + value=owner_id.to_hash(), + ) + + @staticmethod + def to_model(owner_id_grpc: types_pb2_refs.OwnerID) -> OwnerId: + """ + Converts gRPC message to OwnerId DTO + """ + if not owner_id_grpc or owner_id_grpc.ByteSize() == 0: + return None + + try: + return OwnerId( + value=b58encode(owner_id_grpc.value).decode('utf-8') + ) + except Exception as e: + raise ValueError(f"Failed to encode Base58 value: {owner_id_grpc.value}") diff --git a/frostfs_sdk/models/mappers/placement_policy_mapper.py b/frostfs_sdk/models/mappers/placement_policy_mapper.py index 98cfedc..66a6d2d 100644 --- a/frostfs_sdk/models/mappers/placement_policy_mapper.py +++ b/frostfs_sdk/models/mappers/placement_policy_mapper.py @@ -1,7 +1,10 @@ from typing import Optional + from frostfs_sdk.models.mappers.filter_mapper import FilterMapper -import protos.models.netmap.types_pb2 as types_pb2_netmap +from frostfs_sdk.models.mappers.selector_mapper import SelectorMapper +from frostfs_sdk.models.mappers.replica_mapper import ReplicaMapper from frostfs_sdk.models.dto.placement_policy import PlacementPolicy +from frostfs_sdk.protos.models.netmap import types_pb2 as types_pb2_netmap class PlacementPolicyMapper: @@ -21,10 +24,10 @@ class PlacementPolicyMapper: return types_pb2_netmap.PlacementPolicy( unique=policy.unique, - container_backup_factor=policy.backup_factor, + container_backup_factor=policy.backup_factory, filters=FilterMapper.to_grpc_messages(policy.filters), - # selectors=SelectorMapper.to_grpc_messages(policy.selectors), - # replicas=ReplicaMapper.to_grpc_messages(policy.replicas) + selectors=SelectorMapper.to_grpc_messages(policy.selectors), + replicas=ReplicaMapper.to_grpc_messages(policy.replicas) ) @staticmethod @@ -42,9 +45,9 @@ class PlacementPolicyMapper: return None return PlacementPolicy( - # replicas=ReplicaMapper.to_models(policy_grpc.replicas), + replicas=ReplicaMapper.to_models(policy_grpc.replicas), unique=policy_grpc.unique, - backup_factor=policy_grpc.container_backup_factor, + backup_factory=policy_grpc.container_backup_factor, filters=FilterMapper.to_models(policy_grpc.filters), - # selectors=SelectorMapper.to_models(policy_grpc.selectors) + selectors=SelectorMapper.to_models(policy_grpc.selectors) ) diff --git a/frostfs_sdk/models/mappers/replica_mapper.py b/frostfs_sdk/models/mappers/replica_mapper.py index e69de29..8823ca0 100644 --- a/frostfs_sdk/models/mappers/replica_mapper.py +++ b/frostfs_sdk/models/mappers/replica_mapper.py @@ -0,0 +1,56 @@ +from typing import List, Optional + +from frostfs_sdk.models.dto.replica import Replica +from frostfs_sdk.protos.models.netmap import types_pb2 as types_pb2_netmap + + +class ReplicaMapper: + @staticmethod + def to_grpc_messages(replicas: List[Replica]) -> Optional[List[types_pb2_netmap.Replica]]: + if not replicas: + return None + return [ReplicaMapper.to_grpc_message(selector) for selector in replicas] + + @staticmethod + def to_grpc_message(replica: Replica) -> Optional[types_pb2_netmap.Replica]: + """ + Converts Replica DTO to gRPC message + + Args: + replice: Replica DTO object + + Returns: + gRPC Replica message + """ + if not replica: + return None + + return types_pb2_netmap.Replica( + count=replica.count, + selector=replica.selector + ) + + @staticmethod + def to_models(grpc_replicas: List[types_pb2_netmap.Replica]) -> Optional[List[Replica]]: + if not grpc_replicas: + return None + return [ReplicaMapper.to_model(grpc_replica) for grpc_replica in grpc_replicas] + + @staticmethod + def to_model(grpc_replica: types_pb2_netmap.Replica) -> Optional[Replica]: + """ + Converts gRPC message to Replica DTO + + Args: + grpc_replica: gRPC Replica message + + Returns: + Replica DTO object + """ + if not grpc_replica or grpc_replica.ByteSize() == 0: + return None + + return Replica( + count=grpc_replica.count, + selectors=grpc_replica.selector + ) diff --git a/frostfs_sdk/models/mappers/selector_mapper.py b/frostfs_sdk/models/mappers/selector_mapper.py index e69de29..d536f3c 100644 --- a/frostfs_sdk/models/mappers/selector_mapper.py +++ b/frostfs_sdk/models/mappers/selector_mapper.py @@ -0,0 +1,73 @@ +from typing import List, Optional + +from frostfs_sdk.models.dto.selector import Selector +from frostfs_sdk.models.enums.selector_clause import SelectorClause +from frostfs_sdk.protos.models.netmap import types_pb2 as types_pb2_netmap + + +class SelectorMapper: + @staticmethod + def to_grpc_messages(selectors: List[Selector]) -> Optional[List[types_pb2_netmap.Selector]]: + if not selectors: + return None + return [SelectorMapper.to_grpc_message(selector) for selector in selectors] + + @staticmethod + def to_grpc_message(selector: Selector) -> Optional[types_pb2_netmap.Selector]: + """ + Converts Selector DTO to gRPC message + + Args: + selector: Selector DTO object + + Returns: + gRPC Selector message + """ + if not selector: + return None + + clause_grpc = types_pb2_netmap.Clause(selector.clause.value) + if clause_grpc is None: + raise ValueError(f"Unknown enum value: {selector.clause.name} for {types_pb2_netmap.Clause.__name__}") + + + return types_pb2_netmap.Selector( + name=selector.name, + count=selector.count, + clause=clause_grpc, + attribute=selector.attribute, + filter=selector.filter + ) + + @staticmethod + def to_models(grpc_selectors: List[types_pb2_netmap.Selector]) -> Optional[List[Selector]]: + if not grpc_selectors: + return None + return [SelectorMapper.to_model(grpc_selector) for grpc_selector in grpc_selectors] + + @staticmethod + def to_model(selector_grpc: types_pb2_netmap.Selector) -> Optional[Selector]: + """ + Converts gRPC message to Selector DTO + + Args: + selector_grpc: gRPC Selector message + + Returns: + Selector DTO object + """ + if not selector_grpc or selector_grpc.ByteSize() == 0: + return None + + clause = SelectorClause.get(selector_grpc.clause) + if clause is None: + raise ValueError(f"Unknown enum value: {selector_grpc.clause} for {SelectorClause.__name__}") + + + return Selector( + name=selector_grpc.name, + count=selector_grpc.count, + clause=clause, + attribute=selector_grpc.attribute, + filter=selector_grpc.filter + ) diff --git a/frostfs_sdk/models/mappers/uuid_extension.py b/frostfs_sdk/models/mappers/uuid_extension.py new file mode 100644 index 0000000..e1bdbdf --- /dev/null +++ b/frostfs_sdk/models/mappers/uuid_extension.py @@ -0,0 +1,31 @@ +import uuid +from typing import Optional + +UUID_BYTE_ARRAY_LENGTH = 16 + + +class UuidExtension: + @staticmethod + def to_uuid(bytes_: Optional[bytes]) -> uuid.UUID: + """ + Converts a byte array into a UUID object. + """ + if bytes_ is None or len(bytes_) != UUID_BYTE_ARRAY_LENGTH: + raise ValueError(f"Wrong UUID size: expected {UUID_BYTE_ARRAY_LENGTH} bytes, got {len(bytes_)}") + + # Unpack the byte array into two 64-bit integers + most_sig_bits = int.from_bytes(bytes_[:8], byteorder='big', signed=False) + least_sig_bits = int.from_bytes(bytes_[8:], byteorder='big', signed=False) + + return uuid.UUID(int=(most_sig_bits << 64) | least_sig_bits) + + @staticmethod + def to_bytes(uuid_: Optional[uuid.UUID]) -> bytes: + """ + Converts a UUID object into a byte array. + """ + if uuid_ is None: + raise ValueError(f"Input parameter is missing: {uuid.UUID.__name__}") + + # Pack the UUID into a 16-byte array + return (uuid_.int >> 64).to_bytes(8, byteorder='big') + (uuid_.int & 0xFFFFFFFFFFFFFFFF).to_bytes(8, byteorder='big') diff --git a/frostfs_sdk/models/mappers/version_mapper.py b/frostfs_sdk/models/mappers/version_mapper.py new file mode 100644 index 0000000..6eedcda --- /dev/null +++ b/frostfs_sdk/models/mappers/version_mapper.py @@ -0,0 +1,31 @@ +from typing import Optional +from frostfs_sdk.models.dto.version import Version +from frostfs_sdk.protos.models.refs import types_pb2 as types_pb2_refs + + +class VersionMapper: + @staticmethod + def to_grpc_message(version: Optional[Version]) -> Optional[types_pb2_refs.Version]: + """ + Converts a Version object to a gRPC Version message. + """ + if version is None: + return None + + return types_pb2_refs.Version( + major=version.major, + minor=version.minor + ) + + @staticmethod + def to_model(grpc_version: Optional[types_pb2_refs.Version]) -> Optional[Version]: + """ + Converts a gRPC Version message to a Version object. + """ + if grpc_version is None or grpc_version.ByteSize() == 0: + return None + + return Version( + major=grpc_version.major, + minor=grpc_version.minor + ) diff --git a/generate_proto.sh b/generate_proto.sh index 00a4046..6695de4 100755 --- a/generate_proto.sh +++ b/generate_proto.sh @@ -6,7 +6,7 @@ # Define directories -PROTOS_DIR="./protos" +PROTOS_DIR="./frostfs_sdk/protos" SOURCE_DIR="${PROTOS_DIR}/source" MODELS_DIR="${PROTOS_DIR}/models" REPO_FROSTFS_API_PROTOS="https://git.frostfs.info/TrueCloudLab/frostfs-api.git" diff --git a/tests/client/test_create_container.py b/tests/client/test_create_container.py index 0395d96..0876a38 100644 --- a/tests/client/test_create_container.py +++ b/tests/client/test_create_container.py @@ -1,14 +1,15 @@ -# import pytest +import pytest -# from frostfs_sdk.client.frostfs_client import FrostfsClient -# from frostfs_sdk.client.parameters.container_create import PrmContainerCreate -# from frostfs_sdk.models.dto.container import ContainerId -# from tests.helpers.models import Helpers +from frostfs_sdk import FrostfsClient, ContainerCreateParam, ContainerId +from frostfs_sdk.client.parameters.call_context_param import CallContextParam, TimeUnit +from tests.helpers.models import Helpers -# @pytest.mark.container -# class TestContainer: -# def test_create_container(self, default_frostfs_client: FrostfsClient, helpers: Helpers): -# req_body: PrmContainerCreate = helpers.container.create_params_container_create() -# cid: ContainerId = default_frostfs_client.container.create(req_body) -# print(cid.value) +@pytest.mark.container +class TestContainer: + def test_create_container(self, default_frostfs_client: FrostfsClient, helpers: Helpers): + call_context = CallContextParam(timeout=1, time_unit=TimeUnit.MINUTES) + req_body: ContainerCreateParam = helpers.container.create_params_container_create() + + cid: ContainerId = default_frostfs_client.container.create_container(req_body, call_context) + print(cid.value) diff --git a/tests/conftest.py b/tests/conftest.py index 9dcd47b..3b6d352 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,6 @@ import pytest -# from frostfs_sdk.client.frostfs_client import FrostfsClient -# from frostfs_sdk.client.models.client_settings import ClientSettings +from frostfs_sdk import ClientSettings, FrostfsClient from tests.helpers.models import ClientCryptograpy, Helpers @@ -10,13 +9,14 @@ def client_cryptography(): return ClientCryptograpy() -# @pytest.fixture(scope="session") -# def default_frostfs_client(): -# client_settings = ClientSettings( -# wif="", -# address="10.78.128.25:8080" -# ) -# return FrostfsClient(client_settings) +@pytest.fixture(scope="session") +def default_frostfs_client(): + client_settings = ClientSettings( + wif="KxnEZ7FsPgKMdL9PYt9vsDkXiSw6qP9J8dpR4eVMsDpJyJxcYpve", + address="10.78.130.201:8080", + # address="localhost:8080" + ) + return FrostfsClient(client_settings) @pytest.fixture(scope="session") diff --git a/tests/helpers/params_container.py b/tests/helpers/params_container.py index 3817246..93651d9 100644 --- a/tests/helpers/params_container.py +++ b/tests/helpers/params_container.py @@ -1,22 +1,16 @@ -from frostfs_sdk.client.parameters.container_create import PrmContainerCreate -from frostfs_sdk.client.parameters.wait import PrmWait -from frostfs_sdk.models.dto.container import Container -from frostfs_sdk.models.dto.placement_policy import PlacementPolicy -from frostfs_sdk.models.dto.replica import Replica -from frostfs_sdk.models.dto.session_token import SessionToken -from frostfs_sdk.models.enums.basic_acl import BasicAcl +from frostfs_sdk import ContainerCreateParam, WaitParam, Container, PlacementPolicy, Replica, BasicAcl class ParamsContainerHelper: def create_params_container_create(self): - req_container_create = PrmContainerCreate( + req_container_create = ContainerCreateParam( container=Container( - basicAcl=BasicAcl.PUBLIC_RW, placementPolicy=PlacementPolicy( replicas=[Replica(count=1)], - unique=True + unique=True, + backup_factory=0 ) ), - wait_params=PrmWait() + wait_params=WaitParam() ) return req_container_create