← Kursa Dön
📄 Text · 25 min

Gerçek Dünya Projesi: CLI Todo App

Bu kursu boyunca öğrendiğin her şeyi şimdi birleştirme zamanı. Değişkenler, fonksiyonlar, sınıflar, dosya işlemleri, exception handling, test yazma, clean code... Hepsini tek bir projede kullanacağız.

Analoji: Bir müzisyen düşün. Nota okumayı öğrendi, akor bilgisi var, ritim duygusu gelişti. Ama gerçek müzisyen olması için bunları bir şarkıda birleştirmesi gerekiyor. İşte bu ders, senin ilk "şarkın."

Yapacağımız şey: Komut satırından çalışan bir Todo (yapılacaklar) uygulaması. Basit ama gerçek dünyada kullanılabilir kalitede. Modüler, test edilmiş, paketlenmiş.


Proje Yapısı

Profesyonel bir Python projesinin yapısı nasıl olmalı:

todo_app/
├── todo_app/
│   ├── __init__.py
│   ├── models.py          # Veri modelleri
│   ├── storage.py         # JSON ile veri kaydetme/yükleme
│   ├── commands.py        # CLI komutları
│   └── cli.py             # Argparse arayüzü
├── tests/
│   ├── __init__.py
│   ├── test_models.py
│   ├── test_storage.py
│   └── test_commands.py
├── pyproject.toml          # Proje konfigürasyonu
└── README.md

Her dosyanın tek bir sorumluluğu var:

  • models.py → Veri yapısı (Todo nesnesi)

  • storage.py → Veriyi diske kaydetme/yükleme

  • commands.py → İş mantığı (ekle, listele, tamamla, sil)

  • cli.py → Kullanıcı arayüzü (argparse)

Bu yapı Separation of Concerns (Sorumlulukların Ayrılması) prensibini uyguluyor. Her modül tek bir iş yapıyor ve bağımsız test edilebiliyor.


Adım 1: Todo Model Sınıfı

Her todo öğesi için bir veri modeli lazım. Python 3.7+'dan itibaren dataclass bu iş için harika:

# todo_app/models.py
from dataclasses import dataclass, field, asdict
from datetime import datetime
from typing import Optional
import uuid

@dataclass
class Todo:
    """Tek bir todo öğesini temsil eder"""
    title: str
    id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
    completed: bool = False
    created_at: str = field(
        default_factory=lambda: datetime.now().isoformat(timespec="seconds")
    )
    completed_at: Optional[str] = None
    priority: str = "normal"  # low, normal, high
    
    def complete(self):
        """Todo'yu tamamla"""
        self.completed = True
        self.completed_at = datetime.now().isoformat(timespec="seconds")
    
    def to_dict(self) -> dict:
        """Sözlüğe dönüştür (JSON serialization için)"""
        return asdict(self)
    
    @classmethod
    def from_dict(cls, data: dict) -> "Todo":
        """Sözlükten oluştur (JSON deserialization için)"""
        return cls(**data)
    
    def __str__(self) -> str:
        status = "✅" if self.completed else "⬜"
        priority_icons = {"low": "🔵", "normal": "🟡", "high": "🔴"}
        icon = priority_icons.get(self.priority, "🟡")
        return f"{status} [{self.id}] {icon} {self.title}"

Neler yaptık:

  • dataclass ile boilerplate kodu azalttık (__init__, __repr__ otomatik)

  • uuid ile her todo'ya benzersiz ID atadık

  • to_dict / from_dict ile JSON'a dönüşüm

  • __str__ ile güzel görüntü

  • Type hint'ler ile tüm tipler belirtildi

# Kullanım
todo = Todo(title="Python öğren", priority="high")
print(todo)
# ⬜ [a1b2c3d4] 🔴 Python öğren

todo.complete()
print(todo)
# ✅ [a1b2c3d4] 🔴 Python öğren

Adım 2: JSON ile Veri Kaydetme/Yükleme

