Ana içeriğe geç
Kesit Python
Orta · Otomasyon, web ve araçlar

Ofis ve iş akışı otomasyonu

pathlib ve shutil ile klasör düzenler, toplu yeniden adlandırır ve tarih damgalı yedek alırsın; EmailMessage ile ekli e-posta hazırlar, zamanlama ve dosya izlemeyi kurar, dry-run, log ve geri alma ile otomasyonu güvenli hale getirirsin.

Uygulamada aç, 27 alıştırmayı çözÜcretsiz, hesap gerekmez, kurulum yok.

Bu sayfa dersin okuma sürümüdür. Metin bir kutuyu çalıştırmanı ya da adıma geçmeni söylüyorsa dersi uygulamada aç.

Neden önemli?

Anadolu Market'in muhasebe ekibinde her pazartesi aynı tören yaşanır: "Gelen" klasörüne düşen 40-50 fatura PDF'i, Excel dökümü ve fiş fotoğrafı tek tek açılır, doğru klasöre sürüklenir, adı "2026-10-26_fatura_01.pdf" biçimine çevrilir, klasörün yedeği alınır ve haftalık özet e-postayla yöneticiye gider. Bu iş yarım gün sürer, sıkıcıdır ve sıkıcı işte hata yapılır. Aynı işi 60 satırlık bir Python betiği birkaç saniyede, her seferinde aynı biçimde yapar. Buna ofis otomasyonu (office automation) ya da iş akışı otomasyonu (workflow automation) denir.

Bu derste gerçek bir otomasyon betiğinin parçalarını kuracaksın: klasör düzenleme, toplu yeniden adlandırma, tarih damgalı yedek, e-posta hazırlama, PDF/Word dokunuşları, zamanlama ve dosya izleme. En önemlisi, otomasyonun güvenli olmasını sağlayan üç alışkanlığı öğreneceksin: kuru çalıştırma (dry-run), loglama ve geri alma. pathlib ve shutil ile ilk tanışmanı Dosya işlemleri ve Standart kütüphane turu derslerinde yapmıştın; burada onları bir iş sürecine dönüştürüyoruz.

İşSıklıkElle süreOtomasyona değer mi?
Gelen klasörünü uzantıya göre ayırmakHer gün15 dkKesinlikle: kural net, risk düşük
Haftalık satış raporunu e-postalamakHaftada bir30 dkEvet: biçim sabit, veri değişken
Yıllık bütçe sunumunu hazırlamakYılda bir2 günKısmen: yorum insan işi, tablolar otomatik
Müşteri şikâyetine cevap yazmakHer günDeğişkenHayır: yargı gerektirir; yalnız taslak hazırla

Kural basit: sık tekrarlanan, kuralı yazıya dökülebilen ve hatası geri alınabilen işler otomasyonun ilk adaylarıdır.

Güvenli oyun alanı: geçici klasör

Otomasyon kodunu ilk kez gerçek "Belgeler" klasöründe denemek, yeni sürücünün otoyola çıkması gibidir. Her zaman önce geçici bir klasörde (temporary directory) dene. Bu sayfadaki örnekler tarayıcıdaki bellek içi dosya sisteminde çalışır; kendi bilgisayarında da aynı kod geçici klasör kullandığı için gerçek dosyalarına dokunmaz.

import tempfile
from pathlib import Path

kok = Path(tempfile.mkdtemp()) / "Gelen"
kok.mkdir()
for ad in ["Fatura_Ekim.PDF", "satislar.csv", "fis 01.jpg", "notlar.txt", "stok.xlsx"]:
    (kok / ad).write_text("örnek içerik", encoding="utf-8")

for dosya in sorted(kok.iterdir()):
    print(f"{dosya.name:20} uzantı={dosya.suffix.lower():6} boyut={dosya.stat().st_size} bayt")

Path.iterdir() klasördeki öğeleri, suffix uzantıyı, stat() boyut ve değişiklik zamanını verir. Uzantıyı her zaman .lower() ile karşılaştır: Windows'tan gelen dosyalar sıkça .PDF ya da .JPG olur.

Klasör düzenleme: uzantıya göre taşıma

