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>
589 lines
23 KiB
Python
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:]))
|