[#3] Added generate proto script create container method

Signed-off-by: Ilyas Niyazov <i.niyazov@yadro.com>
This commit is contained in:
Ilyas Niyazov 2025-03-25 11:13:48 +03:00
parent 19282f13cc
commit 297e107b10
52 changed files with 1380 additions and 74 deletions

View 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

View 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)
"""

View 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

View 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")

View 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)

View 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()

View 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())

View 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 {}

View 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

View 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")

View 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

View 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)

View 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)

View 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())

View 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