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/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()]