iscp._conn のソースコード

import asyncio
import logging
import uuid
from asyncio.exceptions import CancelledError
from collections import defaultdict
from contextlib import contextmanager, suppress
from dataclasses import dataclass
from typing import AsyncIterator, Awaitable, Callable, Dict, Generator, List, Optional
from uuid import UUID

from . import _message as message
from . import _model
from ._downstream import Downstream, DownstreamClosedEventHandler
from ._encoding import Encoding, Protobuf
from ._exceptions import ISCPException, ISCPFailedMessageError, ISCPTransportClosedError
from ._flush_policy import FlushPolicy, IntervalOrBufferSize
from ._message import DownstreamCall, DownstreamChunk, DownstreamMetadata
from ._sequence import Sequence
from ._ticker import Ticker
from ._transport import (
    Connector,
    EncodingName,
    NegotiationParams,
    Reader,
    Transport,
    Unreliable,
    Writer,
)
from ._upstream import (
    AfterReceiveAckCallback,
    BeforeSendDataPointsCallback,
    Upstream,
    UpstreamClosedEventHandler,
)

__all__ = ["TokenSource", "Conn", "DisconnectedEventHandler", "DisconnectedEvent"]

logger = logging.getLogger(__name__)

DEFAULT_PING_INTERVAL = 10.0
DEFAULT_PING_TIMEOUT = 1.0
DEFAULT_FLUSH_POLICY = IntervalOrBufferSize()

TokenSource = Callable[[], str]


@dataclass
class DisconnectedEvent:
    """
    切断時のイベントです。

    Attributes:
        error(Optional[iscp.ISCPException]): 内部エラー発生した場合の例外
    """

    error: Optional[ISCPException] = None


DisconnectedEventHandler = Callable[[DisconnectedEvent], None]


