Feat v0.7.11 query http ergonomics (#65)
All checks were successful
CI/CD / test-frontend (push) Has been skipped
CI/CD / e2e-smoke (push) Has been skipped
CI/CD / test-runners (push) Has been skipped
CI/CD / test-go-pg (push) Successful in 2m53s
CI/CD / test-sqlite (push) Successful in 2m57s
CI/CD / build-and-deploy (push) Successful in 1m13s
CI/CD / detect-changes (push) Successful in 3s

Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #65.
This commit is contained in:
2026-04-02 23:32:17 +00:00
committed by xcaliber
parent f06c6c954b
commit c2d52f50c5
8 changed files with 924 additions and 77 deletions

View File

@@ -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(