# Copyright 2026 jCloud Services GbR # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os from typing import Tuple, Literal, Sequence import ipaddress __all__ = [ 'is_number', 'int_to_bytes', 'bits_to_byte', 'byte_to_bits', 'get_next_lower_integer_multiple', 'find_nearest_lower_number', 'create_file_if_not_exists', 'is_valid_host', 'is_valid_port', 'validate_address_port' ] def is_number(string: str, base: int = 10) -> bool: ''' Checks whether the string is a number. :param string: The string :type string: str :param base: The base :type base: int :return: ``True`` if the string is a number, otherwise ``False`` :rtype: bool ''' if base == 0: return False if not string: return False try: int(string, base) return True except ValueError: return False def int_to_bytes(n: int, signed: bool = False) -> bytes: ''' Converts the integer into bytes (dynamic length). :param n: The integer :type n: int :param signed: Whether the integer is signed or not. :type signed: bool :return: The bytes :rtype: bytes ''' n = int(n) length = (n.bit_length() + 7) // 8 if length == 0: length = 1 return n.to_bytes(length, signed=signed) def _bits_to_byte(bits: Tuple[Literal[0, 1], Literal[0, 1], Literal[0, 1], Literal[0, 1], Literal[0, 1], Literal[0, 1], Literal[0, 1], Literal[0, 1]]) -> bytes: if len(bits) != 8: raise ValueError('bits must be a tuple of 8 bits') byte = 0 for bit in bits: if int(bit) not in (0, 1): raise ValueError('each bit must be 0 or 1') byte = (byte << 1) | int(bit) return byte def bits_to_byte(*bits: Literal[0, 1]) -> bytes: ''' Converts the bits into a single byte. :param bits: The bits :type bits: int :raises ValueError: If there are more than 8 bits specified or a bit is neither ``0`` nor ``1``. :return: The byte :rtype: bytes ''' if len(bits) > 8: raise ValueError('a maximum of 8 bits can be converted to a byte') while len(bits) < 8: bits = (0,) + bits return bytes([_bits_to_byte(bits)]) def byte_to_bits(b: bytes) -> Tuple[Literal[0, 1], Literal[0, 1], Literal[0, 1], Literal[0, 1], Literal[0, 1], Literal[0, 1], Literal[0, 1], Literal[0, 1]]: ''' Converts the byte to a tuple of bits. :param b: The byte :type b: bytes :raises ValueError: If more than one byte is specified or ``b`` is neither a bytestring nor a bytearray, :return: The bits :rtype: tuple ''' if not isinstance(b, (bytes, bytearray)) or len(b) != 1: raise ValueError('input must be a single byte') byte = b[0] return tuple((byte >> i) & 1 for i in reversed(range(8))) def get_next_lower_integer_multiple(value: int, multiple: int) -> int: ''' Returns the next lower integer multiple. :param value: The value :type value: int :param multiple: The factor :type multiple: int :return: The next lower integer multiple :rtype: int ''' return value - (value % multiple) def find_nearest_lower_number(number_list: Sequence, target: int | float) -> int | float: ''' Finds the nearest lower number to the target from the list. :param number_list: The list :type number_list: Sequence :param target: The target :type target: int | float :return: The nearest lower number to the target from the list. :rtype: int | float ''' nearest = None for number in number_list: if number <= target: if nearest is None or number > nearest: nearest = number return nearest def create_file_if_not_exists(path: str, content: bytes = b''): ''' Creates a file if it does not exist. :param path: The file path :type path: str :param content: Optional, the content of the file if it is newly created. :type content: bytes ''' if not os.path.exists(path): with open(path, 'wb') as f: f.write(content) f.close() def is_valid_host(host: str) -> bool: ''' Checks whether the host is a valid IP address. :param host: The host :type host: str :return: ``True`` if the host is a valid IP address, otherwise ``False`` :rtype: bool ''' try: ipaddress.ip_address(host) return True except ValueError: pass def is_valid_port(port: str) -> bool: ''' Checks whether the port is a valid port number. :param port: The port :type port: str :return: ``True`` if the port is a valid port number, otherwise ``False`` :rtype: bool ''' if not port.isdigit(): return False port_num = int(port) return 1 <= port_num <= 65535 def validate_address_port(addr: str) -> bool: if ':' not in addr: return False if addr.startswith('['): try: host, port = addr.rsplit(']:', 1) host = host[1:] except ValueError: return False else: host, port = addr.rsplit(':', 1) return is_valid_host(host) and is_valid_port(port)