38.3 Eszközbevezetés, licenc és verziófrissítés

Licencellenőrzés: az alkalmazáslicenc, a függőségek licence és a modell-/adatlicenc külön ellenőrzendő. A megengedett felhasználás nem következik abból, hogy a csomag telepíthető vagy a súlyok nyilvánosan elérhetők.

38.3.1 Cél és konkrét helyzet

A Northstar lab csapata egy változáskérelmet kap: a szervezet fenntartási szabályzata szerint a projekt pandas-dependenciáját 3.x főverzióra kell emelni, mert az N-1 politika a 2.x sorozatot a következő negyedévben elavultnak fogja minősíteni. A döntést nem „majd kiderül” alapon hozzák meg, hanem verziókezelési szabállyal, licencellenőrzéssel, frissítési teszttel, platformkorláttal és költség-/karbantartási korláttal alátámasztott döntési lapot (decision sheet) készítenek.

Bemenetek: a lab/pyproject.toml verziótartománya, a saját uv.lock a feloldás után, a lab/starter_experiment.py, a projekt tesztjei és a meglévő kontrolljegyzék. Megfigyelési egység: egy ellenőrzési tétel a döntési lapon; minden tételhez érték, dátumozott forrás és állapot (verified, pending, n/a) tartozik. Kimenet: kitöltött döntési lap és go/no-go döntés. Előzetes becslés: a főverzió-emelés elsősorban az implicit láncolt értékadást (chained assignment) törheti; mivel a kezdőprojekt explicit .loc-ot használ, a kockázat várhatóan alacsony – de ez csak hipotézis, amíg a frissítési teszt le nem fut.

38.3.2 Az öt döntési összetevő

Version policy (verziókezelési szabály). A Python-ökoszisztéma jellemzően szemantikus verziózást használ: MAJOR.MINOR.PATCH. A PATCH csak hibajavítást, a MINOR visszafelé kompatibilis bővítést, a MAJOR törő változást jelez. A pyproject.toml tartománya szándék, a uv.lock a feloldás pillanatának rögzített ténye: a tartományból később más verzió is feloldható, a lockból nem. A policy két kérdést ír elő minden emelés előtt: hány támogatott főverziót tartunk életben, és milyen teszt jár az egyes emelési osztályokhoz. A példában az N-1 szabály azt jelenti, hogy a legutóbbi stabil főverziót és az előtte lévőt tartják karbantartva; ha a 3.x stabil, a 2.x karbantartási ablak zárul. Az ellenőrizhető forrás a csomag METADATA Requires-Python mezője és a release notes.

Kis számpélda: 2.2.3 → 2.2.4 patch, 2.2.3 → 2.3.0 minor, 2.2.3 → 3.0.0 major. A konzervatív policy patch-emelésnél füsttesztet, minor-emelésnél teljes tesztet és numerikus kontrollt, major-emelésnél ezeken felül kompatibilitási auditot és rövid canary-ablakot követel.

License check (licencellenőrzés). A csomag licencét nem elég egyszer ismerni: a célverziónál újra ellenőrizni kell, mert a licenc is változhat. A gyakori SPDX-azonosítók közül a permissive családba tartozik a MIT, BSD, Apache-2.0; a copyleft családba a GPL, a hálózati szolgáltatásnál különösen érzékeny AGPL; létezik kettős licencelés is. A döntést a felhasználás módja befolyásolja: belső script, tovább terjesztett csomag vagy nyilvános szolgáltatás különböző kockázati osztály. A döntési lap ezért nem „a pandas szabad-e” kérdést tesz fel, hanem azt, hogy melyik SPDX-kifejezés szerepel a célverzió metaadatában, és mikor nyitották meg az upstream LICENSE fájlt. Jogi állásfoglalást a lap nem helyettesít.

Upgrade test (frissítési teszt). A frissítés nem verziók cseréje, hanem viselkedésváltozás kockázata. Három réteg: füstteszt (import, alapművelet, ismert invariant), teljes projektteszt, majd reprodukálható numerikus kontroll. A kontroll nem „ránézésre ugyanaz”, hanem a Jelentés és nyelvek közötti adatcsere alfejezetben bevezetett abszolút-relatív tolerancia:

|a−b|≤ϵa+ϵr⋅|b|,|a - b| \le \epsilon_a + \epsilon_r \cdot |b|,

