diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml index bbd10d13..335a4a98 100644 --- a/.github/workflows/auto-label.yml +++ b/.github/workflows/auto-label.yml @@ -27,7 +27,7 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, issue_number: item.number, - labels: ['gssoc-ext', 'hacktoberfest-accepted'] + labels: ['gssoc-ext'] }); const addLabel = async (label) => { await github.rest.issues.addLabels({ @@ -36,4 +36,4 @@ jobs: issue_number: item.number, labels: [label] }); - }; + }; \ No newline at end of file diff --git a/DIRECTORY.md b/DIRECTORY.md index 7c3a76b4..e69de29b 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -1 +0,0 @@ -/home/runner/work/_temp/6c91ad44-979f-45e2-97d0-57dc14af8567.sh: line 1: scripts/build_directory_md.py: No such file or directory diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..f3c3575f --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,47 @@ +# Security Policy + +## Reporting a Vulnerability + +We take the security of our project seriously. If you discover a security vulnerability, please follow these steps: + +1. **DO NOT** create a public GitHub issue for the vulnerability. +2. Send a report to our team through our Discord server: https://discord. +3. Provide as much information as possible about the vulnerability: + - Type of issue + - Full paths of source file(s) related to the issue + - Location of the affected source code + - Any special configuration required to reproduce the issue + - Step-by-step instructions to reproduce the issue + - Proof-of-concept or exploit code (if possible) + - Impact of the issue + +## Response Timeline + +- We will acknowledge receipt of your vulnerability report within 48 hours. +- We will provide a more detailed response within 7 days. +- We will work on fixing the vulnerability and will keep you informed of our progress. +- Once the vulnerability is fixed, we will publicly disclose the security issue. + +## Supported Versions + +We will address security vulnerabilities in the following versions: + +| Version | Supported | +| ------- | ------------------ | +| latest | :white_check_mark: | + +## Best Practices + +- Please give us reasonable time to address the issue before making any public disclosure. +- Act in good faith towards our users' privacy and data. +- Do not access or modify other users' data without explicit permission. + +## Recognition + +We appreciate the security research community's efforts in helping keep our project safe. Responsible disclosure of vulnerabilities helps us ensure the security and privacy of our users. + +## Contact + +For any security-related concerns, please contact us through: +- Discord: https://discord. +Thank you for helping keep our community safe! \ No newline at end of file diff --git a/Tests/Cipher_algorithms/test_Rail_Fence_Cipher.py b/Tests/Cipher_algorithms/test_Rail_Fence_Cipher.py new file mode 100644 index 00000000..681b4ea0 --- /dev/null +++ b/Tests/Cipher_algorithms/test_Rail_Fence_Cipher.py @@ -0,0 +1,31 @@ +import unittest +from pysnippets.Cipher_algorithms.Rail_Fence_Cipher import rail_fence_cipher_encrypt, rail_fence_cipher_decrypt + +class TestRailFenceCipher(unittest.TestCase): + + def test_encrypt(self): + self.assertEqual(rail_fence_cipher_encrypt("HELLO", 3), "HOELL") + self.assertEqual(rail_fence_cipher_encrypt("WEAREDISCOVEREDFLEEATONCE", 3), "WECRLTEERDSOEEFEAOCAIVDEN") + + def test_decrypt(self): + self.assertEqual(rail_fence_cipher_decrypt("HOELL", 3), "HELLO") + self.assertEqual(rail_fence_cipher_decrypt("WECRLTEERDSOEEFEAOCAIVDEN", 3), "WEAREDISCOVEREDFLEEATONCE") + + def test_invalid_input_encrypt(self): + with self.assertRaises(ValueError): + rail_fence_cipher_encrypt(12345, 3) + with self.assertRaises(ValueError): + rail_fence_cipher_encrypt("HELLO", "three") + with self.assertRaises(ValueError): + rail_fence_cipher_encrypt("HELLO", -1) + + def test_invalid_input_decrypt(self): + with self.assertRaises(ValueError): + rail_fence_cipher_decrypt(12345, 3) + with self.assertRaises(ValueError): + rail_fence_cipher_decrypt("HOELL", "three") + with self.assertRaises(ValueError): + rail_fence_cipher_decrypt("HOELL", -1) + +if __name__ == '__main__': + unittest.main() diff --git "a/Tests/Cipher_algorithms/test_Vigen\303\250re_Cipher.py" "b/Tests/Cipher_algorithms/test_Vigen\303\250re_Cipher.py" new file mode 100644 index 00000000..8e28fa73 --- /dev/null +++ "b/Tests/Cipher_algorithms/test_Vigen\303\250re_Cipher.py" @@ -0,0 +1,38 @@ +import unittest +from ...pysnippets.Cipher_algorithms.Vigenère_Cipher import VigenereCipher + + +class TestVigenereCipher(unittest.TestCase): + def setUp(self): + self.cipher = VigenereCipher("LEMON") + + def test_encrypt(self): + plaintext = "ATTACK AT DAWN" + expected_ciphertext = "LXFOPV EF RNHR" + self.assertEqual(self.cipher.encrypt(plaintext), expected_ciphertext) + + def test_decrypt(self): + ciphertext = "LXFOPV EF RNHR" + expected_plaintext = "ATTACKATDAWN" + self.assertEqual(self.cipher.decrypt(ciphertext), expected_plaintext) + + def test_non_alpha_key(self): + with self.assertRaises(ValueError): + VigenereCipher("LEMON123") + + def test_empty_string(self): + self.assertEqual(self.cipher.encrypt(""), "") + self.assertEqual(self.cipher.decrypt(""), "") + + def test_case_insensitivity(self): + plaintext = "attack at dawn" + expected_ciphertext = "LXFOPV EF RNHR" + self.assertEqual(self.cipher.encrypt(plaintext), expected_ciphertext) + + def test_special_characters_in_plaintext(self): + plaintext = "ATTACK! AT DAWN." + expected_ciphertext = "LXFOPV EF RNHR" + self.assertEqual(self.cipher.encrypt(plaintext), expected_ciphertext) + +if __name__ == "__main__": + unittest.main() diff --git a/Tests/Cipher_algorithms/test_affine_cipher.py b/Tests/Cipher_algorithms/test_affine_cipher.py new file mode 100644 index 00000000..e6d72463 --- /dev/null +++ b/Tests/Cipher_algorithms/test_affine_cipher.py @@ -0,0 +1,17 @@ +import unittest +from pysnippets.Cipher_algorithms.affine_cipher import AffineCipher + +class TestAffineCipher(unittest.TestCase): + def setUp(self): + self.cipher = AffineCipher(a=5, b=8) + + def test_encrypt(self): + self.assertEqual(self.cipher.encrypt('HELLO'), 'MJQQT') + self.assertEqual(self.cipher.encrypt('AFFINE CIPHER'), 'IHHWVC SWFRCP') + + def test_decrypt(self): + self.assertEqual(self.cipher.decrypt('MJQQT'), 'HELLO') + self.assertEqual(self.cipher.decrypt('IHHWVC SWFRCP'), 'AFFINE CIPHER') + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/Cipher_algorithms/test_caesar_cipher.py b/Tests/Cipher_algorithms/test_caesar_cipher.py new file mode 100644 index 00000000..9fcfbc7e --- /dev/null +++ b/Tests/Cipher_algorithms/test_caesar_cipher.py @@ -0,0 +1,74 @@ +import unittest +import sys +import os + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../..'))) +from pysnippets.Cipher_algorithms.caesar_cipher import CaesarCipher + +class TestCaesarCipher(unittest.TestCase): + def setUp(self): + self.cipher = CaesarCipher() + + def test_default_encryption(self): + message = "Hello, World!" + expected = "Khoor, Zruog!" + self.assertEqual(self.cipher.encrypt(message), expected) + + def test_default_decryption(self): + encrypted = "Khoor, Zruog!" + expected = "Hello, World!" + self.assertEqual(self.cipher.decrypt(encrypted), expected) + + def test_custom_shift(self): + custom_cipher = CaesarCipher(shift=5) + message = "Python Programming" + encrypted = custom_cipher.encrypt(message) + decrypted = custom_cipher.decrypt(encrypted) + self.assertEqual(decrypted, message) + + def test_non_alphabetic_characters(self): + message = "Hello, World! 123" + encrypted = self.cipher.encrypt(message) + decrypted = self.cipher.decrypt(encrypted) + self.assertEqual(message, decrypted) + + def test_brute_force_decryption(self): + message = "Hello, World!" + encrypted = self.cipher.encrypt(message) + decryptions = self.cipher.brute_force_decrypt(encrypted) + found_decryption = any( + decryption == message for _, decryption in decryptions + ) + self.assertTrue(found_decryption) + + def test_full_cycle(self): + message = "The quick brown fox jumps over the lazy dog!" + encrypted = self.cipher.encrypt(message) + decrypted = self.cipher.decrypt(encrypted) + self.assertEqual(message, decrypted) + + def test_empty_string(self): + message = "" + encrypted = self.cipher.encrypt(message) + decrypted = self.cipher.decrypt(encrypted) + self.assertEqual(message, decrypted) + + def test_large_shift(self): + custom_cipher = CaesarCipher(shift=30) + message = "Large Shift" + encrypted = custom_cipher.encrypt(message) + decrypted = custom_cipher.decrypt(encrypted) + self.assertEqual(decrypted, message) + + def test_negative_shift(self): + custom_cipher = CaesarCipher(shift=-3) + message = "Negative Shift" + encrypted = custom_cipher.encrypt(message) + decrypted = custom_cipher.decrypt(encrypted) + self.assertEqual(decrypted, message) + + def test_non_string_input(self): + with self.assertRaises(TypeError): + self.cipher.encrypt(12345) + with self.assertRaises(TypeError): + self.cipher.decrypt(12345) diff --git a/Tests/Cipher_algorithms/test_hill_cipher.py b/Tests/Cipher_algorithms/test_hill_cipher.py new file mode 100644 index 00000000..1fe7600e --- /dev/null +++ b/Tests/Cipher_algorithms/test_hill_cipher.py @@ -0,0 +1,32 @@ +import unittest +from pysnippets.Cipher_algorithms.hill_cipher import HillCipher + +class TestHillCipher(unittest.TestCase): + def setUp(self): + self.key_matrix = [[6, 24, 1], [13, 16, 10], [20, 17, 15]] + self.cipher = HillCipher(self.key_matrix) + + def test_encrypt(self): + plaintext = "ACT" + expected_ciphertext = "POH" + self.assertEqual(self.cipher.encrypt(plaintext), expected_ciphertext) + + def test_decrypt(self): + ciphertext = "POH" + expected_plaintext = "ACT" + self.assertEqual(self.cipher.decrypt(ciphertext), expected_plaintext) + + def test_invalid_key_matrix(self): + with self.assertRaises(ValueError): + HillCipher([[1, 2], [3, 4]]) + + def test_invalid_plaintext_length(self): + with self.assertRaises(ValueError): + self.cipher.encrypt("ACTG") + + def test_invalid_ciphertext_length(self): + with self.assertRaises(ValueError): + self.cipher.decrypt("POHG") + +if __name__ == "__main__": + unittest.main() diff --git a/Tests/Cipher_algorithms/test_playfair_cipher.py b/Tests/Cipher_algorithms/test_playfair_cipher.py new file mode 100644 index 00000000..323f06a3 --- /dev/null +++ b/Tests/Cipher_algorithms/test_playfair_cipher.py @@ -0,0 +1,17 @@ +import unittest +from pysnippets.Cipher_algorithms.playfair_cipher import playfair_encrypt, playfair_decrypt + +class TestPlayfairCipher(unittest.TestCase): + + def test_playfair_encrypt(self): + self.assertEqual(playfair_encrypt("HELLO", "KEYWORD"), "RIJVS") + self.assertEqual(playfair_encrypt("PLAYFAIR", "KEYWORD"), "RLBMZIXR") + self.assertEqual(playfair_encrypt("HACKTOBERFEST", "KEYWORD"), "RIBKZQKZBFXZ") + + def test_playfair_decrypt(self): + self.assertEqual(playfair_decrypt("RIJVS", "KEYWORD"), "HELXLO") + self.assertEqual(playfair_decrypt("RLBMZIXR", "KEYWORD"), "PLAYFAIR") + self.assertEqual(playfair_decrypt("RIBKZQKZBFXZ", "KEYWORD"), "HACKTOBERFEST") + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/algorithms/test_bidirectional_bfs.py b/Tests/algorithms/test_bidirectional_bfs.py new file mode 100644 index 00000000..b397fcc6 --- /dev/null +++ b/Tests/algorithms/test_bidirectional_bfs.py @@ -0,0 +1,50 @@ +import unittest +from pysnippets.algorithms.bidirectional_bfs import bidirectional_search +class TestBidirectionalBFS(unittest.TestCase): + + def setUp(self): + self.graph = { + 'A': ['B', 'C'], + 'B': ['A', 'D', 'E'], + 'C': ['A', 'F'], + 'D': ['B'], + 'E': ['B', 'F'], + 'F': ['C', 'E', 'G'], + 'G': ['F'] + } + + def test_no_path(self): + disconnected_graph = { + 'A': ['B'], + 'B': ['A'], + 'C': ['D'], + 'D': ['C'] + } + path = bidirectional_search(disconnected_graph, 'A', 'D') + self.assertIsNone(path) + + def test_path(self): + path = bidirectional_search(self.graph, 'A', 'G') + self.assertEqual(path, ['A', 'C', 'F', 'G']) + + def test_disconnected_graph(self): + two_node_graph = {'A': [], 'B': []} + path = bidirectional_search(two_node_graph, 'A', 'B') + self.assertIsNone(path) + + def test_invalid_node(self): + with self.assertRaises(ValueError): + bidirectional_search(self.graph, 'A', 'Z') + + def test_single_node_graph(self): + single_node_graph = {'A': []} + path = bidirectional_search(single_node_graph, 'A', 'A') + self.assertEqual(path, ['A']) + + def test_graph_is_dictionary(self): + invalid_graph = ['hello'] + with self.assertRaises(TypeError): + bidirectional_search(invalid_graph, 'hello', 'hello') + + + diff --git a/pysnippets/Cipher_algorithms/Rail_Fence_Cipher.py b/pysnippets/Cipher_algorithms/Rail_Fence_Cipher.py new file mode 100644 index 00000000..47e2a05f --- /dev/null +++ b/pysnippets/Cipher_algorithms/Rail_Fence_Cipher.py @@ -0,0 +1,99 @@ +# Constants for placeholder characters +EMPTY_CHAR = '\n' # Used in encryption matrix +MARKER_CHAR = '*' # Used in decryption matrix + +def toggle_direction(row, key, dir_down): + """ + Helper function to toggle direction in the rail matrix. + + Parameters: + - row (int): Current row index. + - key (int): Total number of rails (key). + - dir_down (bool): Current direction. + + Returns: + - tuple: Updated row and direction. + """ + if row == 0: + dir_down = True + elif row == key - 1: + dir_down = False + return row + (1 if dir_down else -1), dir_down + +def rail_fence_cipher_encrypt(text, key): + """ + Encrypts the given text using the Rail Fence Cipher method. + + Parameters: + - text (str): The text to encrypt. + - key (int): The number of rails to use in the cipher. + + Returns: + - str: The encrypted text. + """ + if not isinstance(text, str) or not isinstance(key, int): + raise ValueError("Invalid input types. 'text' must be a string and 'key' must be an integer.") + if key <= 0: + raise ValueError("Key must be a positive integer.") + + # Initialize the rail matrix + rail = [[EMPTY_CHAR for _ in range(len(text))] for _ in range(key)] + dir_down = False + row, col = 0, 0 + + # Fill the rail matrix with characters in zig-zag fashion + for char in text: + rail[row][col] = char + col += 1 + row, dir_down = toggle_direction(row, key, dir_down) + + # Collect the encrypted text + result = [rail[i][j] for i in range(key) for j in range(len(text)) if rail[i][j] != EMPTY_CHAR] + return "".join(result) + + +def rail_fence_cipher_decrypt(cipher, key): + """ + Decrypts the given cipher text using the Rail Fence Cipher method. + + Parameters: + - cipher (str): The encrypted text to decrypt. + - key (int): The number of rails to use in the cipher. + + Returns: + - str: The decrypted text. + """ + if not isinstance(cipher, str) or not isinstance(key, int): + raise ValueError("Invalid input types. 'cipher' must be a string and 'key' must be an integer.") + if key <= 0: + raise ValueError("Key must be a positive integer.") + + # Initialize the rail matrix + rail = [[EMPTY_CHAR for _ in range(len(cipher))] for _ in range(key)] + dir_down = None + row, col = 0, 0 + + # Mark the pattern path in the rail matrix + for _ in range(len(cipher)): + rail[row][col] = MARKER_CHAR + col += 1 + row, dir_down = toggle_direction(row, key, dir_down) + + # Place characters in the marked positions + index = 0 + for i in range(key): + for j in range(len(cipher)): + if rail[i][j] == MARKER_CHAR and index < len(cipher): + rail[i][j] = cipher[index] + index += 1 + + # Read the matrix in zig-zag fashion to decrypt + result = [] + row, col = 0, 0 + for _ in range(len(cipher)): + if rail[row][col] != MARKER_CHAR: + result.append(rail[row][col]) + col += 1 + row, dir_down = toggle_direction(row, key, dir_down) + + return "".join(result) \ No newline at end of file diff --git "a/pysnippets/Cipher_algorithms/Vigen\303\250re_Cipher.py" "b/pysnippets/Cipher_algorithms/Vigen\303\250re_Cipher.py" new file mode 100644 index 00000000..ecbdb401 --- /dev/null +++ "b/pysnippets/Cipher_algorithms/Vigen\303\250re_Cipher.py" @@ -0,0 +1,77 @@ +class VigenereCipher: + def __init__(self, key: str): + """Initialize the Vigenère cipher with a key. + + Args: + key (str): The key for the cipher, must be alphabetic. + + Raises: + ValueError: If the key is not alphabetic. + """ + if not key.isalpha(): + raise ValueError("Key must consist of alphabetic characters only.") + self.key = key.upper() + + def _format_text(self, text: str) -> str: + """Format the text by filtering non-alphabetic characters and converting to uppercase. + + Args: + text (str): The text to format. + + Returns: + str: Formatted text containing only uppercase alphabetic characters. + """ + return ''.join(filter(str.isalpha, text)).upper() + + def _extend_key(self, text: str) -> str: + """Extend the key to match the length of the text. + + Args: + text (str): The text to match the key against. + + Returns: + str: The extended key. + """ + if not text: # Check for empty text + raise ValueError("Text cannot be empty.") + key_length = len(self.key) + extended_key = (self.key * (len(text) // key_length)) + self.key[:len(text) % key_length] + return extended_key + + def encrypt(self, plaintext: str) -> str: + """Encrypt the plaintext using the Vigenère cipher. + + Args: + plaintext (str): The plaintext to encrypt. + + Returns: + str: The encrypted ciphertext. + """ + formatted_text = self._format_text(plaintext) + extended_key = self._extend_key(formatted_text) + ciphertext = [] + + for p, k in zip(formatted_text, extended_key): + encrypted_char = chr(((ord(p) - ord('A') + ord(k) - ord('A')) % 26) + ord('A')) + ciphertext.append(encrypted_char) + + return ''.join(ciphertext) + + def decrypt(self, ciphertext: str) -> str: + """Decrypt the ciphertext using the Vigenère cipher. + + Args: + ciphertext (str): The ciphertext to decrypt. + + Returns: + str: The decrypted plaintext. + """ + formatted_text = self._format_text(ciphertext) + extended_key = self._extend_key(formatted_text) + plaintext = [] + + for c, k in zip(formatted_text, extended_key): + decrypted_char = chr(((ord(c) - ord('A') - (ord(k) - ord('A'))) % 26) + ord('A')) + plaintext.append(decrypted_char) + + return ''.join(plaintext) \ No newline at end of file diff --git a/pysnippets/Cipher_algorithms/affine_cipher.py b/pysnippets/Cipher_algorithms/affine_cipher.py new file mode 100644 index 00000000..62d0adb8 --- /dev/null +++ b/pysnippets/Cipher_algorithms/affine_cipher.py @@ -0,0 +1,70 @@ +class AffineCipher: + """ + A class to implement the Affine Cipher for encryption and decryption. + """ + + ALPHABET_LENGTH = 26 # Length of the alphabet + + def __init__(self, a, b): + """ + Initializes the Affine Cipher with specified coefficients. + + Parameters: + - a (int): The multiplicative key (must be coprime with ALPHABET_LENGTH). + - b (int): The additive key. + """ + if not self.is_valid_key(a): + raise ValueError(f"The value of 'a' ({a}) must be coprime with {self.ALPHABET_LENGTH}.") + + self.a = a + self.b = b + self.m = self.ALPHABET_LENGTH # Length of the alphabet + + def is_valid_key(self, a): + """ + Checks if 'a' is coprime with the alphabet length. + + Parameters: + - a (int): The multiplicative key. + + Returns: + - bool: True if 'a' is coprime with ALPHABET_LENGTH, False otherwise. + """ + return self.gcd(a, self.m) == 1 + + def gcd(self, x, y): + """Compute the greatest common divisor of x and y.""" + while y: + x, y = y, x % y + return x + + def encrypt(self, plaintext): + """ + Encrypts the plaintext using the affine cipher. + + Parameters: + - plaintext (str): The text to encrypt. + + Returns: + - str: The encrypted ciphertext. + """ + return ''.join( + chr(((self.a * (ord(char) - ord('A')) + self.b) % self.m) + ord('A')) + if char.isalpha() else char for char in plaintext.upper() + ) + + def decrypt(self, ciphertext): + """ + Decrypts the ciphertext using the affine cipher. + + Parameters: + - ciphertext (str): The text to decrypt. + + Returns: + - str: The decrypted plaintext. + """ + a_inv = pow(self.a, -1, self.m) # Modular multiplicative inverse of a + return ''.join( + chr(((a_inv * ((ord(char) - ord('A')) - self.b)) % self.m) + ord('A')) + if char.isalpha() else char for char in ciphertext.upper() + ) \ No newline at end of file diff --git a/pysnippets/Cipher_algorithms/caesar_cipher.py b/pysnippets/Cipher_algorithms/caesar_cipher.py new file mode 100644 index 00000000..6e518b62 --- /dev/null +++ b/pysnippets/Cipher_algorithms/caesar_cipher.py @@ -0,0 +1,108 @@ +import string + +ALPHABET_LENGTH = 26 + +class CaesarCipher: + """ + A Caesar Cipher class that supports encryption, decryption, and brute-force decryption. + """ + + def __init__(self, shift=3): + """ + Initializes the Caesar cipher with a given shift value. + + Parameters: + - shift (int): The number of positions each letter is shifted in the alphabet. + """ + self.shift = shift % ALPHABET_LENGTH + self.encrypt_table = self._create_translation_table(self.shift) + self.decrypt_table = self._create_translation_table(-self.shift) + + def _create_translation_table(self, shift): + """ + Creates a translation table for encryption or decryption. + + Parameters: + - shift (int): The number of positions each letter is shifted in the alphabet. + + Returns: + - dict: A translation table for str.translate(). + """ + lower = string.ascii_lowercase + upper = string.ascii_uppercase + lower_shifted = lower[shift:] + lower[:shift] + upper_shifted = upper[shift:] + upper[:shift] + trans_table = str.maketrans( + lower + upper, + lower_shifted + upper_shifted + ) + return trans_table + + def encrypt(self, message): + """ + Encrypts a message using the Caesar cipher. + + Parameters: + - message (str): The plaintext message to encrypt. + + Returns: + - str: The encrypted message. + """ + if not isinstance(message, str): + raise TypeError("Message must be a string") + return message.translate(self.encrypt_table) + + def decrypt(self, encrypted_message): + """ + Decrypts a message using the Caesar cipher. + + Parameters: + - encrypted_message (str): The encrypted message to decrypt. + + Returns: + - str: The decrypted message. + """ + if not isinstance(encrypted_message, str): + raise TypeError("Encrypted message must be a string") + return encrypted_message.translate(self.decrypt_table) + + def brute_force_decrypt(self, encrypted_message): + """ + Attempts to brute-force decrypt an encrypted message by trying all possible shifts. + + Parameters: + - encrypted_message (str): The encrypted message to decrypt. + + Returns: + - list: A list of tuples with shift values and corresponding decrypted messages. + """ + possible_decryptions = [] + for potential_shift in range(ALPHABET_LENGTH): + temp_table = self._create_translation_table(-potential_shift) + decrypted = encrypted_message.translate(temp_table) + possible_decryptions.append((potential_shift, decrypted)) + return possible_decryptions + +def main(): + cipher = CaesarCipher(shift=3) + original_message = "Hello, World!" + print("Original Message:", original_message) + + # Encrypt the message + encrypted_message = cipher.encrypt(original_message) + print("Encrypted Message:", encrypted_message) + + # Decrypt the message + decrypted_message = cipher.decrypt(encrypted_message) + print("Decrypted Message:", decrypted_message) + + # Brute-force decryption + print("\nBrute Force Decryption:") + possible_decryptions = cipher.brute_force_decrypt(encrypted_message) + for shift, decryption in possible_decryptions: + print(f"Shift {shift}: {decryption}") + print("-" * 30) + +# Run the main function if this file is executed +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/pysnippets/Cipher_algorithms/hill_cipher.py b/pysnippets/Cipher_algorithms/hill_cipher.py new file mode 100644 index 00000000..d7eb1a3d --- /dev/null +++ b/pysnippets/Cipher_algorithms/hill_cipher.py @@ -0,0 +1,45 @@ +import numpy as np + +class HillCipher: + def __init__(self, key_matrix): + self.key_matrix = np.array(key_matrix) + self.modulus = 26 + self.check_key_matrix() + + def check_key_matrix(self): + if self.key_matrix.shape[0] != self.key_matrix.shape[1]: + raise ValueError("Key matrix must be square") + if np.linalg.det(self.key_matrix) == 0: + raise ValueError("Key matrix must be invertible") + + def encrypt(self, plaintext): + plaintext = plaintext.upper().replace(" ", "") + if len(plaintext) % self.key_matrix.shape[0] != 0: + raise ValueError("Plaintext length must be a multiple of key matrix size") + + plaintext_vector = [ord(char) - ord('A') for char in plaintext] + plaintext_matrix = np.array(plaintext_vector).reshape(-1, self.key_matrix.shape[0]) + + encrypted_matrix = np.dot(plaintext_matrix, self.key_matrix) % self.modulus + encrypted_text = ''.join(chr(int(num) + ord('A')) for num in encrypted_matrix.flatten()) + + return encrypted_text + + def decrypt(self, ciphertext): + ciphertext = ciphertext.upper().replace(" ", "") + if len(ciphertext) % self.key_matrix.shape[0] != 0: + raise ValueError("Ciphertext length must be a multiple of key matrix size") + + ciphertext_vector = [ord(char) - ord('A') for char in ciphertext] + ciphertext_matrix = np.array(ciphertext_vector).reshape(-1, self.key_matrix.shape[0]) + + inverse_key_matrix = np.linalg.inv(self.key_matrix) + adjugate_matrix = np.round(inverse_key_matrix * np.linalg.det(self.key_matrix)).astype(int) % self.modulus + determinant = int(np.round(np.linalg.det(self.key_matrix))) % self.modulus + determinant_inv = pow(determinant, -1, self.modulus) + inverse_key_matrix_mod = (determinant_inv * adjugate_matrix) % self.modulus + + decrypted_matrix = np.dot(ciphertext_matrix, inverse_key_matrix_mod) % self.modulus + decrypted_text = ''.join(chr(int(num) + ord('A')) for num in decrypted_matrix.flatten()) + + return decrypted_text diff --git a/pysnippets/Cipher_algorithms/playfair_cipher.py b/pysnippets/Cipher_algorithms/playfair_cipher.py new file mode 100644 index 00000000..b14f3bed --- /dev/null +++ b/pysnippets/Cipher_algorithms/playfair_cipher.py @@ -0,0 +1,124 @@ +# Constants +PADDING_CHAR = 'X' +REPLACEMENT_CHAR = 'I' + +def generate_key_table(key): + """ + Generates a 5x5 key table for the Playfair cipher. + + Parameters: + - key (str): The keyword to generate the table from. + + Returns: + - list: A 5x5 matrix of characters as the key table. + """ + # Remove duplicate characters in the key, preserving the order + key = ''.join(sorted(set(key), key=lambda x: key.index(x))) + # Fill the table with A-Z, excluding 'J' and already included key characters + key += ''.join(chr(i) for i in range(65, 91) if chr(i) not in key and chr(i) != 'J') + return [list(key[i:i+5]) for i in range(0, 25, 5)] + +def preprocess_text(text): + """ + Preprocesses the plaintext by converting to uppercase, replacing 'J' with 'I', + and adding padding characters where needed. + + Parameters: + - text (str): The plaintext message to preprocess. + + Returns: + - str: The processed text with padding. + """ + text = text.upper().replace('J', REPLACEMENT_CHAR) + processed_text = "" + i = 0 + while i < len(text): + processed_text += text[i] + # Add padding if two consecutive letters are the same + if i + 1 < len(text) and text[i] == text[i + 1]: + processed_text += PADDING_CHAR + elif i + 1 < len(text): + processed_text += text[i + 1] + i += 1 + i += 1 + # Append padding if the length is odd + if len(processed_text) % 2 != 0: + processed_text += PADDING_CHAR + return processed_text + +def find_position(char, key_table): + """ + Finds the position of a character in the key table. + + Parameters: + - char (str): The character to locate in the table. + - key_table (list): The 5x5 matrix key table. + + Returns: + - tuple: The row and column indices of the character. + """ + for i, row in enumerate(key_table): + if char in row: + return i, row.index(char) + return None + +def playfair_encrypt(plaintext, key): + """ + Encrypts the plaintext using the Playfair cipher with the provided key. + + Parameters: + - plaintext (str): The text to encrypt. + - key (str): The key for generating the cipher's key table. + + Returns: + - str: The encrypted ciphertext. + """ + key_table = generate_key_table(key) + plaintext = preprocess_text(plaintext) + ciphertext = "" + for i in range(0, len(plaintext), 2): + row1, col1 = find_position(plaintext[i], key_table) + row2, col2 = find_position(plaintext[i + 1], key_table) + if row1 == row2: + # Same row: shift right + ciphertext += key_table[row1][(col1 + 1) % 5] + ciphertext += key_table[row2][(col2 + 1) % 5] + elif col1 == col2: + # Same column: shift down + ciphertext += key_table[(row1 + 1) % 5][col1] + ciphertext += key_table[(row2 + 1) % 5][col2] + else: + # Rectangle swap + ciphertext += key_table[row1][col2] + ciphertext += key_table[row2][col1] + return ciphertext + +def playfair_decrypt(ciphertext, key): + """ + Decrypts the ciphertext using the Playfair cipher with the provided key. + + Parameters: + - ciphertext (str): The text to decrypt. + - key (str): The key for generating the cipher's key table. + + Returns: + - str: The decrypted plaintext. + """ + key_table = generate_key_table(key) + plaintext = "" + for i in range(0, len(ciphertext), 2): + row1, col1 = find_position(ciphertext[i], key_table) + row2, col2 = find_position(ciphertext[i + 1], key_table) + if row1 == row2: + # Same row: shift left + plaintext += key_table[row1][(col1 - 1) % 5] + plaintext += key_table[row2][(col2 - 1) % 5] + elif col1 == col2: + # Same column: shift up + plaintext += key_table[(row1 - 1) % 5][col1] + plaintext += key_table[(row2 - 1) % 5][col2] + else: + # Rectangle swap + plaintext += key_table[row1][col2] + plaintext += key_table[row2][col1] + return plaintext \ No newline at end of file diff --git a/pysnippets/Communication/send_email.py b/pysnippets/Communication/send_email.py index 88f7ff9d..db218995 100644 --- a/pysnippets/Communication/send_email.py +++ b/pysnippets/Communication/send_email.py @@ -1,50 +1,44 @@ import smtplib +import os from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart +from typing import Optional def send_email( - sender_email, - sender_password, - recepient_email, - subject, - body, - smtp_server="smtp.gmail.com", - smtp_port=587, -): + sender_email: str, + sender_password: str, + recipient_email: str, + subject: str, + body: str, + content_type: str = "plain", + smtp_server: str = "smtp.gmail.com", + smtp_port: int = 587, +) -> bool: """ - Sends an email using the provided SMTP server. + Sends an email to the specified recipient. Args: - sender_email (str): The email address of the sender. + sender_email (str): The sender's email address. sender_password (str): The password for the sender's email account. - recepient_mail (str): The email address of the recepient. - subject (str): The subject of the email. - body (str): The body of the email. - smtp_server (str, optional): The SMTP server to use. Defaults to 'smtp.gmail.com'. + recipient_email (str): The recipient's email address. + subject (str): The subject line of the email. + body (str): The content of the email. + content_type (str, optional): The format of the email body, either 'plain' or 'html'. Defaults to 'plain'. + smtp_server (str, optional): The SMTP server to connect to. Defaults to 'smtp.gmail.com'. smtp_port (int, optional): The port to use for the SMTP server. Defaults to 587. - Raises: - smtplib.SMTPAuthenticationError: If authentication fails. - smtplib.SMTPException: If there's an error sending the email. - - Example: - send_email( - "aashishnkumar@gmail.com", - "your_password", - "aashishnandakumar.official.in@gmail.com", - "Test Subject", - "This is a test email", - ) + Returns: + bool: True if the email is sent successfully, False otherwise. """ # create the email message message = MIMEMultipart() message["From"] = sender_email - message["To"] = recepient_email + message["To"] = recipient_email message["Subject"] = subject # attach the body of the email - message.attach(MIMEText(body, "plain")) + message.attach(MIMEText(body, content_type)) try: # create a secure SSL/TLS connection @@ -55,22 +49,35 @@ def send_email( # send the email server.send_message(message) print("Email sent successfully!") + return True except smtplib.SMTPAuthenticationError: - print( - "SMTP Authentication Error: The server didn't accept the username/password combination." - ) - raise + print("SMTP Authentication Error: Please check your credentials.") + except smtplib.SMTPConnectError: + print("SMTP Connection Error: Unable to connect to the SMTP server.") + except smtplib.SMTPServerDisconnected: + print("SMTP Disconnection Error: The server unexpectedly disconnected.") except smtplib.SMTPException as e: - print(f"SMTP Error: An error occured while sending the email: {str(e)}") - raise + print(f"SMTP Error: An error occurred while sending the email: {str(e)}") + except Exception as e: + print(f"Network Error: A network-related error occurred: {str(e)}") + + return False if __name__ == "__main__": - # NOTE: Never hardcode sensitive information like this in practice - send_email( - "sender@gmail.com", - "password", - "receiver@gmail.com", - "Test Subject", - "This is a test Email", - ) + # Load credentials from environment variables for security + sender_email = os.getenv("SENDER_EMAIL") + sender_password = os.getenv("SENDER_PASSWORD") + + # Ensure credentials are provided + if not sender_email or not sender_password: + print("Error: Please set the SENDER_EMAIL and SENDER_PASSWORD environment variables.") + else: + send_email( + sender_email=sender_email, + sender_password=sender_password, + recipient_email="receiver@gmail.com", + subject="Test Subject", + body="

