stackbox 0.1.0__py3-none-any.whl
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.
- stackbox/__init__.py +9 -0
- stackbox/__main__.py +3 -0
- stackbox/app.py +405 -0
- stackbox/backends/__init__.py +107 -0
- stackbox/backends/base.py +181 -0
- stackbox/backends/cloudflare.py +241 -0
- stackbox/backends/daytona.py +306 -0
- stackbox/backends/e2b.py +241 -0
- stackbox/backends/hosted.py +536 -0
- stackbox/backends/modal.py +190 -0
- stackbox/backends/opensandbox.py +148 -0
- stackbox/backends/vercel.py +309 -0
- stackbox/catalog.py +156 -0
- stackbox/cli.py +873 -0
- stackbox/config.py +294 -0
- stackbox/execd.py +118 -0
- stackbox/ledger.py +387 -0
- stackbox/router.py +1049 -0
- stackbox/vcr.py +113 -0
- stackbox-0.1.0.dist-info/METADATA +288 -0
- stackbox-0.1.0.dist-info/RECORD +23 -0
- stackbox-0.1.0.dist-info/WHEEL +4 -0
- stackbox-0.1.0.dist-info/entry_points.txt +3 -0
stackbox/catalog.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""What the gateway knows about each provider out of the box.
|
|
2
|
+
|
|
3
|
+
Defaults (list prices, plan limits, sizes) let a backend be declared with just its kind,
|
|
4
|
+
or auto-detected from credentials in the environment. Everything here can be overridden
|
|
5
|
+
per backend in the config file. Prices are list prices checked 2026-11; see the
|
|
6
|
+
``pricing`` links.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import importlib.util
|
|
12
|
+
import os
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class Provider:
|
|
20
|
+
kind: str
|
|
21
|
+
label: str
|
|
22
|
+
credentials: tuple[tuple[str, ...], ...] # any one group of env vars, all set
|
|
23
|
+
extra: str | None # `uv sync --extra <extra>` / pip install 'stackbox[<extra>]'
|
|
24
|
+
module: str | None # import that proves the extra is installed
|
|
25
|
+
defaults: dict[str, Any] = field(default_factory=dict)
|
|
26
|
+
signup: str = ""
|
|
27
|
+
pricing: str = ""
|
|
28
|
+
notes: str = ""
|
|
29
|
+
|
|
30
|
+
def detected(self) -> bool:
|
|
31
|
+
if self.kind == "modal" and (Path.home() / ".modal.toml").is_file():
|
|
32
|
+
return True
|
|
33
|
+
return any(all(os.environ.get(v) for v in group) for group in self.credentials)
|
|
34
|
+
|
|
35
|
+
def missing_credentials(self) -> str:
|
|
36
|
+
return " + ".join(self.credentials[0]) if self.credentials else ""
|
|
37
|
+
|
|
38
|
+
def installed(self) -> bool:
|
|
39
|
+
return self.module is None or importlib.util.find_spec(self.module) is not None
|
|
40
|
+
|
|
41
|
+
def price_1vcpu_2gib(self) -> float:
|
|
42
|
+
d = self.defaults
|
|
43
|
+
return d.get("cost_per_vcpu_hour", 0.0) + 2 * d.get("cost_per_gib_hour", 0.0)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
PROVIDERS: dict[str, Provider] = {
|
|
47
|
+
p.kind: p
|
|
48
|
+
for p in (
|
|
49
|
+
Provider(
|
|
50
|
+
kind="e2b",
|
|
51
|
+
label="E2B",
|
|
52
|
+
credentials=(("E2B_API_KEY",),),
|
|
53
|
+
extra="e2b",
|
|
54
|
+
module="e2b",
|
|
55
|
+
defaults={
|
|
56
|
+
"max_active": 20,
|
|
57
|
+
"max_timeout_s": 3600,
|
|
58
|
+
"cost_per_vcpu_hour": 0.0504,
|
|
59
|
+
"cost_per_gib_hour": 0.0162,
|
|
60
|
+
"startup_prior_s": 1.5,
|
|
61
|
+
},
|
|
62
|
+
signup="https://e2b.dev/dashboard?tab=keys",
|
|
63
|
+
pricing="https://e2b.dev/docs/pricing",
|
|
64
|
+
notes="Firecracker microVMs; images become templates (built once, ~45 s)",
|
|
65
|
+
),
|
|
66
|
+
Provider(
|
|
67
|
+
kind="modal",
|
|
68
|
+
label="Modal",
|
|
69
|
+
credentials=(("MODAL_TOKEN_ID", "MODAL_TOKEN_SECRET"),),
|
|
70
|
+
extra="modal",
|
|
71
|
+
module="modal",
|
|
72
|
+
defaults={
|
|
73
|
+
"max_active": 20,
|
|
74
|
+
"max_timeout_s": 86400,
|
|
75
|
+
"cost_per_vcpu_hour": 0.0710,
|
|
76
|
+
"cost_per_gib_hour": 0.0242,
|
|
77
|
+
"startup_prior_s": 1.0,
|
|
78
|
+
"cold_start_prior_s": 15.0,
|
|
79
|
+
},
|
|
80
|
+
signup="https://modal.com/settings/tokens",
|
|
81
|
+
pricing="https://modal.com/pricing",
|
|
82
|
+
notes="gVisor; any registry image; or `modal token set` (~/.modal.toml)",
|
|
83
|
+
),
|
|
84
|
+
Provider(
|
|
85
|
+
kind="daytona",
|
|
86
|
+
label="Daytona",
|
|
87
|
+
credentials=(("DAYTONA_API_KEY",),),
|
|
88
|
+
extra="daytona",
|
|
89
|
+
module="daytona",
|
|
90
|
+
defaults={
|
|
91
|
+
"max_active": 20,
|
|
92
|
+
"max_timeout_s": 3600,
|
|
93
|
+
"cost_per_vcpu_hour": 0.0504,
|
|
94
|
+
"cost_per_gib_hour": 0.0162,
|
|
95
|
+
"startup_prior_s": 3.0,
|
|
96
|
+
"cold_start_prior_s": 20.0,
|
|
97
|
+
},
|
|
98
|
+
signup="https://app.daytona.io/dashboard/keys",
|
|
99
|
+
pricing="https://www.daytona.io/pricing",
|
|
100
|
+
notes="any registry image; account memory quota is discovered automatically",
|
|
101
|
+
),
|
|
102
|
+
Provider(
|
|
103
|
+
kind="vercel",
|
|
104
|
+
label="Vercel Sandbox",
|
|
105
|
+
credentials=(
|
|
106
|
+
("VERCEL_TOKEN", "VERCEL_TEAM_ID", "VERCEL_PROJECT_ID"),
|
|
107
|
+
("VERCEL_OIDC_TOKEN",),
|
|
108
|
+
),
|
|
109
|
+
extra="vercel",
|
|
110
|
+
module="vercel.sandbox",
|
|
111
|
+
defaults={
|
|
112
|
+
"max_active": 10,
|
|
113
|
+
"max_timeout_s": 2700,
|
|
114
|
+
"cost_per_vcpu_hour": 0.128,
|
|
115
|
+
"cost_per_gib_hour": 0.0212,
|
|
116
|
+
"cpu_billed_fraction": 0.5,
|
|
117
|
+
"startup_prior_s": 1.0,
|
|
118
|
+
},
|
|
119
|
+
signup="https://vercel.com/account/tokens",
|
|
120
|
+
pricing="https://vercel.com/docs/sandbox/pricing",
|
|
121
|
+
notes="bills active CPU only; images are pushed to Vercel's registry on prepare (needs Docker)",
|
|
122
|
+
),
|
|
123
|
+
Provider(
|
|
124
|
+
kind="cloudflare",
|
|
125
|
+
label="Cloudflare Containers",
|
|
126
|
+
credentials=(
|
|
127
|
+
("CLOUDFLARE_BRIDGE_URL", "CLOUDFLARE_BRIDGE_TOKEN"),
|
|
128
|
+
("CLOUDFLARE_BRIDGE_URL", "BRIDGE_TOKEN"),
|
|
129
|
+
),
|
|
130
|
+
extra=None,
|
|
131
|
+
module=None,
|
|
132
|
+
defaults={
|
|
133
|
+
"url": "env:CLOUDFLARE_BRIDGE_URL",
|
|
134
|
+
"api_key": "env:CLOUDFLARE_BRIDGE_TOKEN|BRIDGE_TOKEN",
|
|
135
|
+
"max_active": 20,
|
|
136
|
+
"max_timeout_s": 86400,
|
|
137
|
+
"cost_per_vcpu_hour": 0.072,
|
|
138
|
+
"cost_per_gib_hour": 0.009,
|
|
139
|
+
"cpu_billed_fraction": 0.5,
|
|
140
|
+
"startup_prior_s": 1.5,
|
|
141
|
+
},
|
|
142
|
+
signup="https://dash.cloudflare.com/profile/api-tokens",
|
|
143
|
+
pricing="https://developers.cloudflare.com/containers/pricing/",
|
|
144
|
+
notes="via a bridge Worker you deploy once (bridges/cloudflare); bills active CPU only",
|
|
145
|
+
),
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def defaults_for(kind: str) -> dict[str, Any]:
|
|
151
|
+
p = PROVIDERS.get(kind)
|
|
152
|
+
return dict(p.defaults) if p else {}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def detected() -> list[Provider]:
|
|
156
|
+
return [p for p in PROVIDERS.values() if p.detected()]
|