İlk otomasyonumuz klasik "indirilenler temizliği". Önce bir kural tablosu (uzantı → kategori) kur, sonra her dosya için hedefi hesapla. Kodun iki aşamalı olduğuna dikkat et: önce plan (ne yapılacağını listele), sonra uygula. Bu ayrım dry-run'ı bedavaya getirir.

import tempfile, shutil
from pathlib import Path

KURAL = {".pdf": "Belgeler", ".docx": "Belgeler", ".txt": "Belgeler",
         ".csv": "Tablolar", ".xlsx": "Tablolar", ".jpg": "Görseller", ".png": "Görseller"}

def plan_yap(kok):
    plan = []
    for dosya in sorted(p for p in kok.iterdir() if p.is_file()):
        kategori = KURAL.get(dosya.suffix.lower(), "Diğer")
        plan.append((dosya, kok / kategori / dosya.name))
    return plan

def uygula(plan, dry_run=True):
    for kaynak, hedef in plan:
        etiket = "[DRY-RUN] " if dry_run else ""
        print(f"{etiket}{kaynak.name} -> {hedef.parent.name}/")
        if not dry_run:
            hedef.parent.mkdir(parents=True, exist_ok=True)
            shutil.move(kaynak, hedef)

kok = Path(tempfile.mkdtemp())
for ad in ["Fatura_Ekim.PDF", "satislar.csv", "fis01.jpg", "README"]:
    (kok / ad).write_text("x")

uygula(plan_yap(kok))              # önce yalnızca göster
uygula(plan_yap(kok), dry_run=False)
print(sorted(str(p.relative_to(kok)) for p in kok.rglob("*") if p.is_file()))
Plan-uygula ayrımıPlanı bir liste olarak döndürmek; onu test etmeyi, ekrana basmayı, kullanıcıdan onay almayı ve log'a yazmayı kolaylaştırır. Dosyaya dokunan kod ne kadar küçük olursa hata ihtimali o kadar düşer.

mkdir(parents=True, exist_ok=True) ara klasörleri de oluşturur ve klasör zaten varsa hata vermez. shutil.move farklı disk bölümleri arasında da çalışır; Path.rename ise aynı bölüm içinde daha hızlıdır ama Windows'ta hedef varsa hata verir.

Toplu yeniden adlandırma

Fiş fotoğrafları "IMG_4821.JPG", "Fiş - Şubat (son).jpg" gibi adlarla gelir. Arşiv için tutarlı bir ad kuralı belirle: tarih öneki + Türkçe karakter ve boşluk içermeyen gövde + sıra numarası. Tarih önekini YYYYMMDD biçiminde yazarsan dosya adına göre sıralama aynı zamanda kronolojik sıralama olur.

import re

TR = str.maketrans("çğıöşüÇĞİÖŞÜ", "cgiosuCGIOSU")

def sade(metin):
    metin = metin.translate(TR).lower()
    metin = re.sub(r"[^a-z0-9]+", "_", metin)
    return metin.strip("_")

adlar = ["Fiş - Şubat (son).JPG", "IMG_4821.jpg", "Kargo İrsaliyesi.jpg"]
for i, ad in enumerate(sorted(adlar), start=1):
    govde, _, uzanti = ad.rpartition(".")
    yeni = f"20261026_{i:03d}_{sade(govde)}.{uzanti.lower()}"
    print(f"{ad:25} -> {yeni}")
Çakışma kontrolüİki farklı dosya aynı yeni ada dönüşebilir ("Rapor.pdf" ve "rapor.PDF"). Yeniden adlandırmadan önce hedefin var olup olmadığına (hedef.exists()) bak; varsa atla ya da sonuna sayaç ekle. Aksi halde bir dosyanın üzerine sessizce yazarsın ve veri kaybı olur.

Tarih damgalı yedek

Bir klasörü değiştirmeden önce yedeğini almak en ucuz sigortadır. Yedek adına zaman damgası (timestamp) koy; eskileri belirli bir sayının üzerinde sil (rotasyon). Testlerde ve örneklerde sabit bir tarih kullan ki sonuç her çalıştırmada aynı olsun; gerçek betikte datetime.now() kullanırsın.

