Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
6 changes: 6 additions & 0 deletions .changeset/lucky-moons-refactor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'e2b': patch
'@e2b/python-sdk': patch
---

Rework the multi-client binding: the `E2B` client now builds its resource classes through a hidden `ClientFactory.withOpts` (JS) / `ClientFactory._with_params` (Python) method that returns a subclass with the connection config bound, instead of inlining subclass creation in the client constructor
28 changes: 4 additions & 24 deletions packages/js-sdk/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,29 +68,9 @@ export class E2B {
* through this client's resource classes.
*/
constructor(opts?: E2BClientOpts) {
// Options are copied so later mutations of the caller's object cannot
// change the bound configuration. `signal` is dropped rather than only
// typed away, since it cancels a single request and a caller passing a
// wider-typed object (or plain JS) would otherwise bind it to every call.
const boundOpts: E2BClientOpts = { ...(opts ?? {}) }
delete (boundOpts as ConnectionOpts).signal

this.Sandbox = class extends Sandbox {
protected static override readonly boundOpts = boundOpts
}

this.Volume = class extends Volume {
protected static override readonly boundOpts = boundOpts
}

this.Secret = class extends Secret {
protected static override readonly boundOpts = boundOpts
}

this.Template = callableTemplate(
class extends TemplateBase {
protected static override readonly boundOpts = boundOpts
}
)
this.Sandbox = Sandbox.withOpts(opts)
this.Volume = Volume.withOpts(opts)
this.Secret = Secret.withOpts(opts)
this.Template = callableTemplate(TemplateBase.withOpts(opts))
}
}
29 changes: 29 additions & 0 deletions packages/js-sdk/src/connectionConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,35 @@ export class ClientFactory {
*/
protected static readonly boundOpts?: Omit<ConnectionOpts, 'signal'>

/**
* Return a subclass of this class with `opts` bound to it, used as the
* defaults for every call instead of the environment variables.
* Per-call options still take precedence over the bound options.
*
* @internal
* @hidden
* @hide
*/
// The static sides of the subclasses are not assignable to
// `typeof ClientFactory` (their constructors differ), and TS has no
// polymorphic `this` for statics, so the constraint is structural and the
// subclass expression cannot be typed as `T` without the cast.
static withOpts<T extends { prototype: ClientFactory }>(
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated
this: T,
opts?: Omit<ConnectionOpts, 'signal'>
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated
): T {
Comment thread
mishushakov marked this conversation as resolved.
Outdated
// Options are copied so later mutations of the caller's object cannot
// change the bound configuration. `signal` is dropped rather than only
// typed away, since it cancels a single request and a caller passing a
// wider-typed object (or plain JS) would otherwise bind it to every call.
const boundOpts: Omit<ConnectionOpts, 'signal'> = { ...(opts ?? {}) }
delete (boundOpts as ConnectionOpts).signal

return class extends (this as unknown as typeof ClientFactory) {
protected static override readonly boundOpts = boundOpts
} as unknown as T
}

/**
* Merge the connection options bound to this class with the per-call options,
* with the per-call options taking precedence.
Expand Down
32 changes: 8 additions & 24 deletions packages/python-sdk/e2b/client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from typing import Dict, Type, TypeVar, cast

from typing_extensions import Unpack

from e2b.connection_config import ApiParams
Expand All @@ -11,22 +9,12 @@
from e2b.volume.volume_async import AsyncVolume
from e2b.volume.volume_sync import Volume

T = TypeVar("T")


class E2BClientParams(ApiParams, total=False):
"""Params bound to an :class:`E2B` client, used as the defaults for every
call made through its resource classes. Same shape as :class:`ApiParams`."""


def _bind(cls: Type[T], api_params: ApiParams) -> Type[T]:
"""Generate a subclass of ``cls`` carrying ``api_params`` as its bound params."""
return cast(
Type[T],
type(cls.__name__, (cls,), {"_bound_api_params": api_params}),
)


class E2B:
"""
E2B client with an explicitly bound connection configuration.
Expand Down Expand Up @@ -60,30 +48,26 @@ def __init__(self, **opts: Unpack[E2BClientParams]):
:param opts: API params used as the defaults for every call made
through this client's resource classes.
"""
# Params are copied so later mutations of the caller's dicts cannot
# change the bound configuration.
api_params = cast(ApiParams, dict(cast(Dict[str, object], opts)))

self.Sandbox = _bind(Sandbox, api_params)
self.Sandbox = Sandbox._with_params(**opts)
"""`Sandbox` class bound to this client's connection configuration."""

self.AsyncSandbox = _bind(AsyncSandbox, api_params)
self.AsyncSandbox = AsyncSandbox._with_params(**opts)
"""`AsyncSandbox` class bound to this client's connection configuration."""

self.Volume = _bind(Volume, api_params)
self.Volume = Volume._with_params(**opts)
"""`Volume` class bound to this client's connection configuration."""

self.AsyncVolume = _bind(AsyncVolume, api_params)
self.AsyncVolume = AsyncVolume._with_params(**opts)
"""`AsyncVolume` class bound to this client's connection configuration."""

self.Template = _bind(Template, api_params)
self.Template = Template._with_params(**opts)
"""`Template` class bound to this client's connection configuration."""

self.AsyncTemplate = _bind(AsyncTemplate, api_params)
self.AsyncTemplate = AsyncTemplate._with_params(**opts)
"""`AsyncTemplate` class bound to this client's connection configuration."""

self.Secret = _bind(Secret, api_params)
self.Secret = Secret._with_params(**opts)
"""`Secret` class bound to this client's connection configuration."""

self.AsyncSecret = _bind(AsyncSecret, api_params)
self.AsyncSecret = AsyncSecret._with_params(**opts)
"""`AsyncSecret` class bound to this client's connection configuration."""
24 changes: 23 additions & 1 deletion packages/python-sdk/e2b/connection_config.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import logging
import os

from typing import cast, Mapping, Optional, Dict, TypedDict, Union
from typing import cast, Mapping, Optional, Dict, Type, TypedDict, TypeVar, Union

import httpx
from typing_extensions import Unpack
Expand Down Expand Up @@ -109,6 +109,9 @@ def merge_api_params(
return cast(ApiParams, merged)


_ClientT = TypeVar("_ClientT", bound="ClientFactory")


class ClientFactory:
"""
Base class for the resource classes (`Sandbox`, `Volume`, `Template`,
Expand All @@ -128,6 +131,25 @@ class ClientFactory:
:meta private:
"""

@classmethod
def _with_params(
cls: Type[_ClientT], **params: Unpack[ApiParams]
) -> Type[_ClientT]:
"""
Return a subclass of this class with ``params`` bound to it, used as
the defaults for every call instead of the environment variables.
Per-call params still take precedence over the bound params.

The keyword arguments form a fresh dict, so later mutations of the
caller's dicts cannot change the bound configuration.

:meta private:
"""
return cast(
Type[_ClientT],
type(cls.__name__, (cls,), {"_bound_api_params": params}),
)

@classmethod
def _resolve_api_params(cls, **opts: Unpack[ApiParams]) -> ApiParams:
"""
Expand Down
Loading