Compare commits

...
Author SHA1 Message Date
William Fu-Hinthorn 99a5cb875d phony 2025-05-20 15:59:26 -07:00
William Fu-Hinthorn 78239caed0 Some fancy stuff 2025-05-20 15:59:26 -07:00
18 changed files with 2611 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
.PHONY: all format build test
# Default target executed when no arguments are given to make.
all: help
format:
go fmt ./...
build:
go build ./...
test:
go test ./...
######################
# HELP
######################
help:
@echo '===================='
@echo '-- DOCUMENTATION --'
@echo '-- LINTING --'
@echo 'format - run code formatters'
@echo 'build - build the project'
@echo 'test - run unit tests'
+10
View File
@@ -0,0 +1,10 @@
module langchain.dev/langgraph
go 1.23.0
toolchain go1.23.9
require (
github.com/google/uuid v1.6.0 // indirect
golang.org/x/sync v0.14.0 // indirect
)
+4
View File
@@ -0,0 +1,4 @@
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+522
View File
@@ -0,0 +1,522 @@
package pregel
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
)
func PrepareNextTasks(
ctx context.Context,
checkpoint Checkpoint,
pendingWrites []interface{},
processes map[string]PregelNode,
channels map[string]BaseChannel,
managed ManagedValueMapping,
config RunnableConfig,
step int,
forExecution bool,
store BaseStore,
checkpointer BaseCheckpointSaver,
// ─ optimisation hints (optional) ─
triggerToNodes map[string][]string,
updatedChannels map[string]struct{},
) (map[string]interface{}, error) {
// Decode checkpoint.id (UUID/xxhash) into raw bytes for deterministic task-id hashing.
cleanID := strings.ReplaceAll(checkpoint.ID, "-", "")
checkpointIDBytes, err := hex.DecodeString(cleanID)
if err != nil {
return nil, err
}
nullVersion := checkpointNullVersion(checkpoint)
tasks := make(map[string]interface{})
// Consume pending sends
for idx := range checkpoint.PendingSends {
task, err := PrepareSingleTask(
ctx,
[]interface{}{PUSH, idx},
"",
checkpoint,
checkpointIDBytes,
nullVersion,
pendingWrites,
processes,
channels,
managed,
config,
step,
forExecution,
store,
checkpointer,
)
if err != nil {
return nil, err
}
if task == nil {
continue
}
if id, ok := taskID(task); ok {
tasks[id] = task
}
}
var candidateNodes []string
if len(updatedChannels) > 0 && len(triggerToNodes) > 0 {
nodeSet := map[string]struct{}{}
for ch := range updatedChannels {
for _, n := range triggerToNodes[ch] {
nodeSet[n] = struct{}{}
}
}
for n := range nodeSet {
candidateNodes = append(candidateNodes, n)
}
sort.Strings(candidateNodes) // deterministic order
} else if len(checkpoint.ChannelVersions) == 0 {
candidateNodes = nil
} else {
for n := range processes {
candidateNodes = append(candidateNodes, n)
}
sort.Strings(candidateNodes)
}
for _, name := range candidateNodes {
task, err := PrepareSingleTask(
ctx,
[]interface{}{PULL, name},
"", // checksum only used when resuming a partial step
checkpoint,
checkpointIDBytes,
nullVersion,
pendingWrites,
processes,
channels,
managed,
config,
step,
forExecution,
store,
checkpointer,
)
if err != nil {
return nil, err
}
if task == nil {
continue
}
if id, ok := taskID(task); ok {
tasks[id] = task
}
}
return tasks, nil
}
func PrepareSingleTask(
ctx context.Context,
taskPath []interface{}, // e.g. [PUSH, idx] OR [PULL, "node"]
taskIDChecksum string, // optional used when resuming
checkpoint Checkpoint, // state captured at end of previous step
checkpointIDBytes []byte, // checkpoint.id as bytes (uuid / xxhash)
checkpointNullVersion interface{}, // sentinel “null” version value
pendingWrites []interface{}, // successful writes from *this* step so far
processes map[string]PregelNode, // graph definition
channels map[string]BaseChannel, // live channel values
managed ManagedValueMapping, // placeholder resolver
config RunnableConfig, // config inherited from graph.Invoke()
step int, // current super-step (n+1)
forExecution bool, // false = planning pass, true = exec pass
store BaseStore, // needed for reads/writes
checkpointer BaseCheckpointSaver, // used only when executing
) (interface{}, error) {
// Ensure checkpoint.ChannelVersions is initialized
if checkpoint.ChannelVersions == nil {
checkpoint.ChannelVersions = make(map[string]int64)
}
cfgSection := config.Configurable
if cfgSection == nil {
cfgSection = map[string]interface{}{}
}
parentNS, _ := cfgSection[CONFIG_KEY_CHECKPOINT_NS].(string)
emitConfig := func(base RunnableConfig, md map[string]interface{}) RunnableConfig {
// Make a shallow copy of the struct
out := base
if out.Configurable == nil {
out.Configurable = map[string]interface{}{}
}
confClone := make(map[string]interface{}, len(out.Configurable))
for k, v := range out.Configurable {
confClone[k] = v
}
confClone[CONFIG_KEY_SCRATCHPAD] = createScratchpad(
out.Configurable[CONFIG_KEY_SCRATCHPAD].(map[string]interface{}),
pendingWrites,
md["langgraph_checkpoint_ns"].(string),
md["langgraph_checkpoint_ns"].(string),
out.Configurable[CONFIG_KEY_RESUME_MAP].(map[string]interface{}),
)
confClone[CONFIG_KEY_CHECKPOINTER] = checkpointer
out.Configurable = confClone
if out.Metadata == nil {
out.Metadata = map[string]interface{}{}
}
for k, v := range md {
out.Metadata[k] = v
}
return out
}
// Convenience for checksum comparison
checkSumMatch := func(need string) error {
if taskIDChecksum != "" && taskIDChecksum != need {
return fmt.Errorf("%s != %s", need, taskIDChecksum)
}
return nil
}
// PUSH
if len(taskPath) > 0 && taskPath[0] == PUSH {
// PUSH triggered via explicit Call (happens during node execution)
// taskPath shape: [PUSH, parentPath, writeIdx, parentTaskID, Call]
if len(taskPath) >= 5 {
call, ok := taskPath[4].(Call)
if ok {
name, isStr := call.Func.(string)
if !isStr {
name = "unknown"
}
// Hash-stable checkpoint namespace
var checkpointNS string
if parentNS == "" {
checkpointNS = name
} else {
checkpointNS = parentNS + NS_SEP + name
}
// Deterministic task-id
taskID := taskIDFunc(
checkpointIDBytes,
checkpointNS,
strconv.Itoa(step),
name,
PUSH,
taskPathStr(taskPath[1]),
fmt.Sprintf("%v", taskPath[2]),
)
if err := checkSumMatch(taskID); err != nil {
return nil, err
}
taskCheckpointNS := checkpointNS + NS_END + taskID
metadata := map[string]interface{}{
"langgraph_step": step,
"langgraph_node": name,
"langgraph_triggers": []string{PUSH},
"langgraph_path": taskPath[:3],
"langgraph_checkpoint_ns": taskCheckpointNS,
}
if forExecution {
var node NodeRunnable
if proc, ok := processes[name]; ok {
node = proc.Node
}
return PregelExecutableTask{
PregelTask: PregelTask{
ID: taskID,
Name: name,
Path: taskPath[:3],
},
Input: call.Input,
Node: node,
Writes: []Write{},
Config: emitConfig(config, metadata),
Triggers: []string{PUSH},
}, nil
}
return PregelTask{ID: taskID, Name: name, Path: taskPath[:3]}, nil
}
}
// ---------------------------------------------------------------------
// 1b. Standard pending-send packet: taskPath shape [PUSH, idx]
// ---------------------------------------------------------------------
if len(taskPath) == 2 {
idx, ok := taskPath[1].(int)
if !ok || idx >= len(checkpoint.PendingSends) {
return nil, nil
}
packet := checkpoint.PendingSends[idx]
proc, ok := processes[packet.Node]
if !ok || proc.Node == nil {
return nil, nil
}
checkpointNS := parentNS
if checkpointNS != "" {
checkpointNS += NS_SEP + packet.Node
} else {
checkpointNS = packet.Node
}
taskID := taskIDFunc(
checkpointIDBytes,
checkpointNS,
strconv.Itoa(step),
packet.Node,
PUSH,
strconv.Itoa(idx),
)
if err := checkSumMatch(taskID); err != nil {
return nil, err
}
taskCheckpointNS := checkpointNS + NS_END + taskID
metadata := map[string]interface{}{
"langgraph_step": step,
"langgraph_node": packet.Node,
"langgraph_triggers": []string{PUSH},
"langgraph_path": taskPath,
"langgraph_checkpoint_ns": taskCheckpointNS,
}
if forExecution {
return PregelExecutableTask{
PregelTask: PregelTask{
ID: taskID,
Name: packet.Node,
Path: taskPath,
},
Input: packet.Arg,
Node: proc.Node,
Writes: nil,
Config: emitConfig(config, metadata),
Triggers: []string{PUSH},
}, nil
}
return PregelTask{ID: taskID, Name: packet.Node, Path: taskPath}, nil
}
// An ill-formed PUSH path nothing to schedule
return nil, nil
}
// PULL branch
if len(taskPath) > 0 && taskPath[0] == PULL {
if len(taskPath) < 2 {
return nil, nil
}
name, ok := taskPath[1].(string)
if !ok {
return nil, nil
}
proc, ok := processes[name]
if !ok || proc.Node == nil {
return nil, nil
}
seen := map[string]interface{}{}
if v, _ := checkpoint.VersionsSeen[name].(map[string]interface{}); v != nil {
for k, vv := range v { // shallow copy
seen[k] = vv
}
}
var triggers []string
for _, ch := range proc.Triggers {
cv, exists := checkpoint.ChannelVersions[ch]
if !exists {
cv = checkpointNullVersion.(int64) // use the provided null version
}
sv, _ := seen[ch].(int64) // default to 0 if not exists or wrong type
if compareVersion(cv, sv) > 0 {
triggers = append(triggers, ch)
}
}
if len(triggers) == 0 {
return nil, nil // not ready
}
sort.Strings(triggers)
input := map[string]interface{}{}
for _, ch := range proc.Triggers {
if v, ok := channels[ch]; ok {
input[ch] = v
}
}
checkpointNS := parentNS
if checkpointNS != "" {
checkpointNS += NS_SEP + name
} else {
checkpointNS = name
}
taskID := taskIDFunc(
checkpointIDBytes,
checkpointNS,
strconv.Itoa(step),
name,
PULL,
// join triggers to guarantee deterministic id
fmt.Sprintf("%v", triggers),
)
if err := checkSumMatch(taskID); err != nil {
return nil, err
}
taskCheckpointNS := checkpointNS + NS_END + taskID
metadata := map[string]interface{}{
"langgraph_step": step,
"langgraph_node": name,
"langgraph_triggers": triggers,
"langgraph_path": taskPath,
"langgraph_checkpoint_ns": taskCheckpointNS,
}
if forExecution {
return PregelExecutableTask{
PregelTask: PregelTask{
ID: taskID,
Name: name,
Path: taskPath,
},
Input: input,
Node: proc.Node,
Writes: nil,
Config: emitConfig(config, metadata),
Triggers: triggers,
}, nil
}
return PregelTask{ID: taskID, Name: name, Path: taskPath}, nil
}
return nil, nil
}
// Private / Helpers
// taskIDFunc deterministically hashes the checkpoint-scoped information that
// must be unique for a task in a given super-step.
func taskIDFunc(checkpointIDBytes []byte, parts ...string) string {
h := sha256.New()
_, _ = h.Write(checkpointIDBytes)
for _, p := range parts {
_, _ = h.Write([]byte(p))
}
return hex.EncodeToString(h.Sum(nil))
}
// taskPathStr is only used so the path element contributes to the hash in a
// deterministic textual form.
func taskPathStr(path interface{}) string {
return fmt.Sprintf("%v", path)
}
// createScratchpad returns an *immutable* copy of the scratchpad that will
// be injected into the task-local Config. We:
//
// 1. start from the previous scratchpad (if any),
// 2. merge in any successful writes from earlier tasks in this super-step,
// 3. copy-on-write so individual tasks never share interior maps.
//
// The logic below is intentionally simple; extend as needed.
func createScratchpad(
current map[string]interface{},
pendingWrites []interface{},
taskID string,
checkpointHash string,
resumeMap map[string]interface{},
) map[string]interface{} {
out := map[string]interface{}{}
for k, v := range current {
out[k] = v
}
if len(pendingWrites) > 0 {
out["pending_writes"] = append([]interface{}{}, pendingWrites...)
}
if checkpointHash != "" {
out["checkpoint_hash"] = checkpointHash
}
if resumeMap != nil {
out["resume_map"] = resumeMap
}
out["task_id"] = taskID
return out
}
func checkpointNullVersion(_ Checkpoint) interface{} {
// Return the zero value for int64 as the null version
return int64(0)
}
func taskID(t interface{}) (string, bool) {
switch v := t.(type) {
case PregelTask:
return v.ID, true
case PregelExecutableTask:
return v.ID, true
default:
return "", false
}
}
func compareVersion(a, b interface{}) int {
switch av := a.(type) {
case int:
bv, _ := b.(int)
return av - bv
case int64:
var bv int64
switch bvVal := b.(type) {
case int64:
bv = bvVal
case int:
bv = int64(bvVal)
default:
bv = 0
}
if av == bv {
return 0
}
if av < bv {
return -1
}
return 1
case string:
bv, _ := b.(string)
if av == bv {
return 0
}
if av < bv {
return -1
}
return 1
// Fallback to reflect.DeepEqual comparison: not perfect but safe.
default:
if reflect.DeepEqual(a, b) {
return 0
}
return 1
}
}
+28
View File
@@ -0,0 +1,28 @@
package pregel
import (
"context"
"time"
"github.com/google/uuid"
)
func EmptyCheckpoint() (*Checkpoint, error) {
uid, err := uuid.NewV6()
if err != nil {
return nil, err
}
return &Checkpoint{
Version: 1,
ID: uid.String(),
Timestamp: time.Now().Format(time.RFC3339),
ChannelValues: map[string]interface{}{},
ChannelVersions: map[string]int64{},
VersionsSeen: map[string]interface{}{},
PendingSends: []Send{},
}, nil
}
type Checkpointer interface {
PutWrites(ctx context.Context, checkpoint Checkpoint, writes []Write) error
}
+138
View File
@@ -0,0 +1,138 @@
package pregel
import (
"context"
"errors"
)
// Pregel is the top-level graph object.
type Pregel struct {
Name string
Nodes map[string]PregelNode
Channels map[string]BaseChannel
LoopCfg RunnableConfig
Checkptr BaseCheckpointSaver
Store BaseStore
Debug bool
}
func (g *Pregel) Stream(
input any,
cfg RunnableConfig,
opts *StreamOptions,
) (<-chan StreamChunk, <-chan error) {
eventCh := make(chan StreamChunk, 16)
errCh := make(chan error, 1)
if opts == nil {
opts = &StreamOptions{}
}
ctx := opts.Context
if ctx == nil {
ctx = context.Background()
}
mode := opts.Mode
if mode == "" {
mode = StreamValues
}
// TODO: opts.Debug
// ensure output channels are set / valid
outChans := opts.OutputChannels
if len(outChans) == 0 {
for k := range g.Channels {
if _, ok := g.Channels[k]; ok {
outChans = append(outChans, k)
}
}
}
if opts.MaxConcurrency > 0 {
cfg.MaxConcurrency = opts.MaxConcurrency
}
if cfg.MaxConcurrency == 0 {
cfg.MaxConcurrency = 4
}
if cfg.RecursionLimit == 0 {
cfg.RecursionLimit = 25
}
if opts.CheckpointDuring != nil {
cfg.Configurable[CONFIG_KEY_CHECKPOINT_DURING] = *opts.CheckpointDuring
}
checkpoint, err := EmptyCheckpoint()
if err != nil {
errCh <- err
return nil, errCh
}
loop := NewLoop(
ctx,
*checkpoint,
g.Nodes,
g.channelsAsConcrete(),
nil, // managed values
cfg,
nil, // g.checkpointer, // may be nil
nil, // g.store,
)
loop.interruptBefore = opts.InterruptBefore
loop.interruptAfter = opts.InterruptAfter
loop.streamCh = eventCh
loop.streamMode = mode
// loop.debug = debug
go func() {
defer close(eventCh)
defer close(errCh)
// Create a runner to execute tasks
runner := NewPregelRunner(loop, nil)
// Use the tick method in a loop instead of Run()
for {
more, err := loop.tick(outChans)
if err != nil {
// Check if this is a GraphInterrupt error
var interrupt GraphInterrupt
if errors.As(err, &interrupt) {
// Handle interrupt gracefully
break
}
// Otherwise, it's a real error
errCh <- err
return
}
runnerOpts := TickOptions{
MaxConcurrency: cfg.MaxConcurrency,
}
if opts.Debug != nil && *opts.Debug {
runnerOpts.OnStepWrite = func(step int, writes []Write) {
// TODO: Handle debugging info
}
}
if err := runner.tick(runnerOpts); err != nil {
errCh <- err
return
}
// No more iterations needed, we're done
if !more {
break
}
}
}()
return eventCh, errCh
}
func (g *Pregel) channelsAsConcrete() map[string]BaseChannel {
out := make(map[string]BaseChannel, len(g.Channels))
for k, v := range g.Channels {
if ch, ok := v.(BaseChannel); ok {
out[k] = ch
}
}
return out
}
+500
View File
@@ -0,0 +1,500 @@
package pregel
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"sync"
"time"
)
type GraphInterrupt struct {
Interrupts any
}
func (e GraphInterrupt) Error() string { return "graph interrupted" }
type GraphDelegate struct {
Payload map[string]any
}
func (e GraphDelegate) Error() string { return "graph delegation requested" }
func hashID(checkpointID string, parts ...string) string {
b, _ := hex.DecodeString(checkpointID)
h := sha256.New()
h.Write(b)
for _, p := range parts {
h.Write([]byte(p))
}
return hex.EncodeToString(h.Sum(nil))
}
type PregelLoop struct {
ctx context.Context
cancel context.CancelFunc
cfg RunnableConfig
store BaseStore
checkpoint Checkpoint
checkporter BaseCheckpointSaver
processes map[string]PregelNode
channels map[string]BaseChannel
managed ManagedValueMapping
step int
stop int
interruptBefore []string
interruptAfter []string
pendingWrites []WriteRecord
tasks map[string]*PregelExecutableTask
toInterrupt []*PregelExecutableTask
triggerToNodes map[string][]string
updatedChans map[string]struct{}
// synchronisation / workers
workers int
wg sync.WaitGroup
errMu sync.Mutex
runErr error
// Streaming
streamCh chan<- StreamChunk
streamMode StreamMode
pendingMu sync.Mutex
checkpointPendingWrites []PendingWrite
checkpointer Checkpointer // interface with PutWrites()
checkpointConfig RunnableConfig
emit func(task *PregelExecutableTask, writes []Write, cached bool)
}
type WriteRecord struct {
Task string
Chan string
Value any
}
// NewLoop initialises a fully-featured loop.
func NewLoop(
ctx context.Context,
checkpoint Checkpoint,
processes map[string]PregelNode,
channels map[string]BaseChannel,
managed ManagedValueMapping,
cfg RunnableConfig,
checkporter BaseCheckpointSaver,
store BaseStore,
) *PregelLoop {
c, cancel := context.WithCancel(ctx)
// Ensure checkpoint is properly initialized
if checkpoint.ChannelVersions == nil {
checkpoint = NewCheckpoint()
}
loop := &PregelLoop{
ctx: c,
cancel: cancel,
checkpoint: checkpoint,
processes: processes,
channels: channels,
managed: managed,
cfg: cfg,
checkporter: checkporter,
store: store,
step: 0,
stop: cfg.RecursionLimit,
workers: cfg.MaxConcurrency,
pendingWrites: make([]WriteRecord, 0, 16),
tasks: map[string]*PregelExecutableTask{},
}
if loop.workers <= 0 {
loop.workers = 1
}
return loop
}
// Run blocks until completion (or first error)
func (l *PregelLoop) Run() error {
defer l.cancel()
for {
more, err := l.tick(nil)
if err != nil {
if errors.As(err, &GraphInterrupt{}) {
return nil
}
return err
}
if !more {
break
}
}
return nil
}
// tick executes a single iteration of the Pregel loop.
// Returns true if more iterations are needed, false if done.
func (l *PregelLoop) tick(inputKeys []string) (bool, error) {
// TODO: Use inputKeys to get the first values.
// Check if we need to evaluate interrupts before execution
if err := l.evaluateInterrupt("before"); err != nil {
return false, err
}
// Build tasks
tasks, err := PrepareNextTasks(
l.ctx,
l.checkpoint,
convertPending(l.pendingWrites),
l.processes,
l.channels,
l.managed,
l.cfg,
l.step,
true,
l.store,
l.checkporter,
l.triggerToNodes,
l.updatedChans,
)
if err != nil {
return false, err
}
if len(tasks) == 0 {
return false, nil // done, no more tasks
}
l.tasks = make(map[string]*PregelExecutableTask)
for k, v := range tasks {
te := v.(PregelExecutableTask)
l.tasks[k] = &te
}
// parallel execute
workCh := make(chan *PregelExecutableTask)
errCh := make(chan error, l.workers)
for i := 0; i < l.workers; i++ {
go l.worker(workCh, errCh)
}
for _, t := range l.tasks {
if len(t.Writes) > 0 {
continue // already satisfied
}
workCh <- t
}
close(workCh)
for i := 0; i < l.workers; i++ {
if err := <-errCh; err != nil {
return false, err
}
}
// All tasks finished; apply writes
if err := l.applyWrites(); err != nil {
return false, err
}
// checkpoint
if err := l.saveCheckpoint(); err != nil {
return false, err
}
// Check if we need to evaluate interrupts after execution
if err := l.evaluateInterrupt("after"); err != nil {
return false, err
}
// Check if we've exceeded the recursion limit
l.step++
if l.step > l.stop {
return false, fmt.Errorf("exceeded recursion limit (%d)", l.stop)
}
return true, nil
}
// prepareAndExecuteStep is kept for backward compatibility
func (l *PregelLoop) prepareAndExecuteStep() error {
more, err := l.tick(nil)
if err != nil {
return err
}
if !more {
return nil
}
return nil
}
func (l *PregelLoop) worker(in <-chan *PregelExecutableTask, out chan<- error) {
for task := range in {
err := l.runTask(task)
out <- err
}
}
func (l *PregelLoop) runTask(t *PregelExecutableTask) error {
// retry loop
attempts := 0
max := 1
if p, ok := l.processes[t.Name]; ok {
max = maxAttempts(p.Retry)
}
for {
attempts++
select {
case <-l.ctx.Done():
return l.ctx.Err()
default:
}
writes, err := t.Node.Invoke(l.ctx, t.Input, t.Config, l)
if err == nil {
for _, w := range writes {
l.recordWrite(t.ID, w.Channel, w.Value)
}
t.Writes = writes
return nil
}
if attempts >= max {
return err
}
time.Sleep(backoffDelay(attempts))
}
}
// putWrites is called by PregelRunner (or nested tasks via the SEND helper)
// to persist writes produced by a task *during the current super-step*.
// It is safe for concurrent use.
func (l *PregelLoop) putWrites(taskID string, writes []Write) {
if len(writes) == 0 {
return
}
// ---------------------------------------------------------------------
// 1. Deduplicate if every write is for a “special” indexed channel.
// (“last one wins”, exactly like in TS / Python)
// ---------------------------------------------------------------------
allIndexed := true
for _, w := range writes {
if _, ok := WRITES_IDX_MAP[w.Channel]; !ok {
allIndexed = false
break
}
}
if allIndexed {
dedup := make(map[string]Write, len(writes))
for _, w := range writes {
dedup[w.Channel] = w
}
writes = make([]Write, 0, len(dedup))
for _, w := range dedup {
writes = append(writes, w)
}
}
// ---------------------------------------------------------------------
// 2. Merge into l.checkpointPendingWrites.
// We need a mutex because PregelRunner goroutines call us in parallel.
// ---------------------------------------------------------------------
l.pendingMu.Lock()
for _, w := range writes {
replaced := false
// If it is an indexed channel and an entry already exists for (task,channel),
// overwrite it (=> keep only the newest write).
if _, special := WRITES_IDX_MAP[w.Channel]; special {
for i := range l.checkpointPendingWrites {
pw := &l.checkpointPendingWrites[i]
if pw.TaskID == taskID && pw.Channel == w.Channel {
pw.Value = w.Value
replaced = true
break
}
}
}
// Otherwise (or if not found) just append.
if !replaced {
l.checkpointPendingWrites = append(
l.checkpointPendingWrites,
PendingWrite{TaskID: taskID, Channel: w.Channel, Value: w.Value},
)
}
}
l.pendingMu.Unlock()
// ---------------------------------------------------------------------
// 3. Forward the writes to the configured checkpointer (if any).
// We dont block the caller a quick “fire-and-forget” goroutine
// is fine because checkpointer.PutWrites() is thread-safe by design.
// ---------------------------------------------------------------------
// if l.checkpointer != nil {
// cfg := l.checkpointConfig // shallow copy is enough we never mutate it
// go l.checkpointer.PutWrites(cfg, writes, taskID)
// }
// ---------------------------------------------------------------------
// 4. Emit stream/debug output if the loop is already running.
// ---------------------------------------------------------------------
if len(l.tasks) > 0 {
l.outputWrites(taskID, writes, false)
}
}
// outputWrites mirrors TS _outputWrites (omits hidden tasks & handles modes).
// This is a *minimal* version; extend if you need streaming/debug UI parity.
func (l *PregelLoop) outputWrites(taskID string, writes []Write, cached bool) {
task, ok := l.tasks[taskID]
if !ok {
return
}
for _, tag := range task.Config.Tags {
if tag == TAG_HIDDEN {
return
}
}
// TODO: implement streaming
// delegate to whatever streaming mechanism you implemented…
// if l.emit != nil {
// l.emit(task, writes, cached)
// }
}
func maxAttempts(r RetryPolicy) int {
if r.MaxAttempts <= 0 {
return 1
}
return r.MaxAttempts
}
func backoffDelay(at int) time.Duration { return time.Duration(at) * 50 * time.Millisecond }
func (l *PregelLoop) Send(taskID string, writes []Write) {
for _, w := range writes {
l.recordWrite(taskID, w.Channel, w.Value)
}
}
// Read returns a copy of current channel values
func (l *PregelLoop) Read(selectKeys []string) map[string]any {
out := map[string]any{}
for _, k := range selectKeys {
if ch, ok := l.channels[k]; ok {
out[k] = ch.Get()
}
}
return out
}
func (l *PregelLoop) AcceptPush(origin PregelExecutableTask, writeIdx int, call *Call) (*PregelExecutableTask, error) {
ppath := origin.Path
newPath := []interface{}{PUSH, ppath, writeIdx, origin.ID, call}
cpid, _ := hex.DecodeString(l.checkpoint.ID)
nullVer := -1
task, err := PrepareSingleTask(
l.ctx,
newPath,
"",
l.checkpoint,
cpid,
nullVer,
convertPending(l.pendingWrites),
l.processes,
l.channels,
l.managed,
l.cfg,
l.step,
true,
l.store,
l.checkporter,
)
if err != nil {
return nil, err
}
if task == nil {
return nil, nil
}
te := task.(PregelExecutableTask)
l.tasks[te.ID] = &te
return &te, nil
}
func (l *PregelLoop) recordWrite(taskID, ch string, val any) {
l.pendingWrites = append(l.pendingWrites, WriteRecord{taskID, ch, val})
}
func convertPending(ws []WriteRecord) []interface{} {
out := make([]interface{}, 0, len(ws))
for _, w := range ws {
out = append(out, []interface{}{w.Task, w.Chan, w.Value})
}
return out
}
func (l *PregelLoop) applyWrites() error {
if len(l.pendingWrites) == 0 {
return nil
}
for _, wr := range l.pendingWrites {
ch, ok := l.channels[wr.Chan]
if !ok {
ch = &simpleChan{}
l.channels[wr.Chan] = ch
}
ch.Set(wr.Value)
// TODO: Handle other version types.
if _, exists := l.checkpoint.ChannelVersions[wr.Chan]; !exists {
l.checkpoint.ChannelVersions[wr.Chan] = 0
}
l.checkpoint.ChannelVersions[wr.Chan]++
}
l.pendingWrites = l.pendingWrites[:0]
return nil
}
func (l *PregelLoop) saveCheckpoint() error {
if l.checkporter == nil {
return nil
}
md := map[string]any{
"step": l.step,
"source": "loop",
"time": time.Now().UTC().Format(time.RFC3339Nano),
}
return l.checkporter.Put(l.cfg, l.checkpoint, md, nil)
}
func (l *PregelLoop) evaluateInterrupt(stage string) error {
var conditions []string
if stage == "before" {
conditions = l.interruptBefore
} else {
conditions = l.interruptAfter
}
if len(conditions) == 0 {
return nil
}
seen := map[string]struct{}{}
for _, t := range l.tasks {
for _, trg := range t.Triggers {
seen[trg] = struct{}{}
}
}
for _, cond := range conditions {
if _, ok := seen[cond]; ok || cond == "*" {
return GraphInterrupt{}
}
}
return nil
}
type Result struct {
Err error
}
+217
View File
@@ -0,0 +1,217 @@
// runner.go
package pregel
import (
"context"
"errors"
"sync"
"time"
"golang.org/x/sync/errgroup"
)
// PregelRunner is responsible for executing the set of tasks that a
// PregelLoop prepared for the *current super-step*. It runs them with
// respect to retry-policy, max-concurrency, timeouts, cancellation and
// Pregel-specific error semantics (GraphInterrupt / GraphBubbleUp).
type PregelRunner struct {
loop *PregelLoop
nodeFinished func(string) // Optional user-callback
}
// NewPregelRunner links the runner to its parent loop.
func NewPregelRunner(loop *PregelLoop, nodeFinished func(string)) *PregelRunner {
return &PregelRunner{loop: loop, nodeFinished: nodeFinished}
}
// TickOptions mirrors the semantics in the TS/Python implementations.
type TickOptions struct {
Timeout time.Duration // Deadline for the whole super-step
RetryPolicy RetryPolicy // Per-task retry policy
OnStepWrite func(int, []Write) // Hook after *all* writes are committed
MaxConcurrency int // ≤0 ⇒ unlimited
Ctx context.Context // Root ctx (optional)
}
// Tick executes every task whose Writes slice is still empty.
// It returns when *all* tasks have completed (successfully or not) **or**
// when the first non-interrupt error bubbles up.
func (r *PregelRunner) tick(opt TickOptions) error {
// Choose base context
ctx := opt.Ctx
if ctx == nil {
ctx = context.Background()
}
// We cancel siblings on first fatal error
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// Optional global timeout
if opt.Timeout > 0 {
ctx, cancel = context.WithTimeout(ctx, opt.Timeout)
defer cancel()
}
// Gather tasks that still need to run in this super-step
var pending []*PregelExecutableTask
for _, t := range r.loop.tasks {
if len(t.Writes) == 0 {
pending = append(pending, t)
}
}
if len(pending) == 0 {
return nil // nothing to do
}
// errgroup manages goroutines and collects the first returned error
g, gctx := errgroup.WithContext(ctx)
maxConc := opt.MaxConcurrency
if maxConc <= 0 {
maxConc = len(pending)
}
sem := make(chan struct{}, maxConc)
var mu sync.Mutex
for _, task := range pending {
task := task // capture
sem <- struct{}{}
g.Go(func() error {
defer func() { <-sem }()
err := runWithRetry(gctx, opt.RetryPolicy, func(c context.Context) error {
// NOTE: Node.Run must honour ctx for cancellation / deadlines.
writes, runErr := task.Node.Invoke(c, task.Input, task.Config, r.loop)
if runErr == nil {
task.Writes = writes
}
return runErr
})
r.commit(task, err)
switch {
case err == nil:
return nil
case errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded):
return err // propagate
}
var gi GraphInterrupt
if errors.As(err, &gi) {
mu.Lock()
defer mu.Unlock()
// kep track so that loop can raise combined interrupt later
return gi
}
cancel()
return err
})
}
// Wait for all goroutines (or first fatal error)
if err := g.Wait(); err != nil {
return err
}
// Step-level callback after *all* commits
if opt.OnStepWrite != nil {
var all []Write
for _, t := range r.loop.tasks {
all = append(all, t.Writes...)
}
opt.OnStepWrite(r.loop.step, all)
}
return nil
}
// commit replicates the Python/TS commit semantics.
func (r *PregelRunner) commit(task *PregelExecutableTask, execErr error) {
// On success ensure at least one NO_WRITES marker so loop knows it's done.
if execErr == nil && len(task.Writes) == 0 {
task.Writes = append(task.Writes, Write{Channel: NO_WRITES})
}
// Persist writes (or error) through the loops thread-safe adaptor.
switch {
case execErr == nil:
r.loop.putWrites(task.ID, task.Writes)
case errors.As(execErr, new(GraphInterrupt)):
// Interrupt carries its own writes payload
r.loop.putWrites(task.ID, task.Writes)
default:
// Record generic error
r.loop.putWrites(task.ID, []Write{{Channel: ERROR, Value: execErr}})
}
// optional callback
if execErr == nil && r.nodeFinished != nil {
r.nodeFinished(task.Name)
}
}
// runWithRetry is a minimal exponential-back-off retry helper.
func runWithRetry(ctx context.Context, pol RetryPolicy, fn func(context.Context) error) error {
if pol.MaxAttempts <= 0 {
pol.MaxAttempts = 1
}
// if pol.Backoff == nil {
// // default: exponential capped at 2 s
// pol.Backoff = func(attempt int) time.Duration {
// d := time.Duration(math.Pow(2, float64(attempt))) * 50 * time.Millisecond
// if d > 2*time.Second {
// d = 2 * time.Second
// }
// return d
// }
// }
// if pol.Retryable == nil {
// pol.Retryable = func(error) bool { return true }
// }
var err error
for attempt := 0; attempt < pol.MaxAttempts; attempt++ {
if err = fn(ctx); err == nil { // || !pol.Retryable(err) {
return err
}
// // wait before next try
// wait := pol.Backoff(attempt)
// select {
// case <-time.After(wait):
// case <-ctx.Done():
// return ctx.Err()
// }
}
return err
}
/* --------------------------------------------------------------------------
Missing symbols? If your project does not yet declare the following items
just add minimal stubs like the ones below (remove before wiring in
real implementations to avoid duplicates).
// Constants that mark write types
const (
ERROR = "error"
NO_WRITES = "no_writes"
)
// GraphInterrupt / BubbleUp marker errors
type GraphInterrupt struct{ Msg string }
func (g GraphInterrupt) Error() string { return g.Msg }
type GraphBubbleUp struct{ error }
// Minimal Write + RetryPolicy
type Write struct{ Channel string; Value any }
type RetryPolicy struct {
MaxAttempts int
Backoff func(attempt int) time.Duration
Retryable func(error) bool
}
// PregelExecutableTask, PregelLoop, etc. should exist elsewhere.
// -------------------------------------------------------------------------- */
+292
View File
@@ -0,0 +1,292 @@
package pregel
import (
"context"
"sync"
)
// Constants for task types and reserved keys
const (
// Task types
PUSH = "__pregel_push" // Denotes push-style tasks, ie. those created by Send objects
PULL = "__pregel_pull" // Denotes pull-style tasks, ie. those triggered by edges
// Reserved write keys
INPUT = "__input__" // For values passed as input to the graph
INTERRUPT = "__interrupt__" // For dynamic interrupts raised by nodes
RESUME = "__resume__" // For values passed to resume a node after an interrupt
ERROR = "__error__" // For errors raised by nodes
NO_WRITES = "__no_writes__" // Marker to signal node didn't write anything
SCHEDULED = "__scheduled__" // Marker to signal node was scheduled (in distributed mode)
TASKS = "__pregel_tasks" // For Send objects returned by nodes/edges
RETURN = "__return__" // For writes of a task where we simply record the return value
// Public constants
START = "__start__" // The first (maybe virtual) node in graph-style Pregel
END = "__end__" // The last (maybe virtual) node in graph-style Pregel
SELF = "__self__" // The implicit branch that handles each node's Control values
PREVIOUS = "__previous__" // Previous value
// Other constants
NS_SEP = "|" // For checkpoint_ns, separates each level (ie. graph|subgraph|subsubgraph)
NS_END = ":" // For checkpoint_ns, for each level, separates the namespace from the task_id
NULL_TASK_ID = "00000000-0000-0000-0000-000000000000" // The task_id to use for writes that are not associated with a task
CONF = "configurable" // Key for the configurable dict in RunnableConfig
// Reserved config.configurable keys
CONFIG_KEY_SEND = "__pregel_send" // Holds the `write` function that accepts writes to state/edges/reserved keys
CONFIG_KEY_READ = "__pregel_read" // Holds the `read` function that returns a copy of the current state
CONFIG_KEY_CALL = "__pregel_call" // Holds the `call` function that accepts a node/func, args and returns a future
CONFIG_KEY_CHECKPOINTER = "__pregel_checkpointer" // Holds a `BaseCheckpointSaver` passed from parent graph to child graphs
CONFIG_KEY_STREAM = "__pregel_stream" // Holds a `StreamProtocol` passed from parent graph to child graphs
CONFIG_KEY_STREAM_WRITER = "__pregel_stream_writer" // Holds a `StreamWriter` for stream_mode=custom
CONFIG_KEY_STORE = "__pregel_store" // Holds a `BaseStore` made available to managed values
CONFIG_KEY_CACHE = "__pregel_cache" // Holds a `BaseCache` made available to subgraphs
CONFIG_KEY_RESUMING = "__pregel_resuming" // Holds a boolean indicating if subgraphs should resume from a previous checkpoint
CONFIG_KEY_TASK_ID = "__pregel_task_id" // Holds the task ID for the current task
CONFIG_KEY_DEDUPE_TASKS = "__pregel_dedupe_tasks" // Holds a boolean indicating if tasks should be deduplicated (for distributed mode)
CONFIG_KEY_ENSURE_LATEST = "__pregel_ensure_latest" // Holds a boolean indicating whether to assert the requested checkpoint is the latest
CONFIG_KEY_DELEGATE = "__pregel_delegate" // Holds a boolean indicating whether to delegate subgraphs (for distributed mode)
CONFIG_KEY_THREAD_ID = "thread_id" // Holds the thread ID for the current invocation
CONFIG_KEY_CHECKPOINT_MAP = "checkpoint_map" // Holds a mapping of checkpoint_ns -> checkpoint_id for parent graphs
CONFIG_KEY_CHECKPOINT_ID = "checkpoint_id" // Holds the current checkpoint_id, if any
CONFIG_KEY_CHECKPOINT_NS = "checkpoint_ns" // Holds the current checkpoint_ns, "" for root graph
CONFIG_KEY_NODE_FINISHED = "__pregel_node_finished" // Holds a callback to be called when a node is finished
CONFIG_KEY_SCRATCHPAD = "__pregel_scratchpad" // Holds a mutable dict for temporary storage scoped to the current task
CONFIG_KEY_PREVIOUS = "__pregel_previous" // Holds the previous return value from a stateful Pregel graph
CONFIG_KEY_RUNNER_SUBMIT = "__pregel_runner_submit" // Holds a function that receives tasks from runner, executes them and returns results
CONFIG_KEY_CHECKPOINT_DURING = "__pregel_checkpoint_during" // Holds a boolean indicating whether to checkpoint during the run (or only at the end)
CONFIG_KEY_RESUME_MAP = "__pregel_resume_map" // Holds a mapping of task ns -> resume value for resuming tasks
TAG_HIDDEN = "langsmith:hidden" // Holds a boolean indicating whether to hide a node/edge from certain tracing/streaming environments.
)
// StreamMode defines how the graph streams its output
type StreamMode string
// WRITES_IDX_MAP maps special channel names to negative indices
// to avoid conflicts with regular writes.
var WRITES_IDX_MAP = map[string]int{
ERROR: -1,
SCHEDULED: -2,
INTERRUPT: -3,
RESUME: -4,
}
// TS
// export type PendingWriteValue = unknown;
// export type PendingWrite<Channel = string> = [Channel, PendingWriteValue];
// export type CheckpointPendingWrite<TaskId = string> = [
// TaskId,
// ...PendingWrite<string>
// ];
// Py
// PendingWrite = Tuple[str, str, Any]
type PendingWrite struct {
TaskID string
Channel string
Value interface{}
}
const (
// StreamValues emits all values in the state after each step
StreamValues StreamMode = "values"
// StreamUpdates emits only the node or task names and updates
StreamUpdates StreamMode = "updates"
// StreamCustom emits custom data from inside nodes or tasks
StreamCustom StreamMode = "custom"
// StreamMessages emits LLM messages token-by-token
StreamMessages StreamMode = "messages"
// StreamDebug emits debug events with as much information as possible
StreamDebug StreamMode = "debug"
)
// PregelTask represents a task in the Pregel system
type PregelTask struct {
ID string
Name string
Path []interface{}
Error error
Interrupts []interface{}
Result interface{}
}
// PregelExecutableTask represents a task that can be executed
type PregelExecutableTask struct {
PregelTask
Input interface{}
Node NodeRunnable
Writes []Write
Config RunnableConfig
Triggers []string
RetryPolicy interface{}
CacheKey *CacheKey
Writers map[string]interface{} // Flat writers
Subgraphs map[string]interface{} // Subgraphs
}
// StreamChunk is what the consumer receives.
type StreamChunk struct {
Namespace []string // sub-graph path (reserved for future use)
Mode StreamMode
Payload any
}
type StreamOptions struct {
Mode StreamMode
OutputChannels []string // defaults to all non-context channels
InterruptBefore []string // interrupt gate (before)
InterruptAfter []string // interrupt gate (after)
MaxConcurrency int // overrides config[ "max_concurrency" ]
CheckpointDuring *bool // nil → inherit config
Debug *bool // nil → inherit graph.debug
Context context.Context // optional, default = context.Background()
}
// CacheKey represents a key for caching
type CacheKey struct {
Namespace []string
Key string
TTL int64
}
type PregelNode struct {
Node NodeRunnable
Triggers []string
Metadata map[string]interface{}
Tags []string
CachePolicy interface{} // CachePolicy equivalent
RetryPolicy interface{} // RetryPolicy equivalent
FlatWriters map[string]interface{}
Subgraphs map[string]interface{}
Retry RetryPolicy
}
type NodeRunnable interface {
Invoke(ctx context.Context, input any, cfg RunnableConfig, loop LoopCallback) ([]Write, error)
}
type Write struct {
Channel string
Value any
}
// Checkpoint represents a checkpoint in the Pregel system
type Checkpoint struct {
ID string
ChannelValues map[string]interface{} `json:"channel_values,omitempty"`
ChannelVersions map[string]int64 `json:"channel_versions,omitempty"`
VersionsSeen map[string]interface{} `json:"versions_seen,omitempty"`
PendingSends []Send `json:"pending_sends,omitempty"`
Version int `json:"version,omitempty"`
Timestamp string `json:"timestamp,omitempty"`
}
// NewCheckpoint creates a new Checkpoint with all fields properly initialized
func NewCheckpoint() Checkpoint {
return Checkpoint{
ChannelValues: make(map[string]interface{}),
ChannelVersions: make(map[string]int64),
VersionsSeen: make(map[string]interface{}),
PendingSends: make([]Send, 0),
}
}
// Send represents a message to be sent to a node
type Send struct {
Node string
Arg interface{}
}
// Call represents a function call
type Call struct {
Func interface{} // Function to call
Input []interface{} // Arguments
Callbacks interface{} // Callbacks
CachePolicy interface{} // CachePolicy
Retry interface{} // RetryPolicy
}
// PregelTaskWrites represents writes from a task
type PregelTaskWrites struct {
Path []interface{}
Name string
Writes []interface{} // Deque in Python
Triggers []string
}
// ---------------------------------------------------------------------------
// Interfaces from previous snippets (slim versions here)
// ---------------------------------------------------------------------------
type RetryPolicy struct {
MaxAttempts int
BackoffMs int
}
type BaseChannel interface {
Set(v any)
Get() any
}
type simpleChan struct{ val atomicValue }
type atomicValue struct {
mu sync.RWMutex
v any
}
func (a *atomicValue) Store(v any) {
a.mu.Lock()
a.v = v
a.mu.Unlock()
}
func (a *atomicValue) Load() (v any) { a.mu.RLock(); v = a.v; a.mu.RUnlock(); return }
func (c *simpleChan) Set(v any) { c.val.Store(v) }
func (c *simpleChan) Get() any { return c.val.Load() }
// Managed values -------------------------------------------------------------
type WritableManagedValue interface {
Update([]any) error
}
type ManagedValueMapping map[string]WritableManagedValue
type SendPacket struct {
Node string
Arg any
}
type BaseCheckpointSaver interface {
Put(cfg RunnableConfig, cp Checkpoint, md map[string]any, newVers map[string]int) error
GetTuple(cfg RunnableConfig) (*Checkpoint, error)
}
// Stores ---------------------------------------------------------------------
type BaseStore interface{}
// Loop callback interface passed to Nodes for localWrite / localRead
type LoopCallback interface {
Send(taskID string, writes []Write)
Read(selectKeys []string) map[string]any
AcceptPush(originTask PregelExecutableTask, writeIdx int, call *Call) (*PregelExecutableTask, error)
}
// RunnableConfig represents configuration for a Runnable.
// Fields are optional
type RunnableConfig struct {
Tags []string `json:"tags,omitempty"` // Tags for this call and sub-calls.
Metadata map[string]interface{} `json:"metadata,omitempty"` // Metadata for this call and sub-calls.
Callbacks interface{} `json:"callbacks,omitempty"` // Callbacks for this call and sub-calls.
RunName *string `json:"run_name,omitempty"` // Name for the tracer run for this call.
MaxConcurrency int `json:"max_concurrency,omitempty"` // Max number of parallel calls.
RecursionLimit int `json:"recursion_limit,omitempty"` // Max recursion depth.
Configurable map[string]interface{} `json:"configurable,omitempty"` // Runtime values for configurable attributes.
RunID *string `json:"run_id,omitempty"` // Unique identifier for the tracer run (UUID as string).
}
+4
View File
@@ -0,0 +1,4 @@
.PHONY: build
build:
uv run python -m grpc_tools.protoc -I . --python_out=stubs/ --grpc_python_out=stubs/ --pyi_out=stubs/ server.proto
+63
View File
@@ -0,0 +1,63 @@
# LangGraph Worker Python gRPC Server
This directory contains a Python implementation of the gRPC server defined in `server.proto`. The server implements the `Worker` service which provides methods for streaming nodes and invoking reducers.
## Setup
1. Install the required dependencies:
```bash
pip install -r requirements.txt
```
2. Compile the Protocol Buffer definition to generate Python code:
```bash
python compile_proto.py
```
This will generate the necessary Python modules in the `stubs` directory.
## Server Implementation
The server implementation is in `grpc_server.py`. It provides:
- A `WorkerServicer` class that implements the `Worker` service defined in the proto file
- Methods to register handlers for nodes and reducers
- Helper methods to create write and error events
## Running the Server
To run the server:
```bash
python grpc_server.py [port]
```
By default, the server listens on port 50051.
## Customizing the Server
To customize the server behavior, modify the `register_handlers` function in `grpc_server.py` to register your own node and reducer handlers.
Example:
```python
def register_handlers(servicer: WorkerServicer):
# Custom node handler
def my_node_handler(inputs, config, path):
# Process inputs and return results
return {"output": b"Processed result"}
# Register the handler
servicer.register_node_handler("my_node", my_node_handler)
```
## Protocol Buffer Definition
The Protocol Buffer definition in `server.proto` defines:
- `Config`: Configuration for checkpoints
- `PregelExecutableTask`: Task information for execution
- `Event`: Output events (write or error)
- `Worker` service: Service with methods for streaming nodes and invoking reducers
+206
View File
@@ -0,0 +1,206 @@
import concurrent.futures
import logging
import sys
import time
from typing import Dict, Callable, Iterator, Dict
from stubs import server_pb2, server_pb2_grpc
import grpc
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
)
logger = logging.getLogger(__name__)
class WorkerServicer(server_pb2_grpc.WorkerServicer):
"""Implementation of the Worker service."""
def __init__(self):
# You might want to initialize resources here
self.node_handlers: Dict[str, Callable] = {}
self.reducer_handlers: Dict[str, Callable] = {}
def register_node_handler(self, name: str, handler: Callable):
"""Register a handler for a specific node."""
self.node_handlers[name] = handler
def register_reducer_handler(self, name: str, handler: Callable):
"""Register a handler for a specific reducer."""
self.reducer_handlers[name] = handler
def StreamNode(self, request: server_pb2.PregelExecutableTask,
context: grpc.ServicerContext) -> Iterator[server_pb2.Event]:
"""Call stream on a task.
Args:
request: The PregelExecutableTask containing task details
context: The gRPC context
Yields:
Event messages with write or error events
"""
logger.info(f"StreamNode called with task_id: {request.task_id}, name: {request.name}")
try:
# Check if we have a handler for this node
if request.name not in self.node_handlers:
error_msg = f"No handler registered for node: {request.name}"
logger.error(error_msg)
# Return an error event
yield self._create_error_event("handler_not_found", error_msg.encode())
return
# Call the handler
handler = self.node_handlers[request.name]
# Process inputs (you may need to deserialize them based on your needs)
inputs = request.input
# Call the handler and process its results
results = handler(inputs, request.config, request.path)
# Yield results as Event messages
for name, value in results.items():
yield self._create_write_event(name, value)
except Exception as e:
logger.exception(f"Error in StreamNode: {str(e)}")
yield self._create_error_event("internal_error", str(e).encode())
def InvokeReducer(self, request: server_pb2.PregelExecutableTask,
context: grpc.ServicerContext) -> Iterator[server_pb2.Event]:
"""Invoke a reducer.
Args:
request: The PregelExecutableTask containing task details
context: The gRPC context
Yields:
Event messages with write or error events
"""
logger.info(f"InvokeReducer called with task_id: {request.task_id}, name: {request.name}")
try:
# Check if we have a handler for this reducer
if request.name not in self.reducer_handlers:
error_msg = f"No handler registered for reducer: {request.name}"
logger.error(error_msg)
# Return an error event
yield self._create_error_event("handler_not_found", error_msg.encode())
return
# Call the handler
handler = self.reducer_handlers[request.name]
# Process inputs (you may need to deserialize them based on your needs)
inputs = request.input
# Call the handler and process its results
results = handler(inputs, request.config, request.path)
# Yield results as Event messages
for name, value in results.items():
yield self._create_write_event(name, value)
except Exception as e:
logger.exception(f"Error in InvokeReducer: {str(e)}")
yield self._create_error_event("internal_error", str(e).encode())
def _create_write_event(self, name: str, value: bytes) -> server_pb2.Event:
"""Create a write event."""
event = server_pb2.Event()
event.write.name = name
event.write.value = value
return event
def _create_error_event(self, name: str, value: bytes) -> server_pb2.Event:
"""Create an error event."""
event = server_pb2.Event()
event.error.name = name
event.error.value = value
return event
def serve(port: int = 50051, max_workers: int = 10):
"""Start the gRPC server.
Args:
port: The port to listen on
max_workers: Maximum number of worker threads
"""
server = grpc.server(
concurrent.futures.ThreadPoolExecutor(max_workers=max_workers)
)
# Create and register the servicer
servicer = WorkerServicer()
server_pb2_grpc.add_WorkerServicer_to_server(servicer, server)
# Add a secure port (you might want to add proper credentials in production)
server.add_insecure_port(f'[::]:{port}')
# Start the server
server.start()
logger.info(f"Server started, listening on port {port}")
# Keep the server running until interrupted
try:
while True:
time.sleep(86400) # Sleep for a day
except KeyboardInterrupt:
logger.info("Shutting down server...")
server.stop(0)
def register_handlers(servicer: WorkerServicer):
"""Register handlers for nodes and reducers.
This is where you would register your custom handlers for different
node types and reducers.
Args:
servicer: The WorkerServicer instance
"""
# Example node handler
def example_node_handler(inputs, config, path):
# Process inputs and return results
# This is just a placeholder implementation
return {"result": b"Example node result"}
# Example reducer handler
def example_reducer_handler(inputs, config, path):
# Process inputs and return results
# This is just a placeholder implementation
return {"result": b"Example reducer result"}
# Register handlers
servicer.register_node_handler("example_node", example_node_handler)
servicer.register_reducer_handler("example_reducer", example_reducer_handler)
def main():
"""Main entry point."""
# Parse command line arguments if needed
port = 50051
if len(sys.argv) > 1:
try:
port = int(sys.argv[1])
except ValueError:
logger.error(f"Invalid port number: {sys.argv[1]}")
sys.exit(1)
# Create the servicer
servicer = WorkerServicer()
# Register handlers
register_handlers(servicer)
# Start the server
serve(port=port)
if __name__ == "__main__":
main()
@@ -0,0 +1,15 @@
[project]
name = "worker-py"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
[dependency-groups]
dev = [
"grpcio>=1.71.0",
"grpcio-tools>=1.71.0",
"grpclib>=0.4.8",
"protobuf>=5.29.4",
]
+58
View File
@@ -0,0 +1,58 @@
syntax = "proto3";
package langgraph;
message Config {
string checkpoint_ns = 1;
}
message PregelExecutableTask{
string task_id = 1;
string name = 2;
repeated string input= 3;
Config config = 4;
repeated string path = 5;
}
message Event {
message Write {
string name = 1;
bytes value = 2;
}
message Error {
string name = 1;
bytes value = 2;
}
oneof event_oneof {
Write write = 1;
Error error = 2;
}
}
message Empty {
}
message ListGraphsResponse {
message Graph {
message Node {
string name = 1;
repeated string input = 2;
}
repeated Node nodes = 1;
repeated string channel_names = 2;
}
repeated Graph graphs = 1;
}
service Worker {
// Call stream on a task
rpc StreamNode(PregelExecutableTask) returns (stream Event) {}
// Invoke a reducer
rpc InvokeReducer(PregelExecutableTask) returns (stream Event) {}
// List available graphs
rpc ListGraphs(Empty) returns (ListGraphsResponse) {}
}
@@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: server.proto
# Protobuf Python Version: 5.29.0
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
5,
29,
0,
'',
'server.proto'
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0cserver.proto\x12\tlanggraph\"\x1f\n\x06\x43onfig\x12\x15\n\rcheckpoint_ns\x18\x01 \x01(\t\"u\n\x14PregelExecutableTask\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\r\n\x05input\x18\x03 \x03(\t\x12!\n\x06\x63onfig\x18\x04 \x01(\x0b\x32\x11.langgraph.Config\x12\x0c\n\x04path\x18\x05 \x03(\t\"\xb4\x01\n\x05\x45vent\x12\'\n\x05write\x18\x01 \x01(\x0b\x32\x16.langgraph.Event.WriteH\x00\x12\'\n\x05\x65rror\x18\x02 \x01(\x0b\x32\x16.langgraph.Event.ErrorH\x00\x1a$\n\x05Write\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x0c\x1a$\n\x05\x45rror\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x0c\x42\r\n\x0b\x65vent_oneof\"\x07\n\x05\x45mpty\"\xc7\x01\n\x12ListGraphsResponse\x12\x33\n\x06graphs\x18\x01 \x03(\x0b\x32#.langgraph.ListGraphsResponse.Graph\x1a|\n\x05Graph\x12\x37\n\x05nodes\x18\x01 \x03(\x0b\x32(.langgraph.ListGraphsResponse.Graph.Node\x12\x15\n\rchannel_names\x18\x02 \x03(\t\x1a#\n\x04Node\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05input\x18\x02 \x03(\t2\xd6\x01\n\x06Worker\x12\x43\n\nStreamNode\x12\x1f.langgraph.PregelExecutableTask\x1a\x10.langgraph.Event\"\x00\x30\x01\x12\x46\n\rInvokeReducer\x12\x1f.langgraph.PregelExecutableTask\x1a\x10.langgraph.Event\"\x00\x30\x01\x12?\n\nListGraphs\x12\x10.langgraph.Empty\x1a\x1d.langgraph.ListGraphsResponse\"\x00\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'server_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None
_globals['_CONFIG']._serialized_start=27
_globals['_CONFIG']._serialized_end=58
_globals['_PREGELEXECUTABLETASK']._serialized_start=60
_globals['_PREGELEXECUTABLETASK']._serialized_end=177
_globals['_EVENT']._serialized_start=180
_globals['_EVENT']._serialized_end=360
_globals['_EVENT_WRITE']._serialized_start=271
_globals['_EVENT_WRITE']._serialized_end=307
_globals['_EVENT_ERROR']._serialized_start=309
_globals['_EVENT_ERROR']._serialized_end=345
_globals['_EMPTY']._serialized_start=362
_globals['_EMPTY']._serialized_end=369
_globals['_LISTGRAPHSRESPONSE']._serialized_start=372
_globals['_LISTGRAPHSRESPONSE']._serialized_end=571
_globals['_LISTGRAPHSRESPONSE_GRAPH']._serialized_start=447
_globals['_LISTGRAPHSRESPONSE_GRAPH']._serialized_end=571
_globals['_LISTGRAPHSRESPONSE_GRAPH_NODE']._serialized_start=536
_globals['_LISTGRAPHSRESPONSE_GRAPH_NODE']._serialized_end=571
_globals['_WORKER']._serialized_start=574
_globals['_WORKER']._serialized_end=788
# @@protoc_insertion_point(module_scope)
@@ -0,0 +1,72 @@
from google.protobuf.internal import containers as _containers
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union
DESCRIPTOR: _descriptor.FileDescriptor
class Config(_message.Message):
__slots__ = ("checkpoint_ns",)
CHECKPOINT_NS_FIELD_NUMBER: _ClassVar[int]
checkpoint_ns: str
def __init__(self, checkpoint_ns: _Optional[str] = ...) -> None: ...
class PregelExecutableTask(_message.Message):
__slots__ = ("task_id", "name", "input", "config", "path")
TASK_ID_FIELD_NUMBER: _ClassVar[int]
NAME_FIELD_NUMBER: _ClassVar[int]
INPUT_FIELD_NUMBER: _ClassVar[int]
CONFIG_FIELD_NUMBER: _ClassVar[int]
PATH_FIELD_NUMBER: _ClassVar[int]
task_id: str
name: str
input: _containers.RepeatedScalarFieldContainer[str]
config: Config
path: _containers.RepeatedScalarFieldContainer[str]
def __init__(self, task_id: _Optional[str] = ..., name: _Optional[str] = ..., input: _Optional[_Iterable[str]] = ..., config: _Optional[_Union[Config, _Mapping]] = ..., path: _Optional[_Iterable[str]] = ...) -> None: ...
class Event(_message.Message):
__slots__ = ("write", "error")
class Write(_message.Message):
__slots__ = ("name", "value")
NAME_FIELD_NUMBER: _ClassVar[int]
VALUE_FIELD_NUMBER: _ClassVar[int]
name: str
value: bytes
def __init__(self, name: _Optional[str] = ..., value: _Optional[bytes] = ...) -> None: ...
class Error(_message.Message):
__slots__ = ("name", "value")
NAME_FIELD_NUMBER: _ClassVar[int]
VALUE_FIELD_NUMBER: _ClassVar[int]
name: str
value: bytes
def __init__(self, name: _Optional[str] = ..., value: _Optional[bytes] = ...) -> None: ...
WRITE_FIELD_NUMBER: _ClassVar[int]
ERROR_FIELD_NUMBER: _ClassVar[int]
write: Event.Write
error: Event.Error
def __init__(self, write: _Optional[_Union[Event.Write, _Mapping]] = ..., error: _Optional[_Union[Event.Error, _Mapping]] = ...) -> None: ...
class Empty(_message.Message):
__slots__ = ()
def __init__(self) -> None: ...
class ListGraphsResponse(_message.Message):
__slots__ = ("graphs",)
class Graph(_message.Message):
__slots__ = ("nodes", "channel_names")
class Node(_message.Message):
__slots__ = ("name", "input")
NAME_FIELD_NUMBER: _ClassVar[int]
INPUT_FIELD_NUMBER: _ClassVar[int]
name: str
input: _containers.RepeatedScalarFieldContainer[str]
def __init__(self, name: _Optional[str] = ..., input: _Optional[_Iterable[str]] = ...) -> None: ...
NODES_FIELD_NUMBER: _ClassVar[int]
CHANNEL_NAMES_FIELD_NUMBER: _ClassVar[int]
nodes: _containers.RepeatedCompositeFieldContainer[ListGraphsResponse.Graph.Node]
channel_names: _containers.RepeatedScalarFieldContainer[str]
def __init__(self, nodes: _Optional[_Iterable[_Union[ListGraphsResponse.Graph.Node, _Mapping]]] = ..., channel_names: _Optional[_Iterable[str]] = ...) -> None: ...
GRAPHS_FIELD_NUMBER: _ClassVar[int]
graphs: _containers.RepeatedCompositeFieldContainer[ListGraphsResponse.Graph]
def __init__(self, graphs: _Optional[_Iterable[_Union[ListGraphsResponse.Graph, _Mapping]]] = ...) -> None: ...
@@ -0,0 +1,186 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
import warnings
import server_pb2 as server__pb2
GRPC_GENERATED_VERSION = '1.71.0'
GRPC_VERSION = grpc.__version__
_version_not_supported = False
try:
from grpc._utilities import first_version_is_lower
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
except ImportError:
_version_not_supported = True
if _version_not_supported:
raise RuntimeError(
f'The grpc package installed is at version {GRPC_VERSION},'
+ f' but the generated code in server_pb2_grpc.py depends on'
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
)
class WorkerStub(object):
"""Missing associated documentation comment in .proto file."""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.StreamNode = channel.unary_stream(
'/langgraph.Worker/StreamNode',
request_serializer=server__pb2.PregelExecutableTask.SerializeToString,
response_deserializer=server__pb2.Event.FromString,
_registered_method=True)
self.InvokeReducer = channel.unary_stream(
'/langgraph.Worker/InvokeReducer',
request_serializer=server__pb2.PregelExecutableTask.SerializeToString,
response_deserializer=server__pb2.Event.FromString,
_registered_method=True)
self.ListGraphs = channel.unary_unary(
'/langgraph.Worker/ListGraphs',
request_serializer=server__pb2.Empty.SerializeToString,
response_deserializer=server__pb2.ListGraphsResponse.FromString,
_registered_method=True)
class WorkerServicer(object):
"""Missing associated documentation comment in .proto file."""
def StreamNode(self, request, context):
"""Call stream on a task
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def InvokeReducer(self, request, context):
"""Invoke a reducer
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def ListGraphs(self, request, context):
"""List available graphs
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def add_WorkerServicer_to_server(servicer, server):
rpc_method_handlers = {
'StreamNode': grpc.unary_stream_rpc_method_handler(
servicer.StreamNode,
request_deserializer=server__pb2.PregelExecutableTask.FromString,
response_serializer=server__pb2.Event.SerializeToString,
),
'InvokeReducer': grpc.unary_stream_rpc_method_handler(
servicer.InvokeReducer,
request_deserializer=server__pb2.PregelExecutableTask.FromString,
response_serializer=server__pb2.Event.SerializeToString,
),
'ListGraphs': grpc.unary_unary_rpc_method_handler(
servicer.ListGraphs,
request_deserializer=server__pb2.Empty.FromString,
response_serializer=server__pb2.ListGraphsResponse.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'langgraph.Worker', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))
server.add_registered_method_handlers('langgraph.Worker', rpc_method_handlers)
# This class is part of an EXPERIMENTAL API.
class Worker(object):
"""Missing associated documentation comment in .proto file."""
@staticmethod
def StreamNode(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_stream(
request,
target,
'/langgraph.Worker/StreamNode',
server__pb2.PregelExecutableTask.SerializeToString,
server__pb2.Event.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def InvokeReducer(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_stream(
request,
target,
'/langgraph.Worker/InvokeReducer',
server__pb2.PregelExecutableTask.SerializeToString,
server__pb2.Event.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def ListGraphs(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/langgraph.Worker/ListGraphs',
server__pb2.Empty.SerializeToString,
server__pb2.ListGraphsResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
+214
View File
@@ -0,0 +1,214 @@
version = 1
revision = 2
requires-python = ">=3.12"
[[package]]
name = "grpcio"
version = "1.71.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1c/95/aa11fc09a85d91fbc7dd405dcb2a1e0256989d67bf89fa65ae24b3ba105a/grpcio-1.71.0.tar.gz", hash = "sha256:2b85f7820475ad3edec209d3d89a7909ada16caab05d3f2e08a7e8ae3200a55c", size = 12549828, upload-time = "2025-03-10T19:28:49.203Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4c/83/bd4b6a9ba07825bd19c711d8b25874cd5de72c2a3fbf635c3c344ae65bd2/grpcio-1.71.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:0ff35c8d807c1c7531d3002be03221ff9ae15712b53ab46e2a0b4bb271f38537", size = 5184101, upload-time = "2025-03-10T19:24:54.11Z" },
{ url = "https://files.pythonhosted.org/packages/31/ea/2e0d90c0853568bf714693447f5c73272ea95ee8dad107807fde740e595d/grpcio-1.71.0-cp312-cp312-macosx_10_14_universal2.whl", hash = "sha256:b78a99cd1ece4be92ab7c07765a0b038194ded2e0a26fd654591ee136088d8d7", size = 11310927, upload-time = "2025-03-10T19:24:56.1Z" },
{ url = "https://files.pythonhosted.org/packages/ac/bc/07a3fd8af80467390af491d7dc66882db43884128cdb3cc8524915e0023c/grpcio-1.71.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:dc1a1231ed23caac1de9f943d031f1bc38d0f69d2a3b243ea0d664fc1fbd7fec", size = 5654280, upload-time = "2025-03-10T19:24:58.55Z" },
{ url = "https://files.pythonhosted.org/packages/16/af/21f22ea3eed3d0538b6ef7889fce1878a8ba4164497f9e07385733391e2b/grpcio-1.71.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6beeea5566092c5e3c4896c6d1d307fb46b1d4bdf3e70c8340b190a69198594", size = 6312051, upload-time = "2025-03-10T19:25:00.682Z" },
{ url = "https://files.pythonhosted.org/packages/49/9d/e12ddc726dc8bd1aa6cba67c85ce42a12ba5b9dd75d5042214a59ccf28ce/grpcio-1.71.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5170929109450a2c031cfe87d6716f2fae39695ad5335d9106ae88cc32dc84c", size = 5910666, upload-time = "2025-03-10T19:25:03.01Z" },
{ url = "https://files.pythonhosted.org/packages/d9/e9/38713d6d67aedef738b815763c25f092e0454dc58e77b1d2a51c9d5b3325/grpcio-1.71.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5b08d03ace7aca7b2fadd4baf291139b4a5f058805a8327bfe9aece7253b6d67", size = 6012019, upload-time = "2025-03-10T19:25:05.174Z" },
{ url = "https://files.pythonhosted.org/packages/80/da/4813cd7adbae6467724fa46c952d7aeac5e82e550b1c62ed2aeb78d444ae/grpcio-1.71.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f903017db76bf9cc2b2d8bdd37bf04b505bbccad6be8a81e1542206875d0e9db", size = 6637043, upload-time = "2025-03-10T19:25:06.987Z" },
{ url = "https://files.pythonhosted.org/packages/52/ca/c0d767082e39dccb7985c73ab4cf1d23ce8613387149e9978c70c3bf3b07/grpcio-1.71.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:469f42a0b410883185eab4689060a20488a1a0a00f8bbb3cbc1061197b4c5a79", size = 6186143, upload-time = "2025-03-10T19:25:08.877Z" },
{ url = "https://files.pythonhosted.org/packages/00/61/7b2c8ec13303f8fe36832c13d91ad4d4ba57204b1c723ada709c346b2271/grpcio-1.71.0-cp312-cp312-win32.whl", hash = "sha256:ad9f30838550695b5eb302add33f21f7301b882937460dd24f24b3cc5a95067a", size = 3604083, upload-time = "2025-03-10T19:25:10.736Z" },
{ url = "https://files.pythonhosted.org/packages/fd/7c/1e429c5fb26122055d10ff9a1d754790fb067d83c633ff69eddcf8e3614b/grpcio-1.71.0-cp312-cp312-win_amd64.whl", hash = "sha256:652350609332de6dac4ece254e5d7e1ff834e203d6afb769601f286886f6f3a8", size = 4272191, upload-time = "2025-03-10T19:25:13.12Z" },
{ url = "https://files.pythonhosted.org/packages/04/dd/b00cbb45400d06b26126dcfdbdb34bb6c4f28c3ebbd7aea8228679103ef6/grpcio-1.71.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:cebc1b34ba40a312ab480ccdb396ff3c529377a2fce72c45a741f7215bfe8379", size = 5184138, upload-time = "2025-03-10T19:25:15.101Z" },
{ url = "https://files.pythonhosted.org/packages/ed/0a/4651215983d590ef53aac40ba0e29dda941a02b097892c44fa3357e706e5/grpcio-1.71.0-cp313-cp313-macosx_10_14_universal2.whl", hash = "sha256:85da336e3649a3d2171e82f696b5cad2c6231fdd5bad52616476235681bee5b3", size = 11310747, upload-time = "2025-03-10T19:25:17.201Z" },
{ url = "https://files.pythonhosted.org/packages/57/a3/149615b247f321e13f60aa512d3509d4215173bdb982c9098d78484de216/grpcio-1.71.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:f9a412f55bb6e8f3bb000e020dbc1e709627dcb3a56f6431fa7076b4c1aab0db", size = 5653991, upload-time = "2025-03-10T19:25:20.39Z" },
{ url = "https://files.pythonhosted.org/packages/ca/56/29432a3e8d951b5e4e520a40cd93bebaa824a14033ea8e65b0ece1da6167/grpcio-1.71.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47be9584729534660416f6d2a3108aaeac1122f6b5bdbf9fd823e11fe6fbaa29", size = 6312781, upload-time = "2025-03-10T19:25:22.823Z" },
{ url = "https://files.pythonhosted.org/packages/a3/f8/286e81a62964ceb6ac10b10925261d4871a762d2a763fbf354115f9afc98/grpcio-1.71.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c9c80ac6091c916db81131d50926a93ab162a7e97e4428ffc186b6e80d6dda4", size = 5910479, upload-time = "2025-03-10T19:25:24.828Z" },
{ url = "https://files.pythonhosted.org/packages/35/67/d1febb49ec0f599b9e6d4d0d44c2d4afdbed9c3e80deb7587ec788fcf252/grpcio-1.71.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:789d5e2a3a15419374b7b45cd680b1e83bbc1e52b9086e49308e2c0b5bbae6e3", size = 6013262, upload-time = "2025-03-10T19:25:26.987Z" },
{ url = "https://files.pythonhosted.org/packages/a1/04/f9ceda11755f0104a075ad7163fc0d96e2e3a9fe25ef38adfc74c5790daf/grpcio-1.71.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:1be857615e26a86d7363e8a163fade914595c81fec962b3d514a4b1e8760467b", size = 6643356, upload-time = "2025-03-10T19:25:29.606Z" },
{ url = "https://files.pythonhosted.org/packages/fb/ce/236dbc3dc77cf9a9242adcf1f62538734ad64727fabf39e1346ad4bd5c75/grpcio-1.71.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a76d39b5fafd79ed604c4be0a869ec3581a172a707e2a8d7a4858cb05a5a7637", size = 6186564, upload-time = "2025-03-10T19:25:31.537Z" },
{ url = "https://files.pythonhosted.org/packages/10/fd/b3348fce9dd4280e221f513dd54024e765b21c348bc475516672da4218e9/grpcio-1.71.0-cp313-cp313-win32.whl", hash = "sha256:74258dce215cb1995083daa17b379a1a5a87d275387b7ffe137f1d5131e2cfbb", size = 3601890, upload-time = "2025-03-10T19:25:33.421Z" },
{ url = "https://files.pythonhosted.org/packages/be/f8/db5d5f3fc7e296166286c2a397836b8b042f7ad1e11028d82b061701f0f7/grpcio-1.71.0-cp313-cp313-win_amd64.whl", hash = "sha256:22c3bc8d488c039a199f7a003a38cb7635db6656fa96437a8accde8322ce2366", size = 4273308, upload-time = "2025-03-10T19:25:35.79Z" },
]
[[package]]
name = "grpcio-tools"
version = "1.71.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "grpcio" },
{ name = "protobuf" },
{ name = "setuptools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/05/d2/c0866a48c355a6a4daa1f7e27e210c7fa561b1f3b7c0bce2671e89cfa31e/grpcio_tools-1.71.0.tar.gz", hash = "sha256:38dba8e0d5e0fb23a034e09644fdc6ed862be2371887eee54901999e8f6792a8", size = 5326008, upload-time = "2025-03-10T19:29:03.38Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/de/e4/156956b92ad0298290c3d68e6670bc5a6fbefcccfe1ec3997480605e7135/grpcio_tools-1.71.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:61c0409d5bdac57a7bd0ce0ab01c1c916728fe4c8a03d77a25135ad481eb505c", size = 2385480, upload-time = "2025-03-10T19:27:46.425Z" },
{ url = "https://files.pythonhosted.org/packages/c1/08/9930eb4bb38c5214041c9f24f8b35e9864a7938282db986836546c782d52/grpcio_tools-1.71.0-cp312-cp312-macosx_10_14_universal2.whl", hash = "sha256:28784f39921d061d2164a9dcda5164a69d07bf29f91f0ea50b505958292312c9", size = 5951891, upload-time = "2025-03-10T19:27:48.219Z" },
{ url = "https://files.pythonhosted.org/packages/73/65/931f29ec9c33719d48e1e30446ecce6f5d2cd4e4934fa73fbe07de41c43b/grpcio_tools-1.71.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:192808cf553cedca73f0479cc61d5684ad61f24db7a5f3c4dfe1500342425866", size = 2351967, upload-time = "2025-03-10T19:27:50.09Z" },
{ url = "https://files.pythonhosted.org/packages/b8/26/2ec8748534406214f20a4809c36efcfa88d1a26246e8312102e3ef8c295d/grpcio_tools-1.71.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:989ee9da61098230d3d4c8f8f8e27c2de796f1ff21b1c90110e636d9acd9432b", size = 2745003, upload-time = "2025-03-10T19:27:52.333Z" },
{ url = "https://files.pythonhosted.org/packages/f1/33/87b4610c86a4e10ee446b543a4d536f94ab04f828bab841f0bc1a083de72/grpcio_tools-1.71.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:541a756276c8a55dec991f6c0106ae20c8c8f5ce8d0bdbfcb01e2338d1a8192b", size = 2476455, upload-time = "2025-03-10T19:27:54.493Z" },
{ url = "https://files.pythonhosted.org/packages/00/7c/f7f0cc36a43be9d45b3ce2a55245f3c7d063a24b7930dd719929e58871a4/grpcio_tools-1.71.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:870c0097700d13c403e5517cb7750ab5b4a791ce3e71791c411a38c5468b64bd", size = 2854333, upload-time = "2025-03-10T19:27:56.693Z" },
{ url = "https://files.pythonhosted.org/packages/07/c4/34b9ea62b173c13fa7accba5f219355b320c05c80c79c3ba70fe52f47b2f/grpcio_tools-1.71.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:abd57f615e88bf93c3c6fd31f923106e3beb12f8cd2df95b0d256fa07a7a0a57", size = 3304297, upload-time = "2025-03-10T19:27:58.437Z" },
{ url = "https://files.pythonhosted.org/packages/5c/ef/9d3449db8a07688dc3de7dcbd2a07048a128610b1a491c5c0cb3e90a00c5/grpcio_tools-1.71.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:753270e2d06d37e6d7af8967d1d059ec635ad215882041a36294f4e2fd502b2e", size = 2916212, upload-time = "2025-03-10T19:28:00.208Z" },
{ url = "https://files.pythonhosted.org/packages/2e/c6/990e8194c934dfe7cf89ef307c319fa4f2bc0b78aeca707addbfa1e502f1/grpcio_tools-1.71.0-cp312-cp312-win32.whl", hash = "sha256:0e647794bd7138b8c215e86277a9711a95cf6a03ff6f9e555d54fdf7378b9f9d", size = 948849, upload-time = "2025-03-10T19:28:01.81Z" },
{ url = "https://files.pythonhosted.org/packages/42/95/3c36d3205e6bd19853cc2420e44b6ef302eb4cfcf56498973c7e85f6c03b/grpcio_tools-1.71.0-cp312-cp312-win_amd64.whl", hash = "sha256:48debc879570972d28bfe98e4970eff25bb26da3f383e0e49829b2d2cd35ad87", size = 1120294, upload-time = "2025-03-10T19:28:03.517Z" },
{ url = "https://files.pythonhosted.org/packages/84/a7/70dc7e9957bcbaccd4dcb6cc11215e0b918f546d55599221522fe0d073e0/grpcio_tools-1.71.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:9a78d07d6c301a25ef5ede962920a522556a1dfee1ccc05795994ceb867f766c", size = 2384758, upload-time = "2025-03-10T19:28:05.327Z" },
{ url = "https://files.pythonhosted.org/packages/65/79/57320b28d0a0c5ec94095fd571a65292f8ed7e1c47e59ae4021e8a48d49b/grpcio_tools-1.71.0-cp313-cp313-macosx_10_14_universal2.whl", hash = "sha256:580ac88141c9815557e63c9c04f5b1cdb19b4db8d0cb792b573354bde1ee8b12", size = 5951661, upload-time = "2025-03-10T19:28:07.879Z" },
{ url = "https://files.pythonhosted.org/packages/80/3d/343df5ed7c5dd66fc7a19e4ef3e97ccc4f5d802122b04cd6492f0dcd79f5/grpcio_tools-1.71.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:f7c678e68ece0ae908ecae1c4314a0c2c7f83e26e281738b9609860cc2c82d96", size = 2351571, upload-time = "2025-03-10T19:28:09.909Z" },
{ url = "https://files.pythonhosted.org/packages/56/2f/b9736e8c84e880c4237f5b880c6c799b4977c5cde190999bc7ab4b2ec445/grpcio_tools-1.71.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56ecd6cc89b5e5eed1de5eb9cafce86c9c9043ee3840888cc464d16200290b53", size = 2744580, upload-time = "2025-03-10T19:28:11.866Z" },
{ url = "https://files.pythonhosted.org/packages/76/9b/bdb384967353da7bf64bac4232f4cf8ae43f19d0f2f640978d4d4197e667/grpcio_tools-1.71.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e52a041afc20ab2431d756b6295d727bd7adee813b21b06a3483f4a7a15ea15f", size = 2475978, upload-time = "2025-03-10T19:28:14.236Z" },
{ url = "https://files.pythonhosted.org/packages/26/71/1411487fd7862d347b98fda5e3beef611a71b2ac2faac62a965d9e2536b3/grpcio_tools-1.71.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2a1712f12102b60c8d92779b89d0504e0d6f3a59f2b933e5622b8583f5c02992", size = 2853314, upload-time = "2025-03-10T19:28:16.085Z" },
{ url = "https://files.pythonhosted.org/packages/03/06/59d0523eb1ba2f64edc72cb150152fa1b2e77061cae3ef3ecd3ef2a87f51/grpcio_tools-1.71.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:41878cb7a75477e62fdd45e7e9155b3af1b7a5332844021e2511deaf99ac9e6c", size = 3303981, upload-time = "2025-03-10T19:28:18.129Z" },
{ url = "https://files.pythonhosted.org/packages/c2/71/fb9fb49f2b738ec1dfbbc8cdce0b26e5f9c5fc0edef72e453580620d6a36/grpcio_tools-1.71.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:682e958b476049ccc14c71bedf3f979bced01f6e0c04852efc5887841a32ad6b", size = 2915876, upload-time = "2025-03-10T19:28:20.045Z" },
{ url = "https://files.pythonhosted.org/packages/bd/0f/0d49f6fe6fa2d09e9820dd9eeb30437e86002303076be2b6ada0fb52b8f2/grpcio_tools-1.71.0-cp313-cp313-win32.whl", hash = "sha256:0ccfb837152b7b858b9f26bb110b3ae8c46675d56130f6c2f03605c4f129be13", size = 948245, upload-time = "2025-03-10T19:28:21.876Z" },
{ url = "https://files.pythonhosted.org/packages/bb/14/ab131a39187bfea950280b2277a82d2033469fe8c86f73b10b19f53cc5ca/grpcio_tools-1.71.0-cp313-cp313-win_amd64.whl", hash = "sha256:ffff9bc5eacb34dd26b487194f7d44a3e64e752fc2cf049d798021bf25053b87", size = 1119649, upload-time = "2025-03-10T19:28:23.679Z" },
]
[[package]]
name = "grpclib"
version = "0.4.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "h2" },
{ name = "multidict" },
]
sdist = { url = "https://files.pythonhosted.org/packages/19/75/0f0d3524b38b35e5cd07334b754aa9bd0570140ad982131b04ebfa3b0374/grpclib-0.4.8.tar.gz", hash = "sha256:d8823763780ef94fed8b2c562f7485cf0bbee15fc7d065a640673667f7719c9a", size = 62793, upload-time = "2025-05-04T16:27:30.051Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/03/8b/ad381ec1b8195fa4a9a693cb8087e031b99530c0d6b8ad036dcb99e144c4/grpclib-0.4.8-py3-none-any.whl", hash = "sha256:a5047733a7acc1c1cee6abf3c841c7c6fab67d2844a45a853b113fa2e6cd2654", size = 76311, upload-time = "2025-05-04T16:27:22.818Z" },
]
[[package]]
name = "h2"
version = "4.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "hpack" },
{ name = "hyperframe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1b/38/d7f80fd13e6582fb8e0df8c9a653dcc02b03ca34f4d72f34869298c5baf8/h2-4.2.0.tar.gz", hash = "sha256:c8a52129695e88b1a0578d8d2cc6842bbd79128ac685463b887ee278126ad01f", size = 2150682, upload-time = "2025-02-02T07:43:51.815Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/9e/984486f2d0a0bd2b024bf4bc1c62688fcafa9e61991f041fb0e2def4a982/h2-4.2.0-py3-none-any.whl", hash = "sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0", size = 60957, upload-time = "2025-02-01T11:02:26.481Z" },
]
[[package]]
name = "hpack"
version = "4.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" },
]
[[package]]
name = "hyperframe"
version = "6.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" },
]
[[package]]
name = "multidict"
version = "6.4.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/91/2f/a3470242707058fe856fe59241eee5635d79087100b7042a867368863a27/multidict-6.4.4.tar.gz", hash = "sha256:69ee9e6ba214b5245031b76233dd95408a0fd57fdb019ddcc1ead4790932a8e8", size = 90183, upload-time = "2025-05-19T14:16:37.381Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/b5/5675377da23d60875fe7dae6be841787755878e315e2f517235f22f59e18/multidict-6.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dc388f75a1c00000824bf28b7633e40854f4127ede80512b44c3cfeeea1839a2", size = 64293, upload-time = "2025-05-19T14:14:44.724Z" },
{ url = "https://files.pythonhosted.org/packages/34/a7/be384a482754bb8c95d2bbe91717bf7ccce6dc38c18569997a11f95aa554/multidict-6.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:98af87593a666f739d9dba5d0ae86e01b0e1a9cfcd2e30d2d361fbbbd1a9162d", size = 38096, upload-time = "2025-05-19T14:14:45.95Z" },
{ url = "https://files.pythonhosted.org/packages/66/6d/d59854bb4352306145bdfd1704d210731c1bb2c890bfee31fb7bbc1c4c7f/multidict-6.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aff4cafea2d120327d55eadd6b7f1136a8e5a0ecf6fb3b6863e8aca32cd8e50a", size = 37214, upload-time = "2025-05-19T14:14:47.158Z" },
{ url = "https://files.pythonhosted.org/packages/99/e0/c29d9d462d7cfc5fc8f9bf24f9c6843b40e953c0b55e04eba2ad2cf54fba/multidict-6.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:169c4ba7858176b797fe551d6e99040c531c775d2d57b31bcf4de6d7a669847f", size = 224686, upload-time = "2025-05-19T14:14:48.366Z" },
{ url = "https://files.pythonhosted.org/packages/dc/4a/da99398d7fd8210d9de068f9a1b5f96dfaf67d51e3f2521f17cba4ee1012/multidict-6.4.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b9eb4c59c54421a32b3273d4239865cb14ead53a606db066d7130ac80cc8ec93", size = 231061, upload-time = "2025-05-19T14:14:49.952Z" },
{ url = "https://files.pythonhosted.org/packages/21/f5/ac11add39a0f447ac89353e6ca46666847051103649831c08a2800a14455/multidict-6.4.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7cf3bd54c56aa16fdb40028d545eaa8d051402b61533c21e84046e05513d5780", size = 232412, upload-time = "2025-05-19T14:14:51.812Z" },
{ url = "https://files.pythonhosted.org/packages/d9/11/4b551e2110cded705a3c13a1d4b6a11f73891eb5a1c449f1b2b6259e58a6/multidict-6.4.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f682c42003c7264134bfe886376299db4cc0c6cd06a3295b41b347044bcb5482", size = 231563, upload-time = "2025-05-19T14:14:53.262Z" },
{ url = "https://files.pythonhosted.org/packages/4c/02/751530c19e78fe73b24c3da66618eda0aa0d7f6e7aa512e46483de6be210/multidict-6.4.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920f9cf2abdf6e493c519492d892c362007f113c94da4c239ae88429835bad1", size = 223811, upload-time = "2025-05-19T14:14:55.232Z" },
{ url = "https://files.pythonhosted.org/packages/c7/cb/2be8a214643056289e51ca356026c7b2ce7225373e7a1f8c8715efee8988/multidict-6.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:530d86827a2df6504526106b4c104ba19044594f8722d3e87714e847c74a0275", size = 216524, upload-time = "2025-05-19T14:14:57.226Z" },
{ url = "https://files.pythonhosted.org/packages/19/f3/6d5011ec375c09081f5250af58de85f172bfcaafebff286d8089243c4bd4/multidict-6.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ecde56ea2439b96ed8a8d826b50c57364612ddac0438c39e473fafad7ae1c23b", size = 229012, upload-time = "2025-05-19T14:14:58.597Z" },
{ url = "https://files.pythonhosted.org/packages/67/9c/ca510785df5cf0eaf5b2a8132d7d04c1ce058dcf2c16233e596ce37a7f8e/multidict-6.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:dc8c9736d8574b560634775ac0def6bdc1661fc63fa27ffdfc7264c565bcb4f2", size = 226765, upload-time = "2025-05-19T14:15:00.048Z" },
{ url = "https://files.pythonhosted.org/packages/36/c8/ca86019994e92a0f11e642bda31265854e6ea7b235642f0477e8c2e25c1f/multidict-6.4.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7f3d3b3c34867579ea47cbd6c1f2ce23fbfd20a273b6f9e3177e256584f1eacc", size = 222888, upload-time = "2025-05-19T14:15:01.568Z" },
{ url = "https://files.pythonhosted.org/packages/c6/67/bc25a8e8bd522935379066950ec4e2277f9b236162a73548a2576d4b9587/multidict-6.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:87a728af265e08f96b6318ebe3c0f68b9335131f461efab2fc64cc84a44aa6ed", size = 234041, upload-time = "2025-05-19T14:15:03.759Z" },
{ url = "https://files.pythonhosted.org/packages/f1/a0/70c4c2d12857fccbe607b334b7ee28b6b5326c322ca8f73ee54e70d76484/multidict-6.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9f193eeda1857f8e8d3079a4abd258f42ef4a4bc87388452ed1e1c4d2b0c8740", size = 231046, upload-time = "2025-05-19T14:15:05.698Z" },
{ url = "https://files.pythonhosted.org/packages/c1/0f/52954601d02d39742aab01d6b92f53c1dd38b2392248154c50797b4df7f1/multidict-6.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be06e73c06415199200e9a2324a11252a3d62030319919cde5e6950ffeccf72e", size = 227106, upload-time = "2025-05-19T14:15:07.124Z" },
{ url = "https://files.pythonhosted.org/packages/af/24/679d83ec4379402d28721790dce818e5d6b9f94ce1323a556fb17fa9996c/multidict-6.4.4-cp312-cp312-win32.whl", hash = "sha256:622f26ea6a7e19b7c48dd9228071f571b2fbbd57a8cd71c061e848f281550e6b", size = 35351, upload-time = "2025-05-19T14:15:08.556Z" },
{ url = "https://files.pythonhosted.org/packages/52/ef/40d98bc5f986f61565f9b345f102409534e29da86a6454eb6b7c00225a13/multidict-6.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:5e2bcda30d5009996ff439e02a9f2b5c3d64a20151d34898c000a6281faa3781", size = 38791, upload-time = "2025-05-19T14:15:09.825Z" },
{ url = "https://files.pythonhosted.org/packages/df/2a/e166d2ffbf4b10131b2d5b0e458f7cee7d986661caceae0de8753042d4b2/multidict-6.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:82ffabefc8d84c2742ad19c37f02cde5ec2a1ee172d19944d380f920a340e4b9", size = 64123, upload-time = "2025-05-19T14:15:11.044Z" },
{ url = "https://files.pythonhosted.org/packages/8c/96/e200e379ae5b6f95cbae472e0199ea98913f03d8c9a709f42612a432932c/multidict-6.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6a2f58a66fe2c22615ad26156354005391e26a2f3721c3621504cd87c1ea87bf", size = 38049, upload-time = "2025-05-19T14:15:12.902Z" },
{ url = "https://files.pythonhosted.org/packages/75/fb/47afd17b83f6a8c7fa863c6d23ac5ba6a0e6145ed8a6bcc8da20b2b2c1d2/multidict-6.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5883d6ee0fd9d8a48e9174df47540b7545909841ac82354c7ae4cbe9952603bd", size = 37078, upload-time = "2025-05-19T14:15:14.282Z" },
{ url = "https://files.pythonhosted.org/packages/fa/70/1af3143000eddfb19fd5ca5e78393985ed988ac493bb859800fe0914041f/multidict-6.4.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9abcf56a9511653fa1d052bfc55fbe53dbee8f34e68bd6a5a038731b0ca42d15", size = 224097, upload-time = "2025-05-19T14:15:15.566Z" },
{ url = "https://files.pythonhosted.org/packages/b1/39/d570c62b53d4fba844e0378ffbcd02ac25ca423d3235047013ba2f6f60f8/multidict-6.4.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6ed5ae5605d4ad5a049fad2a28bb7193400700ce2f4ae484ab702d1e3749c3f9", size = 230768, upload-time = "2025-05-19T14:15:17.308Z" },
{ url = "https://files.pythonhosted.org/packages/fd/f8/ed88f2c4d06f752b015933055eb291d9bc184936903752c66f68fb3c95a7/multidict-6.4.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbfcb60396f9bcfa63e017a180c3105b8c123a63e9d1428a36544e7d37ca9e20", size = 231331, upload-time = "2025-05-19T14:15:18.73Z" },
{ url = "https://files.pythonhosted.org/packages/9c/6f/8e07cffa32f483ab887b0d56bbd8747ac2c1acd00dc0af6fcf265f4a121e/multidict-6.4.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0f1987787f5f1e2076b59692352ab29a955b09ccc433c1f6b8e8e18666f608b", size = 230169, upload-time = "2025-05-19T14:15:20.179Z" },
{ url = "https://files.pythonhosted.org/packages/e6/2b/5dcf173be15e42f330110875a2668ddfc208afc4229097312212dc9c1236/multidict-6.4.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d0121ccce8c812047d8d43d691a1ad7641f72c4f730474878a5aeae1b8ead8c", size = 222947, upload-time = "2025-05-19T14:15:21.714Z" },
{ url = "https://files.pythonhosted.org/packages/39/75/4ddcbcebe5ebcd6faa770b629260d15840a5fc07ce8ad295a32e14993726/multidict-6.4.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83ec4967114295b8afd120a8eec579920c882831a3e4c3331d591a8e5bfbbc0f", size = 215761, upload-time = "2025-05-19T14:15:23.242Z" },
{ url = "https://files.pythonhosted.org/packages/6a/c9/55e998ae45ff15c5608e384206aa71a11e1b7f48b64d166db400b14a3433/multidict-6.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:995f985e2e268deaf17867801b859a282e0448633f1310e3704b30616d269d69", size = 227605, upload-time = "2025-05-19T14:15:24.763Z" },
{ url = "https://files.pythonhosted.org/packages/04/49/c2404eac74497503c77071bd2e6f88c7e94092b8a07601536b8dbe99be50/multidict-6.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d832c608f94b9f92a0ec8b7e949be7792a642b6e535fcf32f3e28fab69eeb046", size = 226144, upload-time = "2025-05-19T14:15:26.249Z" },
{ url = "https://files.pythonhosted.org/packages/62/c5/0cd0c3c6f18864c40846aa2252cd69d308699cb163e1c0d989ca301684da/multidict-6.4.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d21c1212171cf7da703c5b0b7a0e85be23b720818aef502ad187d627316d5645", size = 221100, upload-time = "2025-05-19T14:15:28.303Z" },
{ url = "https://files.pythonhosted.org/packages/71/7b/f2f3887bea71739a046d601ef10e689528d4f911d84da873b6be9194ffea/multidict-6.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:cbebaa076aaecad3d4bb4c008ecc73b09274c952cf6a1b78ccfd689e51f5a5b0", size = 232731, upload-time = "2025-05-19T14:15:30.263Z" },
{ url = "https://files.pythonhosted.org/packages/e5/b3/d9de808349df97fa75ec1372758701b5800ebad3c46ae377ad63058fbcc6/multidict-6.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c93a6fb06cc8e5d3628b2b5fda215a5db01e8f08fc15fadd65662d9b857acbe4", size = 229637, upload-time = "2025-05-19T14:15:33.337Z" },
{ url = "https://files.pythonhosted.org/packages/5e/57/13207c16b615eb4f1745b44806a96026ef8e1b694008a58226c2d8f5f0a5/multidict-6.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8cd8f81f1310182362fb0c7898145ea9c9b08a71081c5963b40ee3e3cac589b1", size = 225594, upload-time = "2025-05-19T14:15:34.832Z" },
{ url = "https://files.pythonhosted.org/packages/3a/e4/d23bec2f70221604f5565000632c305fc8f25ba953e8ce2d8a18842b9841/multidict-6.4.4-cp313-cp313-win32.whl", hash = "sha256:3e9f1cd61a0ab857154205fb0b1f3d3ace88d27ebd1409ab7af5096e409614cd", size = 35359, upload-time = "2025-05-19T14:15:36.246Z" },
{ url = "https://files.pythonhosted.org/packages/a7/7a/cfe1a47632be861b627f46f642c1d031704cc1c0f5c0efbde2ad44aa34bd/multidict-6.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:8ffb40b74400e4455785c2fa37eba434269149ec525fc8329858c862e4b35373", size = 38903, upload-time = "2025-05-19T14:15:37.507Z" },
{ url = "https://files.pythonhosted.org/packages/68/7b/15c259b0ab49938a0a1c8f3188572802704a779ddb294edc1b2a72252e7c/multidict-6.4.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6a602151dbf177be2450ef38966f4be3467d41a86c6a845070d12e17c858a156", size = 68895, upload-time = "2025-05-19T14:15:38.856Z" },
{ url = "https://files.pythonhosted.org/packages/f1/7d/168b5b822bccd88142e0a3ce985858fea612404edd228698f5af691020c9/multidict-6.4.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d2b9712211b860d123815a80b859075d86a4d54787e247d7fbee9db6832cf1c", size = 40183, upload-time = "2025-05-19T14:15:40.197Z" },
{ url = "https://files.pythonhosted.org/packages/e0/b7/d4b8d98eb850ef28a4922ba508c31d90715fd9b9da3801a30cea2967130b/multidict-6.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d2fa86af59f8fc1972e121ade052145f6da22758f6996a197d69bb52f8204e7e", size = 39592, upload-time = "2025-05-19T14:15:41.508Z" },
{ url = "https://files.pythonhosted.org/packages/18/28/a554678898a19583548e742080cf55d169733baf57efc48c2f0273a08583/multidict-6.4.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50855d03e9e4d66eab6947ba688ffb714616f985838077bc4b490e769e48da51", size = 226071, upload-time = "2025-05-19T14:15:42.877Z" },
{ url = "https://files.pythonhosted.org/packages/ee/dc/7ba6c789d05c310e294f85329efac1bf5b450338d2542498db1491a264df/multidict-6.4.4-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5bce06b83be23225be1905dcdb6b789064fae92499fbc458f59a8c0e68718601", size = 222597, upload-time = "2025-05-19T14:15:44.412Z" },
{ url = "https://files.pythonhosted.org/packages/24/4f/34eadbbf401b03768dba439be0fb94b0d187facae9142821a3d5599ccb3b/multidict-6.4.4-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66ed0731f8e5dfd8369a883b6e564aca085fb9289aacabd9decd70568b9a30de", size = 228253, upload-time = "2025-05-19T14:15:46.474Z" },
{ url = "https://files.pythonhosted.org/packages/c0/e6/493225a3cdb0d8d80d43a94503fc313536a07dae54a3f030d279e629a2bc/multidict-6.4.4-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:329ae97fc2f56f44d91bc47fe0972b1f52d21c4b7a2ac97040da02577e2daca2", size = 226146, upload-time = "2025-05-19T14:15:48.003Z" },
{ url = "https://files.pythonhosted.org/packages/2f/70/e411a7254dc3bff6f7e6e004303b1b0591358e9f0b7c08639941e0de8bd6/multidict-6.4.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c27e5dcf520923d6474d98b96749e6805f7677e93aaaf62656005b8643f907ab", size = 220585, upload-time = "2025-05-19T14:15:49.546Z" },
{ url = "https://files.pythonhosted.org/packages/08/8f/beb3ae7406a619100d2b1fb0022c3bb55a8225ab53c5663648ba50dfcd56/multidict-6.4.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:058cc59b9e9b143cc56715e59e22941a5d868c322242278d28123a5d09cdf6b0", size = 212080, upload-time = "2025-05-19T14:15:51.151Z" },
{ url = "https://files.pythonhosted.org/packages/9c/ec/355124e9d3d01cf8edb072fd14947220f357e1c5bc79c88dff89297e9342/multidict-6.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:69133376bc9a03f8c47343d33f91f74a99c339e8b58cea90433d8e24bb298031", size = 226558, upload-time = "2025-05-19T14:15:52.665Z" },
{ url = "https://files.pythonhosted.org/packages/fd/22/d2b95cbebbc2ada3be3812ea9287dcc9712d7f1a012fad041770afddb2ad/multidict-6.4.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d6b15c55721b1b115c5ba178c77104123745b1417527ad9641a4c5e2047450f0", size = 212168, upload-time = "2025-05-19T14:15:55.279Z" },
{ url = "https://files.pythonhosted.org/packages/4d/c5/62bfc0b2f9ce88326dbe7179f9824a939c6c7775b23b95de777267b9725c/multidict-6.4.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a887b77f51d3d41e6e1a63cf3bc7ddf24de5939d9ff69441387dfefa58ac2e26", size = 217970, upload-time = "2025-05-19T14:15:56.806Z" },
{ url = "https://files.pythonhosted.org/packages/79/74/977cea1aadc43ff1c75d23bd5bc4768a8fac98c14e5878d6ee8d6bab743c/multidict-6.4.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:632a3bf8f1787f7ef7d3c2f68a7bde5be2f702906f8b5842ad6da9d974d0aab3", size = 226980, upload-time = "2025-05-19T14:15:58.313Z" },
{ url = "https://files.pythonhosted.org/packages/48/fc/cc4a1a2049df2eb84006607dc428ff237af38e0fcecfdb8a29ca47b1566c/multidict-6.4.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a145c550900deb7540973c5cdb183b0d24bed6b80bf7bddf33ed8f569082535e", size = 220641, upload-time = "2025-05-19T14:15:59.866Z" },
{ url = "https://files.pythonhosted.org/packages/3b/6a/a7444d113ab918701988d4abdde373dbdfd2def7bd647207e2bf645c7eac/multidict-6.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc5d83c6619ca5c9672cb78b39ed8542f1975a803dee2cda114ff73cbb076edd", size = 221728, upload-time = "2025-05-19T14:16:01.535Z" },
{ url = "https://files.pythonhosted.org/packages/2b/b0/fdf4c73ad1c55e0f4dbbf2aa59dd37037334091f9a4961646d2b7ac91a86/multidict-6.4.4-cp313-cp313t-win32.whl", hash = "sha256:3312f63261b9df49be9d57aaa6abf53a6ad96d93b24f9cc16cf979956355ce6e", size = 41913, upload-time = "2025-05-19T14:16:03.199Z" },
{ url = "https://files.pythonhosted.org/packages/8e/92/27989ecca97e542c0d01d05a98a5ae12198a243a9ee12563a0313291511f/multidict-6.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:ba852168d814b2c73333073e1c7116d9395bea69575a01b0b3c89d2d5a87c8fb", size = 46112, upload-time = "2025-05-19T14:16:04.909Z" },
{ url = "https://files.pythonhosted.org/packages/84/5d/e17845bb0fa76334477d5de38654d27946d5b5d3695443987a094a71b440/multidict-6.4.4-py3-none-any.whl", hash = "sha256:bd4557071b561a8b3b6075c3ce93cf9bfb6182cb241805c3d66ced3b75eff4ac", size = 10481, upload-time = "2025-05-19T14:16:36.024Z" },
]
[[package]]
name = "protobuf"
version = "5.29.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/17/7d/b9dca7365f0e2c4fa7c193ff795427cfa6290147e5185ab11ece280a18e7/protobuf-5.29.4.tar.gz", hash = "sha256:4f1dfcd7997b31ef8f53ec82781ff434a28bf71d9102ddde14d076adcfc78c99", size = 424902, upload-time = "2025-03-19T21:23:24.25Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/b2/043a1a1a20edd134563699b0e91862726a0dc9146c090743b6c44d798e75/protobuf-5.29.4-cp310-abi3-win32.whl", hash = "sha256:13eb236f8eb9ec34e63fc8b1d6efd2777d062fa6aaa68268fb67cf77f6839ad7", size = 422709, upload-time = "2025-03-19T21:23:08.293Z" },
{ url = "https://files.pythonhosted.org/packages/79/fc/2474b59570daa818de6124c0a15741ee3e5d6302e9d6ce0bdfd12e98119f/protobuf-5.29.4-cp310-abi3-win_amd64.whl", hash = "sha256:bcefcdf3976233f8a502d265eb65ea740c989bacc6c30a58290ed0e519eb4b8d", size = 434506, upload-time = "2025-03-19T21:23:11.253Z" },
{ url = "https://files.pythonhosted.org/packages/46/de/7c126bbb06aa0f8a7b38aaf8bd746c514d70e6a2a3f6dd460b3b7aad7aae/protobuf-5.29.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:307ecba1d852ec237e9ba668e087326a67564ef83e45a0189a772ede9e854dd0", size = 417826, upload-time = "2025-03-19T21:23:13.132Z" },
{ url = "https://files.pythonhosted.org/packages/a2/b5/bade14ae31ba871a139aa45e7a8183d869efe87c34a4850c87b936963261/protobuf-5.29.4-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:aec4962f9ea93c431d5714ed1be1c93f13e1a8618e70035ba2b0564d9e633f2e", size = 319574, upload-time = "2025-03-19T21:23:14.531Z" },
{ url = "https://files.pythonhosted.org/packages/46/88/b01ed2291aae68b708f7d334288ad5fb3e7aa769a9c309c91a0d55cb91b0/protobuf-5.29.4-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:d7d3f7d1d5a66ed4942d4fefb12ac4b14a29028b209d4bfb25c68ae172059922", size = 319672, upload-time = "2025-03-19T21:23:15.839Z" },
{ url = "https://files.pythonhosted.org/packages/12/fb/a586e0c973c95502e054ac5f81f88394f24ccc7982dac19c515acd9e2c93/protobuf-5.29.4-py3-none-any.whl", hash = "sha256:3fde11b505e1597f71b875ef2fc52062b6a9740e5f7c8997ce878b6009145862", size = 172551, upload-time = "2025-03-19T21:23:22.682Z" },
]
[[package]]
name = "setuptools"
version = "80.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8d/d2/ec1acaaff45caed5c2dedb33b67055ba9d4e96b091094df90762e60135fe/setuptools-80.8.0.tar.gz", hash = "sha256:49f7af965996f26d43c8ae34539c8d99c5042fbff34302ea151eaa9c207cd257", size = 1319720, upload-time = "2025-05-20T14:02:53.503Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/58/29/93c53c098d301132196c3238c312825324740851d77a8500a2462c0fd888/setuptools-80.8.0-py3-none-any.whl", hash = "sha256:95a60484590d24103af13b686121328cc2736bee85de8936383111e421b9edc0", size = 1201470, upload-time = "2025-05-20T14:02:51.348Z" },
]
[[package]]
name = "worker-py"
version = "0.1.0"
source = { virtual = "." }
[package.dev-dependencies]
dev = [
{ name = "grpcio" },
{ name = "grpcio-tools" },
{ name = "grpclib" },
{ name = "protobuf" },
]
[package.metadata]
[package.metadata.requires-dev]
dev = [
{ name = "grpcio", specifier = ">=1.71.0" },
{ name = "grpcio-tools", specifier = ">=1.71.0" },
{ name = "grpclib", specifier = ">=0.4.8" },
{ name = "protobuf", specifier = ">=5.29.4" },
]