Ana içeriğe geç
Kesit Python
İleri · Üretime hazırlık

Paketleme ve dağıtım: pyproject, CLI, sürümleme ve wheel

Kodunu kurulabilir bir pakete dönüştür: pyproject.toml, src düzeni, argparse ile CLI, entry point, semver, wheel/sdist, pip install -e ve Docker'a kısa bakış.

Uygulamada aç, 21 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?

"Bilgisayarımda çalışıyor" cümlesi, kurumsal yazılımda bir hata raporudur. Ilgaz Bank'ta yazdığın kredi skoru modülü üç farklı ekibin projesinde, bir zamanlanmış görevde ve bir Docker konteynerinde çalışacaksa; kodu paket olarak kurulabilir, sürümlenebilir ve komut satırından çağrılabilir yapman gerekir. Bu ders modern Python paketleme standardını (PEP 517/518/621) ve dağıtım zincirini anlatır. Pyodide'de pip ve derleme araçları yok; bu yüzden yapılandırma dosyalarını gösterim olarak okuyacak, CLI ve sürümleme mantığını ise gerçekten çalıştıracağız.

pyproject.toml ve src düzeni

pyproject.toml, paketin tek yapılandırma dosyasıdır: ad, sürüm, bağımlılıklar, derleme aracı ve araç ayarları (ruff, mypy, pytest). src düzeni (src layout) paketi src/paket_adi/ altına koyar; böylece testler kurulmamış kaynak klasörü yerine kurulu paketi içe aktarır ve "yerelde çalışıyor, kurulunca çalışmıyor" hatası erken yakalanır.