import tempfile, shutil
from datetime import datetime
from pathlib import Path

alan = Path(tempfile.mkdtemp())
kaynak = alan / "Muhasebe"
kaynak.mkdir()
(kaynak / "ekim.csv").write_text("tutar\n100\n")
yedekler = alan / "Yedekler"

for gun in [20, 21, 22, 23, 24]:
    an = datetime(2026, 10, gun, 18, 0)
    hedef = yedekler / f"Muhasebe_{an:%Y%m%d_%H%M}"
    shutil.copytree(kaynak, hedef)

# rotasyon: yalnızca en yeni 3 yedek kalsın
hepsi = sorted(p for p in yedekler.iterdir() if p.is_dir())
for eski in hepsi[:-3]:
    shutil.rmtree(eski)
    print("silindi:", eski.name)
print("kalan:", [p.name for p in sorted(yedekler.iterdir())])

Tek dosyalık arşiv istersen shutil.make_archive(str(hedef), "zip", kaynak) klasörü zip'ler. shutil.copy2 tek dosyayı değiştirilme zamanıyla birlikte kopyalar.

Otomasyonun riskleri: dry-run, log, geri alma

Elle yapılan hata bir dosyayı bozar; otomasyon hatası binlercesini aynı anda bozar. Bu yüzden profesyonel otomasyon betiklerinde üç emniyet kemeri vardır:

  1. Kuru çalıştırma (dry-run): Betik yapacaklarını yazdırır ama hiçbir dosyaya dokunmaz. Varsayılan mod dry-run olsun; gerçek çalıştırma için açıkça --uygula gibi bir bayrak iste.
  2. Loglama (logging): Her işlemi zamanıyla birlikte bir log dosyasına yaz. "Dün gece betik ne yaptı?" sorusunun cevabı print çıktısında değil, log dosyasındadır.
  3. Geri alma (undo): Yapılan her taşımayı (kaynak, hedef) çifti olarak kaydet; ters sırayla uygulayarak işlemi geri al.
import logging, tempfile, shutil, io
from pathlib import Path

akis = io.StringIO()
logging.basicConfig(stream=akis, level=logging.INFO, format="%(levelname)s %(message)s", force=True)
log = logging.getLogger("duzenleyici")

kok = Path(tempfile.mkdtemp())
for ad in ["a.pdf", "b.csv"]:
    (kok / ad).write_text("x")

gunluk = []                       # geri alma günlüğü
for dosya in sorted(kok.glob("*.*")):
    hedef = kok / dosya.suffix[1:].upper() / dosya.name
    hedef.parent.mkdir(exist_ok=True)
    shutil.move(dosya, hedef)
    gunluk.append((dosya, hedef))
    log.info("taşındı %s -> %s", dosya.name, hedef.parent.name)

for kaynak, hedef in reversed(gunluk):  # GERİ AL
    shutil.move(hedef, kaynak)
    log.warning("geri alındı %s", kaynak.name)

print(akis.getvalue())
print(sorted(p.name for p in kok.iterdir() if p.is_file()))

logging modülünde mesajı %s yer tutucularıyla ver (f-string değil): seviye kapalıysa metin hiç oluşturulmaz. Gerçek betikte stream yerine filename="otomasyon.log" verirsin.

E-posta: mesajı oluşturmak ve göndermek

E-posta iki ayrı iştir: mesajı oluşturmak (başlıklar, gövde, ekler) ve göndermek (SMTP sunucusuna bağlanmak). Birincisi tamamen email.message.EmailMessage ile yapılır ve tarayıcıda da çalışır; ikincisi ağ ister.

from email.message import EmailMessage

msg = EmailMessage()
msg["From"] = "rapor@anadolumarket.com.tr"
msg["To"] = ", ".join(["mudur@anadolumarket.com.tr", "finans@anadolumarket.com.tr"])
msg["Subject"] = "Haftalık satış özeti - 43. hafta"
msg.set_content("Merhaba,\n\nBu haftanın özeti ektedir.\n\nOtomasyon Botu")
msg.add_alternative("<h3>Haftalık özet</h3><p>Toplam ciro: <b>182.450 TL</b></p>", subtype="html")
msg.add_attachment("magaza,ciro\nKadıköy,98200\nÇankaya,84250\n", subtype="csv", filename="ozet.csv")

