impello 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- impello-0.2.0/.gitignore +5 -0
- impello-0.2.0/PKG-INFO +109 -0
- impello-0.2.0/README.md +97 -0
- impello-0.2.0/pyproject.toml +30 -0
- impello-0.2.0/src/impello/__init__.py +84 -0
- impello-0.2.0/src/impello/_defaults.py +57 -0
- impello-0.2.0/tests/test_binding.py +81 -0
- impello-0.2.0/tests/test_defaults.py +78 -0
impello-0.2.0/.gitignore
ADDED
impello-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: impello
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python SDK for Impello sandboxes.
|
|
5
|
+
Project-URL: Homepage, https://impello.ai
|
|
6
|
+
Project-URL: Repository, https://github.com/21-Dreams/impello
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Keywords: agents,code-interpreter,impello,microvm,sandbox
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Requires-Dist: e2b<3,>=2.46.0
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# `impello`
|
|
14
|
+
|
|
15
|
+
The Python SDK for Impello sandboxes.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install impello
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Use
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from impello import Sandbox
|
|
27
|
+
|
|
28
|
+
sandbox = Sandbox.create()
|
|
29
|
+
result = sandbox.commands.run("echo hello")
|
|
30
|
+
|
|
31
|
+
print(result.stdout)
|
|
32
|
+
|
|
33
|
+
sandbox.pause()
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Set your key in the environment, or pass it to each call:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
export IMPELLO_API_KEY=imp_...
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
Sandbox.list(api_key="imp_...")
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
For a process that talks to more than one account or more than one fleet, bind
|
|
47
|
+
the settings to a client instead:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from impello import Impello
|
|
51
|
+
|
|
52
|
+
client = Impello(api_key="imp_...")
|
|
53
|
+
sandbox = client.Sandbox.create()
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Settings
|
|
57
|
+
|
|
58
|
+
Every setting reads an `IMPELLO_` name first, then the matching `E2B_` name.
|
|
59
|
+
|
|
60
|
+
| Variable | Default | What it does |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `IMPELLO_API_KEY` | none | The key. Starts with `imp_` |
|
|
63
|
+
| `IMPELLO_DOMAIN` | `sandbox.impello.ai` | The domain the API and the sandboxes sit on |
|
|
64
|
+
| `IMPELLO_API_URL` | `https://api.<domain>` | The API address. Set this to reach a self-hosted API |
|
|
65
|
+
| `IMPELLO_SANDBOX_URL` | derived from the domain | The address a sandbox is reached at |
|
|
66
|
+
| `IMPELLO_DEBUG` | `false` | Talk to `http://localhost:3000` |
|
|
67
|
+
|
|
68
|
+
The `E2B_` fallback is the migration, not politeness. Callers already export
|
|
69
|
+
`E2B_API_KEY`, `E2B_API_URL` and `E2B_DOMAIN`. Rename when it suits you.
|
|
70
|
+
Nothing breaks if you never do.
|
|
71
|
+
|
|
72
|
+
Only the domain has a default. Without one the SDK falls back to E2B's own
|
|
73
|
+
`e2b.app`, and the failure is a connection to somebody else's fleet.
|
|
74
|
+
|
|
75
|
+
Params passed to a single call beat a client's params, which beat the
|
|
76
|
+
environment. That is E2B's own rule and this package keeps it.
|
|
77
|
+
|
|
78
|
+
## Why this one is a wrapper, when the TypeScript one is a copy
|
|
79
|
+
|
|
80
|
+
The TypeScript client cannot talk to Impello at all. Its `validateApiKey` runs
|
|
81
|
+
inside the `ApiClient` constructor, is not exported, has no off switch, and
|
|
82
|
+
accepts only `e2b_`. So `@impello/sdk` had to be a copy.
|
|
83
|
+
|
|
84
|
+
Python has no `e2b_` pattern anywhere. It takes an `imp_` key today. So the
|
|
85
|
+
only thing missing is where to send the request, and that is all this package
|
|
86
|
+
supplies.
|
|
87
|
+
|
|
88
|
+
It supplies it through `ClientFactory._resolve_api_params`, which is E2B's own
|
|
89
|
+
seam for binding defaults. That means one method is overridden rather than the
|
|
90
|
+
twelve descriptors on `Sandbox`, and **`e2b` itself is untouched**: importing
|
|
91
|
+
`impello` does not retarget a plain `e2b` call in the same process, and it
|
|
92
|
+
never writes to `os.environ`. A test holds that.
|
|
93
|
+
|
|
94
|
+
## This package will be replaced
|
|
95
|
+
|
|
96
|
+
A permanent wrapper is the worst outcome. `pip show impello` prints `e2b`, and
|
|
97
|
+
the client sends a `publisher: e2b` header to our own servers. So the wrapper
|
|
98
|
+
ships now and the full clone lands before we take payments, with
|
|
99
|
+
`e2b/api/client/` regenerated from our own spec.
|
|
100
|
+
|
|
101
|
+
See `docs/decisions/sdk-self-contained.md`.
|
|
102
|
+
|
|
103
|
+
## Develop
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
python -m venv .venv
|
|
107
|
+
./.venv/bin/pip install -e . pytest
|
|
108
|
+
./.venv/bin/python -m pytest
|
|
109
|
+
```
|
impello-0.2.0/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# `impello`
|
|
2
|
+
|
|
3
|
+
The Python SDK for Impello sandboxes.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install impello
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Use
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from impello import Sandbox
|
|
15
|
+
|
|
16
|
+
sandbox = Sandbox.create()
|
|
17
|
+
result = sandbox.commands.run("echo hello")
|
|
18
|
+
|
|
19
|
+
print(result.stdout)
|
|
20
|
+
|
|
21
|
+
sandbox.pause()
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Set your key in the environment, or pass it to each call:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
export IMPELLO_API_KEY=imp_...
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
Sandbox.list(api_key="imp_...")
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
For a process that talks to more than one account or more than one fleet, bind
|
|
35
|
+
the settings to a client instead:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from impello import Impello
|
|
39
|
+
|
|
40
|
+
client = Impello(api_key="imp_...")
|
|
41
|
+
sandbox = client.Sandbox.create()
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Settings
|
|
45
|
+
|
|
46
|
+
Every setting reads an `IMPELLO_` name first, then the matching `E2B_` name.
|
|
47
|
+
|
|
48
|
+
| Variable | Default | What it does |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| `IMPELLO_API_KEY` | none | The key. Starts with `imp_` |
|
|
51
|
+
| `IMPELLO_DOMAIN` | `sandbox.impello.ai` | The domain the API and the sandboxes sit on |
|
|
52
|
+
| `IMPELLO_API_URL` | `https://api.<domain>` | The API address. Set this to reach a self-hosted API |
|
|
53
|
+
| `IMPELLO_SANDBOX_URL` | derived from the domain | The address a sandbox is reached at |
|
|
54
|
+
| `IMPELLO_DEBUG` | `false` | Talk to `http://localhost:3000` |
|
|
55
|
+
|
|
56
|
+
The `E2B_` fallback is the migration, not politeness. Callers already export
|
|
57
|
+
`E2B_API_KEY`, `E2B_API_URL` and `E2B_DOMAIN`. Rename when it suits you.
|
|
58
|
+
Nothing breaks if you never do.
|
|
59
|
+
|
|
60
|
+
Only the domain has a default. Without one the SDK falls back to E2B's own
|
|
61
|
+
`e2b.app`, and the failure is a connection to somebody else's fleet.
|
|
62
|
+
|
|
63
|
+
Params passed to a single call beat a client's params, which beat the
|
|
64
|
+
environment. That is E2B's own rule and this package keeps it.
|
|
65
|
+
|
|
66
|
+
## Why this one is a wrapper, when the TypeScript one is a copy
|
|
67
|
+
|
|
68
|
+
The TypeScript client cannot talk to Impello at all. Its `validateApiKey` runs
|
|
69
|
+
inside the `ApiClient` constructor, is not exported, has no off switch, and
|
|
70
|
+
accepts only `e2b_`. So `@impello/sdk` had to be a copy.
|
|
71
|
+
|
|
72
|
+
Python has no `e2b_` pattern anywhere. It takes an `imp_` key today. So the
|
|
73
|
+
only thing missing is where to send the request, and that is all this package
|
|
74
|
+
supplies.
|
|
75
|
+
|
|
76
|
+
It supplies it through `ClientFactory._resolve_api_params`, which is E2B's own
|
|
77
|
+
seam for binding defaults. That means one method is overridden rather than the
|
|
78
|
+
twelve descriptors on `Sandbox`, and **`e2b` itself is untouched**: importing
|
|
79
|
+
`impello` does not retarget a plain `e2b` call in the same process, and it
|
|
80
|
+
never writes to `os.environ`. A test holds that.
|
|
81
|
+
|
|
82
|
+
## This package will be replaced
|
|
83
|
+
|
|
84
|
+
A permanent wrapper is the worst outcome. `pip show impello` prints `e2b`, and
|
|
85
|
+
the client sends a `publisher: e2b` header to our own servers. So the wrapper
|
|
86
|
+
ships now and the full clone lands before we take payments, with
|
|
87
|
+
`e2b/api/client/` regenerated from our own spec.
|
|
88
|
+
|
|
89
|
+
See `docs/decisions/sdk-self-contained.md`.
|
|
90
|
+
|
|
91
|
+
## Develop
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
python -m venv .venv
|
|
95
|
+
./.venv/bin/pip install -e . pytest
|
|
96
|
+
./.venv/bin/python -m pytest
|
|
97
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "impello"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Python SDK for Impello sandboxes."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
keywords = ["impello", "sandbox", "microvm", "agents", "code-interpreter"]
|
|
13
|
+
dependencies = [
|
|
14
|
+
# Pinned to a major. The next major may move `_resolve_api_params`, which is
|
|
15
|
+
# the one seam this package is built on. See src/impello/_defaults.py.
|
|
16
|
+
"e2b>=2.46.0,<3",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Homepage = "https://impello.ai"
|
|
21
|
+
Repository = "https://github.com/21-Dreams/impello"
|
|
22
|
+
|
|
23
|
+
[dependency-groups]
|
|
24
|
+
dev = ["pytest>=8"]
|
|
25
|
+
|
|
26
|
+
[tool.hatch.build.targets.wheel]
|
|
27
|
+
packages = ["src/impello"]
|
|
28
|
+
|
|
29
|
+
[tool.pytest.ini_options]
|
|
30
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""The Python SDK for Impello sandboxes.
|
|
2
|
+
|
|
3
|
+
Impello runs its own build of E2B's open-source ``infra``, so the sandbox API
|
|
4
|
+
is the same API. Unlike the TypeScript client, the Python one needs no changes
|
|
5
|
+
to work against it: there is no ``e2b_`` key pattern anywhere in ``e2b``, so it
|
|
6
|
+
takes an ``imp_`` key today. All this package has to supply is where to send
|
|
7
|
+
the request.
|
|
8
|
+
|
|
9
|
+
So this is a wrapper, and it is deliberately thin. It will be replaced by a
|
|
10
|
+
full clone before we take payments, for the same reason the TypeScript client
|
|
11
|
+
is one: ``pip show impello`` printing ``e2b`` is a poor thing for a paid
|
|
12
|
+
product to say about itself, and the client sends a ``publisher: e2b`` header
|
|
13
|
+
to our own servers. See ``docs/decisions/sdk-self-contained.md``.
|
|
14
|
+
|
|
15
|
+
Usage is E2B's, unchanged::
|
|
16
|
+
|
|
17
|
+
from impello import Sandbox
|
|
18
|
+
|
|
19
|
+
sandbox = Sandbox.create()
|
|
20
|
+
result = sandbox.commands.run("echo hello")
|
|
21
|
+
sandbox.pause()
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import e2b as _e2b
|
|
25
|
+
from e2b import * # noqa: F401,F403
|
|
26
|
+
from e2b.connection_config import ApiParams, merge_api_params
|
|
27
|
+
|
|
28
|
+
from ._defaults import DEFAULT_DOMAIN, defaults
|
|
29
|
+
|
|
30
|
+
__version__ = "0.2.0"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _ImpelloDefaults:
|
|
34
|
+
"""Bind Impello's settings underneath every call.
|
|
35
|
+
|
|
36
|
+
``ClientFactory._resolve_api_params`` is E2B's own seam for this: every
|
|
37
|
+
class method funnels through it, and ``merge_api_params`` already gives
|
|
38
|
+
per-call params precedence over bound ones. So this overrides one method
|
|
39
|
+
instead of mirroring the twelve descriptors on ``Sandbox``, and it changes
|
|
40
|
+
nothing about ``e2b`` itself. Importing this package does not retarget a
|
|
41
|
+
plain ``e2b`` call in the same process, and it does not touch ``os.environ``.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def _resolve_api_params(cls, **opts: "ApiParams") -> "ApiParams":
|
|
46
|
+
return merge_api_params(defaults(), super()._resolve_api_params(**opts))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Sandbox(_ImpelloDefaults, _e2b.Sandbox):
|
|
50
|
+
"""An Impello sandbox."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class AsyncSandbox(_ImpelloDefaults, _e2b.AsyncSandbox):
|
|
54
|
+
"""An Impello sandbox, async."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Impello(_e2b.E2B):
|
|
58
|
+
"""A client with its connection settings bound explicitly.
|
|
59
|
+
|
|
60
|
+
Use this instead of the module-level ``Sandbox`` when one process talks to
|
|
61
|
+
more than one account or more than one fleet. Clients are isolated from
|
|
62
|
+
each other. Params passed here beat the environment; params passed to a
|
|
63
|
+
single call beat both.
|
|
64
|
+
|
|
65
|
+
Example::
|
|
66
|
+
|
|
67
|
+
from impello import Impello
|
|
68
|
+
|
|
69
|
+
client = Impello(api_key="imp_...")
|
|
70
|
+
sandbox = client.Sandbox.create()
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, **opts: "ApiParams"):
|
|
74
|
+
super().__init__(**merge_api_params(defaults(), opts))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
__all__ = [
|
|
78
|
+
*[name for name in dir(_e2b) if not name.startswith("_")],
|
|
79
|
+
"Sandbox",
|
|
80
|
+
"AsyncSandbox",
|
|
81
|
+
"Impello",
|
|
82
|
+
"DEFAULT_DOMAIN",
|
|
83
|
+
"__version__",
|
|
84
|
+
]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Where an Impello setting comes from, and in what order.
|
|
2
|
+
|
|
3
|
+
Every setting reads an ``IMPELLO_`` name first, then the matching ``E2B_``
|
|
4
|
+
name. The fallback is the migration, not politeness: callers already export
|
|
5
|
+
``E2B_API_KEY``, ``E2B_API_URL`` and ``E2B_DOMAIN``, and the flag day that
|
|
6
|
+
changes the key prefix is hard enough without renaming every environment on the
|
|
7
|
+
same afternoon. Rename when convenient. Nothing breaks if you never do.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from e2b.connection_config import ApiParams
|
|
14
|
+
|
|
15
|
+
DEFAULT_DOMAIN = "sandbox.impello.ai"
|
|
16
|
+
|
|
17
|
+
# Each entry is one setting and the ``ApiParams`` key it fills.
|
|
18
|
+
_SETTINGS = (
|
|
19
|
+
("API_KEY", "api_key"),
|
|
20
|
+
("API_URL", "api_url"),
|
|
21
|
+
("DOMAIN", "domain"),
|
|
22
|
+
("SANDBOX_URL", "sandbox_url"),
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _read(name: str) -> Optional[str]:
|
|
27
|
+
"""Read one setting, Impello's name first."""
|
|
28
|
+
return os.getenv(f"IMPELLO_{name}") or os.getenv(f"E2B_{name}")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def defaults() -> ApiParams:
|
|
32
|
+
"""The params bound to every Impello call, resolved fresh each time.
|
|
33
|
+
|
|
34
|
+
Read at call time rather than at import, so a test or a caller that sets an
|
|
35
|
+
environment variable after importing this package still gets it.
|
|
36
|
+
|
|
37
|
+
A setting that is absent is left out of the result. It must not appear as
|
|
38
|
+
``None``, because ``merge_api_params`` drops a ``None`` from the per-call
|
|
39
|
+
params but keeps one in the bound params, where it would clear the value
|
|
40
|
+
the SDK would otherwise resolve for itself.
|
|
41
|
+
"""
|
|
42
|
+
params = {}
|
|
43
|
+
|
|
44
|
+
for name, key in _SETTINGS:
|
|
45
|
+
value = _read(name)
|
|
46
|
+
if value:
|
|
47
|
+
params[key] = value
|
|
48
|
+
|
|
49
|
+
debug = _read("DEBUG")
|
|
50
|
+
if debug:
|
|
51
|
+
params["debug"] = debug.lower() == "true"
|
|
52
|
+
|
|
53
|
+
# Only the domain has a default. Without one the SDK falls back to E2B's
|
|
54
|
+
# own `e2b.app`, and the failure is a connection to somebody else's fleet.
|
|
55
|
+
params.setdefault("domain", DEFAULT_DOMAIN)
|
|
56
|
+
|
|
57
|
+
return params
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""How the settings reach a call, and what must still beat them."""
|
|
2
|
+
|
|
3
|
+
import e2b
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
import impello
|
|
7
|
+
from impello._defaults import DEFAULT_DOMAIN
|
|
8
|
+
|
|
9
|
+
API_KEY = "imp_" + "0" * 40
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture(autouse=True)
|
|
13
|
+
def clean_env(monkeypatch):
|
|
14
|
+
for name in ("API_KEY", "API_URL", "DOMAIN", "SANDBOX_URL", "DEBUG"):
|
|
15
|
+
monkeypatch.delenv(f"IMPELLO_{name}", raising=False)
|
|
16
|
+
monkeypatch.delenv(f"E2B_{name}", raising=False)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_a_call_is_pointed_at_impello():
|
|
20
|
+
assert impello.Sandbox._resolve_api_params()["domain"] == DEFAULT_DOMAIN
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_the_async_sandbox_is_pointed_there_too():
|
|
24
|
+
assert impello.AsyncSandbox._resolve_api_params()["domain"] == DEFAULT_DOMAIN
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_a_per_call_param_still_wins():
|
|
28
|
+
"""E2B's own precedence rule. Breaking it would make a self-hosted or
|
|
29
|
+
second-account call unreachable."""
|
|
30
|
+
resolved = impello.Sandbox._resolve_api_params(domain="elsewhere.test")
|
|
31
|
+
assert resolved["domain"] == "elsewhere.test"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_a_per_call_none_does_not_clear_the_default():
|
|
35
|
+
resolved = impello.Sandbox._resolve_api_params(domain=None)
|
|
36
|
+
assert resolved["domain"] == DEFAULT_DOMAIN
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_the_environment_reaches_the_call(monkeypatch):
|
|
40
|
+
monkeypatch.setenv("IMPELLO_API_KEY", API_KEY)
|
|
41
|
+
assert impello.Sandbox._resolve_api_params()["api_key"] == API_KEY
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_plain_e2b_is_left_alone():
|
|
45
|
+
"""Importing this package must not retarget somebody else's e2b call in
|
|
46
|
+
the same process. That is why this binds a subclass instead of patching
|
|
47
|
+
ConnectionConfig or writing to os.environ."""
|
|
48
|
+
assert e2b.Sandbox._resolve_api_params() == {}
|
|
49
|
+
assert e2b.connection_config.ConnectionConfig().domain == "e2b.app"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_an_impello_client_binds_the_defaults():
|
|
53
|
+
client = impello.Impello(api_key=API_KEY)
|
|
54
|
+
assert client.Sandbox._resolve_api_params()["domain"] == DEFAULT_DOMAIN
|
|
55
|
+
assert client.Sandbox._resolve_api_params()["api_key"] == API_KEY
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_an_impello_client_can_be_pointed_elsewhere():
|
|
59
|
+
client = impello.Impello(api_key=API_KEY, domain="elsewhere.test")
|
|
60
|
+
assert client.Sandbox._resolve_api_params()["domain"] == "elsewhere.test"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_the_sandbox_is_still_an_e2b_sandbox():
|
|
64
|
+
"""The wrapper adds settings. It must not fork behaviour."""
|
|
65
|
+
assert issubclass(impello.Sandbox, e2b.Sandbox)
|
|
66
|
+
assert issubclass(impello.AsyncSandbox, e2b.AsyncSandbox)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_the_surface_is_re_exported():
|
|
70
|
+
for name in ("SandboxException", "SandboxNotFoundException", "SandboxQuery"):
|
|
71
|
+
assert getattr(impello, name) is getattr(e2b, name)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_the_resolved_params_build_the_impello_address():
|
|
75
|
+
"""The end of the chain. Everything above is plumbing; this is the value
|
|
76
|
+
that decides which fleet the request reaches."""
|
|
77
|
+
from e2b.connection_config import ConnectionConfig
|
|
78
|
+
|
|
79
|
+
config = ConnectionConfig(**impello.Sandbox._resolve_api_params())
|
|
80
|
+
|
|
81
|
+
assert config.api_url == "https://api.sandbox.impello.ai"
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""What a setting resolves to, and what it must never resolve to."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from impello._defaults import DEFAULT_DOMAIN, defaults
|
|
6
|
+
|
|
7
|
+
SETTINGS = [
|
|
8
|
+
("API_KEY", "api_key"),
|
|
9
|
+
("API_URL", "api_url"),
|
|
10
|
+
("DOMAIN", "domain"),
|
|
11
|
+
("SANDBOX_URL", "sandbox_url"),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture(autouse=True)
|
|
16
|
+
def clean_env(monkeypatch):
|
|
17
|
+
"""Neither prefix is set unless a test sets it."""
|
|
18
|
+
for name, _ in SETTINGS + [("DEBUG", "debug")]:
|
|
19
|
+
monkeypatch.delenv(f"IMPELLO_{name}", raising=False)
|
|
20
|
+
monkeypatch.delenv(f"E2B_{name}", raising=False)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_the_domain_defaults_to_impello():
|
|
24
|
+
"""Without this the SDK falls back to E2B's own `e2b.app`, and the failure
|
|
25
|
+
is a connection to somebody else's fleet."""
|
|
26
|
+
assert defaults()["domain"] == DEFAULT_DOMAIN
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_nothing_else_is_invented():
|
|
30
|
+
"""Only the domain has a default. Every other key must be absent, not
|
|
31
|
+
None: merge_api_params keeps a None in the bound params, where it would
|
|
32
|
+
clear a value the SDK would otherwise resolve for itself."""
|
|
33
|
+
assert set(defaults()) == {"domain"}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.mark.parametrize("name,key", SETTINGS)
|
|
37
|
+
def test_impello_name_is_read(monkeypatch, name, key):
|
|
38
|
+
monkeypatch.setenv(f"IMPELLO_{name}", "from-impello")
|
|
39
|
+
assert defaults()[key] == "from-impello"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.mark.parametrize("name,key", SETTINGS)
|
|
43
|
+
def test_e2b_name_is_the_fallback(monkeypatch, name, key):
|
|
44
|
+
"""Callers already export the E2B names. Renaming every environment on
|
|
45
|
+
flag day is not a thing this package asks for."""
|
|
46
|
+
monkeypatch.setenv(f"E2B_{name}", "from-e2b")
|
|
47
|
+
assert defaults()[key] == "from-e2b"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.mark.parametrize("name,key", SETTINGS)
|
|
51
|
+
def test_impello_name_wins_when_both_are_set(monkeypatch, name, key):
|
|
52
|
+
monkeypatch.setenv(f"IMPELLO_{name}", "from-impello")
|
|
53
|
+
monkeypatch.setenv(f"E2B_{name}", "from-e2b")
|
|
54
|
+
assert defaults()[key] == "from-impello"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pytest.mark.parametrize("name,key", SETTINGS)
|
|
58
|
+
def test_an_empty_value_is_not_a_value(monkeypatch, name, key):
|
|
59
|
+
monkeypatch.setenv(f"IMPELLO_{name}", "")
|
|
60
|
+
monkeypatch.setenv(f"E2B_{name}", "from-e2b")
|
|
61
|
+
assert defaults()[key] == "from-e2b"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_debug_is_read_as_a_flag(monkeypatch):
|
|
65
|
+
monkeypatch.setenv("IMPELLO_DEBUG", "TRUE")
|
|
66
|
+
assert defaults()["debug"] is True
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_debug_is_false_when_it_says_anything_else(monkeypatch):
|
|
70
|
+
monkeypatch.setenv("IMPELLO_DEBUG", "yes")
|
|
71
|
+
assert defaults()["debug"] is False
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_settings_are_read_at_call_time_not_import_time(monkeypatch):
|
|
75
|
+
"""A caller that configures itself after importing still gets it."""
|
|
76
|
+
assert "api_key" not in defaults()
|
|
77
|
+
monkeypatch.setenv("IMPELLO_API_KEY", "imp_" + "0" * 40)
|
|
78
|
+
assert defaults()["api_key"] == "imp_" + "0" * 40
|