# Skript-Referenz

Diese Seite dokumentiert die öffentliche Headless-Scripting-API, die vom Python-Paket `studio` bereitgestellt wird.

Der Fokus liegt auf diesen öffentlichen Symbolen:

* `StudioScenario`
* `install_logging_event`
* `enable_logging_stdout`
* `disable_logging_stdout`

Wenn Sie noch nie ein Szenario headless ausgeführt haben, lesen Sie zuerst den Abschnitt "Headless" und kehren Sie dann hierher zurück, um die API-Details zu prüfen.

***

## Imports

Alle hier dokumentierten APIs werden vom Top-Level-Paket `studio` exportiert:

```python
from studio import (
	StudioScenario,
	install_logging_event,
	enable_logging_stdout,
	disable_logging_stdout,
)
```

***

## Typ-Stubs

Die öffentliche Headless-Oberfläche sieht folgendermaßen aus:

```python
from __future__ import annotations

from typing import Any, Callable, Optional


def enable_logging_stdout() -> None: ...
def disable_logging_stdout() -> None: ...
def install_logging_event(event: Callable[[str], None]) -> None: ...


class StudioScenario:
	def __init__(self, *, verification_code: str = "") -> None: ...

	def load_scenario(self, path: str) -> Optional[StudioScenario]: ...
	def load_scenario_raw(self, content: str) -> bool: ...
	def save_scenario(self, path: str) -> bool: ...
	def clear_scenario(self) -> bool: ...
	def disable_load_errors(self) -> StudioScenario: ...
	def load_custom_nodes(self, path: str) -> bool: ...

	def get_ports(
		self,
		input_op_title: str = "Subsystem In",
		output_op_title: str = "Subsystem Out",
	) -> dict[str, list[str]]: ...
	def get_name(self) -> str: ...

	def run(self, args: tuple[Any, ...] = ()) -> tuple[list[Any], ...]: ...
	def run_server(
		self,
		inputs: tuple[Any, ...] = (),
		host: str = "127.0.0.1",
		port: int = 8080,
		display_controls: bool = False,
		header: str = "Scenario Server",
	) -> None: ...
	def cleanup(self) -> bool: ...

	def enable_logging_stdout(self) -> StudioScenario: ...
	def disable_logging_stdout(self) -> StudioScenario: ...
	def install_logging_hook(self, hook: Callable[[str, int], None], level: int = 0) -> StudioScenario: ...

	def installStartEvent(self, event: Callable[..., None]) -> StudioScenario: ...
	def installEndEvent(self, event: Callable[..., None]) -> StudioScenario: ...
	def installStepStartEvent(self, event: Callable[..., None]) -> StudioScenario: ...
	def installStepEndEvent(self, event: Callable[..., None]) -> StudioScenario: ...
```

***

## StudioScenario

`StudioScenario` ist der Haupteinstiegspunkt zum Laden und Ausführen einer `.pmod`-Datei ohne die Desktop-Benutzeroberfläche.

### Konstruktor

```python
scenario = StudioScenario(verification_code="...")
```

#### `verification_code`

Dieser Wert wird zur Lizenzierung für die Headless-Ausführung verwendet.

Gängige Muster:

* Lokal / Server: Übergeben Sie den Verifikationscode als String.
* Docker / CI: Mounten Sie eine Lizenzdatei in den Container und übergeben Sie den **Pfad** als `verification_code`.

> ⚠️ Warnung: Für nicht-interaktive Umgebungen (Docker/CI/Services) immer `verification_code` explizit übergeben.

***

### Typischer Ablauf

In den meisten Skripten:

1. Erstellen Sie das Szenario (`StudioScenario(...)`).
2. Laden Sie eine `.pmod` (`load_scenario(...)`).
3. Führen Sie es aus (`run(...)`).
4. Bereinigen Sie (`cleanup()`).

Beispiel:

```python
import os

from studio import StudioScenario, enable_logging_stdout


def main() -> None:
	# Optional: gibt Laufzeit-Logs auf stdout aus (nützlich beim Debugging).
	enable_logging_stdout()

	verification_code = os.environ.get("AUGELAB_VERIFICATION_CODE", "")
	scenario = StudioScenario(verification_code=verification_code)

	try:
		loaded = scenario.load_scenario("your_scenario.pmod")
		if loaded is None:
			raise FileNotFoundError("Could not load .pmod file")

		# Wenn Ihr Szenario N Eingangsports hat, müssen Sie genau N Argumente übergeben.
		result = scenario.run(args=())
		print("Scenario result:", result)
	finally:
		scenario.cleanup()


if __name__ == "__main__":
	main()
```

> ℹ️ Hinweis: `cleanup()` ist wichtig in lang laufenden Prozessen (Services, Batch-Jobs), um Speicher freizugeben und den Laufzeitzustand zurückzusetzen.

***

### Laden einer `.pmod`

```python
scenario.load_scenario("path/to/scenario.pmod")
```

* Gibt `StudioScenario` bei Erfolg zurück.
* Gibt `None` zurück, wenn der Pfad nicht existiert.

Wenn Ihr Szenario auf externe Ressourcen verweist, behalten Sie die gleiche Verzeichnisstruktur bei wie auf dem Desktop (siehe Abschnitt „Transferring .pmod files“).

***

### Ausführen eines Szenarios

```python
outputs = scenario.run(args=(... ,))
```