ahol aa és bb ugyanannak a mennyiségnek a jelölt és a baseline értéke ugyanabban a mértékegységben, ϵa\epsilon_a az abszolút küszöb, ϵr\epsilon_r a relatív küszöb. Egész értékeknél (sorazonosítók, darabszámok, minor unitok) bitpontos egyezést várunk; lebegőpontos értékeknél ϵa=10−9\epsilon_a = 10^{-9} és ϵr=10−7\epsilon_r = 10^{-7} észszerű szigorú választás.

Platform constraint (platformkorlát). Egy verzió létezése nem jelenti, hogy az adott gépen fut. A lock feloldás a Python-verzió, az operációs rendszer és a CPU-architektúra kombinációjától függ: egy macOS arm64 wheel nem érvényes Linux x86-64-en, és egy bináris csomag feltételezi a megfelelő libc/BLAS-környezetet. A uv.lock platformcímkéken keresztül rögzíti, mely wheel mely környezethez tartozik; a --locked futtatás ezt ellenőrzi. A döntési lap ezért külön tételként rögzíti a támogatott platformot, és nem engedi, hogy a fejlesztői gép sikeréből a másik környezetre következtessünk.

Cost cap (költségplafon). Helyi bevezetésnél a „költség” karbantartási idő, managed notebook vagy felhőszolgáltatás esetén ellenőrzött díjszabásból számolt keret. A plafon négy elemet jelent: elszámolási metrika (pl. vCPU-óra, compute unit-óra, tárolt GB), riasztási küszöb (pl. a keret 80%-ánál), hard limit (a fölött a számítás leáll), és visszaállítási út (a régi helyi környezet visszaáll). Az aktuális díjat ez a könyv nem találja ki; a döntési lap a szolgáltató hivatalos árazó oldalát és az ellenőrzés dátumát kéri.

38.3.3 Teljes minimális munkapélda: pandas főverziós jelölt

A munkamenet a lab projektgyökérből indul, bash alatt. A szállított labban nincs uv.lock; a baseline lockot egyszer kell létrehozni, és azt verziózni kell.

cd lab
uv lock
uv sync --locked
uv run pytest -q

Az uv lock a pyproject.toml tartományaiból felold és uv.lock-ot ír. Az uv sync --locked megtagadja a futtatást, ha a lock és a projekt nem egyezik; a --frozen a további aktualitásvizsgálatot is kihagyja. A baseline teszt eredményét rögzítjük a döntési lapon.

Ezután a jelölt ágat külön worktree-ben nyitjuk, hogy a baseline érintetlen maradjon:

git worktree add ../lab-candidate pandas3-candidate
cd ../lab-candidate
# itt a pyproject.toml pandas-tartományát a 3.x sorozatra állítjuk
uv lock
uv sync --locked
uv run pytest -q

A füstteszt a projekt adatán egy, a pandas-verziótól független invariánst ellenőriz:

import pandas as pd

df = pd.read_csv("data/orders.csv", parse_dates=["order_ts"])
assert df["target_late"].isin({0, 1}).all()
by_segment = df.groupby("segment")["basket_minor"].sum()
assert by_segment.sum() == df["basket_minor"].sum()
print(df["basket_minor"].sum())

A csoportösszegek összege a teljes összeggel egyezik: ez a Spark DataFrame és végrehajtási terv alfejezetből visszahívott relációs invariant. A kimenet szám; az ellenőrzés az assert miatt már a kódon belül megtörtént.

A numerikus kontroll a starter_experiment.py kimenetét hasonlítja össze a jelölt és a baseline környezetben:

cd ../lab-candidate
uv run python starter_experiment.py
cp artifacts/split.json ../lab/artifacts/split_candidate.json
cd ../lab
uv run python starter_experiment.py
cp artifacts/split.json artifacts/split_baseline.json
uv run python compare_splits.py artifacts/split_baseline.json artifacts/split_candidate.json

A compare_splits.py gén specifikus, rekurzív JSON-összehasonlító: egészekre és sztringekre bitpontos egyezést, lebegőpontos értékekre a fenti toleranciaképletet alkalmazza.

import json
import math
import sys

ATOL = 1e-9
RTOL = 1e-7

