diff --git a/Makefile b/Makefile index adb8fbd..aebcf98 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,9 @@ refresh: clean develop test lint run: python -m ${MODULE} +build: + python setup.py build + build_dist: python setup.py sdist bdist_wheel diff --git a/karuha/bot.py b/karuha/bot.py index e63bf9a..ef57198 100644 --- a/karuha/bot.py +++ b/karuha/bot.py @@ -4,9 +4,9 @@ import sys from asyncio.queues import Queue, QueueEmpty from collections import defaultdict -from contextlib import asynccontextmanager +from contextlib import asynccontextmanager, contextmanager from enum import IntEnum -from typing import (Any, AsyncGenerator, Callable, Coroutine, Dict, List, Literal, Optional, +from typing import (Any, AsyncGenerator, Callable, Coroutine, Dict, Generator, List, Literal, Optional, Union, overload) from typing_extensions import Self, deprecated from weakref import WeakSet, ref @@ -250,9 +250,13 @@ async def send_message(self, wait_tid: Optional[str] = None, /, **kwds: Message) :rtype: Optional[Message] """ client_msg = pb.ClientMsg(**kwds) # type: ignore - await self.queue.put(client_msg) - if wait_tid is not None: - return await self._wait_reply(wait_tid) + if wait_tid is None: + await self.queue.put(client_msg) + return + + with self._wait_reply(wait_tid) as future: + await self.queue.put(client_msg) + return await future async def async_run(self) -> None: server = self.server @@ -351,12 +355,13 @@ def _get_tid(self) -> str: self._tid_counter += 1 return tid - async def _wait_reply(self, tid: Optional[str] = None) -> Any: + @contextmanager + def _wait_reply(self, tid: Optional[str] = None) -> Generator[asyncio.Future, None, None]: tid = tid or self._get_tid() future = asyncio.get_running_loop().create_future() self._wait_list[tid] = future try: - return await future + yield future finally: assert self._wait_list.pop(tid, None) is future diff --git a/karuha/event/base.py b/karuha/event/base.py index 2d2e6ab..1fb53cf 100644 --- a/karuha/event/base.py +++ b/karuha/event/base.py @@ -10,7 +10,7 @@ class Event(object): """base class for all events""" - + __slots__ = [] __handlers__: ClassVar[List[Callable[[Self], Coroutine]]] = [] diff --git a/karuha/event/bot.py b/karuha/event/bot.py index 86f7e1a..d0ff039 100644 --- a/karuha/event/bot.py +++ b/karuha/event/bot.py @@ -22,7 +22,7 @@ def call_handler(self, handler: Callable[[Self], Coroutine]) -> Awaitable: # Server Event Part # ========================= - + class ServerEvent(BotEvent): """base class for all events from server""" @@ -31,11 +31,11 @@ class ServerEvent(BotEvent): def __init__(self, bot: "bot.Bot", message: Message) -> None: super().__init__(bot) self.server_message = message - + def __init_subclass__(cls, on_field: str, **kwds: Any) -> None: super().__init_subclass__(**kwds) bot.Bot.server_event_map[on_field].append(cls.new) - + class DataEvent(ServerEvent, on_field="data"): """ @@ -58,12 +58,17 @@ class DataEvent(ServerEvent, on_field="data"): } ``` - Data messages have a `seq` field which holds a sequential numeric ID generated by the server. The IDs are guaranteed to be unique within a topic. IDs start from 1 and sequentially increment with every successful [`{pub}`](#pub) message received by the topic. - - See [Format of Content](https://github.com/tinode/chat/blob/master/docs/API.md#format-of-content) for `content` format considerations. + Data messages have a `seq` field which holds a sequential numeric ID generated by the server. + The IDs are guaranteed to be unique within a topic. IDs start from 1 and sequentially increment + with every successful [{pub}](https://github.com/tinode/chat/blob/master/docs/API.md#pub) message received by the topic. + + See [Format of Content](https://github.com/tinode/chat/blob/master/docs/API.md#format-of-content) + for `content` format considerations. - See [{pub}](https://github.com/tinode/chat/blob/master/docs/API.md#pub) message for the possible values of the `head` field. + See [{pub}](https://github.com/tinode/chat/blob/master/docs/API.md#pub) message + for the possible values of the `head` field. """ + __slots__ = [] server_message: pb.ServerData @@ -87,6 +92,7 @@ class CtrlEvent(ServerEvent, on_field="ctrl"): } ``` """ + __slots__ = [] server_message: pb.ServerCtrl @@ -94,7 +100,8 @@ class CtrlEvent(ServerEvent, on_field="ctrl"): class MetaEvent(ServerEvent, on_field="meta"): """ - Information about topic metadata or subscribers, sent in response to `{get}`, `{set}` or `{sub}` message to the originating session. + Information about topic metadata or subscribers, sent in response to `{get}`, + `{set}` or `{sub}` message to the originating session. ```js meta: { @@ -198,6 +205,7 @@ class MetaEvent(ServerEvent, on_field="meta"): } ``` """ + __slots__ = [] server_message: pb.ServerMeta @@ -205,7 +213,8 @@ class MetaEvent(ServerEvent, on_field="meta"): class PresEvent(ServerEvent, on_field="pres"): """ - Tinode uses `{pres}` message to inform clients of important events. A separate [document](https://docs.google.com/spreadsheets/d/e/2PACX-1vStUDHb7DPrD8tF5eANLu4YIjRkqta8KOhLvcj2precsjqR40eDHvJnnuuS3bw-NcWsP1QKc7GSTYuX/pubhtml?gid=1959642482&single=true) explains all possible use cases. + Tinode uses `{pres}` message to inform clients of important events. + A separate [document](https://docs.google.com/spreadsheets/d/e/2PACX-1vStUDHb7DPrD8tF5eANLu4YIjRkqta8KOhLvcj2precsjqR40eDHvJnnuuS3bw-NcWsP1QKc7GSTYuX/pubhtml?gid=1959642482&single=true) explains all possible use cases. ```js pres: { @@ -242,10 +251,12 @@ class PresEvent(ServerEvent, on_field="pres"): * del: messages were deleted - The `{pres}` messages are purely transient: they are not stored and no attempt is made to deliver them later if the destination is temporarily unavailable. + The `{pres}` messages are purely transient: they are not stored and no attempt is made + to deliver them later if the destination is temporarily unavailable. Timestamp is not present in `{pres}` messages. - """ + """ # noqa + __slots__ = [] server_message: pb.ServerPres @@ -253,7 +264,10 @@ class PresEvent(ServerEvent, on_field="pres"): class InfoEvent(ServerEvent, on_field="info"): """ - Forwarded client-generated notification `{note}`. Server guarantees that the message complies with this specification and that content of `topic` and `from` fields is correct. The other content is copied from the `{note}` message verbatim and may potentially be incorrect or misleading if the originator so desires. + Forwarded client-generated notification `{note}`. Server guarantees that the message complies + with this specification and that content of `topic` and `from` fields is correct. + The other content is copied from the `{note}` message verbatim and + may potentially be incorrect or misleading if the originator so desires. ```js info: { @@ -268,6 +282,7 @@ class InfoEvent(ServerEvent, on_field="info"): } ``` """ + __slots__ = [] server_message: pb.ServerInfo @@ -297,21 +312,33 @@ class PublishEvent(ClientEvent): } ``` - Topic subscribers receive the `content` in the [`{data}`](#data) message. By default the originating session gets a copy of `{data}` like any other session currently attached to the topic. If for some reason the originating session does not want to receive the copy of the data it just published, set `noecho` to `true`. + Topic subscribers receive the `content` in the [`{data}`](#data) message. + By default the originating session gets a copy of `{data}` + like any other session currently attached to the topic. + If for some reason the originating session does not want to receive + the copy of the data it just published, set `noecho` to `true`. - See [Format of Content](https://github.com/tinode/chat/blob/master/docs/API.md#format-of-content) for `content` format considerations. + See [Format of Content](https://github.com/tinode/chat/blob/master/docs/API.md#format-of-content) + for `content` format considerations. The following values are currently defined for the `head` field: * `attachments`: an array of paths indicating media attached to this message `["/v0/file/s/sJOD_tZDPz0.jpg"]`. * `auto`: `true` when the message was sent automatically, i.e. by a chatbot or an auto-responder. - * `forwarded`: an indicator that the message is a forwarded message, a unique ID of the original message, `"grp1XUtEhjv6HND:123"`. + * `forwarded`: an indicator that the message is a forwarded message, + a unique ID of the original message, `"grp1XUtEhjv6HND:123"`. * `mentions`: an array of user IDs mentioned (`@alice`) in the message: `["usr1XUtEhjv6HND", "usr2il9suCbuko"]`. - * `mime`: MIME-type of the message content, `"text/x-drafty"`; a `null` or a missing value is interpreted as `"text/plain"`. - * `replace`: an indicator that the message is a correction/replacement for another message, a topic-unique ID of the message being updated/replaced, `":123"` - * `reply`: an indicator that the message is a reply to another message, a unique ID of the original message, `"grp1XUtEhjv6HND:123"`. - * `sender`: a user ID of the sender added by the server when the message is sent on behalf of another user, `"usr1XUtEhjv6HND"`. - * `thread`: an indicator that the message is a part of a conversation thread, a topic-unique ID of the first message in the thread, `":123"`; `thread` is intended for tagging a flat list of messages as opposite to creating a tree. + * `mime`: MIME-type of the message content, `"text/x-drafty"`; + a `null` or a missing value is interpreted as `"text/plain"`. + * `replace`: an indicator that the message is a correction/replacement for another message, + a topic-unique ID of the message being updated/replaced, `":123"` + * `reply`: an indicator that the message is a reply to another message, + a unique ID of the original message, `"grp1XUtEhjv6HND:123"`. + * `sender`: a user ID of the sender added by the server when the message is sent + on behalf of another user, `"usr1XUtEhjv6HND"`. + * `thread`: an indicator that the message is a part of a conversation thread, + a topic-unique ID of the first message in the thread, `":123"`; + `thread` is intended for tagging a flat list of messages as opposite to creating a tree. * `webrtc`: a string representing the state of the video call the message represents. Possible values: * `"started"`: call has been initiated and being established * `"accepted"`: call has been accepted and established @@ -322,10 +349,13 @@ class PublishEvent(ClientEvent): * `"disconnected"`: call was terminated by the server for other reasons (e.g. due to an error) * `webrtc-duration`: a number representing a video call duration (in milliseconds). - Application-specific fields should start with an `x--`. Although the server does not enforce this rule yet, it may start doing so in the future. + Application-specific fields should start with an `x--`. + Although the server does not enforce this rule yet, it may start doing so in the future. - The unique message ID should be formed as `:` whenever possible, such as `"grp1XUtEhjv6HND:123"`. If the topic is omitted, i.e. `":123"`, it's assumed to be the current topic. + The unique message ID should be formed as `:` whenever possible, such as `"grp1XUtEhjv6HND:123"`. + If the topic is omitted, i.e. `":123"`, it's assumed to be the current topic. """ + __slots__ = ["text", "topic"] def __init__(self, bot: "bot.Bot", topic: str, text: Union[str, Drafty, BaseText]) -> None: @@ -342,20 +372,31 @@ class SubscribeEvent(ClientEvent): * attaching session to a previously subscribed topic * fetching topic data - User creates a new group topic by sending `{sub}` packet with the `topic` field set to `new12321` (regular topic) or `nch12321` (channel) where `12321` denotes any string including an empty string. Server will create a topic and respond back to the session with the name of the newly created topic. + User creates a new group topic by sending `{sub}` packet with the `topic` field set + to `new12321` (regular topic) or `nch12321` (channel) where `12321` denotes any string including an empty string. + Server will create a topic and respond back to the session with the name of the newly created topic. User creates a new peer to peer topic by sending `{sub}` packet with `topic` set to peer's user ID. The user is always subscribed to and the session is attached to the newly created topic. - If the user had no relationship with the topic, sending `{sub}` packet creates it. Subscribing means to establish a relationship between session's user and the topic where no relationship existed in the past. + If the user had no relationship with the topic, sending `{sub}` packet creates it. + Subscribing means to establish a relationship between session's user and + the topic where no relationship existed in the past. - Joining (attaching to) a topic means for the session to start consuming content from the topic. Server automatically differentiates between subscribing and joining/attaching based on context: if the user had no prior relationship with the topic, the server subscribes the user then attaches the current session to the topic. If relationship existed, the server only attaches the session to the topic. When subscribing, the server checks user's access permissions against topic's access control list. It may grant immediate access, deny access, may generate a request for approval from topic managers. + Joining (attaching to) a topic means for the session to start consuming content from the topic. + Server automatically differentiates between subscribing and joining/attaching based on context: + if the user had no prior relationship with the topic, the server subscribes the user + then attaches the current session to the topic. + If relationship existed, the server only attaches the session to the topic. + When subscribing, the server checks user's access permissions against topic's access control list. + It may grant immediate access, deny access, may generate a request for approval from topic managers. Server replies to the `{sub}` with a `{ctrl}`. - The `{sub}` message may include a `get` and `set` fields which mirror `{get}` and `{set}` messages. If included, server will treat them as a subsequent `{set}` and `{get}` messages on the same topic. If the `get` is set, the reply may include `{meta}` and `{data}` messages. - + The `{sub}` message may include a `get` and `set` fields which mirror `{get}` and `{set}` messages. + If included, server will treat them as a subsequent `{set}` and `{get}` messages on the same topic. If the `get` is set, + the reply may include `{meta}` and `{data}` messages. ```js sub: { @@ -434,6 +475,7 @@ class SubscribeEvent(ClientEvent): } ``` """ + __slots__ = ["topic", "since"] def __init__(self, bot: "bot.Bot", topic: str, *, since: Optional[int] = None) -> None: @@ -448,7 +490,9 @@ class LeaveEvent(ClientEvent): * leaving the topic without unsubscribing (`unsub=false`) * unsubscribing (`unsub=true`) - Server responds to `{leave}` with a `{ctrl}` packet. Leaving without unsubscribing affects just the current session. Leaving with unsubscribing will affect all user's sessions. + Server responds to `{leave}` with a `{ctrl}` packet. + Leaving without unsubscribing affects just the current session. + Leaving with unsubscribing will affect all user's sessions. ```js leave: { @@ -459,6 +503,7 @@ class LeaveEvent(ClientEvent): } ``` """ + __slots__ = ["topic"] def __init__(self, bot: "bot.Bot", topic: str) -> None: diff --git a/karuha/event/dispatcher.py b/karuha/event/dispatcher.py index ad27a03..1743597 100644 --- a/karuha/event/dispatcher.py +++ b/karuha/event/dispatcher.py @@ -26,7 +26,7 @@ def deactivate(self) -> None: self.dispatchers.remove(self) @classmethod - def dispatch(cls, message: T) -> None: + def dispatch(cls, message: T, /) -> None: if not cls.dispatchers: return selected = max( @@ -44,9 +44,9 @@ def activated(self) -> bool: class FutureDispatcher(AbstractDispatcher[T]): __slots__ = ["future"] - def __init__(self, future: asyncio.Future) -> None: + def __init__(self, /, future: asyncio.Future) -> None: super().__init__() self.future = future - def run(self, message: T) -> None: + def run(self, message: T, /) -> None: self.future.set_result(message)