mirror of
https://github.com/exo-explore/exo.git
synced 2025-12-23 22:27:50 -05:00
Merge Basic Interfaces
Co-authored-by: Alex Cheema <alexcheema123@gmail.com> Co-authored-by: Seth Howes <sethshowes@gmail.com> Co-authored-by: Matt Beton <matthew.beton@gmail.com> Co-authored-by: Andrei Cravtov <the.andrei.cravtov@gmail.com>
This commit is contained in:
10
.github/actions/lint-check/action.yml
vendored
Normal file
10
.github/actions/lint-check/action.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
name: Lint Check
|
||||
|
||||
description: "Check for lint errors"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Lint check
|
||||
run: nix develop -c just lint-check
|
||||
shell: bash
|
||||
20
.github/actions/verify-clean/action.yml
vendored
Normal file
20
.github/actions/verify-clean/action.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: Verify Clean Working Tree
|
||||
|
||||
description: "Fail the job if the previous step left the working tree dirty"
|
||||
|
||||
inputs:
|
||||
step:
|
||||
description: "The name of the step that just executed"
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Check git diff
|
||||
shell: bash
|
||||
run: |
|
||||
if ! git diff --quiet; then
|
||||
echo "Error: ${{ inputs.step }} left working tree dirty." >&2
|
||||
git --no-pager diff >&2
|
||||
exit 1
|
||||
fi
|
||||
39
.github/workflows/pipeline.yml
vendored
39
.github/workflows/pipeline.yml
vendored
@@ -14,12 +14,24 @@ jobs:
|
||||
typecheck:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Configure git user
|
||||
run: |
|
||||
git config --local user.email "github-actions@users.noreply.github.com"
|
||||
git config --local user.name "github-actions bot"
|
||||
shell: bash
|
||||
|
||||
- uses: cachix/install-nix-action@v31
|
||||
with:
|
||||
github_access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: ./.github/actions/typecheck
|
||||
ci:
|
||||
needs: typecheck
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: write
|
||||
contents: read
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
@@ -38,27 +50,8 @@ jobs:
|
||||
with:
|
||||
github_access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: ./.github/actions/regenerate-protobufs
|
||||
|
||||
- name: Commit regenerated protobufs
|
||||
uses: ./.github/actions/conditional-commit
|
||||
- uses: ./.github/actions/verify-clean
|
||||
with:
|
||||
message: "chore(proto) regenerate protobufs"
|
||||
step: regenerate-protobufs
|
||||
|
||||
- uses: ./.github/actions/format
|
||||
|
||||
- name: Commit formatted code
|
||||
uses: ./.github/actions/conditional-commit
|
||||
with:
|
||||
message: "chore(format): format code"
|
||||
|
||||
- uses: ./.github/actions/lint
|
||||
|
||||
- name: Commit lint fixes
|
||||
uses: ./.github/actions/conditional-commit
|
||||
with:
|
||||
message: "chore(lint): fix linting errors"
|
||||
|
||||
- name: Push changes
|
||||
run: git push
|
||||
shell: bash
|
||||
- uses: ./.github/actions/lint-check
|
||||
@@ -22,6 +22,8 @@
|
||||
pkgs.uv
|
||||
pkgs.just
|
||||
pkgs.protobuf
|
||||
pkgs.rustc
|
||||
pkgs.cargo
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
12
justfile
12
justfile
@@ -1,6 +1,11 @@
|
||||
regenerate-protobufs:
|
||||
protoc --proto_path=shared/protobufs/schemas --python_out=shared/protobufs/types --pyi_out=shared/protobufs/types shared/protobufs/schemas/*.proto
|
||||
uv run ruff format ./shared/protobufs/types
|
||||
#!/usr/bin/env bash
|
||||
if [ -f shared/protobufs/schemas/*.proto ]; then
|
||||
protoc --proto_path=shared/protobufs/schemas --python_out=shared/protobufs/types --pyi_out=shared/protobufs/types shared/protobufs/schemas/*.proto
|
||||
uv run ruff format ./shared/protobufs/types
|
||||
else
|
||||
echo "No .proto files found in shared/protobufs/schemas/"
|
||||
fi
|
||||
|
||||
fmt:
|
||||
uv run ruff format master worker shared engines/*
|
||||
@@ -8,6 +13,9 @@ fmt:
|
||||
lint:
|
||||
uv run ruff check --fix master worker shared engines/*
|
||||
|
||||
lint-check:
|
||||
uv run ruff check master worker shared engines/*
|
||||
|
||||
test:
|
||||
uv run pytest master worker shared engines/*
|
||||
|
||||
|
||||
29
master/api.py
Normal file
29
master/api.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from typing import Protocol
|
||||
|
||||
from shared.types.models.common import ModelId
|
||||
from shared.types.models.model import ModelInfo
|
||||
from shared.types.models.sources import ModelSource
|
||||
from shared.types.networking.topology import ControlPlaneTopology, DataPlaneTopology
|
||||
from shared.types.worker.common import InstanceId
|
||||
from shared.types.worker.downloads import DownloadProgress
|
||||
from shared.types.worker.instances import Instance
|
||||
|
||||
|
||||
class ControlPlaneAPI(Protocol):
|
||||
def get_control_plane_topology(self) -> ControlPlaneTopology: ...
|
||||
|
||||
def get_data_plane_topology(self) -> DataPlaneTopology: ...
|
||||
|
||||
def list_instances(self) -> list[Instance]: ...
|
||||
|
||||
def get_instance(self, instance_id: InstanceId) -> Instance: ...
|
||||
|
||||
def create_instance(self, model_id: ModelId) -> InstanceId: ...
|
||||
|
||||
def remove_instance(self, instance_id: InstanceId) -> None: ...
|
||||
|
||||
def get_model_data(self, model_id: ModelId) -> ModelInfo: ...
|
||||
|
||||
def download_model(self, model_id: ModelId, model_source: ModelSource) -> None: ...
|
||||
|
||||
def get_download_progress(self, model_id: ModelId) -> DownloadProgress: ...
|
||||
@@ -1,24 +1,28 @@
|
||||
from hashlib import sha3_224 as hasher
|
||||
from typing import Sequence, TypeVar
|
||||
from uuid import UUID
|
||||
|
||||
from shared.types.event_sourcing import EventId, EventTypes, IdemKeyGenerator, State
|
||||
from shared.types.events.common import EventCategories, EventId, IdemKeyGenerator, State
|
||||
|
||||
EventTypeT = TypeVar("EventTypeT", bound=EventTypes)
|
||||
EventCategoryT = TypeVar("EventCategoryT", bound=EventCategories)
|
||||
|
||||
|
||||
def get_idem_tag_generator(base: str) -> IdemKeyGenerator[EventTypeT]:
|
||||
def get_idem_tag_generator(base: str) -> IdemKeyGenerator[EventCategoryT]:
|
||||
"""Generates idempotency keys for events.
|
||||
|
||||
The keys are generated by hashing the state sequence number against a base string.
|
||||
You can pick any base string, **so long as it's not used in any other function that generates idempotency keys**.
|
||||
"""
|
||||
|
||||
def get_idem_keys(state: State[EventTypeT], num_keys: int) -> Sequence[EventId]:
|
||||
def get_idem_keys(state: State[EventCategoryT], num_keys: int) -> Sequence[EventId]:
|
||||
def recurse(n: int, last: bytes) -> Sequence[EventId]:
|
||||
if n == 0:
|
||||
return []
|
||||
next_hash = hasher(last).digest()
|
||||
return (EventId(next_hash.hex()), *recurse(n - 1, next_hash))
|
||||
return (
|
||||
EventId(UUID(bytes=next_hash, version=4)),
|
||||
*recurse(n - 1, next_hash),
|
||||
)
|
||||
|
||||
initial_bytes = state.sequence_number.to_bytes(8, byteorder="big", signed=False)
|
||||
return recurse(num_keys, initial_bytes)
|
||||
|
||||
@@ -1,21 +1,48 @@
|
||||
import logging
|
||||
import logging.handlers
|
||||
from collections.abc import Sequence
|
||||
from collections.abc import Sequence, Set
|
||||
from enum import Enum
|
||||
from queue import Queue
|
||||
|
||||
from pydantic import BaseModel
|
||||
from rich.logging import RichHandler
|
||||
|
||||
|
||||
class LogEntryType(str, Enum):
|
||||
telemetry = "telemetry"
|
||||
metrics = "metrics"
|
||||
cluster = "cluster"
|
||||
|
||||
|
||||
class LogEntry(BaseModel):
|
||||
event_type: Set[LogEntryType]
|
||||
|
||||
|
||||
class LogFilterByType(logging.Filter):
|
||||
def __init__(self, log_types: Set[LogEntryType]):
|
||||
super().__init__()
|
||||
self.log_types = log_types
|
||||
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
message = record.getMessage()
|
||||
LogEntry.model_validate_json(message)
|
||||
return True
|
||||
|
||||
|
||||
def configure_logger(
|
||||
logger_name: str,
|
||||
log_level: int = logging.INFO,
|
||||
effect_handlers: Sequence[logging.Handler] | None = None,
|
||||
) -> logging.Logger:
|
||||
existing_logger = logging.Logger.manager.loggerDict.get(logger_name)
|
||||
if existing_logger is not None:
|
||||
raise RuntimeError(f"Logger with name '{logger_name}' already exists.")
|
||||
|
||||
logger = logging.getLogger(logger_name)
|
||||
logger.setLevel(log_level)
|
||||
logger.propagate = False
|
||||
logging.raiseExceptions = True
|
||||
|
||||
# If the named logger already has handlers, we assume it has been configured.
|
||||
if logger.hasHandlers():
|
||||
return logger
|
||||
|
||||
@@ -33,13 +60,20 @@ def configure_logger(
|
||||
return logger
|
||||
|
||||
|
||||
def attach_to_queue(logger: logging.Logger, queue: Queue[logging.LogRecord]) -> None:
|
||||
logger.addHandler(logging.handlers.QueueHandler(queue))
|
||||
def attach_to_queue(
|
||||
logger: logging.Logger,
|
||||
filter_with: Sequence[logging.Filter],
|
||||
queue: Queue[logging.LogRecord],
|
||||
) -> None:
|
||||
handler = logging.handlers.QueueHandler(queue)
|
||||
for log_filter in filter_with:
|
||||
handler.addFilter(log_filter)
|
||||
logger.addHandler(handler)
|
||||
|
||||
|
||||
def create_queue_listener(
|
||||
log_queue: Queue[logging.LogRecord],
|
||||
effect_handlers: list[logging.Handler],
|
||||
effect_handlers: Sequence[logging.Handler],
|
||||
) -> logging.handlers.QueueListener:
|
||||
listener = logging.handlers.QueueListener(
|
||||
log_queue, *effect_handlers, respect_handler_level=True
|
||||
|
||||
20
shared/openai.py
Normal file
20
shared/openai.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from typing import TYPE_CHECKING, Literal, TypeAlias, get_type_hints
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import openai.types as openai_types
|
||||
import openai.types.chat as openai_chat
|
||||
|
||||
types = openai_types
|
||||
chat = openai_chat
|
||||
else:
|
||||
types = None
|
||||
chat = None
|
||||
|
||||
FinishReason: TypeAlias = Literal[
|
||||
"stop", "length", "tool_calls", "content_filter", "function_call"
|
||||
]
|
||||
assert (
|
||||
get_type_hints(chat.chat_completion_chunk.Choice)["finish_reason"] == FinishReason
|
||||
), "Upstream changed Choice.finish_reason; update FinishReason alias."
|
||||
|
||||
__all__ = ["types", "chat", "FinishReason"]
|
||||
3
shared/protobufs/types/mlx/nn/__init__.pyi
Normal file
3
shared/protobufs/types/mlx/nn/__init__.pyi
Normal file
@@ -0,0 +1,3 @@
|
||||
from mlx.nn.layers import *
|
||||
from mlx.nn import init as init, losses as losses
|
||||
from mlx.nn.utils import average_gradients as average_gradients, value_and_grad as value_and_grad
|
||||
@@ -5,6 +5,7 @@ description = "Shared utilities for the Exo project"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"openai>=1.93.0",
|
||||
"pathlib>=1.0.1",
|
||||
"protobuf>=6.31.1",
|
||||
"pydantic>=2.11.7",
|
||||
|
||||
12
shared/types/api.py
Normal file
12
shared/types/api.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from typing import Literal
|
||||
|
||||
from openai.types.chat.completion_create_params import CompletionCreateParams
|
||||
from pydantic import BaseModel
|
||||
|
||||
from shared.types.tasks.common import TaskId
|
||||
|
||||
|
||||
class ChatTask(BaseModel):
|
||||
task_id: TaskId
|
||||
kind: Literal["chat"] = "chat"
|
||||
task_data: CompletionCreateParams
|
||||
16
shared/types/common.py
Normal file
16
shared/types/common.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from uuid import uuid4
|
||||
|
||||
from pydantic import UUID4, Field
|
||||
from pydantic.dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class NewUUID:
|
||||
uuid: UUID4 = Field(default_factory=lambda: uuid4())
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.uuid)
|
||||
|
||||
|
||||
class NodeId(NewUUID):
|
||||
pass
|
||||
@@ -1,99 +0,0 @@
|
||||
from typing import (
|
||||
Annotated,
|
||||
Callable,
|
||||
Generic,
|
||||
Literal,
|
||||
Protocol,
|
||||
Sequence,
|
||||
Tuple,
|
||||
TypeVar,
|
||||
get_args,
|
||||
)
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field, TypeAdapter
|
||||
from pydantic.types import UuidVersion
|
||||
|
||||
_EventId = Annotated[UUID, UuidVersion(4)]
|
||||
EventId = type("EventID", (UUID,), {})
|
||||
EventIdParser: TypeAdapter[EventId] = TypeAdapter(_EventId)
|
||||
|
||||
EventTypes = Literal["create", "update", "delete"]
|
||||
EventTypeT = TypeVar("EventTypeT", bound=EventTypes)
|
||||
TEventType = TypeVar("TEventType", bound=EventTypes, covariant=True)
|
||||
|
||||
|
||||
class Event(BaseModel, Generic[TEventType]):
|
||||
event_type: TEventType
|
||||
idem_key: EventId
|
||||
|
||||
|
||||
class State(BaseModel, Generic[EventTypeT]):
|
||||
event_types: tuple[EventTypeT, ...] = get_args(EventTypeT)
|
||||
sequence_number: int = Field(default=0, ge=0)
|
||||
|
||||
|
||||
AnnotatedEventType = Annotated[EventTypes, Field(discriminator="event_type")]
|
||||
EventTypeParser: TypeAdapter[AnnotatedEventType] = TypeAdapter(AnnotatedEventType)
|
||||
|
||||
Applicator = Callable[[State[EventTypeT], Event[TEventType]], State[EventTypeT]]
|
||||
Apply = Callable[[State[EventTypeT], Event[EventTypeT]], State[EventTypeT]]
|
||||
SagaApplicator = Callable[
|
||||
[State[EventTypeT], Event[TEventType]], Sequence[Event[EventTypeT]]
|
||||
]
|
||||
Saga = Callable[[State[EventTypeT], Event[EventTypeT]], Sequence[Event[EventTypeT]]]
|
||||
|
||||
StateAndEvent = Tuple[State[EventTypeT], Event[EventTypeT]]
|
||||
EffectHandler = Callable[[StateAndEvent[EventTypeT], State[EventTypeT]], None]
|
||||
EventPublisher = Callable[[Event[EventTypeT]], None]
|
||||
|
||||
|
||||
class EventOutbox(Protocol):
|
||||
def send(self, events: Sequence[Event[EventTypeT]]) -> None: ...
|
||||
|
||||
|
||||
class EventProcessor(Protocol):
|
||||
def update(
|
||||
self,
|
||||
state: State[EventTypeT],
|
||||
apply: Apply[EventTypeT],
|
||||
effect_handlers: Sequence[EffectHandler[EventTypeT]],
|
||||
) -> State[EventTypeT]: ...
|
||||
|
||||
|
||||
def get_saga_effect_handler(
|
||||
sagas: Saga[EventTypeT], event_publisher: EventPublisher[EventTypeT]
|
||||
) -> EffectHandler[EventTypeT]:
|
||||
def effect_handler(state_and_event: StateAndEvent[EventTypeT]) -> None:
|
||||
trigger_state, trigger_event = state_and_event
|
||||
for event in sagas(trigger_state, trigger_event):
|
||||
event_publisher(event)
|
||||
|
||||
return lambda state_and_event, _: effect_handler(state_and_event)
|
||||
|
||||
|
||||
def get_effects_from_sagas(
|
||||
sagas: Sequence[Saga[EventTypeT]], event_publisher: EventPublisher[EventTypeT]
|
||||
) -> Sequence[EffectHandler[EventTypeT]]:
|
||||
return [get_saga_effect_handler(saga, event_publisher) for saga in sagas]
|
||||
|
||||
|
||||
IdemKeyGenerator = Callable[[State[EventTypeT], int], Sequence[EventId]]
|
||||
|
||||
_CommandId = Annotated[UUID, UuidVersion(4)]
|
||||
CommandId = type("CommandID", (UUID,), {})
|
||||
CommandIdParser: TypeAdapter[CommandId] = TypeAdapter(_CommandId)
|
||||
|
||||
CommandTypes = Literal["create", "update", "delete"]
|
||||
CommandTypeT = TypeVar("CommandTypeT", bound=EventTypes)
|
||||
TCommandType = TypeVar("TCommandType", bound=EventTypes, covariant=True)
|
||||
|
||||
|
||||
class Command(BaseModel, Generic[TEventType, TCommandType]):
|
||||
command_type: TCommandType
|
||||
idem_key: CommandId
|
||||
|
||||
|
||||
Decide = Callable[
|
||||
[State[EventTypeT], Command[TEventType, TCommandType]], Sequence[Event[EventTypeT]]
|
||||
]
|
||||
93
shared/types/events/chunks.py
Normal file
93
shared/types/events/chunks.py
Normal file
@@ -0,0 +1,93 @@
|
||||
from enum import Enum
|
||||
from typing import Annotated, Generic, Literal, TypeVar
|
||||
|
||||
from openai.types.chat.chat_completion import ChatCompletion
|
||||
from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
|
||||
from pydantic import BaseModel, Field, TypeAdapter
|
||||
|
||||
from shared.openai import FinishReason
|
||||
from shared.types.models.common import ModelId
|
||||
from shared.types.tasks.common import TaskId
|
||||
|
||||
OpenAIResponse = (
|
||||
ChatCompletion | ChatCompletionChunk
|
||||
) ## Currently we only support chat completions
|
||||
|
||||
|
||||
class ChunkType(str, Enum):
|
||||
token = "token"
|
||||
image = "image"
|
||||
|
||||
|
||||
ChunkT = TypeVar("ChunkT", bound=ChunkType)
|
||||
|
||||
|
||||
class BaseChunk(BaseModel, Generic[ChunkT]):
|
||||
task_id: TaskId
|
||||
idx: int
|
||||
model: ModelId
|
||||
|
||||
|
||||
###
|
||||
|
||||
|
||||
class TokenChunkData(BaseModel):
|
||||
text: str
|
||||
token_id: int
|
||||
finish_reason: FinishReason | None = None
|
||||
|
||||
|
||||
class ImageChunkData(BaseModel):
|
||||
data: bytes
|
||||
|
||||
|
||||
###
|
||||
|
||||
|
||||
class TokenChunk(BaseChunk[ChunkType.token]):
|
||||
chunk_data: TokenChunkData
|
||||
chunk_type: Literal[ChunkType.token] = Field(default=ChunkType.token, frozen=True)
|
||||
|
||||
|
||||
class ImageChunk(BaseChunk[ChunkType.image]):
|
||||
chunk_data: ImageChunkData
|
||||
chunk_type: Literal[ChunkType.image] = Field(default=ChunkType.image, frozen=True)
|
||||
|
||||
|
||||
###
|
||||
|
||||
GenerationChunk = Annotated[TokenChunk | ImageChunk, Field(discriminator="chunk_type")]
|
||||
GenerationChunkTypeAdapter: TypeAdapter[GenerationChunk] = TypeAdapter(GenerationChunk)
|
||||
|
||||
# my_chunk: dict[str, Any] = TokenChunk(
|
||||
# task_id=TaskId('nicerid'),
|
||||
# idx=0,
|
||||
# chunk_data=TokenChunkData(
|
||||
# text='hello',
|
||||
# token_id=12,
|
||||
# ),
|
||||
# chunk_type=ChunkType.token,
|
||||
# model='llama-3.1',
|
||||
# ).model_dump()
|
||||
# print(my_chunk)
|
||||
# restored = GenerationChunkTypeAdapter.validate_python(my_chunk)
|
||||
# print(restored)
|
||||
|
||||
#### OpenAI API Interfaces ###
|
||||
|
||||
"""
|
||||
def send_task(task: Any) -> AsyncGenerator[GenerationChunk]:
|
||||
# This is the 'command' - turns the task into an event and pushes to the event queue.
|
||||
# Tokens are then read off the event queue and pushed back to the api via an AsyncGenerator.
|
||||
...
|
||||
|
||||
def parse_chunk_to_openai_response(chunk: GenerationChunk) -> OpenAIResponse:
|
||||
...
|
||||
|
||||
async def handle_task(task: Any) -> AsyncGenerator[OpenAIResponse]:
|
||||
## In our api call function, we will do:
|
||||
generator: AsyncGenerator[GenerationChunk] = send_task(task)
|
||||
|
||||
async for chunk in generator:
|
||||
yield parse_chunk_to_openai_response(chunk)
|
||||
"""
|
||||
274
shared/types/events/common.py
Normal file
274
shared/types/events/common.py
Normal file
@@ -0,0 +1,274 @@
|
||||
from enum import Enum, auto
|
||||
from typing import (
|
||||
Annotated,
|
||||
Callable,
|
||||
Generic,
|
||||
Protocol,
|
||||
Sequence,
|
||||
Tuple,
|
||||
TypeVar,
|
||||
)
|
||||
|
||||
from pydantic import BaseModel, Field, TypeAdapter, model_validator
|
||||
|
||||
from shared.types.common import NewUUID, NodeId
|
||||
|
||||
|
||||
class EventId(NewUUID):
|
||||
pass
|
||||
|
||||
|
||||
class TimerId(NewUUID):
|
||||
pass
|
||||
|
||||
|
||||
class MLXEventTypes(str, Enum):
|
||||
MLXInferenceSagaPrepare = "MLXInferenceSagaPrepare"
|
||||
MLXInferenceSagaStartPrepare = "MLXInferenceSagaStartPrepare"
|
||||
|
||||
|
||||
class TaskEventTypes(str, Enum):
|
||||
TaskCreated = "TaskCreated"
|
||||
TaskUpdated = "TaskUpdated"
|
||||
TaskDeleted = "TaskDeleted"
|
||||
|
||||
|
||||
class StreamingEventTypes(str, Enum):
|
||||
ChunkGenerated = "ChunkGenerated"
|
||||
|
||||
|
||||
class InstanceEventTypes(str, Enum):
|
||||
InstanceCreated = "InstanceCreated"
|
||||
InstanceDeleted = "InstanceDeleted"
|
||||
InstanceToBeReplacedAtomically = "InstanceToBeReplacedAtomically"
|
||||
InstanceReplacedAtomically = "InstanceReplacedAtomically"
|
||||
InstanceStatusUpdated = "InstanceStatusUpdated"
|
||||
|
||||
|
||||
class InstanceStateEventTypes(str, Enum):
|
||||
InstanceRunnerStateUpdated = "InstanceRunnerStateUpdated"
|
||||
|
||||
|
||||
class NodePerformanceEventTypes(str, Enum):
|
||||
NodePerformanceProfiled = "NodePerformanceProfiled"
|
||||
|
||||
|
||||
class DataPlaneEventTypes(str, Enum):
|
||||
DataPlaneEdgeCreated = "DataPlaneEdgeCreated"
|
||||
DataPlaneEdgeProfiled = "DataPlaneEdgeProfiled"
|
||||
DataPlaneEdgeDeleted = "DataPlaneEdgeDeleted"
|
||||
|
||||
|
||||
class ControlPlaneEventTypes(str, Enum):
|
||||
WorkerConnected = "WorkerConnected"
|
||||
WorkerStatusUpdated = "WorkerStatusUpdated"
|
||||
WorkerDisconnected = "WorkerDisconnected"
|
||||
|
||||
|
||||
class TimerEventTypes(str, Enum):
|
||||
TimerCreated = "TimerCreated"
|
||||
TimerFired = "TimerFired"
|
||||
|
||||
|
||||
class ResourceEventTypes(str, Enum):
|
||||
ResourceProfiled = "ResourceProfiled"
|
||||
|
||||
|
||||
class EventCategories(str, Enum):
|
||||
TaskEventTypes = auto()
|
||||
StreamingEventTypes = auto()
|
||||
InstanceEventTypes = auto()
|
||||
InstanceStateEventTypes = auto()
|
||||
NodePerformanceEventTypes = auto()
|
||||
ControlPlaneEventTypes = auto()
|
||||
DataPlaneEventTypes = auto()
|
||||
TimerEventTypes = auto()
|
||||
MLXEventTypes = auto()
|
||||
|
||||
|
||||
PossibleEventOfEventTypeT = TypeVar("PossibleEventOfEventTypeT", bound=Enum)
|
||||
|
||||
# T=(A|B) <: U=(A|B|C) ==> Event[A|B] <: Event[A|BCategoryOfEventsT_cov = TypeVar(name="CategoryOfEventsT_cov", bound=EventCategories, covariant=True)
|
||||
CategoryOfEventsT_cov = TypeVar(
|
||||
name="CategoryOfEventsT_cov", bound=EventCategories, contravariant=True
|
||||
)
|
||||
CategoryOfEventsT_con = TypeVar(
|
||||
name="CategoryOfEventsT_con", bound=EventCategories, contravariant=True
|
||||
)
|
||||
CategoryOfEventsT_inv = TypeVar(
|
||||
name="CategoryOfEventsT_inv",
|
||||
bound=EventCategories,
|
||||
covariant=False,
|
||||
contravariant=False,
|
||||
)
|
||||
|
||||
|
||||
class Event(BaseModel, Generic[PossibleEventOfEventTypeT]):
|
||||
event_type: PossibleEventOfEventTypeT
|
||||
event_category: EventCategories
|
||||
event_id: EventId
|
||||
|
||||
def check_origin_id(self, origin_id: NodeId) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class TaskEvent(Event[TaskEventTypes]):
|
||||
event_type: TaskEventTypes
|
||||
|
||||
|
||||
class InstanceEvent(Event[InstanceEventTypes]):
|
||||
event_type: InstanceEventTypes
|
||||
|
||||
|
||||
class InstanceStateEvent(Event[InstanceStateEventTypes]):
|
||||
event_type: InstanceStateEventTypes
|
||||
|
||||
|
||||
class MLXEvent(Event[MLXEventTypes]):
|
||||
event_type: MLXEventTypes
|
||||
|
||||
|
||||
class NodePerformanceEvent(Event[NodePerformanceEventTypes]):
|
||||
event_type: NodePerformanceEventTypes
|
||||
|
||||
|
||||
class ControlPlaneEvent(Event[ControlPlaneEventTypes]):
|
||||
event_type: ControlPlaneEventTypes
|
||||
|
||||
|
||||
class StreamingEvent(Event[StreamingEventTypes]):
|
||||
event_type: StreamingEventTypes
|
||||
|
||||
|
||||
class DataPlaneEvent(Event[DataPlaneEventTypes]):
|
||||
event_type: DataPlaneEventTypes
|
||||
|
||||
|
||||
class TimerEvent(Event[TimerEventTypes]):
|
||||
event_type: TimerEventTypes
|
||||
|
||||
|
||||
class ResourceEvent(Event[ResourceEventTypes]):
|
||||
event_type: ResourceEventTypes
|
||||
|
||||
|
||||
class WrappedMessage(BaseModel, Generic[PossibleEventOfEventTypeT]):
|
||||
message: Event[PossibleEventOfEventTypeT]
|
||||
origin_id: NodeId
|
||||
|
||||
@model_validator(mode="after")
|
||||
def check_origin_id(self) -> "WrappedMessage[PossibleEventOfEventTypeT]":
|
||||
if self.message.check_origin_id(self.origin_id):
|
||||
return self
|
||||
raise ValueError("Invalid Event: Origin ID Does Not Match")
|
||||
|
||||
|
||||
class PersistedEvent(BaseModel, Generic[PossibleEventOfEventTypeT]):
|
||||
event: Event[PossibleEventOfEventTypeT]
|
||||
sequence_number: int = Field(gt=0)
|
||||
|
||||
|
||||
class State(BaseModel, Generic[CategoryOfEventsT_cov]):
|
||||
event_category: CategoryOfEventsT_cov
|
||||
sequence_number: int = Field(default=0, ge=0)
|
||||
|
||||
|
||||
AnnotatedEventType = Annotated[
|
||||
Event[EventCategories], Field(discriminator="event_category")
|
||||
]
|
||||
EventTypeParser: TypeAdapter[AnnotatedEventType] = TypeAdapter(AnnotatedEventType)
|
||||
|
||||
|
||||
# it's not possible to enforce this at compile time, so we have to do it at runtime
|
||||
def mock_todo[T](something: T | None) -> T: ...
|
||||
|
||||
|
||||
def apply(
|
||||
state: State[CategoryOfEventsT_inv], event: Event[CategoryOfEventsT_inv]
|
||||
) -> State[CategoryOfEventsT_inv]: ...
|
||||
|
||||
|
||||
# T=(A|B) <: U=(A|B|C) ==> Apply[A|B] <: Apply[A|B|C]
|
||||
SagaApplicator = Callable[
|
||||
[State[CategoryOfEventsT_inv], Event[CategoryOfEventsT_inv]],
|
||||
Sequence[Event[CategoryOfEventsT_inv]],
|
||||
]
|
||||
Saga = Callable[
|
||||
[State[CategoryOfEventsT_inv], Event[CategoryOfEventsT_inv]],
|
||||
Sequence[Event[CategoryOfEventsT_inv]],
|
||||
]
|
||||
Apply = Callable[
|
||||
[State[CategoryOfEventsT_inv], Event[CategoryOfEventsT_inv]],
|
||||
State[CategoryOfEventsT_inv],
|
||||
]
|
||||
StateAndEvent = Tuple[State[CategoryOfEventsT_inv], Event[CategoryOfEventsT_inv]]
|
||||
EffectHandler = Callable[
|
||||
[StateAndEvent[CategoryOfEventsT_inv], State[CategoryOfEventsT_inv]], None
|
||||
]
|
||||
EventPublisher = Callable[[Event[CategoryOfEventsT_inv]], None]
|
||||
|
||||
|
||||
class MutableState[EventCategoryT: EventCategories](Protocol):
|
||||
def apply(
|
||||
self,
|
||||
event: Event[EventCategoryT],
|
||||
applicator: Apply[EventCategoryT],
|
||||
effect_handlers: Sequence[EffectHandler[EventCategoryT]],
|
||||
) -> None: ...
|
||||
|
||||
|
||||
class EventOutbox(Protocol):
|
||||
def send(self, events: Sequence[Event[EventCategories]]) -> None: ...
|
||||
|
||||
|
||||
#
|
||||
# T=[A|B] <: U=[A|B|C] => EventProcessor[A|B] :> EventProcessor[A|B|C]
|
||||
#
|
||||
class EventProcessor[EventCategoryT: EventCategories](Protocol):
|
||||
def get_events_to_apply(
|
||||
self, state: State[EventCategoryT]
|
||||
) -> Sequence[Event[EventCategoryT]]: ...
|
||||
|
||||
|
||||
def get_saga_effect_handler[EventCategoryT: EventCategories](
|
||||
saga: Saga[EventCategoryT], event_publisher: EventPublisher[EventCategoryT]
|
||||
) -> EffectHandler[EventCategoryT]:
|
||||
def effect_handler(state_and_event: StateAndEvent[EventCategoryT]) -> None:
|
||||
trigger_state, trigger_event = state_and_event
|
||||
for event in saga(trigger_state, trigger_event):
|
||||
event_publisher(event)
|
||||
|
||||
return lambda state_and_event, _: effect_handler(state_and_event)
|
||||
|
||||
|
||||
def get_effects_from_sagas[EventCategoryT: EventCategories](
|
||||
sagas: Sequence[Saga[EventCategoryT]],
|
||||
event_publisher: EventPublisher[EventCategoryT],
|
||||
) -> Sequence[EffectHandler[EventCategoryT]]:
|
||||
return [get_saga_effect_handler(saga, event_publisher) for saga in sagas]
|
||||
|
||||
|
||||
IdemKeyGenerator = Callable[[State[CategoryOfEventsT_cov], int], Sequence[EventId]]
|
||||
|
||||
|
||||
class CommandId(NewUUID):
|
||||
pass
|
||||
|
||||
|
||||
class CommandTypes(str, Enum):
|
||||
Create = "Create"
|
||||
Update = "Update"
|
||||
Delete = "Delete"
|
||||
|
||||
|
||||
class Command[EventCategoryT: EventCategories, CommandType: CommandTypes](BaseModel):
|
||||
command_type: CommandType
|
||||
command_id: CommandId
|
||||
|
||||
|
||||
CommandTypeT = TypeVar("CommandTypeT", bound=CommandTypes, covariant=True)
|
||||
|
||||
Decide = Callable[
|
||||
[State[CategoryOfEventsT_cov], Command[CategoryOfEventsT_cov, CommandTypeT]],
|
||||
Sequence[Event[CategoryOfEventsT_cov]],
|
||||
]
|
||||
185
shared/types/events/events.py
Normal file
185
shared/types/events/events.py
Normal file
@@ -0,0 +1,185 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Literal, Tuple
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from shared.types.common import NodeId
|
||||
from shared.types.events.common import (
|
||||
ControlPlaneEvent,
|
||||
ControlPlaneEventTypes,
|
||||
DataPlaneEvent,
|
||||
DataPlaneEventTypes,
|
||||
InstanceEvent,
|
||||
InstanceEventTypes,
|
||||
InstanceStateEvent,
|
||||
InstanceStateEventTypes,
|
||||
MLXEvent,
|
||||
MLXEventTypes,
|
||||
NodePerformanceEvent,
|
||||
NodePerformanceEventTypes,
|
||||
ResourceEvent,
|
||||
ResourceEventTypes,
|
||||
StreamingEvent,
|
||||
StreamingEventTypes,
|
||||
TaskEvent,
|
||||
TaskEventTypes,
|
||||
TimerEvent,
|
||||
TimerEventTypes,
|
||||
TimerId,
|
||||
)
|
||||
from shared.types.networking.control_plane import (
|
||||
ControlPlaneEdgeId,
|
||||
ControlPlaneEdgeType,
|
||||
)
|
||||
from shared.types.networking.data_plane import (
|
||||
DataPlaneEdge,
|
||||
DataPlaneEdgeId,
|
||||
DataPlaneEdgeProfile,
|
||||
)
|
||||
from shared.types.profiling.common import NodePerformanceProfile, ProfiledResourceName
|
||||
from shared.types.tasks.common import (
|
||||
TaskData,
|
||||
TaskId,
|
||||
TaskState,
|
||||
TaskStatusIncompleteType,
|
||||
TaskStatusType,
|
||||
TaskType,
|
||||
)
|
||||
from shared.types.worker.common import InstanceId, NodeStatus
|
||||
from shared.types.worker.instances import InstanceData, InstanceStatus
|
||||
from shared.types.worker.runners import RunnerId, RunnerState, RunnerStateType
|
||||
|
||||
|
||||
class TimerData(BaseModel):
|
||||
timer_id: TimerId
|
||||
|
||||
|
||||
class TaskCreated[TaskTypeT: TaskType](TaskEvent):
|
||||
event_type: TaskEventTypes = TaskEventTypes.TaskCreated
|
||||
task_id: TaskId
|
||||
task_data: TaskData[TaskTypeT]
|
||||
task_state: TaskState[Literal[TaskStatusIncompleteType.Pending], TaskTypeT]
|
||||
on_instance: InstanceId
|
||||
|
||||
|
||||
class TaskUpdated[TaskTypeT: TaskType](TaskEvent):
|
||||
event_type: TaskEventTypes = TaskEventTypes.TaskUpdated
|
||||
task_id: TaskId
|
||||
update_data: TaskState[TaskStatusType, TaskTypeT]
|
||||
|
||||
|
||||
class TaskDeleted(TaskEvent):
|
||||
event_type: TaskEventTypes = TaskEventTypes.TaskDeleted
|
||||
task_id: TaskId
|
||||
|
||||
|
||||
class InstanceCreated(InstanceEvent):
|
||||
event_type: InstanceEventTypes = InstanceEventTypes.InstanceCreated
|
||||
instance_id: InstanceId
|
||||
instance_data: InstanceData
|
||||
target_status: InstanceStatus
|
||||
|
||||
|
||||
class InstanceDeleted(InstanceEvent):
|
||||
event_type: InstanceEventTypes = InstanceEventTypes.InstanceDeleted
|
||||
instance_id: InstanceId
|
||||
|
||||
|
||||
class InstanceStatusUpdated(InstanceEvent):
|
||||
event_type: InstanceEventTypes = InstanceEventTypes.InstanceStatusUpdated
|
||||
instance_id: InstanceId
|
||||
instance_status: InstanceStatus
|
||||
|
||||
|
||||
class InstanceRunnerStateUpdated(InstanceStateEvent):
|
||||
event_type: InstanceStateEventTypes = (
|
||||
InstanceStateEventTypes.InstanceRunnerStateUpdated
|
||||
)
|
||||
instance_id: InstanceId
|
||||
state_update: Tuple[RunnerId, RunnerState[RunnerStateType]]
|
||||
|
||||
|
||||
class InstanceToBeReplacedAtomically(InstanceEvent):
|
||||
event_type: InstanceEventTypes = InstanceEventTypes.InstanceToBeReplacedAtomically
|
||||
transition: Tuple[InstanceId, InstanceId]
|
||||
|
||||
|
||||
class InstanceReplacedAtomically(InstanceEvent):
|
||||
event_type: InstanceEventTypes = InstanceEventTypes.InstanceReplacedAtomically
|
||||
transition: Tuple[InstanceId, InstanceId]
|
||||
|
||||
|
||||
class MLXInferenceSagaPrepare(MLXEvent):
|
||||
event_type: MLXEventTypes = MLXEventTypes.MLXInferenceSagaPrepare
|
||||
task_id: TaskId
|
||||
instance_id: InstanceId
|
||||
|
||||
|
||||
class MLXInferenceSagaStartPrepare(MLXEvent):
|
||||
event_type: MLXEventTypes = MLXEventTypes.MLXInferenceSagaStartPrepare
|
||||
task_id: TaskId
|
||||
instance_id: InstanceId
|
||||
|
||||
|
||||
class NodePerformanceProfiled(NodePerformanceEvent):
|
||||
event_type: NodePerformanceEventTypes = (
|
||||
NodePerformanceEventTypes.NodePerformanceProfiled
|
||||
)
|
||||
node_id: NodeId
|
||||
node_profile: NodePerformanceProfile
|
||||
|
||||
|
||||
class WorkerConnected(ControlPlaneEvent):
|
||||
event_type: ControlPlaneEventTypes = ControlPlaneEventTypes.WorkerConnected
|
||||
edge: DataPlaneEdge
|
||||
|
||||
|
||||
class WorkerStatusUpdated(ControlPlaneEvent):
|
||||
event_type: ControlPlaneEventTypes = ControlPlaneEventTypes.WorkerStatusUpdated
|
||||
node_id: NodeId
|
||||
node_state: NodeStatus
|
||||
|
||||
|
||||
class WorkerDisconnected(ControlPlaneEvent):
|
||||
event_type: ControlPlaneEventTypes = ControlPlaneEventTypes.WorkerConnected
|
||||
vertex_id: ControlPlaneEdgeId
|
||||
|
||||
|
||||
class ChunkGenerated(StreamingEvent):
|
||||
event_type: StreamingEventTypes = StreamingEventTypes.ChunkGenerated
|
||||
task_id: TaskId
|
||||
instance_id: InstanceId
|
||||
chunk: Any
|
||||
|
||||
|
||||
class DataPlaneEdgeCreated(DataPlaneEvent):
|
||||
event_type: DataPlaneEventTypes = DataPlaneEventTypes.DataPlaneEdgeCreated
|
||||
vertex: ControlPlaneEdgeType
|
||||
|
||||
|
||||
class DataPlaneEdgeProfiled(DataPlaneEvent):
|
||||
event_type: DataPlaneEventTypes = DataPlaneEventTypes.DataPlaneEdgeProfiled
|
||||
edge_id: DataPlaneEdgeId
|
||||
edge_profile: DataPlaneEdgeProfile
|
||||
|
||||
|
||||
class DataPlaneEdgeDeleted(DataPlaneEvent):
|
||||
event_type: DataPlaneEventTypes = DataPlaneEventTypes.DataPlaneEdgeDeleted
|
||||
edge_id: DataPlaneEdgeId
|
||||
|
||||
|
||||
class TimerScheduled(TimerEvent):
|
||||
event_type: TimerEventTypes = TimerEventTypes.TimerCreated
|
||||
timer_data: TimerData
|
||||
|
||||
|
||||
class TimerFired(TimerEvent):
|
||||
event_type: TimerEventTypes = TimerEventTypes.TimerFired
|
||||
timer_data: TimerData
|
||||
|
||||
|
||||
class ResourceProfiled(ResourceEvent):
|
||||
event_type: ResourceEventTypes = ResourceEventTypes.ResourceProfiled
|
||||
resource_name: ProfiledResourceName
|
||||
resource_profile: NodePerformanceProfile
|
||||
171
shared/types/graphs/common.py
Normal file
171
shared/types/graphs/common.py
Normal file
@@ -0,0 +1,171 @@
|
||||
from collections.abc import Mapping
|
||||
from typing import Callable, Generic, Protocol, Set, Tuple, TypeVar, overload
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from shared.types.common import NewUUID
|
||||
|
||||
EdgeTypeT = TypeVar("EdgeTypeT", covariant=True)
|
||||
VertexTypeT = TypeVar("VertexTypeT", covariant=True)
|
||||
EdgeIdT = TypeVar("EdgeIdT", bound=NewUUID)
|
||||
VertexIdT = TypeVar("VertexIdT", bound=NewUUID)
|
||||
|
||||
|
||||
class VertexData(BaseModel, Generic[VertexTypeT]):
|
||||
vertex_type: VertexTypeT
|
||||
|
||||
|
||||
class EdgeData(BaseModel, Generic[EdgeTypeT]):
|
||||
edge_type: EdgeTypeT
|
||||
|
||||
|
||||
class BaseEdge(BaseModel, Generic[EdgeTypeT, EdgeIdT, VertexIdT]):
|
||||
edge_vertices: Tuple[VertexIdT, VertexIdT]
|
||||
edge_data: EdgeData[EdgeTypeT]
|
||||
|
||||
|
||||
class BaseVertex(BaseModel, Generic[VertexTypeT, EdgeIdT]):
|
||||
vertex_data: VertexData[VertexTypeT]
|
||||
|
||||
|
||||
class Vertex(
|
||||
BaseVertex[VertexTypeT, EdgeIdT], Generic[VertexTypeT, EdgeIdT, VertexIdT]
|
||||
):
|
||||
vertex_id: VertexIdT
|
||||
|
||||
|
||||
class Edge(
|
||||
BaseEdge[EdgeTypeT, EdgeIdT, VertexIdT], Generic[EdgeTypeT, EdgeIdT, VertexIdT]
|
||||
):
|
||||
edge_id: EdgeIdT
|
||||
|
||||
|
||||
class GraphData(BaseModel, Generic[EdgeTypeT, VertexTypeT, EdgeIdT, VertexIdT]):
|
||||
edges: Mapping[EdgeIdT, EdgeData[EdgeTypeT]]
|
||||
vertices: Mapping[VertexIdT, VertexData[VertexTypeT]]
|
||||
|
||||
|
||||
class GraphProtocol(Protocol, Generic[EdgeTypeT, VertexTypeT, EdgeIdT, VertexIdT]):
|
||||
def list_edges(self) -> Set[EdgeIdT]: ...
|
||||
def list_vertices(self) -> Set[VertexIdT]: ...
|
||||
def get_vertices_from_edges(
|
||||
self, edges: Set[EdgeIdT]
|
||||
) -> Mapping[EdgeIdT, Set[VertexIdT]]: ...
|
||||
def get_edges_from_vertices(
|
||||
self, vertices: Set[VertexIdT]
|
||||
) -> Mapping[VertexIdT, Set[EdgeIdT]]: ...
|
||||
def get_edge_data(
|
||||
self, edges: Set[EdgeIdT]
|
||||
) -> Mapping[EdgeIdT, EdgeData[EdgeTypeT]]: ...
|
||||
def get_vertex_data(
|
||||
self, vertices: Set[VertexIdT]
|
||||
) -> Mapping[VertexIdT, VertexData[VertexTypeT]]: ...
|
||||
|
||||
|
||||
class MutableGraphProtocol(GraphProtocol[EdgeTypeT, VertexTypeT, EdgeIdT, VertexIdT]):
|
||||
def check_edges_exists(self, edge_id: EdgeIdT) -> bool: ...
|
||||
def check_vertex_exists(self, vertex_id: VertexIdT) -> bool: ...
|
||||
def _add_edge(self, edge_id: EdgeIdT, edge_data: EdgeData[EdgeTypeT]) -> None: ...
|
||||
def _add_vertex(
|
||||
self, vertex_id: VertexIdT, vertex_data: VertexData[VertexTypeT]
|
||||
) -> None: ...
|
||||
def _remove_edge(self, edge_id: EdgeIdT) -> None: ...
|
||||
def _remove_vertex(self, vertex_id: VertexIdT) -> None: ...
|
||||
###
|
||||
@overload
|
||||
def attach_edge(self, edge: Edge[EdgeTypeT, EdgeIdT, VertexIdT]) -> None: ...
|
||||
@overload
|
||||
def attach_edge(
|
||||
self,
|
||||
edge: Edge[EdgeTypeT, EdgeIdT, VertexIdT],
|
||||
extra_vertex: Vertex[VertexTypeT, EdgeIdT, VertexIdT],
|
||||
) -> None: ...
|
||||
def attach_edge(
|
||||
self,
|
||||
edge: Edge[EdgeTypeT, EdgeIdT, VertexIdT],
|
||||
extra_vertex: Vertex[VertexTypeT, EdgeIdT, VertexIdT] | None = None,
|
||||
) -> None:
|
||||
base_vertex = edge.edge_vertices[0]
|
||||
target_vertex = edge.edge_vertices[1]
|
||||
base_vertex_exists = self.check_vertex_exists(base_vertex)
|
||||
target_vertex_exists = self.check_vertex_exists(target_vertex)
|
||||
|
||||
if not base_vertex_exists:
|
||||
raise ValueError("Base Vertex Does Not Exist")
|
||||
|
||||
match (target_vertex_exists, extra_vertex is not None):
|
||||
case (True, False):
|
||||
raise ValueError("New Vertex Already Exists")
|
||||
case (False, True):
|
||||
if extra_vertex is None:
|
||||
raise ValueError("BUG: Extra Vertex Must Be Provided")
|
||||
self._add_vertex(extra_vertex.vertex_id, extra_vertex.vertex_data)
|
||||
case (False, False):
|
||||
raise ValueError(
|
||||
"New Vertex Must Be Provided For Non-Existent Target Vertex"
|
||||
)
|
||||
case (True, True):
|
||||
raise ValueError("New Vertex Already Exists")
|
||||
|
||||
self._add_edge(edge.edge_id, edge.edge_data)
|
||||
|
||||
|
||||
class Graph(
|
||||
BaseModel,
|
||||
Generic[EdgeTypeT, VertexTypeT, EdgeIdT, VertexIdT],
|
||||
GraphProtocol[EdgeTypeT, VertexTypeT, EdgeIdT, VertexIdT],
|
||||
):
|
||||
graph_data: GraphData[EdgeTypeT, VertexTypeT, EdgeIdT, VertexIdT]
|
||||
|
||||
|
||||
# the first element in the return value is the filtered graph; the second is the
|
||||
# (possibly empty) set of sub-graphs that were detached during filtering.
|
||||
def filter_by_edge_data(
|
||||
graph: Graph[EdgeTypeT, VertexTypeT, EdgeIdT, VertexIdT],
|
||||
keep: VertexIdT,
|
||||
predicate: Callable[[EdgeData[EdgeTypeT]], bool],
|
||||
) -> Tuple[
|
||||
Graph[EdgeTypeT, VertexTypeT, EdgeIdT, VertexIdT],
|
||||
Set[Graph[EdgeTypeT, VertexTypeT, EdgeIdT, VertexIdT]],
|
||||
]: ...
|
||||
|
||||
|
||||
# the first element in the return value is the filtered graph; the second is the
|
||||
# (possibly empty) set of sub-graphs that were detached during filtering.
|
||||
def filter_by_vertex_data(
|
||||
graph: Graph[EdgeTypeT, VertexTypeT, EdgeIdT, VertexIdT],
|
||||
keep: VertexIdT,
|
||||
predicate: Callable[[VertexData[VertexTypeT]], bool],
|
||||
) -> Tuple[
|
||||
Graph[EdgeTypeT, VertexTypeT, EdgeIdT, VertexIdT],
|
||||
Set[Graph[EdgeTypeT, VertexTypeT, EdgeIdT, VertexIdT]],
|
||||
]: ...
|
||||
|
||||
|
||||
def map_vertices_onto_graph(
|
||||
vertices: Mapping[VertexIdT, VertexData[VertexTypeT]],
|
||||
graph: Graph[EdgeTypeT, VertexTypeT, EdgeIdT, VertexIdT],
|
||||
) -> Tuple[Graph[EdgeTypeT, VertexTypeT, EdgeIdT, VertexIdT], Set[VertexIdT]]: ...
|
||||
|
||||
|
||||
def map_edges_onto_graph(
|
||||
edges: Mapping[EdgeIdT, EdgeData[EdgeTypeT]],
|
||||
graph: Graph[EdgeTypeT, VertexTypeT, EdgeIdT, VertexIdT],
|
||||
) -> Tuple[Graph[EdgeTypeT, VertexTypeT, EdgeIdT, VertexIdT], Set[EdgeIdT]]: ...
|
||||
|
||||
|
||||
def split_graph_by_edge(
|
||||
graph: Graph[EdgeTypeT, VertexTypeT, EdgeIdT, VertexIdT],
|
||||
edge: EdgeIdT,
|
||||
keep: VertexIdT,
|
||||
) -> Tuple[
|
||||
Graph[EdgeTypeT, VertexTypeT, EdgeIdT, VertexIdT],
|
||||
Set[Graph[EdgeTypeT, VertexTypeT, EdgeIdT, VertexIdT]],
|
||||
]: ...
|
||||
|
||||
|
||||
def merge_graphs_by_edge(
|
||||
graphs: Set[Graph[EdgeTypeT, VertexTypeT, EdgeIdT, VertexIdT]],
|
||||
edge: EdgeIdT,
|
||||
keep: VertexIdT,
|
||||
) -> Tuple[Graph[EdgeTypeT, VertexTypeT, EdgeIdT, VertexIdT], Set[EdgeIdT]]: ...
|
||||
17
shared/types/graphs/resource_graph.py
Normal file
17
shared/types/graphs/resource_graph.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from collections.abc import Mapping
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from shared.types.common import NodeId
|
||||
from shared.types.networking.topology import ControlPlaneTopology, DataPlaneTopology
|
||||
from shared.types.profiling.common import NodePerformanceProfile
|
||||
|
||||
|
||||
class ResourceGraph(BaseModel): ...
|
||||
|
||||
|
||||
def get_graph_of_compute_resources(
|
||||
control_plane_topology: ControlPlaneTopology,
|
||||
data_plane_topology: DataPlaneTopology,
|
||||
node_profiles: Mapping[NodeId, NodePerformanceProfile],
|
||||
) -> ResourceGraph: ...
|
||||
5
shared/types/models/common.py
Normal file
5
shared/types/models/common.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from shared.types.common import NewUUID
|
||||
|
||||
|
||||
class ModelId(NewUUID):
|
||||
pass
|
||||
9
shared/types/models/metadata.py
Normal file
9
shared/types/models/metadata.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import BaseModel, PositiveInt
|
||||
|
||||
|
||||
class ModelMetadata(BaseModel):
|
||||
pretty_name: str
|
||||
storage_size_kilobytes: Annotated[int, PositiveInt]
|
||||
n_layers: Annotated[int, PositiveInt]
|
||||
18
shared/types/models/model.py
Normal file
18
shared/types/models/model.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from typing import Sequence, final
|
||||
|
||||
from pydantic import BaseModel, TypeAdapter
|
||||
|
||||
from shared.types.models.common import ModelId
|
||||
from shared.types.models.metadata import ModelMetadata
|
||||
from shared.types.models.sources import ModelSource
|
||||
|
||||
|
||||
@final
|
||||
# Concerned by the naming here; model could also be an instance of a model.
|
||||
class ModelInfo(BaseModel):
|
||||
model_id: ModelId
|
||||
model_sources: Sequence[ModelSource]
|
||||
model_metadata: ModelMetadata
|
||||
|
||||
|
||||
ModelIdAdapter: TypeAdapter[ModelId] = TypeAdapter(ModelId)
|
||||
66
shared/types/models/sources.py
Normal file
66
shared/types/models/sources.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from enum import Enum
|
||||
from typing import Annotated, Any, Generic, Literal, TypeVar, Union, final
|
||||
|
||||
from pydantic import AnyHttpUrl, BaseModel, Field, TypeAdapter
|
||||
|
||||
from shared.types.models.common import ModelId
|
||||
|
||||
|
||||
class SourceType(str, Enum):
|
||||
HuggingFace = "HuggingFace"
|
||||
GitHub = "GitHub"
|
||||
|
||||
|
||||
class SourceFormatType(str, Enum):
|
||||
HuggingFaceTransformers = "HuggingFaceTransformers"
|
||||
|
||||
|
||||
T = TypeVar("T", bound=SourceType)
|
||||
S = TypeVar("S", bound=SourceFormatType)
|
||||
|
||||
RepoPath = Annotated[str, Field(pattern=r"^[^/]+/[^/]+$")]
|
||||
|
||||
|
||||
class BaseModelSource(BaseModel, Generic[T, S]):
|
||||
model_uuid: ModelId
|
||||
source_type: T
|
||||
source_format: S
|
||||
source_data: Any
|
||||
|
||||
|
||||
@final
|
||||
class HuggingFaceModelSourceData(BaseModel):
|
||||
path: RepoPath
|
||||
|
||||
|
||||
@final
|
||||
class GitHubModelSourceData(BaseModel):
|
||||
url: AnyHttpUrl
|
||||
|
||||
|
||||
@final
|
||||
class HuggingFaceModelSource(
|
||||
BaseModelSource[SourceType.HuggingFace, SourceFormatType.HuggingFaceTransformers]
|
||||
):
|
||||
source_type: Literal[SourceType.HuggingFace] = SourceType.HuggingFace
|
||||
source_format: Literal[SourceFormatType.HuggingFaceTransformers] = (
|
||||
SourceFormatType.HuggingFaceTransformers
|
||||
)
|
||||
source_data: HuggingFaceModelSourceData
|
||||
|
||||
|
||||
@final
|
||||
class GitHubModelSource(BaseModelSource[SourceType.GitHub, S]):
|
||||
source_type: Literal[SourceType.GitHub] = SourceType.GitHub
|
||||
source_data: GitHubModelSourceData
|
||||
|
||||
|
||||
_ModelSource = Annotated[
|
||||
Union[
|
||||
HuggingFaceModelSource,
|
||||
GitHubModelSource[SourceFormatType.HuggingFaceTransformers],
|
||||
],
|
||||
Field(discriminator="source_type"),
|
||||
]
|
||||
ModelSource = BaseModelSource[SourceType, SourceFormatType]
|
||||
ModelSourceAdapter: TypeAdapter[ModelSource] = TypeAdapter(_ModelSource)
|
||||
11
shared/types/networking/control_plane.py
Normal file
11
shared/types/networking/control_plane.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from typing import TypeAlias
|
||||
|
||||
from shared.types.common import NewUUID, NodeId
|
||||
from shared.types.graphs.common import Edge
|
||||
|
||||
|
||||
class ControlPlaneEdgeId(NewUUID):
|
||||
pass
|
||||
|
||||
|
||||
ControlPlaneEdgeType: TypeAlias = Edge[None, ControlPlaneEdgeId, NodeId]
|
||||
68
shared/types/networking/data_plane.py
Normal file
68
shared/types/networking/data_plane.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from enum import Enum
|
||||
from typing import Annotated, Literal, TypeVar, Union, final
|
||||
|
||||
from pydantic import BaseModel, Field, IPvAnyAddress, TypeAdapter
|
||||
|
||||
from shared.types.common import NewUUID, NodeId
|
||||
from shared.types.graphs.common import Edge
|
||||
|
||||
|
||||
class DataPlaneEdgeId(NewUUID):
|
||||
pass
|
||||
|
||||
|
||||
class AddressingProtocol(str, Enum):
|
||||
IPvAnyAddress = "IPvAnyAddress"
|
||||
|
||||
|
||||
class ApplicationProtocol(str, Enum):
|
||||
MLX = "MLX"
|
||||
|
||||
|
||||
AdP = TypeVar("AdP", bound=AddressingProtocol)
|
||||
ApP = TypeVar("ApP", bound=ApplicationProtocol)
|
||||
|
||||
|
||||
@final
|
||||
class DataPlaneEdgeProfile(BaseModel):
|
||||
throughput: float
|
||||
latency: float
|
||||
jitter: float
|
||||
|
||||
|
||||
class CommonDataPlaneEdgeData(BaseModel):
|
||||
edge_data_transfer_rate: DataPlaneEdgeProfile | None = None
|
||||
|
||||
|
||||
class MlxEdgeMetadata(BaseModel):
|
||||
source_ip: IPvAnyAddress
|
||||
sink_ip: IPvAnyAddress
|
||||
|
||||
|
||||
class BaseDataPlaneEdgeData[AdP: AddressingProtocol, ApP: ApplicationProtocol](
|
||||
BaseModel
|
||||
):
|
||||
addressing_protocol: AdP
|
||||
application_protocol: ApP
|
||||
common_data: CommonDataPlaneEdgeData
|
||||
|
||||
|
||||
class MlxEdge(
|
||||
BaseDataPlaneEdgeData[AddressingProtocol.IPvAnyAddress, ApplicationProtocol.MLX]
|
||||
):
|
||||
addressing_protocol: Literal[AddressingProtocol.IPvAnyAddress] = (
|
||||
AddressingProtocol.IPvAnyAddress
|
||||
)
|
||||
application_protocol: Literal[ApplicationProtocol.MLX] = ApplicationProtocol.MLX
|
||||
mlx_metadata: MlxEdgeMetadata
|
||||
|
||||
|
||||
DataPlaneEdgeData = Union[MlxEdge]
|
||||
|
||||
_DataPlaneEdgeData = Annotated[
|
||||
DataPlaneEdgeData,
|
||||
Field(discriminator="addressing_protocol"),
|
||||
]
|
||||
DataPlaneEdgeAdapter: TypeAdapter[DataPlaneEdgeData] = TypeAdapter(_DataPlaneEdgeData)
|
||||
|
||||
DataPlaneEdge = Edge[DataPlaneEdgeData, DataPlaneEdgeId, NodeId]
|
||||
29
shared/types/networking/services.py
Normal file
29
shared/types/networking/services.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from typing import Callable, NewType, Protocol
|
||||
|
||||
from shared.types.networking.control_plane import (
|
||||
ControlPlaneEdgeId,
|
||||
ControlPlaneEdgeType,
|
||||
)
|
||||
|
||||
TopicName = NewType("TopicName", str)
|
||||
|
||||
PubSubMessageHandler = Callable[[TopicName, object], None]
|
||||
NodeConnectedHandler = Callable[
|
||||
[
|
||||
ControlPlaneEdgeId,
|
||||
ControlPlaneEdgeType,
|
||||
],
|
||||
None,
|
||||
]
|
||||
NodeDisconnectedHandler = Callable[[ControlPlaneEdgeId], None]
|
||||
|
||||
|
||||
class DiscoveryService(Protocol):
|
||||
def on_node_connected(self, handler: NodeConnectedHandler) -> None: ...
|
||||
def on_node_disconnected(self, handler: NodeDisconnectedHandler) -> None: ...
|
||||
|
||||
|
||||
class PubSubService(Protocol):
|
||||
def on_message_received(
|
||||
self, topic_name: TopicName, handler: PubSubMessageHandler
|
||||
) -> None: ...
|
||||
72
shared/types/networking/topology.py
Normal file
72
shared/types/networking/topology.py
Normal file
@@ -0,0 +1,72 @@
|
||||
from shared.types.common import NodeId
|
||||
from shared.types.graphs.common import Graph, GraphData
|
||||
from shared.types.networking.control_plane import ControlPlaneEdgeId
|
||||
from shared.types.networking.data_plane import (
|
||||
DataPlaneEdgeData,
|
||||
DataPlaneEdgeId,
|
||||
)
|
||||
from shared.types.worker.common import NodeStatus
|
||||
|
||||
|
||||
class DataPlaneTopology(
|
||||
Graph[
|
||||
DataPlaneEdgeData,
|
||||
None,
|
||||
DataPlaneEdgeId,
|
||||
NodeId,
|
||||
]
|
||||
):
|
||||
graph_data: GraphData[
|
||||
DataPlaneEdgeData,
|
||||
None,
|
||||
DataPlaneEdgeId,
|
||||
NodeId,
|
||||
]
|
||||
|
||||
|
||||
class OrphanedPartOfDataPlaneTopology(
|
||||
Graph[
|
||||
DataPlaneEdgeData,
|
||||
None,
|
||||
DataPlaneEdgeId,
|
||||
NodeId,
|
||||
]
|
||||
):
|
||||
graph_data: GraphData[
|
||||
DataPlaneEdgeData,
|
||||
None,
|
||||
DataPlaneEdgeId,
|
||||
NodeId,
|
||||
]
|
||||
|
||||
|
||||
class ControlPlaneTopology(
|
||||
Graph[
|
||||
None,
|
||||
NodeStatus,
|
||||
ControlPlaneEdgeId,
|
||||
NodeId,
|
||||
]
|
||||
):
|
||||
graph_data: GraphData[
|
||||
None,
|
||||
NodeStatus,
|
||||
ControlPlaneEdgeId,
|
||||
NodeId,
|
||||
]
|
||||
|
||||
|
||||
class OrphanedPartOfControlPlaneTopology(
|
||||
Graph[
|
||||
None,
|
||||
NodeStatus,
|
||||
ControlPlaneEdgeId,
|
||||
NodeId,
|
||||
]
|
||||
):
|
||||
graph_data: GraphData[
|
||||
None,
|
||||
NodeStatus,
|
||||
ControlPlaneEdgeId,
|
||||
NodeId,
|
||||
]
|
||||
54
shared/types/profiling/common.py
Normal file
54
shared/types/profiling/common.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from enum import Enum
|
||||
from typing import Annotated, Generic, Literal, TypeVar
|
||||
|
||||
from pydantic import BaseModel, Field, TypeAdapter
|
||||
|
||||
|
||||
class ProfiledResourceName(str, Enum):
|
||||
memory = "memory"
|
||||
system = "system"
|
||||
|
||||
|
||||
ProfiledResourceT = TypeVar(name="ProfiledResourceT", bound=ProfiledResourceName)
|
||||
|
||||
|
||||
class BasePerformanceProfile(BaseModel, Generic[ProfiledResourceT]):
|
||||
"""
|
||||
Details a single resource (or resource type) that is being monitored by the resource monitor.
|
||||
"""
|
||||
|
||||
|
||||
class MemoryPerformanceProfile(BasePerformanceProfile[ProfiledResourceName.memory]):
|
||||
resource_name: Literal[ProfiledResourceName.memory] = Field(
|
||||
default=ProfiledResourceName.memory, frozen=True
|
||||
)
|
||||
ram_total: int
|
||||
ram_used: int
|
||||
swap_total: int
|
||||
swap_used: int
|
||||
|
||||
|
||||
class NetworkInterfaceInfo(BaseModel):
|
||||
name: str
|
||||
ip_address: str
|
||||
type: str
|
||||
|
||||
|
||||
class SystemPerformanceProfile(BasePerformanceProfile[ProfiledResourceName.system]):
|
||||
resource_name: Literal[ProfiledResourceName.system] = Field(
|
||||
default=ProfiledResourceName.system, frozen=True
|
||||
)
|
||||
model_id: str
|
||||
chip_id: str
|
||||
memory: int
|
||||
network_interfaces: list[NetworkInterfaceInfo] = Field(default_factory=list)
|
||||
|
||||
|
||||
NodePerformanceProfile = Annotated[
|
||||
MemoryPerformanceProfile | SystemPerformanceProfile,
|
||||
Field(discriminator="resource_name"),
|
||||
]
|
||||
|
||||
NodePerformanceProfileTypeAdapter: TypeAdapter[NodePerformanceProfile] = TypeAdapter(
|
||||
NodePerformanceProfile
|
||||
)
|
||||
85
shared/types/states/master.py
Normal file
85
shared/types/states/master.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from collections.abc import Mapping, Sequence
|
||||
from enum import Enum
|
||||
from queue import Queue
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from shared.types.common import NodeId
|
||||
from shared.types.events.common import (
|
||||
Event,
|
||||
EventCategories,
|
||||
State,
|
||||
)
|
||||
from shared.types.graphs.resource_graph import ResourceGraph
|
||||
from shared.types.networking.data_plane import (
|
||||
DataPlaneEdge,
|
||||
DataPlaneEdgeId,
|
||||
)
|
||||
from shared.types.networking.topology import (
|
||||
ControlPlaneTopology,
|
||||
DataPlaneTopology,
|
||||
OrphanedPartOfControlPlaneTopology,
|
||||
OrphanedPartOfDataPlaneTopology,
|
||||
)
|
||||
from shared.types.profiling.common import NodePerformanceProfile
|
||||
from shared.types.states.shared import SharedState
|
||||
from shared.types.tasks.common import TaskData, TaskType
|
||||
from shared.types.worker.instances import InstanceData, InstanceId
|
||||
|
||||
|
||||
class ExternalCommand(BaseModel): ...
|
||||
|
||||
|
||||
class CachePolicyType(str, Enum):
|
||||
KeepAll = "KeepAll"
|
||||
|
||||
|
||||
CachePolicyTypeT = TypeVar("CachePolicyTypeT", bound=CachePolicyType)
|
||||
|
||||
|
||||
class CachePolicy(BaseModel, Generic[CachePolicyTypeT]):
|
||||
policy_type: CachePolicyTypeT
|
||||
|
||||
|
||||
class NodePerformanceProfileState(State[EventCategories.NodePerformanceEventTypes]):
|
||||
node_profiles: Mapping[NodeId, NodePerformanceProfile]
|
||||
|
||||
|
||||
class DataPlaneNetworkState(State[EventCategories.DataPlaneEventTypes]):
|
||||
topology: DataPlaneTopology
|
||||
history: Sequence[OrphanedPartOfDataPlaneTopology]
|
||||
|
||||
def delete_edge(self, edge_id: DataPlaneEdgeId) -> None: ...
|
||||
def add_edge(self, edge: DataPlaneEdge) -> None: ...
|
||||
|
||||
|
||||
class ControlPlaneNetworkState(State[EventCategories.ControlPlaneEventTypes]):
|
||||
topology: ControlPlaneTopology
|
||||
history: Sequence[OrphanedPartOfControlPlaneTopology]
|
||||
|
||||
def delete_edge(self, edge_id: DataPlaneEdgeId) -> None: ...
|
||||
def add_edge(self, edge: DataPlaneEdge) -> None: ...
|
||||
|
||||
|
||||
class MasterState(SharedState):
|
||||
data_plane_network_state: DataPlaneNetworkState
|
||||
control_plane_network_state: ControlPlaneNetworkState
|
||||
job_inbox: Queue[TaskData[TaskType]]
|
||||
job_outbox: Queue[TaskData[TaskType]]
|
||||
cache_policy: CachePolicy[CachePolicyType]
|
||||
|
||||
|
||||
def get_shard_assignments(
|
||||
inbox: Queue[ExternalCommand],
|
||||
outbox: Queue[ExternalCommand],
|
||||
resource_graph: ResourceGraph,
|
||||
current_instances: Mapping[InstanceId, InstanceData],
|
||||
cache_policy: CachePolicy[CachePolicyType],
|
||||
) -> Mapping[InstanceId, InstanceData]: ...
|
||||
|
||||
|
||||
def get_transition_events(
|
||||
current_instances: Mapping[InstanceId, InstanceData],
|
||||
target_instances: Mapping[InstanceId, InstanceData],
|
||||
) -> Sequence[Event[EventCategories]]: ...
|
||||
30
shared/types/states/shared.py
Normal file
30
shared/types/states/shared.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from collections.abc import Mapping
|
||||
from typing import Sequence
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from shared.types.common import NodeId
|
||||
from shared.types.events.common import EventCategories, State
|
||||
from shared.types.tasks.common import Task, TaskId, TaskStatusType, TaskType
|
||||
from shared.types.worker.common import InstanceId
|
||||
from shared.types.worker.instances import BaseInstance
|
||||
|
||||
|
||||
class KnownInstances(State[EventCategories.InstanceStateEventTypes]):
|
||||
instances: Mapping[InstanceId, BaseInstance]
|
||||
|
||||
|
||||
class Tasks(State[EventCategories.TaskEventTypes]):
|
||||
tasks: Mapping[TaskId, Task[TaskType, TaskStatusType]]
|
||||
|
||||
|
||||
class SharedState(BaseModel):
|
||||
node_id: NodeId
|
||||
known_instances: KnownInstances
|
||||
compute_tasks: Tasks
|
||||
|
||||
def get_node_id(self) -> NodeId: ...
|
||||
|
||||
def get_tasks_by_instance(
|
||||
self, instance_id: InstanceId
|
||||
) -> Sequence[Task[TaskType, TaskStatusType]]: ...
|
||||
17
shared/types/states/worker.py
Normal file
17
shared/types/states/worker.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from collections.abc import Mapping
|
||||
|
||||
from shared.types.common import NodeId
|
||||
from shared.types.events.common import (
|
||||
EventCategories,
|
||||
State,
|
||||
)
|
||||
from shared.types.states.shared import SharedState
|
||||
from shared.types.worker.common import NodeStatus
|
||||
|
||||
|
||||
class NodeStatusState(State[EventCategories.ControlPlaneEventTypes]):
|
||||
node_status: Mapping[NodeId, NodeStatus]
|
||||
|
||||
|
||||
class WorkerState(SharedState):
|
||||
node_status: NodeStatusState
|
||||
120
shared/types/tasks/common.py
Normal file
120
shared/types/tasks/common.py
Normal file
@@ -0,0 +1,120 @@
|
||||
from collections.abc import Mapping
|
||||
from enum import Enum
|
||||
from typing import Annotated, Generic, Literal, TypeVar, Union
|
||||
|
||||
import openai.types.chat as openai
|
||||
from pydantic import BaseModel, Field, TypeAdapter
|
||||
|
||||
from shared.types.common import NewUUID
|
||||
from shared.types.worker.common import InstanceId, RunnerId
|
||||
|
||||
|
||||
class TaskId(NewUUID):
|
||||
pass
|
||||
|
||||
|
||||
class TaskType(str, Enum):
|
||||
ChatCompletionNonStreaming = "ChatCompletionNonStreaming"
|
||||
ChatCompletionStreaming = "ChatCompletionStreaming"
|
||||
|
||||
|
||||
TaskTypeT = TypeVar("TaskTypeT", bound=TaskType, covariant=True)
|
||||
|
||||
|
||||
class TaskData(BaseModel, Generic[TaskTypeT]): ...
|
||||
|
||||
|
||||
class ChatCompletionNonStreamingTask(TaskData[TaskType.ChatCompletionNonStreaming]):
|
||||
task_type: Literal[TaskType.ChatCompletionNonStreaming] = (
|
||||
TaskType.ChatCompletionNonStreaming
|
||||
)
|
||||
task_data: openai.completion_create_params.CompletionCreateParams
|
||||
|
||||
|
||||
class ChatCompletionStreamingTask(TaskData[TaskType.ChatCompletionStreaming]):
|
||||
task_type: Literal[TaskType.ChatCompletionStreaming] = (
|
||||
TaskType.ChatCompletionStreaming
|
||||
)
|
||||
task_data: openai.completion_create_params.CompletionCreateParams
|
||||
|
||||
|
||||
class TaskStatusIncompleteType(str, Enum):
|
||||
Pending = "Pending"
|
||||
Running = "Running"
|
||||
Failed = "Failed"
|
||||
|
||||
|
||||
class TaskStatusCompleteType(str, Enum):
|
||||
Complete = "Complete"
|
||||
|
||||
|
||||
TaskStatusType = Union[TaskStatusIncompleteType, TaskStatusCompleteType]
|
||||
|
||||
|
||||
class TaskArtifact[TaskTypeT: TaskType, TaskStatusTypeT: TaskStatusType](BaseModel): ...
|
||||
|
||||
|
||||
class IncompleteTaskArtifact[TaskTypeT: TaskType](
|
||||
TaskArtifact[TaskTypeT, TaskStatusIncompleteType]
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class TaskStatusUpdate[TaskStatusTypeT: TaskStatusType](BaseModel):
|
||||
task_status: TaskStatusTypeT
|
||||
|
||||
|
||||
class PendingTaskStatus(TaskStatusUpdate[TaskStatusIncompleteType.Pending]):
|
||||
task_status: Literal[TaskStatusIncompleteType.Pending] = (
|
||||
TaskStatusIncompleteType.Pending
|
||||
)
|
||||
|
||||
|
||||
class RunningTaskStatus(TaskStatusUpdate[TaskStatusIncompleteType.Running]):
|
||||
task_status: Literal[TaskStatusIncompleteType.Running] = (
|
||||
TaskStatusIncompleteType.Running
|
||||
)
|
||||
|
||||
|
||||
class CompletedTaskStatus(TaskStatusUpdate[TaskStatusCompleteType.Complete]):
|
||||
task_status: Literal[TaskStatusCompleteType.Complete] = (
|
||||
TaskStatusCompleteType.Complete
|
||||
)
|
||||
|
||||
|
||||
class FailedTaskStatus(TaskStatusUpdate[TaskStatusIncompleteType.Failed]):
|
||||
task_status: Literal[TaskStatusIncompleteType.Failed] = (
|
||||
TaskStatusIncompleteType.Failed
|
||||
)
|
||||
error_message: Mapping[RunnerId, str]
|
||||
|
||||
|
||||
class TaskState[TaskStatusTypeT: TaskStatusType, TaskTypeT: TaskType](BaseModel):
|
||||
task_status: TaskStatusUpdate[TaskStatusTypeT]
|
||||
task_artifact: TaskArtifact[TaskTypeT, TaskStatusTypeT]
|
||||
|
||||
|
||||
class BaseTask[TaskTypeT: TaskType, TaskStatusTypeT: TaskStatusType](BaseModel):
|
||||
task_type: TaskTypeT
|
||||
task_data: TaskData[TaskTypeT]
|
||||
task_state: TaskState[TaskStatusTypeT, TaskTypeT]
|
||||
on_instance: InstanceId
|
||||
|
||||
|
||||
BaseTaskAnnotated = Annotated[
|
||||
Union[
|
||||
BaseTask[Literal[TaskType.ChatCompletionNonStreaming], TaskStatusType],
|
||||
BaseTask[Literal[TaskType.ChatCompletionStreaming], TaskStatusType],
|
||||
],
|
||||
Field(discriminator="task_type"),
|
||||
]
|
||||
|
||||
BaseTaskValidator: TypeAdapter[BaseTask[TaskType, TaskStatusType]] = TypeAdapter(
|
||||
BaseTaskAnnotated
|
||||
)
|
||||
|
||||
|
||||
class Task[TaskTypeT: TaskType, TaskStatusTypeT: TaskStatusType](
|
||||
BaseTask[TaskTypeT, TaskStatusTypeT]
|
||||
):
|
||||
task_id: TaskId
|
||||
102
shared/types/worker/commands_runner.py
Normal file
102
shared/types/worker/commands_runner.py
Normal file
@@ -0,0 +1,102 @@
|
||||
from enum import Enum
|
||||
from typing import Annotated, Generic, Literal, TypeVar
|
||||
|
||||
from pydantic import BaseModel, Field, TypeAdapter
|
||||
|
||||
from shared.openai import FinishReason
|
||||
from shared.types.api import ChatTask
|
||||
from shared.types.worker.mlx import Host
|
||||
from shared.types.worker.shards import PartitionStrategy, ShardMetadata
|
||||
|
||||
## Messages passed TO the runner
|
||||
|
||||
|
||||
class MessageType(str, Enum):
|
||||
Setup = "setup"
|
||||
ChatTask = "chat_task"
|
||||
Exit = "exit"
|
||||
|
||||
|
||||
MT = TypeVar(name="MT", bound=MessageType)
|
||||
|
||||
|
||||
class BaseRunnerMessage(BaseModel, Generic[MT]):
|
||||
pass
|
||||
|
||||
|
||||
class SetupMessage(BaseRunnerMessage[MessageType.Setup]):
|
||||
type: Literal[MessageType.Setup] = Field(default=MessageType.Setup, frozen=True)
|
||||
model_shard_meta: ShardMetadata[PartitionStrategy]
|
||||
hosts: list[Host]
|
||||
|
||||
|
||||
class ChatTaskMessage(BaseRunnerMessage[MessageType.ChatTask]):
|
||||
type: Literal[MessageType.ChatTask] = Field(
|
||||
default=MessageType.ChatTask, frozen=True
|
||||
)
|
||||
task: ChatTask
|
||||
|
||||
|
||||
class ExitMessage(BaseRunnerMessage[MessageType.Exit]):
|
||||
type: Literal[MessageType.Exit] = Field(default=MessageType.Exit, frozen=True)
|
||||
|
||||
|
||||
RunnerMessage = Annotated[
|
||||
SetupMessage | ChatTaskMessage | ExitMessage, Field(discriminator="type")
|
||||
]
|
||||
RunnerMessageTypeAdapter: TypeAdapter[RunnerMessage] = TypeAdapter(RunnerMessage)
|
||||
|
||||
## Responses passed FROM the runner
|
||||
|
||||
|
||||
class RunnerResponseType(str, Enum):
|
||||
GenerationResponse = "generation_response"
|
||||
FinishedResponse = "finished_response"
|
||||
PrintResponse = "print_response"
|
||||
ErrorResponse = "error_response"
|
||||
|
||||
|
||||
RRT = TypeVar(name="RRT", bound=RunnerResponseType)
|
||||
|
||||
|
||||
class BaseRunnerResponse(BaseModel, Generic[RRT]):
|
||||
pass
|
||||
|
||||
|
||||
class GenerationResponse(BaseRunnerResponse[RunnerResponseType.GenerationResponse]):
|
||||
type: Literal[RunnerResponseType.GenerationResponse] = Field(
|
||||
default=RunnerResponseType.GenerationResponse, frozen=True
|
||||
)
|
||||
text: str
|
||||
token: int
|
||||
# logprobs: Optional[list[float]] = None # too big. we can change to be top-k
|
||||
finish_reason: FinishReason | None = None
|
||||
|
||||
|
||||
class PrintResponse(BaseRunnerResponse[RunnerResponseType.PrintResponse]):
|
||||
type: Literal[RunnerResponseType.PrintResponse] = Field(
|
||||
default=RunnerResponseType.PrintResponse, frozen=True
|
||||
)
|
||||
text: str
|
||||
|
||||
|
||||
class FinishedResponse(BaseRunnerResponse[RunnerResponseType.FinishedResponse]):
|
||||
type: Literal[RunnerResponseType.FinishedResponse] = Field(
|
||||
default=RunnerResponseType.FinishedResponse, frozen=True
|
||||
)
|
||||
|
||||
|
||||
class ErrorResponse(BaseRunnerResponse[RunnerResponseType.ErrorResponse]):
|
||||
type: Literal[RunnerResponseType.ErrorResponse] = Field(
|
||||
default=RunnerResponseType.ErrorResponse, frozen=True
|
||||
)
|
||||
error_type: str
|
||||
error_message: str
|
||||
traceback: str | None = None
|
||||
|
||||
|
||||
RunnerResponse = Annotated[
|
||||
GenerationResponse | PrintResponse | FinishedResponse | ErrorResponse,
|
||||
Field(discriminator="type"),
|
||||
]
|
||||
RunnerResponseTypeAdapter: TypeAdapter[RunnerResponse] = TypeAdapter(RunnerResponse)
|
||||
17
shared/types/worker/common.py
Normal file
17
shared/types/worker/common.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from enum import Enum
|
||||
|
||||
from shared.types.common import NewUUID
|
||||
|
||||
|
||||
class InstanceId(NewUUID):
|
||||
pass
|
||||
|
||||
|
||||
class RunnerId(NewUUID):
|
||||
pass
|
||||
|
||||
|
||||
class NodeStatus(str, Enum):
|
||||
Idle = "Idle"
|
||||
Running = "Running"
|
||||
Paused = "Paused"
|
||||
85
shared/types/worker/downloads.py
Normal file
85
shared/types/worker/downloads.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from enum import Enum
|
||||
from typing import (
|
||||
Annotated,
|
||||
Callable,
|
||||
Generic,
|
||||
Literal,
|
||||
NewType,
|
||||
Sequence,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
from pydantic import BaseModel, Field, PositiveInt
|
||||
|
||||
from shared.types.common import NodeId
|
||||
from shared.types.models.common import ModelId
|
||||
from shared.types.models.sources import ModelSource
|
||||
from shared.types.worker.shards import PartitionStrategy, ShardMetadata
|
||||
|
||||
|
||||
class DownloadProgressData(BaseModel):
|
||||
total_bytes: Annotated[int, PositiveInt]
|
||||
downloaded_bytes: Annotated[int, PositiveInt]
|
||||
|
||||
|
||||
class DownloadStatus(str, Enum):
|
||||
Pending = "Pending"
|
||||
Downloading = "Downloading"
|
||||
Completed = "Completed"
|
||||
Failed = "Failed"
|
||||
|
||||
|
||||
DownloadStatusT = TypeVar("DownloadStatusT", bound=DownloadStatus)
|
||||
|
||||
|
||||
class BaseDownloadProgress(BaseModel, Generic[DownloadStatusT]):
|
||||
node_id: NodeId
|
||||
download_status: DownloadStatusT
|
||||
|
||||
|
||||
class DownloadPending(BaseDownloadProgress[DownloadStatus.Pending]):
|
||||
download_status: Literal[DownloadStatus.Pending] = Field(DownloadStatus.Pending)
|
||||
|
||||
|
||||
class DownloadCompleted(BaseDownloadProgress[DownloadStatus.Completed]):
|
||||
download_status: Literal[DownloadStatus.Completed] = Field(DownloadStatus.Completed)
|
||||
|
||||
|
||||
class DownloadFailed(BaseDownloadProgress[DownloadStatus.Failed]):
|
||||
download_status: Literal[DownloadStatus.Failed] = Field(DownloadStatus.Failed)
|
||||
error_message: str
|
||||
|
||||
|
||||
class DownloadOngoing(BaseDownloadProgress[DownloadStatus.Downloading]):
|
||||
download_status: Literal[DownloadStatus.Downloading] = Field(
|
||||
DownloadStatus.Downloading
|
||||
)
|
||||
download_progress: DownloadProgressData
|
||||
|
||||
|
||||
DownloadProgress = Annotated[
|
||||
Union[
|
||||
DownloadPending,
|
||||
DownloadCompleted,
|
||||
DownloadFailed,
|
||||
DownloadOngoing,
|
||||
],
|
||||
Field(discriminator="download_status"),
|
||||
]
|
||||
|
||||
|
||||
BytesToDownload = NewType("BytesToDownload", int)
|
||||
BytesDownloaded = NewType("BytesDownloaded", int)
|
||||
|
||||
DownloadEffectHandler = Callable[
|
||||
[ModelId, DownloadStatus, BytesToDownload, BytesDownloaded], None
|
||||
]
|
||||
|
||||
|
||||
def download_shard(
|
||||
model_id: ModelId,
|
||||
model_source: ModelSource,
|
||||
shard_meta: ShardMetadata[PartitionStrategy],
|
||||
effect_handlers: Sequence[DownloadEffectHandler],
|
||||
) -> None: ...
|
||||
35
shared/types/worker/instances.py
Normal file
35
shared/types/worker/instances.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from collections.abc import Mapping
|
||||
from enum import Enum
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from shared.types.worker.common import InstanceId
|
||||
from shared.types.worker.runners import (
|
||||
RunnerId,
|
||||
RunnerState,
|
||||
RunnerStateType,
|
||||
ShardAssignments,
|
||||
)
|
||||
|
||||
|
||||
class InstanceStatus(str, Enum):
|
||||
ACTIVE = "active"
|
||||
INACTIVE = "inactive"
|
||||
|
||||
|
||||
class InstanceState(BaseModel):
|
||||
runner_states: Mapping[RunnerId, RunnerState[RunnerStateType]]
|
||||
|
||||
|
||||
class InstanceData(BaseModel):
|
||||
shard_assignments: ShardAssignments
|
||||
|
||||
|
||||
class BaseInstance(BaseModel):
|
||||
instance_data: InstanceData
|
||||
instance_state: InstanceState
|
||||
instance_status: InstanceStatus
|
||||
|
||||
|
||||
class Instance(BaseInstance):
|
||||
instance_id: InstanceId
|
||||
13
shared/types/worker/mlx.py
Normal file
13
shared/types/worker/mlx.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from pydantic import BaseModel, field_validator
|
||||
|
||||
|
||||
# TODO: Is this the right place for this? Host is consumed by worker, but typically stored in the master
|
||||
class Host(BaseModel):
|
||||
host: str
|
||||
port: int
|
||||
|
||||
@field_validator("port")
|
||||
def check_port(self, v: int) -> int:
|
||||
if not (0 <= v <= 65535):
|
||||
raise ValueError("Port must be between 0 and 65535")
|
||||
return v
|
||||
73
shared/types/worker/resource_monitor.py
Normal file
73
shared/types/worker/resource_monitor.py
Normal file
@@ -0,0 +1,73 @@
|
||||
import asyncio
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Coroutine
|
||||
from typing import Callable, Set
|
||||
|
||||
from shared.types.events.events import ResourceProfiled
|
||||
from shared.types.profiling.common import (
|
||||
MemoryPerformanceProfile,
|
||||
NodePerformanceProfile,
|
||||
SystemPerformanceProfile,
|
||||
)
|
||||
|
||||
|
||||
class EventLog:
|
||||
def append(self, event: ResourceProfiled) -> None: ...
|
||||
|
||||
|
||||
class ResourceCollector(ABC):
|
||||
"""
|
||||
Details a single resource (or resource type) that is being monitored by the resource monitor.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
|
||||
@abstractmethod
|
||||
async def collect(self) -> NodePerformanceProfile: ...
|
||||
|
||||
|
||||
class SystemResourceCollector(ResourceCollector):
|
||||
def __init__(self):
|
||||
super().__init__("system")
|
||||
|
||||
@abstractmethod
|
||||
async def collect(self) -> SystemPerformanceProfile: ...
|
||||
|
||||
|
||||
class MemoryResourceCollector(ResourceCollector):
|
||||
def __init__(self):
|
||||
super().__init__("memory")
|
||||
|
||||
@abstractmethod
|
||||
async def collect(self) -> MemoryPerformanceProfile: ...
|
||||
|
||||
|
||||
class ResourceMonitor:
|
||||
def __init__(
|
||||
self,
|
||||
collectors: list[ResourceCollector],
|
||||
effect_handlers: Set[Callable[[NodePerformanceProfile], None]],
|
||||
):
|
||||
self.effect_handlers: Set[Callable[[NodePerformanceProfile], None]] = (
|
||||
effect_handlers
|
||||
)
|
||||
self.collectors: list[ResourceCollector] = collectors
|
||||
|
||||
# Since there's no implementation, this breaks the typechecker.
|
||||
# self.collectors: list[ResourceCollector] = [
|
||||
# SystemResourceCollector(),
|
||||
# MemoryResourceCollector(),
|
||||
# ]
|
||||
|
||||
async def _collect(self) -> list[NodePerformanceProfile]:
|
||||
tasks: list[Coroutine[None, None, NodePerformanceProfile]] = [
|
||||
collector.collect() for collector in self.collectors
|
||||
]
|
||||
return await asyncio.gather(*tasks)
|
||||
|
||||
async def collect(self) -> None:
|
||||
profiles = await self._collect()
|
||||
for profile in profiles:
|
||||
for effect_handler in self.effect_handlers:
|
||||
effect_handler(profile)
|
||||
74
shared/types/worker/runners.py
Normal file
74
shared/types/worker/runners.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from collections.abc import Mapping, Sequence
|
||||
from enum import Enum
|
||||
from typing import Generic, Literal, TypeVar
|
||||
|
||||
from pydantic import BaseModel, model_validator
|
||||
|
||||
from shared.types.common import NodeId
|
||||
from shared.types.models.common import ModelId
|
||||
from shared.types.worker.common import RunnerId
|
||||
from shared.types.worker.downloads import BaseDownloadProgress, DownloadStatus
|
||||
from shared.types.worker.shards import PartitionStrategy, ShardMetadata
|
||||
|
||||
|
||||
class RunnerStateType(str, Enum):
|
||||
Rejected = "Rejected"
|
||||
Starting = "Starting"
|
||||
Downloading = "Downloading"
|
||||
Running = "Running"
|
||||
Failed = "Failed"
|
||||
|
||||
|
||||
RunnerStateTypeT = TypeVar("RunnerStateTypeT", bound=RunnerStateType)
|
||||
|
||||
|
||||
class RunnerState(BaseModel, Generic[RunnerStateTypeT]):
|
||||
runner_state: RunnerStateTypeT
|
||||
|
||||
|
||||
class RejectedRunnerState(RunnerState[RunnerStateType.Rejected]):
|
||||
runner_state: Literal[RunnerStateType.Rejected]
|
||||
|
||||
|
||||
class StartingRunnerState(RunnerState[RunnerStateType.Starting]):
|
||||
runner_state: Literal[RunnerStateType.Starting]
|
||||
|
||||
|
||||
class DownloadingRunnerState(RunnerState[RunnerStateType.Downloading]):
|
||||
runner_state: Literal[RunnerStateType.Downloading]
|
||||
download_progress: BaseDownloadProgress[DownloadStatus]
|
||||
|
||||
|
||||
class RunningRunnerState(RunnerState[RunnerStateType.Running]):
|
||||
runner_state: Literal[RunnerStateType.Running]
|
||||
|
||||
|
||||
class FailedRunnerState(RunnerState[RunnerStateType.Failed]):
|
||||
runner_state: Literal[RunnerStateType.Failed]
|
||||
error_message: str | None = None
|
||||
|
||||
|
||||
class RunnerData(BaseModel):
|
||||
runner_id: RunnerId
|
||||
runner_state: RunnerState[RunnerStateType] = RunnerState(
|
||||
runner_state=RunnerStateType.Starting
|
||||
)
|
||||
|
||||
|
||||
PartitionStrategyT = TypeVar(name="PartitionStrategyT", bound=PartitionStrategy)
|
||||
|
||||
|
||||
class ShardAssignments(BaseModel):
|
||||
model_id: ModelId
|
||||
runner_to_shard: Mapping[RunnerId, ShardMetadata[PartitionStrategy]]
|
||||
node_to_runner: Mapping[NodeId, Sequence[RunnerId]]
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_runners_exist(self) -> "ShardAssignments":
|
||||
for runners in self.node_to_runner.values():
|
||||
for runner_id in runners:
|
||||
if runner_id not in self.runner_to_shard:
|
||||
raise ValueError(
|
||||
f"Runner {runner_id} in node_to_runner does not exist in runner_to_shard"
|
||||
)
|
||||
return self
|
||||
54
shared/types/worker/shards.py
Normal file
54
shared/types/worker/shards.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from enum import Enum
|
||||
from typing import Annotated, Generic, Literal, TypeVar
|
||||
|
||||
from pydantic import BaseModel, DirectoryPath, Field, TypeAdapter
|
||||
|
||||
from shared.types.common import NodeId
|
||||
from shared.types.models.common import ModelId
|
||||
|
||||
|
||||
class PartitionStrategy(str, Enum):
|
||||
pipeline = "pipeline"
|
||||
|
||||
|
||||
PartitionStrategyT = TypeVar(name="PartitionStrategyT", bound=PartitionStrategy)
|
||||
|
||||
|
||||
class ShardMetadata(BaseModel, Generic[PartitionStrategyT]):
|
||||
"""
|
||||
Defines a specific shard of the model that is ready to be run on a device.
|
||||
Replaces previous `Shard` object.
|
||||
"""
|
||||
|
||||
device_rank: int
|
||||
world_size: int
|
||||
model_id: ModelId
|
||||
model_path: DirectoryPath
|
||||
|
||||
|
||||
class PipelineShardMeta(ShardMetadata[PartitionStrategy.pipeline]):
|
||||
"""
|
||||
Pipeline parallelism shard meta.
|
||||
"""
|
||||
|
||||
partition_strategy: Literal[PartitionStrategy.pipeline] = Field(
|
||||
default=PartitionStrategy.pipeline, frozen=True
|
||||
)
|
||||
start_layer: Annotated[int, Field(ge=0)]
|
||||
end_layer: Annotated[int, Field(ge=0)]
|
||||
|
||||
|
||||
_ShardMeta = Annotated[PipelineShardMeta, Field(discriminator="partition_strategy")]
|
||||
ShardMetaAdapter: TypeAdapter[ShardMetadata[PartitionStrategy]] = TypeAdapter(
|
||||
_ShardMeta
|
||||
)
|
||||
|
||||
|
||||
class ShardPlacement(BaseModel, Generic[PartitionStrategyT]):
|
||||
"""
|
||||
A shard placement is the description of a model distributed across a set of nodes.
|
||||
The Generic[PartitionStrategyT] enforces that the shard assignments all use the same partition strategy.
|
||||
"""
|
||||
|
||||
model_id: ModelId
|
||||
shard_assignments: dict[NodeId, ShardMetadata[PartitionStrategyT]]
|
||||
9
shared/utils.py
Normal file
9
shared/utils.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from typing import Any, Type, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def ensure_type(obj: Any, expected_type: Type[T]) -> T: # type: ignore
|
||||
if not isinstance(obj, expected_type):
|
||||
raise TypeError(f"Expected {expected_type}, got {type(obj)}") # type: ignore
|
||||
return obj
|
||||
148
uv.lock
generated
148
uv.lock
generated
@@ -29,6 +29,19 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
{ name = "sniffio", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "basedpyright"
|
||||
version = "1.29.4"
|
||||
@@ -41,6 +54,24 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/dc/180fe721a2574fb3aad4051adcca196ac2d18adaf75122f5eeb47436cca2/basedpyright-1.29.4-py3-none-any.whl", hash = "sha256:e087513979972f83010639c6c1a1c13dd3b1d24ee45f8ecff747962cc2063d6f", size = 11476859, upload-time = "2025-06-11T22:25:52.01Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.6.15"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "distro"
|
||||
version = "1.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exo"
|
||||
version = "0.2.0"
|
||||
@@ -105,6 +136,7 @@ name = "exo-shared"
|
||||
version = "0.1.0"
|
||||
source = { editable = "shared" }
|
||||
dependencies = [
|
||||
{ name = "openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
{ name = "pathlib", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
{ name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
{ name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
@@ -118,6 +150,7 @@ dev = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "openai", specifier = ">=1.93.0" },
|
||||
{ name = "pathlib", specifier = ">=1.0.1" },
|
||||
{ name = "protobuf", specifier = ">=6.31.1" },
|
||||
{ name = "pydantic", specifier = ">=2.11.7" },
|
||||
@@ -138,6 +171,52 @@ dependencies = [
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "exo-shared", editable = "shared" }]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
{ name = "h11", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
{ name = "certifi", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
{ name = "httpcore", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
{ name = "idna", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.1.0"
|
||||
@@ -147,6 +226,38 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiter"
|
||||
version = "0.10.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/9d/ae7ddb4b8ab3fb1b51faf4deb36cb48a4fbbd7cb36bad6a5fca4741306f7/jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500", size = 162759, upload-time = "2025-05-18T19:04:59.73Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/b0/279597e7a270e8d22623fea6c5d4eeac328e7d95c236ed51a2b884c54f70/jiter-0.10.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0588107ec8e11b6f5ef0e0d656fb2803ac6cf94a96b2b9fc675c0e3ab5e8644", size = 311617, upload-time = "2025-05-18T19:04:02.078Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/e3/0916334936f356d605f54cc164af4060e3e7094364add445a3bc79335d46/jiter-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cafc4628b616dc32530c20ee53d71589816cf385dd9449633e910d596b1f5c8a", size = 318947, upload-time = "2025-05-18T19:04:03.347Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/8e/fd94e8c02d0e94539b7d669a7ebbd2776e51f329bb2c84d4385e8063a2ad/jiter-0.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:520ef6d981172693786a49ff5b09eda72a42e539f14788124a07530f785c3ad6", size = 344618, upload-time = "2025-05-18T19:04:04.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/b0/f9f0a2ec42c6e9c2e61c327824687f1e2415b767e1089c1d9135f43816bd/jiter-0.10.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:554dedfd05937f8fc45d17ebdf298fe7e0c77458232bcb73d9fbbf4c6455f5b3", size = 368829, upload-time = "2025-05-18T19:04:06.912Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/57/5bbcd5331910595ad53b9fd0c610392ac68692176f05ae48d6ce5c852967/jiter-0.10.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bc299da7789deacf95f64052d97f75c16d4fc8c4c214a22bf8d859a4288a1c2", size = 491034, upload-time = "2025-05-18T19:04:08.222Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/be/c393df00e6e6e9e623a73551774449f2f23b6ec6a502a3297aeeece2c65a/jiter-0.10.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5161e201172de298a8a1baad95eb85db4fb90e902353b1f6a41d64ea64644e25", size = 388529, upload-time = "2025-05-18T19:04:09.566Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/3e/df2235c54d365434c7f150b986a6e35f41ebdc2f95acea3036d99613025d/jiter-0.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e2227db6ba93cb3e2bf67c87e594adde0609f146344e8207e8730364db27041", size = 350671, upload-time = "2025-05-18T19:04:10.98Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/77/71b0b24cbcc28f55ab4dbfe029f9a5b73aeadaba677843fc6dc9ed2b1d0a/jiter-0.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15acb267ea5e2c64515574b06a8bf393fbfee6a50eb1673614aa45f4613c0cca", size = 390864, upload-time = "2025-05-18T19:04:12.722Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/d3/ef774b6969b9b6178e1d1e7a89a3bd37d241f3d3ec5f8deb37bbd203714a/jiter-0.10.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:901b92f2e2947dc6dfcb52fd624453862e16665ea909a08398dde19c0731b7f4", size = 522989, upload-time = "2025-05-18T19:04:14.261Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/41/9becdb1d8dd5d854142f45a9d71949ed7e87a8e312b0bede2de849388cb9/jiter-0.10.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d0cb9a125d5a3ec971a094a845eadde2db0de85b33c9f13eb94a0c63d463879e", size = 513495, upload-time = "2025-05-18T19:04:15.603Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/46/caa2c1342655f57d8f0f2519774c6d67132205909c65e9aa8255e1d7b4f4/jiter-0.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:28ed2a4c05a1f32ef0e1d24c2611330219fed727dae01789f4a335617634b1ca", size = 318225, upload-time = "2025-05-18T19:04:20.583Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/84/c7d44c75767e18946219ba2d703a5a32ab37b0bc21886a97bc6062e4da42/jiter-0.10.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a4c418b1ec86a195f1ca69da8b23e8926c752b685af665ce30777233dfe070", size = 350235, upload-time = "2025-05-18T19:04:22.363Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/9b/1d646da42c3de6c2188fdaa15bce8ecb22b635904fc68be025e21249ba44/jiter-0.10.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:5e9251a5e83fab8d87799d3e1a46cb4b7f2919b895c6f4483629ed2446f66522", size = 310866, upload-time = "2025-05-18T19:04:24.891Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/0e/26538b158e8a7c7987e94e7aeb2999e2e82b1f9d2e1f6e9874ddf71ebda0/jiter-0.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:023aa0204126fe5b87ccbcd75c8a0d0261b9abdbbf46d55e7ae9f8e22424eeb8", size = 318772, upload-time = "2025-05-18T19:04:26.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/fb/d302893151caa1c2636d6574d213e4b34e31fd077af6050a9c5cbb42f6fb/jiter-0.10.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c189c4f1779c05f75fc17c0c1267594ed918996a231593a21a5ca5438445216", size = 344534, upload-time = "2025-05-18T19:04:27.495Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/d8/5780b64a149d74e347c5128d82176eb1e3241b1391ac07935693466d6219/jiter-0.10.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15720084d90d1098ca0229352607cd68256c76991f6b374af96f36920eae13c4", size = 369087, upload-time = "2025-05-18T19:04:28.896Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/5b/f235a1437445160e777544f3ade57544daf96ba7e96c1a5b24a6f7ac7004/jiter-0.10.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4f2fb68e5f1cfee30e2b2a09549a00683e0fde4c6a2ab88c94072fc33cb7426", size = 490694, upload-time = "2025-05-18T19:04:30.183Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/a9/9c3d4617caa2ff89cf61b41e83820c27ebb3f7b5fae8a72901e8cd6ff9be/jiter-0.10.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce541693355fc6da424c08b7edf39a2895f58d6ea17d92cc2b168d20907dee12", size = 388992, upload-time = "2025-05-18T19:04:32.028Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/b1/344fd14049ba5c94526540af7eb661871f9c54d5f5601ff41a959b9a0bbd/jiter-0.10.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31c50c40272e189d50006ad5c73883caabb73d4e9748a688b216e85a9a9ca3b9", size = 351723, upload-time = "2025-05-18T19:04:33.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/89/4c0e345041186f82a31aee7b9d4219a910df672b9fef26f129f0cda07a29/jiter-0.10.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fa3402a2ff9815960e0372a47b75c76979d74402448509ccd49a275fa983ef8a", size = 392215, upload-time = "2025-05-18T19:04:34.827Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/58/ee607863e18d3f895feb802154a2177d7e823a7103f000df182e0f718b38/jiter-0.10.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:1956f934dca32d7bb647ea21d06d93ca40868b505c228556d3373cbd255ce853", size = 522762, upload-time = "2025-05-18T19:04:36.19Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/d0/9123fb41825490d16929e73c212de9a42913d68324a8ce3c8476cae7ac9d/jiter-0.10.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:fcedb049bdfc555e261d6f65a6abe1d5ad68825b7202ccb9692636c70fcced86", size = 513427, upload-time = "2025-05-18T19:04:37.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/0c/5fe86614ea050c3ecd728ab4035534387cd41e7c1855ef6c031f1ca93e3f/jiter-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5ed975b83a2b8639356151cef5c0d597c68376fc4922b45d0eb384ac058cfa00", size = 318527, upload-time = "2025-05-18T19:04:40.612Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/4a/4175a563579e884192ba6e81725fc0448b042024419be8d83aa8a80a3f44/jiter-0.10.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa96f2abba33dc77f79b4cf791840230375f9534e5fac927ccceb58c5e604a5", size = 354213, upload-time = "2025-05-18T19:04:41.894Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "3.0.0"
|
||||
@@ -210,6 +321,25 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/cd/e2b5083df581fc1d08eb93feb6f8fbd3d56b113cef9b59d8e0fb7d4dd4f3/nodejs_wheel_binaries-22.16.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7f526ca6a132b0caf633566a2a78c6985fe92857e7bfdb37380f76205a10b808", size = 60763005, upload-time = "2025-05-22T07:27:41.39Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "1.93.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
{ name = "distro", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
{ name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
{ name = "jiter", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
{ name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
{ name = "sniffio", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
{ name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e4/d7/e91c6a9cf71726420cddf539852ee4c29176ebb716a702d9118d0409fd8e/openai-1.93.0.tar.gz", hash = "sha256:988f31ade95e1ff0585af11cc5a64510225e4f5cd392698c675d0a9265b8e337", size = 486573, upload-time = "2025-06-27T21:21:39.421Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/64/46/a10d9df4673df56f71201d129ba1cb19eaff3366d08c8664d61a7df52e65/openai-1.93.0-py3-none-any.whl", hash = "sha256:3d746fe5498f0dd72e0d9ab706f26c91c0f646bf7459e5629af8ba7c9dbdf090", size = 755038, upload-time = "2025-06-27T21:21:37.532Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "25.0"
|
||||
@@ -347,6 +477,24 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/c0/b0b508193b0e8a1654ec683ebab18d309861f8bd64e3a2f9648b80d392cb/ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492", size = 11602992, upload-time = "2025-06-05T21:00:06.249Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.67.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "types-protobuf"
|
||||
version = "6.30.2.20250516"
|
||||
|
||||
Reference in New Issue
Block a user