def compare(a, b, path="$"):
    if type(a) is not type(b):
        return [f"{path}: típuseltérés {type(a).__name__} != {type(b).__name__}"]
    if isinstance(a, dict):
        out = []
        for key in sorted(set(a) | set(b)):
            if key not in a:
                out.append(f"{path}.{key}: csak a jobb oldalon")
            elif key not in b:
                out.append(f"{path}.{key}: csak a bal oldalon")
            else:
                out.extend(compare(a[key], b[key], f"{path}.{key}"))
        return out
    if isinstance(a, list):
        if len(a) != len(b):
            return [f"{path}: hosszeltérés {len(a)} != {len(b)}"]
        out = []
        for i, (x, y) in enumerate(zip(a, b)):
            out.extend(compare(x, y, f"{path}[{i}]"))
        return out
    if isinstance(a, float) and isinstance(b, float):
        if math.isclose(a, b, rel_tol=RTOL, abs_tol=ATOL):
            return []
        return [f"{path}: {a} != {b} tűrésen kívül"]
    return [] if a == b else [f"{path}: {a!r} != {b!r}"]

def main():
    _, left, right = sys.argv
    a = json.load(open(left, encoding="utf-8"))
    b = json.load(open(right, encoding="utf-8"))
    issues = compare(a, b)
    if issues:
        print("\n".join(issues))
        sys.exit(1)
    print("OK: a két split-kontroll a tűrésen belül egyezik.")

if __name__ == "__main__":
    main()

A listaág sorrendérzékeny: ha a két fájl listái eltérő sorrendben tartalmaznák ugyanazokat az azonosítókat, az hibának számít. Ez szándékos, mert a split fájlban a sorrend maga is része a szerződésnek.

A döntési lap sablonja:

# Tétel Ellenőrzött érték Forrás és dátum Állapot
1 Baseline verzió a jelenlegi lab pandas-verzió saját uv.lock és futtatási jegyzék verified
2 Célverzió a policy szerint 3.x jelölt; N-1 szabály szervezeti policy verziószámmal pending
3 Licenc SPDX-kifejezés a célverzión PyPI metadata + upstream LICENSE pending
4 Platform wheel Python-verzió, OS, architektúra uv lock és PyPI wheel-lista pending
5 Füstteszt import, csoportinvariáns zöld lokális munkamenet pending
6 Teljes tesztsor uv run pytest -q zöld lokális munkamenet pending
7 Numerikus kontroll split.json egyezés a tűrésen belül compare_splits.py kimenet pending
8 Költség/karbantartás n/a – helyi CPU-n – n/a
9 Visszaállítási terv régi lock + uv sync --locked --frozen dokumentálva a repóban verified

38.3.4 A lap értelmezése

A sablon nem kész döntés, hanem gyűjtőhely. A go döntéshez minden következő feltételnek teljesülnie kell: (1) az 1–4. tételeknél a forrás a tényleges ellenőrzés dátumával szerepel, és az ellenőrzés nem régebbi 90 napnál; (2) az 5–7. tétel lokálisan lefutott, a tolarencián belül; (3) a 9. tétel visszaállítási sora konkrét parancsot tartalmaz, nem „megoldjuk”; (4) a 8. tétel n/a mellett explicit módon kimondja, hogy nincs felhő- vagy szolgáltatásköltség. Az n/a állapot itt maga is állítás: a következő hónapban, managed notebookra átálláskor ezt a sort újra ki kell tölteni, nem örökölhető.

A frissítési teszt numerikus értelmezése: ha a split-azonosítók bitpontosan egyeznek, és a lebegőpontos mezők az abszolút-relatív tűrésen belül maradnak, akkor a főverzió-emelés a kezdőprojekt döntési felületét nem változtatta meg. Ez statisztikai értelemben nem „bizonyíték arra, hogy a 3.x mindenhol azonos”, hanem egy konkrét inputra, konkrét kontrollra vonatkozó megfigyelés. A compare_splits.py kimenete a tanuló gépén lehet OK: ...; automatikusan még nem ellenőrzött, ezért itt csak a lehetséges kimenetet írtuk le.

