mirror of
https://github.com/EvolutionAPI/evolution-client-python.git
synced 2026-02-04 13:56:23 -06:00
initial commit
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
import asyncio
|
||||
import contextlib
|
||||
from itertools import count
|
||||
from typing import Optional
|
||||
|
||||
from jeepney.auth import Authenticator, BEGIN
|
||||
from jeepney.bus import get_bus
|
||||
from jeepney import Message, MessageType, Parser
|
||||
from jeepney.wrappers import ProxyBase, unwrap_msg
|
||||
from jeepney.bus_messages import message_bus
|
||||
from .common import (
|
||||
MessageFilters, FilterHandle, ReplyMatcher, RouterClosed, check_replyable,
|
||||
)
|
||||
|
||||
|
||||
class DBusConnection:
|
||||
"""A plain D-Bus connection with no matching of replies.
|
||||
|
||||
This doesn't run any separate tasks: sending and receiving are done in
|
||||
the task that calls those methods. It's suitable for implementing servers:
|
||||
several worker tasks can receive requests and send replies.
|
||||
For a typical client pattern, see :class:`DBusRouter`.
|
||||
"""
|
||||
def __init__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
|
||||
self.reader = reader
|
||||
self.writer = writer
|
||||
self.parser = Parser()
|
||||
self.outgoing_serial = count(start=1)
|
||||
self.unique_name = None
|
||||
self.send_lock = asyncio.Lock()
|
||||
|
||||
async def send(self, message: Message, *, serial=None):
|
||||
"""Serialise and send a :class:`~.Message` object"""
|
||||
async with self.send_lock:
|
||||
if serial is None:
|
||||
serial = next(self.outgoing_serial)
|
||||
self.writer.write(message.serialise(serial))
|
||||
await self.writer.drain()
|
||||
|
||||
async def receive(self) -> Message:
|
||||
"""Return the next available message from the connection"""
|
||||
while True:
|
||||
msg = self.parser.get_next_message()
|
||||
if msg is not None:
|
||||
return msg
|
||||
|
||||
b = await self.reader.read(4096)
|
||||
if not b:
|
||||
raise EOFError
|
||||
self.parser.add_data(b)
|
||||
|
||||
async def close(self):
|
||||
"""Close the D-Bus connection"""
|
||||
self.writer.close()
|
||||
await self.writer.wait_closed()
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
await self.close()
|
||||
|
||||
|
||||
async def open_dbus_connection(bus='SESSION'):
|
||||
"""Open a plain D-Bus connection
|
||||
|
||||
:return: :class:`DBusConnection`
|
||||
"""
|
||||
bus_addr = get_bus(bus)
|
||||
reader, writer = await asyncio.open_unix_connection(bus_addr)
|
||||
|
||||
# Authentication flow
|
||||
authr = Authenticator()
|
||||
for req_data in authr:
|
||||
writer.write(req_data)
|
||||
await writer.drain()
|
||||
b = await reader.read(1024)
|
||||
if not b:
|
||||
raise EOFError("Socket closed before authentication")
|
||||
authr.feed(b)
|
||||
|
||||
writer.write(BEGIN)
|
||||
await writer.drain()
|
||||
# Authentication finished
|
||||
|
||||
conn = DBusConnection(reader, writer)
|
||||
|
||||
# Say *Hello* to the message bus - this must be the first message, and the
|
||||
# reply gives us our unique name.
|
||||
async with DBusRouter(conn) as router:
|
||||
reply_body = await asyncio.wait_for(Proxy(message_bus, router).Hello(), 10)
|
||||
conn.unique_name = reply_body[0]
|
||||
|
||||
return conn
|
||||
|
||||
class DBusRouter:
|
||||
"""A 'client' D-Bus connection which can wait for a specific reply.
|
||||
|
||||
This runs a background receiver task, and makes it possible to send a
|
||||
request and wait for the relevant reply.
|
||||
"""
|
||||
_nursery_mgr = None
|
||||
_send_cancel_scope = None
|
||||
_rcv_cancel_scope = None
|
||||
|
||||
def __init__(self, conn: DBusConnection):
|
||||
self._conn = conn
|
||||
self._replies = ReplyMatcher()
|
||||
self._filters = MessageFilters()
|
||||
self._rcv_task = asyncio.create_task(self._receiver())
|
||||
|
||||
@property
|
||||
def unique_name(self):
|
||||
return self._conn.unique_name
|
||||
|
||||
async def send(self, message, *, serial=None):
|
||||
"""Send a message, don't wait for a reply"""
|
||||
await self._conn.send(message, serial=serial)
|
||||
|
||||
async def send_and_get_reply(self, message) -> Message:
|
||||
"""Send a method call message and wait for the reply
|
||||
|
||||
Returns the reply message (method return or error message type).
|
||||
"""
|
||||
check_replyable(message)
|
||||
if self._rcv_task.done():
|
||||
raise RouterClosed("This DBusRouter has stopped")
|
||||
|
||||
serial = next(self._conn.outgoing_serial)
|
||||
|
||||
with self._replies.catch(serial, asyncio.Future()) as reply_fut:
|
||||
await self.send(message, serial=serial)
|
||||
return (await reply_fut)
|
||||
|
||||
def filter(self, rule, *, queue: Optional[asyncio.Queue] =None, bufsize=1):
|
||||
"""Create a filter for incoming messages
|
||||
|
||||
Usage::
|
||||
|
||||
with router.filter(rule) as queue:
|
||||
matching_msg = await queue.get()
|
||||
|
||||
:param MatchRule rule: Catch messages matching this rule
|
||||
:param asyncio.Queue queue: Send matching messages here
|
||||
:param int bufsize: If no queue is passed in, create one with this size
|
||||
"""
|
||||
return FilterHandle(self._filters, rule, queue or asyncio.Queue(bufsize))
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
if self._rcv_task.done():
|
||||
self._rcv_task.result() # Throw exception if receive task failed
|
||||
else:
|
||||
self._rcv_task.cancel()
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await self._rcv_task
|
||||
return False
|
||||
|
||||
# Code to run in receiver task ------------------------------------
|
||||
|
||||
def _dispatch(self, msg: Message):
|
||||
"""Handle one received message"""
|
||||
if self._replies.dispatch(msg):
|
||||
return
|
||||
|
||||
for filter in list(self._filters.matches(msg)):
|
||||
try:
|
||||
filter.queue.put_nowait(msg)
|
||||
except asyncio.QueueFull:
|
||||
pass
|
||||
|
||||
async def _receiver(self):
|
||||
"""Receiver loop - runs in a separate task"""
|
||||
try:
|
||||
while True:
|
||||
msg = await self._conn.receive()
|
||||
self._dispatch(msg)
|
||||
finally:
|
||||
# Send errors to any tasks still waiting for a message.
|
||||
self._replies.drop_all()
|
||||
|
||||
class open_dbus_router:
|
||||
"""Open a D-Bus 'router' to send and receive messages
|
||||
|
||||
Use as an async context manager::
|
||||
|
||||
async with open_dbus_router() as router:
|
||||
...
|
||||
"""
|
||||
conn = None
|
||||
req_ctx = None
|
||||
|
||||
def __init__(self, bus='SESSION'):
|
||||
self.bus = bus
|
||||
|
||||
async def __aenter__(self):
|
||||
self.conn = await open_dbus_connection(self.bus)
|
||||
self.req_ctx = DBusRouter(self.conn)
|
||||
return await self.req_ctx.__aenter__()
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
await self.req_ctx.__aexit__(exc_type, exc_val, exc_tb)
|
||||
await self.conn.close()
|
||||
|
||||
|
||||
class Proxy(ProxyBase):
|
||||
"""An asyncio proxy for calling D-Bus methods
|
||||
|
||||
You can call methods on the proxy object, such as ``await bus_proxy.Hello()``
|
||||
to make a method call over D-Bus and wait for a reply. It will either
|
||||
return a tuple of returned data, or raise :exc:`.DBusErrorResponse`.
|
||||
The methods available are defined by the message generator you wrap.
|
||||
|
||||
:param msggen: A message generator object.
|
||||
:param ~asyncio.DBusRouter router: Router to send and receive messages.
|
||||
"""
|
||||
def __init__(self, msggen, router):
|
||||
super().__init__(msggen)
|
||||
self._router = router
|
||||
|
||||
def __repr__(self):
|
||||
return 'Proxy({}, {})'.format(self._msggen, self._router)
|
||||
|
||||
def _method_call(self, make_msg):
|
||||
async def inner(*args, **kwargs):
|
||||
msg = make_msg(*args, **kwargs)
|
||||
assert msg.header.message_type is MessageType.method_call
|
||||
reply = await self._router.send_and_get_reply(msg)
|
||||
return unwrap_msg(reply)
|
||||
|
||||
return inner
|
||||
Reference in New Issue
Block a user