This is a test email

This email contains HTML formatting.

", + content_type="html" + ) \ No newline at end of file diff --git a/pysnippets/Dynamic Programming/coin_change.py b/pysnippets/Dynamic Programming/coin_change.py index 2f3ae37f..b2359b66 100644 --- a/pysnippets/Dynamic Programming/coin_change.py +++ b/pysnippets/Dynamic Programming/coin_change.py @@ -1,14 +1,37 @@ def coin_change(coins, amount): + # Input validation + if not coins or amount < 0: + return -1 + dp = [float('inf')] * (amount + 1) dp[0] = 0 + coin_used = [-1] * (amount + 1) # To track coins used for coin in coins: for x in range(coin, amount + 1): - dp[x] = min(dp[x], dp[x - coin] + 1) + if dp[x - coin] + 1 < dp[x]: # If using the coin results in fewer coins + dp[x] = dp[x - coin] + 1 + coin_used[x] = coin # Track the coin used + + if dp[amount] == float('inf'): + return -1 + + # To find the coins used for the minimum coins + result_coins = [] + while amount > 0: + if coin_used[amount] == -1: # No coins could form this amount + break + result_coins.append(coin_used[amount]) + amount -= coin_used[amount] - return dp[amount] if dp[amount] != float('inf') else -1 + return dp[amount], result_coins if __name__ == "__main__": coins = [1, 2, 5] amount = 11 - print(coin_change(coins, amount)) # Output: 3 + min_coins, used_coins = coin_change(coins, amount) # Output: (3, [5, 5, 1]) + if min_coins != -1: + print(f"Minimum coins needed: {min_coins}") + print(f"Coins used: {used_coins}") + else: + print("Amount cannot be formed with the given coins.") \ No newline at end of file diff --git a/pysnippets/Dynamic Programming/edit_distance.py b/pysnippets/Dynamic Programming/edit_distance.py index e32c9f19..8635ec8b 100644 --- a/pysnippets/Dynamic Programming/edit_distance.py +++ b/pysnippets/Dynamic Programming/edit_distance.py @@ -1,21 +1,28 @@ def edit_distance(s1, s2): + # Input validation + if not isinstance(s1, str) or not isinstance(s2, str): + raise ValueError("Both inputs must be strings.") + m, n = len(s1), len(s2) dp = [[0] * (n + 1) for _ in range(m + 1)] for i in range(m + 1): for j in range(n + 1): if i == 0: - dp[i][j] = j + dp[i][j] = j # If s1 is empty, all characters of s2 need to be inserted elif j == 0: - dp[i][j] = i + dp[i][j] = i # If s2 is empty, all characters of s1 need to be deleted elif s1[i - 1] == s2[j - 1]: - dp[i][j] = dp[i - 1][j - 1] + dp[i][j] = dp[i - 1][j - 1] # Characters match else: - dp[i][j] = 1 + min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) + dp[i][j] = 1 + min(dp[i - 1][j], # Deletion + dp[i][j - 1], # Insertion + dp[i - 1][j - 1]) # Substitution return dp[m][n] if __name__ == "__main__": s1 = "horse" s2 = "ros" - print(edit_distance(s1, s2)) # Output: 3 + distance = edit_distance(s1, s2) # Output: 3 + print(f"The edit distance between '{s1}' and '{s2}' is: {distance}") diff --git a/pysnippets/Dynamic Programming/longest_common_subsequence.py b/pysnippets/Dynamic Programming/longest_common_subsequence.py index 5ccf34b4..211271c5 100644 --- a/pysnippets/Dynamic Programming/longest_common_subsequence.py +++ b/pysnippets/Dynamic Programming/longest_common_subsequence.py @@ -1,4 +1,8 @@ def longest_common_subsequence(x, y): + # Input validation + if not isinstance(x, str) or not isinstance(y, str): + raise ValueError("Both inputs must be strings.") + m, n = len(x), len(y) dp = [[0] * (n + 1) for _ in range(m + 1)] @@ -9,9 +13,24 @@ def longest_common_subsequence(x, y): else: dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) - return dp[m][n] + # Reconstructing the LCS + lcs = [] + while m > 0 and n > 0: + if x[m - 1] == y[n - 1]: + lcs.append(x[m - 1]) + m -= 1 + n -= 1 + elif dp[m - 1][n] > dp[m][n - 1]: + m -= 1 + else: + n -= 1 + + lcs.reverse() # Reverse the list to get the correct order + return dp[len(x)][len(y)], ''.join(lcs) if __name__ == "__main__": x = "AGGTAB" y = "GXTXAYB" - print(longest_common_subsequence(x, y)) # Output: 4 + length, sequence = longest_common_subsequence(x, y) # Output: (4, "GTAB") + print(f"The length of the longest common subsequence is: {length}") + print(f"The longest common subsequence is: '{sequence}'") \ No newline at end of file diff --git a/pysnippets/Dynamic Programming/maximum_subarray_sum.py b/pysnippets/Dynamic Programming/maximum_subarray_sum.py index bd39ce79..543fada1 100644 --- a/pysnippets/Dynamic Programming/maximum_subarray_sum.py +++ b/pysnippets/Dynamic Programming/maximum_subarray_sum.py @@ -1,4 +1,8 @@ def max_subarray_sum(nums): + # Input validation + if not isinstance(nums, list) or len(nums) == 0: + raise ValueError("Input must be a non-empty list of numbers.") + max_so_far = max_ending_here = nums[0] for x in nums[1:]: @@ -9,4 +13,5 @@ def max_subarray_sum(nums): if __name__ == "__main__": nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4] - print(max_subarray_sum(nums)) # Output: 6 + max_sum = max_subarray_sum(nums) # Output: 6 + print(f"The maximum sum of a contiguous subarray is: {max_sum}") \ No newline at end of file diff --git a/pysnippets/Dynamic Programming/two_city_scheduling.py b/pysnippets/Dynamic Programming/two_city_scheduling.py index 2bdabad9..7f438fac 100644 --- a/pysnippets/Dynamic Programming/two_city_scheduling.py +++ b/pysnippets/Dynamic Programming/two_city_scheduling.py @@ -1,4 +1,8 @@ def two_city_scheduling(costs): + # Input validation + if not isinstance(costs, list) or len(costs) % 2 != 0: + raise ValueError("Input must be a list of pairs with an even length.") + n = len(costs) // 2 dp = [[0] * (n + 1) for _ in range(n + 1)] @@ -14,4 +18,5 @@ def two_city_scheduling(costs): if __name__ == "__main__": costs = [[10, 20], [30, 200], [50, 30], [200, 500]] - print(two_city_scheduling(costs)) # Output: 370 + min_cost = two_city_scheduling(costs) # Output: 370 + print(f"The minimum cost to schedule people to two cities is: {min_cost}") \ No newline at end of file diff --git a/pysnippets/Dynamic Programming/unique_paths.py b/pysnippets/Dynamic Programming/unique_paths.py index 46520dde..045d0fd3 100644 --- a/pysnippets/Dynamic Programming/unique_paths.py +++ b/pysnippets/Dynamic Programming/unique_paths.py @@ -1,12 +1,17 @@ def unique_paths(m, n): - dp = [[1] * n for _ in range(m)] + # Input validation + if not (isinstance(m, int) and isinstance(n, int)) or m <= 0 or n <= 0: + raise ValueError("Both m and n must be positive integers.") + + dp = [1] * n # Only one row needed for storage, initialized with 1 for i in range(1, m): for j in range(1, n): - dp[i][j] = dp[i - 1][j] + dp[i][j - 1] + dp[j] += dp[j - 1] # Update dp[j] with paths from the cell above - return dp[m - 1][n - 1] + return dp[-1] # The last element contains the number of unique paths if __name__ == "__main__": m, n = 3, 7 - print(unique_paths(m, n)) # Output: 28 + num_paths = unique_paths(m, n) # Output: 28 + print(f"The number of unique paths in a grid of size {m} x {n} is: {num_paths}") \ No newline at end of file diff --git a/pysnippets/Hashing/Hashing.py b/pysnippets/Hashing/Hashing.py index c6ec1393..77f836d8 100644 --- a/pysnippets/Hashing/Hashing.py +++ b/pysnippets/Hashing/Hashing.py @@ -1,30 +1,52 @@ import hashlib +from typing import Any, List, Optional, Tuple class HashTable: - def __init__(self, size=10): + def __init__(self, size: int = 10) -> None: self.size = size - self.table = [[] for _ in range(self.size)] + self.table: List[List[Tuple[int, Any]]] = [[] for _ in range(self.size)] - def _hash_function(self, key): + def _hash_function(self, key: int) -> int: # Using simple modulo hash function return key % self.size - def insert(self, key, value): + def _resize(self) -> None: + old_table = self.table + self.size *= 2 + self.table = [[] for _ in range(self.size)] + + for bucket in old_table: + for key, value in bucket: + self.insert(key, value) + + def load_factor(self) -> float: + num_elements = sum(len(bucket) for bucket in self.table) + return num_elements / self.size + + def insert(self, key: int, value: Any) -> None: + if not isinstance(key, int): + raise TypeError("Key must be an integer.") + if self.load_factor() > 0.7: # Check load factor + self._resize() hash_key = self._hash_function(key) for pair in self.table[hash_key]: if pair[0] == key: pair[1] = value return - self.table[hash_key].append([key, value]) + self.table[hash_key].append((key, value)) - def search(self, key): + def search(self, key: int) -> Optional[Any]: + if not isinstance(key, int): + raise TypeError("Key must be an integer.") hash_key = self._hash_function(key) for pair in self.table[hash_key]: if pair[0] == key: return pair[1] return None - def delete(self, key): + def delete(self, key: int) -> bool: + if not isinstance(key, int): + raise TypeError("Key must be an integer.") hash_key = self._hash_function(key) for i, pair in enumerate(self.table[hash_key]): if pair[0] == key: @@ -32,19 +54,17 @@ def delete(self, key): return True return False - def display(self): + def display(self) -> None: for index, bucket in enumerate(self.table): - print(f"Index {index}: {bucket}") + print(f"Index {index} ({len(bucket)} entries): {bucket}") @staticmethod - def string_hash(s, table_size): - hash_value = 0 - for char in s: - hash_value += ord(char) + def string_hash(s: str, table_size: int) -> int: + hash_value = sum(ord(char) for char in s) return hash_value % table_size @staticmethod - def check_collisions(keys, table_size): + def check_collisions(keys: List[int], table_size: int) -> List[int]: hash_table = {} collisions = [] @@ -58,7 +78,7 @@ def check_collisions(keys, table_size): return collisions @staticmethod - def sha256_hash(string): + def sha256_hash(string: str) -> str: return hashlib.sha256(string.encode()).hexdigest() @@ -89,4 +109,4 @@ def sha256_hash(string): print("Collisions in [1, 2, 12, 22, 32]:", ht.check_collisions([1, 2, 12, 22, 32], 10)) # SHA-256 Hashing - print("SHA-256 hash of 'Hello, World!':", ht.sha256_hash("Hello, World!")) + print("SHA-256 hash of 'Hello, World!':", ht.sha256_hash("Hello, World!")) \ No newline at end of file diff --git a/pysnippets/algorithms/bidirectional_bfs.py b/pysnippets/algorithms/bidirectional_bfs.py new file mode 100644 index 00000000..ed27175d --- /dev/null +++ b/pysnippets/algorithms/bidirectional_bfs.py @@ -0,0 +1,109 @@ +from collections import deque +def bidirectional_search(graph, start, target): + ''' + Performs a bidirectional search on an undirected graph and returns the shortest path between two nodes. + To do this, it simultaneously uses two breadth-first searches (BFS) from the start and target nodes. + When the two searches meet, the path is reconstructed by backtracking to the start and target nodes. + Args: + graph (dict): A dictionary representing an undirected graph where keys are node identifiers and values + are lists of neighboring nodes. + start: The starting node in the graph for the search. Represents a key in the `graph` dictionary. + target: The target node in the graph for the search. Represents a key in the `graph` dictionary. + + Returns: + list: A list representing the shortest path from `start` to `target`. If no path exists, returns `None`. + ''' + if not isinstance(graph, dict): + raise TypeError("Ensure graph is a dictionary with nodes as keys and lists of neighbors as values.") + if start == target: + return [start] + if start not in graph or target not in graph: + raise ValueError(f"Start ({start}) and target ({target}) nodes must exist in the graph.") + + + queue_start = deque([start]) + queue_target = deque([target]) + + visited_start = {start} + visited_target = {target} + + parents_start = {start: None} + parents_target = {target: None} + + while queue_start and queue_target: + path = bfs(graph, visited_start, queue_start, parents_start, visited_target) + if path: + path_start = _remake_path_start(parents_start, path) + path_target = _remake_path_target(parents_target, path) + return path_start + path_target[1:] + path = bfs(graph, visited_target, queue_target, parents_target, visited_start) + if path: + path_start = _remake_path_start(parents_start, path) + path_target = _remake_path_target(parents_target, path) + return path_start + path_target[1:] + +def bfs(graph, visited, queue, parents, other_visited): + ''' + breadth-first search from the current node to seek neighbours. + + Args: + graph (dict): The graph represented as a dictionary where each key is a node and its value is a list + of neighboring nodes. + visited (set): A set of nodes already visited in this direction of the search. + queue (deque): The BFS queue holding nodes to explore. + parents (dict): A dictionary mapping each visited node to its parent node, can be used to remake paths. + other_visited (set): A set of nodes visited by the BFS running in the opposite direction. + + Returns: + The meeting node if the current search intersects with the other search, otherwise `None`. + ''' + current_node = queue.popleft() + for neighbor in graph[current_node]: + if neighbor not in visited: + parents[neighbor] = current_node + visited.add(neighbor) + queue.append(neighbor) + + if neighbor in other_visited: + return neighbor + return None + +def _remake_path_start(parents_start, meeting_node): + ''' + Create the path from the start node to the meeting node by using the parent nodes. + + Args: + parents_start (dict): A dictionary where each key is a node and the value + is the parent node in the path from the start node. + meeting_node: The node where the start and target nodes meet. + + Returns: + list: A list of nodes representing the path from the start node to the meeting node. + ''' + path_start = [] + node = meeting_node + while node is not None: + path_start.append(node) + node = parents_start[node] + path_start.reverse() + return path_start + +def _remake_path_target(parents_target, meeting_node): + ''' + Create the path from the target node to the meeting node by tracing + back through the parent nodes. + + Args: + parents_target (dict): A dictionary where each key is a node and the value + is the parent node in the path from the target node. + meeting_node: The node where the bidirectional search from start and target meets. + + Returns: + list: A list of nodes representing the path from the meeting node to the target node. + ''' + node = meeting_node + path_target = [] + while node is not None: + path_target.append(node) + node = parents_target[node] + return path_target