Todo'lar bir yerde saklanmalı. Veritabanı yerine basit bir JSON dosyası kullanacağız:

# todo_app/storage.py
import json
import os
from pathlib import Path
from typing import List

from .models import Todo

DEFAULT_FILE = Path.home() / ".todo_app" / "todos.json"

class TodoStorage:
    """JSON dosyasına todo kaydetme/yükleme"""
    
    def __init__(self, filepath: Path = DEFAULT_FILE):
        self.filepath = Path(filepath)
    
    def _ensure_directory(self):
        """Dizin yoksa oluştur"""
        self.filepath.parent.mkdir(parents=True, exist_ok=True)
    
    def load(self) -> List[Todo]:
        """Dosyadan todo'ları yükle"""
        if not self.filepath.exists():
            return []
        
        try:
            with open(self.filepath, "r", encoding="utf-8") as f:
                data = json.load(f)
            return [Todo.from_dict(item) for item in data]
        except (json.JSONDecodeError, KeyError) as e:
            print(f"⚠️ Veri dosyası bozuk: {e}")
            return []
    
    def save(self, todos: List[Todo]) -> None:
        """Todo'ları dosyaya kaydet"""
        self._ensure_directory()
        
        data = [todo.to_dict() for todo in todos]
        
        # Atomik yazma: önce geçici dosyaya yaz, sonra taşı
        temp_file = self.filepath.with_suffix(".tmp")
        try:
            with open(temp_file, "w", encoding="utf-8") as f:
                json.dump(data, f, indent=2, ensure_ascii=False)
            temp_file.replace(self.filepath)
        except OSError:
            if temp_file.exists():
                temp_file.unlink()
            raise
    
    def exists(self) -> bool:
        """Veri dosyası var mı?"""
        return self.filepath.exists()
    
    def clear(self) -> None:
        """Tüm verileri sil"""
        if self.filepath.exists():
            self.filepath.unlink()

Önemli detaylar:

  • Atomik yazma: Önce .tmp dosyaya yaz, sonra replace ile asıl dosyaya taşı. Yazma sırasında program çökerse veri kaybolmaz.

  • Encoding: utf-8 ve ensure_ascii=False ile Türkçe karakterler düzgün kaydedilir.

  • Error handling: JSON bozuksa boş liste döner, çökmez.

  • Path: pathlib.Path ile platform-bağımsız dosya yolları.

Oluşan JSON dosyası şöyle görünür:

[
  {
    "title": "Python öğren",
    "id": "a1b2c3d4",
    "completed": false,
    "created_at": "2024-01-15T10:30:00",
    "completed_at": null,
    "priority": "normal"
  },
  {
    "title": "Test yaz",
    "id": "e5f6g7h8",
    "completed": true,
    "created_at": "2024-01-15T11:00:00",
    "completed_at": "2024-01-15T14:30:00",
    "priority": "high"
  }
]

Adım 3: Komutlar — İş Mantığı

Şimdi asıl işi yapan fonksiyonları yazalım. Her komut bir fonksiyon:

# todo_app/commands.py
from typing import List, Optional

from .models import Todo
from .storage import TodoStorage