38.3.5 Feladatok

  1. Értelmezés. A következő verzióváltások közül melyik patch, minor, major emelés a MAJOR.MINOR.PATCH szabály szerint: (a) 2.2.3 → 2.2.4; (b) 2.2.3 → 2.3.0; (c) 2.2.3 → 3.0.0; (d) 3.0.0 → 3.1.0. A konzervatív policyben melyikhez elég csak füstteszt, melyikhez kérünk teljes numerikus kontrollt is?

  2. Megvalósítás. A csapat managed notebook szolgáltatást vezetne be három elemző számára: Linux konténer, 4 vCPU, havi kb. 40 használati óra, a bemenő adatok Parquet-fájlok helyi hálózaton. Készíts a fenti mintához illeszkedő, legalább nyolcsoros döntési lapot: minden sorhoz írj ellenőrizhető forrást, és ahol most nem lehetséges az ellenőrzés, pending állapotot a szükséges forrás megnevezésével. Aktuális díjat ne írj.

  3. Hibadiagnózis. A kolléga a pyproject.toml numpy>=1.26,<3 tartományával dolgozik, de nem készített uv.lock-ot, és mindig sima uv sync-et futtat. Egy hónappal később a teszt zöld, a numerikus kontroll azonban 1e-4 relatív eltérést mutat a baseline kontrolljegyzékhez képest. Mi a policy hiba, mi okozhatta az eltérést, és milyen paranccsal állítod helyre a baseline-környezetet?

  4. Megváltozott feltétel. Az előző managed notebook bevezetés költségplafonja: havi 100 elszámolási egység, riasztás 80 egységnél, hard limit 100 egységnél. Add meg: mit kell naponta mérni, mi történjen a hard limit elérésekor, és melyik helyi elem maradjon készenlétben visszaállítási útnak? Az egység árát ne tippeld; a metrika definícióját a szolgáltató fogalomtárából kell idézni.

  5. Továbbvitel. A managed notebook kliensparancsa MIT licencű, ezért a kolléga szerint a teljes szolgáltatás ingyenes. Nevezd meg a lap azon sorát, amely ezt a következtetést megcáfolja, és add meg, milyen dokumentumot kell dátummal ellenőrizni.

38.3.6 Megoldások és indoklás

1. (a) patch: PATCH nő, API- és funkcióváltozás nincs; füstteszt elegendő. (b) minor: új funkció, visszafelé kompatibilis; füstteszt mellett teljes tesztet és numerikus kontrollt kérünk, mert a belső számítási sorrend akkor is változhat, ha a publikus API nem tör. (c) major: törő változás lehetséges; a konzervatív policy teljes kompatibilitási auditot, az összes függő csomag ellenőrzését és rövid canary-időszakot ír elő. (d) minor, ugyanaz az osztály, mint (b). A numerikus kontroll mindig a saját kontrolljegyzékre vonatkozik, nem a csomag upstream tesztjeire.

2. Egy lehetséges lap (a 4. oszlop szerint minden sor pending, ahol helyi futtatás vagy friss forrás hiányzik):

# Tétel Ellenőrzött érték Forrás és dátum Állapot
1 Szolgáltatástípus managed notebook, Linux konténer szolgáltató dokumentációja pending
2 Verziópolicy a szolgáltató által kínált környezetverzió szolgáltató release notes pending
3 Klienslicenc MIT kliens, de a szolgáltatás külön vizsgálandó kliens repo LICENSE pending
4 Platform 4 vCPU, Linux, Parquet-hozzáférés szolgáltató instance-leírás pending
5 Füstteszt egy notebook betölt, olvas Parquet-ot lokális próbagép pending
6 Hozzáférés 3 felhasználó, legkisebb jogosultság szervezeti hozzáférési szabály pending
7 Adatkijárat Parquet be- és kimenet útvonala hálózati terv pending
8 Költségplafon metrika, riasztás, hard limit szolgáltató árazó és számlázó oldala pending
9 Visszaállítás helyi uv/Jupyter készenlétben dokumentált verified

A táblázat nem tartalmaz árat, mert a feladat ezt tiltja; a 8. sor forrása pontosan megnevezi, mit kell később dátumozva ellenőrizni.