print(msg["Subject"])
print(msg.get_content_type())
print([p.get_content_type() for p in msg.iter_parts()])
print([p.get_filename() for p in msg.iter_attachments()])

set_content düz metin gövdeyi, add_alternative aynı içeriğin HTML sürümünü ekler (istemci hangisini gösterebiliyorsa onu seçer). add_attachment metin için subtype, ikili veri (PDF, resim) için maintype ve subtype ister: msg.add_attachment(pdf_bytes, maintype="application", subtype="pdf", filename="fatura.pdf"). Ek eklenince mesaj otomatik olarak multipart/mixed olur.

Göndermek için smtplib kullanırsın. Bu kod ağ gerektirdiği için burada yalnızca gösterim:

import os, smtplib

kullanici = os.environ["SMTP_KULLANICI"]
sifre = os.environ["SMTP_SIFRE"]          # uygulama şifresi, ASLA koda yazma

with smtplib.SMTP_SSL("smtp.gmail.com", 465, timeout=30) as sunucu:
    sunucu.login(kullanici, sifre)
    sunucu.send_message(msg)
Kendi bilgisayarında deneGmail ve Outlook, hesap şifrenle SMTP girişine izin vermez. İki adımlı doğrulamayı aç, hesap ayarlarından 16 karakterlik bir uygulama şifresi (app password) üret ve ortam değişkenine koy: Windows'ta setx SMTP_SIFRE "...", macOS/Linux'ta export SMTP_SIFRE="...". Kurumsal ortamda genellikle şirketin iç SMTP sunucusu (port 587 + starttls()) kullanılır. Denemelerde mesajı göndermek yerine open("taslak.eml", "wb").write(bytes(msg)) ile dosyaya yazıp e-posta istemcinde açabilirsin.

PDF ve Word dokunuşları

Ofiste en sık istenen iki iş: birçok PDF'i birleştirmek ve şablon bir Word belgesine veri doldurmak. pypdf ve python-docx bilgisayarında pip install pypdf python-docx ile kurulur; gerçek dosyayla görevler PDF ve Word belgeleriyle otomasyon dersinde, tarayıcıda çalışır.

from pathlib import Path
from pypdf import PdfReader, PdfWriter

yazici = PdfWriter()
for yol in sorted(Path("faturalar").glob("*.pdf")):
    okuyucu = PdfReader(yol)
    print(yol.name, len(okuyucu.pages), "sayfa")
    for sayfa in okuyucu.pages:
        yazici.add_page(sayfa)
yazici.write("ekim_faturalari.pdf")
print(PdfReader("ekim_faturalari.pdf").pages[0].extract_text()[:200])
from docx import Document

belge = Document()
belge.add_heading("Anadolu Market - Haftalık Rapor", level=1)
belge.add_paragraph("Bu hafta toplam ciro 182.450 TL oldu.")
tablo = belge.add_table(rows=1, cols=2)
tablo.rows[0].cells[0].text, tablo.rows[0].cells[1].text = "Mağaza", "Ciro"
for magaza, ciro in [("Kadıköy", "98.200"), ("Çankaya", "84.250")]:
    hucreler = tablo.add_row().cells
    hucreler[0].text, hucreler[1].text = magaza, ciro
belge.save("haftalik_rapor.docx")

Word şablonunda "{{musteri}}" gibi yer tutucuları değiştirmek gerekiyorsa Şablonla rapor üretme dersindeki Jinja mantığını docxtpl paketiyle Word'e taşıyabilirsin.

Zamanlama: betik kendiliğinden çalışsın

Betiği her pazartesi elle çalıştırmak otomasyonun yarısıdır. Üç yol var:

AraçNeredeÖrnekNot
schedule paketiHer yerde, Python içindeschedule.every().monday.at("08:30").do(is)Betik sürekli açık kalmalı
cronLinux / macOS30 8 * * 1 /usr/bin/python3 /opt/rapor.pySunucularda standart
Görev ZamanlayıcıWindowsschtasks /create /sc weekly /d MON /st 08:30 /tn Rapor /tr "py C:\is\rapor.py"Arayüzden de kurulur