class TodoApp:
    """Todo uygulama mantığı"""
    
    def __init__(self, storage: Optional[TodoStorage] = None):
        self.storage = storage or TodoStorage()
        self.todos: List[Todo] = self.storage.load()
    
    def add(self, title: str, priority: str = "normal") -> Todo:
        """Yeni todo ekle"""
        if not title.strip():
            raise ValueError("Todo başlığı boş olamaz!")
        
        if priority not in ("low", "normal", "high"):
            raise ValueError(f"Geçersiz öncelik: {priority}")
        
        todo = Todo(title=title.strip(), priority=priority)
        self.todos.append(todo)
        self._save()
        return todo
    
    def list_todos(
        self,
        show_all: bool = False,
        priority: Optional[str] = None,
    ) -> List[Todo]:
        """Todo'ları listele"""
        result = self.todos
        
        # Tamamlanmamışları göster (varsayılan)
        if not show_all:
            result = [t for t in result if not t.completed]
        
        # Önceliğe göre filtrele
        if priority:
            result = [t for t in result if t.priority == priority]
        
        # Önceliğe göre sırala: high → normal → low
        priority_order = {"high": 0, "normal": 1, "low": 2}
        result.sort(key=lambda t: (t.completed, priority_order.get(t.priority, 1)))
        
        return result
    
    def complete(self, todo_id: str) -> Todo:
        """Todo'yu tamamla"""
        todo = self._find_by_id(todo_id)
        if todo.completed:
            raise ValueError(f"Todo zaten tamamlanmış: {todo.title}")
        
        todo.complete()
        self._save()
        return todo
    
    def delete(self, todo_id: str) -> Todo:
        """Todo'yu sil"""
        todo = self._find_by_id(todo_id)
        self.todos.remove(todo)
        self._save()
        return todo
    
    def stats(self) -> dict:
        """İstatistikler"""
        total = len(self.todos)
        completed = sum(1 for t in self.todos if t.completed)
        pending = total - completed
        
        return {
            "total": total,
            "completed": completed,
            "pending": pending,
            "completion_rate": f"{completed/total*100:.0f}%" if total > 0 else "0%",
        }
    
    def _find_by_id(self, todo_id: str) -> Todo:
        """ID ile todo bul"""
        for todo in self.todos:
            if todo.id == todo_id or todo.id.startswith(todo_id):
                return todo
        raise ValueError(f"Todo bulunamadı: {todo_id}")
    
    def _save(self):
        """Değişiklikleri kaydet"""
        self.storage.save(self.todos)

Dikkat ettiysen:

  • Dependency Injection: storage parametre olarak alınıyor. Test ederken mock storage verebilirsin.

  • Validation: Her komut girdileri doğruluyor, hatalı girdi için ValueError fırlatıyor.

  • Kısmi ID eşleşme: _find_by_id tam ID yerine başlangıç kısmıyla da eşleşir. Kullanıcı a1b2 yazarsa a1b2c3d4 bulunur.

  • Private metodlar: _save ve _find_by_id underscore ile "dahili" olarak işaretli.


Adım 4: argparse ile CLI Arayüzü

Kullanıcının terminal'den etkileşim kuracağı arayüz:

# todo_app/cli.py
import argparse
import sys

from .commands import TodoApp

def create_parser() -> argparse.ArgumentParser:
    """CLI parser oluştur"""
    parser = argparse.ArgumentParser(
        prog="todo",
        description="📝 CLI Todo Uygulaması",
        epilog="Örnek: todo add 'Markete git' --priority high",
    )
    
    subparsers = parser.add_subparsers(dest="command", help="Komutlar")
    
    # === add komutu ===
    add_parser = subparsers.add_parser("add", help="Yeni todo ekle")
    add_parser.add_argument("title", help="Todo başlığı")
    add_parser.add_argument(
        "-p", "--priority",
        choices=["low", "normal", "high"],
        default="normal",
        help="Öncelik seviyesi (varsayılan: normal)",
    )
    
    # === list komutu ===
    list_parser = subparsers.add_parser("list", help="Todo'ları listele")
    list_parser.add_argument(
        "-a", "--all",
        action="store_true",
        help="Tamamlananları da göster",
    )
    list_parser.add_argument(
        "-p", "--priority",
        choices=["low", "normal", "high"],
        help="Önceliğe göre filtrele",
    )
    
    # === complete komutu ===
    complete_parser = subparsers.add_parser("done", help="Todo'yu tamamla")
    complete_parser.add_argument("id", help="Todo ID (kısmi olabilir)")
    
    # === delete komutu ===
    delete_parser = subparsers.add_parser("delete", help="Todo'yu sil")
    delete_parser.add_argument("id", help="Todo ID")
    
    # === stats komutu ===
    subparsers.add_parser("stats", help="İstatistikleri göster")
    
    return parser


