Initial dependency build
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
def hello() -> str:
|
||||
return "Hello from dynalab-protocol!"
|
||||
# Copyright (C) 2026 Hector van der Aa <hector@h3cx.dev>
|
||||
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
71
src/dynalab_protocol/models.py
Normal file
71
src/dynalab_protocol/models.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# Copyright (C) 2026 Hector van der Aa <hector@h3cx.dev>
|
||||
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from typing import Annotated, Literal
|
||||
from uuid import UUID
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
PROTOCOL_VERSION = 1
|
||||
|
||||
|
||||
class VersionDescriptor(BaseModel):
|
||||
type: Literal["alpha", "beta", "release"]
|
||||
major: int
|
||||
minor: int
|
||||
patch: int
|
||||
|
||||
|
||||
class SignalDescriptor(BaseModel):
|
||||
id: UUID
|
||||
name: str
|
||||
type: Literal["number", "binary"]
|
||||
min_value: float | None = None
|
||||
max_value: float | None = None
|
||||
|
||||
|
||||
class DynaLabHello(BaseModel):
|
||||
type: Literal["dynalab_hello"] = "dynalab_hello"
|
||||
instance_id: UUID
|
||||
version: VersionDescriptor
|
||||
protocol_version: int = PROTOCOL_VERSION
|
||||
|
||||
heartbeat_interval_ms: int = 1000
|
||||
heartbeat_timeout_ms: int = 5000
|
||||
|
||||
protocol_version: int = PROTOCOL_VERSION
|
||||
|
||||
|
||||
class ConnectorHello(BaseModel):
|
||||
type: Literal["connector_hello"] = "connector_hello"
|
||||
connector_uuid: UUID
|
||||
protocol_version: int = PROTOCOL_VERSION
|
||||
|
||||
connector_name: str
|
||||
connector_version: str
|
||||
|
||||
signals: list[SignalDescriptor]
|
||||
|
||||
|
||||
class HandshakeAccepted(BaseModel):
|
||||
type: Literal["handshake_accepted"] = "handshake_accepted"
|
||||
|
||||
accepted_signals: list[UUID]
|
||||
|
||||
|
||||
class HandshakeRejected(BaseModel):
|
||||
type: Literal["handshake_rejected"] = "handshake_rejected"
|
||||
reason: str
|
||||
|
||||
|
||||
class Heartbeat(BaseModel):
|
||||
type: Literal["heartbeat"] = "heartbeat"
|
||||
sequence: int
|
||||
send_timestamp: int
|
||||
return_timestamp: int | None = None
|
||||
|
||||
|
||||
ProtocolMessage = Annotated[
|
||||
DynaLabHello | ConnectorHello | HandshakeAccepted | HandshakeRejected | Heartbeat,
|
||||
Field(discriminator="type"),
|
||||
]
|
||||
33
src/dynalab_protocol/wire.py
Normal file
33
src/dynalab_protocol/wire.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# Copyright (C) 2026 Hector van der Aa <hector@h3cx.dev>
|
||||
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
from dynalab_protocol.models import ProtocolMessage
|
||||
|
||||
|
||||
_MESSAGE_ADAPTER = TypeAdapter(ProtocolMessage)
|
||||
|
||||
|
||||
async def write_message(writer: StreamWriter, message: ProtocolMessage) -> None:
|
||||
writer.write(message.model_dump_json().encode("utf-8") + b"\n")
|
||||
await writer.drain()
|
||||
|
||||
|
||||
async def read_message(
|
||||
reader: StreamReader, *, max_frame_bytes: int = 65_536
|
||||
) -> ProtocolMessage:
|
||||
line = await reader.readline()
|
||||
|
||||
if not line:
|
||||
raise ConnectionError("Peer disconnected")
|
||||
|
||||
if len(line) > max_frame_bytes:
|
||||
raise ValueError("Incoming protocol frame exceeds maximum size")
|
||||
|
||||
if not line.endswith(b"\n"):
|
||||
raise ValueError("Protocol frame is missing newline delimiter")
|
||||
|
||||
return _MESSAGE_ADAPTER.validate_json(line)
|
||||
Reference in New Issue
Block a user