[#3] Added generate proto script create container method
Signed-off-by: Ilyas Niyazov <i.niyazov@yadro.com>
This commit is contained in:
parent
19282f13cc
commit
297e107b10
52 changed files with 1380 additions and 74 deletions
11
.gitignore
vendored
11
.gitignore
vendored
|
@ -1,4 +1,15 @@
|
||||||
# ---> Python
|
# ---> Python
|
||||||
|
# ignore IDE files
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
venv.*
|
||||||
|
.repo
|
||||||
|
.git
|
||||||
|
.git_shadow
|
||||||
|
|
||||||
|
# ignore folders
|
||||||
|
protos
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
|
|
@ -1,44 +0,0 @@
|
||||||
import base58
|
|
||||||
import ecdsa
|
|
||||||
|
|
||||||
|
|
||||||
class KeyExtension:
|
|
||||||
def get_private_key_from_wif(self, wif: str) -> bytes:
|
|
||||||
"""
|
|
||||||
Converts a WIF private key to a byte array.
|
|
||||||
|
|
||||||
:param wif: WIF private key in string format.
|
|
||||||
:return: Private key in byte format (32 bytes).
|
|
||||||
:raises ValueError: If the WIF key is incorrect.
|
|
||||||
"""
|
|
||||||
assert not self.is_empty(wif)
|
|
||||||
|
|
||||||
decoded = base58.b58decode_check(wif)
|
|
||||||
if len(decoded) != 34 or decoded[0] != 0x80 or decoded[-1] != 0x01:
|
|
||||||
raise ValueError("Incorrect WIF private key")
|
|
||||||
|
|
||||||
private_key = decoded[1:-1]
|
|
||||||
return private_key
|
|
||||||
|
|
||||||
def get_public_key(self, private_key: bytes) -> bytes:
|
|
||||||
"""
|
|
||||||
Extract public key from Private key
|
|
||||||
|
|
||||||
:param private_key: Private key in byte format (32 bytes).
|
|
||||||
:return: compressed public key in byte format (33 bytes).
|
|
||||||
:raises ValueError: If the private_key key is empty or null.
|
|
||||||
"""
|
|
||||||
assert not self.is_empty(private_key)
|
|
||||||
|
|
||||||
if len(private_key) != 32:
|
|
||||||
raise ValueError(f"Incorrect len of private key, Expected: 32, Actual: {len(private_key)}")
|
|
||||||
|
|
||||||
public_key = ecdsa.SigningKey.from_string(private_key, curve=ecdsa.NIST256p).get_verifying_key()
|
|
||||||
compressed_public_key = public_key.to_string("compressed")
|
|
||||||
return compressed_public_key
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_empty(sequence_symbols: bytes | str):
|
|
||||||
if len(sequence_symbols) == 0 or sequence_symbols is None:
|
|
||||||
raise ValueError(f"Empty sequence symbols of key: {sequence_symbols}")
|
|
||||||
return False
|
|
|
@ -1,26 +0,0 @@
|
||||||
import ecdsa
|
|
||||||
from hashlib import sha256, sha512
|
|
||||||
|
|
||||||
|
|
||||||
class Signer:
|
|
||||||
def sign_rfc6979(self, private_key: bytes, message: bytes) -> bytes:
|
|
||||||
if len(private_key) == 0 or private_key is None:
|
|
||||||
raise ValueError(f"Incorrect private_key: {private_key}")
|
|
||||||
|
|
||||||
sk = ecdsa.SigningKey.from_string(private_key, curve=ecdsa.NIST256p, hashfunc=sha256)
|
|
||||||
|
|
||||||
signature = sk.sign_deterministic(message)
|
|
||||||
|
|
||||||
return signature
|
|
||||||
|
|
||||||
def sign(self, private_key: bytes, message: bytes) -> bytes:
|
|
||||||
if len(private_key) == 0 or private_key is None:
|
|
||||||
raise ValueError(f"Incorrect private key: {private_key}")
|
|
||||||
|
|
||||||
sk = ecdsa.SigningKey.from_string(private_key, curve=ecdsa.NIST256p, hashfunc=sha512)
|
|
||||||
signature = sk.sign(message)
|
|
||||||
|
|
||||||
# the first byte indicates the node version marker
|
|
||||||
signature_with_marker = bytes([0x04]) + signature
|
|
||||||
|
|
||||||
return signature_with_marker
|
|
2
frostfs_sdk/__init__.py
Normal file
2
frostfs_sdk/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
from frostfs_sdk.client import *
|
||||||
|
from frostfs_sdk.models import *
|
10
frostfs_sdk/client/__init__.py
Normal file
10
frostfs_sdk/client/__init__.py
Normal file
|
@ -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
|
33
frostfs_sdk/client/frostfs_client.py
Normal file
33
frostfs_sdk/client/frostfs_client.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# Create channel and Stubs
|
||||||
|
import grpc
|
||||||
|
|
||||||
|
from frostfs_sdk.client.utils.session_cache 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_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(client_settings.address)
|
||||||
|
self.ecdsa: ECDSA = ECDSA(wif=client_settings.wif)
|
||||||
|
|
||||||
|
client_environment = ClientEnvironment(self.ecdsa, self.channel, client_settings.address, Version(), SessionCache(0))
|
||||||
|
self.container = ContainerClient(client_environment)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.channel.close()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
import frostfs_sdk
|
||||||
|
|
||||||
|
WIF = "L5XNVUzPnma6m4mPrWEN6CcTscJERcfX3yvb1cdffdxe1iriAshU"
|
||||||
|
address = "10.78.128.25:8080"
|
||||||
|
client = frostfs_sdk.FrostfsClient(ClientSettings(WIF, address))
|
||||||
|
params = frostfs_sdk.models.PrmsCreateContainer(name="1234")
|
||||||
|
client.container.create(params)
|
||||||
|
"""
|
22
frostfs_sdk/client/models/client_environment.py
Normal file
22
frostfs_sdk/client/models/client_environment.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import grpc
|
||||||
|
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.utils.session_cache import SessionCache
|
||||||
|
|
||||||
|
|
||||||
|
class ClientEnvironment:
|
||||||
|
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_public_key(ecdsa.public_key))
|
||||||
|
self.session_cache = session_cache
|
||||||
|
self.address = address
|
||||||
|
self._session_key = None
|
||||||
|
|
||||||
|
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
|
19
frostfs_sdk/client/models/client_settings.py
Normal file
19
frostfs_sdk/client/models/client_settings.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
class ClientSettings:
|
||||||
|
def __init__(self, wif: str = None, address: str = None):
|
||||||
|
"""
|
||||||
|
Initializes client settings with validation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
wif: Wallet import format string
|
||||||
|
address: FrostFS node host address
|
||||||
|
"""
|
||||||
|
self.wif = wif
|
||||||
|
self.address = address
|
||||||
|
|
||||||
|
# Perform validation after initialization
|
||||||
|
self.validate()
|
||||||
|
|
||||||
|
def validate(self):
|
||||||
|
"""Performs runtime validation of the settings"""
|
||||||
|
if not (self.address and self.wif):
|
||||||
|
raise ValueError("The value must be specified ADDRESS and WIF")
|
8
frostfs_sdk/client/models/ecdsa_model.py
Normal file
8
frostfs_sdk/client/models/ecdsa_model.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from frostfs_sdk.cryptography.key_extension import KeyExtension
|
||||||
|
|
||||||
|
|
||||||
|
class ECDSA:
|
||||||
|
def __init__(self, wif: str):
|
||||||
|
self.wif = wif
|
||||||
|
self.private_key: bytes = KeyExtension().get_private_key_from_wif(wif)
|
||||||
|
self.public_key: bytes = KeyExtension().get_public_key(self.private_key)
|
21
frostfs_sdk/client/parameters/call_context_param.py
Normal file
21
frostfs_sdk/client/parameters/call_context_param.py
Normal file
|
@ -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()
|
18
frostfs_sdk/client/parameters/container_param.py
Normal file
18
frostfs_sdk/client/parameters/container_param.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Optional, Dict
|
||||||
|
|
||||||
|
from frostfs_sdk.models.dto.container import Container
|
||||||
|
from frostfs_sdk.client.utils.session_cache import SessionToken
|
||||||
|
from frostfs_sdk.client.parameters.wait_param import WaitParam
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ContainerCreateParam:
|
||||||
|
container: Container
|
||||||
|
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', WaitParam())
|
14
frostfs_sdk/client/parameters/create_session_param.py
Normal file
14
frostfs_sdk/client/parameters/create_session_param.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CreateSessionParam:
|
||||||
|
"""
|
||||||
|
Represents parameters for creating a session.
|
||||||
|
"""
|
||||||
|
expiration: int # -1 indicates maximum expiration
|
||||||
|
x_headers: Optional[Dict[str, str]] = field(default_factory=dict)
|
||||||
|
|
||||||
|
def __init__(self, expiration: int, x_headers: Optional[Dict[str, str]] = None):
|
||||||
|
self.expiration = expiration
|
||||||
|
self.x_headers = x_headers if x_headers is not None else {}
|
21
frostfs_sdk/client/parameters/wait_param.py
Normal file
21
frostfs_sdk/client/parameters/wait_param.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class WaitParam:
|
||||||
|
DEFAULT_TIMEOUT: timedelta = field(default=timedelta(seconds=120), init=False)
|
||||||
|
DEFAULT_POLL_INTERVAL: timedelta = field(default=timedelta(seconds=5), init=False)
|
||||||
|
|
||||||
|
timeout: timedelta = DEFAULT_TIMEOUT
|
||||||
|
poll_interval: timedelta = DEFAULT_POLL_INTERVAL
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
if self.timeout is None:
|
||||||
|
object.__setattr__(self, 'timeout', self.DEFAULT_TIMEOUT)
|
||||||
|
if self.poll_interval is None:
|
||||||
|
object.__setattr__(self, 'poll_interval', self.DEFAULT_POLL_INTERVAL)
|
||||||
|
|
||||||
|
def get_deadline(self) -> datetime:
|
||||||
|
return datetime.now() + self.timeout
|
64
frostfs_sdk/client/services/container.py
Normal file
64
frostfs_sdk/client/services/container.py
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
# 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.services.session import SessionClient
|
||||||
|
from frostfs_sdk.client.parameters.container_param import ContainerCreateParam
|
||||||
|
from frostfs_sdk.client.parameters.call_context_param import CallContextParam
|
||||||
|
from frostfs_sdk.client.utils.session_cache import SessionToken
|
||||||
|
from frostfs_sdk.client.utils.request_constructor import RequestConstructor
|
||||||
|
from frostfs_sdk.cryptography.signer import Signer
|
||||||
|
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.client.parameters.create_session_param import CreateSessionParam
|
||||||
|
|
||||||
|
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(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_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.value)
|
||||||
|
|
||||||
|
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, self.get_context)
|
||||||
|
|
||||||
|
body = service_pb2_container.PutRequest.Body(
|
||||||
|
container=grpc_container,
|
||||||
|
signature=Signer.sign_message_rfc_6979(self.get_context.ecdsa, grpc_container)
|
||||||
|
)
|
||||||
|
|
||||||
|
request = service_pb2_container.PutRequest(body=body)
|
||||||
|
session_token = self.get_or_create_session(param.session_token, ctx)
|
||||||
|
proto_token = session_token
|
||||||
|
RequestConstructor.add_meta_header(request, param.x_headers, proto_token)
|
||||||
|
|
||||||
|
signed_request = Signer.sign(self.ecdsa.private_key, request)
|
||||||
|
return signed_request
|
||||||
|
|
||||||
|
def get_or_create_session(self, session_ctx: SessionToken, ctx: CallContextParam) -> bytes:
|
||||||
|
if session_ctx:
|
||||||
|
return session_ctx.token
|
||||||
|
|
||||||
|
session_token_from_cache = self.get_context.session_cache.try_get_value(self.get_context.get_session_key())
|
||||||
|
if session_token_from_cache:
|
||||||
|
return session_token_from_cache.token
|
||||||
|
|
||||||
|
new_session_token = SessionClient(self.get_context).create_session(CreateSessionParam(expiration=-1), ctx)
|
||||||
|
if new_session_token:
|
||||||
|
self.get_context.session_cache.set_value(self.get_context.get_session_key(), new_session_token)
|
||||||
|
return new_session_token.token
|
||||||
|
|
||||||
|
raise ValueError("cannot create session")
|
12
frostfs_sdk/client/services/context_accessor.py
Normal file
12
frostfs_sdk/client/services/context_accessor.py
Normal file
|
@ -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
|
38
frostfs_sdk/client/services/session.py
Normal file
38
frostfs_sdk/client/services/session.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
from typing import Optional
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from frostfs_sdk.client.utils.session_cache import SessionToken
|
||||||
|
from frostfs_sdk.cryptography.signer import Signer
|
||||||
|
from frostfs_sdk.models.mappers.session_mapper import SessionMapper
|
||||||
|
from frostfs_sdk.models.mappers.owner_id_mapper import OwnerIdMapper
|
||||||
|
from frostfs_sdk.client.models.client_environment import ClientEnvironment
|
||||||
|
from frostfs_sdk.client.services.context_accessor import ContextAccessor
|
||||||
|
from frostfs_sdk.client.utils.request_constructor import RequestConstructor
|
||||||
|
from frostfs_sdk.client.parameters.call_context_param import CallContextParam
|
||||||
|
from frostfs_sdk.client.parameters.create_session_param import CreateSessionParam
|
||||||
|
|
||||||
|
from frostfs_sdk.protos.models.session import service_pb2_grpc as service_pb2_grpc_session
|
||||||
|
from frostfs_sdk.protos.models.session import service_pb2 as service_pb2_session
|
||||||
|
from frostfs_sdk.protos.models.session import types_pb2 as types_pb2_session
|
||||||
|
|
||||||
|
|
||||||
|
class SessionClient(ContextAccessor):
|
||||||
|
def __init__(self, client_environment: ClientEnvironment):
|
||||||
|
super().__init__(client_environment)
|
||||||
|
self.session_stub = service_pb2_grpc_session.SessionServiceStub(client_environment.channel)
|
||||||
|
|
||||||
|
def create_session(self, param: CreateSessionParam, ctx: CallContextParam) -> SessionToken:
|
||||||
|
body = service_pb2_session.CreateRequest.Body(
|
||||||
|
owner_id=OwnerIdMapper.to_grpc_message(self.get_context.owner_id),
|
||||||
|
expiration=param.expiration
|
||||||
|
)
|
||||||
|
request = service_pb2_session.CreateRequest(
|
||||||
|
body=body
|
||||||
|
)
|
||||||
|
RequestConstructor.add_meta_header(request, None, None)
|
||||||
|
signed_request = Signer.sign_message(self.get_context.ecdsa.private_key, request)
|
||||||
|
response: service_pb2_session.CreateResponse = self.session_stub.Create(request)
|
||||||
|
|
||||||
|
session_token_grpc = types_pb2_session.SessionToken(response.body)
|
||||||
|
token = SessionMapper.serialize(session_token_grpc)
|
||||||
|
return SessionToken(token=token)
|
30
frostfs_sdk/client/utils/message_helper.py
Normal file
30
frostfs_sdk/client/utils/message_helper.py
Normal file
|
@ -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 getattr(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")
|
||||||
|
|
||||||
|
setattr(message, message.DESCRIPTOR.fields[field_name], value)
|
47
frostfs_sdk/client/utils/request_constructor.py
Normal file
47
frostfs_sdk/client/utils/request_constructor.py
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
from google.protobuf.message import Message
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
from frostfs_sdk.models.mappers.meta_header_mapper import MetaHeaderMapper
|
||||||
|
from frostfs_sdk.models.mappers.session_mapper import SessionMapper
|
||||||
|
from frostfs_sdk.models.dto.meta_header import MetaHeader
|
||||||
|
from frostfs_sdk.protos.models.session import types_pb2 as types_pb2_session
|
||||||
|
|
||||||
|
|
||||||
|
META_HEADER_FIELD_NAME = "meta_header"
|
||||||
|
|
||||||
|
|
||||||
|
class RequestConstructor:
|
||||||
|
@staticmethod
|
||||||
|
def add_meta_header(request: Message, x_headers: Optional[Dict[str, str]] = None, session_token: types_pb2_session.SessionToken = None):
|
||||||
|
"""
|
||||||
|
Adds a meta header to the request.
|
||||||
|
|
||||||
|
:param request: A Protobuf Message.Builder object.
|
||||||
|
:param x_headers: Optional dictionary of custom headers.
|
||||||
|
:param session_token: Optional session token.
|
||||||
|
:raises ValueError: If the request or required fields are missing.
|
||||||
|
"""
|
||||||
|
if request is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
descriptor = request.DESCRIPTOR
|
||||||
|
if getattr(descriptor, META_HEADER_FIELD_NAME) is None:
|
||||||
|
raise ValueError(f"Required Protobuf field is missing: {META_HEADER_FIELD_NAME}")
|
||||||
|
|
||||||
|
meta_header = getattr(request, META_HEADER_FIELD_NAME)
|
||||||
|
if meta_header.ByteSize() > 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
meta_header_builder = MetaHeaderMapper.to_grpc_message(MetaHeader())
|
||||||
|
|
||||||
|
if session_token and session_token.ByteSize() > 0:
|
||||||
|
meta_header_builder.session_token = session_token
|
||||||
|
|
||||||
|
if x_headers:
|
||||||
|
grpc_x_headers = [
|
||||||
|
types_pb2_session.XHeader(key=key, value=value)
|
||||||
|
for key, value in x_headers.items()
|
||||||
|
]
|
||||||
|
meta_header_builder.x_headers.extend(grpc_x_headers)
|
||||||
|
|
||||||
|
setattr(request, META_HEADER_FIELD_NAME, meta_header_builder.build())
|
39
frostfs_sdk/client/utils/session_cache.py
Normal file
39
frostfs_sdk/client/utils/session_cache.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
@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: str):
|
||||||
|
return key in self.cache
|
||||||
|
|
||||||
|
def try_get_value(self, key: str) -> Optional[SessionToken]:
|
||||||
|
if not key:
|
||||||
|
return None
|
||||||
|
return self.cache.get(key)
|
||||||
|
|
||||||
|
|
||||||
|
def set_value(self, key: str, value: SessionToken):
|
||||||
|
if key is not None:
|
||||||
|
self.cache[key] = value
|
||||||
|
|
||||||
|
def delete_by_prefix(self, prefix: str):
|
||||||
|
# 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
|
100
frostfs_sdk/cryptography/key_extension.py
Normal file
100
frostfs_sdk/cryptography/key_extension.py
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import base58
|
||||||
|
import ecdsa
|
||||||
|
import hashlib
|
||||||
|
from struct import pack, unpack
|
||||||
|
from Crypto.Hash import RIPEMD160
|
||||||
|
|
||||||
|
|
||||||
|
COMPRESSED_PUBLIC_KEY_LENGTH = 33
|
||||||
|
NEO_ADDRESS_VERSION = 0x35
|
||||||
|
UNCOMPRESSED_PUBLIC_KEY_LENGTH = 65
|
||||||
|
DECODE_ADDRESS_LENGTH = 21
|
||||||
|
PS_IN_HASH160 = 0x0C
|
||||||
|
CHECK_SIG_DESCRIPTOR = int.from_bytes(
|
||||||
|
hashlib.sha256("System.Crypto.CheckSig".encode('ascii')).digest()[:4], byteorder='little'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class KeyExtension:
|
||||||
|
def get_private_key_from_wif(self, wif: str) -> bytes:
|
||||||
|
"""
|
||||||
|
Converts a WIF private key to a byte array.
|
||||||
|
|
||||||
|
:param wif: WIF private key in string format.
|
||||||
|
:return: Private key in byte format (32 bytes).
|
||||||
|
:raises ValueError: If the WIF key is incorrect.
|
||||||
|
"""
|
||||||
|
assert not self.is_empty(wif)
|
||||||
|
|
||||||
|
decoded = base58.b58decode_check(wif)
|
||||||
|
if len(decoded) != 34 or decoded[0] != 0x80 or decoded[-1] != 0x01:
|
||||||
|
raise ValueError("Incorrect WIF private key")
|
||||||
|
|
||||||
|
private_key = decoded[1:-1]
|
||||||
|
return private_key
|
||||||
|
|
||||||
|
def get_public_key(self, private_key: bytes) -> bytes:
|
||||||
|
"""
|
||||||
|
Extract public key from Private key
|
||||||
|
|
||||||
|
:param private_key: Private key in byte format (32 bytes).
|
||||||
|
:return: compressed public key in byte format (33 bytes).
|
||||||
|
:raises ValueError: If the private_key key is empty or null.
|
||||||
|
"""
|
||||||
|
assert not self.is_empty(private_key)
|
||||||
|
|
||||||
|
if len(private_key) != 32:
|
||||||
|
raise ValueError(f"Incorrect len of private key, Expected: 32, Actual: {len(private_key)}")
|
||||||
|
|
||||||
|
public_key = ecdsa.SigningKey.from_string(private_key, curve=ecdsa.NIST256p).get_verifying_key()
|
||||||
|
compressed_public_key = public_key.to_string("compressed")
|
||||||
|
return compressed_public_key
|
||||||
|
|
||||||
|
def get_owner_id_by_public_key(self, public_key: bytes) -> str:
|
||||||
|
if len(public_key) != COMPRESSED_PUBLIC_KEY_LENGTH:
|
||||||
|
raise ValueError(f"Encoded compressed public key has wrong length. Expected {COMPRESSED_PUBLIC_KEY_LENGTH}, got {len(public_key)}")
|
||||||
|
|
||||||
|
script_hash = self.get_script_hash(public_key)
|
||||||
|
data = bytearray(DECODE_ADDRESS_LENGTH)
|
||||||
|
data[0] = NEO_ADDRESS_VERSION
|
||||||
|
data[1:] = script_hash
|
||||||
|
return base58.b58encode_check(data).decode('utf-8')
|
||||||
|
|
||||||
|
def get_script_hash(self, public_key: bytes):
|
||||||
|
script = self.create_signature_redeem_script(public_key)
|
||||||
|
sha256_hash = hashlib.sha256(script).digest()
|
||||||
|
return self.get_ripemd160(sha256_hash)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_signature_redeem_script(public_key: bytes):
|
||||||
|
if len(public_key) != COMPRESSED_PUBLIC_KEY_LENGTH:
|
||||||
|
raise ValueError(f"Encoded compressed public key has wrong length. Expected {COMPRESSED_PUBLIC_KEY_LENGTH}, got {len(public_key)}")
|
||||||
|
|
||||||
|
script = bytearray([PS_IN_HASH160, COMPRESSED_PUBLIC_KEY_LENGTH])
|
||||||
|
script.extend(public_key)
|
||||||
|
script.append(UNCOMPRESSED_PUBLIC_KEY_LENGTH)
|
||||||
|
script.extend(pack("<I", CHECK_SIG_DESCRIPTOR))
|
||||||
|
return bytes(script)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_ripemd160(value):
|
||||||
|
if value is None:
|
||||||
|
raise ValueError("Input parameter is missing")
|
||||||
|
|
||||||
|
digest = RIPEMD160.new()
|
||||||
|
digest.update(value)
|
||||||
|
return digest.digest()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_empty(sequence_symbols: bytes | str):
|
||||||
|
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}"
|
54
frostfs_sdk/cryptography/signer.py
Normal file
54
frostfs_sdk/cryptography/signer.py
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
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:
|
||||||
|
raise ValueError(f"Incorrect private_key: {private_key}")
|
||||||
|
|
||||||
|
sk = ecdsa.SigningKey.from_string(private_key, curve=ecdsa.NIST256p, hashfunc=sha256)
|
||||||
|
|
||||||
|
signature = sk.sign_deterministic(message)
|
||||||
|
|
||||||
|
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:
|
||||||
|
raise ValueError(f"Incorrect private key: {private_key}")
|
||||||
|
|
||||||
|
sk = ecdsa.SigningKey.from_string(private_key, curve=ecdsa.NIST256p, hashfunc=sha512)
|
||||||
|
signature = sk.sign(message)
|
||||||
|
|
||||||
|
# the first byte indicates the node version marker
|
||||||
|
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())
|
||||||
|
)
|
9
frostfs_sdk/models/__init__.py
Normal file
9
frostfs_sdk/models/__init__.py
Normal file
|
@ -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
|
29
frostfs_sdk/models/dto/container.py
Normal file
29
frostfs_sdk/models/dto/container.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Container:
|
||||||
|
# basicAcl: BasicAcl # TODO: will remove it?
|
||||||
|
placementPolicy: PlacementPolicy
|
||||||
|
nonce: uuid.UUID = field(default_factory=uuid.uuid4)
|
||||||
|
version: Optional[Version] = None
|
||||||
|
owner_id: Optional[OwnerId] = None
|
||||||
|
attributes: Dict[str, str] = field(default_factory=dict)
|
||||||
|
|
||||||
|
def __init__(self, placementPolicy: PlacementPolicy):
|
||||||
|
self.nonce = uuid.uuid4()
|
||||||
|
self.placementPolicy = placementPolicy
|
||||||
|
self.attributes = {}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ContainerId:
|
||||||
|
value: str
|
14
frostfs_sdk/models/dto/filter.py
Normal file
14
frostfs_sdk/models/dto/filter.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from frostfs_sdk.models.enums.filter_operation import FilterOperation
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Filter:
|
||||||
|
"""
|
||||||
|
Data Transfer Object for Filter configuration
|
||||||
|
"""
|
||||||
|
name: str
|
||||||
|
key: str
|
||||||
|
operation: FilterOperation
|
||||||
|
value: str
|
47
frostfs_sdk/models/dto/meta_header.py
Normal file
47
frostfs_sdk/models/dto/meta_header.py
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from frostfs_sdk.models.dto.version import Version, DEFAULT_MAJOR_VERSION, DEFAULT_MINOR_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
class MetaHeader:
|
||||||
|
def __init__(self, version=None, epoch: int = 0, ttl: int = 2):
|
||||||
|
"""
|
||||||
|
Initializes a MetaHeader object.
|
||||||
|
|
||||||
|
:param version: A Version object representing the version.
|
||||||
|
:param epoch: An integer representing the epoch (default: 0).
|
||||||
|
:param ttl: An integer representing the time-to-live (default: 2).
|
||||||
|
"""
|
||||||
|
self._version = None
|
||||||
|
self._epoch = None
|
||||||
|
self._ttl = None
|
||||||
|
|
||||||
|
# Set default values if not provided
|
||||||
|
self.set_version(version or Version(DEFAULT_MAJOR_VERSION, DEFAULT_MINOR_VERSION))
|
||||||
|
self.set_epoch(epoch)
|
||||||
|
self.set_ttl(ttl)
|
||||||
|
|
||||||
|
def get_version(self):
|
||||||
|
return self._version
|
||||||
|
|
||||||
|
def get_epoch(self):
|
||||||
|
return self._epoch
|
||||||
|
|
||||||
|
def get_ttl(self):
|
||||||
|
return self._ttl
|
||||||
|
|
||||||
|
def set_version(self, version):
|
||||||
|
if version is None:
|
||||||
|
raise ValueError(f"Input parameter is missing: {Version.__name__}")
|
||||||
|
self._version = version
|
||||||
|
|
||||||
|
def set_epoch(self, epoch: int):
|
||||||
|
if epoch < 0:
|
||||||
|
raise ValueError("Epoch must be greater than or equal to zero")
|
||||||
|
self._epoch = epoch
|
||||||
|
|
||||||
|
def set_ttl(self, ttl: int):
|
||||||
|
if ttl <= 0:
|
||||||
|
raise ValueError("TTL must be greater than zero")
|
||||||
|
self._ttl = ttl
|
21
frostfs_sdk/models/dto/owner_id.py
Normal file
21
frostfs_sdk/models/dto/owner_id.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
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
|
14
frostfs_sdk/models/dto/placement_policy.py
Normal file
14
frostfs_sdk/models/dto/placement_policy.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
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
|
12
frostfs_sdk/models/dto/replica.py
Normal file
12
frostfs_sdk/models/dto/replica.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
EMPTY_STRING = ""
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Replica:
|
||||||
|
count: int
|
||||||
|
selector: str = field(default=EMPTY_STRING)
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
self.selector = self.selector if self.selector else EMPTY_STRING
|
15
frostfs_sdk/models/dto/selector.py
Normal file
15
frostfs_sdk/models/dto/selector.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from frostfs_sdk.models.enums.selector_clause import SelectorClause
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Selector:
|
||||||
|
"""
|
||||||
|
Data Transfer Object for Selector configuration
|
||||||
|
"""
|
||||||
|
name: str
|
||||||
|
count: int
|
||||||
|
clause: SelectorClause
|
||||||
|
attribute: str
|
||||||
|
filter: str
|
19
frostfs_sdk/models/dto/version.py
Normal file
19
frostfs_sdk/models/dto/version.py
Normal file
|
@ -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
|
||||||
|
|
7
frostfs_sdk/models/enums/basic_acl.py
Normal file
7
frostfs_sdk/models/enums/basic_acl.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
class BasicAcl(Enum):
|
||||||
|
PRIVATE = 0x1C8C8CCC
|
||||||
|
PUBLIC_RO = 0x1FBF8CFF
|
||||||
|
PUBLIC_RW = 0x1FBFBFFF
|
||||||
|
PUBLIC_APPEND = 0x1FBF9FFF
|
42
frostfs_sdk/models/enums/filter_operation.py
Normal file
42
frostfs_sdk/models/enums/filter_operation.py
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
class FilterOperation:
|
||||||
|
"""
|
||||||
|
Enum for filter operations with integer value mapping
|
||||||
|
"""
|
||||||
|
OPERATION_UNSPECIFIED = 0
|
||||||
|
EQ = 1
|
||||||
|
NE = 2
|
||||||
|
GT = 3
|
||||||
|
GE = 4
|
||||||
|
LT = 5
|
||||||
|
LE = 6
|
||||||
|
OR = 7
|
||||||
|
AND = 8
|
||||||
|
NOT = 9
|
||||||
|
LIKE = 10
|
||||||
|
|
||||||
|
_value_map = {
|
||||||
|
0: OPERATION_UNSPECIFIED,
|
||||||
|
1: EQ,
|
||||||
|
2: NE,
|
||||||
|
3: GT,
|
||||||
|
4: GE,
|
||||||
|
5: LT,
|
||||||
|
6: LE,
|
||||||
|
7: OR,
|
||||||
|
8: AND,
|
||||||
|
9: NOT,
|
||||||
|
10: LIKE
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, value: int) -> 'FilterOperation':
|
||||||
|
"""
|
||||||
|
Get enum instance by integer value
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: Integer value of the operation
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Corresponding FilterOperation instance
|
||||||
|
"""
|
||||||
|
return cls._value_map.get(value)
|
14
frostfs_sdk/models/enums/selector_clause.py
Normal file
14
frostfs_sdk/models/enums/selector_clause.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
from enum import Enum, unique
|
||||||
|
|
||||||
|
@unique
|
||||||
|
class SelectorClause(Enum):
|
||||||
|
CLAUSE_UNSPECIFIED = 0
|
||||||
|
SAME = 1
|
||||||
|
DISTINCT = 2
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, value: int):
|
||||||
|
try:
|
||||||
|
return cls(value)
|
||||||
|
except ValueError:
|
||||||
|
raise KeyError(f"Unknown enum value: {value}")
|
77
frostfs_sdk/models/mappers/container_mapper.py
Normal file
77
frostfs_sdk/models/mappers/container_mapper.py
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import grpc
|
||||||
|
|
||||||
|
from frostfs_sdk.client.models.client_environment import ClientEnvironment
|
||||||
|
from frostfs_sdk.client.services.context_accessor import ContextAccessor
|
||||||
|
from frostfs_sdk.models.mappers.placement_policy_mapper import PlacementPolicyMapper
|
||||||
|
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:
|
||||||
|
@staticmethod
|
||||||
|
def to_grpc_message(container: Container, client_context: ClientEnvironment) -> Optional[types_pb2_container.Container]:
|
||||||
|
"""
|
||||||
|
Converts Container DTO to gRPC message
|
||||||
|
|
||||||
|
Args:
|
||||||
|
container: Container DTO object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
gRPC Container message builder
|
||||||
|
"""
|
||||||
|
if not container:
|
||||||
|
return None
|
||||||
|
|
||||||
|
attributes = [
|
||||||
|
types_pb2_container.Container.Attribute(key=k, value=v)
|
||||||
|
for k, v in container.attributes.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
if container.owner_id:
|
||||||
|
owner_id = OwnerIdMapper.to_grpc_message(container.owner_id)
|
||||||
|
else:
|
||||||
|
owner_id = OwnerIdMapper.to_grpc_message(client_context.owner_id)
|
||||||
|
|
||||||
|
if container.version:
|
||||||
|
version = VersionMapper.to_grpc_message(container.version)
|
||||||
|
else:
|
||||||
|
version = VersionMapper.to_grpc_message(client_context.version)
|
||||||
|
|
||||||
|
grpc_container = types_pb2_container.Container(
|
||||||
|
nonce=container.nonce.bytes,
|
||||||
|
placement_policy=PlacementPolicyMapper.to_grpc_message(container.placementPolicy),
|
||||||
|
owner_id=owner_id,
|
||||||
|
version=version,
|
||||||
|
attributes=attributes
|
||||||
|
)
|
||||||
|
|
||||||
|
return grpc_container
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def to_model(container_grpc: types_pb2_container.Container) -> Optional[Container]:
|
||||||
|
"""
|
||||||
|
Converts gRPC message to Container DTO
|
||||||
|
|
||||||
|
Args:
|
||||||
|
container_grpc: gRPC Container message
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Container DTO object
|
||||||
|
"""
|
||||||
|
if not container_grpc or container_grpc.ByteSize() == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
attributes = {attr.key: attr.value for attr in container_grpc.attributes}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
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),
|
||||||
|
attributes=attributes
|
||||||
|
)
|
60
frostfs_sdk/models/mappers/filter_mapper.py
Normal file
60
frostfs_sdk/models/mappers/filter_mapper.py
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from frostfs_sdk.models.enums.filter_operation import FilterOperation
|
||||||
|
from frostfs_sdk.models.dto.filter import Filter
|
||||||
|
from frostfs_sdk.protos.models.netmap import types_pb2 as types_pb2_netmap
|
||||||
|
|
||||||
|
|
||||||
|
class FilterMapper:
|
||||||
|
@staticmethod
|
||||||
|
def to_grpc_messages(filters: List[Filter]) -> List[types_pb2_netmap.Filter]:
|
||||||
|
"""
|
||||||
|
Converts list of Filter DTOs to gRPC messages with nested conversion
|
||||||
|
"""
|
||||||
|
if not filters:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return [FilterMapper.to_grpc_message(f) for f in filters]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def to_grpc_message(filter_dto: Filter) -> types_pb2_netmap.Filter:
|
||||||
|
"""
|
||||||
|
Converts Filter DTO to gRPC message with nested filters
|
||||||
|
"""
|
||||||
|
|
||||||
|
operation = types_pb2_netmap.Filter.Operation.Value(filter_dto.operation.value)
|
||||||
|
return types_pb2_netmap.Filter(
|
||||||
|
name=filter_dto.name,
|
||||||
|
key=filter_dto.key,
|
||||||
|
op=operation,
|
||||||
|
value=filter_dto.value,
|
||||||
|
filters=FilterMapper.to_grpc_messages(filter_dto.filters)
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def to_models(filters_grpc: List[types_pb2_netmap.Filter]) -> Optional[List[Filter]]:
|
||||||
|
"""
|
||||||
|
Converts gRPC messages to Filter DTOs with nested conversion
|
||||||
|
"""
|
||||||
|
if not filters_grpc:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return [FilterMapper.to_model(f) for f in filters_grpc]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def to_model(filter_grpc: types_pb2_netmap.Filter) -> Optional[Filter]:
|
||||||
|
"""
|
||||||
|
Converts gRPC message to Filter DTO with nested filters
|
||||||
|
"""
|
||||||
|
if not filter_grpc or filter_grpc.ByteSize() == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
operation = FilterOperation(filter_grpc.op)
|
||||||
|
|
||||||
|
return Filter(
|
||||||
|
name=filter_grpc.name,
|
||||||
|
key=filter_grpc.key,
|
||||||
|
operation=operation,
|
||||||
|
value=filter_grpc.value,
|
||||||
|
filters=FilterMapper.to_models(filter_grpc.filters)
|
||||||
|
)
|
26
frostfs_sdk/models/mappers/meta_header_mapper.py
Normal file
26
frostfs_sdk/models/mappers/meta_header_mapper.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
from frostfs_sdk.models.mappers.version_mapper import VersionMapper
|
||||||
|
from frostfs_sdk.models.dto.meta_header import MetaHeader
|
||||||
|
from frostfs_sdk.protos.models.session import types_pb2 as types_pb2_session
|
||||||
|
|
||||||
|
|
||||||
|
class MetaHeaderMapper:
|
||||||
|
"""
|
||||||
|
Maps a MetaHeader object to a Protobuf RequestMetaHeader object.
|
||||||
|
"""
|
||||||
|
@staticmethod
|
||||||
|
def to_grpc_message(meta_header: MetaHeader):
|
||||||
|
"""
|
||||||
|
Converts a MetaHeader object to a Protobuf RequestMetaHeader object.
|
||||||
|
|
||||||
|
:param meta_header: A MetaHeader object.
|
||||||
|
:return: A Protobuf RequestMetaHeader object.
|
||||||
|
:raises ValueError: If the input meta_header is None.
|
||||||
|
"""
|
||||||
|
if meta_header is None:
|
||||||
|
raise ValueError(f"Input parameter is missing: {MetaHeader.__name__}")
|
||||||
|
|
||||||
|
return types_pb2_session.RequestMetaHeader(
|
||||||
|
version=VersionMapper.to_grpc_message(meta_header.get_version()),
|
||||||
|
epoch=meta_header.get_epoch(),
|
||||||
|
ttl=meta_header.get_ttl()
|
||||||
|
)
|
32
frostfs_sdk/models/mappers/owner_id_mapper.py
Normal file
32
frostfs_sdk/models/mappers/owner_id_mapper.py
Normal file
|
@ -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}")
|
53
frostfs_sdk/models/mappers/placement_policy_mapper.py
Normal file
53
frostfs_sdk/models/mappers/placement_policy_mapper.py
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from frostfs_sdk.models.mappers.filter_mapper import FilterMapper
|
||||||
|
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:
|
||||||
|
@staticmethod
|
||||||
|
def to_grpc_message(policy: PlacementPolicy) -> Optional[types_pb2_netmap.PlacementPolicy]:
|
||||||
|
"""
|
||||||
|
Converts PlacementPolicy DTO to gRPC message
|
||||||
|
|
||||||
|
Args:
|
||||||
|
policy: PlacementPolicy DTO object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
gRPC PlacementPolicy message
|
||||||
|
"""
|
||||||
|
if not policy:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return types_pb2_netmap.PlacementPolicy(
|
||||||
|
unique=policy.unique,
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def to_model(policy_grpc: types_pb2_netmap.PlacementPolicy) -> Optional[PlacementPolicy]:
|
||||||
|
"""
|
||||||
|
Converts gRPC message to PlacementPolicy DTO
|
||||||
|
|
||||||
|
Args:
|
||||||
|
policy_grpc: gRPC PlacementPolicy message
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PlacementPolicy DTO object
|
||||||
|
"""
|
||||||
|
if not policy_grpc or policy_grpc.ByteSize() == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return PlacementPolicy(
|
||||||
|
replicas=ReplicaMapper.to_models(policy_grpc.replicas),
|
||||||
|
unique=policy_grpc.unique,
|
||||||
|
backup_factory=policy_grpc.container_backup_factor,
|
||||||
|
filters=FilterMapper.to_models(policy_grpc.filters),
|
||||||
|
selectors=SelectorMapper.to_models(policy_grpc.selectors)
|
||||||
|
)
|
56
frostfs_sdk/models/mappers/replica_mapper.py
Normal file
56
frostfs_sdk/models/mappers/replica_mapper.py
Normal file
|
@ -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
|
||||||
|
)
|
73
frostfs_sdk/models/mappers/selector_mapper.py
Normal file
73
frostfs_sdk/models/mappers/selector_mapper.py
Normal file
|
@ -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
|
||||||
|
)
|
44
frostfs_sdk/models/mappers/session_mapper.py
Normal file
44
frostfs_sdk/models/mappers/session_mapper.py
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
from google.protobuf.message import DecodeError
|
||||||
|
from frostfs_sdk.protos.models.session import types_pb2 as types_pb2_session
|
||||||
|
|
||||||
|
|
||||||
|
class SessionMapper:
|
||||||
|
@staticmethod
|
||||||
|
def serialize(token: types_pb2_session.SessionToken) -> bytes:
|
||||||
|
"""
|
||||||
|
Serializes a SessionToken object into a byte array.
|
||||||
|
|
||||||
|
:param token: A SessionToken Protobuf object.
|
||||||
|
:return: A byte array representing the serialized SessionToken.
|
||||||
|
:raises ValueError: If the input token is None.
|
||||||
|
:raises Exception: If serialization fails.
|
||||||
|
"""
|
||||||
|
if token is None:
|
||||||
|
raise ValueError(f"Input parameter is missing: {types_pb2_session.SessionToken.__name__}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Serialize the token to bytes
|
||||||
|
return token.SerializeToString()
|
||||||
|
except Exception as e:
|
||||||
|
raise Exception(f"Serialization failed: {str(e)}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def deserialize_session_token(bytes_data: bytes) -> types_pb2_session.SessionToken:
|
||||||
|
"""
|
||||||
|
Deserializes a byte array into a SessionToken object.
|
||||||
|
|
||||||
|
:param bytes_data: A byte array representing the serialized SessionToken.
|
||||||
|
:return: A SessionToken Protobuf object.
|
||||||
|
:raises ValueError: If the input byte array is None or empty.
|
||||||
|
:raises Exception: If deserialization fails.
|
||||||
|
"""
|
||||||
|
if not bytes_data:
|
||||||
|
raise ValueError(f"Input parameter is missing: {types_pb2_session.SessionToken.__name__}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Deserialize the byte array into a SessionToken object
|
||||||
|
session_token = types_pb2_session.SessionToken()
|
||||||
|
session_token.ParseFromString(bytes_data)
|
||||||
|
return session_token
|
||||||
|
except DecodeError as e:
|
||||||
|
raise Exception(f"Deserialization failed: {str(e)}")
|
31
frostfs_sdk/models/mappers/uuid_extension.py
Normal file
31
frostfs_sdk/models/mappers/uuid_extension.py
Normal file
|
@ -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')
|
31
frostfs_sdk/models/mappers/version_mapper.py
Normal file
31
frostfs_sdk/models/mappers/version_mapper.py
Normal file
|
@ -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
|
||||||
|
)
|
32
generate_proto.sh
Executable file
32
generate_proto.sh
Executable file
|
@ -0,0 +1,32 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# TODO: add to MakeFile
|
||||||
|
# chmod +x generate_proto.sh
|
||||||
|
# ./generate_proto.sh
|
||||||
|
|
||||||
|
|
||||||
|
# Define directories
|
||||||
|
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"
|
||||||
|
|
||||||
|
rm -rf "$PROTOS_DIR"
|
||||||
|
|
||||||
|
echo "1. Create folder ./protos"
|
||||||
|
mkdir -p "$PROTOS_DIR"
|
||||||
|
|
||||||
|
echo "2. Cloning repository into ./protos/source..."
|
||||||
|
mkdir -p "$SOURCE_DIR"
|
||||||
|
git clone "$REPO_FROSTFS_API_PROTOS" "$SOURCE_DIR"
|
||||||
|
|
||||||
|
echo "3. Generating Python code from .proto files to $MODELS_DIR"
|
||||||
|
mkdir -p "$MODELS_DIR"
|
||||||
|
find "$SOURCE_DIR" -name "*.proto" | while read -r proto_file; do
|
||||||
|
python -m grpc_tools.protoc -I "$SOURCE_DIR" --python_out="$MODELS_DIR" --pyi_out="$MODELS_DIR" --grpc_python_out="$MODELS_DIR" "$proto_file"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "4. Making ./protos/models a Python package..."
|
||||||
|
touch "$MODELS_DIR/__init__.py"
|
||||||
|
|
||||||
|
echo "All steps completed successfully."
|
|
@ -1,4 +1,7 @@
|
||||||
base58==2.1.1
|
base58==2.1.1
|
||||||
ecdsa==0.19.0
|
ecdsa==0.19.0
|
||||||
|
grpcio==1.70.0
|
||||||
|
grpcio-tools==1.70.0
|
||||||
|
pycryptodome==3.22.0
|
||||||
|
|
||||||
pytest==8.3.4
|
pytest==8.3.4
|
||||||
|
|
15
tests/client/test_create_container.py
Normal file
15
tests/client/test_create_container.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
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):
|
||||||
|
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)
|
|
@ -1,8 +1,23 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from tests.helpers.models import ClientCryptograpy
|
from frostfs_sdk import ClientSettings, FrostfsClient
|
||||||
|
from tests.helpers.models import ClientCryptograpy, Helpers
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def client_cryptography():
|
def client_cryptography():
|
||||||
return ClientCryptograpy()
|
return ClientCryptograpy()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def default_frostfs_client():
|
||||||
|
client_settings = ClientSettings(
|
||||||
|
wif="KxnEZ7FsPgKMdL9PYt9vsDkXiSw6qP9J8dpR4eVMsDpJyJxcYpve",
|
||||||
|
address="localhost:8080"
|
||||||
|
)
|
||||||
|
return FrostfsClient(client_settings)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def helpers():
|
||||||
|
return Helpers()
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from frostfs_api.cryptography.key_extension import KeyExtension
|
from frostfs_sdk.cryptography.key_extension import KeyExtension
|
||||||
from frostfs_api.cryptography.verifier import Verifier
|
from frostfs_sdk.cryptography.verifier import Verifier
|
||||||
from frostfs_api.cryptography.signer import Signer
|
from frostfs_sdk.cryptography.signer import Signer
|
||||||
|
from tests.helpers.params_container import ParamsContainerHelper
|
||||||
|
|
||||||
|
|
||||||
class ClientCryptograpy:
|
class ClientCryptograpy:
|
||||||
|
@ -8,3 +9,8 @@ class ClientCryptograpy:
|
||||||
self.key_extension = KeyExtension()
|
self.key_extension = KeyExtension()
|
||||||
self.signer = Signer()
|
self.signer = Signer()
|
||||||
self.verifier = Verifier()
|
self.verifier = Verifier()
|
||||||
|
|
||||||
|
|
||||||
|
class Helpers:
|
||||||
|
def __init__(self):
|
||||||
|
self.container = ParamsContainerHelper()
|
||||||
|
|
16
tests/helpers/params_container.py
Normal file
16
tests/helpers/params_container.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
from frostfs_sdk import ContainerCreateParam, WaitParam, Container, PlacementPolicy, Replica, BasicAcl
|
||||||
|
|
||||||
|
|
||||||
|
class ParamsContainerHelper:
|
||||||
|
def create_params_container_create(self):
|
||||||
|
req_container_create = ContainerCreateParam(
|
||||||
|
container=Container(
|
||||||
|
placementPolicy=PlacementPolicy(
|
||||||
|
replicas=[Replica(count=1)],
|
||||||
|
unique=True,
|
||||||
|
backup_factory=0
|
||||||
|
)
|
||||||
|
),
|
||||||
|
wait_params=WaitParam()
|
||||||
|
)
|
||||||
|
return req_container_create
|
Loading…
Add table
Add a link
Reference in a new issue