# Copyright (c) 2026 Kjell Tore Guttormsen. MIT License, see LICENSE. """Én inngang til modellen bak notatet «Den dynamiske rettsstaten». Bruk: python3 kjor.py [--data ] [--modell ] [...] 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 /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. Før diagnose og forlop kalles, avvises ukjente feltnavn i hvert ledd og i "styring" (exit-kode 2, med «Mente du ...?»), fordi kjernen ser bort fra felt den ikke kjenner, og en skrivefeil ellers gir en stille standardverdi. Gyldige felt i et ledd: "navn", kjernens diagnose.TALLFELT (lest fra kjernen i modell-mappa når kommandoen kjøres) og merknadsfeltene i ANNOTASJONSFELT. Gyldige felt i "styring": forlop.STYRINGSFELT hvis kjernen har den, ellers STYRINGSFELT her, pluss merknadsfeltene. Andre felt på øverste nivå (som "merknad" og "kilder") er tillatt. """ from __future__ import annotations import difflib 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 ] [--modell ] [...] Kommandoer: diagnose finn leddet som binder, fra egne tall (trenger bare Python) forlop ["ledd"] stabilitetssjekk og forløp måned for måned (numpy og scipy) scenario [--ut ] kjør en scenariofil (numpy og scipy) notatet [--ut ] 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 lag en kopi av modellen som du kan endre Valg før kommandoen: --data mappa der den egne Python-mappa (venv) med numpy og scipy ligger (standard ~/.dynamisk-rettsstat) --modell 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") ANNOTASJONSFELT = ("merknad", "kilde", "kilder") # fri tekst i et ledd eller i styring; leses ikke av regningen STYRINGSFELT = ("forsinkelse", "reaktivitet") # det forlop.les_styring leser 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 , --modell ." 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 python_navn(system: str = os.name) -> str: """Navnet brukeren skriver for Python: «python» på Windows, ellers «python3».""" return "python" if system == "nt" else "python3" def kommando_tekst(valg: Valg, *args: str, system: str = os.name) -> str: """Kommandolinjen brukeren kan skrive for å kjøre kjor.py med samme --data. På Windows (system "nt") med «python» og Windows-sitering, ellers med «python3» og POSIX-sitering. """ deler = [python_navn(system), str(Path(__file__).resolve())] if valg.data_oppgitt: deler += ["--data", str(valg.data)] deler += list(args) if system == "nt": return subprocess.list2cmdline(deler) return " ".join(shlex.quote(d) for d in deler) 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/.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/.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 feltmelding(hvor: str, felt: str, gyldige: tuple[str, ...]) -> str: """Melding om et ukjent feltnavn, med forslag når det ligner et gyldig.""" treff = difflib.get_close_matches(felt, gyldige, n=1, cutoff=0.6) forslag = f' Mente du "{treff[0]}"?' if treff else "" return (f'Feil i inndata, {hvor}: feltet "{felt}" finnes ikke.{forslag} ' f"Gyldige felt: {', '.join(gyldige)}.") def ukjent_felt(fil: str, diagnose: object, forlop: object = None) -> str | None: """Melding om det første ukjente feltet i et ledd eller i styring, ellers None. Fila leses bare for å finne feltnavn. Kan den ikke leses, eller har den feil form, sier kjernen fra om det selv, med sin egen melding. """ try: data = json.loads(Path(fil).read_text(encoding="utf-8")) except (OSError, ValueError): return None if not isinstance(data, dict): return None tallfelt = tuple(str(f) for f in getattr(diagnose, "TALLFELT", ())) gyldige_ledd = ("navn", *tallfelt, *ANNOTASJONSFELT) alle_ledd = data.get("ledd") for nr, ledd in enumerate(alle_ledd if isinstance(alle_ledd, list) else [], 1): if not isinstance(ledd, dict): continue for felt in ledd: if felt not in gyldige_ledd: return feltmelding(f"ledd {nr} ({ledd.get('navn', 'uten navn')})", felt, gyldige_ledd) styring = data.get("styring") if isinstance(styring, dict): gyldige_styring = (*(str(f) for f in getattr(forlop, "STYRINGSFELT", STYRINGSFELT)), *ANNOTASJONSFELT) for felt in styring: if felt not in gyldige_styring: return feltmelding("styring", felt, gyldige_styring) return None def avvis_ukjente_felt(fil: str, diagnose: object, forlop: object = None) -> int | None: """Skriv meldingen og gi exit-kode 2 når fila har et ukjent felt; None når den er i orden.""" melding = ukjent_felt(fil, diagnose, forlop) if melding is None: return None sys.stdout.flush() print(melding, file=sys.stderr) return FEIL_BRUK def kommando_diagnose(valg: Valg) -> int: """Kjernens diagnose.main.""" if len(valg.args) != 1: melding = "Bruk: kjor.py diagnose " raise Bruksfeil(melding) diagnose = last_kjerne(valg.modell, "diagnose") feil = avvis_ukjente_felt(valg.args[0], diagnose) if feil is not None: return feil 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 ["navn på ledd"]' raise Bruksfeil(melding) forlop = last_kjerne(valg.modell, "forlop") feil = avvis_ukjente_felt(valg.args[0], getattr(forlop, "diagnose", None), forlop) if feil is not None: return feil return fang(lambda: forlop.main(*valg.args), "forlop.py") def kan_ikke_skrive(mappe: Path, feil: OSError) -> int: """Norsk melding og exit-kode 2 når ut-mappa ikke kan lages eller skrives i.""" sys.stdout.flush() print(f"Kan ikke skrive i {mappe}: {feil.strerror or feil}. Velg en annen mappe med --ut.", file=sys.stderr) return FEIL_BRUK 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 » melding = "Bruk: kjor.py notatet [--ut ]" 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 ." 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 ." raise Bruksfeil(melding) utskrift = ut / "sim_utskrift.txt" try: (ut / "data").mkdir(parents=True, exist_ok=True) prove = ut / "data" / ".skriveprove" prove.write_text("", encoding="utf-8") prove.unlink() logg = utskrift.open("w", encoding="utf-8") except OSError as feil: return kan_ikke_skrive(ut, feil) 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") with 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: try: for linje in prosess.stdout or (): sys.stdout.write(linje) logg.write(linje) except OSError as feil: prosess.kill() return kan_ikke_skrive(ut, feil) if prosess.returncode != 0: sys.stdout.flush() print(f"Kjøringen av sim.py stoppet underveis (exit-kode {prosess.returncode}); se meldingene over. " f"Datafilene i {ut / 'data'} er ikke fullstendige.", 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 «{python_navn()} kjor.py --hjelp» for bruk.", file=sys.stderr) return FEIL_BRUK except OSError as feil: # exit-kode 1 er forbeholdt kontroll som finner forskjeller sys.stdout.flush() print(f"Kan ikke lese eller skrive {feil.filename or ''}: {feil.strerror or feil}.", file=sys.stderr) return FEIL_BRUK if __name__ == "__main__": sys.exit(main(sys.argv[1:]))