def main(argv=None):
    """Ana giriş noktası"""
    parser = create_parser()
    args = parser.parse_args(argv)
    
    if not args.command:
        parser.print_help()
        return 0
    
    app = TodoApp()
    
    try:
        if args.command == "add":
            todo = app.add(args.title, args.priority)
            print(f"✅ Eklendi: {todo}")
        
        elif args.command == "list":
            todos = app.list_todos(show_all=args.all, priority=args.priority)
            if not todos:
                print("📭 Yapılacak bir şey yok! (veya --all ile tümünü gör)")
            else:
                print(f"📋 Todo Listesi ({len(todos)} öğe):")
                print("-" * 50)
                for todo in todos:
                    print(f"  {todo}")
        
        elif args.command == "done":
            todo = app.complete(args.id)
            print(f"🎉 Tamamlandı: {todo.title}")
        
        elif args.command == "delete":
            todo = app.delete(args.id)
            print(f"🗑️ Silindi: {todo.title}")
        
        elif args.command == "stats":
            stats = app.stats()
            print(f"📊 İstatistikler:")
            print(f"   Toplam:      {stats['total']}")
            print(f"   Tamamlanan:  {stats['completed']}")
            print(f"   Bekleyen:    {stats['pending']}")
            print(f"   Tamamlanma:  {stats['completion_rate']}")
    
    except ValueError as e:
        print(f"❌ Hata: {e}", file=sys.stderr)
        return 1
    except KeyboardInterrupt:
        print("\n👋 Çıkılıyor...")
        return 0
    
    return 0


if __name__ == "__main__":
    sys.exit(main())

Kullanım Örnekleri

# Todo ekle
$ todo add "Python öğren" --priority high
✅ Eklendi: ⬜ [a1b2c3d4] 🔴 Python öğren

$ todo add "Markete git"
✅ Eklendi: ⬜ [e5f6g7h8] 🟡 Markete git

$ todo add "Kitap oku" --priority low
✅ Eklendi: ⬜ [i9j0k1l2] 🔵 Kitap oku

# Listele
$ todo list
📋 Todo Listesi (3 öğe):
--------------------------------------------------
  ⬜ [a1b2c3d4] 🔴 Python öğren
  ⬜ [e5f6g7h8] 🟡 Markete git
  ⬜ [i9j0k1l2] 🔵 Kitap oku

# Tamamla (kısmi ID yeter)
$ todo done a1b2
🎉 Tamamlandı: Python öğren

# Sadece tamamlanmamışları göster
$ todo list
📋 Todo Listesi (2 öğe):
--------------------------------------------------
  ⬜ [e5f6g7h8] 🟡 Markete git
  ⬜ [i9j0k1l2] 🔵 Kitap oku

# Hepsini göster
$ todo list --all
📋 Todo Listesi (3 öğe):
--------------------------------------------------
  ⬜ [e5f6g7h8] 🟡 Markete git
  ⬜ [i9j0k1l2] 🔵 Kitap oku
  ✅ [a1b2c3d4] 🔴 Python öğren

# İstatistikler
$ todo stats
📊 İstatistikler:
   Toplam:      3
   Tamamlanan:  1
   Bekleyen:    2
   Tamamlanma:  33%

# Sil
$ todo delete e5f6
🗑️ Silindi: Markete git

Adım 5: Exception Handling

Exception handling zaten kodun içinde var ama bir de özel exception sınıfları yazalım:

# todo_app/exceptions.py
class TodoAppError(Exception):
    """Uygulama hatalarının base class'ı"""
    pass

class TodoNotFoundError(TodoAppError):
    """Todo bulunamadığında"""
    def __init__(self, todo_id: str):
        self.todo_id = todo_id
        super().__init__(f"Todo bulunamadı: {todo_id}")

class DuplicateTodoError(TodoAppError):
    """Aynı başlıklı todo eklenmeye çalışıldığında"""
    def __init__(self, title: str):
        self.title = title
        super().__init__(f"Bu todo zaten var: {title}")

