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ör | Görevi |
|---|---|
| pyproject.toml | Paket üst verisi, bağımlılıklar, araç ayarları |
| src/anadolu_skor/__init__.py | Paket kökü; __version__ burada |
| src/anadolu_skor/cli.py | Komut satırı giriş noktası |
| tests/ | pytest testleri (paketin dışında) |
| README.md, LICENSE, CHANGELOG.md | Belgeleme ve lisans; README PyPI sayfası olur |
| dist/ | Üretilen wheel ve sdist (git'e girmez) |
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']))
main 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.
python -m venv .venv && source .venv/bin/activatepip install -e ".[dev]"→ paket + geliştirme bağımlılıklarıanadolu-skor skor --gelir 50000→ entry point çalışırpytest→ testler kurulu paketi içe aktarırpython -m build→dist/anadolu_skor-1.2.0-py3-none-any.whlve.tar.gztwine 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')))
'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ş: 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ç | requires | build-backend | Ne zaman? |
|---|---|---|---|
| hatchling | ["hatchling"] | hatchling.build | Yeni saf Python projeleri; sade ayar, src düzenini kendisi bulur |
| setuptools | ["setuptools>=69"] | setuptools.build_meta | Eski projeler, C uzantıları, şirket şablonları |
| flit-core | ["flit_core>=3.9"] | flit_core.buildapi | Tek modüllü küçük kütüphaneler |
| poetry-core | ["poetry-core"] | poetry.core.masonry.api | Ekip poetry kullanıyorsa |
| pdm-backend | ["pdm-backend"] | pdm.backend | pdm 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.
python -m pip install --upgrade build twine→ araçları kur (sanal ortam (virtual environment) içinde).rm -rf dist/(Windows:rmdir /s /q dist) → eski sürüm dosyalarını temizle.python -m build→dist/anadolu_skor-1.2.0.tar.gzvedist/anadolu_skor-1.2.0-py3-none-any.whl.python -m twine check dist/*→ README'nin PyPI'de düzgün görüneceğini ve üst verinin geçerli olduğunu denetler;PASSEDgörmelisin.- Yeni bir sanal ortamda
pip install dist/*.whlveanadolu-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...
| Belirti | Neden | Çözüm |
|---|---|---|
400 File already exists | Bu 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ış token | Farklı 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 ModuleNotFoundError | Paket 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ışıyor | dist/ 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
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_scriptsgrubu: kurulumdabin/(Windows'taScripts) 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, senimportlib.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}))
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.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.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.