Feat v0.7.11 query & HTTP ergonomics
Some checks failed
CI/CD / e2e-smoke (pull_request) Has been skipped
CI/CD / test-go-pg (pull_request) Has been cancelled
CI/CD / detect-changes (pull_request) Successful in 4s
CI/CD / test-frontend (pull_request) Has been skipped
CI/CD / build-and-deploy (pull_request) Has been cancelled
CI/CD / test-sqlite (pull_request) Has been cancelled
CI/CD / test-runners (pull_request) Has been skipped
Some checks failed
CI/CD / e2e-smoke (pull_request) Has been skipped
CI/CD / test-go-pg (pull_request) Has been cancelled
CI/CD / detect-changes (pull_request) Successful in 4s
CI/CD / test-frontend (pull_request) Has been skipped
CI/CD / build-and-deploy (pull_request) Has been cancelled
CI/CD / test-sqlite (pull_request) Has been cancelled
CI/CD / test-runners (pull_request) Has been skipped
Add four new Starlark sandbox builtins to reduce extension friction around SQL decomposition and synchronous HTTP fan-out: - db.count(table, filters) — integer row count - db.aggregate(table, column, op, filters) — single-value aggregation - db.query_batch(queries) — up to 10 query specs in one call - http.batch(requests) — concurrent dispatch of up to 10 HTTP requests Extract buildSelectQuery helper from dbQuery for reuse by query_batch. 21 new tests (14 db, 7 http). No new permissions or schema changes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
// resp = http.put(url, body="", headers={})
|
||||
// resp = http.delete(url, headers={})
|
||||
// resp = http.request(method, url, body="", headers={})
|
||||
// resps = http.batch([{"method": "GET", "url": "..."}, ...])
|
||||
//
|
||||
// # resp is a dict: {"status": 200, "headers": {...}, "body": "..."}
|
||||
//
|
||||
@@ -37,6 +38,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -230,9 +232,125 @@ func BuildHTTPModule(ctx context.Context, cfg HTTPModuleConfig) *starlarkstruct.
|
||||
}
|
||||
return doRequest("DELETE", rawURL, "", dictToStringMap(hdrs))
|
||||
}),
|
||||
|
||||
"batch": starlark.NewBuiltin("http.batch", httpBatch(ctx, ac, cfg.timeout(), cfg.maxBody())),
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Batch Dispatch ─────────────────────────
|
||||
|
||||
// httpBatch implements http.batch(requests).
|
||||
// requests is a list of dicts, each with keys: method, url, body?, headers?.
|
||||
// Dispatches all requests concurrently (max 10). Individual failures return
|
||||
// error response dicts rather than aborting the batch.
|
||||
func httpBatch(
|
||||
ctx context.Context,
|
||||
ac *accessControl,
|
||||
timeout time.Duration,
|
||||
maxBody int64,
|
||||
) func(*starlark.Thread, *starlark.Builtin, starlark.Tuple, []starlark.Tuple) (starlark.Value, error) {
|
||||
return func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
|
||||
var requests *starlark.List
|
||||
|
||||
if err := starlark.UnpackArgs(b.Name(), args, kwargs,
|
||||
"requests", &requests,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n := requests.Len()
|
||||
if n == 0 {
|
||||
return nil, fmt.Errorf("http.batch: requests list is empty")
|
||||
}
|
||||
if n > 10 {
|
||||
return nil, fmt.Errorf("http.batch: max 10 requests, got %d", n)
|
||||
}
|
||||
|
||||
// Parse all request specs before dispatching (fail fast on bad input).
|
||||
type reqSpec struct {
|
||||
method, url, body string
|
||||
headers map[string]string
|
||||
}
|
||||
specs := make([]reqSpec, n)
|
||||
for i := 0; i < n; i++ {
|
||||
d, ok := requests.Index(i).(*starlark.Dict)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("http.batch[%d]: each request must be a dict, got %s", i, requests.Index(i).Type())
|
||||
}
|
||||
|
||||
method, url, body, headers, err := parseBatchRequestDict(d)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("http.batch[%d]: %w", i, err)
|
||||
}
|
||||
specs[i] = reqSpec{method: method, url: url, body: body, headers: headers}
|
||||
}
|
||||
|
||||
// Dispatch concurrently.
|
||||
results := make([]starlark.Value, n)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i, spec := range specs {
|
||||
wg.Add(1)
|
||||
go func(idx int, s reqSpec) {
|
||||
defer wg.Done()
|
||||
|
||||
resp, err := executeHTTPRequest(ctx, ac, s.method, s.url, s.body, s.headers, timeout, maxBody)
|
||||
if err != nil {
|
||||
results[idx] = buildErrorResponseDict(err.Error())
|
||||
} else {
|
||||
results[idx] = resp
|
||||
}
|
||||
}(i, spec)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return starlark.NewList(results), nil
|
||||
}
|
||||
}
|
||||
|
||||
// parseBatchRequestDict extracts method, url, body, headers from a Starlark dict.
|
||||
func parseBatchRequestDict(d *starlark.Dict) (method, rawURL, body string, headers map[string]string, err error) {
|
||||
methodVal, found, _ := d.Get(starlark.String("method"))
|
||||
if !found || methodVal == starlark.None {
|
||||
return "", "", "", nil, fmt.Errorf("missing required key \"method\"")
|
||||
}
|
||||
method, ok := starlark.AsString(methodVal)
|
||||
if !ok {
|
||||
return "", "", "", nil, fmt.Errorf("\"method\" must be a string, got %s", methodVal.Type())
|
||||
}
|
||||
method = strings.ToUpper(method)
|
||||
|
||||
urlVal, found, _ := d.Get(starlark.String("url"))
|
||||
if !found || urlVal == starlark.None {
|
||||
return "", "", "", nil, fmt.Errorf("missing required key \"url\"")
|
||||
}
|
||||
rawURL, ok = starlark.AsString(urlVal)
|
||||
if !ok {
|
||||
return "", "", "", nil, fmt.Errorf("\"url\" must be a string, got %s", urlVal.Type())
|
||||
}
|
||||
|
||||
if bodyVal, found, _ := d.Get(starlark.String("body")); found && bodyVal != starlark.None {
|
||||
body, _ = starlark.AsString(bodyVal)
|
||||
}
|
||||
|
||||
if hdrsVal, found, _ := d.Get(starlark.String("headers")); found && hdrsVal != starlark.None {
|
||||
if hdrsDict, ok := hdrsVal.(*starlark.Dict); ok {
|
||||
headers = dictToStringMap(hdrsDict)
|
||||
}
|
||||
}
|
||||
|
||||
return method, rawURL, body, headers, nil
|
||||
}
|
||||
|
||||
// buildErrorResponseDict creates an error response dict for a failed batch request.
|
||||
func buildErrorResponseDict(errMsg string) starlark.Value {
|
||||
resp := starlark.NewDict(3)
|
||||
_ = resp.SetKey(starlark.String("status"), starlark.MakeInt(0))
|
||||
_ = resp.SetKey(starlark.String("headers"), starlark.NewDict(0))
|
||||
_ = resp.SetKey(starlark.String("body"), starlark.String("error: "+errMsg))
|
||||
return resp
|
||||
}
|
||||
|
||||
// ─── Request Execution ──────────────────────
|
||||
|
||||
func executeHTTPRequest(
|
||||
|
||||
Reference in New Issue
Block a user