class StorageError(TodoAppError):
    """Dosya okuma/yazma hatası"""
    pass

Bu exception'ları commands.py'da kullanalım:

# commands.py'daki _find_by_id güncellemesi
from .exceptions import TodoNotFoundError, DuplicateTodoError

def _find_by_id(self, todo_id: str) -> Todo:
    for todo in self.todos:
        if todo.id == todo_id or todo.id.startswith(todo_id):
            return todo
    raise TodoNotFoundError(todo_id)

def add(self, title: str, priority: str = "normal") -> Todo:
    if not title.strip():
        raise ValueError("Todo başlığı boş olamaz!")
    
    # Aynı başlıklı todo var mı kontrol et
    existing = [t for t in self.todos if t.title.lower() == title.strip().lower()]
    if existing:
        raise DuplicateTodoError(title)
    
    # ... geri kalan kod

💡 İpucu: Özel exception sınıfları yazmak neden önemli? Çünkü except ValueError yazdığında, hatanın senin fonksiyonundan mı yoksa başka bir yerden mi geldiğini bilemezsin. except TodoNotFoundError çok daha spesifik ve anlaşılır.


Adım 6: Test Yazma

Her modül için testler yazalım. Testler kodun "güvenlik ağı":

# tests/test_models.py
import pytest
from todo_app.models import Todo

class TestTodo:
    def test_create_todo(self):
        todo = Todo(title="Test todo")
        assert todo.title == "Test todo"
        assert todo.completed is False
        assert todo.priority == "normal"
        assert todo.id is not None
    
    def test_complete_todo(self):
        todo = Todo(title="Test")
        todo.complete()
        assert todo.completed is True
        assert todo.completed_at is not None
    
    def test_to_dict_and_back(self):
        original = Todo(title="Test", priority="high")
        data = original.to_dict()
        restored = Todo.from_dict(data)
        assert restored.title == original.title
        assert restored.id == original.id
        assert restored.priority == original.priority
    
    def test_string_representation(self):
        todo = Todo(title="Ödev yap", priority="high")
        text = str(todo)
        assert "Ödev yap" in text
        assert "🔴" in text  # High priority icon
    
    @pytest.mark.parametrize("priority,icon", [
        ("low", "🔵"),
        ("normal", "🟡"),
        ("high", "🔴"),
    ])
    def test_priority_icons(self, priority, icon):
        todo = Todo(title="Test", priority=priority)
        assert icon in str(todo)
# tests/test_storage.py
import pytest
import json
from pathlib import Path
from todo_app.models import Todo
from todo_app.storage import TodoStorage

@pytest.fixture
def temp_storage(tmp_path):
    """Geçici dizinde storage oluştur"""
    filepath = tmp_path / "test_todos.json"
    return TodoStorage(filepath=filepath)

@pytest.fixture
def sample_todos():
    return [
        Todo(title="Todo 1", priority="high"),
        Todo(title="Todo 2", priority="normal"),
        Todo(title="Todo 3", priority="low"),
    ]

class TestStorage:
    def test_save_and_load(self, temp_storage, sample_todos):
        temp_storage.save(sample_todos)
        loaded = temp_storage.load()
        
        assert len(loaded) == 3
        assert loaded[0].title == "Todo 1"
        assert loaded[1].title == "Todo 2"
    
    def test_load_empty(self, temp_storage):
        todos = temp_storage.load()
        assert todos == []
    
    def test_load_corrupted_file(self, temp_storage):
        # Bozuk JSON dosyası oluştur
        temp_storage.filepath.parent.mkdir(parents=True, exist_ok=True)
        temp_storage.filepath.write_text("not valid json {{{")
        
        todos = temp_storage.load()
        assert todos == []  # Bozuk dosyada boş liste döner
    
    def test_save_creates_directory(self, tmp_path):
        filepath = tmp_path / "nested" / "dir" / "todos.json"
        storage = TodoStorage(filepath=filepath)
        
        storage.save([Todo(title="Test")])
        assert filepath.exists()
    
    def test_clear(self, temp_storage, sample_todos):
        temp_storage.save(sample_todos)
        assert temp_storage.exists()
        
        temp_storage.clear()
        assert not temp_storage.exists()