Cron ifadesi beş alandan oluşur: dakika saat gün ay haftagünü. * "her", 1-5 aralık, 8,12 liste, */15 "15'te bir" demektir; haftagünü 0 (ya da 7) pazardır. Yani */15 9-17 * * 1-5 "hafta içi 09:00-17:45 arası çeyrek saatte bir" anlamına gelir.

# pip install schedule
import time, schedule

def haftalik_rapor():
    print("rapor hazırlanıyor...")

schedule.every().monday.at("08:30").do(haftalik_rapor)
while True:
    schedule.run_pending()
    time.sleep(30)
Zamanlanmış betiğin tuzaklarıCron ve Görev Zamanlayıcı betiği senin terminalinden farklı bir ortamda çalıştırır: çalışma klasörü farklıdır (göreli yollar bozulur), sanal ortam (virtual environment) etkin değildir, ortam değişkenleri eksik olabilir. Yolları Path(__file__).parent ile mutlak kur, sanal ortamın python'unu tam yoluyla çağır ve çıktıyı log dosyasına yönlendir.

Dosya izleme

"Klasöre yeni fatura düşer düşmez işle" senaryosu için iki yol var: watchdog paketi işletim sisteminin bildirimlerini dinler (pip install watchdog); daha basit ve her yerde çalışan yol ise yoklama (polling): belirli aralıkla klasörün anlık görüntüsünü (snapshot) alıp öncekiyle karşılaştırmak.

def anlik(dosyalar):
    """ad -> (boyut, değiştirilme zamanı) sözlüğü; gerçekte Path.stat() ile doldurulur"""
    return dict(dosyalar)

once = anlik({"a.pdf": (120, 1000), "b.csv": (40, 1000)})
sonra = anlik({"a.pdf": (150, 1060), "c.jpg": (900, 1061)})

eklenen = sorted(sonra.keys() - once.keys())
silinen = sorted(once.keys() - sonra.keys())
degisen = sorted(ad for ad in once.keys() & sonra.keys() if once[ad] != sonra[ad])
print("eklenen:", eklenen, "| silinen:", silinen, "| değişen:", degisen)

Sözlüklerin keys() görünümleri küme gibi davranır: fark (-) ve kesişim (&) doğrudan çalışır. Yarım kopyalanmış bir dosyayı işlememek için boyutun iki ardışık yoklamada değişmediğini görmeyi beklemek yaygın bir hiledir.

Gerçek işteBir lojistik firmasında irsaliye PDF'lerini müşteri koduna göre klasörleyen betik, ilk gün "Diğer" klasörüne 300 dosya taşıdı: tedarikçi dosya adındaki ayıracı "-" yerine "_" yapmıştı. Ekip betiği dry-run modunda çalıştırıp planı bir CSV'ye yazdırmayı ve "Diğer"e düşen dosya sayısı eşiği aşarsa uyarı e-postası atmayı ekledi. Otomasyonun asıl değeri işi hızlı yapmasında değil, beklenmedik durumu fark edip durmasındadır.
Derinleş

Zamanlanmış bir betik sessizce başarısız olursa, sorunu genellikle bir yönetici "rapor neden gelmedi?" diye sorduğunda öğrenirsin. Bu bölüm iki eksiği kapatıyor: her çalıştırmanın sonunda bir çalışma özeti üretmek ve hata olduğunda bildirim göndermek (Teams/Slack webhook ve e-posta); ardından Windows Görev Zamanlayıcı'nın "neden elle çalışıyor da zamanlanınca çalışmıyor" tuzakları.

Çalışma özeti: her çalıştırma bir kayıt

Betiğin ana gövdesini bir sarmalayıcıya al: başlangıç-bitiş zamanı, işlenen ve atlanan dosya sayısı, uyarılar ve durum tek bir sözlükte toplansın. Bu özet hem loga, hem bildirime, hem de (istersen) bir CSV'ye "çalışma geçmişi" olarak yazılır.

