Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions leap0/_async/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def __init__(
timeout: float = DEFAULT_CLIENT_TIMEOUT,
auth_header: str = "authorization",
bearer: bool = True,
otel_enabled: bool | None = None,
sdk_otel_enabled: bool | None = None,
):
if config is not None:
provided_overrides = {
Expand All @@ -117,7 +117,7 @@ def __init__(
"sandbox_domain": sandbox_domain,
"auth_header": auth_header if auth_header != "authorization" else None,
"bearer": bearer if bearer is not True else None,
"otel_enabled": otel_enabled,
"sdk_otel_enabled": sdk_otel_enabled,
}
if timeout != DEFAULT_CLIENT_TIMEOUT:
provided_overrides["timeout"] = timeout
Expand All @@ -133,7 +133,7 @@ def __init__(
timeout=timeout,
auth_header=auth_header,
bearer=bearer,
otel_enabled=otel_enabled,
sdk_otel_enabled=sdk_otel_enabled,
)
self._transport = AsyncTransport(
api_key=config.api_key,
Expand Down Expand Up @@ -164,7 +164,7 @@ def __init__(
self.code_interpreter = AsyncCodeInterpreterClient(self._transport, sandbox_domain=config.sandbox_domain)
self.desktop = AsyncDesktopClient(self._transport, sandbox_domain=config.sandbox_domain)

if config.otel_enabled:
if config.sdk_otel_enabled:
self._init_otel()

def _init_otel(self) -> None:
Expand Down
39 changes: 26 additions & 13 deletions leap0/_async/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@
from functools import wraps
from typing import TYPE_CHECKING, Generic, Protocol, TypeVar, cast

from ..constants import OTEL_EXPORTER_OTLP_ENDPOINT_ENV, OTEL_EXPORTER_OTLP_HEADERS_ENV
from .._internal.types import SandboxFactory
from ..models.config import DEFAULT_MEMORY_MIB, DEFAULT_TEMPLATE_NAME, DEFAULT_TIMEOUT_MIN, DEFAULT_VCPU
from ..models.config import (
DEFAULT_MEMORY_MIB,
DEFAULT_TEMPLATE_NAME,
DEFAULT_TIMEOUT_MIN,
DEFAULT_VCPU,
)
from ..models.sandbox import CreateSandboxParams, Sandbox as SandboxData, SandboxRef, SandboxStatus, sandbox_id_of
from .._schemas.sandbox import NetworkPolicyDict, SandboxCreateResponseDict, SandboxStatusResponseDict
from .._utils.errors import intercept_errors
Expand All @@ -20,8 +26,8 @@


_OTEL_ENV_KEYS = (
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_HEADERS",
OTEL_EXPORTER_OTLP_ENDPOINT_ENV,
OTEL_EXPORTER_OTLP_HEADERS_ENV,
)


Expand Down Expand Up @@ -140,11 +146,18 @@ def websocket_url(self, path: str = "/", *, port: int | None = None) -> str:


def _inject_otel_env(env_vars: dict[str, str] | None) -> dict[str, str] | None:
otel = {k: v for k in _OTEL_ENV_KEYS if (v := os.environ.get(k))}
if not otel and not env_vars:
return None
if not otel:
return env_vars
endpoint = os.environ.get(OTEL_EXPORTER_OTLP_ENDPOINT_ENV)
if not endpoint:
raise ValueError(
f"otel_export=True requires {OTEL_EXPORTER_OTLP_ENDPOINT_ENV} in the local environment"
)
otel = {OTEL_EXPORTER_OTLP_ENDPOINT_ENV: endpoint}
for key in _OTEL_ENV_KEYS:
if key == OTEL_EXPORTER_OTLP_ENDPOINT_ENV:
continue
value = os.environ.get(key)
if value:
otel[key] = value
merged = dict(otel)
if env_vars:
merged.update(env_vars)
Expand Down Expand Up @@ -182,7 +195,7 @@ async def create(
memory_mib: int = DEFAULT_MEMORY_MIB,
timeout_min: int = DEFAULT_TIMEOUT_MIN,
auto_pause: bool = False,
telemetry: bool = False,
otel_export: bool = False,
env_vars: dict[str, str] | None = None,
network_policy: NetworkPolicyDict | None = None,
http_timeout: float | None = None,
Expand All @@ -195,7 +208,7 @@ async def create(
memory_mib: Memory in MiB (512 to 8192, must be even).
timeout_min: Sandbox timeout in minutes (1 to 480).
auto_pause: Whether the sandbox should auto-pause on timeout.
telemetry: Whether OpenTelemetry variables should be injected.
otel_export: Whether OpenTelemetry exporter variables should be injected.
env_vars: Environment variables to set inside the sandbox.
network_policy: Outbound network policy for the sandbox.
http_timeout: Optional HTTP request timeout in seconds for this SDK call.
Expand All @@ -209,12 +222,12 @@ async def create(
memory_mib=memory_mib,
timeout_min=timeout_min,
auto_pause=auto_pause,
telemetry=telemetry,
env_vars=_inject_otel_env(env_vars) if telemetry else env_vars,
otel_export=otel_export,
env_vars=_inject_otel_env(env_vars) if otel_export else env_vars,
network_policy=network_policy,
)
payload = params.to_payload()
payload.pop("telemetry", None)
payload.pop("otel_export", None)
data: SandboxCreateResponseDict = await self._transport.request_json(
"POST", "/v1/sandbox", json=payload, expected_status=201,
timeout=http_timeout,
Expand Down
16 changes: 13 additions & 3 deletions leap0/_sync/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from types import TracebackType
from typing import Self
import warnings

from opentelemetry import metrics, trace
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
Expand Down Expand Up @@ -90,15 +91,24 @@ def __init__(
auth_header: str = "authorization",
bearer: bool = True,
otel_enabled: bool | None = None,
sdk_otel_enabled: bool | None = None,
):
if otel_enabled is not None:
warnings.warn(
"otel_enabled is deprecated; use sdk_otel_enabled instead",
DeprecationWarning,
stacklevel=2,
)
if sdk_otel_enabled is None:
sdk_otel_enabled = otel_enabled
config = Leap0Config(
api_key=api_key,
base_url=base_url,
sandbox_domain=sandbox_domain,
timeout=timeout,
auth_header=auth_header,
bearer=bearer,
otel_enabled=otel_enabled,
sdk_otel_enabled=sdk_otel_enabled,
)
self._transport = Transport(
api_key=config.api_key,
Expand Down Expand Up @@ -128,7 +138,7 @@ def __init__(
self.code_interpreter = CodeInterpreterClient(self._transport, sandbox_domain=config.sandbox_domain)
self.desktop = DesktopClient(self._transport, sandbox_domain=config.sandbox_domain)

if config.otel_enabled:
if config.sdk_otel_enabled:
self._init_otel()

def _init_otel(self) -> None:
Expand Down Expand Up @@ -223,5 +233,5 @@ def Leap0(config: Leap0Config) -> Leap0Client:
timeout=config.timeout,
auth_header=config.auth_header,
bearer=config.bearer,
otel_enabled=config.otel_enabled,
sdk_otel_enabled=config.sdk_otel_enabled,
)
47 changes: 32 additions & 15 deletions leap0/_sync/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@
from functools import wraps
from typing import Generic, Protocol, TypeVar, cast

from ..constants import OTEL_EXPORTER_OTLP_ENDPOINT_ENV, OTEL_EXPORTER_OTLP_HEADERS_ENV
from .._internal.types import SandboxFactory
from ..models.config import DEFAULT_MEMORY_MIB, DEFAULT_TEMPLATE_NAME, DEFAULT_TIMEOUT_MIN, DEFAULT_VCPU
from ..models.config import (
DEFAULT_MEMORY_MIB,
DEFAULT_TEMPLATE_NAME,
DEFAULT_TIMEOUT_MIN,
DEFAULT_VCPU,
)
from ..models.sandbox import CreateSandboxParams, Sandbox as SandboxData, SandboxRef, SandboxStatus, sandbox_id_of
from .._schemas.sandbox import NetworkPolicyDict, SandboxCreateResponseDict, SandboxStatusResponseDict
from .._utils.errors import intercept_errors
Expand All @@ -14,8 +20,8 @@


_OTEL_ENV_KEYS = (
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_HEADERS",
OTEL_EXPORTER_OTLP_ENDPOINT_ENV,
OTEL_EXPORTER_OTLP_HEADERS_ENV,
)

SandboxT = TypeVar("SandboxT", SandboxData, SandboxStatus, "Sandbox")
Expand Down Expand Up @@ -137,11 +143,18 @@ def websocket_url(self, path: str = "/", *, port: int | None = None) -> str:


def _inject_otel_env(env_vars: dict[str, str] | None) -> dict[str, str] | None:
otel = {k: v for k in _OTEL_ENV_KEYS if (v := os.environ.get(k))}
if not otel and not env_vars:
return None
if not otel:
return env_vars
endpoint = os.environ.get(OTEL_EXPORTER_OTLP_ENDPOINT_ENV)
if not endpoint:
raise ValueError(
f"otel_export=True requires {OTEL_EXPORTER_OTLP_ENDPOINT_ENV} in the local environment"
)
otel = {OTEL_EXPORTER_OTLP_ENDPOINT_ENV: endpoint}
for key in _OTEL_ENV_KEYS:
if key == OTEL_EXPORTER_OTLP_ENDPOINT_ENV:
continue
value = os.environ.get(key)
if value:
otel[key] = value
merged = dict(otel)
if env_vars:
merged.update(env_vars)
Expand Down Expand Up @@ -183,7 +196,8 @@ def create(
memory_mib: int = DEFAULT_MEMORY_MIB,
timeout_min: int = DEFAULT_TIMEOUT_MIN,
auto_pause: bool = False,
telemetry: bool = False,
otel_export: bool | None = None,
telemetry: bool | None = None,
env_vars: dict[str, str] | None = None,
network_policy: NetworkPolicyDict | None = None,
http_timeout: float | None = None,
Expand All @@ -196,28 +210,31 @@ def create(
memory_mib: Memory in MiB (512 to 8192, must be even).
timeout_min: Sandbox timeout in minutes (1 to 480, default 5).
auto_pause: Automatically pause the sandbox into a snapshot on timeout.
telemetry: Enable OpenTelemetry export. Reads ``OTEL_EXPORTER_OTLP_ENDPOINT``
and ``OTEL_EXPORTER_OTLP_HEADERS`` from the local environment and
injects them into the sandbox.
otel_export: Inject OpenTelemetry exporter environment into the sandbox.
Requires ``OTEL_EXPORTER_OTLP_ENDPOINT`` in the local environment and
also forwards ``OTEL_EXPORTER_OTLP_HEADERS`` when present.
telemetry: Deprecated alias for ``otel_export``. Use ``otel_export`` instead.
env_vars: Environment variables to set inside the sandbox.
network_policy: Outbound network policy for the sandbox.
http_timeout: Optional HTTP request timeout in seconds for this SDK call.

Returns:
SandboxT | SandboxData | SandboxStatus: Created sandbox object.
"""
effective_otel_export = otel_export if otel_export is not None else bool(telemetry)

params = CreateSandboxParams(
template_name=template_name,
vcpu=vcpu,
memory_mib=memory_mib,
timeout_min=timeout_min,
auto_pause=auto_pause,
telemetry=telemetry,
env_vars=_inject_otel_env(env_vars) if telemetry else env_vars,
otel_export=effective_otel_export,
env_vars=_inject_otel_env(env_vars) if effective_otel_export else env_vars,
network_policy=network_policy,
)
payload = params.to_payload()
payload.pop("telemetry", None)
payload.pop("otel_export", None)
data: SandboxCreateResponseDict = self._transport.request_json(
"POST", "/v1/sandbox", json=payload, expected_status=201,
timeout=http_timeout,
Expand Down
13 changes: 13 additions & 0 deletions leap0/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from __future__ import annotations

LEAP0_API_KEY_ENV = "LEAP0_API_KEY"

LEAP0_BASE_URL_ENV = "LEAP0_BASE_URL"

LEAP0_SANDBOX_DOMAIN_ENV = "LEAP0_SANDBOX_DOMAIN"

LEAP0_SDK_OTEL_ENABLED_ENV = "LEAP0_SDK_OTEL_ENABLED"

OTEL_EXPORTER_OTLP_ENDPOINT_ENV = "OTEL_EXPORTER_OTLP_ENDPOINT"

OTEL_EXPORTER_OTLP_HEADERS_ENV = "OTEL_EXPORTER_OTLP_HEADERS"
29 changes: 20 additions & 9 deletions leap0/models/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@
import os
from pydantic import BaseModel, ConfigDict, model_validator

from ..constants import (
LEAP0_API_KEY_ENV,
LEAP0_BASE_URL_ENV,
LEAP0_SANDBOX_DOMAIN_ENV,
LEAP0_SDK_OTEL_ENABLED_ENV,
OTEL_EXPORTER_OTLP_ENDPOINT_ENV,
)

DEFAULT_BASE_URL = "https://api.leap0.dev"

DEFAULT_SANDBOX_DOMAIN = "sandbox.leap0.dev"
Expand Down Expand Up @@ -40,7 +48,7 @@ class Leap0Config(BaseModel):
timeout: float = DEFAULT_CLIENT_TIMEOUT
auth_header: str = "authorization"
bearer: bool = True
otel_enabled: bool | None = None
sdk_otel_enabled: bool | None = None

@model_validator(mode="after")
def _resolve_and_validate(self) -> Leap0Config:
Expand All @@ -53,10 +61,10 @@ def _resolve_and_validate(self) -> Leap0Config:

api_key = self.api_key
if api_key is None:
api_key = os.environ.get("LEAP0_API_KEY")
api_key = os.environ.get(LEAP0_API_KEY_ENV)
api_key = api_key.strip() if api_key else api_key
if not api_key:
raise ValueError("api_key is required or set LEAP0_API_KEY")
raise ValueError(f"api_key is required or set {LEAP0_API_KEY_ENV}")

auth_header = self.auth_header.strip()
if not auth_header:
Expand All @@ -65,14 +73,17 @@ def _resolve_and_validate(self) -> Leap0Config:
self.timeout = timeout
self.api_key = api_key
self.auth_header = auth_header
if self.otel_enabled is None:
self.otel_enabled = os.environ.get("LEAP0_OTEL_ENABLED") == "true" or bool(
os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT")
)
self.base_url = _resolve_env_str(self.base_url, "LEAP0_BASE_URL", DEFAULT_BASE_URL)
if self.sdk_otel_enabled is None:
sdk_otel_env = os.environ.get(LEAP0_SDK_OTEL_ENABLED_ENV)
sdk_otel_env = sdk_otel_env.strip() if sdk_otel_env is not None else None
if sdk_otel_env:
self.sdk_otel_enabled = sdk_otel_env.lower() == "true"
else:
self.sdk_otel_enabled = bool(os.environ.get(OTEL_EXPORTER_OTLP_ENDPOINT_ENV))
self.base_url = _resolve_env_str(self.base_url, LEAP0_BASE_URL_ENV, DEFAULT_BASE_URL)
self.sandbox_domain = _resolve_env_str(
self.sandbox_domain,
"LEAP0_SANDBOX_DOMAIN",
LEAP0_SANDBOX_DOMAIN_ENV,
DEFAULT_SANDBOX_DOMAIN,
)
return self
2 changes: 1 addition & 1 deletion leap0/models/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class CreateSandboxParams(BaseModel):
memory_mib: int = DEFAULT_MEMORY_MIB
timeout_min: int = DEFAULT_TIMEOUT_MIN
auto_pause: bool = False
telemetry: bool = False
otel_export: bool = False
env_vars: dict[str, str] | None = None
network_policy: NetworkPolicyDict | None = None

Expand Down
Loading