# tests/test_commands.py
import pytest
from todo_app.commands import TodoApp
from todo_app.storage import TodoStorage

@pytest.fixture
def app(tmp_path):
    """Test için temiz bir TodoApp"""
    storage = TodoStorage(filepath=tmp_path / "test.json")
    return TodoApp(storage=storage)

class TestAdd:
    def test_add_todo(self, app):
        todo = app.add("Test todo")
        assert todo.title == "Test todo"
        assert len(app.todos) == 1
    
    def test_add_with_priority(self, app):
        todo = app.add("Urgent", priority="high")
        assert todo.priority == "high"
    
    def test_add_empty_title_raises(self, app):
        with pytest.raises(ValueError, match="boş olamaz"):
            app.add("")
    
    def test_add_whitespace_title_raises(self, app):
        with pytest.raises(ValueError, match="boş olamaz"):
            app.add("   ")
    
    def test_add_invalid_priority_raises(self, app):
        with pytest.raises(ValueError, match="Geçersiz öncelik"):
            app.add("Test", priority="urgent")

class TestComplete:
    def test_complete_todo(self, app):
        todo = app.add("Test")
        completed = app.complete(todo.id)
        assert completed.completed is True
    
    def test_complete_already_done_raises(self, app):
        todo = app.add("Test")
        app.complete(todo.id)
        with pytest.raises(ValueError, match="zaten tamamlanmış"):
            app.complete(todo.id)
    
    def test_complete_partial_id(self, app):
        todo = app.add("Test")
        partial_id = todo.id[:4]
        completed = app.complete(partial_id)
        assert completed.id == todo.id

class TestDelete:
    def test_delete_todo(self, app):
        todo = app.add("To delete")
        app.delete(todo.id)
        assert len(app.todos) == 0
    
    def test_delete_nonexistent_raises(self, app):
        with pytest.raises(ValueError):
            app.delete("nonexistent")

class TestList:
    def test_list_empty(self, app):
        assert app.list_todos() == []
    
    def test_list_hides_completed(self, app):
        todo1 = app.add("Active")
        todo2 = app.add("Done")
        app.complete(todo2.id)
        
        active = app.list_todos()
        assert len(active) == 1
        assert active[0].title == "Active"
    
    def test_list_all(self, app):
        app.add("Active")
        todo2 = app.add("Done")
        app.complete(todo2.id)
        
        all_todos = app.list_todos(show_all=True)
        assert len(all_todos) == 2
    
    def test_list_filter_priority(self, app):
        app.add("Low task", priority="low")
        app.add("High task", priority="high")
        
        high = app.list_todos(priority="high")
        assert len(high) == 1
        assert high[0].title == "High task"

class TestStats:
    def test_empty_stats(self, app):
        stats = app.stats()
        assert stats["total"] == 0
        assert stats["completion_rate"] == "0%"
    
    def test_stats_with_todos(self, app):
        app.add("Task 1")
        todo2 = app.add("Task 2")
        app.add("Task 3")
        app.complete(todo2.id)
        
        stats = app.stats()
        assert stats["total"] == 3
        assert stats["completed"] == 1
        assert stats["pending"] == 2
        assert stats["completion_rate"] == "33%"

Testleri Çalıştırma

# Tüm testleri çalıştır
pytest tests/ -v