import json, traceback
from datetime import datetime

def calistir(is_fonksiyonu, ad, simdi=datetime.now):
    ozet = {"is": ad, "baslangic": simdi().isoformat(timespec="seconds"), "durum": "BAŞARILI",
            "islenen": 0, "atlanan": 0, "uyarilar": []}
    try:
        is_fonksiyonu(ozet)                    # iş, sayaçları ve uyarıları özete yazar
        if ozet["uyarilar"]:
            ozet["durum"] = "UYARILI"
    except Exception as e:
        ozet["durum"] = "HATA"
        ozet["hata"] = f"{type(e).__name__}: {e}"
        ozet["iz"] = traceback.format_exc(limit=3)
    ozet["bitis"] = simdi().isoformat(timespec="seconds")
    return ozet

def rapor_isi(ozet):
    for ad in ["ocak.xlsx", "subat.xlsx", "~$mart.xlsx"]:
        if ad.startswith("~$"):                # Excel'in açık dosya kilidi
            ozet["atlanan"] += 1
            ozet["uyarilar"].append(f"{ad} kilit dosyası, atlandı")
            continue
        ozet["islenen"] += 1

sabit = lambda: datetime(2024, 11, 4, 8, 30)
ozet = calistir(rapor_isi, "aylik_rapor", sabit)
print(json.dumps({k: v for k, v in ozet.items() if k != "iz"}, ensure_ascii=False))

Bildirim: webhook ve e-posta

Microsoft Teams ve Slack, bir kanala dışarıdan mesaj göndermek için gelen webhook (incoming webhook) adresi verir: bu adrese JSON ile POST atarsın, mesaj kanala düşer. Teams'te yeni yol "Workflows" uygulamasındaki "webhook isteği alındığında kanala gönder" akışıdır; eski Office 365 bağlayıcıları kaldırılmaktadır. Adres bir parola gibidir: bilen herkes kanala mesaj atabilir, bu yüzden ortam değişkeninde tutulur.

import os, json, urllib.request

def webhook_gonder(ozet):
    govde = {"text": f"[{ozet['durum']}] {ozet['is']}: {ozet['islenen']} işlendi, "
                     f"{ozet['atlanan']} atlandı" + (f"\nHata: {ozet['hata']}" if "hata" in ozet else "")}
    istek = urllib.request.Request(os.environ["TEAMS_WEBHOOK_URL"], data=json.dumps(govde).encode("utf-8"),
                                   headers={"Content-Type": "application/json"}, method="POST")
    try:
        with urllib.request.urlopen(istek, timeout=10) as y:
            return y.status
    except OSError as e:
        print("Bildirim gönderilemedi:", e)    # bildirim hatası asıl işi düşürmesin

Kurallar: yalnız durum değişince ya da hata olunca bildir (her başarılı çalıştırmada mesaj atmak, kanalı sessize alınan bir gürültüye çevirir); aynı hatayı her 5 dakikada bir tekrar gönderme (susturma süresi); mesaja kişisel veri ya da tam yığın izi koyma, ayrıntıyı log dosyasının yolunu vererek ilet; bildirim gönderilemezse bunu logla ama asıl işi başarısız sayma. E-posta için derste gördüğün EmailMessage + smtplib kalıbını aynı özet metniyle kullan; ek olarak log dosyasının son 50 satırını ekleyebilirsin.

Windows Görev Zamanlayıcı tuzakları

