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