# Coverage ile
pytest tests/ --cov=todo_app --cov-report=term-missing
tests/test_commands.py::TestAdd::test_add_todo PASSED
tests/test_commands.py::TestAdd::test_add_with_priority PASSED
tests/test_commands.py::TestAdd::test_add_empty_title_raises PASSED
tests/test_commands.py::TestComplete::test_complete_todo PASSED
tests/test_commands.py::TestComplete::test_complete_partial_id PASSED
tests/test_commands.py::TestDelete::test_delete_todo PASSED
tests/test_commands.py::TestList::test_list_hides_completed PASSED
tests/test_commands.py::TestStats::test_stats_with_todos PASSED
...

========================= 20 passed in 0.15s =========================

---------- coverage ----------
Name                    Stmts   Miss  Cover   Missing
------------------------------------------------------
todo_app/__init__.py        0      0   100%
todo_app/commands.py       52      2    96%   
todo_app/models.py         28      0   100%
todo_app/storage.py        32      1    97%
------------------------------------------------------
TOTAL                     112      3    97%

%97 coverage! 🎉


Adım 7: Packaging — Paketleme

Projeyi bir Python paketi olarak dağıtmak için pyproject.toml:

# pyproject.toml
[build-system]
requires = ["setuptools>=68.0", "wheel"]
build-backend = "setuptools.backends._legacy:_Backend"

[project]
name = "todo-cli-app"
version = "1.0.0"
description = "Basit CLI Todo Uygulaması"
readme = "README.md"
requires-python = ">=3.8"
license = {text = "MIT"}
authors = [
    {name = "Senin Adın", email = "sen@example.com"}
]

[project.scripts]
todo = "todo_app.cli:main"

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v --cov=todo_app"

[project.scripts] bölümü çok önemli. Bu satır sayesinde paket kurulduğunda todo komutu terminal'de kullanılabilir olur.

__init__.py

# todo_app/__init__.py
"""CLI Todo Uygulaması"""
__version__ = "1.0.0"

Kurulum ve Kullanım

# Geliştirme modunda kur
pip install -e .

# Artık her yerden kullanabilirsin
todo add "İlk görev" --priority high
todo list
todo done a1b2
todo stats

pip install -e . (editable mode) ile kodu değiştirdiğinde tekrar kurmana gerek kalmaz.


Adım 8: README.md

Her projenin bir README'si olmalı:

# 📝 Todo CLI

Komut satırından çalışan basit bir todo uygulaması.

## Kurulum

```bash
pip install -e .

Kullanım

# Todo ekle
todo add "Markete git" --priority high

# Listele
todo list
todo list --all
todo list --priority high

# Tamamla
todo done <id>

# Sil
todo delete <id>

# İstatistikler
todo stats

Geliştirme

# Testleri çalıştır
pytest

# Coverage ile
pytest --cov=todo_app --cov-report=html

---

## Tüm Kodların Birleşik Görünümü

Projenin tam yapısı:

todo_app/ ├── todo_app/ │ ├── __init__.py # Paket tanımı, versiyon │ ├── models.py # Todo dataclass │ ├── storage.py # JSON dosya işlemleri │ ├── commands.py # İş mantığı (add, list, complete, delete) │ ├── exceptions.py # Özel exception sınıfları │ └── cli.py # argparse CLI arayüzü ├── tests/ │ ├── __init__.py │ ├── test_models.py # Model testleri │ ├── test_storage.py # Storage testleri │ └── test_commands.py # Komut testleri ├── pyproject.toml # Proje konfigürasyonu └── README.md # Dokümantasyon


Bu projede kullandığımız kavramlar:
- ✅ **Dataclass** — `Todo` modeli
- ✅ **Type hints** — Tüm fonksiyon parametreleri ve dönüş tipleri
- ✅ **File I/O** — JSON ile kaydetme/yükleme
- ✅ **Exception handling** — Özel exception sınıfları, try/except
- ✅ **OOP** — Sınıflar, metodlar, `classmethod`
- ✅ **Modüler yapı** — Her dosya tek sorumluluk
- ✅ **argparse** — CLI arayüzü
- ✅ **pytest** — Kapsamlı testler, fixtures, parametrize
- ✅ **Clean code** — Anlamlı isimler, kısa fonksiyonlar, SOLID
- ✅ **Packaging** — `pyproject.toml`, entry points

---

## Next Steps — Bundan Sonra Ne Yapabilirsin?

Bu proje bir temel. Üzerine ekleyebileceğin şeyler:

### 1. Web Arayüzü (Flask)

```python
from flask import Flask, jsonify, request
from todo_app.commands import TodoApp