BelirtiNedenÇözüm
Elle çalışıyor, zamanlanınca "dosya bulunamadı"Çalışma klasörü C:\Windows\System32"Başlangıç yeri" (Start in) alanını doldur; kodda Path(__file__).parent kullan
Yalnız oturum açıkken çalışıyor"Yalnızca kullanıcı oturum açtığında çalıştır" seçili"Kullanıcı oturum açmış olsa da olmasa da çalıştır"ı seç; parola istenir
Oturum kapalıyken ağ sürücüsüne erişemiyorZ: gibi eşlenmiş sürücüler yalnız etkileşimli oturumda vardırUNC yolu kullan: \\sunucu\paylasim\rapor; hesaba paylaşım izni ver
Parola değişince görev sessizce durduGörev, kaydedilmiş parolayla çalışırParolası süresiz servis hesabı ya da gMSA kullan; parola değişince görevi güncelle
Excel/Outlook otomasyonu takılıyorOturum kapalıyken masaüstü uygulaması açılamazopenpyxl/pandas gibi arayüzsüz kütüphaneler, SMTP ya da Graph API kullan
"Son çalıştırma sonucu 0x1"Betik 1 çıkış koduyla bitti (Python hatası)Betiğin kendi log dosyasına bak; görev çıktısı hiçbir yere yazılmaz
Yanlış Python ya da paket bulunamadıPATH'teki python farklıPrograma sanal ortamın tam yolunu ver: C:\is\.venv\Scripts\python.exe
schtasks /create /tn "AnadoluRapor" /sc daily /st 07:45 ^
  /tr "C:\is\.venv\Scripts\python.exe C:\is\rapor.py" ^
  /ru "ANADOLU\svc_rapor" /rp * /rl LIMITED
schtasks /query /tn "AnadoluRapor" /v /fo LIST     # son çalıştırma zamanı ve sonucu
schtasks /run /tn "AnadoluRapor"                   # şimdi dene

/ru ve /rp * görevin hangi hesapla, oturum açık olmadan çalışacağını belirler (* parolayı gizli sorar). /rl LIMITED yönetici yetkisi vermez; gerekmedikçe yükseltme. "Ayarlar" sekmesinde "görev zaten çalışıyorsa yeni örnek başlatma" seçeneği, derste gördüğün kilit dosyasını tamamlar.

Kimlik bilgisi tuzağı"Oturum açmasa da çalıştır" seçildiğinde görev, Windows Kimlik Bilgisi Yöneticisi'ne (Credential Manager) senin oturumunda kaydedilmiş sırlara erişemeyebilir ve %USERPROFILE% farklı çözülebilir. Sırları ortam değişkenine görev hesabı için tanımla ya da dosyayı yalnız o hesabın okuyabildiği bir klasöre koy; kendi kişisel hesabını otomasyon için kullanma.
Gerçek işteOfis otomasyonunun güvenilir olup olmadığını kullanıcılar çıktıdan değil sessizlikten anlar: hata olduğunda bir dakika içinde kanala düşen kısa bir mesaj ve ertesi sabah "dün 3 dosya atlandı, nedeni şu" diyen özet, betiğe duyulan güveni kurar.
Uzmanlaş

Etkisiz (idempotent) betik

İyi bir otomasyon betiğini iki kez üst üste çalıştırmak bir kez çalıştırmakla aynı sonucu vermelidir. Zaten taşınmış dosyayı tekrar taşımaya çalışmamak, var olan yedeği ezmemek, aynı e-postayı iki kez göndermemek (gönderildi bilgisini bir durum dosyasına yazmak) bu ilkenin parçalarıdır. Zamanlanmış görevler bazen iki kez tetiklenir ya da yarıda kesilir; idempotent betik bu durumları sorunsuz atlatır.

Kilit dosyası ve çakışan çalıştırmalar

Betik 15 dakikada bir çalışıyorsa ve bir çalıştırma 20 dakika sürerse ikisi aynı dosyalar üzerinde yarışır. Başta Path("calisiyor.lock").touch(exist_ok=False) ile kilit al, FileExistsError gelirse çık, finally içinde kilidi sil.

Atomik yazma ve geri dönüşüm kutusu

Raporu önce rapor.tmp'ye yazıp sonra os.replace ile asıl ada çevir; yarım dosya oluşmaz. Silme yerine send2trash paketiyle geri dönüşüm kutusuna gönder ya da "Silinecekler" klasörüne taşı ve 30 gün sonra temizle.

Gizli bilgiler ve yetki

Şifre, API anahtarı ve SMTP bilgileri koda değil ortam değişkenine ya da .env dosyasına (git'e eklenmeyen) yazılır. Otomasyon hesabına yalnızca gereken klasörlerde yetki ver; "her yere yazabilen bot" en tehlikeli kullanıcıdır.

Alıştırmalara geç (27)