Source code for pyhandle.client.dbhandleclient

"""
This class provides different methods to manage Handles using the database interface.

Author: Sofiane Bendoukha (DKRZ), 2017
"""
from __future__ import absolute_import

import binascii
import codecs
import logging
import sys
import uuid
import pymysql

from pyhandle.clientcredentials import PIDClientCredentials
from pyhandle.dbhsexceptions import DBHandleNotFoundException, DBHandleKeyNotFoundException, \
    DBHandleAlreadyExistsException, DBHandleKeyNotSpecifiedException
from pyhandle.pyhandleclient import HandleClient

from .. import util
from ..util import timeutil

LOGGER = logging.getLogger(__name__)
LOGGER.addHandler(util.NullHandler())


class DBHandleClient(HandleClient):
    _handle_db_connection = None
    _handle_db_cur = None
    HANDLE_CLIENT = 'db'

[docs] def __init__(self, credentials, **args): ''' Initialize the DB client. Instantiate a connection object and connect to the HS database :param db_host: host where the handle server database is located :param db_user: username to log in as :param db_password: password for db_user :param db_name: database name ''' LOGGER.debug('\n' + 60 * '*' + '\nInstantiation of DBHandleClient\n' + 60 * '*') if credentials is None: raise ValueError('No credentials given') if isinstance(credentials, PIDClientCredentials): self.credentials = credentials.get_all_args() else: self.credentials = credentials self.db_host = self.credentials['db_host'] self.db_user = self.credentials['db_user'] self.db_password = self.credentials['db_password'] self.db_name = self.credentials['db_name'] try: self._handle_db_connection = pymysql.connect(self.db_host, self.db_user, self.db_password, self.db_name, cursorclass=pymysql.cursors.DictCursor) self._handle_db_cur = self._handle_db_connection.cursor() except pymysql.InternalError as error: code, message = error.args print(">>>>>>>>>>>>>", code, message) sys.exit(1)
def execute_query(self, query): ''' Execute the SQL query. :param query: the sql query to be performed :return: result_as_dict: the result of the sql query as dictionary ''' LOGGER.debug('Execute query') result = None self.query = query try: with self._handle_db_cur as cursor: self_handle_db_cur = self._handle_db_connection.cursor() self_handle_db_cur.execute(self.query) result = self_handle_db_cur.fetchall() self._handle_db_connection.commit() LOGGER.debug('Query result %s', result) except pymysql.InternalError as error: code, message = error.args print(">>>>>>>>>>>>>", code, message) return result
[docs] def search_handle(self, pattern=None, limit=None, offset=None, **args): ''' Search for handles containing the specified key with the specified value (one key-value). The number of record can be controlled by limit and offset. :param pattern: Optional. This value can be set to do search depending on the handle name. :param limit: Optional. Limit the number of results returned in a SQL statement. :param offset: Optional. Page number, first row to return. Initial raw is 0. :param args: e.g URL='www.example.com' :return: A list of all Handles (list of strings) that bear the given key with given value of given prefix or server. The list may be empty and may also contain more than one element. ''' query_result = [] list_handles = [] # Check if there is any key-value pairs to be searched. if len(args) == 0: LOGGER.debug('search_handle: No key value pair was specified.') msg = 'No search terms have been specified. Please specify' + \ ' at least one key-value-pair.' raise DBHandleKeyNotSpecifiedException(msg=msg) if limit: if offset is None: offset = 0 list_handles= self.__search_handle_limit( pattern=pattern, limit=limit, offset=offset, **args ) else: for key in args.keys(): key = key value = args[key] if pattern: query = "SELECT handle from handles WHERE handle LIKE '%s' AND type= '%s' AND data='%s'" % (pattern, key, value) else: query = "SELECT handle from handles WHERE type= '%s' AND data LIKE '%s'" % (key, value) query_result = self.execute_query(query) for key in range(len(query_result)): list_handles.append(query_result[key]['handle']) return list_handles
def __search_handle_limit(self, pattern, limit, offset, **args): ''' Search handles with limit and offset. :param pattern: Optional. This value can be set to do search depending on the handle name. :param limit: Optional. Number of records to return from the SQL statement. :param offset: Page number, first row to return. Initial raw is 0. :param args: search items as key-value e.g URL='www.example.com' :return: list_handles: A list of all Handles (list of strings) that bear the given key with given value of given prefix or server. The list may be empty and may also contain more than one element. ''' query_result = [] list_handles = [] # Check if there is any key-value pairs to be searched. if len(args) == 0: LOGGER.debug('search_handle: No key value pair was specified.') msg = 'No search terms have been specified. Please specify' + \ ' at least one key-value-pair.' raise DBHandleKeyNotSpecifiedException(msg=msg) for key in args.keys(): key = key value = args[key] if pattern: query = "SELECT handle from handles WHERE handle LIKE '%s' AND type= '%s' AND data='%s' LIMIT %s" \ " OFFSET %s" % (pattern, key, value, limit, offset) else: query = "SELECT handle from handles WHERE type= '%s' AND data LIKE '%s' LIMIT %s OFFSET %s" % (key, value, limit, offset) query_result = self.execute_query(query) for key in range(len(query_result)): list_handles.append(query_result[key]['handle']) return list_handles #TODO
[docs] def search_handle_multiple_keys(self, **args): ''' Search for handles containing the specified key(s) with the specified value(s). :param args: Several search fields and values can be specified as key-value-pairs, e.g. CHECKSUM=123456, URL=www.foo.com :return: A list of all Handles (list of strings) that bear the given key with given value of given prefix or server. The list may be empty and may also contain more than one element. ''' list_handles = [] temp_list = [] # Check if there is any key-value pairs to be searched. if len(args) == 0: LOGGER.debug('search_handle: No key value pair was specified.') msg = 'No search terms have been specified. Please specify' + \ ' at least one key-value-pair.' raise DBHandleKeyNotSpecifiedException(msg=msg) for key, value in args.items(): query = "SELECT handle from handles WHERE type LIKE '%s' AND data LIKE '%s'" % (key, value) query_result = self.get_query_from_user(query) if query_result is None: break for k in range(len(query_result)): if query_result[k]['handle'] in list_handles: temp_list.append(query_result[k]['handle']) list_handles.append(query_result[k]['handle']) return temp_list
def execute_query_customized(self, handle, key=None, query=None): ''' Execute the customized SQL query. :param key: Optional. :param handle: Handle name :param query: the sql query to be performed :return: Query result as list of dictionaries ''' LOGGER.debug('Get handle, key and execute query') result = None try: self_handle_db_cur = self._handle_db_connection.cursor() self_handle_db_cur.execute(query, (handle, key)) result = self_handle_db_cur.fetchall() LOGGER.debug('Query result %s', result) except pymysql.InternalError as error: code, message = error.args print(">>>>>>>>>>>>>", code, message) return result def convert_query_result_to_dict(self, query_result): ''' Convert the query result (list) to dictionary :param query_result: the result of the sql query (list) :return: The result of the query as a dictionary (No HS values) ''' LOGGER.debug('Convert query result to dict') result_as_dict = {} hs_keys = ['HS_ADMIN', 'HS_SITE', 'HS_PUBKEY', 'HS_SECKEY', 'HS_ALIAS', 'HS_VLIST', 'HS_SERV'] all_keys = list(set().union(*(d.keys() for d in query_result))) skip = False for key in range(len(query_result)): skiped_key = query_result[key]['type'].decode('utf-8') if skiped_key in hs_keys: skip = True continue if skip: skip = False continue temp_dict = {query_result[key]['type'].decode('utf-8'): query_result[key]['data'].decode('utf-8')} result_as_dict.update(temp_dict) LOGGER.debug('Query result %s', result_as_dict) return result_as_dict def check_if_handle_exists(self, handle): ''' This statement is used to query whether or not a given handle exists in the database :param handle: The handle being queried. :return: True if handle exists. ''' query_result_as_dict = {} query = "SELECT count(*) FROM handles WHERE handle= '%s'" % handle query_result = self.execute_query(query) query_result_as_dict = query_result[0] query_result_as_dict = query_result_as_dict['count(*)'] LOGGER.debug('Query result %s', query_result_as_dict) if query_result_as_dict != 0: return True else: return False def generate_PID_name(self, prefix=None): ''' Generate a unique random Handle name (random UUID). The Handle is not registered. If a prefix is specified, the PID name has the syntax <prefix>/<generatedname>, otherwise it just returns the generated random name (suffix for the Handle). :param prefix: Optional. The prefix to be used for the Handle name. :return: The handle name in the form <prefix>/<generatedsuffix> or <generatedsuffix>. ''' LOGGER.debug('generate_PID_name...') randomuuid = uuid.uuid4() if prefix is not None: return prefix + '/' + str(randomuuid) else: return str(randomuuid)
[docs] def generate_and_register_handle(self, prefix, location, checksum=None, **extratypes): ''' Register a new Handle with a unique random name (random UUID). :param prefix: The prefix of the handle to be registered. The method will generate a suffix. :param location: The URL of the data entity to be referenced. :param checksum: Optional. The checksum string. :param extratypes: Optional. Additional key value pairs as dict. :return: The new handle name. ''' LOGGER.debug('generate_and_register_handle...') handle = self.generate_PID_name(prefix) handle = self.register_handle( handle, location, checksum, overwrite=True, **extratypes ) return handle
[docs] def register_handle(self, handle, url, overwrite=False, **args): ''' Register a new Handle with given name. If the handle already exists and overwrite is not set to True, the method will throw an exception. :param handle: The full name of the handle to be registered (prefix and suffix) :param url: The URL of the data entity to be referenced :param overwrite: Optional. If set to True, an existing handle record will be overwritten. Defaults to False. :param args: Mandatory key_value parameters for the admin of the Handle being created. Example: admin_handle = 'prefix/suffix'. admin_handle_index = 200. perm = '111111111111' :raises: :exc:`~pyhandle.handleexceptions.HandleAlreadyExistsException` Only if overwrite is not set or set to False. ''' LOGGER.debug('register_handle...') # Get values for HS_ADMIN self.admin_handle = args['admin_handle'] self.admin_handle_index = args['admin_handle_index'] self.perm = args['permissions'] # Set current UNIX time ts = timeutil.generate_timestamp() handle_exists = self.check_if_handle_exists(handle) # default idx for url idx = 1 # If already exists and can't be overwritten: if not overwrite: if handle_exists: msg = 'Could not register handle' LOGGER.error(msg + ', as it already exists.') raise DBHandleAlreadyExistsException(handle=handle, msg=msg) if handle_exists: self.delete_handle(handle) # Create handle without HS_ADMIN query = "INSERT INTO handles (handle, idx, type, data, ttl_type, ttl, timestamp, refs, admin_read, " \ "admin_write, pub_read, " \ "pub_write) values (" \ "'%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s')" % (handle, idx, 'URL', url, '0', '86400', ts, '', '1', '1', '1', '0') self.execute_query(query) # Add HS_ADMIN values (default values 0.NA/prefix) self.add_admin_entry(handle, self.admin_handle, self.admin_handle_index, self.perm)
def add_handle_value(self, handle, key): ''' Add a key-value pair from a handle record. :param handle: Handle from whose record they entry should be deleted. :param key: Key to be deleted. ''' LOGGER.debug('Add handle value') if self.check_if_handle_exists(handle): query = "INSERT INTO handles WHERE handle= '%s' AND type='%s'" % (handle, key) self.execute_query_customized(handle, key, query)
[docs] def get_value_from_handle(self, handle, key): ''' Retrieve a single value from a single handle. :param handle: The handle to take the value from. :param key: The key. :raises: :exc:`~pyhandle.dbhsexceptions.DBHandleNotFoundException` :raises: :exc:`~pyhandle.dbhsexceptions.DBHandleKeyNotFoundException` :return: A string containing the value. ''' LOGGER.debug('Get value from handle') query = "SELECT data FROM handles WHERE handle= '%s' AND type='%s'" % (handle, key) handle_records = self.execute_query(query) handle_record_exists = self.check_if_handle_exists(handle) key_exists = self.check_if_key_exists(handle, key) if handle_record_exists: if key_exists: result = handle_records[0] handle_value = result['data'] return handle_value else: msg = 'Key not found in Handle' raise DBHandleKeyNotFoundException(handle=handle, key=key, msg=msg) else: msg = 'Cannot modify unexisting handle' raise DBHandleNotFoundException(handle=handle, msg=msg)
[docs] def delete_handle(self, handle): ''' Delete handle and its handle record. :param handle: Handle being deleted :raises: :exc:`~pyhandle.dbhsexceptions.DBHandleNotFoundException` ''' LOGGER.debug('delete handle') handle_exists = self.check_if_handle_exists(handle) # delete query if handle_exists: query = "DELETE FROM handles WHERE handle= '%s'" % handle self.execute_query(query) else: msg = 'Handle does not exists' raise DBHandleNotFoundException(handle=handle)
[docs] def delete_handle_value(self, handle, key): ''' Delete a key-value pair from a handle record. If the key exists more than once, all key-value pairs with this key are deleted. :param handle: Handle from whose record the entry should be deleted. :param key: Key to be deleted. Also accepts a list of keys. ''' LOGGER.debug('delete_handle_value...') key_exists = self.check_if_key_exists(handle, key) if key_exists: query = "DELETE FROM handles WHERE handle= '%s' AND type='%s'" % (handle, key) self.execute_query(query) else: msg = 'Key not found in Handle' raise DBHandleKeyNotFoundException(handle=handle, key=key, msg=msg)
[docs] def get_list_of_idx(self, handle): ''' Create a list of indices already used. :param handle: the handle to extract the indexes from. :return: list_idx: List of indices ''' list_idx = [] query = "SELECT idx FROM handles WHERE handle='%s'" % handle query_result = self.execute_query(query) for key in range(len(query_result)): list_idx.append(query_result[key]['idx']) return list_idx
[docs] def list_all_handles(self): ''' Get a list of all of the handles in the database. :return: A list of all handles in the table ''' list_all_handles = [] query = 'SELECT DISTINCT handle FROM handles' result_query = self.execute_query(query) for key in range(len(result_query)): list_all_handles.append(str(result_query[key]['handle'])) return list_all_handles
def list_handles_by_prefix(self, prefix): ''' Get a list of handles in the database that have a given prefix. :param prefix: The prefix, including the slash ('/') character. :raises: :exc:`~pyhandle.handleexceptions.HandleNotFoundOnDBException` :return: List of handles ''' query = "SELECT DISTINCT handle FROM handles WHERE handle LIKE '%s'" % prefix handle_records = self.execute_query(query) return handle_records def retrieve_handle_record_without_HS_values(self, handle): ''' Retrieve a handle record from the Handle server database as a dict. :param handle: The handle whose record is to be retrieved. :raises: :exc:`~pyhandle.dbhsexceptions.DBHandleNotFoundException` :return: The handle record values as a list. ''' LOGGER.info("Retrieving handle record (db)") handle_record_exists = self.check_if_handle_exists(handle) if handle_record_exists: query = "SELECT type, data FROM handles WHERE handle= '%s'" % handle handle_records = self.execute_query(query) else: msg = 'Handle not found' raise DBHandleNotFoundException(handle=handle) handle_records_as_dict = self.convert_query_result_to_dict(handle_records) return handle_records_as_dict
[docs] def modify_handle_value(self, handle, ttl=None, add_if_not_exists=True, **kvpairs): ''' This statement is used to update a single handle value with new values. The value to update is identified by the handle and index. Modify entries (key-value pairs). :param handle: Handle whose record is to be modified :param ttl: Optional. Integer value. If ttl should be set to a non-default value. :param kvpairs: The user can specify several key-value-pairs. These will be the handle value types and values that will be modified. The keys are the names or the handle value types (e.g. "URL"). The values are the new values to store in "data". :param add_if_not_exists: Optional. :raises: :exc:`~pyhandle.dbhsexceptions.DBHandleNotFoundException` ''' LOGGER.info("Modify handle value (db)") handle_key = list(kvpairs.keys()) handle_value = list(kvpairs.values()) # check if handle and the key already exist handle_record_exists = self.check_if_handle_exists(handle) key_exists = self.check_if_key_exists(handle, handle_key[0]) if handle_record_exists: if key_exists: # update the value of the key idx_key = self.get_idx_existing_key(handle, handle_key[0]) query = "UPDATE handles set data = '%s' WHERE handle = '%s' and idx = '%s'" % (handle_value[0], handle, idx_key) self.execute_query(query) elif add_if_not_exists: LOGGER.debug('modify_handle_value: Adding entry "' + str(handle_key) + '"' + \ ' to handle ' + handle) self.create_new_value(handle, handle_key=handle_key[0], handle_value=handle_value[0]) else: msg = 'Cannot modify unexisting handle' raise DBHandleNotFoundException(handle=handle, msg=msg)
def get_idx_existing_key(self, handle, key): query = "SELECT idx FROM handles WHERE handle='%s' AND type= '%s'" % (handle, key) query_result = self.execute_query(query) list_idx = query_result[0]['idx'] return list_idx def create_new_value(self, handle, **kvpairs): ''' Add new handle value. :param: handle: The handle in which the value will be added :param: **kvpairs: any other key-value pairs ''' ts = timeutil.generate_timestamp() if kvpairs is not None: self.handle_key = str(kvpairs['handle_key']) self.handle_value = str(kvpairs['handle_value']) newidx = self.create_new_index(handle) query = "INSERT INTO handles (idx, handle, type, data, ttl_type, ttl, timestamp, refs, admin_read, " \ "admin_write, pub_read, pub_write) VALUES ('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', " \ "'%s', '%s', '%s')" % ( newidx, handle, self.handle_key, self.handle_value, '0', '86400', ts, '', '1', '1', '1', '0') self.execute_query(query) def add_admin_entry(self, handle, admin_handle, admin_handle_index, perm): ''' Create an HS_ADMIN entry. The admin value is byte array constant, which means that all handle created use the same HS_ADMIN value. :param handle: The handle in which the HS-ADMIN value is added :param admin_handle: The administrator of the Handle (prefix/suffx) :param admin_handle_index: The index of the admin_handle :param perm: The permissions of the administrator of the handle in form '101001010100' ''' index_length = 8 hsadmin_index = hex(admin_handle_index)[2:] # Add missing values to index hex for k in range(index_length - len(hsadmin_index)): hsadmin_index = "0" + hsadmin_index admin_perm = hex(int(perm, 2))[2:] if sys.version_info[0] >= 3: admin_handle = codecs.encode(admin_handle.encode(), 'hex').decode('UTF-8') else: admin_handle = admin_handle.encode('hex') hsadmin_hex_value = '0' + admin_perm + "0000000f" + admin_handle + hsadmin_index admin_idx = '100' ts = timeutil.generate_timestamp() query = "INSERT INTO handles (idx, handle, type, data, ttl_type, ttl, timestamp, refs, admin_read, " \ "admin_write, " \ "pub_read, pub_write) VALUES ('%s', '%s', '%s', UNHEX('%s'), '%s', '%s', '%s', '%s', '%s', '%s', '%s', " \ "'%s')" \ % (admin_idx, handle, 'HS_ADMIN', hsadmin_hex_value, '0', '86400', ts, '', '1', '1', '1', '0') self.execute_query(query) def create_new_index(self, handle, url=False, hs_admin=False): ''' Find an index not yet used in the handle record and not reserved for any (other) special type. :param handle: The handle in which indices will be allocated :param url: If True, an index for an URL entry is returned (1, unless it is already in use). :param hs_admin: If True, an index for HS_ADMIN is returned (100 or one of the following). :return: An integer. ''' start = 2 # reserved indices: reserved_for_url = set([1]) reserved_for_admin = set(range(100, 200)) prohibited_indices = reserved_for_url | reserved_for_admin if url: prohibited_indices = prohibited_indices - reserved_for_url start = 1 elif hs_admin: prohibited_indices = prohibited_indices - reserved_for_admin start = 100 # existing indices list_of_indices_already_used = set(self.get_list_of_idx(handle)) # find new index: all_prohibited_indices = list_of_indices_already_used | prohibited_indices searchmax = max(start, max(all_prohibited_indices)) + 2 for index in range(start, searchmax): if index not in all_prohibited_indices: return index def check_if_value_exists(self, handle, **kvpairs): self.handle = handle handle_record = self.retrieve_handle_record_without_HS_values(self.handle) handle_key = kvpairs['handle_key'] value_exists = False if handle_key[0] in handle_record.keys(): return True else: return False def check_if_key_exists(self, handle, key): ''' Check if a given key exists in a handle record. :param handle: The handle in which the key is searched :param key: The key :return: True if key exists ''' query_result = self.retrieve_handle_record_without_HS_values(handle) handle_keys = list(query_result.keys()) if key in handle_keys: return True return False def retrieve_handle_record_json(self, handle): ''' This statement is used to retrieve the set of handle values associated with a handle from the database. :param handle: The name of the handle to be queried. :return: handle_records: This contains idx positive integer value: unique across all values for the handle type alphanumeric value: indicates the type of the data in each value data alphanumeric value; the data associated with the value ttl type byte/short; 0=relative, 1=absolute ttl numeric: cache timeout for this value in seconds (if ttl type is absolute, then this indicates the date/time of expiration in seconds since Jan 1 0:00:00 1970. timestamp numeric: the date that this value was last modified, in seconds since Jan 1 0:00:00 1970 refs alphanumeric: list of tab delimited index:handle pairs. In each pair, tabs that occur in the handle part are escaped as \t. admin read boolean: indicates whether clients with administrative privileges have access to retrieve the handle value admin write boolean: indicates whether clients with administrative privileges have permission to modify the handle value pub read boolean: indicates whether all clients have permission to retrieve the handle value pub write boolean: indicates whether all clients have permission to modify the handle value ''' # pylint: disable=missing-docstring query = "SELECT idx, type, data, ttl, timestamp FROM handles WHERE handle = '%s'" % handle handle_records = self.execute_query(query) return handle_records def get_query_from_user(self, query): ''' Execute the SQL query formulated by user. :param query: the sql query to be performed :return: The handle record ''' handle_records = self.execute_query(query) LOGGER.debug('Get query from user %s', handle_records) return handle_records
[docs] @staticmethod def create_list_of_queries(query): ''' Create a list of queries, which will be executed in a specific order. :param query: The query to be added to the list. :return: A list of queries ''' list_queries = [] list_queries.append(query) return list_queries
@staticmethod def pretty_print(record): ''' Print the query result as a table. :param record: The result of the query as dict. ''' for key, value in record.items(): print("----------------------------------------") print("{:<15} {:<25}".format(key, value))
[docs] @staticmethod def connection_status(handle_db_connection): ''' Check whether a connection to DB is open. :param handle_db_connection: :return: True if connection is open ''' if handle_db_connection: return True return False
def _db_disconnect(self): ''' Close the connection to the database. ''' self._handle_db_connection.close()
[docs] def retrieve_handle_record(self, handle): ''' Retrieve all Handle record values including HS_ADMIN Extract HS_ADMIN value from the query result and converts it to Hex. :param handle: The handle from which the values are retrieved. :return: handle_records: All values of the Handle including HS_ADMIN. ''' scale = 16 num_of_bits = 12 hsadmin = '' handle_records = self.retrieve_handle_record_without_HS_values(handle) handle_records_json = self.retrieve_handle_record_json(handle) # Get value of HS_ADMIN for k in range(len(handle_records_json)): if handle_records_json[k]['type'] == b'HS_ADMIN': hsadmin = handle_records_json[k]['data'] hexhsadmin = binascii.hexlify(hsadmin) hs_admin_index = self.get_hs_admin_index(hexhsadmin) length_record = len(hexhsadmin) admin_handle = binascii.unhexlify(hexhsadmin[12:int(length_record - 8)]).decode('utf-8') # Get and convert permissions permissions = hexhsadmin[:4] perm_bin = bin(int(permissions, scale))[2:].zfill(num_of_bits) temp_dict = {'handle': admin_handle, 'index': hs_admin_index, 'permissions': perm_bin} handle_records.update({'HS_ADMIN': temp_dict}) return handle_records
@staticmethod def get_hs_admin_index(hexhsadmin): hexadmin_length = len(hexhsadmin) hs_admin_index = int(hexhsadmin[hexadmin_length - 8:hexadmin_length], 16) return hs_admin_index def get_permissions_from_hsadmin_hex(self, handle): ''' :param handle: The handle for which the permissions are retrieved. :return: permission: Bit-like permissions ''' handle_record_hex = self.convert_hs_admin_values_to_hex(handle) permissions = handle_record_hex[:4] return permissions def convert_permissions_to_binary(self, handle): ''' Get permissions in HEX form and convert them to binary. :param handle: The Handle that contains the permissions :return: perm_bin ''' scale = 16 num_of_bits = 12 perm = self.get_permissions_from_hsadmin_hex(handle) perm_bin = bin(int(perm, scale))[2:].zfill(num_of_bits) return perm_bin