Initial immudb_wrapper implementation #1
							
								
								
									
										339
									
								
								immudb_wrapper.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										339
									
								
								immudb_wrapper.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,339 @@
 | 
			
		||||
import hashlib
 | 
			
		||||
import json
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
import re
 | 
			
		||||
from dataclasses import asdict
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from traceback import format_exc
 | 
			
		||||
from typing import IO, Any, Dict, Optional, Union
 | 
			
		||||
from urllib.parse import urlparse
 | 
			
		||||
 | 
			
		||||
from git import Repo
 | 
			
		||||
from grpc import RpcError
 | 
			
		||||
from immudb import ImmudbClient
 | 
			
		||||
from immudb.datatypes import SafeGetResponse, SetResponse
 | 
			
		||||
from immudb.rootService import RootService
 | 
			
		||||
 | 
			
		||||
Dict = Dict[str, Any]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ImmudbWrapper(ImmudbClient):
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self,
 | 
			
		||||
        username: str = 'immudb',
 | 
			
		||||
        password: str = 'immudb',
 | 
			
		||||
        database: str = 'defaultdb',
 | 
			
		||||
        immudb_address: Optional[str] = 'localhost:3322',
 | 
			
		||||
        root_service: Optional[RootService] = None,
 | 
			
		||||
        public_key_file: Optional[str] = None,
 | 
			
		||||
        timeout: Optional[int] = None,
 | 
			
		||||
        max_grpc_message_length: Optional[int] = None,
 | 
			
		||||
        logger: Optional[logging.Logger] = None,
 | 
			
		||||
    ):
 | 
			
		||||
        """
 | 
			
		||||
        The wrapper around binary `immuclient` from Codenotary.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            username (str): Immudb username to log in (default: "immudb").
 | 
			
		||||
            password (str): Immudb password to log in (default: "immudb").
 | 
			
		||||
            database (str): Immudb database to be used (default: "defaultdb").
 | 
			
		||||
            immudb_address (str, optional): url in format ``host:port``
 | 
			
		||||
                (e.g. ``localhost:3322``) of your immudb instance.
 | 
			
		||||
                Defaults to ``localhost:3322`` when no value is set.
 | 
			
		||||
            root_service (RootService, optional): object that implements
 | 
			
		||||
                RootService, allowing requests to be verified. Optional.
 | 
			
		||||
                By default in-memory RootService instance will be created
 | 
			
		||||
            public_key_file (str, optional): path of the public key to use
 | 
			
		||||
                for authenticating requests. Optional.
 | 
			
		||||
            timeout (int, optional): global timeout for GRPC requests. Requests
 | 
			
		||||
                will hang until the server responds if no timeout is set.
 | 
			
		||||
            max_grpc_message_length (int, optional): maximum size of message
 | 
			
		||||
                the server should send. The default (4Mb) is used if no
 | 
			
		||||
                value is set.
 | 
			
		||||
            logger (logging.Logger, optional): Logger to be used
 | 
			
		||||
        """
 | 
			
		||||
        self.username = username
 | 
			
		||||
        self.password = password
 | 
			
		||||
        self.database = database
 | 
			
		||||
        if not logger:
 | 
			
		||||
            self._logger = logging.getLogger()
 | 
			
		||||
        super().__init__(
 | 
			
		||||
            immudUrl=immudb_address,
 | 
			
		||||
            rs=root_service,
 | 
			
		||||
            publicKeyFile=public_key_file,
 | 
			
		||||
            timeout=timeout,
 | 
			
		||||
            max_grpc_message_length=max_grpc_message_length,
 | 
			
		||||
        )
 | 
			
		||||
        self.login(
 | 
			
		||||
            username=self.username,
 | 
			
		||||
            password=self.password,
 | 
			
		||||
        )
 | 
			
		||||
        self.useDatabase(self.encode(self.database))
 | 
			
		||||
 | 
			
		||||
    def encode(
 | 
			
		||||
        self,
 | 
			
		||||
        value: Union[str, bytes, dict],
 | 
			
		||||
    ) -> bytes:
 | 
			
		||||
        if isinstance(value, str):
 | 
			
		||||
            result = value.encode()
 | 
			
		||||
        elif isinstance(value, bytes):
 | 
			
		||||
            result = value
 | 
			
		||||
        elif isinstance(value, dict):
 | 
			
		||||
            result = json.dumps(value).encode()
 | 
			
		||||
        else:
 | 
			
		||||
            raise ValueError(
 | 
			
		||||
                "Cannot encode value that isn't str, bytes or dict."
 | 
			
		||||
            )
 | 
			
		||||
        return result
 | 
			
		||||
 | 
			
		||||
    def to_dict(
 | 
			
		||||
        self,
 | 
			
		||||
        response: SafeGetResponse,
 | 
			
		||||
    ) -> Dict:
 | 
			
		||||
        result = asdict(response)
 | 
			
		||||
        result['key'] = result['key'].decode()
 | 
			
		||||
        result['value'] = json.loads(result['value'].decode())
 | 
			
		||||
        return result
 | 
			
		||||
 | 
			
		||||
    def get_size_format(
 | 
			
		||||
        self,
 | 
			
		||||
        value: int,
 | 
			
		||||
        factor: int = 1024,
 | 
			
		||||
        suffix: str = "B",
 | 
			
		||||
    ) -> str:
 | 
			
		||||
        """
 | 
			
		||||
        Scale bytes to its proper byte format
 | 
			
		||||
        e.g:
 | 
			
		||||
| 
					
	
	
	
	
	
	
	
	 | 
			||||
            1253656 => '1.20 MB'
 | 
			
		||||
            1253656678 => '1.17 GB'
 | 
			
		||||
        """
 | 
			
		||||
        for unit in ["", "K", "M", "G", "T", "P", "E", "Z"]:
 | 
			
		||||
            if value < factor:
 | 
			
		||||
                return f"{value:.2f} {unit}{suffix}"
 | 
			
		||||
            value /= factor
 | 
			
		||||
        return f"{value:.2f} Y{suffix}"
 | 
			
		||||
 | 
			
		||||
    def get_directory_size(self, path: Union[str, os.PathLike]) -> int:
 | 
			
		||||
        return sum(file.stat().st_size for file in Path(path).rglob('*'))
 | 
			
		||||
 | 
			
		||||
    def get_file_size(self, file_path: Union[str, os.PathLike]) -> int:
 | 
			
		||||
        return Path(file_path).stat().st_size
 | 
			
		||||
 | 
			
		||||
    def get_hasher(self, checksum_type: str = 'sha256'):
 | 
			
		||||
        """
 | 
			
		||||
        Returns a corresponding hashlib hashing function for the specified
 | 
			
		||||
        checksum type.
 | 
			
		||||
 | 
			
		||||
        Parameters
 | 
			
		||||
        ----------
 | 
			
		||||
        checksum_type : str
 | 
			
		||||
            Checksum type (e.g. sha1, sha256).
 | 
			
		||||
 | 
			
		||||
        Returns
 | 
			
		||||
| 
					
	
	
	
	
	
	
	
	 
				
					
						soksanichenko
						commented  
			
		For the future For the future
https://pypi.org/project/humanize/ 
			
			
		 | 
			||||
        -------
 | 
			
		||||
        hashlib._Hash
 | 
			
		||||
            Hashlib hashing function.
 | 
			
		||||
        """
 | 
			
		||||
        return hashlib.new(checksum_type)
 | 
			
		||||
 | 
			
		||||
    def hash_file(
 | 
			
		||||
        self,
 | 
			
		||||
        file_path: Union[str, IO],
 | 
			
		||||
        hash_type: str = 'sha256',
 | 
			
		||||
        buff_size: int = 1048576,
 | 
			
		||||
        hasher=None,
 | 
			
		||||
    ) -> str:
 | 
			
		||||
        """
 | 
			
		||||
        Returns checksum (hexadecimal digest) of the file.
 | 
			
		||||
 | 
			
		||||
        Parameters
 | 
			
		||||
        ----------
 | 
			
		||||
        file_path : str or file-like
 | 
			
		||||
            File to hash. It could be either a path or a file descriptor.
 | 
			
		||||
        hash_type : str
 | 
			
		||||
            Hash type (e.g. sha1, sha256).
 | 
			
		||||
        buff_size : int
 | 
			
		||||
            Number of bytes to read at once.
 | 
			
		||||
        hasher : hashlib._Hash
 | 
			
		||||
            Any hash algorithm from hashlib.
 | 
			
		||||
 | 
			
		||||
        Returns
 | 
			
		||||
        -------
 | 
			
		||||
        str
 | 
			
		||||
            Checksum (hexadecimal digest) of the file.
 | 
			
		||||
        """
 | 
			
		||||
        if hasher is None:
 | 
			
		||||
            hasher = self.get_hasher(hash_type)
 | 
			
		||||
 | 
			
		||||
        def feed_hasher(_fd):
 | 
			
		||||
            buff = _fd.read(buff_size)
 | 
			
		||||
            while len(buff):
 | 
			
		||||
                if not isinstance(buff, bytes):
 | 
			
		||||
                    buff = buff.encode('utf')
 | 
			
		||||
                hasher.update(buff)
 | 
			
		||||
                buff = _fd.read(buff_size)
 | 
			
		||||
 | 
			
		||||
        if isinstance(file_path, str):
 | 
			
		||||
            with open(file_path, "rb") as fd:
 | 
			
		||||
                feed_hasher(fd)
 | 
			
		||||
        else:
 | 
			
		||||
            file_path.seek(0)
 | 
			
		||||
            feed_hasher(file_path)
 | 
			
		||||
        return hasher.hexdigest()
 | 
			
		||||
 | 
			
		||||
    def hash_content(
 | 
			
		||||
        self,
 | 
			
		||||
        content: Union[str, bytes],
 | 
			
		||||
    ) -> str:
 | 
			
		||||
        hasher = self.get_hasher()
 | 
			
		||||
        if isinstance(content, str):
 | 
			
		||||
            content = content.encode()
 | 
			
		||||
        hasher.update(content)
 | 
			
		||||
        return hasher.hexdigest()
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
| 
					
	
	
	
	
	
	
	
	 
				
					
						soksanichenko
						commented  
			
		Why didn't you use utf-8? Why didn't you use utf-8? 
			
			
		 | 
			||||
    def extract_git_metadata(
 | 
			
		||||
        repo_path: Union[str, os.PathLike],
 | 
			
		||||
    ) -> Dict:
 | 
			
		||||
        with Repo(repo_path) as repo:
 | 
			
		||||
            url = urlparse(repo.remote().url)
 | 
			
		||||
            commit = repo.commit()
 | 
			
		||||
            name = (
 | 
			
		||||
                f'git@{url.netloc}'
 | 
			
		||||
                f'{re.sub(r"^/", ":", url.path)}'
 | 
			
		||||
                f'@{commit.hexsha[:7]}'
 | 
			
		||||
            )
 | 
			
		||||
            return {
 | 
			
		||||
                'Name': name,
 | 
			
		||||
                'git': {
 | 
			
		||||
                    'Author': {
 | 
			
		||||
                        'Email': commit.author.email,
 | 
			
		||||
                        'Name': commit.author.name,
 | 
			
		||||
                        'When': commit.authored_datetime.strftime(
 | 
			
		||||
                            '%Y-%m-%dT%H:%M:%S%z',
 | 
			
		||||
                        ),
 | 
			
		||||
                    },
 | 
			
		||||
                    'Commit': commit.hexsha,
 | 
			
		||||
                    'Committer': {
 | 
			
		||||
                        'Email': commit.committer.email,
 | 
			
		||||
                        'Name': commit.committer.name,
 | 
			
		||||
                        'When': commit.committed_datetime.strftime(
 | 
			
		||||
                            '%Y-%m-%dT%H:%M:%S%z',
 | 
			
		||||
                        ),
 | 
			
		||||
                    },
 | 
			
		||||
                    'Message': commit.message,
 | 
			
		||||
                    'PGPSignature': commit.gpgsig,
 | 
			
		||||
                    'Parents': [
 | 
			
		||||
                        parent.hexsha for parent in commit.iter_parents()
 | 
			
		||||
                    ],
 | 
			
		||||
                    'Tree': commit.tree.hexsha,
 | 
			
		||||
                },
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def default_metadata(self) -> Dict:
 | 
			
		||||
        return {
 | 
			
		||||
            'sbom_api_ver': '0.2',
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def verified_get(
 | 
			
		||||
        self,
 | 
			
		||||
        key: Union[str, bytes],
 | 
			
		||||
        revision: Optional[int] = None,
 | 
			
		||||
    ) -> Dict:
 | 
			
		||||
        try:
 | 
			
		||||
            return self.to_dict(
 | 
			
		||||
                self.verifiedGet(
 | 
			
		||||
                    key=self.encode(key),
 | 
			
		||||
                    atRevision=revision,
 | 
			
		||||
                ),
 | 
			
		||||
            )
 | 
			
		||||
        except RpcError:
 | 
			
		||||
            return {'error': format_exc()}
 | 
			
		||||
 | 
			
		||||
    def verified_set(
 | 
			
		||||
        self,
 | 
			
		||||
        key: Union[str, bytes],
 | 
			
		||||
        value: Union[str, bytes, Dict],
 | 
			
		||||
    ) -> Dict:
 | 
			
		||||
        try:
 | 
			
		||||
            return asdict(
 | 
			
		||||
                self.verifiedSet(
 | 
			
		||||
                    key=self.encode(key),
 | 
			
		||||
                    value=self.encode(value),
 | 
			
		||||
                ),
 | 
			
		||||
            )
 | 
			
		||||
        except RpcError:
 | 
			
		||||
            return {'error': format_exc()}
 | 
			
		||||
 | 
			
		||||
    def notarize(
 | 
			
		||||
        self,
 | 
			
		||||
        key: str,
 | 
			
		||||
        value: Union[str, bytes, Dict],
 | 
			
		||||
    ) -> Dict:
 | 
			
		||||
        return self.verified_set(key, value)
 | 
			
		||||
 | 
			
		||||
    def notarize_file(
 | 
			
		||||
        self,
 | 
			
		||||
        file: str,
 | 
			
		||||
        user_metadata: Dict,
 | 
			
		||||
    ) -> Dict:
 | 
			
		||||
        hash_file = self.hash_file(file)
 | 
			
		||||
        payload = {
 | 
			
		||||
            'Name': Path(file).name,
 | 
			
		||||
            'Kind': 'file',
 | 
			
		||||
            'Size': self.get_size_format(self.get_file_size(file)),
 | 
			
		||||
            'Hash': hash_file,
 | 
			
		||||
            'Metadata': {
 | 
			
		||||
                **self.default_metadata,
 | 
			
		||||
                **user_metadata,
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
        return self.notarize(
 | 
			
		||||
            key=hash_file,
 | 
			
		||||
            value=payload,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def notarize_git_repo(
 | 
			
		||||
        self,
 | 
			
		||||
        repo_path: Union[str, os.PathLike],
 | 
			
		||||
        user_metadata: Dict,
 | 
			
		||||
    ) -> Dict:
 | 
			
		||||
        git_metadata = self.extract_git_metadata(repo_path)
 | 
			
		||||
        metadata_hash = self.hash_content(json.dumps(git_metadata['git']))
 | 
			
		||||
        payload = {
 | 
			
		||||
            'Name': git_metadata['Name'],
 | 
			
		||||
            'Kind': 'git',
 | 
			
		||||
            'Size': self.get_size_format(self.get_directory_size(repo_path)),
 | 
			
		||||
            'Hash': metadata_hash,
 | 
			
		||||
            'Metadata': {
 | 
			
		||||
                'git': git_metadata['git'],
 | 
			
		||||
                **self.default_metadata,
 | 
			
		||||
                **user_metadata,
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
        return self.notarize(
 | 
			
		||||
            key=metadata_hash,
 | 
			
		||||
            value=payload,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def authenticate(
 | 
			
		||||
        self,
 | 
			
		||||
        key: Union[str, bytes],
 | 
			
		||||
    ) -> Dict:
 | 
			
		||||
        return self.verified_get(key)
 | 
			
		||||
 | 
			
		||||
    def authenticate_file(self, file: str) -> Dict:
 | 
			
		||||
        return self.authenticate(self.hash_file(file))
 | 
			
		||||
 | 
			
		||||
    def authenticate_git_repo(
 | 
			
		||||
        self,
 | 
			
		||||
        repo_path: Union[str, os.PathLike],
 | 
			
		||||
    ) -> Dict:
 | 
			
		||||
        metadata_hash = self.hash_content(
 | 
			
		||||
            json.dumps(
 | 
			
		||||
                self.extract_git_metadata(repo_path)['git'],
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        return self.authenticate(metadata_hash)
 | 
			
		||||
							
								
								
									
										26
									
								
								setup.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								setup.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,26 @@
 | 
			
		||||
from setuptools import setup
 | 
			
		||||
 | 
			
		||||
setup(
 | 
			
		||||
    name='immudb_wrapper',
 | 
			
		||||
    version='0.1.0',
 | 
			
		||||
    author='Daniil Anfimov',
 | 
			
		||||
    author_email='anfimovdan@gmail.com',
 | 
			
		||||
    description='The wrapper around binary `immudbclient` from Codenotary.',
 | 
			
		||||
    url='https://git.almalinux.org/almalinux/immudb_wrapper',
 | 
			
		||||
    project_urls={
 | 
			
		||||
        'Bug Tracker': 'https://git.almalinux.org/almalinux/immudb_wrapper/issues',
 | 
			
		||||
    },
 | 
			
		||||
    classifiers=[
 | 
			
		||||
        'Programming Language :: Python :: 3',
 | 
			
		||||
        'License :: OSI Approved :: '
 | 
			
		||||
        'GNU General Public License v3 or later (GPLv3+)',
 | 
			
		||||
        'Operating System :: OS Independent',
 | 
			
		||||
    ],
 | 
			
		||||
    py_modules=['immudb_wrapper'],
 | 
			
		||||
    scripts=['immudb_wrapper.py'],
 | 
			
		||||
    install_requires=[
 | 
			
		||||
        'GitPython>=3.1.20',
 | 
			
		||||
        'immudb-py>=1.4.0'
 | 
			
		||||
    ],
 | 
			
		||||
    python_requires='>=3.6',
 | 
			
		||||
)
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	
I guess better to move the list of keys to a separate var and use them in the loop