Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
621f5f7272 | ||
|
|
cdbf6396ab | ||
|
|
0957bff43a |
@@ -174,3 +174,6 @@ cython_debug/
|
|||||||
# PyPI configuration file
|
# PyPI configuration file
|
||||||
.pypirc
|
.pypirc
|
||||||
|
|
||||||
|
|
||||||
|
# Doxygen output
|
||||||
|
docs/doxygen/
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
PROJECT_NAME = "Caesar Cipher"
|
||||||
|
PROJECT_BRIEF = "Udemy 100 Days of Code - Day 8"
|
||||||
|
OUTPUT_DIRECTORY = docs/doxygen
|
||||||
|
INPUT = src README.md docs/index.md
|
||||||
|
USE_MDFILE_AS_MAINPAGE = README.md
|
||||||
|
FILE_PATTERNS = *.py *.md
|
||||||
|
RECURSIVE = YES
|
||||||
|
EXTRACT_ALL = YES
|
||||||
|
OPTIMIZE_OUTPUT_JAVA = YES
|
||||||
|
GENERATE_HTML = YES
|
||||||
|
GENERATE_LATEX = NO
|
||||||
|
QUIET = YES
|
||||||
@@ -1,2 +1,76 @@
|
|||||||
# 008-caesar_cipher
|
# 🔐 Caesar Cipher
|
||||||
|
|
||||||
|
A beginner-friendly Python program that encodes and decodes messages with the Caesar cipher.
|
||||||
|
|
||||||
|
## 📚 Table of Contents
|
||||||
|
|
||||||
|
- [Overview](#-overview)
|
||||||
|
- [Requirements](#-requirements)
|
||||||
|
- [Setup](#-setup)
|
||||||
|
- [Run](#-run)
|
||||||
|
- [Tests](#-tests)
|
||||||
|
- [License](#-license)
|
||||||
|
- [Links](#-links)
|
||||||
|
|
||||||
|
## 🧭 Overview
|
||||||
|
|
||||||
|
The Caesar cipher shifts every letter by a fixed number of places (with a shift of 3, `E` becomes `H`). This project, from Udemy's *100 Days of Code: The Complete Python Pro Bootcamp* (Day 8), finds letter positions with `list.index` and wraps around the alphabet with the modulo operator. Parts 1 and 2 cover encryption and decryption; a combined program follows in Part 3. Code lives in `src/`, tests in `tests/`, docs in `docs/`.
|
||||||
|
|
||||||
|
## 📋 Requirements
|
||||||
|
|
||||||
|
- Python 3.13 or newer
|
||||||
|
- `pytest` (development only; no runtime dependencies)
|
||||||
|
- Optional: [Doxygen](https://www.doxygen.nl/) to build the API docs
|
||||||
|
|
||||||
|
## 🛠️ Setup
|
||||||
|
|
||||||
|
Create and activate a local virtual environment, then upgrade pip and install the project:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m venv .venv
|
||||||
|
```
|
||||||
|
|
||||||
|
Activate it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux / macOS
|
||||||
|
source .venv/bin/activate
|
||||||
|
# Git Bash on Windows
|
||||||
|
source .venv/Scripts/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Windows PowerShell
|
||||||
|
.venv\Scripts\Activate.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
python -m pip install -e ".[dev]"
|
||||||
|
```
|
||||||
|
|
||||||
|
## ▶️ Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m caesar_cipher
|
||||||
|
```
|
||||||
|
|
||||||
|
or, after installation, `caesar-cipher`.
|
||||||
|
|
||||||
|
## 🧪 Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m pytest
|
||||||
|
```
|
||||||
|
|
||||||
|
Build the API docs with `doxygen Doxyfile` (output in `docs/doxygen/html`).
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
GNU AGPL-3.0 – see [LICENSE](LICENSE).
|
||||||
|
|
||||||
|
## 🔗 Links
|
||||||
|
|
||||||
|
- [Repository](https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code)
|
||||||
|
- [Documentation](docs/index.md)
|
||||||
|
- [Issue tracker](https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/008-caesar_cipher/issues)
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Caesar Cipher – documentation
|
||||||
|
|
||||||
|
Each letter is moved `shift` places through the alphabet:
|
||||||
|
`new_position = (ALPHABET.index(letter) + shift) % 26`.
|
||||||
|
The modulo keeps positions in range for shifts past `z`.
|
||||||
|
Decryption is encryption with `-shift`. Non-letters are left untouched and letter case is preserved.
|
||||||
|
|
||||||
|
API reference: run `doxygen Doxyfile` from the repository root and open
|
||||||
|
`docs/doxygen/html/index.html`. The step-by-step plan is in [plan.md](plan.md).
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=77"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "caesar-cipher"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "A beginner-friendly Caesar cipher encoder/decoder (Udemy 100 Days of Code, Day 8)."
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
license = "AGPL-3.0-or-later"
|
||||||
|
license-files = ["LICENSE"]
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = ["pytest>=8"]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
caesar-cipher = "caesar_cipher.main:main"
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["src"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
pythonpath = ["src"]
|
||||||
|
testpaths = ["tests"]
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
## @file __init__.py
|
||||||
|
# @brief Public API of the caesar_cipher package.
|
||||||
|
|
||||||
|
from caesar_cipher.cipher import decrypt, encrypt
|
||||||
|
|
||||||
|
__all__ = ["encrypt", "decrypt"]
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
## @file __main__.py
|
||||||
|
# @brief Enables `python -m caesar_cipher`.
|
||||||
|
|
||||||
|
from caesar_cipher.main import main
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
## @file cipher.py
|
||||||
|
# @brief Caesar cipher encryption and decryption.
|
||||||
|
|
||||||
|
from caesar_cipher.constants import ALPHABET
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt(text: str, shift: int) -> str:
|
||||||
|
## @brief Encrypt a text with the Caesar cipher.
|
||||||
|
# @param text Original text.
|
||||||
|
# @param shift Positions to move each letter (may be negative or > 26).
|
||||||
|
# @return The encrypted text; non-letters are kept, case is preserved.
|
||||||
|
result = []
|
||||||
|
for char in text:
|
||||||
|
lower = char.lower()
|
||||||
|
if lower in ALPHABET:
|
||||||
|
new_position = (ALPHABET.index(lower) + shift) % len(ALPHABET)
|
||||||
|
new_char = ALPHABET[new_position]
|
||||||
|
result.append(new_char.upper() if char.isupper() else new_char)
|
||||||
|
else:
|
||||||
|
result.append(char)
|
||||||
|
return "".join(result)
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt(text: str, shift: int) -> str:
|
||||||
|
## @brief Decrypt a Caesar-cipher text.
|
||||||
|
# @param text Encrypted text.
|
||||||
|
# @param shift Shift that was used to encrypt.
|
||||||
|
# @return The original text.
|
||||||
|
return encrypt(text, -shift)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
## @file constants.py
|
||||||
|
# @brief Constants used by the Caesar cipher.
|
||||||
|
|
||||||
|
## Lowercase alphabet used to look up letter positions.
|
||||||
|
ALPHABET = list("abcdefghijklmnopqrstuvwxyz")
|
||||||
|
|
||||||
|
## Prompt for the message.
|
||||||
|
PROMPT_TEXT = "Type your message:\n"
|
||||||
|
## Prompt for the shift number.
|
||||||
|
PROMPT_SHIFT = "Type the shift number:\n"
|
||||||
|
|
||||||
|
## Name of the environment file holding tokens.
|
||||||
|
ENV_FILE = ".env"
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
## @file env.py
|
||||||
|
# @brief Minimal stdlib `.env` reader (no third-party dependency).
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from caesar_cipher.constants import ENV_FILE
|
||||||
|
|
||||||
|
|
||||||
|
def load_env(path: str | Path = ENV_FILE) -> dict[str, str]:
|
||||||
|
## @brief Read KEY=VALUE pairs from an env file.
|
||||||
|
# @param path Path of the env file.
|
||||||
|
# @return Mapping of variables; empty if the file does not exist.
|
||||||
|
values: dict[str, str] = {}
|
||||||
|
env_path = Path(path)
|
||||||
|
if not env_path.is_file():
|
||||||
|
return values
|
||||||
|
for line in env_path.read_text(encoding="utf-8").splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith("#") or "=" not in line:
|
||||||
|
continue
|
||||||
|
key, _, value = line.partition("=")
|
||||||
|
values[key.strip()] = value.strip().strip("\"'")
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
def get_token(name: str, path: str | Path = ENV_FILE) -> str | None:
|
||||||
|
## @brief Get a token from the process environment or the env file.
|
||||||
|
# @param name Variable name, e.g. GITEA_TOKEN.
|
||||||
|
# @return The token or None when not set.
|
||||||
|
return os.environ.get(name) or load_env(path).get(name)
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
## @file main.py
|
||||||
|
# @brief Interactive command line interface.
|
||||||
|
|
||||||
|
from caesar_cipher import constants as c
|
||||||
|
from caesar_cipher.cipher import encrypt
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
## @brief Ask for a message and a shift, then print the encrypted text.
|
||||||
|
text = input(c.PROMPT_TEXT)
|
||||||
|
try:
|
||||||
|
shift = int(input(c.PROMPT_SHIFT))
|
||||||
|
except ValueError:
|
||||||
|
print("The shift must be a whole number.")
|
||||||
|
return
|
||||||
|
print(f"Here's the encoded result: {encrypt(text, shift)}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
from caesar_cipher import decrypt, encrypt
|
||||||
|
from caesar_cipher.env import load_env
|
||||||
|
|
||||||
|
|
||||||
|
def test_encrypt_basic():
|
||||||
|
assert encrypt("hello", 5) == "mjqqt"
|
||||||
|
|
||||||
|
|
||||||
|
def test_wraps_around_alphabet():
|
||||||
|
assert encrypt("xyz", 3) == "abc"
|
||||||
|
assert encrypt("abc", 29) == "def"
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_letters_and_case_preserved():
|
||||||
|
assert encrypt("Hi there, 42!", 1) == "Ij uifsf, 42!"
|
||||||
|
|
||||||
|
|
||||||
|
def test_negative_shift():
|
||||||
|
assert encrypt("def", -3) == "abc"
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_env(tmp_path):
|
||||||
|
f = tmp_path / ".env"
|
||||||
|
f.write_text("# c\nA=1\nB='two'\n", encoding="utf-8")
|
||||||
|
assert load_env(f) == {"A": "1", "B": "two"}
|
||||||
|
assert load_env(tmp_path / "missing") == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_decrypt_basic():
|
||||||
|
assert decrypt("mjqqt", 5) == "hello"
|
||||||
|
|
||||||
|
|
||||||
|
def test_decrypt_roundtrip():
|
||||||
|
assert decrypt(encrypt("Hello, World!", 7), 7) == "Hello, World!"
|
||||||
|
|
||||||
|
|
||||||
|
def test_decrypt_wraps_around_alphabet():
|
||||||
|
assert decrypt("abc", 3) == "xyz"
|
||||||
|
assert decrypt("def", 29) == "abc"
|
||||||
Reference in New Issue
Block a user