mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-21 07:02:25 +02:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99a5cb875d | ||
|
|
78239caed0 |
@@ -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'
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 don’t 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
|
||||
}
|
||||
@@ -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 loop’s 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.
|
||||
// -------------------------------------------------------------------------- */
|
||||
@@ -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).
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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)
|
||||
Generated
+214
@@ -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" },
|
||||
]
|
||||
Reference in New Issue
Block a user