diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..019f533 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,61 @@ +name: CI / CD - Morse Code + +on: + pull_request: + branches: [pipfile-experiment] + push: + tags: ["v*"] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 5 + strategy: + matrix: + python-version: ["3.10", "3.11"] + + steps: + - uses: actions/checkout@v4 + + - name: Install Python and Pipenv + uses: kojoru/prepare-pipenv@v1 + with: + python-version: ${{ matrix.python-version }} + + + - name: Turn on 'editable' mode + run: | + pipenv install -e . + + - name: Run pytest + run: | + pipenv install pytest + pipenv --venv + pipenv run python -m pytest + + deliver: + if: github.event_name != 'pull_request' + needs: [build] + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - uses: actions/checkout@v4 + + - name: Install Python, pipenv and Pipfile packages + uses: kojoru/prepare-pipenv@v1 + with: + python-version: "3.10" + + - name: Build package + run: | + pipenv install build + pipenv run python -m build . + + # publish to PyPI Test server + # - name: Publish to TestPyPI + # uses: pypa/gh-action-pypi-publish@release/v1 + # with: + # password: ${{ secrets.TEST_PYPI_API_TOKEN }} + # repository-url: https://test.pypi.org/legacy/ diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..7e1d224 --- /dev/null +++ b/Pipfile @@ -0,0 +1,13 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +pytest = "*" +morse-code = {file = ".", editable = true} + +[dev-packages] + +[requires] +python_version = "3.10" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..7c5eb6e --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,66 @@ +{ + "_meta": { + "hash": { + "sha256": "8d4fce0340a506ab07fcfec2c61ecc8853335394bbda7148e9f28d01a844aded" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.10" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "iniconfig": { + "hashes": [ + "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", + "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12" + ], + "markers": "python_version >= '3.10'", + "version": "==2.3.0" + }, + "morse-code": { + "editable": true, + "file": "." + }, + "packaging": { + "hashes": [ + "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", + "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" + ], + "markers": "python_version >= '3.8'", + "version": "==25.0" + }, + "pluggy": { + "hashes": [ + "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", + "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" + ], + "markers": "python_version >= '3.9'", + "version": "==1.6.0" + }, + "pygments": { + "hashes": [ + "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", + "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" + ], + "markers": "python_version >= '3.8'", + "version": "==2.19.2" + }, + "pytest": { + "hashes": [ + "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", + "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==8.4.2" + } + }, + "develop": {} +} diff --git a/examples/demo.py b/examples/demo.py new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6413373 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,31 @@ +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "morse_code" +version = "0.1.0" +description = "A Python package for encoding and decoding Morse code." +readme = "README.md" +requires-python = ">=3.10" +license = { file = "LICENSE" } +authors = [{ name = "Team Lumen" }] +keywords = ["python", "morse code", "encoder", "decoder"] +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent" +] + +[project.urls] +Homepage = "https://github.com/swe-students-fall2025/3-python-package-team_lumen" +Issues = "https://github.com/swe-students-fall2025/3-python-package-team_lumen/issues" + +[tool.setuptools] +package-dir = {"" = "src"} + +[tool.setuptools.packages.find] +where = ["src"] + +[project.scripts] +morseify = "morseify.cli:cli" diff --git a/src/morseify/__init__.py b/src/morseify/__init__.py new file mode 100644 index 0000000..7b188a8 --- /dev/null +++ b/src/morseify/__init__.py @@ -0,0 +1,27 @@ +""" +Morse Code Package +A Python package for encoding and decoding Morse code with additional utilities. +""" + +from morseify.core import ( + encode, + decode, + is_valid +) +from morseify.normalize import ( + normalize_text, + normalize_code +) +from morseify.explain import explain +from morseify.quiz import quiz + +__all__ = [ + 'encode', + 'decode', + 'is_valid', + 'normalize_text', + 'normalize_code', + 'explain', + 'quiz' +] + diff --git a/src/morseify/__main__.py b/src/morseify/__main__.py new file mode 100644 index 0000000..e974edb --- /dev/null +++ b/src/morseify/__main__.py @@ -0,0 +1,41 @@ +from .core import encode, decode, is_valid +from .explain import explain +from .quiz import quiz + + +def main(): + print("Morse Code Encode/Decode Test") + # Test cases + test_cases = [ + "HELLO", + "HELLO WORLD", + "SOS", + "PYTHON", + "123", + "A1B2C3" + ] + + for text in test_cases: + encoded = encode(text) + decoded = decode(encoded) + + print(f"\nOriginal: '{text}'") + print(f"Encoded : '{encoded}'") + print(f"Decoded : '{decoded}'") + + # Check if round-trip works + if decoded == text: + print("✓ Round-trip successful!") + else: + print(f"✗ Round-trip failed! Expected: '{text}'") + + # Uncomment for interactive mode + # print("\nInteractive mode:") + # text = input("Enter text to encode: ") + # encoded = encode(text) + # print(f"Encoded: {encoded}") + # decoded = decode(encoded) + # print(f"Decoded: {decoded}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/morseify/cli.py b/src/morseify/cli.py new file mode 100644 index 0000000..296591b --- /dev/null +++ b/src/morseify/cli.py @@ -0,0 +1,41 @@ +""" +Command-line interface for morseify. +""" + +import sys +from morseify.core import encode, decode, is_valid + + +def cli(): + """ + Command-line interface for morseify. + Auto-detects whether input is text (encode) or morse code (decode). + + Usage: + morseify "HELLO" # Encodes text to morse code + morseify ".... . .-.. .-.. ---" # Decodes morse code to text + """ + if len(sys.argv) < 2: + print("Usage: morseify ") + print("Examples:") + print(' morseify "HELLO"') + print(' morseify ".... . .-.. .-.. ---"') + sys.exit(1) + + # Join all arguments in case user passes multiple words + input_text = ' '.join(sys.argv[1:]) + + # Auto-detect: if input contains only morse characters (., -, /, space), decode it + # Otherwise, encode it + valid_morse_chars = {'.', '-', '/', ' '} + is_morse = all(char in valid_morse_chars for char in input_text) and input_text.strip() + + if is_morse and is_valid(input_text): + # It's valid morse code, decode it + result = decode(input_text) + print(result) + else: + # It's text, encode it + result = encode(input_text) + print(result) + diff --git a/src/morseify/core.py b/src/morseify/core.py new file mode 100644 index 0000000..04c9343 --- /dev/null +++ b/src/morseify/core.py @@ -0,0 +1,112 @@ +from morseify.mapping import LETTER_TO_MORSE +from morseify.mapping import MORSE_TO_LETTER +from morseify.normalize import normalize_text +from morseify.normalize import normalize_code + +def encode(text): + """ + Convert English text to morse code. + + Args: + text: English text string to encode + + Returns: + Morse code string + """ + + # Normalize the text first(delete after normalize function is implemented) + # if text is None: + # return None + # normalized = text.upper().strip() + + normalized = normalize_text(text) + + # Convert each character to morse code + result = [] + for char in normalized: + if char == ' ': + # Space between words becomes ' / ' + result.append('/') + else: + # Get morse code for character + morse_char = LETTER_TO_MORSE.get(char, '') + if morse_char: + result.append(morse_char) + + # Join with spaces between letters + return ' '.join(result) + + +def decode(morse_code): + """ + Convert morse code to English text. + + Args: + morse_code: Morse code string to decode + + Returns: + English text string, or error message if morse code is invalid + """ + # Check if the morse code is valid + if not is_valid(morse_code): + return "Morse code is not valid" + + # If valid: normalize and decode + normalized = normalize_code(morse_code) + + # Split morse code by spaces to get individual morse sequences + morse_words = normalized.split(' ') + + result = [] + for i in morse_words: + if i == '': + # Skip empty sequences (multiple spaces) + continue + elif i == '/': + # Word separator + result.append(' ') + else: + # Look up the morse sequence in the dictionary + letter = MORSE_TO_LETTER.get(i, '') + result.append(letter) + + return ''.join(result) + + + + + + +def is_valid(morse_code): + """ + Check if morse code is valid. + + Args: + morse_code: Morse code string to check if valid + + Returns: + Boolean: True if valid, False otherwise + """ + + # check if morse_code is str or empty + try: + morse_code = morse_code.strip() + except AttributeError: + return False + + if not morse_code: + return False + + #check if valid chars or sequence + validchars = {'.', '-', '/', ' '} + if any(ch not in validchars for ch in morse_code): + return False + + for seq in morse_code.split(' '): + if not seq or seq == '/': + continue + if seq not in MORSE_TO_LETTER: + return False + return True + + diff --git a/src/morseify/explain.py b/src/morseify/explain.py new file mode 100644 index 0000000..d17e65a --- /dev/null +++ b/src/morseify/explain.py @@ -0,0 +1,3 @@ +def explain(morse_message): + # TODO: Implement explanation logic + pass \ No newline at end of file diff --git a/src/morseify/mapping.py b/src/morseify/mapping.py new file mode 100644 index 0000000..78e2a87 --- /dev/null +++ b/src/morseify/mapping.py @@ -0,0 +1,13 @@ +LETTER_TO_MORSE = { + "A": ".-", "B": "-...", "C": "-.-.", "D": "-..", "E": ".", + "F": "..-.", "G": "--.", "H": "....", "I": "..", "J": ".---", + "K": "-.-", "L": ".-..", "M": "--", "N": "-.", "O": "---", + "P": ".--.", "Q": "--.-", "R": ".-.", "S": "...", "T": "-", + "U": "..-", "V": "...-", "W": ".--", "X": "-..-", "Y": "-.--", + "Z": "--..", + "0": "-----", "1": ".----", "2": "..---", "3": "...--", "4": "....-", + "5": ".....", "6": "-....", "7": "--...", "8": "---..", "9": "----.", + ".": ".-.-.-", ",": "--..--", "?": "..--..", "/": "-..-.", "-": "-....-", + "(": "-.--.", ")": "-.--.-" +} +MORSE_TO_LETTER = {v: k for k, v in LETTER_TO_MORSE.items()} diff --git a/src/morseify/normalize.py b/src/morseify/normalize.py new file mode 100644 index 0000000..38d3392 --- /dev/null +++ b/src/morseify/normalize.py @@ -0,0 +1,56 @@ +import re +import string + +def normalize_text(text): + """ + Clean and standardize English text before encoding to Morse. + + Args: + text (str): Input text string + + Returns: + str: Normalized text ready for encoding + """ + if not isinstance(text, str): + return "" + # convert to uppercase + text = text.upper() + + # keep only supporting text + morse_punct = ".,?/-()" + allowed_chars = string.ascii_uppercase + string.digits + ' ' + morse_punct + cleaned = ''.join(ch for ch in text if ch in allowed_chars) + + # clean multiple or leading/trailing spaces + cleaned = re.sub(r'\s+', ' ', cleaned) + cleaned = cleaned.strip() + + return cleaned + + +def normalize_code(morse_code): + """ + Clean and standardize Morse code text before decoding. + + Args: + morse_code (str): Raw Morse code string + + Returns: + str: Normalized Morse code ready for decoding + """ + if not isinstance(morse_code, str): + return "" + + # replace any tabs with a space + morse_code = morse_code.replace('\t', ' ') + + # clean multiple spaces + morse_code = re.sub(r'\s+', ' ', morse_code) + + # clean multiple slashes + morse_code = re.sub(r'/+', '/', morse_code) + + # clean any leading/trailing space or slashes + morse_code = morse_code.strip(' /') + + return morse_code diff --git a/src/morseify/quiz.py b/src/morseify/quiz.py new file mode 100644 index 0000000..da116b5 --- /dev/null +++ b/src/morseify/quiz.py @@ -0,0 +1,132 @@ +import random +import sys +from pathlib import Path + +# Add parent directory to path if running as script +if __name__ == "__main__": + sys.path.insert(0, str(Path(__file__).parent.parent)) + +try: + from morseify.core import encode, decode, is_valid +except ModuleNotFoundError: + from .core import encode, decode, is_valid + +# List of sentences for the quiz +QUIZ_SENTENCES = [ + "HELLO WORLD", + "LET US TRY", + "MORSE CODE IS FUN", + "PYTHON ROCKS", + "LEARNING TAKES TIME", + "FORZA ROMA", + "SWE IS USEFUL", + "RUNNING OUT OF IDEAS", + "THIS SHOULD BE ENOUGH", + "I AM NOT VERY CREATIVE" +] + + +def quiz(sentence=None, mode=None): + """ + Interactive morse code quiz. + + Args: + sentence: Optional - specific sentence to use, or None for random + mode: Optional - 'reading' or 'writing', or None to ask + """ + if sentence is None: + selected_sentence = random.choice(QUIZ_SENTENCES) + else: + selected_sentence = sentence.upper().strip() + + + # Ask for mode if not provided + if mode is None: + print("\n" + "=" * 60) + print("MORSE CODE QUIZ") + print("=" * 60) + print("Choose your mode:") + print("'reading' - Decode morse code → text") + print("'writing' - Encode text → morse code") + print() + mode = input("Enter the mode ('reading' or 'writing'): ").strip().lower() + + while mode not in ['reading', 'writing']: + print("\nInvalid mode!") + print("Please enter 'reading' or 'writing'") + mode = input("\nEnter the mode: ").strip().lower() + + print("\n" + "=" * 60) + if mode == 'reading': + print("READING MODE: Decode morse code → text") + print("=" * 60) + morse_to_decode = encode(selected_sentence) + print(f"\nMorse code: {morse_to_decode}") + print() + + # Loop until correct or user gives up + while True: + answer = input("Your answer: ").upper().strip() + + if answer == selected_sentence: + print("\nCorrect! Well done!") + break + else: + print("\nIncorrect!") + retry = input("Try again? (yes/no): ").strip().lower() + + if retry == 'yes': + continue # Try again + else: # 'no' or anything else means give up + print("\n" + "-" * 60) + print("ANSWER REVEALED") + print("-" * 60) + print(f"Your answer: {answer}") + print(f"Correct answer: {selected_sentence}") + print(f"\nThe morse code '{morse_to_decode}' translates to '{selected_sentence}'") + print("-" * 60) + break + + else: # writing mode + print("WRITING MODE: Encode text → morse code") + print("=" * 60) + print(f"\nText to encode: {selected_sentence}") + print() + correct_morse = encode(selected_sentence) + + # Loop until correct or user gives up + while True: + answer = input("Your answer: ").strip() + + # Validate morse code format first + while not is_valid(answer): + print("\nInvalid morse code format!") + print("Please use only dots (.), dashes (-), spaces, and slashes (/)") + print("Example: ... --- ... (for SOS)") + answer = input("\nYour answer: ").strip() + + # Check if correct + if answer == correct_morse: + print("\nCorrect! Well done!") + break + else: + print("\nIncorrect!") + retry = input("Try again? (yes/no): ").strip().lower() + + if retry == 'yes': + continue # Try again + else: # 'no' or anything else means give up + print("\n" + "-" * 60) + print("ANSWER REVEALED") + print("-" * 60) + print(f"Your answer: {answer}") + print(f"Correct answer: {correct_morse}") + print(f"\n'{selected_sentence}' in morse code is: {correct_morse}") + print("-" * 60) + break + + print("=" * 60) + + +if __name__ == "__main__": + quiz() \ No newline at end of file diff --git a/tests/test_decode.py b/tests/test_decode.py new file mode 100644 index 0000000..26af617 --- /dev/null +++ b/tests/test_decode.py @@ -0,0 +1,2 @@ +def test_encode(): + assert True \ No newline at end of file diff --git a/tests/test_encode.py b/tests/test_encode.py new file mode 100644 index 0000000..03bb3d5 --- /dev/null +++ b/tests/test_encode.py @@ -0,0 +1,2 @@ +def test_decode(): + assert True \ No newline at end of file diff --git a/tests/test_is_valid.py b/tests/test_is_valid.py new file mode 100644 index 0000000..32a6635 --- /dev/null +++ b/tests/test_is_valid.py @@ -0,0 +1,2 @@ +def test_is_valid(): + assert True \ No newline at end of file diff --git a/tests/test_normalize_code.py b/tests/test_normalize_code.py new file mode 100644 index 0000000..abcd8d4 --- /dev/null +++ b/tests/test_normalize_code.py @@ -0,0 +1,2 @@ +def test_normalize_code(): + assert True \ No newline at end of file diff --git a/tests/test_normalize_text.py b/tests/test_normalize_text.py new file mode 100644 index 0000000..14519ca --- /dev/null +++ b/tests/test_normalize_text.py @@ -0,0 +1,2 @@ +def test_normalize_text(): + assert True \ No newline at end of file