[ドキュメント]class Conn(object): """ iSCPのコネクションを表すクラスです。 .. attention:: このクラスのオブジェクトは、必ず Conn の `connect()` を使用して生成してください。 .. code-block:: python async with await Conn.connect(...) as conn: pass このコードは以下のコードと等価です。 .. code-block:: python conn = await Conn.connect(...) try: pass finally: await conn.close() """
[ドキュメント] @classmethod async def connect( cls, address: str, connector: Connector, *, encoding: EncodingName | str = EncodingName.PROTOBUF, node_id: Optional[str] = None, ping_interval: float = DEFAULT_PING_INTERVAL, ping_timeout: float = DEFAULT_PING_TIMEOUT, project_uuid: Optional[UUID | str] = None, token_source: Optional[TokenSource] = None, disconnected_event_handler: Optional[DisconnectedEventHandler] = None, ): """ iSCPブローカーに接続します。 Args: address(str): 接続先のアドレス。`127.0.0.1:8080` 形式。 connector(iscp.Connector): トランスポートのコネクターです。 encoding(iscp.EncodingName|str): エンコーディング名 node_id(Optional[str]): ノードID ping_interval(float): Ping間隔(秒) ping_timeout(float): Pingタイムアウト(秒)。タイムアウトが発生した場合トランスポートを切断します。 project_uuid(Optional[UUID | str]): プロジェクトUUID token_source(Optional[iscp.TokenSource]): トークンソース。iSCPコネクションを開くたびに、このメソッドをコールしトークンを取得します。 disconnected_event_handler(Optional[iscp.DisconnectedEventHandler]): 切断イベントのハンドラ Returns: iscp.Conn: iSCPコネクション """ # TODO: JSONサポート if isinstance(encoding, str): encoding = EncodingName.parse(encoding) tr = await connector.connect(address, NegotiationParams(enc=encoding)) if isinstance(project_uuid, str): project_uuid = UUID(project_uuid) res = cls( transport=tr, node_id=node_id, ping_interval=ping_interval, ping_timeout=ping_timeout, project_uuid=project_uuid, token_source=token_source, disconnected_event_handler=disconnected_event_handler, ) return await res._open()
def __init__( self, transport: Transport, *, node_id: Optional[str] = None, protocol_version: str = "2.0.0", ping_interval: float = DEFAULT_PING_INTERVAL, ping_timeout: float = DEFAULT_PING_TIMEOUT, project_uuid: Optional[UUID] = None, token_source: Optional[TokenSource] = None, disconnected_event_handler: Optional[DisconnectedEventHandler] = None, ): self._transport = transport self._unreliable_writer: Optional[Writer] = None self._unreliable_reader: Optional[Reader] = None if isinstance(self._transport, Unreliable): wr, rd = self._transport.get_unreliable() self._unreliable_writer: Optional[Writer] = wr self._unreliable_reader: Optional[Reader] = rd self._protocol_version = protocol_version self._node_id = node_id self._ping_timeout = ping_timeout self._ping_interval = ping_interval self._req_sequence = Sequence.for_request_id() self._stream_sequence = Sequence.for_stream_id() self._project_uuid = project_uuid self._token_source = token_source # Event self._called_close_event = asyncio.Event() self._closed = asyncio.Event() # Read queue self._downstream_call_queue: asyncio.Queue[Optional[DownstreamCall]] = asyncio.Queue(256) self._downstream_reply_queue: asyncio.Queue[Optional[DownstreamCall]] = asyncio.Queue(256) # Request callback queues self._request_callback_queues: Dict[int, asyncio.Queue[message.RequestMessage]] = {} self._upstream_call_callback_queues: Dict[str, asyncio.Queue[message.UpstreamCallAck]] = {} self._downstream_reply_callback_queues: Dict[str, asyncio.Queue[_model.DownstreamReplyCall]] = {} # StreamHolder self._upstreams: Dict[int, Upstream] = {} self._downstreams: Dict[int, Downstream] = {} self._metadata_downstreams: Dict[str, Dict[int, Downstream]] = defaultdict(lambda: {}) # source_node_id and downstreams # All tasks # cancel everything on close self._all_tasks: List[asyncio.Task] = [] self._encoding: Encoding = Protobuf() self._disconnected_handler = disconnected_event_handler self._internal_error: Optional[ISCPException] = None async def __aenter__(self): return self @property def node_id(self): """ ノードIDを取得します。 Returns: Optional[str]: ノードID """ return self._node_id @property def ping_interval(self): """ Ping間隔(秒)を取得します。 Returns: float: Ping間隔(秒) """ return self._ping_interval @property def ping_timeout(self): """ Pingタイムアウト(秒)を取得します。 Returns: float: Pingタイムアウト(秒) """ return self._ping_timeout @property def project_uuid(self): """ プロジェクトUUIDを取得します。 Returns: Optional[UUID]: プロジェクトUUID """ return self._project_uuid @property def address(self): """ アドレスを取得します。 Returns: str: アドレス """ return self._transport.address() @property def encoding(self): """ エンコーディング名を取得します。 Returns: iscp.EncodingName: エンコーディング名 """ return self._encoding.name() @property def transport(self): """ トランスポート名を取得します。 Returns: iscp.TransportName: トランスポート名 """ return self._transport.name() async def _open(self): negotiation_params = self._transport.negotiation_params() if negotiation_params.enc == EncodingName.PROTOBUF: self._encoding: Encoding = Protobuf() else: self._encoding: Encoding = Protobuf() resp = await self._wait_for_connected() if resp.result_code != message.ResultCode.SUCCEEDED: raise ISCPFailedMessageError(received_message=resp, message=f"code=[{resp.result_code}] msg=[{resp.result_string}]") asyncio.create_task(self._wait_called_close_then_close()) asyncio.create_task(self._wait_closed_for_event_handler()) self._start() return self def _start(self): # transport read loop self._all_tasks.append(asyncio.create_task(self._read_loop())) if self._unreliable_reader: self._all_tasks.append(asyncio.create_task(self._unreliable_read_loop(self._unreliable_reader))) # keep alive loop self._all_tasks.append(asyncio.create_task(self._keep_alive_loop())) async def _stop(self): for t in self._all_tasks: t.cancel() with suppress(CancelledError): await asyncio.wait(self._all_tasks) async def _unreliable_read_loop(self, reader: Reader): while not self._called_close_event.is_set(): msg = await self._encoding.decodeFrom(reader) # dispatch if isinstance(msg, DownstreamChunk): await self._downstreams[msg.stream_id_alias]._handle_chunk(msg) async def _read_loop(self): while not self._called_close_event.is_set(): await asyncio.sleep(0) msg = await self._read() # dispatch if isinstance(msg, message.Ping): await self._write( message.Pong( request_id=msg.request_id, extension_fields=message.PongExtensionFields(), ) ) elif isinstance(msg, DownstreamChunk): await self._downstreams[msg.stream_id_alias]._handle_chunk(msg) elif isinstance(msg, DownstreamMetadata): if msg.stream_id_alias in self._metadata_downstreams[msg.source_node_id]: await self._metadata_downstreams[msg.source_node_id][msg.stream_id_alias]._handle_metadata(msg) elif isinstance(msg, message.DownstreamChunkAckComplete): ( await self._downstreams[msg.stream_id_alias]._handle_chunk_ack_complete(msg) if msg.stream_id_alias in self._downstreams else None ) elif isinstance(msg, message.UpstreamChunkAck): await self._upstreams[msg.stream_id_alias]._handle_ack(msg) if msg.stream_id_alias in self._upstreams else None elif isinstance(msg, DownstreamCall): if msg.request_call_id == "": await self._downstream_call_queue.put(msg) elif msg.request_call_id in self._downstream_reply_callback_queues: await self._downstream_reply_queue.put(msg) await self._downstream_reply_callback_queues[msg.request_call_id].put( _model.DownstreamReplyCall( call_id=msg.call_id, name=msg.name, payload=msg.payload, request_call_id=msg.request_call_id, source_node_id=msg.source_node_id, type=msg.type, ) ) elif isinstance(msg, message.UpstreamCallAck): if msg.call_id in self._upstream_call_callback_queues: await self._upstream_call_callback_queues[msg.call_id].put(msg) elif isinstance(msg, message.RequestMessage): reply_queue = self._request_callback_queues[msg.request_id] if reply_queue is None: continue await reply_queue.put(msg) del self._request_callback_queues[msg.request_id] else: logger.error(f"received unrecognized message[{msg}]") await self.close() return async def _wait_closed_for_event_handler(self): await self._closed.wait() if self._disconnected_handler: self._disconnected_handler(DisconnectedEvent(error=self._internal_error)) async def _wait_called_close_then_close(self): await self._called_close_event.wait() await self._close() async def _send_request(self, msg: message.RequestMessage) -> message.RequestMessage: """リクエストメッセージを送信します。 Args: msg (iscp.RequestMessage): リクエスト・メッセージ。msg内のrequest_id属性は必ず上書きされます。 Returns: iscp.RequestMessage: 要求に対する応答メッセージ """ if self._called_close_event.is_set(): raise ISCPTransportClosedError() msg.request_id = self._req_sequence() reply_queue = asyncio.Queue(maxsize=1) self._request_callback_queues[msg.request_id] = reply_queue await self._write(msg) res = await reply_queue.get() reply_queue.task_done() return res async def _send_upstream_chunk(self, msg: message.UpstreamChunk) -> None: await self._write(msg) async def _send_upstream_chunk_unreliable(self, msg: message.UpstreamChunk) -> None: if self._unreliable_writer: await self._encoding.encodeTo(self._unreliable_writer, msg) return # fallback await self._send_upstream_chunk(msg) async def _keep_alive_loop(self): try: async with Ticker.of(self._ping_interval) as ticker: async for _ in ticker(): pong = await asyncio.wait_for( self._send_request( message.Ping(request_id=0, extension_fields=message.PingExtensionFields()), ), timeout=self._ping_timeout, ) logger.debug(f"received pong timeout[{pong.request_id}]") if self._called_close_event.is_set(): return except asyncio.TimeoutError: logger.error(f"ping pong timeout[{self._ping_timeout}]") self._internal_error = ISCPException(message="ping pong timeout") self._called_close_event.set() return except Exception as e: logger.error(e) async def __aexit__(self, exc_type, exc, tb): await self.close() async def _wait_for_connected(self) -> message.ConnectResponse: req = message.ConnectRequest( request_id=self._req_sequence(), node_id=self._node_id if self._node_id else "", protocol_version=self._protocol_version, ping_interval=self._ping_interval, ping_timeout=self._ping_timeout, extension_fields=message.ConnectRequestExtensionFields( access_token=self._token_source() if self._token_source else "", intdash=message.IntdashExtensionFields( project_uuid=self._project_uuid or UUID("00000000-0000-0000-0000-000000000000") ), ), ) req.extension_fields.access_token = self._token_source() if self._token_source else "" req.extension_fields.intdash.project_uuid = ( self._project_uuid if self._project_uuid else UUID("00000000-0000-0000-0000-000000000000") ) await self._write(req) resp = await self._read() if isinstance(resp, message.ConnectResponse): return resp else: raise ISCPFailedMessageError(received_message=resp)
[ドキュメント] async def open_upstream( self, session_id: str, *, expiry_interval: float = 1, data_ids: Optional[List[message.DataID]] = None, qos: message.QoS | str = message.QoS.UNRELIABLE, ack_interval: float = 1, persist: bool = False, send_data_points_callback: Optional[BeforeSendDataPointsCallback] = None, receive_ack_callback: Optional[AfterReceiveAckCallback] = None, flush_policy: FlushPolicy = DEFAULT_FLUSH_POLICY, close_timeout: float = 1, closed_event_handler: Optional[UpstreamClosedEventHandler] = None, close_session: bool = False, ) -> Upstream: """ アップストリームを開きます。 Args: session_id (str): セッションID expiry_interval (float): 有効期限(秒) data_ids (Optional[List[iscp.DataID]]): データIDのリスト qos (iscp.QoS|str): QoS ack_interval (float): Ack返却間隔(秒) persist (bool): 永続化するかどうか send_data_points_callback (Optional[iscp.BeforeSendDataPointsCallback]): データポイント送信時のフック receive_ack_callback (Optional[iscp.AfterReceiveAckCallback]): Ack受信時のフック flush_policy (iscp.FlushPolicy): データポイントのフラッシュポリシー close_timeout (float): クローズのタイムアウト(秒) closed_event_handler (Optional[iscp.UpstreamClosedEventHandler]): アップストリームクローズイベントのハンドラ close_session(bool): アップストリームクローズ時にセッションをクローズするかどうか """ if data_ids is None: data_ids = [] if isinstance(qos, str): qos = message.QoS.parse(qos) resp = await self._send_request( message.UpstreamOpenRequest( request_id=0, session_id=session_id or "", expiry_interval=expiry_interval, data_ids=data_ids, qos=qos, ack_interval=ack_interval, extension_fields=message.UpstreamOpenRequestExtensionFields(persist=persist), ) ) if isinstance(resp, message.UpstreamOpenResponse): if resp.result_code is not message.ResultCode.SUCCEEDED: raise ISCPFailedMessageError( received_message=resp, message=f"code=[{resp.result_code}] msg=[{resp.result_string}]" ) up = Upstream( id=resp.assigned_stream_id, id_alias=resp.assigned_stream_id_alias, data_id_aliases=resp.data_id_aliases, server_time=resp.server_time, requester=self._send_request, sender=( self._send_upstream_chunk_unreliable if qos is message.QoS.UNRELIABLE and self._unreliable_writer else self._send_upstream_chunk ), receive_ack_callback=receive_ack_callback, send_data_point_callback=send_data_points_callback, close_timeout=close_timeout, session_id=session_id or "", ack_interval=ack_interval, expiry_interval=expiry_interval, flush_policy=flush_policy, qos=qos, persist=persist, closed_event_handler=closed_event_handler, close_session=close_session, ) await up._start() self._upstreams[up._id_alias] = up self._all_tasks.append(asyncio.create_task(self._remove_upstream_when_closed(up))) return up raise ISCPFailedMessageError(received_message=resp)
async def _remove_upstream_when_closed(self, up: Upstream): await up._closed.wait() del self._upstreams[up._id_alias]
[ドキュメント] async def open_downstream( self, filters: List[message.DownstreamFilter], *, data_ids: Optional[List[message.DataID]] = None, qos: message.QoS | str = message.QoS.UNRELIABLE, expiry_interval: float = 1, ack_interval: float = 1, closed_event_handler: Optional[DownstreamClosedEventHandler] = None, omit_empty_chunk=False, ) -> Downstream: """ ダウンストリームを開きます。 Args: filters (List[iscp.DownstreamFilter]): ダウンストリームフィルタのリスト data_ids (Optional[List[iscp.DataID]]): データIDのリスト qos (iscp.QoS|str): QoS expiry_interval (float): 有効期限(秒) ack_interval (float): Ack返却間隔(秒) closed_event_handler (Optional[iscp.DownstreamClosedEventHandler]): ダウンストリームクローズイベントのハンドラ omit_empty_chunk (bool): 空チャンク省略フラグ。Trueにすると空のDataPointGroupのチャンクはサーバーから配信されません。 Returns: iscp.Downstream: ダウンストリーム """ stream_id_alias = self._stream_sequence() data_id_alias_sequence = Sequence.for_data_id_alias() if isinstance(qos, str): qos = message.QoS.parse(qos) req = message.DownstreamOpenRequest( request_id=0, desired_stream_id_alias=stream_id_alias, downstream_filters=filters, expiry_interval=expiry_interval, data_id_aliases={} if data_ids is None else {data_id_alias_sequence(): v for v in data_ids}, qos=qos, extension_fields=message.DownstreamOpenRequestExtensionFields(), omit_empty_chunk=omit_empty_chunk, ) resp = await self._send_request(req) if isinstance(resp, message.DownstreamOpenResponse): if resp.result_code is not message.ResultCode.SUCCEEDED: raise ISCPFailedMessageError( received_message=resp, message=f"code=[{resp.result_code}] msg=[{resp.result_string}]" ) down = Downstream( id_alias=stream_id_alias, id=resp.assigned_stream_id, server_time=resp.server_time, filters=[], requester=self._send_request, expiry_interval=expiry_interval, stream_message_sender=self._write, request_message_sender=self._write, qos=qos, data_id_alias_sequence=data_id_alias_sequence, data_id_aliases=req.data_id_aliases, closed_event_handler=closed_event_handler, ack_interval=ack_interval, ) self._downstreams[stream_id_alias] = down self._all_tasks.append(asyncio.create_task(self._remove_downstream_when_closed(down))) for f in filters: self._metadata_downstreams[f.source_node_id][stream_id_alias] = down await down._start() return down raise ISCPFailedMessageError(received_message=resp)
async def _remove_downstream_when_closed(self, down: Downstream): await down._is_closed.wait() del self._downstreams[down._stream_id_alias] async def _write(self, msg: message.Message): await self._encoding.encodeTo(self._transport, msg) async def _read(self) -> message.Message: return await self._encoding.decodeFrom(self._transport)
[ドキュメント] async def close(self): """ コネクションを閉じます。 """ self._called_close_event.set() await self._closed.wait()
async def _close(self): if self._closed.is_set(): return try: await self._write( message.Disconnect( result_code=message.ResultCode.NORMAL_CLOSURE, result_string="NORMAL_CLOSURE", ) ) await self._stop() await self._transport.close() except Exception as e: logger.error(e) finally: self._closed.set()
[ドキュメント] async def send_reply_call(self, reply_call: _model.UpstreamReplyCall) -> str: """ E2Eコールのリプライを送信します。 Args: reply_call(iscp.UpstreamReplyCall): アップストリームリプライコール Returns: str: コールID """ call_id = _random_string() await self._send_call( message.UpstreamCall( call_id=call_id, destination_node_id=reply_call.destination_node_id, name=reply_call.name, type=reply_call.type, payload=reply_call.payload, request_call_id=reply_call.request_call_id, extension_fields=message.UpstreamCallExtensionFields(), ) ) return call_id
[ドキュメント] async def send_call(self, upstream_call: _model.UpstreamCall) -> str: """ E2Eコールを送信します。 Args: upstream_call(iscp.UpstreamCall): アップストリームコール Returns: str: コールID """ call_id = _random_string() await self._send_call( message.UpstreamCall( call_id=call_id, request_call_id="", destination_node_id=upstream_call.destination_node_id, name=upstream_call.name, type=upstream_call.type, payload=upstream_call.payload, extension_fields=message.UpstreamCallExtensionFields(), ) ) return call_id
async def _send_call(self, upstream_call: message.UpstreamCall): self._upstream_call_callback_queues[upstream_call.call_id] = asyncio.Queue(1) await self._write(upstream_call) resp = await self._upstream_call_callback_queues[upstream_call.call_id].get() if resp.result_code is not message.ResultCode.SUCCEEDED: raise ISCPFailedMessageError(received_message=resp, message=f"code=[{resp.result_code}] msg=[{resp.result_string}]")
[ドキュメント] async def calls(self, *, timeout: Optional[float] = None) -> AsyncIterator[_model.DownstreamCall]: """ E2Eコールを受信します。 Args: timeout (Optional[float]): 読み込みタイムアウト(秒)。指定しない場合はタイムアウトしません。 Yields: iscp.DownstreamCall: ダウンストリームコール Examples: >>> async for call in conn.calls(): pass """ while True: res = await asyncio.wait_for(self._downstream_call_queue.get(), timeout=timeout) self._downstream_call_queue.task_done() if not res: return yield _model.DownstreamCall( call_id=res.call_id, source_node_id=res.source_node_id, name=res.name, type=res.type, payload=res.payload, )
[ドキュメント] async def reply_calls(self, *, timeout: Optional[float] = None) -> AsyncIterator[_model.DownstreamReplyCall]: """ E2Eリプライコールを受信します。 Args: timeout (Optional[float]): 読み込みタイムアウト(秒)。指定しない場合はタイムアウトしません。 Yields: iscp.DownstreamReplyCall: ダウンストリームリプライコール Examples: >>> async for call in conn.reply_calls(): pass """ while True: res = await asyncio.wait_for(self._downstream_reply_queue.get(), timeout=timeout) self._downstream_reply_queue.task_done() if not res: return yield _model.DownstreamReplyCall( call_id=res.call_id, request_call_id=res.request_call_id, source_node_id=res.source_node_id, name=res.name, type=res.type, payload=res.payload, )
@contextmanager def _subscribe_reply_call( self, ) -> Generator[tuple[str, Awaitable[_model.DownstreamReplyCall]], None, None]: """ リプライコールをサブスクライブします。 Args: request_call_id (str): サブスクライブするリクエストコールID Yields: Awaitable[iscp.DownstreamCall]: ダウンストリームコールのAwaitableオブジェクト Examples: >>> with conn.subscribe_reply_call() as (call_id, aw): conn.send_call(...) # ここでcall_idを利用して下さい。 reply_call = await aw """ call_id = _random_string() self._downstream_reply_callback_queues[call_id] = asyncio.Queue(1) try: yield (call_id, asyncio.create_task(self._downstream_reply_callback_queues[call_id].get())) finally: self._unsubscribe_reply_call(call_id) def _unsubscribe_reply_call( self, request_call_id: str, ): del self._downstream_reply_callback_queues[request_call_id]
[ドキュメント] async def send_call_and_wait_reply_call(self, upstream_call: _model.UpstreamCall) -> _model.DownstreamReplyCall: """ E2Eコールを送信し、リプライコールを受信するまで待ちます。 Args: upstream_call(iscp.UpstreamCall): アップストリームコール Returns: iscp.DownstreamReplyCall: ダウンストリームリプライコール """ # start with self._subscribe_reply_call() as (call_id, fut): await self._send_call( message.UpstreamCall( call_id=call_id, request_call_id="", destination_node_id=upstream_call.destination_node_id, name=upstream_call.name, type=upstream_call.type, payload=upstream_call.payload, extension_fields=message.UpstreamCallExtensionFields(), ) ) got = await fut return _model.DownstreamReplyCall( call_id=got.call_id, name=got.name, type=got.type, payload=got.payload, request_call_id=got.request_call_id, source_node_id=got.source_node_id, )
[ドキュメント] async def send_base_time( self, base_time: message.BaseTime, *, persist: bool = False, ): """ 基準時刻を送信します。 Args: base_time(iscp.BaseTime): 基準時刻 persist (bool): 永続化するかどうか """ return await self._send_metadata(metadata=base_time, persist=persist)
async def _send_metadata( self, metadata: message.SendableMetadata, *, persist: bool = False, ): """ メタデータを送信します。 Args: base_time(iscp.SendableMetadata): メタデータ persist (bool): 永続化するかどうか """ # reply msg = await self._send_request( message.UpstreamMetadata( request_id=0, metadata=metadata, extension_fields=message.UpstreamMetadataExtensionFields(persist=persist), ) ) if isinstance(msg, message.UpstreamMetadataAck): if msg.result_code is message.ResultCode.SUCCEEDED: return raise ISCPFailedMessageError(received_message=msg, message=f"code=[{msg.result_code}] msg=[{msg.result_string}]") raise ISCPFailedMessageError(received_message=msg)
def _random_string() -> str: return uuid.uuid4().__str__()