buildfunctions 0.1.0__py3-none-any.whl → 0.2.1__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.
- buildfunctions/__init__.py +139 -1
- buildfunctions/client.py +282 -0
- buildfunctions/cpu_function.py +167 -0
- buildfunctions/cpu_sandbox.py +393 -0
- buildfunctions/dotdict.py +39 -0
- buildfunctions/errors.py +90 -0
- buildfunctions/framework.py +22 -0
- buildfunctions/gpu_function.py +241 -0
- buildfunctions/gpu_sandbox.py +443 -0
- buildfunctions/http_client.py +97 -0
- buildfunctions/memory.py +28 -0
- buildfunctions/py.typed +0 -0
- buildfunctions/resolve_code.py +109 -0
- buildfunctions/types.py +227 -0
- buildfunctions/uploader.py +198 -0
- buildfunctions-0.2.1.dist-info/METADATA +176 -0
- buildfunctions-0.2.1.dist-info/RECORD +18 -0
- {buildfunctions-0.1.0.dist-info → buildfunctions-0.2.1.dist-info}/WHEEL +1 -2
- buildfunctions/api.py +0 -2
- buildfunctions-0.1.0.dist-info/METADATA +0 -6
- buildfunctions-0.1.0.dist-info/RECORD +0 -6
- buildfunctions-0.1.0.dist-info/top_level.txt +0 -1
buildfunctions/__init__.py
CHANGED
|
@@ -1 +1,139 @@
|
|
|
1
|
-
|
|
1
|
+
"""Buildfunctions SDK - Python SDK for the serverless platform for AI agents.
|
|
2
|
+
|
|
3
|
+
Example:
|
|
4
|
+
import asyncio
|
|
5
|
+
from buildfunctions import Buildfunctions, CPUSandbox, GPUSandbox, GPUFunction
|
|
6
|
+
|
|
7
|
+
async def main():
|
|
8
|
+
# Initialize the client (authenticates with the API)
|
|
9
|
+
client = await Buildfunctions({
|
|
10
|
+
"apiToken": os.environ["BUILDFUNCTIONS_API_TOKEN"]
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
# Access authenticated user info (supports both dot and bracket notation)
|
|
14
|
+
print(client.user.username)
|
|
15
|
+
print(client.authenticatedAt)
|
|
16
|
+
|
|
17
|
+
# Create a CPU sandbox
|
|
18
|
+
sandbox = await CPUSandbox.create({
|
|
19
|
+
"name": "my-sandbox",
|
|
20
|
+
"language": "python",
|
|
21
|
+
"memory": "512MB",
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
# Run code
|
|
25
|
+
result = await sandbox.run()
|
|
26
|
+
print(result.response)
|
|
27
|
+
|
|
28
|
+
# Clean up
|
|
29
|
+
await sandbox.delete()
|
|
30
|
+
|
|
31
|
+
asyncio.run(main())
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
# Client exports - match TypeScript SDK naming exactly
|
|
35
|
+
from buildfunctions.client import (
|
|
36
|
+
Buildfunctions,
|
|
37
|
+
buildfunctions,
|
|
38
|
+
createClient,
|
|
39
|
+
create_client,
|
|
40
|
+
init,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Function builders - match TypeScript SDK naming exactly
|
|
44
|
+
from buildfunctions.cpu_function import CPUFunction, create_cpu_function
|
|
45
|
+
from buildfunctions.gpu_function import GPUFunction, create_gpu_function
|
|
46
|
+
|
|
47
|
+
# Sandbox factories - match TypeScript SDK naming exactly
|
|
48
|
+
from buildfunctions.cpu_sandbox import CPUSandbox, create_cpu_sandbox
|
|
49
|
+
from buildfunctions.gpu_sandbox import GPUSandbox, create_gpu_sandbox
|
|
50
|
+
|
|
51
|
+
# Errors
|
|
52
|
+
from buildfunctions.errors import (
|
|
53
|
+
AuthenticationError,
|
|
54
|
+
BuildfunctionsError,
|
|
55
|
+
CapacityError,
|
|
56
|
+
NotFoundError,
|
|
57
|
+
ValidationError,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Types
|
|
61
|
+
from buildfunctions.types import (
|
|
62
|
+
AuthenticatedUser,
|
|
63
|
+
AuthResponse,
|
|
64
|
+
BuildfunctionsConfig,
|
|
65
|
+
CPUFunctionOptions,
|
|
66
|
+
CPUSandboxConfig,
|
|
67
|
+
CPUSandboxInstance,
|
|
68
|
+
CreateFunctionOptions,
|
|
69
|
+
DeployedFunction,
|
|
70
|
+
ErrorCode,
|
|
71
|
+
FileMetadata,
|
|
72
|
+
FindUniqueOptions,
|
|
73
|
+
Framework,
|
|
74
|
+
FunctionConfig,
|
|
75
|
+
GPUFunctionOptions,
|
|
76
|
+
GPUSandboxConfig,
|
|
77
|
+
GPUSandboxInstance,
|
|
78
|
+
GPUType,
|
|
79
|
+
Language,
|
|
80
|
+
ListOptions,
|
|
81
|
+
Memory,
|
|
82
|
+
RunResult,
|
|
83
|
+
Runtime,
|
|
84
|
+
SandboxInstance,
|
|
85
|
+
UploadOptions,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
__all__ = [
|
|
89
|
+
# Client (PascalCase - matches TypeScript)
|
|
90
|
+
"Buildfunctions",
|
|
91
|
+
"createClient",
|
|
92
|
+
"init",
|
|
93
|
+
# Client (snake_case aliases)
|
|
94
|
+
"buildfunctions",
|
|
95
|
+
"create_client",
|
|
96
|
+
# Function builders (PascalCase - matches TypeScript)
|
|
97
|
+
"CPUFunction",
|
|
98
|
+
"GPUFunction",
|
|
99
|
+
# Function builders (snake_case aliases)
|
|
100
|
+
"create_cpu_function",
|
|
101
|
+
"create_gpu_function",
|
|
102
|
+
# Sandbox factories (PascalCase - matches TypeScript)
|
|
103
|
+
"CPUSandbox",
|
|
104
|
+
"GPUSandbox",
|
|
105
|
+
# Sandbox factories (snake_case aliases)
|
|
106
|
+
"create_cpu_sandbox",
|
|
107
|
+
"create_gpu_sandbox",
|
|
108
|
+
# Errors
|
|
109
|
+
"BuildfunctionsError",
|
|
110
|
+
"AuthenticationError",
|
|
111
|
+
"NotFoundError",
|
|
112
|
+
"ValidationError",
|
|
113
|
+
"CapacityError",
|
|
114
|
+
# Types
|
|
115
|
+
"BuildfunctionsConfig",
|
|
116
|
+
"AuthenticatedUser",
|
|
117
|
+
"AuthResponse",
|
|
118
|
+
"Language",
|
|
119
|
+
"Runtime",
|
|
120
|
+
"GPUType",
|
|
121
|
+
"Framework",
|
|
122
|
+
"Memory",
|
|
123
|
+
"FunctionConfig",
|
|
124
|
+
"CPUFunctionOptions",
|
|
125
|
+
"GPUFunctionOptions",
|
|
126
|
+
"CreateFunctionOptions",
|
|
127
|
+
"DeployedFunction",
|
|
128
|
+
"CPUSandboxConfig",
|
|
129
|
+
"GPUSandboxConfig",
|
|
130
|
+
"RunResult",
|
|
131
|
+
"UploadOptions",
|
|
132
|
+
"SandboxInstance",
|
|
133
|
+
"CPUSandboxInstance",
|
|
134
|
+
"GPUSandboxInstance",
|
|
135
|
+
"FindUniqueOptions",
|
|
136
|
+
"ListOptions",
|
|
137
|
+
"ErrorCode",
|
|
138
|
+
"FileMetadata",
|
|
139
|
+
]
|
buildfunctions/client.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""Buildfunctions SDK Client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from buildfunctions.cpu_function import set_api_token
|
|
10
|
+
from buildfunctions.cpu_sandbox import set_cpu_sandbox_api_token
|
|
11
|
+
from buildfunctions.dotdict import DotDict
|
|
12
|
+
from buildfunctions.errors import NotFoundError
|
|
13
|
+
from buildfunctions.framework import detect_framework
|
|
14
|
+
from buildfunctions.gpu_function import GPUFunction, set_gpu_api_token
|
|
15
|
+
from buildfunctions.gpu_sandbox import set_gpu_sandbox_api_token
|
|
16
|
+
from buildfunctions.http_client import create_http_client
|
|
17
|
+
from buildfunctions.memory import parse_memory
|
|
18
|
+
from buildfunctions.resolve_code import get_caller_file, resolve_code
|
|
19
|
+
from buildfunctions.types import (
|
|
20
|
+
AuthResponse,
|
|
21
|
+
BuildfunctionsConfig,
|
|
22
|
+
CreateFunctionOptions,
|
|
23
|
+
DeployedFunction,
|
|
24
|
+
FindUniqueOptions,
|
|
25
|
+
ListOptions,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
DEFAULT_BASE_URL = "https://www.buildfunctions.com"
|
|
29
|
+
DEFAULT_GPU_BUILD_URL = "https://prod-gpu-build.buildfunctions.link"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _format_requirements(requirements: str | list[str] | None) -> str:
|
|
33
|
+
if not requirements:
|
|
34
|
+
return ""
|
|
35
|
+
if isinstance(requirements, list):
|
|
36
|
+
return "\n".join(requirements)
|
|
37
|
+
return requirements
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _get_default_runtime(language: str) -> str:
|
|
41
|
+
if language == "javascript":
|
|
42
|
+
raise ValueError('JavaScript requires explicit runtime: "nodejs" or "deno"')
|
|
43
|
+
return language
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _get_file_extension(language: str) -> str:
|
|
47
|
+
match language:
|
|
48
|
+
case "javascript":
|
|
49
|
+
return ".js"
|
|
50
|
+
case "typescript":
|
|
51
|
+
return ".ts"
|
|
52
|
+
case "python":
|
|
53
|
+
return ".py"
|
|
54
|
+
case "go":
|
|
55
|
+
return ".go"
|
|
56
|
+
case "shell":
|
|
57
|
+
return ".sh"
|
|
58
|
+
case _:
|
|
59
|
+
return ".js"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _create_functions_manager(http: dict[str, Any]) -> DotDict:
|
|
63
|
+
"""Create a functions manager with list/findUnique/get/create/delete methods."""
|
|
64
|
+
|
|
65
|
+
def _wrap_function(fn: dict[str, Any]) -> DotDict:
|
|
66
|
+
async def delete_fn() -> None:
|
|
67
|
+
await http["delete"]("/api/sdk/functions/build", {"siteId": fn["id"]})
|
|
68
|
+
|
|
69
|
+
return DotDict({**fn, "delete": delete_fn})
|
|
70
|
+
|
|
71
|
+
async def list_fn(options: ListOptions | None = None) -> list[DotDict]:
|
|
72
|
+
page = (options or {}).get("page", 1)
|
|
73
|
+
response = await http["get"]("/api/sdk/functions", {"page": page})
|
|
74
|
+
return [_wrap_function(fn) for fn in response["stringifiedQueryResults"]]
|
|
75
|
+
|
|
76
|
+
async def find_unique(options: FindUniqueOptions) -> DotDict | None:
|
|
77
|
+
where = options.get("where", options) if isinstance(options, dict) else options
|
|
78
|
+
|
|
79
|
+
if where.get("id"):
|
|
80
|
+
try:
|
|
81
|
+
fn = await http["get"]("/api/sdk/functions/build", {"siteId": where["id"]})
|
|
82
|
+
return _wrap_function(fn)
|
|
83
|
+
except NotFoundError:
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
if where.get("name"):
|
|
87
|
+
functions = await list_fn()
|
|
88
|
+
for fn in functions:
|
|
89
|
+
if fn.get("name") == where["name"]:
|
|
90
|
+
return fn
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
async def get(site_id: str) -> DotDict:
|
|
96
|
+
fn = await http["get"]("/api/sdk/functions/build", {"siteId": site_id})
|
|
97
|
+
return _wrap_function(fn)
|
|
98
|
+
|
|
99
|
+
async def create(options: CreateFunctionOptions) -> DotDict:
|
|
100
|
+
# Get the caller's file location to resolve relative paths correctly
|
|
101
|
+
caller_file = get_caller_file()
|
|
102
|
+
caller_dir = caller_file.parent if caller_file else None
|
|
103
|
+
|
|
104
|
+
# Resolve code (inline string or file path) relative to the caller's location
|
|
105
|
+
resolved_code = await resolve_code(options["code"], caller_dir)
|
|
106
|
+
|
|
107
|
+
file_ext = _get_file_extension(options["language"])
|
|
108
|
+
name = options["name"].lower()
|
|
109
|
+
is_gpu = options.get("processor_type") == "GPU" or bool(options.get("gpu"))
|
|
110
|
+
runtime = options.get("runtime") or _get_default_runtime(options["language"])
|
|
111
|
+
|
|
112
|
+
if is_gpu:
|
|
113
|
+
requirements = _format_requirements(options.get("requirements"))
|
|
114
|
+
env_variables_list = options.get("env_variables", [])
|
|
115
|
+
env_dict = {v["key"]: v["value"] for v in env_variables_list} if env_variables_list else {}
|
|
116
|
+
|
|
117
|
+
deployed = await GPUFunction.create({
|
|
118
|
+
"name": options["name"],
|
|
119
|
+
"code": resolved_code,
|
|
120
|
+
"language": options["language"],
|
|
121
|
+
"runtime": runtime,
|
|
122
|
+
"gpu": options.get("gpu", "T4"),
|
|
123
|
+
"vcpus": options.get("vcpus"),
|
|
124
|
+
"config": {
|
|
125
|
+
"memory": parse_memory(options["memory"]) if options.get("memory") else 1024,
|
|
126
|
+
"timeout": options.get("timeout", 60),
|
|
127
|
+
},
|
|
128
|
+
"dependencies": requirements,
|
|
129
|
+
"env_variables": env_dict if env_dict else {},
|
|
130
|
+
"cron_schedule": options.get("cron_schedule", ""),
|
|
131
|
+
"framework": options.get("framework") or detect_framework(requirements),
|
|
132
|
+
"model_name": options.get("model_name", ""),
|
|
133
|
+
"model_path": options.get("model_path", ""),
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
if not deployed:
|
|
137
|
+
raise RuntimeError("GPU Function deployment failed")
|
|
138
|
+
return DotDict(deployed) if not isinstance(deployed, DotDict) else deployed
|
|
139
|
+
|
|
140
|
+
# CPU build
|
|
141
|
+
body = {
|
|
142
|
+
"name": name,
|
|
143
|
+
"fileExt": file_ext,
|
|
144
|
+
"sourceWith": resolved_code,
|
|
145
|
+
"sourceWithout": resolved_code,
|
|
146
|
+
"language": options["language"],
|
|
147
|
+
"runtime": runtime,
|
|
148
|
+
"memoryAllocated": parse_memory(options["memory"]) if options.get("memory") else 128,
|
|
149
|
+
"timeout": options.get("timeout", 10),
|
|
150
|
+
"envVariables": json.dumps(options.get("env_variables", [])),
|
|
151
|
+
"requirements": _format_requirements(options.get("requirements")),
|
|
152
|
+
"cronExpression": options.get("cron_schedule", ""),
|
|
153
|
+
"processorType": "CPU",
|
|
154
|
+
"selectedFramework": options.get("framework") or detect_framework(
|
|
155
|
+
_format_requirements(options.get("requirements"))
|
|
156
|
+
),
|
|
157
|
+
"subdomain": name,
|
|
158
|
+
"totalVariables": len(options.get("env_variables", [])),
|
|
159
|
+
"functionCount": 0,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
response = await http["post"]("/api/sdk/functions/build", body)
|
|
163
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
164
|
+
|
|
165
|
+
return _wrap_function({
|
|
166
|
+
"id": response["siteId"],
|
|
167
|
+
"name": name,
|
|
168
|
+
"subdomain": name,
|
|
169
|
+
"endpoint": response["endpoint"],
|
|
170
|
+
"lambdaUrl": response.get("sslCertificateEndpoint", ""),
|
|
171
|
+
"language": options["language"],
|
|
172
|
+
"runtime": runtime,
|
|
173
|
+
"lambdaMemoryAllocated": parse_memory(options["memory"]) if options.get("memory") else 128,
|
|
174
|
+
"timeoutSeconds": options.get("timeout", 10),
|
|
175
|
+
"isGPUF": False,
|
|
176
|
+
"framework": options.get("framework", ""),
|
|
177
|
+
"createdAt": now,
|
|
178
|
+
"updatedAt": now,
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
async def delete_fn(site_id: str) -> None:
|
|
182
|
+
await http["delete"]("/api/sdk/functions/build", {"siteId": site_id})
|
|
183
|
+
|
|
184
|
+
return DotDict({
|
|
185
|
+
"list": list_fn,
|
|
186
|
+
"findUnique": find_unique,
|
|
187
|
+
"find_unique": find_unique,
|
|
188
|
+
"get": get,
|
|
189
|
+
"create": create,
|
|
190
|
+
"delete": delete_fn,
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
async def Buildfunctions(config: BuildfunctionsConfig | None = None) -> DotDict:
|
|
195
|
+
"""Create a Buildfunctions SDK client.
|
|
196
|
+
|
|
197
|
+
Authenticates with the API and returns a client with:
|
|
198
|
+
- functions: Functions manager (list, findUnique, get, create, delete)
|
|
199
|
+
- user: Authenticated user info
|
|
200
|
+
- sessionExpiresAt: Session expiration timestamp
|
|
201
|
+
- authenticatedAt: Authentication timestamp
|
|
202
|
+
- getHttpClient: Returns the underlying HTTP client
|
|
203
|
+
|
|
204
|
+
Supports both dot notation and bracket notation:
|
|
205
|
+
client.user.username OR client["user"]["username"]
|
|
206
|
+
"""
|
|
207
|
+
if config is None:
|
|
208
|
+
import os
|
|
209
|
+
|
|
210
|
+
from dotenv import load_dotenv
|
|
211
|
+
|
|
212
|
+
load_dotenv()
|
|
213
|
+
api_token = os.environ.get("BUILDFUNCTIONS_API_TOKEN", "")
|
|
214
|
+
config = BuildfunctionsConfig(api_token=api_token)
|
|
215
|
+
|
|
216
|
+
api_token = config.get("api_token") or config.get("apiToken", "")
|
|
217
|
+
if not api_token:
|
|
218
|
+
raise ValueError("API token is required")
|
|
219
|
+
|
|
220
|
+
base_url = config.get("base_url") or config.get("baseUrl", DEFAULT_BASE_URL)
|
|
221
|
+
gpu_build_url = config.get("gpu_build_url") or config.get("gpuBuildUrl", DEFAULT_GPU_BUILD_URL)
|
|
222
|
+
|
|
223
|
+
# Don't wrap http in DotDict - it has methods like 'get' that conflict with dict builtins
|
|
224
|
+
http = create_http_client(base_url=base_url, api_token=api_token)
|
|
225
|
+
|
|
226
|
+
auth_response: AuthResponse = await http["post"]("/api/sdk/auth")
|
|
227
|
+
|
|
228
|
+
if not auth_response.get("authenticated"):
|
|
229
|
+
raise RuntimeError("Authentication failed")
|
|
230
|
+
|
|
231
|
+
http["set_token"](auth_response["sessionToken"])
|
|
232
|
+
|
|
233
|
+
user_id = auth_response["user"].get("id", "")
|
|
234
|
+
username = auth_response["user"].get("username") or None
|
|
235
|
+
compute_tier = auth_response["user"].get("compute_tier") or auth_response["user"].get("computeTier") or None
|
|
236
|
+
|
|
237
|
+
set_cpu_sandbox_api_token(auth_response["sessionToken"], base_url)
|
|
238
|
+
set_gpu_sandbox_api_token(auth_response["sessionToken"], gpu_build_url, user_id, username, compute_tier, base_url)
|
|
239
|
+
set_gpu_api_token(auth_response["sessionToken"], gpu_build_url, base_url, user_id, username, compute_tier)
|
|
240
|
+
set_api_token(auth_response["sessionToken"], base_url)
|
|
241
|
+
|
|
242
|
+
functions = _create_functions_manager(http)
|
|
243
|
+
|
|
244
|
+
return DotDict({
|
|
245
|
+
"functions": functions,
|
|
246
|
+
"user": DotDict(auth_response["user"]),
|
|
247
|
+
# Support both naming conventions
|
|
248
|
+
"sessionExpiresAt": auth_response.get("expiresAt"),
|
|
249
|
+
"session_expires_at": auth_response.get("expiresAt"),
|
|
250
|
+
"authenticatedAt": auth_response.get("authenticatedAt"),
|
|
251
|
+
"authenticated_at": auth_response.get("authenticatedAt"),
|
|
252
|
+
"getHttpClient": lambda: http,
|
|
253
|
+
"get_http_client": lambda: http,
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
async def createClient(config: BuildfunctionsConfig | None = None) -> dict[str, Any] | None:
|
|
258
|
+
"""Create a Buildfunctions client, returning None on failure."""
|
|
259
|
+
try:
|
|
260
|
+
return await Buildfunctions(config)
|
|
261
|
+
except Exception:
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# Aliases for snake_case compatibility
|
|
266
|
+
buildfunctions = Buildfunctions
|
|
267
|
+
create_client = createClient
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def init(
|
|
271
|
+
api_token: str,
|
|
272
|
+
base_url: str | None = None,
|
|
273
|
+
gpu_build_url: str | None = None,
|
|
274
|
+
user_id: str | None = None,
|
|
275
|
+
username: str | None = None,
|
|
276
|
+
compute_tier: str | None = None,
|
|
277
|
+
) -> None:
|
|
278
|
+
"""Global initialization - sets tokens across all modules."""
|
|
279
|
+
set_api_token(api_token, base_url)
|
|
280
|
+
set_gpu_api_token(api_token, gpu_build_url, base_url, user_id, username, compute_tier)
|
|
281
|
+
set_cpu_sandbox_api_token(api_token, base_url)
|
|
282
|
+
set_gpu_sandbox_api_token(api_token, gpu_build_url, user_id, username, compute_tier, base_url)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""CPU Function - Deploy serverless functions to Buildfunctions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from buildfunctions.dotdict import DotDict
|
|
11
|
+
from buildfunctions.errors import ValidationError
|
|
12
|
+
from buildfunctions.memory import parse_memory
|
|
13
|
+
from buildfunctions.resolve_code import resolve_code
|
|
14
|
+
from buildfunctions.types import CPUFunctionOptions, DeployedFunction
|
|
15
|
+
|
|
16
|
+
DEFAULT_BASE_URL = "https://www.buildfunctions.com"
|
|
17
|
+
|
|
18
|
+
# Module-level state
|
|
19
|
+
_global_api_token: str | None = None
|
|
20
|
+
_global_base_url: str | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def set_api_token(api_token: str, base_url: str | None = None) -> None:
|
|
24
|
+
"""Set the API token for function deployment."""
|
|
25
|
+
global _global_api_token, _global_base_url
|
|
26
|
+
_global_api_token = api_token
|
|
27
|
+
_global_base_url = base_url
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _get_file_extension(language: str) -> str:
|
|
31
|
+
extensions: dict[str, str] = {
|
|
32
|
+
"javascript": ".js",
|
|
33
|
+
"typescript": ".ts",
|
|
34
|
+
"python": ".py",
|
|
35
|
+
"go": ".go",
|
|
36
|
+
"shell": ".sh",
|
|
37
|
+
}
|
|
38
|
+
return extensions.get(language, ".js")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _get_default_runtime(language: str) -> str:
|
|
42
|
+
if language == "javascript":
|
|
43
|
+
raise ValidationError('JavaScript requires explicit runtime: "nodejs" or "deno"')
|
|
44
|
+
return language
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _format_requirements(requirements: str | list[str] | None) -> str:
|
|
48
|
+
if not requirements:
|
|
49
|
+
return ""
|
|
50
|
+
if isinstance(requirements, list):
|
|
51
|
+
return "\n".join(requirements)
|
|
52
|
+
return requirements
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _validate_options(options: CPUFunctionOptions) -> None:
|
|
56
|
+
name = options.get("name")
|
|
57
|
+
if not name or not isinstance(name, str):
|
|
58
|
+
raise ValidationError("Function name is required")
|
|
59
|
+
|
|
60
|
+
import re
|
|
61
|
+
|
|
62
|
+
if not re.match(r"^[a-z0-9-]+$", name.lower()):
|
|
63
|
+
raise ValidationError("Function name can only contain lowercase letters, numbers, and hyphens")
|
|
64
|
+
|
|
65
|
+
code = options.get("code")
|
|
66
|
+
if not code or not isinstance(code, str):
|
|
67
|
+
raise ValidationError("Function code is required")
|
|
68
|
+
|
|
69
|
+
if not options.get("language"):
|
|
70
|
+
raise ValidationError("Language is required")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _build_request_body(options: CPUFunctionOptions) -> dict[str, Any]:
|
|
74
|
+
name = options["name"]
|
|
75
|
+
language = options["language"]
|
|
76
|
+
code = options["code"]
|
|
77
|
+
config = options.get("config", {})
|
|
78
|
+
env_variables = options.get("env_variables", {})
|
|
79
|
+
dependencies = options.get("dependencies")
|
|
80
|
+
cron_schedule = options.get("cron_schedule")
|
|
81
|
+
|
|
82
|
+
runtime = options.get("runtime") or _get_default_runtime(language)
|
|
83
|
+
file_ext = _get_file_extension(language)
|
|
84
|
+
|
|
85
|
+
env_vars_list = [{"key": k, "value": v} for k, v in env_variables.items()] if env_variables else []
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
"name": name.lower(),
|
|
89
|
+
"language": language,
|
|
90
|
+
"runtime": runtime,
|
|
91
|
+
"sourceWith": code,
|
|
92
|
+
"fileExt": file_ext,
|
|
93
|
+
"processorType": "CPU only",
|
|
94
|
+
"memoryAllocated": parse_memory(config.get("memory", 1024)) if config.get("memory") else 1024,
|
|
95
|
+
"timeout": config.get("timeout", 10) if config else 10,
|
|
96
|
+
"envVariables": json.dumps(env_vars_list),
|
|
97
|
+
"requirements": _format_requirements(dependencies),
|
|
98
|
+
"cronExpression": cron_schedule or "",
|
|
99
|
+
"totalVariables": len(env_variables) if env_variables else 0,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
async def _create_cpu_function(options: CPUFunctionOptions) -> DeployedFunction | None:
|
|
104
|
+
"""Internal function to create and deploy a CPU function."""
|
|
105
|
+
if not _global_api_token:
|
|
106
|
+
raise ValidationError("API key not set. Initialize Buildfunctions client first.")
|
|
107
|
+
|
|
108
|
+
api_token = _global_api_token
|
|
109
|
+
base_url = _global_base_url or DEFAULT_BASE_URL
|
|
110
|
+
|
|
111
|
+
resolved_code = await resolve_code(options["code"])
|
|
112
|
+
resolved_options = {**options, "code": resolved_code}
|
|
113
|
+
_validate_options(resolved_options)
|
|
114
|
+
body = _build_request_body(resolved_options)
|
|
115
|
+
|
|
116
|
+
async with httpx.AsyncClient(timeout=httpx.Timeout(600.0)) as client:
|
|
117
|
+
response = await client.post(
|
|
118
|
+
f"{base_url}/api/sdk/functions/build",
|
|
119
|
+
headers={
|
|
120
|
+
"Content-Type": "application/json",
|
|
121
|
+
"Authorization": f"Bearer {api_token}",
|
|
122
|
+
},
|
|
123
|
+
json=body,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if not response.is_success:
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
data = response.json()
|
|
130
|
+
name = options["name"].lower()
|
|
131
|
+
runtime = options.get("runtime") or _get_default_runtime(options["language"])
|
|
132
|
+
|
|
133
|
+
async def delete_fn() -> None:
|
|
134
|
+
async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as c:
|
|
135
|
+
await c.request(
|
|
136
|
+
"DELETE",
|
|
137
|
+
f"{base_url}/api/sdk/functions/build",
|
|
138
|
+
headers={
|
|
139
|
+
"Content-Type": "application/json",
|
|
140
|
+
"Authorization": f"Bearer {api_token}",
|
|
141
|
+
},
|
|
142
|
+
json={"siteId": data.get("siteId")},
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return DotDict({
|
|
146
|
+
"id": data.get("siteId", ""),
|
|
147
|
+
"name": name,
|
|
148
|
+
"subdomain": name,
|
|
149
|
+
"endpoint": data.get("endpoint", ""),
|
|
150
|
+
"lambdaUrl": data.get("sslCertificateEndpoint", ""),
|
|
151
|
+
"language": options["language"],
|
|
152
|
+
"runtime": runtime,
|
|
153
|
+
"isGPUF": False,
|
|
154
|
+
"delete": delete_fn,
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class CPUFunction:
|
|
159
|
+
"""CPU Function factory - matches TypeScript SDK pattern."""
|
|
160
|
+
|
|
161
|
+
@staticmethod
|
|
162
|
+
async def create(options: CPUFunctionOptions) -> DeployedFunction | None:
|
|
163
|
+
"""Create and deploy a new CPU function."""
|
|
164
|
+
return await _create_cpu_function(options)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
create_cpu_function = _create_cpu_function
|