3. A tartomány fagyasztás nélkül lebegő ablak: egy hónap alatt a numpy új patch/minor verziója kerülhetett a környezetbe, és a 1e-4-es relatív eltérés tipikusan lebegőpontos műveleti sorrend-változás vagy könyvtári algoritmusváltás következménye, nem feltétlenül hiba. A policy hiba az, hogy lock nélkül a „teszt zöld” nem ugyanaz a környezet, mint a kontrolljegyzék készítésekor. A javítás: uv lock, a lock verziózása, ezentúl minden futtatás uv sync --locked-dal. A baseline-hoz tartozó régi lock visszaállítása után uv sync --locked --frozen hozza létre a kontrolljegyzékkel konzisztens környezetet; az eltérő verziókat a döntési lap 2. sorában feljegyzik.

4. Metrika: a szolgáltató fogalomtárában definiált elszámolási egység fogyasztása naponta exportálva, a számlázási nézetből. A riasztási szabály: a kumulált havi fogyasztás 80 egység fölött figyelmeztetést küld a projekt tulajdonosának. Hard limit: 100 egységnél a futtató szkript leállítja az új számításokat; a meglévő adatokat a leállítási eljárás kimenti a szervezeti tárhelyre, mielőtt a szolgáltatás számítási hozzáférése felfüggesztődne. Visszaállítási út: a helyi uv/Jupyter környezet a Parquet-adatokkal a bevezetés előtti állapot szerint készenlétben marad, így a kísérleti munka nem áll le a szolgáltatás kikapcsolásával.

5. A lap 3. sora (klienslicenc) és 8. sora (költségplafon) együtt cáfolja a következtetést: az MIT licenc a klienskód felhasználására vonatkozik, nem a menedzselt infrastruktúra díjára. Ellenőrizendő a szolgáltató hivatalos árazó oldala, a szolgáltatási feltételek (Terms of Service) és az adatkijárat/tárhely díjszabása, mind dátumozva. A nyílt forrású kliens egy ellenőrzési tétel, nem az egész döntés.

38.3.7 Gyakori tévedés ellenpéldával

A leggyakoribb tévedés, hogy a nyílt forrású kliensből a teljes szolgáltatás ingyenességére következtetnek. Ellenpélda: a helyi DuckDB Python-csomag MIT licencű, és önmagában tényleg ingyenesen használható; ha azonban a csapat a DuckDB-t menedzselt szolgáltatásként vagy felhőbeli tárhely felett futtatja, a számítási erőforrás, a tárhely és a hálózati kijárat költsége külön szerződés tárgya. Ugyanez igaz a managed notebook kategóriákra: a nyílt forrású kliens vagy a Jupyter-projekt licence semmilyen állítást nem tesz a mögötte álló infrastruktúra áráról. A döntési lap 8. sora ezért mindig külön tétel, és a n/a állapot is dátumozott döntés, nem örökölt alapbeállítás. Ez a tétel a CPU, GPU és elosztott rendszer közötti döntés alfejezetből ismert költség- és erőforrás-korlát gyakorlati megvalósítása; a numerikus kontroll a Jelentés és nyelvek közötti adatcsere toleranciaszerződését, a hibatűrés pedig a Hibaszeletek és felelős értékelés alfejezetben tanult, számokkal alátámasztott értékelési szemléletet viszi tovább.

Önellenőrzés: minden döntési lapsorban van-e dátumozott forrás? A lock és a projekt együtt van-e verziózva? A frissítési teszt a saját kontrolljegyzékre futott-e, nem az upstream dokumentációjára? A visszaállítási terv konkrét parancsot tartalmaz-e? Ha bármelyik válasz nem, a döntési lap még pending, nem go.

38.3.8 Rövid összefoglaló

Az eszközbevezetés nem egyetlen parancs, hanem öt összetevős döntési folyamat. A verziókezelési szabály rögzíti, milyen emeléshez milyen teszt jár; a licencellenőrzés mindig az adott célverzión fut, dátumozott forrással; a frissítési teszt füsttesztből, teljes tesztsorból és numerikus kontrollból áll, előre rögzített abszolút-relatív toleranciával; a platformkorlát a wheel és a natív környezet együttesét vizsgálja; a költségplafon metrikát, riasztást, hard limitet és visszaállítási utat követel. A döntési lap minden sora ellenőrzött érték–forrás–dátum–állapot négyest hordoz, és a nyílt forrású kliens ingyenessége semmikor nem bizonyítja a mögöttes szolgáltatás költségmentességét. Következő alkalmazásként a Saját data science munkaasztal specifikációja saját data science munkaasztal specifikációját állítja össze ezekből a kritériumokból.