dynamisk-rettsstat/modell/kjor.py
Kjell Tore Guttormsen 464ad38b49
feat: model core, launcher, reproduction check and the first two skills
The core in modell/kjerne/ is an unmodified copy of sim.py, diagnose.py and
forlop.py from den-dynamiske-rettsstaten @2badc25, pinned by sha256.
kjor.py is the single entry point; kontroll.py reproduces the note's 22 data
files and printed results. Skills: diagnose and forlop (Norwegian).

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 13:53:32 +01:00

589 lines
23 KiB
Python

# Copyright (c) 2026 Kjell Tore Guttormsen. MIT License, see LICENSE.
"""Én inngang til modellen bak notatet «Den dynamiske rettsstaten».
Bruk:
python3 kjor.py [--data <mappe>] [--modell <mappe>] <kommando> [...]
Bare standardbiblioteket, Python 3.9 eller nyere. Kommandoene står i HJELP under.
Exit-koder: 0 ok, 1 forskjeller funnet av kontroll, 2 feil i inndata eller bruk,
3 miljøet mangler (numpy eller scipy).
Valg av Python for kommandoene som trenger numpy og scipy: (1) denne tolken hvis den har
numpy 2.0 eller nyere (numpy.trapezoid) og scipy; (2) ellers Python i den egne mappa
<data>/venv, som kjører samme kommando på nytt; (3) ellers exit-kode 3 med en melding om
hva som mangler. Ingenting installeres uten «miljo --opprett».
Grensesnitt for modulene i modell-mappa (scenario.py, kontroll.py, arbeidskopi.py):
hver har ``main(argv: list[str]) -> int``, der argv er argumentene etter kommandonavnet.
Før main kalles, ligger modell-mappa og modell-mappa/kjerne først i sys.path, slik at
``import sim`` gir kjernen i samme modell-mappe. ``sys.exit("melding")`` gir exit-kode 2
og meldingen uendret; ``sys.exit(n)`` gir exit-kode n.
"""
from __future__ import annotations
import importlib
import importlib.util
import json
import os
import platform
import shlex
import subprocess
import sys
import traceback
from pathlib import Path
from typing import TYPE_CHECKING, NamedTuple, TypedDict
if TYPE_CHECKING:
from collections.abc import Callable
from types import ModuleType
HER = Path(__file__).resolve().parent
VENV_MERKE = "DYNAMISK_RETTSSTAT_VENV" # satt når kjor.py er startet på nytt med Python fra den egne mappa
KJERNEMODULER = ("sim", "diagnose", "forlop")
MODELLMODULER = ("scenario", "kontroll", "arbeidskopi")
OK, FORSKJELLER, FEIL_BRUK, MILJO_MANGLER = 0, 1, 2, 3
HJELP = """\
Bruk: python3 kjor.py [--data <mappe>] [--modell <mappe>] <kommando> [...]
Kommandoer:
diagnose <fil> finn leddet som binder, fra egne tall (trenger bare Python)
forlop <fil> ["ledd"] stabilitetssjekk og forløp måned for måned (numpy og scipy)
scenario <fil> [--ut <mappe>] kjør en scenariofil (numpy og scipy)
notatet [--ut <mappe>] kjør notatets simuleringer uendret, om lag 1-2 min (numpy og scipy)
kontroll [--rask] kontroller at modellen regner som notatet
miljo [--opprett] vis Python, numpy og scipy; --opprett installerer dem i en egen mappe
arbeidskopi <mappe> lag en kopi av modellen som du kan endre
Valg før kommandoen:
--data <mappe> mappa der den egne Python-mappa (venv) med numpy og scipy ligger
(standard ~/.dynamisk-rettsstat)
--modell <mappe> bruk modellen i en arbeidskopi i stedet for pluginens
Exit-koder: 0 ok, 1 kontroll fant forskjeller, 2 feil i inndata eller bruk,
3 numpy eller scipy mangler.
"""
TREGER_REGNEMILJO = ("forlop", "scenario", "notatet")
KOMMANDOER = ("diagnose", "forlop", "scenario", "notatet", "kontroll", "miljo", "arbeidskopi")
class Bruksfeil(Exception):
"""Feil i kommandolinjen. Meldingen er til brukeren og gir exit-kode 2."""
class Valg(NamedTuple):
"""Kommandolinjen, tolket."""
argv: list[str] # hele kommandolinjen, slik den kom inn
data: Path # mappa der den egne venv ligger
data_oppgitt: bool # om --data ble oppgitt med en brukbar verdi
modell: Path # modell-mappa som brukes (pluginens eller en arbeidskopi)
kommando: str
args: list[str] # argumentene etter kommandoen
class Beskrivelse(TypedDict):
"""Hva en Python-tolk har av numpy og scipy."""
python: str
sti: str
numpy: str | None
trapezoid: bool
scipy: str | None
class Tolk(NamedTuple):
"""Hvilken Python som skal regne: «denne», «venv» eller «mangler»."""
hvor: str
python: Path | None
mangler: list[str]
# ---------------- kommandolinjen ----------------
def standard_datamappe() -> Path:
"""Mappa som brukes når --data ikke er oppgitt."""
return Path.home() / ".dynamisk-rettsstat"
def datamappe(verdi: str | None) -> tuple[Path, bool]:
"""Tom verdi, eller en verdi med «${» (ikke satt inn av Claude), betyr «ikke oppgitt»."""
if verdi is None or not verdi.strip() or "${" in verdi:
return standard_datamappe(), False
return Path(verdi).expanduser().resolve(), True
def modellmappe(verdi: str | None) -> Path:
"""Modell-mappa: pluginens egen, eller en arbeidskopi (mappa selv eller mappa over modell/)."""
if verdi is None:
return HER
if not verdi.strip():
melding = "--modell må ha en mappe etter seg."
raise Bruksfeil(melding)
sti = Path(verdi).expanduser().resolve()
for kandidat in (sti, sti / "modell"):
if (kandidat / "kjerne").is_dir():
return kandidat
melding = f"Fant ingen modell i {sti}: mappa må ha undermappa kjerne/ eller modell/kjerne/."
raise Bruksfeil(melding)
def tolk_argumenter(argv: list[str]) -> Valg:
"""Tolk de globale valgene og finn kommandoen."""
verdier: dict[str, str | None] = {"--data": None, "--modell": None}
i = 0
while i < len(argv) and argv[i].startswith("-"):
arg = argv[i]
if arg in {"-h", "--hjelp", "--help"}:
return Valg(argv=argv, data=standard_datamappe(), data_oppgitt=False, modell=HER, kommando="hjelp",
args=[])
navn, likhetstegn, verdi = arg.partition("=")
if navn not in verdier:
melding = f"Ukjent valg {arg}. Gyldige valg før kommandoen: --data <mappe>, --modell <mappe>."
raise Bruksfeil(melding)
if likhetstegn:
i += 1
elif i + 1 < len(argv):
verdi = argv[i + 1]
i += 2
else:
melding = f"{navn} må ha en mappe etter seg."
raise Bruksfeil(melding)
verdier[navn] = verdi
if i >= len(argv):
melding = "Mangler kommando."
raise Bruksfeil(melding)
kommando, args = argv[i], argv[i + 1:]
if kommando not in KOMMANDOER and kommando != "_beskriv":
melding = f"Ukjent kommando «{kommando}». Gyldige kommandoer: {', '.join(KOMMANDOER)}."
raise Bruksfeil(melding)
data, oppgitt = datamappe(verdier["--data"])
return Valg(argv, data, oppgitt, modellmappe(verdier["--modell"]), kommando, args)
# ---------------- Python-miljøet ----------------
def beskriv_denne() -> Beskrivelse:
"""Python, numpy og scipy i tolken som kjører nå."""
ut = Beskrivelse(python=platform.python_version(), sti=sys.executable, numpy=None, trapezoid=False, scipy=None)
try:
numpy = importlib.import_module("numpy")
except ImportError:
pass
else:
ut["numpy"] = str(getattr(numpy, "__version__", "ukjent versjon"))
ut["trapezoid"] = hasattr(numpy, "trapezoid")
try:
importlib.import_module("scipy.optimize")
scipy = importlib.import_module("scipy")
except ImportError:
pass
else:
ut["scipy"] = str(getattr(scipy, "__version__", "ukjent versjon"))
return ut
def mangler(beskrivelse: Beskrivelse) -> list[str]:
"""Det som mangler for å regne med numpy og scipy, i ord."""
ut = []
if beskrivelse["numpy"] is None:
ut.append("numpy")
elif not beskrivelse["trapezoid"]:
ut.append(f"numpy 2.0 eller nyere (har {beskrivelse['numpy']})")
if beskrivelse["scipy"] is None:
ut.append("scipy")
return ut
def beskriv_tolk(python: Path) -> Beskrivelse | None:
"""Beskriv en annen Python-tolk ved å la den kjøre denne fila. None hvis det ikke går."""
try:
resultat = subprocess.run(
[str(python), str(Path(__file__).resolve()), "_beskriv"],
capture_output=True, text=True, encoding="utf-8", check=False,
)
except OSError:
return None
if resultat.returncode != 0:
return None
try:
data = json.loads(resultat.stdout.strip().splitlines()[-1])
except (ValueError, IndexError):
return None
return Beskrivelse(
python=str(data["python"]), sti=str(data["sti"]), numpy=data["numpy"],
trapezoid=bool(data["trapezoid"]), scipy=data["scipy"],
)
def venv_mappe(data: Path) -> Path:
"""Den egne Python-mappa med numpy og scipy."""
return data / "venv"
def venv_python(data: Path) -> Path:
"""Python-tolken i den egne mappa."""
if os.name == "nt":
return venv_mappe(data) / "Scripts" / "python.exe"
return venv_mappe(data) / "bin" / "python"
def velg_tolk(data: Path) -> Tolk:
"""Velg Python for kommandoene som trenger numpy og scipy."""
mangel = mangler(beskriv_denne())
if not mangel:
return Tolk("denne", Path(sys.executable), [])
kandidat = venv_python(data)
if kandidat.exists() and os.environ.get(VENV_MERKE) != "1":
return Tolk("venv", kandidat, mangel)
return Tolk("mangler", None, mangel)
def kjor_paa_nytt(python: Path, argv: list[str]) -> int:
"""Kjør samme kommando med en annen Python og gi videre exit-koden."""
sys.stdout.flush()
miljo = dict(os.environ)
miljo[VENV_MERKE] = "1"
return subprocess.run([str(python), str(Path(__file__).resolve()), *argv], env=miljo, check=False).returncode
def kommando_tekst(valg: Valg, *args: str) -> str:
"""Kommandolinjen brukeren kan skrive for å kjøre kjor.py med samme --data."""
deler = ["python3", str(Path(__file__).resolve())]
if valg.data_oppgitt:
deler += ["--data", str(valg.data)]
return " ".join(shlex.quote(d) for d in [*deler, *args])
def melding_mangler(valg: Valg, tolk: Tolk) -> str:
"""Norsk melding for exit-kode 3."""
linjer = [
f"«{valg.kommando}» trenger numpy (2.0 eller nyere) og scipy. Mangler: {', '.join(tolk.mangler)}.",
f"Python som ble brukt: {sys.executable} ({platform.python_version()}).",
]
if os.environ.get(VENV_MERKE) == "1":
linjer.append(f"Den egne mappa {venv_mappe(valg.data)} mangler dem også. Kommandoen under installerer "
"dem der på nytt fra PyPI (krever internett):")
else:
linjer.append(f"Fant ingen egen mappe med numpy og scipy i {venv_mappe(valg.data)}. Kommandoen under "
"lager den og installerer numpy og scipy fra PyPI der (krever internett):")
linjer.append(f" {kommando_tekst(valg, 'miljo', '--opprett')}")
linjer.append("Ingenting installeres uten at du ber om det. diagnose virker uten numpy og scipy.")
return "\n".join(linjer)
# ---------------- å kjøre kjernen og modulene ----------------
def forbered_stier(modell: Path) -> None:
"""Legg modell-mappa og kjernen først i sys.path, og glem moduler lastet fra en annen mappe."""
sys.dont_write_bytecode = True # ingen __pycache__ i plugin-mappa
for navn in (*KJERNEMODULER, *MODELLMODULER):
sys.modules.pop(navn, None)
fjern = {str(modell), str(modell / "kjerne")}
if modell != HER:
fjern.add(str(HER))
sys.path[:] = [sti for sti in sys.path if str(Path(sti or ".").resolve()) not in fjern]
sys.path[:0] = [str(modell / "kjerne"), str(modell)]
def exit_kode(kode: object) -> int:
"""Exit-koden for et sys.exit(kode) fra kjernen eller en modul."""
if kode is None:
return OK
if isinstance(kode, int):
return kode
sys.stdout.flush()
print(kode, file=sys.stderr)
return FEIL_BRUK
def fang(funksjon: Callable[[], object], hva: str) -> int:
"""Kjør funksjonen og gjør sys.exit og uventede feil om til exit-koder."""
try:
resultat = funksjon()
except SystemExit as stopp:
return exit_kode(stopp.code)
except Exception: # noqa: BLE001 - alt som ikke er SystemExit, er en feil i koden, ikke i inndata
sys.stdout.flush()
traceback.print_exc()
print(f"Uventet feil i {hva} (se over).", file=sys.stderr)
return FEIL_BRUK
if isinstance(resultat, int) and not isinstance(resultat, bool):
return resultat
return OK
def last_kjerne(modell: Path, navn: str) -> ModuleType:
"""Last en kjernemodul fra modell-mappa/kjerne."""
forbered_stier(modell)
return importlib.import_module(navn)
def last_modul(modell: Path, navn: str) -> ModuleType | None:
"""Last modell-mappa/<navn>.py. None hvis fila ikke finnes."""
fil = modell / f"{navn}.py"
if not fil.is_file():
return None
forbered_stier(modell)
spec = importlib.util.spec_from_file_location(navn, fil)
if spec is None or spec.loader is None:
return None
modul = importlib.util.module_from_spec(spec)
sys.modules[navn] = modul
spec.loader.exec_module(modul)
return modul
def kjor_modul(valg: Valg, navn: str) -> int:
"""Kall main(argv) i modell-mappa/<navn>.py med argumentene etter kommandoen."""
fil = valg.modell / f"{navn}.py"
if not fil.is_file():
print(f"Kommandoen «{valg.kommando}» er ikke på plass i denne modellen ennå: fant ikke {fil}.",
file=sys.stderr)
return FEIL_BRUK
def start() -> object:
main = getattr(last_modul(valg.modell, navn), "main", None)
if not callable(main):
sys.exit(f"{fil} mangler funksjonen main(argv).")
return main(list(valg.args))
return fang(start, f"{navn}.py")
# ---------------- kommandoene ----------------
def kommando_diagnose(valg: Valg) -> int:
"""Kjernens diagnose.main."""
if len(valg.args) != 1:
melding = "Bruk: kjor.py diagnose <inndata.json>"
raise Bruksfeil(melding)
diagnose = last_kjerne(valg.modell, "diagnose")
return fang(lambda: diagnose.main(valg.args[0]), "diagnose.py")
def kommando_forlop(valg: Valg) -> int:
"""Kjernens forlop.main."""
if len(valg.args) not in {1, 2}:
melding = 'Bruk: kjor.py forlop <inndata.json> ["navn på ledd"]'
raise Bruksfeil(melding)
forlop = last_kjerne(valg.modell, "forlop")
return fang(lambda: forlop.main(*valg.args), "forlop.py")
def kommando_notatet(valg: Valg) -> int:
"""Kjør kjernens sim.py uendret, med ut-mappa som arbeidsmappe (sim.py skriver data/ der)."""
ut = Path.cwd() / "resultater" / "notatet"
args = list(valg.args)
if args:
if len(args) != 2 or args[0] != "--ut": # noqa: PLR2004 - «--ut <mappe>»
melding = "Bruk: kjor.py notatet [--ut <mappe>]"
raise Bruksfeil(melding)
ut = Path(args[1]).expanduser()
ut = ut.resolve()
if ut == valg.modell or valg.modell in ut.parents:
melding = f"Ut-mappa {ut} ligger inne i modell-mappa {valg.modell}. Velg en annen med --ut <mappe>."
raise Bruksfeil(melding)
if ut.exists() and not ut.is_dir():
melding = f"{ut} finnes og er ikke en mappe. Velg en annen med --ut <mappe>."
raise Bruksfeil(melding)
ut.mkdir(parents=True, exist_ok=True)
print(f"Kjører notatets simuleringer (kjerne/sim.py, uendret) i {ut}. Det tar om lag 1-2 minutter.\n",
flush=True)
miljo = dict(os.environ, PYTHONIOENCODING="utf-8", PYTHONDONTWRITEBYTECODE="1", PYTHONUNBUFFERED="1")
utskrift = ut / "sim_utskrift.txt"
with utskrift.open("w", encoding="utf-8") as logg, subprocess.Popen(
[sys.executable, str(valg.modell / "kjerne" / "sim.py")],
cwd=str(ut), stdout=subprocess.PIPE, text=True, encoding="utf-8", env=miljo,
) as prosess:
if prosess.stdout is not None:
for linje in prosess.stdout:
sys.stdout.write(linje)
logg.write(linje)
if prosess.returncode != 0:
print(f"sim.py stoppet med exit-kode {prosess.returncode}.", file=sys.stderr)
return FEIL_BRUK
antall = len(list((ut / "data").glob("*.dat")))
print(f"\nFerdig: {antall} datafiler i {ut / 'data'}, utskriften i {utskrift}.")
print("«kontroll» sammenligner pluginens egen kjøring med notatet.")
return OK
def laaste_versjoner(modell: Path) -> dict[str, str]:
"""Notatets låste versjoner fra modell/requirements.txt."""
ut = {}
fil = modell / "requirements.txt"
if fil.is_file():
for linje in fil.read_text(encoding="utf-8").splitlines():
navn, skille, versjon = linje.split("#")[0].strip().partition("==")
if skille:
ut[navn.strip().lower()] = versjon.strip()
return ut
def vis_tolk(beskrivelse: Beskrivelse, laast: dict[str, str]) -> list[str]:
"""Linjer som beskriver Python, numpy og scipy i en tolk."""
linjer = [f" Python {beskrivelse['python']} ({beskrivelse['sti']})"]
for navn in ("numpy", "scipy"):
versjon = beskrivelse["numpy"] if navn == "numpy" else beskrivelse["scipy"]
notatet = f" (notatet: {laast[navn]})" if navn in laast else ""
linjer.append(f" {navn}: {versjon or 'mangler'}{notatet}")
return linjer
def samme_som_notatet(beskrivelse: Beskrivelse, laast: dict[str, str]) -> bool:
"""Sjekk om tolken har notatets låste versjoner av numpy og scipy."""
return beskrivelse["numpy"] == laast.get("numpy") and beskrivelse["scipy"] == laast.get("scipy")
def rapport_miljo(valg: Valg) -> None:
"""Skriv ut hva denne Python og den egne mappa har."""
laast = laaste_versjoner(valg.modell)
denne = beskriv_denne()
print("Python som kjører nå:")
print("\n".join(vis_tolk(denne, laast)))
vpy = venv_python(valg.data)
print(f"\nEgen mappe for numpy og scipy: {venv_mappe(valg.data)}")
venv: Beskrivelse | None = None
if not vpy.exists():
print(" finnes ikke. «miljo --opprett» lager den og installerer numpy og scipy fra PyPI "
"(krever internett).")
else:
venv = beskriv_tolk(vpy)
if venv is None:
print(f" finnes, men {vpy} lot seg ikke starte. «miljo --opprett» prøver å lage den på nytt.")
else:
print("\n".join(vis_tolk(venv, laast)))
print()
regner = None
if not mangler(denne):
regner = denne
print("forlop, scenario, notatet og kontroll regner med Python som kjører nå.")
elif venv is not None and not mangler(venv):
regner = venv
print("forlop, scenario, notatet og kontroll regner med Python i den egne mappa.")
else:
hva = ", ".join(mangler(venv if venv is not None else denne))
print(f"forlop, scenario, notatet og kontroll kan ikke regne nå: {hva} mangler.")
if regner is not None:
if samme_som_notatet(regner, laast):
print("Versjonene av numpy og scipy er de samme som notatets, så kontroll kan gi bit for bit.")
else:
print("Versjonene av numpy og scipy er ikke de samme som notatets: kontroll sammenligner da "
"med toleranse, ikke bit for bit.")
def kjor_prosess(kommando: list[str]) -> int:
"""Kjør et program (pip, venv) med utskriften rett til skjermen. Exit-koden tilbake."""
sys.stdout.flush()
try:
return subprocess.run(kommando, check=False).returncode
except OSError as feil:
print(f"Kunne ikke starte {kommando[0]}: {feil}", file=sys.stderr)
return 1
def opprett_miljo(valg: Valg) -> int:
"""Lag den egne mappa og installer numpy og scipy: låste versjoner først, ellers nyeste."""
mappe = venv_mappe(valg.data)
vpy = venv_python(valg.data)
if not vpy.exists():
print(f"Lager egen Python-mappa {mappe} med {sys.executable} ({platform.python_version()}).")
valg.data.mkdir(parents=True, exist_ok=True)
if kjor_prosess([sys.executable, "-m", "venv", str(mappe)]) != 0 or not vpy.exists():
print(f"Kunne ikke lage {mappe}. På noen Linux-systemer må pakken python3-venv installeres først.",
file=sys.stderr)
return MILJO_MANGLER
pip = [str(vpy), "-m", "pip", "install", "--disable-pip-version-check"]
krav = valg.modell / "requirements.txt"
laast = laaste_versjoner(valg.modell)
print(f"\nInstallerer notatets versjoner ({', '.join(f'{n} {v}' for n, v in laast.items())}) fra PyPI.")
toleranse = False
if kjor_prosess([*pip, "-r", str(krav)]) != 0:
print("\nNotatets låste versjoner lot seg ikke installere for denne Python-versjonen. "
"Prøver numpy 2.0 eller nyere og scipy.")
toleranse = True
if kjor_prosess([*pip, "numpy>=2.0", "scipy"]) != 0:
print(f"\nInstallasjonen feilet. Se meldingene over. Mappa {mappe} kan slettes.", file=sys.stderr)
return MILJO_MANGLER
print()
rapport_miljo(valg)
if toleranse:
print("\nNumpy og scipy er ikke notatets låste versjoner. Resultatene kontrolleres da med toleranse, "
"ikke bit for bit.")
return OK
def kommando_miljo(valg: Valg) -> int:
"""Vis miljøet, eller lag den egne mappa med --opprett."""
if valg.args == ["--opprett"]:
return opprett_miljo(valg)
if valg.args:
melding = "Bruk: kjor.py miljo [--opprett]"
raise Bruksfeil(melding)
rapport_miljo(valg)
return OK
def kommando_beskriv(_valg: Valg) -> int:
"""Intern kommando for beskriv_tolk(): skriv beskrivelsen av denne tolken som JSON."""
print(json.dumps(beskriv_denne()))
return OK
def kommando_hjelp(_valg: Valg) -> int:
"""Skriv bruken."""
print(HJELP, end="")
return OK
def kommando_regner(valg: Valg) -> int:
"""Kommandoene som trenger numpy og scipy: velg Python først (se toppen av fila)."""
if valg.kommando == "scenario" and not (valg.modell / "scenario.py").is_file():
return kjor_modul(valg, "scenario") # sier at kommandoen ikke er på plass ennå
if valg.kommando == "forlop" and valg.args and not Path(valg.args[0]).exists():
print(f"Fant ikke fila {valg.args[0]}.", file=sys.stderr)
return FEIL_BRUK
tolk = velg_tolk(valg.data)
if tolk.hvor == "venv" and tolk.python is not None:
return kjor_paa_nytt(tolk.python, valg.argv)
if tolk.hvor == "mangler" and valg.kommando != "kontroll": # kontroll gjør det den kan uten
print(melding_mangler(valg, tolk), file=sys.stderr)
return MILJO_MANGLER
handlinger: dict[str, Callable[[Valg], int]] = {
"forlop": kommando_forlop, "notatet": kommando_notatet,
"scenario": lambda v: kjor_modul(v, "scenario"), "kontroll": lambda v: kjor_modul(v, "kontroll"),
}
return handlinger[valg.kommando](valg)
def utfor(valg: Valg) -> int:
"""Utfør kommandoen."""
handlinger: dict[str, Callable[[Valg], int]] = {
"hjelp": kommando_hjelp, "_beskriv": kommando_beskriv,
"diagnose": kommando_diagnose, "miljo": kommando_miljo,
"arbeidskopi": lambda v: kjor_modul(v, "arbeidskopi"),
}
return handlinger.get(valg.kommando, kommando_regner)(valg)
def main(argv: list[str]) -> int:
"""Inngangen. Returnerer exit-koden."""
try:
if not argv:
print(HJELP, end="", file=sys.stderr)
return FEIL_BRUK
return utfor(tolk_argumenter(argv))
except Bruksfeil as feil:
sys.stdout.flush()
print(f"{feil}\nSkriv «python3 kjor.py --hjelp» for bruk.", file=sys.stderr)
return FEIL_BRUK
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))