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.mdHer dosyanın tek bir sorumluluğu var:
models.py→ Veri yapısı (Todo nesnesi)storage.py→ Veriyi diske kaydetme/yüklemecommands.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:
dataclassile boilerplate kodu azalttık (__init__,__repr__otomatik)uuidile her todo'ya benzersiz ID atadıkto_dict/from_dictile 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 öğrenAdı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
.tmpdosyaya yaz, sonrareplaceile asıl dosyaya taşı. Yazma sırasında program çökerse veri kaybolmaz.Encoding:
utf-8veensure_ascii=Falseile Türkçe karakterler düzgün kaydedilir.Error handling: JSON bozuksa boş liste döner, çökmez.
Path:
pathlib.Pathile 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:
storageparametre olarak alınıyor. Test ederken mock storage verebilirsin.Validation: Her komut girdileri doğruluyor, hatalı girdi için
ValueErrorfırlatıyor.Kısmi ID eşleşme:
_find_by_idtam ID yerine başlangıç kısmıyla da eşleşir. Kullanıcıa1b2yazarsaa1b2c3d4bulunur.Private metodlar:
_saveve_find_by_idunderscore 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 gitAdı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ı"""
passBu 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 ValueErroryazdığı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-missingtests/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 statspip 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 statsGeliş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()), 201Aynı 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_dictile serializasyon kolayDependency 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
AI Asistan
Sorularını yanıtlamaya hazır