PYPROJECT = '''
[build-system]
requires = ["setuptools>=69", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "anadolu-skor"
version = "1.2.0"
description = "Ilgaz Bank kredi skoru araçları"
requires-python = ">=3.11"
dependencies = ["httpx>=0.27", "pydantic>=2"]

[project.optional-dependencies]
dev = ["pytest", "ruff", "mypy"]

[project.scripts]
anadolu-skor = "anadolu_skor.cli:main"

[tool.setuptools.packages.find]
where = ["src"]
'''
import tomllib
cfg = tomllib.loads(PYPROJECT)
print(cfg['project']['name'], cfg['project']['version'])
print('bağımlılıklar:', cfg['project']['dependencies'])
print('komut:', cfg['project']['scripts'])
Dosya / klasörGörevi
pyproject.tomlPaket üst verisi, bağımlılıklar, araç ayarları
src/anadolu_skor/__init__.pyPaket kökü; __version__ burada
src/anadolu_skor/cli.pyKomut satırı giriş noktası
tests/pytest testleri (paketin dışında)
README.md, LICENSE, CHANGELOG.mdBelgeleme ve lisans; README PyPI sayfası olur
dist/Üretilen wheel ve sdist (git'e girmez)
requirements.txt ile farkıdependencies paketin ihtiyaç duyduğu aralıkları söyler (httpx>=0.27). requirements.txt ya da lock dosyası (pip-tools, uv, poetry) ise belirli bir ortamda kurulan tam sürümleri sabitler. Kütüphane yazıyorsan geniş aralık; uygulama dağıtıyorsan lock dosyası.

argparse ile CLI

Paket bir komut sunuyorsa giriş noktası main(argv=None) imzalı olmalı: parse_args(argv) listeyi parametre alır; böylece test, sys.argv'ye dokunmadan main(['skor', '--gelir', '5000']) çağırabilir. Alt komutlar (skor, rapor) için add_subparsers kullan ve her alt komuta set_defaults(func=...) ile işlev bağla.

import argparse, json

def skor_hesapla(gelir: float, gecikme: int) -> int:
    return int(max(300, min(900, gelir / 100 - gecikme * 40)))

def cmd_skor(args):
    s = skor_hesapla(args.gelir, args.gecikme)
    if args.json:
        print(json.dumps({'gelir': args.gelir, 'gecikme': args.gecikme, 'skor': s}))
    else:
        print(f'Skor: {s}')
    return 0 if s >= 500 else 2            # çıkış kodu: 2 = riskli

def cmd_surum(args):
    print('anadolu-skor 1.2.0'); return 0

def build_parser():
    p = argparse.ArgumentParser(prog='anadolu-skor', description='Kredi skoru araçları')
    p.add_argument('-v', '--verbose', action='store_true')
    sub = p.add_subparsers(dest='komut', required=True)
    s = sub.add_parser('skor', help='skor hesapla')
    s.add_argument('--gelir', type=float, required=True)
    s.add_argument('--gecikme', type=int, default=0)
    s.add_argument('--json', action='store_true')
    s.set_defaults(func=cmd_skor)
    sub.add_parser('surum', help='sürümü yaz').set_defaults(func=cmd_surum)
    return p

def main(argv=None) -> int:
    args = build_parser().parse_args(argv)
    return args.func(args)

print('çıkış:', main(['skor', '--gelir', '60000', '--gecikme', '1']))
print('çıkış:', main(['skor', '--gelir', '20000', '--gecikme', '3', '--json']))
print('çıkış:', main(['surum']))
Çıkış kodumain int döndürsün, if __name__ == '__main__': raise SystemExit(main()) ile çağrılsın. 0 başarı, 1 genel hata, 2 kullanım hatası geleneği cron ve CI'da "iş başarısız" sinyalini ücretsiz verir. Hata mesajları stderr'a, veri stdout'a: böylece çıktı boruyla başka komuta aktarılabilir.

Entry point ve editable kurulum

[project.scripts] altındaki anadolu-skor = "anadolu_skor.cli:main" satırı, kurulumda anadolu-skor adlı bir çalıştırılabilir üretir; bu dosya main()'i çağırıp dönüş değerini çıkış koduna çevirir. Geliştirme sırasında pip install -e . (editable) paketi kopyalamak yerine kaynağa bağlar: kodu değiştirince yeniden kurmadan etkisi görülür.

  1. python -m venv .venv && source .venv/bin/activate
  2. pip install -e ".[dev]" → paket + geliştirme bağımlılıkları
  3. anadolu-skor skor --gelir 50000 → entry point çalışır
  4. pytest → testler kurulu paketi içe aktarır
  5. python -m build → dist/anadolu_skor-1.2.0-py3-none-any.whl ve .tar.gz
  6. twine upload dist/* (ya da şirket içi depo: Artifactory, Nexus, devpi)

Sürümleme: semver

Semantik sürümleme MAJOR.MINOR.PATCH: PATCH hata düzeltmesi (geriye uyumlu), MINOR yeni özellik (geriye uyumlu), MAJOR kırıcı değişiklik. Ön sürüm ekleri (1.3.0rc1) sıralamada final sürümün önüne gelir. Bağımlılık belirtirken >=1.2,<2 (ya da ~=1.2) kalıbı, kırıcı değişikliği dışarıda bırakır. Sürümü tek yerde tut: pyproject.toml'da ya da __init__.py'de (dynamic = ["version"] ile diğerinden okunur).

import re
from functools import total_ordering

@total_ordering
class Surum:
    _rx = re.compile(r'^(\d+)\.(\d+)\.(\d+)(?:rc(\d+))?$')

    def __init__(self, metin):
        m = self._rx.match(metin)
        if not m:
            raise ValueError(f'geçersiz sürüm: {metin!r}')
        self.major, self.minor, self.patch = (int(m[i]) for i in (1, 2, 3))
        self.rc = int(m[4]) if m[4] else None

    def _anahtar(self):
        # rc'li sürüm aynı numaralı finalden küçük: (.., 0, rc) < (.., 1, 0)
        return (self.major, self.minor, self.patch, 0 if self.rc is not None else 1, self.rc or 0)

    def __eq__(self, o): return self._anahtar() == o._anahtar()
    def __lt__(self, o): return self._anahtar() < o._anahtar()
    def __repr__(self):
        return f'{self.major}.{self.minor}.{self.patch}' + (f'rc{self.rc}' if self.rc is not None else '')

    def uyumlu(self, diger):
        """~= uyumu: aynı major, en az bu sürüm."""
        return self.major == diger.major and diger >= self

print(sorted(Surum(s) for s in ['1.10.0', '1.2.0', '1.2.0rc1', '2.0.0', '1.9.3']))
print(Surum('1.2.0').uyumlu(Surum('1.9.3')), Surum('1.2.0').uyumlu(Surum('2.0.0')))
Sürüm metni sıralaması'1.10.0' < '1.9.0' metin olarak doğrudur ama sürüm olarak yanlıştır. Her zaman sayısal parçalara ayır ya da packaging.version.Version kullan.

wheel, sdist ve Docker

sdist (source distribution) kaynak kodun arşividir; kurulumda derlenir. wheel önceden derlenmiş, doğrudan kopyalanan biçimdir; saf Python paketlerde py3-none-any, C uzantılılarda platforma özel etiket taşır. Kurumsal depolara wheel yüklemek kurulumu hızlandırır ve derleme araçları gerektirmez.

Docker, paketi işletim sistemi ve Python sürümüyle birlikte paketler. Tipik Dockerfile iki aşamalıdır: ilk aşamada wheel üretilir, ikinci aşamada yalnızca wheel kurulur; böylece derleme araçları son imaja girmez ve imaj küçük kalır.

DOCKERFILE = '''
FROM python:3.12-slim AS build
WORKDIR /src
COPY . .
RUN pip install --no-cache-dir build && python -m build --wheel

FROM python:3.12-slim
COPY --from=build /src/dist/*.whl /tmp/
RUN pip install --no-cache-dir /tmp/*.whl && rm /tmp/*.whl
USER 1000
ENTRYPOINT ["anadolu-skor"]
CMD ["--help"]
'''
asamalar = [s for s in DOCKERFILE.splitlines() if s.startswith('FROM')]
print('aşama sayısı:', len(asamalar))
for s in asamalar:
    print(' ', s)
Derinleş

Derinleş: PyPI'ye adım adım yayımlama

Paketi yerelde kurabilmek işin yarısı. Diğer yarısı onu başkalarının pip install anadolu-skor diyerek kurabileceği hale getirmek: doğru derleme arka ucu (build backend) seçmek, dağıtım dosyalarını üretmek, TestPyPI'de prova yapmak, gerçek PyPI'ye yüklemek ve her yayını bir git etiketine bağlamak. Bu bölüm bu zinciri baştan sona yürür. Docker tarafını "Docker ile dağıtım" dersinde, CI iş akışlarını "Kalite ve CI" dersinde ayrıntılı göreceksin; burada yalnızca yayına dokunan kısmı ele alıyoruz.

1. [build-system]: paketi kim derleyecek?

pip ve python -m build önce [build-system] tablosunu okur (PEP 517/518). requires derleme sırasında geçici ortama kurulacak paketleri, build-backend ise çağrılacak modülü söyler. Bu tablo yoksa araçlar eski setuptools davranışına düşer; uyarı verir ve sonuç tahmin edilemez olur.

Arka uçrequiresbuild-backendNe zaman?
hatchling["hatchling"]hatchling.buildYeni saf Python projeleri; sade ayar, src düzenini kendisi bulur
setuptools["setuptools>=69"]setuptools.build_metaEski projeler, C uzantıları, şirket şablonları
flit-core["flit_core>=3.9"]flit_core.buildapiTek modüllü küçük kütüphaneler
poetry-core["poetry-core"]poetry.core.masonry.apiEkip poetry kullanıyorsa
pdm-backend["pdm-backend"]pdm.backendpdm iş akışı

Aşağıda hatchling ile yayına hazır bir pyproject.toml var. PyPI sayfasında görünen her şey (açıklama, README, lisans, sınıflandırıcılar, bağlantılar) [project] tablosundan gelir. Sürümü git etiketinden almak için dynamic = ["version"] ve hatch-vcs eklentisi kullanılır.

import tomllib

PYPROJECT = """
[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"

[project]
name = "anadolu-skor"
dynamic = ["version"]
description = "Ilgaz Bank kredi skoru araçları"
readme = "README.md"
license = "MIT"
requires-python = ">=3.11"
authors = [{name = "Veri Ekibi", email = "veri@example.com"}]
classifiers = ["Programming Language :: Python :: 3", "Operating System :: OS Independent"]
dependencies = ["httpx>=0.27,<1"]

[project.urls]
Homepage = "https://example.com/anadolu-skor"

[project.scripts]
anadolu-skor = "anadolu_skor.cli:main"

[project.entry-points."anadolu_skor.raporlar"]
pdf = "anadolu_skor.rapor:pdf_yaz"

[tool.hatch.version]
source = "vcs"
"""
cfg = tomllib.loads(PYPROJECT)
p = cfg["project"]
print("arka uç:", cfg["build-system"]["build-backend"])
print("dinamik alanlar:", p["dynamic"])
print("komutlar:", p["scripts"])
print("eklenti grupları:", list(p["entry-points"]))

2. Dağıtımı üret: python -m build

build paketi, arka ucu yalıtılmış geçici bir ortamda çağırır ve dist/ klasörüne iki dosya bırakır: .tar.gz (sdist) ve .whl (wheel). Wheel adı bilgi taşır: ad-sürüm(-derleme)-python-abi-platform.whl. Adın içindeki tireler alt çizgiye çevrilir, çünkü ayırıcı tiredir.

  1. python -m pip install --upgrade build twine → araçları kur (sanal ortam (virtual environment) içinde).
  2. rm -rf dist/ (Windows: rmdir /s /q dist) → eski sürüm dosyalarını temizle.
  3. python -m build → dist/anadolu_skor-1.2.0.tar.gz ve dist/anadolu_skor-1.2.0-py3-none-any.whl.
  4. python -m twine check dist/* → README'nin PyPI'de düzgün görüneceğini ve üst verinin geçerli olduğunu denetler; PASSED görmelisin.
  5. Yeni bir sanal ortamda pip install dist/*.whl ve anadolu-skor --help → wheel gerçekten çalışıyor mu?
import re

def wheel_oku(dosya):
    govde = dosya.removesuffix(".whl").split("-")
    ad, surum, *orta, py, abi, plat = govde
    return {"ad": ad, "surum": surum, "derleme": orta[0] if orta else None,
            "python": py, "abi": abi, "platform": plat}

for d in ["anadolu_skor-1.2.0-py3-none-any.whl",
          "numpy-2.1.0-cp312-cp312-manylinux_2_17_x86_64.whl"]:
    w = wheel_oku(d)
    saf = w["abi"] == "none" and w["platform"] == "any"
    print(w["ad"], w["surum"], "saf Python" if saf else "platforma özel: " + w["platform"])

# PyPI ad normalleştirmesi (PEP 503): Anadolu_Skor, anadolu.skor ve anadolu-skor AYNI projedir
print({re.sub(r"[-_.]+", "-", a).lower() for a in ["Anadolu_Skor", "anadolu.skor", "anadolu-skor"]})

3. Önce TestPyPI, sonra PyPI

TestPyPI (test.pypi.org) gerçek PyPI'nin ayrı veritabanlı kopyasıdır. Hesabı ayrıdır; orada yaptığın hata gerçek projeyi etkilemez. PyPI'de bir sürüm numarasını bir kez yükleyebilirsin: dosyayı silsen bile aynı numarayı tekrar kullanamazsın. Provayı bu yüzden TestPyPI'de yaparsın.

# 1) pypi.org ve test.pypi.org'da hesap aç, 2FA'yı aç, Account settings → API tokens → token üret
# 2) TestPyPI'ye yükle (kullanıcı adı: __token__, parola: pypi-... ile başlayan token)
python -m twine upload --repository testpypi dist/*

# 3) Temiz ortamda TestPyPI'den kurmayı dene (bağımlılıklar gerçek PyPI'den gelsin)
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ anadolu-skor

# 4) Her şey yolundaysa gerçek PyPI
python -m twine upload dist/*

Token'ı her seferinde yazmamak için ~/.pypirc dosyası kullanılır; dosya izinlerini chmod 600 ~/.pypirc ile kısıtla ve asla git'e koyma. CI'da daha iyisi Trusted Publishing: PyPI proje ayarlarında GitHub deposunu "güvenilir yayıncı" olarak eklersin; iş akışı OIDC ile kısa ömürlü kimlik alır, depoda hiç gizli token tutulmaz.

[distutils]
index-servers = pypi testpypi

[pypi]
username = __token__
password = pypi-AgEIcHlwaS5vcmc...

[testpypi]
repository = https://test.pypi.org/legacy/
username = __token__
password = pypi-AgENdGVzdC5weXBp...
BelirtiNedenÇözüm
400 File already existsBu sürüm daha önce yüklendi (silinmiş olsa bile)Sürümü yükselt, yeniden derle
403 Forbidden / "not allowed to upload"Ad başkasına ait ya da yanlış tokenFarklı ad seç; proje kapsamlı token üret
twine check: README render hatasıreadme türü yanlış ya da RST hatasıMarkdown kullan, readme = "README.md"
Kurulumdan sonra ModuleNotFoundErrorPaket klasörü wheel'e girmemişwheel'i unzip -l ile incele; src düzeni ayarını kontrol et
Eski sürüm de yüklenmeye çalışıyordist/ temizlenmemişDerlemeden önce dist/ klasörünü sil
TestPyPI'den kurarken bağımlılık bulunamadıBağımlılık TestPyPI'de yok--extra-index-url https://pypi.org/simple/

4. Sürüm etiketleme

Yayın akışı şöyledir: CHANGELOG'u güncelle, sürümü yükselt, commit et, açıklamalı etiket at, etiketi it. hatch-vcs ya da setuptools-scm kullanıyorsan sürüm numarasını hiçbir dosyaya yazmazsın; etiketten okunur, etiketsiz commit'lerde 1.2.1.dev3+g1a2b3c gibi geliştirme sürümü üretilir.

git tag -a v1.2.0 -m "Sürüm 1.2.0"
git push origin v1.2.0          # CI bu etiketi görünce derler ve yayımlar
git describe --tags             # v1.2.0-3-g1a2b3c: etiketten bu yana 3 commit
Yanlış sürüm yayımladıysanPyPI'de sürümü silmek yerine "yank" et: pip install anadolu-skor o sürümü artık seçmez ama ==1.2.0 ile sabitleyenler kırılmaz. Ardından düzeltilmiş 1.2.1 yayımla.

5. Entry point'ler ayrıntılı

Entry point, "bu ad şu modülün şu nesnesidir" diyen kayıttır: ad = "paket.modul:nesne". Üç türü vardır:

  • [project.scripts] → console_scripts grubu: kurulumda bin/ (Windows'ta Scripts) altına çalıştırılabilir dosya üretilir. Fonksiyonun dönüş değeri çıkış kodu olur.
  • [project.gui-scripts] → Windows'ta konsol penceresi açmadan çalışan GUI uygulamaları.
  • [project.entry-points."grup.adi"] → eklenti (plugin) sistemi: başka paketler senin grubuna kayıt ekler, sen importlib.metadata.entry_points(group=...) ile bulup yüklersin. pytest eklentileri bu yolla çalışır.

python -m anadolu_skor ile de çalışsın istiyorsan paketin içine __main__.py koy ve içinde from .cli import main; raise SystemExit(main()) yaz.

from importlib.metadata import EntryPoint

# Kurulu paketlerin kaydını taklit ediyoruz: başka ekipler kendi dışa aktarıcılarını ekledi
kayitlar = [
    EntryPoint(name="json", value="json:dumps", group="anadolu.export"),
    EntryPoint(name="girinti", value="textwrap:indent", group="anadolu.export"),
    EntryPoint(name="bozuk", value="olmayan_paket.mod:yaz", group="anadolu.export"),
]
eklentiler = {}
for ep in kayitlar:
    try:
        eklentiler[ep.name] = ep.load()          # import + getattr
    except ImportError as e:
        print(f"atlandı: {ep.name} ({type(e).__name__})")
print(sorted(eklentiler))
print(ep.module, ep.attr)
print(eklentiler["json"]({"sube": 34, "adet": 3}))
Gerçek işteŞirket içi kütüphanelerde akış neredeyse hep aynıdır: main dalına birleştirilen PR → v1.4.0 etiketi → CI testleri koşar, python -m build, twine check, önce iç TestPyPI benzeri "staging" depoya, onaydan sonra Artifactory/Nexus'a yükler. Senden beklenen, bu adımları elle bir kez yapıp anlamış olman ve pyproject.toml'daki bir hatanın hangi adımda patlayacağını tahmin edebilmendir.
Paket verisiPakete JSON/CSV gibi dosya koyuyorsan open("veri/oranlar.json") yazma; kurulumdan sonra çalışma klasörü farklıdır. importlib.resources.files("anadolu_skor") / "veri/oranlar.json" kullan ve dosyanın wheel'e girdiğini unzip -l ile doğrula.
Uzmanlaş

Kurumsal dağıtım zinciri

Olgun ekiplerde paket sürümü git etiketinden türetilir (setuptools-scm ya da hatch-vcs): v1.2.0 etiketi atılınca CI wheel üretir, testleri koşar, şirket içi PyPI'ye yükler ve Docker imajını aynı etiketle kayıt defterine (registry) iter. Elle sürüm numarası yazmak ve "unuttum" hataları böylece biter. CHANGELOG, "Keep a Changelog" biçiminde tutulur ve her MINOR/MAJOR sürümde kırıcı değişiklikler açıkça yazılır.

Bağımlılık güvenliği: lock dosyası tekrarlanabilir kurulum sağlar; pip-audit bilinen zafiyetleri tarar; Dependabot/Renovate güncellemeleri otomatik PR olarak açar. Docker imajında root olmayan kullanıcı, sabitlenmiş temel imaj etiketi ve --no-cache-dir standarttır.

Alternatif araç zincirleri: uv (çok hızlı pip/venv/lock), poetry ve hatch (bağımlılık + derleme + yayın tek araç). Hepsi aynı pyproject.toml standardına dayanır; ekip hangisini seçerse seçsin bu dersteki kavramlar değişmez.

Alıştırmalara geç (21)