Die Laufzeit validiert, dass die Anzahl der Argumente mit der Anzahl der Szenarioeingänge übereinstimmt.

Wenn Ihre `.pmod` hat:

* 0 Eingänge: rufen Sie `scenario.run()` oder `scenario.run(())` auf
* 1 Eingang: rufen Sie `scenario.run((value,))` auf
* 2 Eingänge: rufen Sie `scenario.run((value1, value2))` auf

> ⚠️ Warnung: `args` muss ein Tuple sein. Für einen einzelnen Eingang nicht das abschließende Komma vergessen: `(value,)`.

***

## Weitere StudioScenario-Methoden

Dieser Abschnitt beschreibt zusätzliche öffentliche Methoden, die Sie für Automatisierung und Debugging verwenden können.

### Laden ohne Fehler bei fehlenden Ressourcen

Wenn Sie Szenarien zwischen Maschinen/Containern verschieben und einige Ressourcen möglicherweise nicht verfügbar sind, können Sie permissiver laden:

```python
from studio import StudioScenario

scenario = StudioScenario(verification_code="YOUR_CODE")
scenario.disable_load_errors()
scenario.load_scenario("scenario.pmod")
```

### Laden / Speichern

```python
scenario.load_scenario_raw(content="...")
scenario.save_scenario("out.pmod")
scenario.clear_scenario()
```

### Custom Nodes

Laden Sie benutzerdefinierte Node-Python-Module aus einem Ordner:

```python
scenario.load_custom_nodes("path/to/custom_nodes")
```

> ⚠️ Warnung: Dies importiert Python-Dateien dynamisch. Laden Sie nur vertrauenswürdigen Code.

### Szenario-Name

```python
name = scenario.get_name()
```

### Ports (Szenario Eingänge/Ausgänge)

Sie können die Namen der Eingangs-/Ausgangsports abfragen:

```python
ports = scenario.get_ports()
print("inputs:", ports["inputs"])
print("outputs:", ports["outputs"])
```

### Einen Webserver starten (optional)

Wenn Sie einen Szenario-Runner per HTTP bereitstellen möchten:

```python
scenario.run_server(host="0.0.0.0", port=8080)
```

### Events

Installieren Sie Lifecycle-Callbacks rund um die Szenarioausführung:

```python
def on_start() -> None:
	print("Scenario started")


scenario.installStartEvent(on_start)
```

***

## Logging-Hilfen

AugeLab Studio verfügt über einen Laufzeit-Logger, der von Blöcken und Szenarien verwendet wird. Im Headless-Modus möchten Sie typischerweise eines der folgenden:

* Logs während der Entwicklung auf stdout ausgeben
* Logs in Ihr eigenes Logging-System weiterleiten

Diese Hilfsfunktionen sind global (sie beeinflussen den Laufzeit-Logger, der von Ihrem Headless-Szenario verwendet wird).

***

### enable\_logging\_stdout()

Aktiviert die „freundliche“ Laufzeitprotokollierung auf stdout.

Verwenden Sie dies, wenn Sie Block-/Szenario-Logs in der Konsole sehen möchten:

```python
from studio import enable_logging_stdout

enable_logging_stdout()
```

***

### disable\_logging\_stdout()

Deaktiviert die Laufzeitprotokollierung nach stdout.

Nützlich wenn:

* Sie `install_logging_event(...)` verwenden und doppelte Logs vermeiden möchten
* Sie saubere Ausgabe möchten (nur eigene `print(...)`-/Logger-Ausgaben)

```python
from studio import disable_logging_stdout

disable_logging_stdout()
```

***

### install\_logging\_event(event: Callable\[\[str], None])

Installiert einen Callback, der Laufzeit-Lognachrichten empfängt.

Das ist der einfachste Weg, AugeLab-Laufzeitlogs in Ihr eigenes Logging-Framework zu integrieren.

```python
import logging

from studio import install_logging_event, disable_logging_stdout

logger = logging.getLogger("augelab")


def forward_to_python_logging(msg: str) -> None:
	logger.info(msg)


disable_logging_stdout()
install_logging_event(forward_to_python_logging)
```

### StudioScenario.install\_logging\_hook(hook, level=0)

Wenn Sie bevorzugen, Hooks pro Szenario zu installieren (mit Filterung nach Level), verwenden Sie:

```python
def hook(msg: str, level: int) -> None:
	print(level, msg)


scenario.install_logging_hook(hook, level=20)
```

> ⚠️ Warnung: Halten Sie Ihren Callback schnell. Wenn Sie schwere Arbeit (I/O, Netzwerk) durchführen müssen, ziehen Sie in Betracht, Nachrichten zu puffern und diese in einem anderen Thread/Prozess zu verarbeiten.

***

## Empfohlene Muster

### 1) Umgebungsvariable für Lizenzierung

Für Skripte, die in unterschiedlichen Umgebungen (lokal vs CI vs Docker) laufen, ist das Übergeben von `verification_code` über eine Umgebungsvariable praktisch portabel:

```python
import os
from studio import StudioScenario

scenario = StudioScenario(verification_code=os.environ["AUGELAB_VERIFICATION_CODE"])
```

### 2) Immer mit try/finally aufräumen

```python
from studio import StudioScenario

scenario = StudioScenario(verification_code="YOUR_CODE")
try:
	scenario.load_scenario("scenario.pmod")
	scenario.run(())
finally:
	scenario.cleanup()
```
