Proje tanımı
Anadolu Market'in 12 mağazası her gün kasa sisteminden bir CSV dökümü üretir ve bunları ortak bir klasöre bırakır. Bölge müdürü her pazartesi sabahı bu dosyaları Excel'de birleştirir, hatalı satırları ayıklar, mağaza ve ürün bazında özet çıkarır, bir e-posta yazıp tabloyu yapıştırır. Bu iş yaklaşık iki saat sürüyor ve geçen ay iki kez aynı dosya iki kere eklendiği için ciro şişik raporlandı.
Senin görevin bu süreci uçtan uca otomatikleştiren, cron ya da Görev Zamanlayıcı ile her pazartesi 07:45'te çalışacak bir komut satırı aracı yazmak: "Anadolu Market haftalık satış raporu otomasyonu". Bu proje orta seviyedeki otomasyon derslerinin hepsini birleştirir: Ofis ve iş akışı otomasyonu (pathlib, dry-run, log, EmailMessage), Veri doğrulama ve pydantic v2, collections ve itertools, Şablonla rapor üretme (Jinja2) ve Standart kütüphane turu (argparse).
| Gereksinim | Nasıl karşılanır |
|---|---|
| Klasördeki tüm günlük CSV'leri oku, başka dosyaları yok say | Path.glob("*.csv") + csv.DictReader |
| Hatalı satırları ayıkla, hangi dosyanın hangi satırı olduğunu raporla | pydantic modeli + _kaynak alanı |
| Aynı satır iki kez gelirse bir kez say | (sipariş no, ürün) anahtarlı küme |
| Mağaza cirosu, en çok satan 3 ürün, günlük ciro | Counter, groupby |
| Okunur HTML rapor ve e-posta taslağı | Jinja2 (autoescape) + EmailMessage |
| Güvenli çalıştırma, iz bırakma | Varsayılan dry-run, --uygula bayrağı, logging |
| Zamanlayıcıdan parametreyle çağrılabilme | argparse CLI, çıkış kodu |
Mimari: küçük, test edilebilir parçalar
Her adım tek bir iş yapan saf bir fonksiyon olsun: girdi alır, çıktı döndürür, mümkünse dosyaya ya da ağa dokunmaz. Yan etkiler (dosya yazma, e-posta) yalnızca en dıştaki calistir fonksiyonunda toplanır. Bu tasarım her parçayı ayrı test etmeni, dry-run'ı tek bir if ile kurmanı ve ileride bir parçayı (ör. CSV yerine veritabanı) değiştirmeni kolaylaştırır.
klasoru_oku(klasor)→ ham satır sözlükleri (her biri_kaynak: "dosya.csv:satır")SatisSatirimodeli → tek satırın kurallarıtemizle(satirlar)→ (geçerli modeller, hata metinleri)ozetle(gecerli)→ özet sözlüğühtml_rapor(ozet, hafta, hatalar)→ HTML metnieposta_taslagi(html, ozet, hafta, alicilar)→EmailMessagearguman_ayristir(argv)→ komut satırı ayarlarıcalistir(argv)→ hepsini sırayla çağırır, loglar, dry-run değilse dosyaları yazar, çıkış kodu döndürür
haftalik_rapor/ paketinde okuma.py, model.py, ozet.py, rapor.py, cli.py ve templates/rapor.html olarak bölünür; tests/ klasöründe her modülün testi durur. Burada tek dosyada ilerliyoruz ki tarayıcıda çalıştırabilesin.Adım 1: Klasörü okumak
Gerçek klasöre dokunmadan çalışmak için örnek veriyi geçici bir klasöre yazıyoruz. Veriye kasıtlı olarak sorunlar ekledik: virgüllü fiyat, boşluklu mağaza adı, boş kategori, iki kez gelmiş bir satır, adedi 0 olan satır, bozuk sipariş numarası ve klasöre karışmış bir .txt dosyası.
import tempfile
from pathlib import Path
BASLIK = "siparis_no,tarih,magaza,urun,kategori,adet,birim_fiyat\n"
def ornek_klasor():
k = Path(tempfile.mkdtemp()) / "satis"
k.mkdir()
(k / "satis_2026-10-26.csv").write_text(BASLIK
+ 'AM-1001,2026-10-26,Kadıköy,Türk kahvesi,İçecek,2,"89,90"\n'
+ "AM-1002,2026-10-26,Çankaya,Çay,İçecek,5,45\n"
+ 'AM-1003,2026-10-26, Kadıköy ,Çaydanlık,Mutfak,1,"1.249,00"\n', encoding="utf-8")
(k / "satis_2026-10-27.csv").write_text(BASLIK
+ "AM-1004,2026-10-27,Bornova,Çay,İçecek,10,45\n"
+ "AM-1002,2026-10-26,Çankaya,Çay,İçecek,5,45\n"
+ "AM-1005,2026-10-27,Çankaya,Filtre kahve,,3,120.5\n"
+ "AM-1006,2026-10-27,Bornova,Su,İçecek,0,7.5\n"
+ "X-1,2026-10-27,Konak,Su,İçecek,2,abc\n", encoding="utf-8")
(k / "okubeni.txt").write_text("CSV değil, okunmamalı", encoding="utf-8")
return k
import csv
from pathlib import Path
def klasoru_oku(klasor):
"""Klasördeki tüm .csv dosyalarını ad sırasıyla okur; her satıra _kaynak ekler."""
satirlar = []
for dosya in sorted(Path(klasor).glob("*.csv")):
with dosya.open(encoding="utf-8", newline="") as f:
for no, satir in enumerate(csv.DictReader(f), start=2):
satir["_kaynak"] = f"{dosya.name}:{no}"
satirlar.append(satir)
return satirlar
satirlar = klasoru_oku(ornek_klasor())
print(len(satirlar), "satır")
for s in satirlar[:2]:
print(s)
enumerate(..., start=2) satır numarasını dosyadaki gerçek satırla eşler (1. satır başlık). Hata raporunda "satis_2026-10-27.csv:5" yazmak, mağaza çalışanının sorunu Excel'de saniyede bulmasını sağlar. newline="" csv modülünün tırnak içindeki satır sonlarını doğru işlemesi için önerilen ayardır.
Adım 2: Model ve temizlik
Tek bir satırın kuralları pydantic modelinde durur: sipariş numarası AM- ile başlar, adet ve fiyat pozitiftir, Türkçe ondalık ("1.249,00") kabul edilir, boş kategori "Diğer" olur. Temizlik fonksiyonu modeli her satıra uygular, hatalıları ve tekrarları ayırır. Tekrarı tanımlayan anahtar (sipariş no, ürün): bir sipariş birden çok ürün içerebilir ama aynı ürün aynı siparişte iki satır olamaz.
from datetime import date
from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator
class SatisSatiri(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True)
siparis_no: str = Field(pattern=r"^AM-\d+$")
tarih: date
magaza: str = Field(min_length=1)
urun: str = Field(min_length=1)
kategori: str = "Diğer"
adet: int = Field(gt=0)
birim_fiyat: float = Field(gt=0)
@field_validator("birim_fiyat", mode="before")
@classmethod
def tr_ondalik(cls, v):
if isinstance(v, str) and "," in v:
v = v.replace(".", "").replace(",", ".")
return v
@field_validator("kategori", mode="before")
@classmethod
def bos_kategori(cls, v):
if v is None or (isinstance(v, str) and not v.strip()):
return "Diğer"
return v
@property
def tutar(self):
return round(self.adet * self.birim_fiyat, 2)
def temizle(satirlar):
"""(geçerli SatisSatiri listesi, hata metinleri) döndürür."""
gecerli, hatalar, gorulen = [], [], set()
for satir in satirlar:
kaynak = satir.get("_kaynak", "?")
veri = {k: v for k, v in satir.items() if not k.startswith("_")}
try:
s = SatisSatiri.model_validate(veri)
except ValidationError as e:
alanlar = ",".join(".".join(map(str, h["loc"])) for h in e.errors())
hatalar.append(f"{kaynak} geçersiz: {alanlar}")
continue
anahtar = (s.siparis_no, s.urun)
if anahtar in gorulen:
hatalar.append(f"{kaynak} tekrar: {s.siparis_no}/{s.urun}")
continue
gorulen.add(anahtar)
gecerli.append(s)
return gecerli, hatalar
ham = [
{"siparis_no": "AM-1", "tarih": "2026-10-26", "magaza": " Kadıköy ", "urun": "Çay", "kategori": "", "adet": "2", "birim_fiyat": "1.249,50", "_kaynak": "a.csv:2"},
{"siparis_no": "AM-1", "tarih": "2026-10-26", "magaza": "Kadıköy", "urun": "Çay", "kategori": "", "adet": "2", "birim_fiyat": "1249.5", "_kaynak": "b.csv:7"},
{"siparis_no": "X-2", "tarih": "2026-10-26", "magaza": "Konak", "urun": "Su", "kategori": "İçecek", "adet": "0", "birim_fiyat": "7,5", "_kaynak": "b.csv:8"},
]
gecerli, hatalar = temizle(ham)
print(gecerli[0].magaza, gecerli[0].kategori, gecerli[0].birim_fiyat, gecerli[0].tutar)
print(hatalar)
Adım 3: Özet
Özet bir sözlük: toplam ciro, sipariş ve satır sayısı, mağaza cirosu (büyükten küçüğe), en çok satan 3 ürün (adet), günlük ciro. Mağaza ve ürün için Counter, gün için itertools.groupby kullanıyoruz. groupby yalnızca ardışık eşit anahtarları gruplar; bu yüzden önce sıralamak zorunludur. Eşitlikte sıralamayı ada göre sabitlemek (key=lambda x: (-x[1], x[0])) raporun her çalıştırmada aynı çıkmasını sağlar; most_common eşitlikleri ekleme sırasına göre bırakır.
from collections import namedtuple
from datetime import date
S = namedtuple("S", "siparis_no tarih magaza urun adet tutar")
D = date.fromisoformat
from collections import Counter
from itertools import groupby
def ozetle(gecerli):
magaza, urun_adet = Counter(), Counter()
for s in gecerli:
magaza[s.magaza] += s.tutar
urun_adet[s.urun] += s.adet
sirali = sorted(gecerli, key=lambda s: s.tarih)
gunluk = {t.isoformat(): round(sum(s.tutar for s in grup), 2)
for t, grup in groupby(sirali, key=lambda s: s.tarih)}
return {
"toplam_ciro": round(sum(magaza.values()), 2),
"satir_sayisi": len(gecerli),
"siparis_sayisi": len({s.siparis_no for s in gecerli}),
"magaza_ciro": sorted(((m, round(c, 2)) for m, c in magaza.items()), key=lambda x: (-x[1], x[0])),
"en_cok_satan": sorted(urun_adet.items(), key=lambda x: (-x[1], x[0]))[:3],
"gunluk_ciro": gunluk,
}
veri = [S("AM-1", D("2026-10-27"), "Kadıköy", "Çay", 5, 225.0),
S("AM-2", D("2026-10-26"), "Çankaya", "Kahve", 2, 179.8),
S("AM-2", D("2026-10-26"), "Çankaya", "Çay", 1, 45.0),
S("AM-3", D("2026-10-27"), "Kadıköy", "Su", 6, 45.0)]
for anahtar, deger in ozetle(veri).items():
print(f"{anahtar:15} {deger}")
Testlerde pydantic modeli yerine aynı niteliklere sahip bir namedtuple kullandık: ozetle yalnızca .magaza, .tutar gibi niteliklere ihtiyaç duyar (ördek tiplemesi, duck typing). Parçalar arası bağımlılık ne kadar azsa test o kadar kolaydır.
Adım 4: HTML rapor ve e-posta taslağı
Sunum Jinja şablonunda, biçim (tl filtresi) tek bir fonksiyonda. autoescape açık: mağaza ya da ürün adında < olsa bile rapor bozulmaz. E-posta taslağı düz metin + HTML alternatifi + mağaza cirosu CSV eki içerir; müdür isterse eki doğrudan Excel'de açar.
from jinja2 import Environment
def tl(x):
return f"{x:,.2f}".translate(str.maketrans(",.", ".,")) + " TL"
ENV = Environment(autoescape=True, trim_blocks=True, lstrip_blocks=True)
ENV.filters["tl"] = tl
RAPOR = ENV.from_string("""<h1>Anadolu Market | {{ hafta }} haftalık satış raporu</h1>
<p>Toplam ciro: <b>{{ ozet.toplam_ciro|tl }}</b> ({{ ozet.siparis_sayisi }} sipariş, {{ ozet.satir_sayisi }} satır)</p>
<table>
{% for magaza, ciro in ozet.magaza_ciro %}
<tr><td>{{ magaza }}</td><td>{{ ciro|tl }}</td></tr>
{% endfor %}
</table>
<ol>
{% for urun, adet in ozet.en_cok_satan %}
<li>{{ urun }} ({{ adet }} adet)</li>
{% endfor %}
</ol>
{% if hatalar %}
<p>Uyarı: {{ hatalar|length }} satır rapora alınmadı.</p>
{% endif %}""")
def html_rapor(ozet, hafta, hatalar=()):
return RAPOR.render(ozet=ozet, hafta=hafta, hatalar=hatalar)
from email.message import EmailMessage
def eposta_taslagi(html, ozet, hafta, alicilar):
msg = EmailMessage()
msg["From"] = "rapor@anadolumarket.com.tr"
msg["To"] = ", ".join(alicilar)
msg["Subject"] = f"{hafta} haftalık satış raporu - {tl(ozet['toplam_ciro'])}"
msg.set_content(f"Merhaba,\n\n{hafta} haftasında toplam ciro {tl(ozet['toplam_ciro'])}.\nAyrıntılar HTML raporda ve ekteki CSV'de.\n")
msg.add_alternative(html, subtype="html")
satirlar = ["magaza,ciro"] + [f"{m},{c:.2f}" for m, c in ozet["magaza_ciro"]]
msg.add_attachment("\n".join(satirlar) + "\n", subtype="csv", filename=f"magaza_ciro_{hafta}.csv")
return msg
ozet = {"toplam_ciro": 2465.3, "satir_sayisi": 5, "siparis_sayisi": 5,
"magaza_ciro": [("Kadıköy", 1428.8), ("Çankaya", 586.5), ("Bornova", 450.0)],
"en_cok_satan": [("Çay", 15), ("Filtre kahve", 3), ("Türk kahvesi", 2)], "gunluk_ciro": {}}
html = html_rapor(ozet, "2026-W44", ["x.csv:3 tekrar"])
print(html)
msg = eposta_taslagi(html, ozet, "2026-W44", ["mudur@anadolumarket.com.tr"])
print(msg["Subject"])
print([p.get_content_type() for p in msg.iter_parts()], [p.get_filename() for p in msg.iter_attachments()])
Adım 5: Komut satırı, log ve dry-run
Zamanlayıcı betiği parametrelerle çağırır: python haftalik_rapor.py --klasor /veri/satis --hafta 2026-W44 --alici mudur@... --uygula. argparse argümanları ayrıştırır, --help çıktısını otomatik üretir ve hatalı girdide anlaşılır mesajla 2 koduyla çıkar. Önemli kararlar:
- Varsayılan dry-run:
--uygulaverilmedikçe hiçbir dosya yazılmaz.action="store_true"bayrağı değer almaz;type=boolkullanma (bool("False")True'dur). - Birden çok alıcı:
action="append"aynı bayrağın tekrarlanmasıyla liste oluşturur. - Hafta doğrulama:
type=parametresine kendi fonksiyonunu veripArgumentTypeErrorfırlatırsan argparse güzel bir hata mesajı basar. - Çıkış kodu: 0 başarı, 1 iş hatası (CSV yok). Zamanlayıcı ve izleme araçları buna bakar.
- Log seviyesi: cron'da
WARNING, elle denemedeDEBUG. Mesajlarda%syer tutucu kullan.
Tam çözüm
Aşağıdaki program tüm parçaları birleştirir ve önce dry-run, sonra gerçek çalıştırma yapar. Çalıştırıp log satırlarını, çıkış kodlarını ve üretilen HTML'i incele.
import tempfile
from pathlib import Path
BASLIK = "siparis_no,tarih,magaza,urun,kategori,adet,birim_fiyat\n"
def ornek_klasor():
k = Path(tempfile.mkdtemp()) / "satis"
k.mkdir()
(k / "satis_2026-10-26.csv").write_text(BASLIK
+ 'AM-1001,2026-10-26,Kadıköy,Türk kahvesi,İçecek,2,"89,90"\n'
+ "AM-1002,2026-10-26,Çankaya,Çay,İçecek,5,45\n"
+ 'AM-1003,2026-10-26, Kadıköy ,Çaydanlık,Mutfak,1,"1.249,00"\n', encoding="utf-8")
(k / "satis_2026-10-27.csv").write_text(BASLIK
+ "AM-1004,2026-10-27,Bornova,Çay,İçecek,10,45\n"
+ "AM-1002,2026-10-26,Çankaya,Çay,İçecek,5,45\n"
+ "AM-1005,2026-10-27,Çankaya,Filtre kahve,,3,120.5\n"
+ "AM-1006,2026-10-27,Bornova,Su,İçecek,0,7.5\n"
+ "X-1,2026-10-27,Konak,Su,İçecek,2,abc\n", encoding="utf-8")
(k / "okubeni.txt").write_text("CSV değil, okunmamalı", encoding="utf-8")
return k
import csv
from pathlib import Path
def klasoru_oku(klasor):
"""Klasördeki tüm .csv dosyalarını ad sırasıyla okur; her satıra _kaynak ekler."""
satirlar = []
for dosya in sorted(Path(klasor).glob("*.csv")):
with dosya.open(encoding="utf-8", newline="") as f:
for no, satir in enumerate(csv.DictReader(f), start=2):
satir["_kaynak"] = f"{dosya.name}:{no}"
satirlar.append(satir)
return satirlar
from datetime import date
from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator
class SatisSatiri(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True)
siparis_no: str = Field(pattern=r"^AM-\d+$")
tarih: date
magaza: str = Field(min_length=1)
urun: str = Field(min_length=1)
kategori: str = "Diğer"
adet: int = Field(gt=0)
birim_fiyat: float = Field(gt=0)
@field_validator("birim_fiyat", mode="before")
@classmethod
def tr_ondalik(cls, v):
if isinstance(v, str) and "," in v:
v = v.replace(".", "").replace(",", ".")
return v
@field_validator("kategori", mode="before")
@classmethod
def bos_kategori(cls, v):
if v is None or (isinstance(v, str) and not v.strip()):
return "Diğer"
return v
@property
def tutar(self):
return round(self.adet * self.birim_fiyat, 2)
def temizle(satirlar):
"""(geçerli SatisSatiri listesi, hata metinleri) döndürür."""
gecerli, hatalar, gorulen = [], [], set()
for satir in satirlar:
kaynak = satir.get("_kaynak", "?")
veri = {k: v for k, v in satir.items() if not k.startswith("_")}
try:
s = SatisSatiri.model_validate(veri)
except ValidationError as e:
alanlar = ",".join(".".join(map(str, h["loc"])) for h in e.errors())
hatalar.append(f"{kaynak} geçersiz: {alanlar}")
continue
anahtar = (s.siparis_no, s.urun)
if anahtar in gorulen:
hatalar.append(f"{kaynak} tekrar: {s.siparis_no}/{s.urun}")
continue
gorulen.add(anahtar)
gecerli.append(s)
return gecerli, hatalar
from collections import Counter
from itertools import groupby
def ozetle(gecerli):
magaza, urun_adet = Counter(), Counter()
for s in gecerli:
magaza[s.magaza] += s.tutar
urun_adet[s.urun] += s.adet
sirali = sorted(gecerli, key=lambda s: s.tarih)
gunluk = {t.isoformat(): round(sum(s.tutar for s in grup), 2)
for t, grup in groupby(sirali, key=lambda s: s.tarih)}
return {
"toplam_ciro": round(sum(magaza.values()), 2),
"satir_sayisi": len(gecerli),
"siparis_sayisi": len({s.siparis_no for s in gecerli}),
"magaza_ciro": sorted(((m, round(c, 2)) for m, c in magaza.items()), key=lambda x: (-x[1], x[0])),
"en_cok_satan": sorted(urun_adet.items(), key=lambda x: (-x[1], x[0]))[:3],
"gunluk_ciro": gunluk,
}
from jinja2 import Environment
def tl(x):
return f"{x:,.2f}".translate(str.maketrans(",.", ".,")) + " TL"
ENV = Environment(autoescape=True, trim_blocks=True, lstrip_blocks=True)
ENV.filters["tl"] = tl
RAPOR = ENV.from_string("""<h1>Anadolu Market | {{ hafta }} haftalık satış raporu</h1>
<p>Toplam ciro: <b>{{ ozet.toplam_ciro|tl }}</b> ({{ ozet.siparis_sayisi }} sipariş, {{ ozet.satir_sayisi }} satır)</p>
<table>
{% for magaza, ciro in ozet.magaza_ciro %}
<tr><td>{{ magaza }}</td><td>{{ ciro|tl }}</td></tr>
{% endfor %}
</table>
<ol>
{% for urun, adet in ozet.en_cok_satan %}
<li>{{ urun }} ({{ adet }} adet)</li>
{% endfor %}
</ol>
{% if hatalar %}
<p>Uyarı: {{ hatalar|length }} satır rapora alınmadı.</p>
{% endif %}""")
def html_rapor(ozet, hafta, hatalar=()):
return RAPOR.render(ozet=ozet, hafta=hafta, hatalar=hatalar)
from email.message import EmailMessage
def eposta_taslagi(html, ozet, hafta, alicilar):
msg = EmailMessage()
msg["From"] = "rapor@anadolumarket.com.tr"
msg["To"] = ", ".join(alicilar)
msg["Subject"] = f"{hafta} haftalık satış raporu - {tl(ozet['toplam_ciro'])}"
msg.set_content(f"Merhaba,\n\n{hafta} haftasında toplam ciro {tl(ozet['toplam_ciro'])}.\nAyrıntılar HTML raporda ve ekteki CSV'de.\n")
msg.add_alternative(html, subtype="html")
satirlar = ["magaza,ciro"] + [f"{m},{c:.2f}" for m, c in ozet["magaza_ciro"]]
msg.add_attachment("\n".join(satirlar) + "\n", subtype="csv", filename=f"magaza_ciro_{hafta}.csv")
return msg
import argparse, re
def hafta_tipi(metin):
if not re.fullmatch(r"\d{4}-W\d{2}", metin):
raise argparse.ArgumentTypeError(f"hafta YYYY-Www biçiminde olmalı: {metin}")
return metin
def arguman_ayristir(argv=None):
p = argparse.ArgumentParser(prog="haftalik_rapor", description="Anadolu Market haftalık satış raporu")
p.add_argument("--klasor", required=True, help="CSV dosyalarının klasörü")
p.add_argument("--hafta", required=True, type=hafta_tipi, help="ör. 2026-W44")
p.add_argument("--cikti", default="cikti", help="rapor klasörü")
p.add_argument("--alici", action="append", default=[], dest="alicilar", help="birden çok kez verilebilir")
p.add_argument("--uygula", action="store_true", help="dosyaları gerçekten yaz (varsayılan: dry-run)")
p.add_argument("--log-seviye", default="INFO", choices=["DEBUG", "INFO", "WARNING", "ERROR"])
return p.parse_args(argv)
import logging
log = logging.getLogger("haftalik_rapor")
def calistir(argv=None):
args = arguman_ayristir(argv)
log.setLevel(args.log_seviye)
satirlar = klasoru_oku(args.klasor)
if not satirlar:
log.error("klasörde CSV satırı yok: %s", args.klasor)
return 1
log.info("%d satır okundu", len(satirlar))
gecerli, hatalar = temizle(satirlar)
for h in hatalar:
log.warning(h)
ozet = ozetle(gecerli)
html = html_rapor(ozet, args.hafta, hatalar)
msg = eposta_taslagi(html, ozet, args.hafta, args.alicilar or ["mudurler@anadolumarket.com.tr"])
cikti = Path(args.cikti)
hedefler = [cikti / f"rapor_{args.hafta}.html", cikti / f"taslak_{args.hafta}.eml"]
if not args.uygula:
for h in hedefler:
log.info("[DRY-RUN] yazılacaktı: %s", h.name)
return 0
cikti.mkdir(parents=True, exist_ok=True)
hedefler[0].write_text(html, encoding="utf-8")
hedefler[1].write_bytes(bytes(msg))
log.info("yazıldı: %s", ", ".join(h.name for h in hedefler))
return 0
if __name__ == "__main__":
import sys
logging.basicConfig(stream=sys.stdout, format="%(levelname)-7s %(message)s", force=True)
klasor = ornek_klasor()
cikti = klasor.parent / "cikti"
print("Çıkış kodu:", calistir(["--klasor", str(klasor), "--hafta", "2026-W44", "--cikti", str(cikti)]))
print("Çıkış kodu:", calistir(["--klasor", str(klasor), "--hafta", "2026-W44", "--cikti", str(cikti),
"--alici", "mudur@anadolumarket.com.tr", "--uygula", "--log-seviye", "WARNING"]))
print(sorted(p.name for p in cikti.iterdir()))
print((cikti / "rapor_2026-W44.html").read_text(encoding="utf-8"))
Çıktıyı okumak
İlk çalıştırmada (dry-run) log önce okunan satır sayısını, ardından atılan üç satırı nedenleriyle gösterir: satis_2026-10-27.csv:3 önceki dosyadaki siparişin tekrarı, :5 adedi 0 olan satır, :6 hem sipariş numarası hem fiyatı bozuk satır. Sonra yazılacak iki dosyanın adını söyler ama klasör oluşturulmaz. İkinci çalıştırmada --uygula ve --log-seviye WARNING verdik: bilgi satırları kaybolur, yalnızca uyarılar kalır ve dosyalar yazılır. Bu, cron'daki tipik kullanımdır: her şey yolundaysa log sessizdir, bir sorun varsa yalnızca o görünür.
Rapordaki toplam ciro 2.465,30 TL'dir. Tekrar ayıklanmasaydı Çankaya'nın cirosu 225 TL fazla, yani yaklaşık %38 şişik görünecekti; bu, otomasyona eklediğin tek bir kümenin değerini somut olarak gösterir. Alıştırmalarda bu parçaları tek tek yazacak, ardından iki mini projeyle aracı genişleteceksin: tüm boru hattını (pipeline) birleştiren calistir ve geçen haftayla karşılaştırma tablosu. Meydan okuma görevindeki anomali tespiti de rapora "dikkat" bölümü eklemenin ilk adımıdır.
Kendi bilgisayarında bu dosyayı haftalik_rapor.py olarak kaydedip ornek_klasor() yerine gerçek klasörü vererek çalıştırabilirsin. Gönderim için Ofis ve iş akışı otomasyonu dersindeki smtplib.SMTP_SSL bloğunu --gonder bayrağının arkasına ekle; varsayılan davranış her zaman yalnızca taslak üretmek olsun.
# Linux/macOS cron (crontab -e): her pazartesi 07:45 45 7 * * 1 /opt/rapor/.venv/bin/python /opt/rapor/haftalik_rapor.py --klasor /veri/satis --hafta $(date +\%G-W\%V) --uygula >> /var/log/rapor.log 2>&1 # Windows Görev Zamanlayıcı schtasks /create /sc weekly /d MON /st 07:45 /tn HaftalikRapor /tr "C:\rapor\.venv\Scripts\python.exe C:\rapor\haftalik_rapor.py --klasor D:\satis --hafta 2026-W44 --uygula"
Bu projeyi üretime taşısaydın
- Testler: Her parça için pytest testleri;
tmp_pathfikstürü ile geçici klasör; HTML çıktısı için anlık görüntü (snapshot) testi.calistiriçin uçtan uca test: örnek klasör → dry-run → çıkış kodu 0 ve hiç dosya yok. - Yapılandırma: Alıcılar, SMTP ve klasör yolları
pydantic-settingsile ortam değişkenlerinden; şifreSecretStr. - İdempotentlik: Aynı hafta için rapor zaten gönderildiyse (durum dosyası ya da SQLite tablosu) ikinci çalıştırma göndermesin.
- Ölçek: Dosyalar büyüdüğünde satır satır pydantic yerine pandas/polars ile vektörel doğrulama; özetleme
groupbyile. - Gözlemlenebilirlik: Log'u JSON biçiminde yazıp merkezi bir sisteme gönder; "atılan satır oranı %5'i aştı" gibi eşiklerde uyarı üret.
- Paketleme:
pyproject.tomliçinde[project.scripts] haftalik-rapor = "haftalik_rapor.cli:calistir"ile komut olarak kur.