app = Flask(__name__)
todo_app = TodoApp()

@app.route("/api/todos", methods=["GET"])
def list_todos():
    todos = todo_app.list_todos(show_all=True)
    return jsonify([t.to_dict() for t in todos])

@app.route("/api/todos", methods=["POST"])
def add_todo():
    data = request.json
    todo = todo_app.add(data["title"], data.get("priority", "normal"))
    return jsonify(todo.to_dict()), 201

Aynı TodoApp sınıfını CLI'dan da web'den de kullanıyorsun. İş mantığı ile arayüz ayrı olduğu için bu mümkün.

2. SQLite Veritabanı

import sqlite3

class SQLiteStorage:
    """JSON yerine SQLite kullanan storage"""
    
    def __init__(self, db_path="todos.db"):
        self.conn = sqlite3.connect(db_path)
        self._create_table()
    
    def _create_table(self):
        self.conn.execute("""
            CREATE TABLE IF NOT EXISTS todos (
                id TEXT PRIMARY KEY,
                title TEXT NOT NULL,
                completed BOOLEAN DEFAULT FALSE,
                priority TEXT DEFAULT 'normal',
                created_at TEXT,
                completed_at TEXT
            )
        """)
        self.conn.commit()
    
    def save(self, todos):
        self.conn.execute("DELETE FROM todos")
        for todo in todos:
            self.conn.execute(
                "INSERT INTO todos VALUES (?, ?, ?, ?, ?, ?)",
                (todo.id, todo.title, todo.completed,
                 todo.priority, todo.created_at, todo.completed_at)
            )
        self.conn.commit()

TodoApp sınıfı storage parametresi aldığı için, JSON'dan SQLite'a geçiş tek satır değişiklik:

app = TodoApp(storage=SQLiteStorage())  # Bitti!

3. REST API

# FastAPI ile
from fastapi import FastAPI

app = FastAPI()

@app.get("/todos")
async def list_todos():
    return todo_app.list_todos(show_all=True)

@app.post("/todos")
async def add_todo(title: str, priority: str = "normal"):
    return todo_app.add(title, priority)

4. Renk ve Tablo Formatı

# rich kütüphanesi ile
from rich.console import Console
from rich.table import Table

console = Console()

def display_todos(todos):
    table = Table(title="📝 Todo Listesi")
    table.add_column("Status", width=3)
    table.add_column("ID", style="dim")
    table.add_column("Title")
    table.add_column("Priority")
    
    for todo in todos:
        status = "✅" if todo.completed else "⬜"
        table.add_row(status, todo.id, todo.title, todo.priority)
    
    console.print(table)

⚠️ Dikkat: Her şeyi bir seferde eklemeye çalışma. Projeyi küçük başlat, çalışır hale getir, sonra adım adım genişlet. YAGNI prensibini hatırla: ihtiyacın olduğunda ekle.


Özet

  • Gerçek bir proje öğrendiklerini birleştirmenin en iyi yoludur — teori yetersiz kalır, pratik şart

  • Proje yapısı önemlidir: her dosyanın tek sorumluluğu olsun (models, storage, commands, cli)

  • dataclass ile veri modelleri temiz ve kısa yazılır, to_dict/from_dict ile serializasyon kolay

  • Dependency Injection (storage parametresi) sayesinde aynı iş mantığını CLI, web, test gibi farklı arayüzlerle kullanabilirsin

  • Test yazmak projenin kalitesini garanti altına alır — her modül için birim testler yaz, %80+ coverage hedefle

  • Küçük başla, adım adım büyüt: Önce çalışsın, sonra güzelleştir, sonra genişlet