# Referencia de scripting

Esta página documenta la API pública para ejecución headless (sin interfaz) expuesta por el paquete `studio` de Python.

Se centra en estos símbolos públicos:

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

Si nunca has ejecutado un escenario en modo headless, primero lee la sección Headless y luego vuelve aquí para ver los detalles de la API.

***

## Imports

Todas las APIs documentadas aquí se exportan desde el paquete de nivel superior `studio`:

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

***

## Type Stubs

La interfaz pública para headless puede encontrarse aquí:

```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` es el punto de entrada principal para cargar y ejecutar un archivo `.pmod` sin la interfaz de escritorio (modo headless).

### Constructor

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

#### `verification_code`

Este valor se usa para configurar la licencia en la ejecución headless.

Patrones comunes:

* Máquina local / servidor: pasa la cadena del verification code.
* Docker / CI: monta un archivo de licencia dentro del contenedor y pasa su **ruta** como `verification_code`.

> Advertencia\
> Para entornos no interactivos (Docker/CI/servicios), siempre pasa `verification_code` explícitamente.

***

### Ciclo típico de uso

En la mayoría de los scripts harás:

1. Crear el escenario (`StudioScenario(...)`)
2. Cargar un `.pmod` (`load_scenario(...)`)
3. Ejecutarlo (`run(...)`)
4. Limpiar recursos (`cleanup()`)

Ejemplo:

```python
import os

from studio import StudioScenario, enable_logging_stdout


def main() -> None:
	# Opcional: imprime logs de runtime en stdout (útil al depurar).
	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")

		# Si tu escenario tiene N puertos de entrada, debes pasar exactamente N args.
		result = scenario.run(args=())
		print("Scenario result:", result)
	finally:
		scenario.cleanup()


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

> Nota\
> `cleanup()` es importante en procesos de larga duración (servicios, jobs por lotes) para liberar memoria y resetear el estado de runtime.

***

### Cargar un `.pmod`

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

* Devuelve `StudioScenario` en caso de éxito.
* Devuelve `None` si la ruta no existe.

Si tu escenario referencia recursos externos, conserva la misma estructura de carpetas usada en la versión de escritorio (ver la sección “Transferring .pmod files”).

***

### Ejecutar un escenario

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

El runtime valida que el número de argumentos coincida con el número de entradas del escenario.

Si tu `.pmod` tiene:

* 0 entradas: llama a `scenario.run()` o `scenario.run(())`
* 1 entrada: llama a `scenario.run((value,))`
* 2 entradas: llama a `scenario.run((value1, value2))`

> Advertencia\
> `args` debe ser una tupla. Para una sola entrada recuerda la coma final: `(value,)`.

***

## Más métodos de `StudioScenario`

Esta sección cubre métodos públicos adicionales que puedes usar para automatización y depuración.

### Cargar sin fallar por recursos faltantes

Si mueves escenarios entre máquinas/contenedores y algunos recursos pueden no estar disponibles, puedes optar por cargar de forma más permisiva:

```python
from studio import StudioScenario

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

### Cargar / guardar

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

### Nodos personalizados

Carga módulos Python de nodos personalizados desde una carpeta:

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

> Advertencia\
> Esto importa dinámicamente archivos Python. Solo carga código de confianza.

### Nombre del escenario

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

### Puertos (entradas/salidas del escenario)

Puedes obtener los nombres de los puertos de entrada y salida:

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

### Ejecutar un servidor web (opcional)

Si quieres exponer un runner de escenario por HTTP:

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

### Eventos

Instala callbacks alrededor del ciclo de ejecución del escenario:

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


scenario.installStartEvent(on_start)
```

***

## Helpers de logging

AugeLab Studio tiene un logger de runtime usado por blocks y escenarios. En modo headless normalmente querrás una de estas opciones:

* imprimir logs en stdout mientras desarrollas
* reenviar logs a tu propio sistema de logging

Estos helpers son globales (afectan al logger de runtime usado por tu escenario headless).

***

### `enable_logging_stdout()`

Habilita logging de runtime “amigable” hacia stdout.

Úsalo cuando quieras ver logs de blocks/escenario en la consola:

```python
from studio import enable_logging_stdout

enable_logging_stdout()
```

***

### `disable_logging_stdout()`

Deshabilita el logging de runtime hacia stdout.

Útil cuando:

* estás usando `install_logging_event(...)` y quieres evitar logs duplicados
* quieres una salida limpia (solo tus propios `print(...)` o el output de tu logger)

```python
from studio import disable_logging_stdout

disable_logging_stdout()
```

***

### `install_logging_event(event: Callable[[str], None])`

Instala un callback que recibe los mensajes de log del runtime.

Esta es la forma más sencilla de integrar los logs de AugeLab en tu propio framework de logging.

```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)`

Si prefieres instalar un hook por escenario (con filtrado por nivel), usa:

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


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

> Advertencia\
> Mantén tu callback rápido. Si necesitas hacer trabajo pesado (I/O, red), considera encolar los mensajes y procesarlos en otro hilo/proceso.

***

## Patrones recomendados

### 1) Variable de entorno para la licencia

Para scripts que se ejecutan en distintos entornos (local vs CI vs Docker), pasar `verification_code` mediante variable de entorno mantiene el código portable:

```python
import os
from studio import StudioScenario

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

### 2) Siempre limpiar con `try/finally`

```python
from studio import StudioScenario

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