import logging import os from typing import cast, Optional, Dict, TypedDict from httpx._types import ProxyTypes from typing_extensions import Unpack from e2b.api.metadata import package_version from e2b.sandbox_domains import is_supported_sandbox_domain REQUEST_TIMEOUT: float = 60.0 # 60 seconds KEEPALIVE_PING_INTERVAL_SEC = 50 # 50 seconds KEEPALIVE_PING_HEADER = "Keepalive-Ping-Interval" class ApiParams(TypedDict, total=False): """ Parameters for a request. In the case of a sandbox, it applies to all **requests made to the returned sandbox**. """ request_timeout: Optional[float] """Timeout for the request in **seconds**, defaults to 60 seconds.""" headers: Optional[Dict[str, str]] """Additional headers to send with the request. Deprecated, use api_headers instead.""" api_headers: Optional[Dict[str, str]] """Additional headers to send with E2B API requests.""" api_key: Optional[str] """E2B API Key to use for authentication, defaults to `E2B_API_KEY` environment variable.""" validate_api_key: Optional[bool] """Whether to validate the format of the E2B API key on the client side. Disable this when your deployment issues API keys that don't match the default `e2b_` format. Defaults to `E2B_VALIDATE_API_KEY` environment variable or `True`.""" domain: Optional[str] """E2B domain to use for authentication, defaults to `E2B_DOMAIN` environment variable.""" api_url: Optional[str] """URL to use for the API, defaults to `https://api.`. For internal use only.""" debug: Optional[bool] """Whether to use debug mode, defaults to `E2B_DEBUG` environment variable.""" proxy: Optional[ProxyTypes] """Proxy to use for the request. In case of a sandbox it applies to all **requests made to the returned sandbox**.""" sandbox_url: Optional[str] """URL to connect to sandbox, defaults to `E2B_SANDBOX_URL` environment variable.""" class ApiParamsWithLogger(ApiParams, total=False): """:class:`ApiParams` plus the construction-time ``logger``. Internal type returned by :meth:`ConnectionConfig.get_api_params` so that the logger a sandbox was created/connected with keeps propagating to the throwaway ``ConnectionConfig`` that instance control-plane methods rebuild. Unlike :class:`ApiParams`, ``logger`` is not a public per-request option. """ logger: Optional[logging.Logger] class ConnectionConfig: """ Configuration for the connection to the API. """ envd_port = 49983 _integration: Optional[str] = None @classmethod def set_integration(cls, integration: Optional[str]) -> None: """ Identify traffic from an integration wrapping the E2B SDK by appending ``integration`` (e.g. ``"e2b-code-interpreter/0.1.0"``) to the ``User-Agent`` header of every request. Call once at startup, before any ``ConnectionConfig`` is constructed — configs read the value at construction time. Pass ``None`` to clear. Internal use only — not part of the public, stable API. :meta private: """ ConnectionConfig._integration = integration @staticmethod def _domain(): return os.getenv("E2B_DOMAIN") or "e2b.app" @staticmethod def _debug(): return os.getenv("E2B_DEBUG", "false").lower() == "true" @staticmethod def _api_key(): return os.getenv("E2B_API_KEY") @staticmethod def _validate_api_key(): return os.getenv("E2B_VALIDATE_API_KEY", "true").lower() != "false" @staticmethod def _api_url(): return os.getenv("E2B_API_URL") @staticmethod def _sandbox_url(): return os.getenv("E2B_SANDBOX_URL") @staticmethod def _access_token(): return os.getenv("E2B_ACCESS_TOKEN") @staticmethod def _build_user_agent() -> str: user_agent_parts = [f"e2b-python-sdk/{package_version}"] if ConnectionConfig._integration: user_agent_parts.append(ConnectionConfig._integration) return " ".join(user_agent_parts) def _apply_user_agent( self, headers: Dict[str, str], user_agent_override: Optional[str], ) -> bool: """ Set the ``User-Agent`` on ``headers``: an explicitly provided value always wins; otherwise the SDK-built one, tagged with the current integration. Returns whether the SDK-built value was applied, so callers can keep it in sync with the current integration on later rebuilds. """ if user_agent_override is not None: headers["User-Agent"] = user_agent_override return False headers["User-Agent"] = self._build_user_agent() return True def __init__( self, domain: Optional[str] = None, debug: Optional[bool] = None, api_key: Optional[str] = None, validate_api_key: Optional[bool] = None, api_url: Optional[str] = None, sandbox_url: Optional[str] = None, access_token: Optional[str] = None, request_timeout: Optional[float] = None, headers: Optional[Dict[str, str]] = None, api_headers: Optional[Dict[str, str]] = None, extra_sandbox_headers: Optional[Dict[str, str]] = None, proxy: Optional[ProxyTypes] = None, logger: Optional[logging.Logger] = None, ): self.logger = logger self.domain = domain or ConnectionConfig._domain() self.debug = debug if debug is not None else ConnectionConfig._debug() self.api_key = api_key or ConnectionConfig._api_key() self.validate_api_key = ( validate_api_key if validate_api_key is not None else ConnectionConfig._validate_api_key() ) # Deprecated: pass the token through `api_headers` instead, e.g. # api_headers={"Authorization": f"Bearer {token}"}. self.access_token = access_token or ConnectionConfig._access_token() self.headers = {**(headers or {}), **(api_headers or {})} self._user_agent_is_sdk_built = self._apply_user_agent( self.headers, self.headers.get("User-Agent"), ) self.__extra_sandbox_headers = extra_sandbox_headers or {} self.proxy = proxy self.request_timeout = ConnectionConfig._get_request_timeout( REQUEST_TIMEOUT, request_timeout, ) self.api_url = ( api_url or ConnectionConfig._api_url() or ("http://localhost:3000" if self.debug else f"https://api.{self.domain}") ) self._sandbox_url: Optional[str] = ( sandbox_url or ConnectionConfig._sandbox_url() ) @staticmethod def _get_request_timeout( default_timeout: Optional[float], request_timeout: Optional[float], ): if request_timeout == 0: return None elif request_timeout is not None: return request_timeout else: return default_timeout def get_request_timeout(self, request_timeout: Optional[float] = None): return self._get_request_timeout(self.request_timeout, request_timeout) def get_sandbox_url(self, sandbox_id: str, sandbox_domain: str) -> str: if self._sandbox_url: return self._sandbox_url # type: ignore[return-value] if self.debug: return f"http://{self.get_host(sandbox_id, sandbox_domain, self.envd_port)}" sandbox_domain = sandbox_domain or self.domain if is_supported_sandbox_domain(sandbox_domain): return f"https://sandbox.{sandbox_domain}" return f"https://{self.get_host(sandbox_id, sandbox_domain, self.envd_port)}" def get_sandbox_direct_url(self, sandbox_id: str, sandbox_domain: str) -> str: if self._sandbox_url: return self._sandbox_url # type: ignore[return-value] if self.debug: return f"http://{self.get_host(sandbox_id, sandbox_domain, self.envd_port)}" return f"https://{self.get_host(sandbox_id, sandbox_domain, self.envd_port)}" def get_host(self, sandbox_id: str, sandbox_domain: str, port: int) -> str: """ Get the host address to connect to the sandbox. You can then use this address to connect to the sandbox port from outside the sandbox via HTTP or WebSocket. :param port: Port to connect to :param sandbox_domain: Domain to connect to :param sandbox_id: Sandbox to connect to :return: Host address to connect to """ if self.debug: return f"localhost:{port}" return f"{port}-{sandbox_id}.{sandbox_domain}" def get_api_params( self, **opts: Unpack[ApiParams], ) -> dict: """ Get the parameters for the API call. This is used to avoid passing the following attributes to the API call: - access_token - api_url It also returns a copy, so the original object is not modified. :return: Dictionary of parameters for the API call """ headers = opts.get("headers") api_headers = opts.get("api_headers") request_timeout = opts.get("request_timeout") api_key = opts.get("api_key") validate_api_key = opts.get("validate_api_key") api_url = opts.get("api_url") domain = opts.get("domain") debug = opts.get("debug") proxy = opts.get("proxy") sandbox_url = opts.get("sandbox_url") req_headers = self.headers.copy() if headers is not None: req_headers.update(headers) if api_headers is not None: req_headers.update(api_headers) if self._user_agent_is_sdk_built: # Same precedence as the merge above: api_headers wins over headers. per_call_user_agent = {**(headers or {}), **(api_headers or {})}.get( "User-Agent" ) self._apply_user_agent(req_headers, per_call_user_agent) # `logger` is a construction-time option rather than a per-request # ApiParams field, but it must propagate to the throwaway # ConnectionConfig that instance control-plane methods (kill, pause, # set_timeout, get_info, connect, ...) rebuild from these params, so # those requests keep logging with the logger the sandbox was created # or connected with. return dict( ApiParamsWithLogger( api_key=api_key if api_key is not None else self.api_key, validate_api_key=( validate_api_key if validate_api_key is not None else self.validate_api_key ), api_url=api_url if api_url is not None else self.api_url, domain=domain if domain is not None else self.domain, debug=debug if debug is not None else self.debug, request_timeout=self.get_request_timeout(request_timeout), headers=req_headers, proxy=proxy if proxy is not None else self.proxy, sandbox_url=( sandbox_url if sandbox_url is not None else cast(Optional[str], self._sandbox_url) ), logger=self.logger, ) ) @property def sandbox_headers(self): """ We need this separate as we use the same header for E2B access token to API and envd access token to sandbox. """ return { "User-Agent": self.headers["User-Agent"], **self.__extra_sandbox_headers, } Username = str """ User used for the operation in the sandbox. """ default_username: Username = "user" """ Default user used for the operation in the sandbox. """