From 9b04a045687a680acbd90f1fa32babebe8f0747f Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 5 Nov 2022 17:55:45 +0000 Subject: [PATCH 01/24] Add async python client for Delta Chat core JSON-RPC --- .github/workflows/ci.yml | 14 +++ CHANGELOG.md | 1 + deltachat-rpc-client/README.md | 33 +++++++ deltachat-rpc-client/examples/echobot.py | 58 ++++++++++++ deltachat-rpc-client/pyproject.toml | 14 +++ .../src/deltachat_rpc_client/__init__.py | 5 + .../src/deltachat_rpc_client/account.py | 68 ++++++++++++++ .../src/deltachat_rpc_client/chat.py | 33 +++++++ .../src/deltachat_rpc_client/contact.py | 44 +++++++++ .../src/deltachat_rpc_client/deltachat.py | 31 +++++++ .../src/deltachat_rpc_client/message.py | 41 +++++++++ .../src/deltachat_rpc_client/rpc.py | 92 +++++++++++++++++++ deltachat-rpc-client/tests/test_something.py | 92 +++++++++++++++++++ deltachat-rpc-client/tox.ini | 20 ++++ 14 files changed, 546 insertions(+) create mode 100644 deltachat-rpc-client/README.md create mode 100755 deltachat-rpc-client/examples/echobot.py create mode 100644 deltachat-rpc-client/pyproject.toml create mode 100644 deltachat-rpc-client/src/deltachat_rpc_client/__init__.py create mode 100644 deltachat-rpc-client/src/deltachat_rpc_client/account.py create mode 100644 deltachat-rpc-client/src/deltachat_rpc_client/chat.py create mode 100644 deltachat-rpc-client/src/deltachat_rpc_client/contact.py create mode 100644 deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py create mode 100644 deltachat-rpc-client/src/deltachat_rpc_client/message.py create mode 100644 deltachat-rpc-client/src/deltachat_rpc_client/rpc.py create mode 100644 deltachat-rpc-client/tests/test_something.py create mode 100644 deltachat-rpc-client/tox.ini diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f9b1378fed..25c0207ac6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -147,6 +147,20 @@ jobs: working-directory: python run: tox -e lint,mypy,doc,py3 + - name: build deltachat-rpc-server + if: ${{ matrix.python }} + uses: actions-rs/cargo@v1 + with: + command: build + args: -p deltachat-rpc-server + + - name: run deltachat-rpc-client tests + if: ${{ matrix.python }} + env: + DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }} + working-directory: deltachat-rpc-client + run: tox -e py3 + - name: install pypy if: ${{ matrix.python }} uses: actions/setup-python@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dc1371d65..044171d367 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### API-Changes - Add Python API to send reactions #3762 - jsonrpc: add message errors to MessageObject #3788 +- jsonrpc: Add async Python client #3734 ### Fixes - Make sure malformed messsages will never block receiving further messages anymore #3771 diff --git a/deltachat-rpc-client/README.md b/deltachat-rpc-client/README.md new file mode 100644 index 0000000000..6b38c361ab --- /dev/null +++ b/deltachat-rpc-client/README.md @@ -0,0 +1,33 @@ +# Delta Chat RPC python client + +RPC client connects to standalone Delta Chat RPC server `deltachat-rpc-server` +and provides asynchronous interface to it. + +## Getting started + +To use Delta Chat RPC client, first build a `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`. +Install it anywhere in your `PATH`. + +## Testing + +1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`. +2. Run `tox`. + +Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output. + +## Using in REPL + +It is recommended to use IPython, because it supports using `await` directly +from the REPL. + +``` +PATH="../target/debug:$PATH" ipython +... +In [1]: from deltachat_rpc_client import * +In [2]: dc = Deltachat(await start_rpc_server()) +In [3]: await dc.get_all_accounts() +Out [3]: [] +In [4]: alice = await dc.add_account() +In [5]: (await alice.get_info())["journal_mode"] +Out [5]: 'wal' +``` diff --git a/deltachat-rpc-client/examples/echobot.py b/deltachat-rpc-client/examples/echobot.py new file mode 100755 index 0000000000..a55ca91a4b --- /dev/null +++ b/deltachat-rpc-client/examples/echobot.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +import asyncio +import logging +import sys + +import deltachat_rpc_client as dc + + +async def main(): + rpc = await dc.start_rpc_server() + deltachat = dc.Deltachat(rpc) + system_info = await deltachat.get_system_info() + logging.info("Running deltachat core %s", system_info["deltachat_core_version"]) + + accounts = await deltachat.get_all_accounts() + account = accounts[0] if accounts else await deltachat.add_account() + + await account.set_config("bot", "1") + if not await account.is_configured(): + logging.info("Account is not configured, configuring") + await account.set_config("addr", sys.argv[1]) + await account.set_config("mail_pw", sys.argv[2]) + await account.configure() + logging.info("Configured") + else: + logging.info("Account is already configured") + await deltachat.start_io() + + async def process_messages(): + fresh_messages = await account.get_fresh_messages() + fresh_message_snapshot_tasks = [ + message.get_snapshot() for message in fresh_messages + ] + fresh_message_snapshots = await asyncio.gather(*fresh_message_snapshot_tasks) + for snapshot in reversed(fresh_message_snapshots): + if not snapshot.is_info: + await snapshot.chat.send_text(snapshot.text) + await snapshot.message.mark_seen() + + # Process old messages. + await process_messages() + + while True: + event = await account.wait_for_event() + if event["type"] == "Info": + logging.info("%s", event["msg"]) + elif event["type"] == "Warning": + logging.warning("%s", event["msg"]) + elif event["type"] == "Error": + logging.error("%s", event["msg"]) + elif event["type"] == "IncomingMsg": + logging.info("Got an incoming message") + await process_messages() + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + asyncio.run(main()) diff --git a/deltachat-rpc-client/pyproject.toml b/deltachat-rpc-client/pyproject.toml new file mode 100644 index 0000000000..cabcc848a6 --- /dev/null +++ b/deltachat-rpc-client/pyproject.toml @@ -0,0 +1,14 @@ +[build-system] +requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"] +build-backend = "setuptools.build_meta" + +[project] +name = "deltachat-rpc-client" +description = "Python client for Delta Chat core JSON-RPC interface" +dependencies = [ + "aiohttp", + "aiodns" +] +dynamic = [ + "version" +] diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py b/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py new file mode 100644 index 0000000000..720e1ebdbd --- /dev/null +++ b/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py @@ -0,0 +1,5 @@ +from .account import Account +from .contact import Contact +from .deltachat import Deltachat +from .message import Message +from .rpc import Rpc, new_online_account, start_rpc_server diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/account.py b/deltachat-rpc-client/src/deltachat_rpc_client/account.py new file mode 100644 index 0000000000..ca2e812a29 --- /dev/null +++ b/deltachat-rpc-client/src/deltachat_rpc_client/account.py @@ -0,0 +1,68 @@ +from typing import Optional + +from .chat import Chat +from .contact import Contact +from .message import Message + + +class Account: + def __init__(self, rpc, account_id): + self.rpc = rpc + self.account_id = account_id + + def __repr__(self): + return "".format(self.account_id) + + async def wait_for_event(self): + """Wait until the next event and return it.""" + return await self.rpc.wait_for_event(self.account_id) + + async def remove(self) -> None: + """Remove the account.""" + await self.rpc.remove_account(self.account_id) + + async def start_io(self) -> None: + """Start the account I/O.""" + await self.rpc.start_io(self.account_id) + + async def stop_io(self) -> None: + """Stop the account I/O.""" + await self.rpc.stop_io(self.account_id) + + async def get_info(self): + return await self.rpc.get_info(self.account_id) + + async def get_file_size(self): + return await self.rpc.get_account_file_size(self.account_id) + + async def is_configured(self) -> bool: + """Return True for configured accounts.""" + return await self.rpc.is_configured(self.account_id) + + async def set_config(self, key: str, value: Optional[str]): + """Set the configuration value key pair.""" + await self.rpc.set_config(self.account_id, key, value) + + async def get_config(self, key: str) -> Optional[str]: + """Get the configuration value.""" + return await self.rpc.get_config(self.account_id, key) + + async def configure(self): + """Configure an account.""" + await self.rpc.configure(self.account_id) + + async def create_contact(self, address: str, name: Optional[str]) -> Contact: + """Create a contact with the given address and, optionally, a name.""" + return Contact( + self.rpc, + self.account_id, + await self.rpc.create_contact(self.account_id, address, name), + ) + + async def secure_join(self, qr: str) -> Chat: + chat_id = await self.rpc.secure_join(self.account_id, qr) + return Chat(self.rpc, self.account_id, self.chat_id) + + async def get_fresh_messages(self): + fresh_msg_ids = await self.rpc.get_fresh_msgs(self.account_id) + return [Message(self.rpc, self.account_id, msg_id) for msg_id in fresh_msg_ids] diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/chat.py b/deltachat-rpc-client/src/deltachat_rpc_client/chat.py new file mode 100644 index 0000000000..6600bb8bc1 --- /dev/null +++ b/deltachat-rpc-client/src/deltachat_rpc_client/chat.py @@ -0,0 +1,33 @@ +class Chat: + def __init__(self, rpc, account_id, chat_id): + self.rpc = rpc + self.account_id = account_id + self.chat_id = chat_id + + async def block(self): + """Block the chat.""" + await self.rpc.block_chat(self.account_id, self.chat_id) + + async def accept(self): + """Accept the contact request.""" + await self.rpc.accept_chat(self.account_id, self.chat_id) + + async def delete(self): + await self.rpc.delete_chat(self.account_id, self.chat_id) + + async def get_encryption_info(self): + await self.rpc.get_chat_encryption_info(self.account_id, self.chat_id) + + async def send_text(self, text: str): + from .message import Message + + msg_id = await self.rpc.misc_send_text_message( + self.account_id, self.chat_id, text + ) + return Message(self.rpc, self.account_id, msg_id) + + async def leave(self): + await self.rpc.leave_group(self.account_id, self.chat_id) + + async def get_fresh_message_count() -> int: + await get_fresh_msg_cnt(self.account_id, self.chat_id) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/contact.py b/deltachat-rpc-client/src/deltachat_rpc_client/contact.py new file mode 100644 index 0000000000..8f60ad16f4 --- /dev/null +++ b/deltachat-rpc-client/src/deltachat_rpc_client/contact.py @@ -0,0 +1,44 @@ +class Contact: + """ + Contact API. + + Essentially a wrapper for RPC, account ID and a contact ID. + """ + + def __init__(self, rpc, account_id, contact_id): + self.rpc = rpc + self.account_id = account_id + self.contact_id = contact_id + + async def block(self): + """Block contact.""" + await self.rpc.block_contact(self.account_id, self.contact_id) + + async def unblock(self): + """Unblock contact.""" + await self.rpc.unblock_contact(self.account_id, self.contact_id) + + async def delete(self): + """Delete contact.""" + await self.rpc.delete_contact(self.account_id, self.contact_id) + + async def change_name(self, name: str): + await self.rpc.change_contact_name(self.account_id, self.contact_id, name) + + async def get_encryption_info(self) -> str: + return await self.rpc.get_contact_encryption_info( + self.account_id, self.contact_id + ) + + async def get_dictionary(self): + """Returns a dictionary with a snapshot of all contact properties.""" + return await self.rpc.get_contact(self.account_id, self.contact_id) + + async def create_chat(self): + from .chat import Chat + + return Chat( + self.rpc, + self.account_id, + await self.rpc.create_chat_by_contact_id(self.account_id, self.contact_id), + ) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py b/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py new file mode 100644 index 0000000000..8b58494182 --- /dev/null +++ b/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py @@ -0,0 +1,31 @@ +from .account import Account + + +class Deltachat: + """ + Delta Chat account manager. + This is the root of the object oriented API. + """ + + def __init__(self, rpc): + self.rpc = rpc + + async def add_account(self): + account_id = await self.rpc.add_account() + return Account(self.rpc, account_id) + + async def get_all_accounts(self): + account_ids = await self.rpc.get_all_account_ids() + return [Account(self.rpc, account_id) for account_id in account_ids] + + async def start_io(self) -> None: + await self.rpc.start_io_for_all_accounts() + + async def stop_io(self) -> None: + await self.rpc.stop_io_for_all_accounts() + + async def maybe_network(self) -> None: + await self.rpc.maybe_network() + + async def get_system_info(self): + return await self.rpc.get_system_info() diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/message.py b/deltachat-rpc-client/src/deltachat_rpc_client/message.py new file mode 100644 index 0000000000..346c6e787e --- /dev/null +++ b/deltachat-rpc-client/src/deltachat_rpc_client/message.py @@ -0,0 +1,41 @@ +from dataclasses import dataclass +from typing import Optional + +from .chat import Chat +from .contact import Contact + + +class Message: + def __init__(self, rpc, account_id, msg_id): + self.rpc = rpc + self.account_id = account_id + self.msg_id = msg_id + + async def send_reaction(self, reactions): + msg_id = await self.rpc.send_reaction(self.account_id, self.msg_id, reactions) + return Message(self.rpc, self.account_id, msg_id) + + async def get_snapshot(self): + message_object = await self.rpc.get_message(self.account_id, self.msg_id) + return MessageSnapshot( + message=self, + chat=Chat(self.rpc, self.account_id, message_object["chatId"]), + sender=Contact(self.rpc, self.account_id, message_object["fromId"]), + text=message_object["text"], + error=message_object.get("error"), + is_info=message_object["isInfo"], + ) + + async def mark_seen(self) -> None: + """Mark the message as seen.""" + await self.rpc.markseen_msgs(self.account_id, [self.msg_id]) + + +@dataclass +class MessageSnapshot: + message: Message + chat: Chat + sender: Contact + text: str + error: Optional[str] + is_info: bool diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py new file mode 100644 index 0000000000..efbaa0b80d --- /dev/null +++ b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py @@ -0,0 +1,92 @@ +import asyncio +import json +import logging +import os + +import aiohttp + + +class JsonRpcError(Exception): + pass + + +class Rpc: + def __init__(self, process): + self.process = process + self.event_queues = {} + self.id = 0 + self.reader_task = asyncio.create_task(self.reader_loop()) + + # Map from request ID to `asyncio.Future` returning the response. + self.request_events = {} + + async def reader_loop(self): + while True: + line = await self.process.stdout.readline() + response = json.loads(line) + if "id" in response: + fut = self.request_events.pop(response["id"]) + fut.set_result(response) + elif response["method"] == "event": + # An event notification. + params = response["params"] + account_id = params["contextId"] + if account_id not in self.event_queues: + self.event_queues[account_id] = asyncio.Queue() + await self.event_queues[account_id].put(params["event"]) + else: + print(response) + + async def wait_for_event(self, account_id): + """Waits for the next event from the given account and returns it.""" + if account_id in self.event_queues: + return await self.event_queues[account_id].get() + + def __getattr__(self, attr): + async def method(*args, **kwargs): + self.id += 1 + request_id = self.id + + params = args + if kwargs: + assert not args + params = kwargs + + request = { + "jsonrpc": "2.0", + "method": attr, + "params": params, + "id": self.id, + } + data = (json.dumps(request) + "\n").encode() + self.process.stdin.write(data) + event = asyncio.Event() + loop = asyncio.get_running_loop() + fut = loop.create_future() + self.request_events[request_id] = fut + response = await fut + if "error" in response: + raise JsonRpcError(response["error"]) + if "result" in response: + return response["result"] + + return method + + +async def start_rpc_server(*args, **kwargs): + proc = await asyncio.create_subprocess_exec( + "deltachat-rpc-server", + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + *args, + **kwargs + ) + rpc = Rpc(proc) + return rpc + + +async def new_online_account(): + url = os.getenv("DCC_NEW_TMP_EMAIL") + async with aiohttp.ClientSession() as session: + async with session.post(url) as response: + return json.loads(await response.text()) diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py new file mode 100644 index 0000000000..1744f3de91 --- /dev/null +++ b/deltachat-rpc-client/tests/test_something.py @@ -0,0 +1,92 @@ +import asyncio +import os + +import pytest +import pytest_asyncio + +import deltachat_rpc_client +from deltachat_rpc_client import Deltachat + + +@pytest_asyncio.fixture +async def rpc(tmp_path): + return await deltachat_rpc_client.start_rpc_server( + env={**os.environ, "DC_ACCOUNTS_PATH": str(tmp_path / "accounts")} + ) + + +@pytest.mark.asyncio +async def test_system_info(rpc): + system_info = await rpc.get_system_info() + assert "arch" in system_info + assert "deltachat_core_version" in system_info + + +@pytest.mark.asyncio +async def test_email_address_validity(rpc): + valid_addresses = [ + "email@example.com", + "36aa165ae3406424e0c61af17700f397cad3fe8ab83d682d0bddf3338a5dd52e@yggmail@yggmail", + ] + invalid_addresses = ["email@", "example.com", "emai221"] + + for addr in valid_addresses: + assert await rpc.check_email_validity(addr) + for addr in invalid_addresses: + assert not await rpc.check_email_validity(addr) + + +@pytest.mark.asyncio +async def test_online_account(rpc): + account_json = await deltachat_rpc_client.new_online_account() + + account_id = await rpc.add_account() + await rpc.set_config(account_id, "addr", account_json["email"]) + await rpc.set_config(account_id, "mail_pw", account_json["password"]) + + await rpc.configure(account_id) + while True: + event = await rpc.wait_for_event(account_id) + if event["type"] == "ConfigureProgress": + # Progress 0 indicates error. + assert event["progress"] != 0 + + if event["progress"] == 1000: + # Success. + break + else: + print(event) + print("Successful configuration") + + +@pytest.mark.asyncio +async def test_object_account(rpc): + deltachat = Deltachat(rpc) + + async def create_configured_account(): + account = await deltachat.add_account() + assert not await account.is_configured() + account_json = await deltachat_rpc_client.new_online_account() + await account.set_config("addr", account_json["email"]) + await account.set_config("mail_pw", account_json["password"]) + await account.configure() + assert await account.is_configured() + return account + + alice, bob = await asyncio.gather( + create_configured_account(), create_configured_account() + ) + + alice_contact_bob = await alice.create_contact(await bob.get_config("addr"), "Bob") + alice_chat_bob = await alice_contact_bob.create_chat() + await alice_chat_bob.send_text("Hello!") + + while True: + event = await bob.wait_for_event() + if event["type"] == "IncomingMsg": + chat_id = event["chatId"] + msg_id = event["msgId"] + break + + message = await rpc.get_message(bob.account_id, msg_id) + assert message["text"] == "Hello!" diff --git a/deltachat-rpc-client/tox.ini b/deltachat-rpc-client/tox.ini new file mode 100644 index 0000000000..20618227b9 --- /dev/null +++ b/deltachat-rpc-client/tox.ini @@ -0,0 +1,20 @@ +[tox] +isolated_build = true +envlist = + py3 + +[testenv] +commands = + pytest {posargs} +setenv = +# Avoid stack overflow when Rust core is built without optimizations. + RUST_MIN_STACK=8388608 + PATH = {env:PATH}{:}{toxinidir}/../target/debug +passenv = + DCC_NEW_TMP_EMAIL +deps = + pytest + pytest-async + pytest-asyncio + aiohttp + aiodns From 4e2aeceeec8bcdbaa5f0a4b97ecd4ec16a5e52ce Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Nov 2022 22:38:25 +0000 Subject: [PATCH 02/24] Do not reverse the list of fresh messages Both reversed and original order do not make much sense for the bot. Ideally bots should have their own key to get the list of fresh messages in the order of IDs. --- deltachat-rpc-client/examples/echobot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deltachat-rpc-client/examples/echobot.py b/deltachat-rpc-client/examples/echobot.py index a55ca91a4b..6c31d5d4d0 100755 --- a/deltachat-rpc-client/examples/echobot.py +++ b/deltachat-rpc-client/examples/echobot.py @@ -32,7 +32,7 @@ async def process_messages(): message.get_snapshot() for message in fresh_messages ] fresh_message_snapshots = await asyncio.gather(*fresh_message_snapshot_tasks) - for snapshot in reversed(fresh_message_snapshots): + for snapshot in fresh_message_snapshots: if not snapshot.is_info: await snapshot.chat.send_text(snapshot.text) await snapshot.message.mark_seen() From db38cc8891194cf4940aa053b125644d4215c72c Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Nov 2022 22:43:05 +0000 Subject: [PATCH 03/24] Add get_fresh_messages_in_arrival_order() call --- .../src/deltachat_rpc_client/account.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/account.py b/deltachat-rpc-client/src/deltachat_rpc_client/account.py index ca2e812a29..55101e11ea 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/account.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/account.py @@ -64,5 +64,16 @@ async def secure_join(self, qr: str) -> Chat: return Chat(self.rpc, self.account_id, self.chat_id) async def get_fresh_messages(self): + """Return the list of fresh messages, newest messages first. + + This call is intended for displaying notifications. + If you are writing a bot, use get_fresh_messages_in_arrival_order instead, + to process oldest messages first. + """ fresh_msg_ids = await self.rpc.get_fresh_msgs(self.account_id) return [Message(self.rpc, self.account_id, msg_id) for msg_id in fresh_msg_ids] + + async def get_fresh_messages_in_arrival_order(self): + """Return the list of fresh messages sorted in the order of their arrival, with ascending IDs.""" + fresh_msg_ids = sorted(await self.rpc.get_fresh_msgs(self.account_id)) + return [Message(self.rpc, self.account_id, msg_id) for msg_id in fresh_msg_ids] From 53d6807e8d2839b518ee4a82129edce400a26dc8 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 30 Nov 2022 22:45:03 +0000 Subject: [PATCH 04/24] Simplify process_messages() in echo bot --- deltachat-rpc-client/examples/echobot.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/deltachat-rpc-client/examples/echobot.py b/deltachat-rpc-client/examples/echobot.py index 6c31d5d4d0..d5b95f3a2c 100755 --- a/deltachat-rpc-client/examples/echobot.py +++ b/deltachat-rpc-client/examples/echobot.py @@ -27,12 +27,8 @@ async def main(): await deltachat.start_io() async def process_messages(): - fresh_messages = await account.get_fresh_messages() - fresh_message_snapshot_tasks = [ - message.get_snapshot() for message in fresh_messages - ] - fresh_message_snapshots = await asyncio.gather(*fresh_message_snapshot_tasks) - for snapshot in fresh_message_snapshots: + for message in await account.get_fresh_messages_in_arrival_order(): + snapshot = await message.get_snapshot() if not snapshot.is_info: await snapshot.chat.send_text(snapshot.text) await snapshot.message.mark_seen() From 240b3351814ab8335afe73945faac9c01762d1ec Mon Sep 17 00:00:00 2001 From: adbenitez Date: Wed, 30 Nov 2022 22:36:49 -0500 Subject: [PATCH 05/24] fix bugs in account.secure_join() and chat.get_fresh_message_count() --- deltachat-rpc-client/src/deltachat_rpc_client/account.py | 2 +- deltachat-rpc-client/src/deltachat_rpc_client/chat.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/account.py b/deltachat-rpc-client/src/deltachat_rpc_client/account.py index 55101e11ea..5e78c7a6e0 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/account.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/account.py @@ -61,7 +61,7 @@ async def create_contact(self, address: str, name: Optional[str]) -> Contact: async def secure_join(self, qr: str) -> Chat: chat_id = await self.rpc.secure_join(self.account_id, qr) - return Chat(self.rpc, self.account_id, self.chat_id) + return Chat(self.rpc, self.account_id, chat_id) async def get_fresh_messages(self): """Return the list of fresh messages, newest messages first. diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/chat.py b/deltachat-rpc-client/src/deltachat_rpc_client/chat.py index 6600bb8bc1..99d6168625 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/chat.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/chat.py @@ -29,5 +29,5 @@ async def send_text(self, text: str): async def leave(self): await self.rpc.leave_group(self.account_id, self.chat_id) - async def get_fresh_message_count() -> int: - await get_fresh_msg_cnt(self.account_id, self.chat_id) + async def get_fresh_message_count(self) -> int: + return await self.rpc.get_fresh_msg_cnt(self.account_id, self.chat_id) From a4be2cddf2469d9bc93b2b6cd049000b5ac2856c Mon Sep 17 00:00:00 2001 From: adbenitez Date: Wed, 30 Nov 2022 23:11:50 -0500 Subject: [PATCH 06/24] remove unused code in rpc.py --- deltachat-rpc-client/src/deltachat_rpc_client/rpc.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py index efbaa0b80d..27b52adba2 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py @@ -1,6 +1,5 @@ import asyncio import json -import logging import os import aiohttp @@ -60,7 +59,6 @@ async def method(*args, **kwargs): } data = (json.dumps(request) + "\n").encode() self.process.stdin.write(data) - event = asyncio.Event() loop = asyncio.get_running_loop() fut = loop.create_future() self.request_events[request_id] = fut From a77c46be20b2f65858b3848e0e1c4a468a818099 Mon Sep 17 00:00:00 2001 From: adbenitez Date: Wed, 30 Nov 2022 23:42:08 -0500 Subject: [PATCH 07/24] fix bug in account.py: arguments declared as optional but not default value was given --- deltachat-rpc-client/src/deltachat_rpc_client/account.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/account.py b/deltachat-rpc-client/src/deltachat_rpc_client/account.py index 5e78c7a6e0..e81e3b97b5 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/account.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/account.py @@ -39,7 +39,7 @@ async def is_configured(self) -> bool: """Return True for configured accounts.""" return await self.rpc.is_configured(self.account_id) - async def set_config(self, key: str, value: Optional[str]): + async def set_config(self, key: str, value: Optional[str] = None): """Set the configuration value key pair.""" await self.rpc.set_config(self.account_id, key, value) @@ -51,7 +51,7 @@ async def configure(self): """Configure an account.""" await self.rpc.configure(self.account_id) - async def create_contact(self, address: str, name: Optional[str]) -> Contact: + async def create_contact(self, address: str, name: Optional[str] = None) -> Contact: """Create a contact with the given address and, optionally, a name.""" return Contact( self.rpc, From aeb7e3a9e11746145acd2aa89f817dccd598b2f1 Mon Sep 17 00:00:00 2001 From: adbenitez Date: Wed, 30 Nov 2022 23:56:50 -0500 Subject: [PATCH 08/24] fix some linter warnings --- deltachat-rpc-client/src/deltachat_rpc_client/account.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/account.py b/deltachat-rpc-client/src/deltachat_rpc_client/account.py index e81e3b97b5..833f7ecc10 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/account.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/account.py @@ -11,7 +11,7 @@ def __init__(self, rpc, account_id): self.account_id = account_id def __repr__(self): - return "".format(self.account_id) + return f"" async def wait_for_event(self): """Wait until the next event and return it.""" @@ -59,8 +59,8 @@ async def create_contact(self, address: str, name: Optional[str] = None) -> Cont await self.rpc.create_contact(self.account_id, address, name), ) - async def secure_join(self, qr: str) -> Chat: - chat_id = await self.rpc.secure_join(self.account_id, qr) + async def secure_join(self, qrdata: str) -> Chat: + chat_id = await self.rpc.secure_join(self.account_id, qrdata) return Chat(self.rpc, self.account_id, chat_id) async def get_fresh_messages(self): @@ -74,6 +74,6 @@ async def get_fresh_messages(self): return [Message(self.rpc, self.account_id, msg_id) for msg_id in fresh_msg_ids] async def get_fresh_messages_in_arrival_order(self): - """Return the list of fresh messages sorted in the order of their arrival, with ascending IDs.""" + """Return fresh messages list sorted in the order of their arrival, with ascending IDs.""" fresh_msg_ids = sorted(await self.rpc.get_fresh_msgs(self.account_id)) return [Message(self.rpc, self.account_id, msg_id) for msg_id in fresh_msg_ids] From 18426561e3128d9c017a78706e84d7ffb83705a4 Mon Sep 17 00:00:00 2001 From: adbenitez Date: Thu, 1 Dec 2022 00:14:07 -0500 Subject: [PATCH 09/24] fix bug in chat.get_encryption_info() --- deltachat-rpc-client/src/deltachat_rpc_client/chat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/chat.py b/deltachat-rpc-client/src/deltachat_rpc_client/chat.py index 99d6168625..00b61bb358 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/chat.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/chat.py @@ -16,7 +16,7 @@ async def delete(self): await self.rpc.delete_chat(self.account_id, self.chat_id) async def get_encryption_info(self): - await self.rpc.get_chat_encryption_info(self.account_id, self.chat_id) + return await self.rpc.get_chat_encryption_info(self.account_id, self.chat_id) async def send_text(self, text: str): from .message import Message From 46594ec7073d4f8b0ca1005392c6847ff21915c6 Mon Sep 17 00:00:00 2001 From: adbenitez Date: Thu, 1 Dec 2022 00:37:45 -0500 Subject: [PATCH 10/24] improve typing hints --- .../src/deltachat_rpc_client/account.py | 20 +++++++------- .../src/deltachat_rpc_client/chat.py | 22 ++++++++++----- .../src/deltachat_rpc_client/contact.py | 24 +++++++++++------ .../src/deltachat_rpc_client/deltachat.py | 11 +++++--- .../src/deltachat_rpc_client/message.py | 7 ++--- .../src/deltachat_rpc_client/rpc.py | 27 +++++++++---------- 6 files changed, 65 insertions(+), 46 deletions(-) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/account.py b/deltachat-rpc-client/src/deltachat_rpc_client/account.py index 833f7ecc10..5c6ec0bc97 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/account.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/account.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import List, Optional from .chat import Chat from .contact import Contact @@ -6,14 +6,14 @@ class Account: - def __init__(self, rpc, account_id): + def __init__(self, rpc, account_id) -> None: self.rpc = rpc self.account_id = account_id - def __repr__(self): + def __repr__(self) -> str: return f"" - async def wait_for_event(self): + async def wait_for_event(self) -> dict: """Wait until the next event and return it.""" return await self.rpc.wait_for_event(self.account_id) @@ -29,17 +29,17 @@ async def stop_io(self) -> None: """Stop the account I/O.""" await self.rpc.stop_io(self.account_id) - async def get_info(self): + async def get_info(self) -> dict: return await self.rpc.get_info(self.account_id) - async def get_file_size(self): + async def get_file_size(self) -> int: return await self.rpc.get_account_file_size(self.account_id) async def is_configured(self) -> bool: """Return True for configured accounts.""" return await self.rpc.is_configured(self.account_id) - async def set_config(self, key: str, value: Optional[str] = None): + async def set_config(self, key: str, value: Optional[str] = None) -> None: """Set the configuration value key pair.""" await self.rpc.set_config(self.account_id, key, value) @@ -47,7 +47,7 @@ async def get_config(self, key: str) -> Optional[str]: """Get the configuration value.""" return await self.rpc.get_config(self.account_id, key) - async def configure(self): + async def configure(self) -> None: """Configure an account.""" await self.rpc.configure(self.account_id) @@ -63,7 +63,7 @@ async def secure_join(self, qrdata: str) -> Chat: chat_id = await self.rpc.secure_join(self.account_id, qrdata) return Chat(self.rpc, self.account_id, chat_id) - async def get_fresh_messages(self): + async def get_fresh_messages(self) -> List[Message]: """Return the list of fresh messages, newest messages first. This call is intended for displaying notifications. @@ -73,7 +73,7 @@ async def get_fresh_messages(self): fresh_msg_ids = await self.rpc.get_fresh_msgs(self.account_id) return [Message(self.rpc, self.account_id, msg_id) for msg_id in fresh_msg_ids] - async def get_fresh_messages_in_arrival_order(self): + async def get_fresh_messages_in_arrival_order(self) -> List[Message]: """Return fresh messages list sorted in the order of their arrival, with ascending IDs.""" fresh_msg_ids = sorted(await self.rpc.get_fresh_msgs(self.account_id)) return [Message(self.rpc, self.account_id, msg_id) for msg_id in fresh_msg_ids] diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/chat.py b/deltachat-rpc-client/src/deltachat_rpc_client/chat.py index 00b61bb358..bd5881c770 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/chat.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/chat.py @@ -1,24 +1,32 @@ +from typing import TYPE_CHECKING + +from .rpc import Rpc + +if TYPE_CHECKING: + from .message import Message + + class Chat: - def __init__(self, rpc, account_id, chat_id): + def __init__(self, rpc: Rpc, account_id: int, chat_id: int) -> None: self.rpc = rpc self.account_id = account_id self.chat_id = chat_id - async def block(self): + async def block(self) -> None: """Block the chat.""" await self.rpc.block_chat(self.account_id, self.chat_id) - async def accept(self): + async def accept(self) -> None: """Accept the contact request.""" await self.rpc.accept_chat(self.account_id, self.chat_id) - async def delete(self): + async def delete(self) -> None: await self.rpc.delete_chat(self.account_id, self.chat_id) - async def get_encryption_info(self): + async def get_encryption_info(self) -> str: return await self.rpc.get_chat_encryption_info(self.account_id, self.chat_id) - async def send_text(self, text: str): + async def send_text(self, text: str) -> "Message": from .message import Message msg_id = await self.rpc.misc_send_text_message( @@ -26,7 +34,7 @@ async def send_text(self, text: str): ) return Message(self.rpc, self.account_id, msg_id) - async def leave(self): + async def leave(self) -> None: await self.rpc.leave_group(self.account_id, self.chat_id) async def get_fresh_message_count(self) -> int: diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/contact.py b/deltachat-rpc-client/src/deltachat_rpc_client/contact.py index 8f60ad16f4..15622ec854 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/contact.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/contact.py @@ -1,3 +1,11 @@ +from typing import TYPE_CHECKING + +from .rpc import Rpc + +if TYPE_CHECKING: + from .chat import Chat + + class Contact: """ Contact API. @@ -5,24 +13,24 @@ class Contact: Essentially a wrapper for RPC, account ID and a contact ID. """ - def __init__(self, rpc, account_id, contact_id): + def __init__(self, rpc: Rpc, account_id: int, contact_id: int) -> None: self.rpc = rpc self.account_id = account_id self.contact_id = contact_id - async def block(self): + async def block(self) -> None: """Block contact.""" await self.rpc.block_contact(self.account_id, self.contact_id) - async def unblock(self): + async def unblock(self) -> None: """Unblock contact.""" await self.rpc.unblock_contact(self.account_id, self.contact_id) - async def delete(self): + async def delete(self) -> None: """Delete contact.""" await self.rpc.delete_contact(self.account_id, self.contact_id) - async def change_name(self, name: str): + async def change_name(self, name: str) -> None: await self.rpc.change_contact_name(self.account_id, self.contact_id, name) async def get_encryption_info(self) -> str: @@ -30,11 +38,11 @@ async def get_encryption_info(self) -> str: self.account_id, self.contact_id ) - async def get_dictionary(self): - """Returns a dictionary with a snapshot of all contact properties.""" + async def get_dictionary(self) -> dict: + """Return a dictionary with a snapshot of all contact properties.""" return await self.rpc.get_contact(self.account_id, self.contact_id) - async def create_chat(self): + async def create_chat(self) -> "Chat": from .chat import Chat return Chat( diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py b/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py index 8b58494182..5a9a1ec804 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py @@ -1,4 +1,7 @@ +from typing import List + from .account import Account +from .rpc import Rpc class Deltachat: @@ -7,14 +10,14 @@ class Deltachat: This is the root of the object oriented API. """ - def __init__(self, rpc): + def __init__(self, rpc: Rpc) -> None: self.rpc = rpc - async def add_account(self): + async def add_account(self) -> Account: account_id = await self.rpc.add_account() return Account(self.rpc, account_id) - async def get_all_accounts(self): + async def get_all_accounts(self) -> List[Account]: account_ids = await self.rpc.get_all_account_ids() return [Account(self.rpc, account_id) for account_id in account_ids] @@ -27,5 +30,5 @@ async def stop_io(self) -> None: async def maybe_network(self) -> None: await self.rpc.maybe_network() - async def get_system_info(self): + async def get_system_info(self) -> dict: return await self.rpc.get_system_info() diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/message.py b/deltachat-rpc-client/src/deltachat_rpc_client/message.py index 346c6e787e..8575a0444c 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/message.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/message.py @@ -3,19 +3,20 @@ from .chat import Chat from .contact import Contact +from .rpc import Rpc class Message: - def __init__(self, rpc, account_id, msg_id): + def __init__(self, rpc: Rpc, account_id: int, msg_id: int) -> None: self.rpc = rpc self.account_id = account_id self.msg_id = msg_id - async def send_reaction(self, reactions): + async def send_reaction(self, reactions: str) -> "Message": msg_id = await self.rpc.send_reaction(self.account_id, self.msg_id, reactions) return Message(self.rpc, self.account_id, msg_id) - async def get_snapshot(self): + async def get_snapshot(self) -> "MessageSnapshot": message_object = await self.rpc.get_message(self.account_id, self.msg_id) return MessageSnapshot( message=self, diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py index 27b52adba2..9f5e21e3b5 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py @@ -1,6 +1,7 @@ import asyncio import json import os +from typing import Any, Dict, Optional import aiohttp @@ -10,16 +11,16 @@ class JsonRpcError(Exception): class Rpc: - def __init__(self, process): + def __init__(self, process: asyncio.Process) -> None: self.process = process - self.event_queues = {} + self.event_queues: Dict[int, asyncio.Queue] = {} self.id = 0 self.reader_task = asyncio.create_task(self.reader_loop()) # Map from request ID to `asyncio.Future` returning the response. - self.request_events = {} + self.request_events: Dict[int, asyncio.Future] = {} - async def reader_loop(self): + async def reader_loop(self) -> None: while True: line = await self.process.stdout.readline() response = json.loads(line) @@ -36,25 +37,23 @@ async def reader_loop(self): else: print(response) - async def wait_for_event(self, account_id): + async def wait_for_event(self, account_id: int) -> Optional[dict]: """Waits for the next event from the given account and returns it.""" if account_id in self.event_queues: return await self.event_queues[account_id].get() + return None - def __getattr__(self, attr): - async def method(*args, **kwargs): + def __getattr__(self, attr: str): + async def method(*args, **kwargs) -> Any: self.id += 1 request_id = self.id - params = args - if kwargs: - assert not args - params = kwargs + assert not (args and kwargs), "Mixing positional and keyword arguments" request = { "jsonrpc": "2.0", "method": attr, - "params": params, + "params": args or kwargs, "id": self.id, } data = (json.dumps(request) + "\n").encode() @@ -71,7 +70,7 @@ async def method(*args, **kwargs): return method -async def start_rpc_server(*args, **kwargs): +async def start_rpc_server(*args, **kwargs) -> Rpc: proc = await asyncio.create_subprocess_exec( "deltachat-rpc-server", stdin=asyncio.subprocess.PIPE, @@ -83,7 +82,7 @@ async def start_rpc_server(*args, **kwargs): return rpc -async def new_online_account(): +async def new_online_account() -> dict: url = os.getenv("DCC_NEW_TMP_EMAIL") async with aiohttp.ClientSession() as session: async with session.post(url) as response: From ab7732d9ae405758cd02d435f7238458c2cd6a52 Mon Sep 17 00:00:00 2001 From: adbenitez Date: Thu, 1 Dec 2022 02:26:50 -0500 Subject: [PATCH 11/24] fix type hint in rpc.py --- deltachat-rpc-client/src/deltachat_rpc_client/rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py index 9f5e21e3b5..a63e7cd463 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py @@ -11,7 +11,7 @@ class JsonRpcError(Exception): class Rpc: - def __init__(self, process: asyncio.Process) -> None: + def __init__(self, process: asyncio.subprocess.Process) -> None: self.process = process self.event_queues: Dict[int, asyncio.Queue] = {} self.id = 0 From 09db0629582f58b13f851c9699aedd2483f9847e Mon Sep 17 00:00:00 2001 From: adbenitez Date: Thu, 1 Dec 2022 02:36:55 -0500 Subject: [PATCH 12/24] fix bug in Rpc.__getattr__() --- deltachat-rpc-client/src/deltachat_rpc_client/rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py index a63e7cd463..22c87ca4aa 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py @@ -53,7 +53,7 @@ async def method(*args, **kwargs) -> Any: request = { "jsonrpc": "2.0", "method": attr, - "params": args or kwargs, + "params": kwargs or args, "id": self.id, } data = (json.dumps(request) + "\n").encode() From ffbfeab977affd1be1de97d55eec524dc2aee9ca Mon Sep 17 00:00:00 2001 From: adbenitez Date: Thu, 1 Dec 2022 03:09:23 -0500 Subject: [PATCH 13/24] add pytest plugin --- deltachat-rpc-client/pyproject.toml | 2 + .../src/deltachat_rpc_client/__init__.py | 2 +- .../src/deltachat_rpc_client/pytestplugin.py | 50 +++++++++++++++++++ .../src/deltachat_rpc_client/rpc.py | 10 ---- deltachat-rpc-client/tests/test_something.py | 46 +++-------------- 5 files changed, 60 insertions(+), 50 deletions(-) create mode 100644 deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py diff --git a/deltachat-rpc-client/pyproject.toml b/deltachat-rpc-client/pyproject.toml index cabcc848a6..acdbeb4044 100644 --- a/deltachat-rpc-client/pyproject.toml +++ b/deltachat-rpc-client/pyproject.toml @@ -12,3 +12,5 @@ dependencies = [ dynamic = [ "version" ] +[project.entry-points.pytest11] +"deltachat_rpc_client.pytestplugin" = "deltachat_rpc_client.pytestplugin" diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py b/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py index 720e1ebdbd..11a168a07f 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py @@ -2,4 +2,4 @@ from .contact import Contact from .deltachat import Deltachat from .message import Message -from .rpc import Rpc, new_online_account, start_rpc_server +from .rpc import Rpc, start_rpc_server diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py new file mode 100644 index 0000000000..bddccb0de5 --- /dev/null +++ b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py @@ -0,0 +1,50 @@ +import json +import os +from typing import List + +import aiohttp +import pytest_asyncio + +from .account import Account +from .deltachat import Deltachat +from .rpc import Rpc, start_rpc_server + + +async def get_temp_credentials() -> dict: + url = os.getenv("DCC_NEW_TMP_EMAIL") + assert url, "Failed to get online account, DCC_NEW_TMP_EMAIL is not set" + async with aiohttp.ClientSession() as session: + async with session.post(url) as response: + return json.loads(await response.text()) + + +class ACFactory: + def __init__(self, deltachat: Deltachat) -> None: + self.deltachat = deltachat + + async def new_configured_account(self) -> Account: + credentials = await get_temp_credentials() + account = await self.deltachat.add_account() + assert not await account.is_configured() + await account.set_config("addr", credentials["email"]) + await account.set_config("mail_pw", credentials["password"]) + await account.configure() + assert await account.is_configured() + return account + + async def get_online_accounts(self, num: int) -> List[Account]: + accounts = [await self.new_configured_account() for _ in range(num)] + await self.deltachat.start_io() + return accounts + + +@pytest_asyncio.fixture +async def rpc(tmp_path) -> Rpc: + return await start_rpc_server( + env={**os.environ, "DC_ACCOUNTS_PATH": str(tmp_path / "accounts")} + ) + + +@pytest_asyncio.fixture +async def acfactory(rpc) -> ACFactory: + return ACFactory(Deltachat(rpc)) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py index 22c87ca4aa..abdc33d45a 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py @@ -1,10 +1,7 @@ import asyncio import json -import os from typing import Any, Dict, Optional -import aiohttp - class JsonRpcError(Exception): pass @@ -80,10 +77,3 @@ async def start_rpc_server(*args, **kwargs) -> Rpc: ) rpc = Rpc(proc) return rpc - - -async def new_online_account() -> dict: - url = os.getenv("DCC_NEW_TMP_EMAIL") - async with aiohttp.ClientSession() as session: - async with session.post(url) as response: - return json.loads(await response.text()) diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py index 1744f3de91..b6f10af167 100644 --- a/deltachat-rpc-client/tests/test_something.py +++ b/deltachat-rpc-client/tests/test_something.py @@ -1,18 +1,4 @@ -import asyncio -import os - import pytest -import pytest_asyncio - -import deltachat_rpc_client -from deltachat_rpc_client import Deltachat - - -@pytest_asyncio.fixture -async def rpc(tmp_path): - return await deltachat_rpc_client.start_rpc_server( - env={**os.environ, "DC_ACCOUNTS_PATH": str(tmp_path / "accounts")} - ) @pytest.mark.asyncio @@ -37,16 +23,10 @@ async def test_email_address_validity(rpc): @pytest.mark.asyncio -async def test_online_account(rpc): - account_json = await deltachat_rpc_client.new_online_account() - - account_id = await rpc.add_account() - await rpc.set_config(account_id, "addr", account_json["email"]) - await rpc.set_config(account_id, "mail_pw", account_json["password"]) - - await rpc.configure(account_id) +async def test_acfactory(acfactory): + account = await acfactory.new_configured_account() while True: - event = await rpc.wait_for_event(account_id) + event = await account.wait_for_event() if event["type"] == "ConfigureProgress": # Progress 0 indicates error. assert event["progress"] != 0 @@ -60,22 +40,8 @@ async def test_online_account(rpc): @pytest.mark.asyncio -async def test_object_account(rpc): - deltachat = Deltachat(rpc) - - async def create_configured_account(): - account = await deltachat.add_account() - assert not await account.is_configured() - account_json = await deltachat_rpc_client.new_online_account() - await account.set_config("addr", account_json["email"]) - await account.set_config("mail_pw", account_json["password"]) - await account.configure() - assert await account.is_configured() - return account - - alice, bob = await asyncio.gather( - create_configured_account(), create_configured_account() - ) +async def test_object_account(acfactory): + alice, bob = await acfactory.get_online_accounts(2) alice_contact_bob = await alice.create_contact(await bob.get_config("addr"), "Bob") alice_chat_bob = await alice_contact_bob.create_chat() @@ -88,5 +54,7 @@ async def create_configured_account(): msg_id = event["msgId"] break + rpc = acfactory.deltachat.rpc message = await rpc.get_message(bob.account_id, msg_id) + assert message["chatId"] == chat_id assert message["text"] == "Hello!" From e6ff513aaca1d72bae07d9b4800af7bd22051320 Mon Sep 17 00:00:00 2001 From: adbenitez Date: Thu, 1 Dec 2022 03:15:25 -0500 Subject: [PATCH 14/24] add support for PEP 561 --- deltachat-rpc-client/pyproject.toml | 13 +++++++++++++ .../src/deltachat_rpc_client/py.typed | 1 + 2 files changed, 14 insertions(+) create mode 100644 deltachat-rpc-client/src/deltachat_rpc_client/py.typed diff --git a/deltachat-rpc-client/pyproject.toml b/deltachat-rpc-client/pyproject.toml index acdbeb4044..4b3bd35206 100644 --- a/deltachat-rpc-client/pyproject.toml +++ b/deltachat-rpc-client/pyproject.toml @@ -12,5 +12,18 @@ dependencies = [ dynamic = [ "version" ] + +[tool.setuptools] +# We declare the package not-zip-safe so that our type hints are also available +# when checking client code that uses our (installed) package. +# Ref: +# https://mypy.readthedocs.io/en/stable/installed_packages.html?highlight=zip#using-installed-packages-with-mypy-pep-561 +zip-safe = false + +[tool.setuptools.package-data] +deltachat_rpc_client = [ + "py.typed" +] + [project.entry-points.pytest11] "deltachat_rpc_client.pytestplugin" = "deltachat_rpc_client.pytestplugin" diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/py.typed b/deltachat-rpc-client/src/deltachat_rpc_client/py.typed new file mode 100644 index 0000000000..6b0a9e8986 --- /dev/null +++ b/deltachat-rpc-client/src/deltachat_rpc_client/py.typed @@ -0,0 +1 @@ +# PEP 561 marker file. See https://peps.python.org/pep-0561/ \ No newline at end of file From d17ac9c83c69f7c434744d1ad1cb960bdc2bffa1 Mon Sep 17 00:00:00 2001 From: adbenitez Date: Thu, 1 Dec 2022 03:34:43 -0500 Subject: [PATCH 15/24] add start_rpc_server() doc string --- deltachat-rpc-client/src/deltachat_rpc_client/rpc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py index abdc33d45a..4be830ff5c 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py @@ -68,6 +68,7 @@ async def method(*args, **kwargs) -> Any: async def start_rpc_server(*args, **kwargs) -> Rpc: + """The given arguments will be passed to asyncio.create_subprocess_exec()""" proc = await asyncio.create_subprocess_exec( "deltachat-rpc-server", stdin=asyncio.subprocess.PIPE, From 85b47465167c1e44c840b606aee90b14a5ec02c1 Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 1 Dec 2022 16:36:36 +0000 Subject: [PATCH 16/24] Turn start_rpc_server into a context manager --- deltachat-rpc-client/examples/echobot.py | 80 +++++++++---------- .../src/deltachat_rpc_client/rpc.py | 7 +- deltachat-rpc-client/tests/test_something.py | 5 +- 3 files changed, 49 insertions(+), 43 deletions(-) diff --git a/deltachat-rpc-client/examples/echobot.py b/deltachat-rpc-client/examples/echobot.py index d5b95f3a2c..18ec47d8a4 100755 --- a/deltachat-rpc-client/examples/echobot.py +++ b/deltachat-rpc-client/examples/echobot.py @@ -7,46 +7,46 @@ async def main(): - rpc = await dc.start_rpc_server() - deltachat = dc.Deltachat(rpc) - system_info = await deltachat.get_system_info() - logging.info("Running deltachat core %s", system_info["deltachat_core_version"]) - - accounts = await deltachat.get_all_accounts() - account = accounts[0] if accounts else await deltachat.add_account() - - await account.set_config("bot", "1") - if not await account.is_configured(): - logging.info("Account is not configured, configuring") - await account.set_config("addr", sys.argv[1]) - await account.set_config("mail_pw", sys.argv[2]) - await account.configure() - logging.info("Configured") - else: - logging.info("Account is already configured") - await deltachat.start_io() - - async def process_messages(): - for message in await account.get_fresh_messages_in_arrival_order(): - snapshot = await message.get_snapshot() - if not snapshot.is_info: - await snapshot.chat.send_text(snapshot.text) - await snapshot.message.mark_seen() - - # Process old messages. - await process_messages() - - while True: - event = await account.wait_for_event() - if event["type"] == "Info": - logging.info("%s", event["msg"]) - elif event["type"] == "Warning": - logging.warning("%s", event["msg"]) - elif event["type"] == "Error": - logging.error("%s", event["msg"]) - elif event["type"] == "IncomingMsg": - logging.info("Got an incoming message") - await process_messages() + async with dc.start_rpc_server() as rpc: + deltachat = dc.Deltachat(rpc) + system_info = await deltachat.get_system_info() + logging.info("Running deltachat core %s", system_info["deltachat_core_version"]) + + accounts = await deltachat.get_all_accounts() + account = accounts[0] if accounts else await deltachat.add_account() + + await account.set_config("bot", "1") + if not await account.is_configured(): + logging.info("Account is not configured, configuring") + await account.set_config("addr", sys.argv[1]) + await account.set_config("mail_pw", sys.argv[2]) + await account.configure() + logging.info("Configured") + else: + logging.info("Account is already configured") + await deltachat.start_io() + + async def process_messages(): + for message in await account.get_fresh_messages_in_arrival_order(): + snapshot = await message.get_snapshot() + if not snapshot.is_info: + await snapshot.chat.send_text(snapshot.text) + await snapshot.message.mark_seen() + + # Process old messages. + await process_messages() + + while True: + event = await account.wait_for_event() + if event["type"] == "Info": + logging.info("%s", event["msg"]) + elif event["type"] == "Warning": + logging.warning("%s", event["msg"]) + elif event["type"] == "Error": + logging.error("%s", event["msg"]) + elif event["type"] == "IncomingMsg": + logging.info("Got an incoming message") + await process_messages() if __name__ == "__main__": diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py index efbaa0b80d..17d508e2a0 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py @@ -2,6 +2,7 @@ import json import logging import os +from contextlib import asynccontextmanager import aiohttp @@ -73,6 +74,7 @@ async def method(*args, **kwargs): return method +@asynccontextmanager async def start_rpc_server(*args, **kwargs): proc = await asyncio.create_subprocess_exec( "deltachat-rpc-server", @@ -82,7 +84,10 @@ async def start_rpc_server(*args, **kwargs): **kwargs ) rpc = Rpc(proc) - return rpc + try: + yield rpc + finally: + proc.terminate() async def new_online_account(): diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py index 1744f3de91..902c3409e3 100644 --- a/deltachat-rpc-client/tests/test_something.py +++ b/deltachat-rpc-client/tests/test_something.py @@ -10,9 +10,10 @@ @pytest_asyncio.fixture async def rpc(tmp_path): - return await deltachat_rpc_client.start_rpc_server( + async with deltachat_rpc_client.start_rpc_server( env={**os.environ, "DC_ACCOUNTS_PATH": str(tmp_path / "accounts")} - ) + ) as rpc: + yield rpc @pytest.mark.asyncio From 29a4404257af40e56b3aa3820e3a58145d01972f Mon Sep 17 00:00:00 2001 From: adbenitez Date: Thu, 1 Dec 2022 20:33:05 -0500 Subject: [PATCH 17/24] enably type-checking in tests --- deltachat-rpc-client/tests/test_something.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py index b6f10af167..bfce9107ea 100644 --- a/deltachat-rpc-client/tests/test_something.py +++ b/deltachat-rpc-client/tests/test_something.py @@ -2,14 +2,14 @@ @pytest.mark.asyncio -async def test_system_info(rpc): +async def test_system_info(rpc) -> None: system_info = await rpc.get_system_info() assert "arch" in system_info assert "deltachat_core_version" in system_info @pytest.mark.asyncio -async def test_email_address_validity(rpc): +async def test_email_address_validity(rpc) -> None: valid_addresses = [ "email@example.com", "36aa165ae3406424e0c61af17700f397cad3fe8ab83d682d0bddf3338a5dd52e@yggmail@yggmail", @@ -23,7 +23,7 @@ async def test_email_address_validity(rpc): @pytest.mark.asyncio -async def test_acfactory(acfactory): +async def test_acfactory(acfactory) -> None: account = await acfactory.new_configured_account() while True: event = await account.wait_for_event() @@ -40,7 +40,7 @@ async def test_acfactory(acfactory): @pytest.mark.asyncio -async def test_object_account(acfactory): +async def test_object_account(acfactory) -> None: alice, bob = await acfactory.get_online_accounts(2) alice_contact_bob = await alice.create_contact(await bob.get_config("addr"), "Bob") From 98b6b5e3f691f20433d8c1be289a6b62d1ab93c6 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 3 Dec 2022 18:37:02 +0000 Subject: [PATCH 18/24] Update instructions on using ipython --- deltachat-rpc-client/README.md | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/deltachat-rpc-client/README.md b/deltachat-rpc-client/README.md index 6b38c361ab..559806391e 100644 --- a/deltachat-rpc-client/README.md +++ b/deltachat-rpc-client/README.md @@ -17,17 +17,24 @@ Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not ca ## Using in REPL +Setup a development environment: +``` +$ tox --devenv env +$ . env/bin/activate +``` + It is recommended to use IPython, because it supports using `await` directly from the REPL. ``` -PATH="../target/debug:$PATH" ipython +$ pip install ipython +$ PATH="../target/debug:$PATH" ipython ... In [1]: from deltachat_rpc_client import * -In [2]: dc = Deltachat(await start_rpc_server()) -In [3]: await dc.get_all_accounts() -Out [3]: [] -In [4]: alice = await dc.add_account() -In [5]: (await alice.get_info())["journal_mode"] -Out [5]: 'wal' +In [2]: rpc_generator = start_rpc_server() +In [3]: rpc = await rpc_generator.__aenter__() +In [4]: dc = Deltachat(rpc) +In [5]: system_info = await dc.get_system_info() +In [6]: system_info["level"] +Out [6]: 'awesome' ``` From 5a3065344e4e8f7cf9a2a542468ac33c5fcc5b51 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 3 Dec 2022 23:30:50 +0000 Subject: [PATCH 19/24] Properly terminate Rpc and remove sleep() hack --- .../src/deltachat_rpc_client/pytestplugin.py | 1 - deltachat-rpc-client/src/deltachat_rpc_client/rpc.py | 7 ++++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py index b8929ed296..bda51cdd54 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py @@ -44,7 +44,6 @@ async def rpc(tmp_path) -> AsyncGenerator: env = {**os.environ, "DC_ACCOUNTS_PATH": str(tmp_path / "accounts")} async with start_rpc_server(env=env) as rpc: yield rpc - await asyncio.sleep(0.1) # avoid RuntimeError: Event loop is closed @pytest_asyncio.fixture diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py index b0cc365288..bd42044a59 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py @@ -37,6 +37,11 @@ async def reader_loop(self) -> None: else: print(response) + async def close(self) -> None: + """Terminate RPC server process and wait until the reader loop finishes.""" + self.process.terminate() + await self.reader_task + async def wait_for_event(self, account_id: int) -> Optional[dict]: """Waits for the next event from the given account and returns it.""" if account_id in self.event_queues: @@ -84,4 +89,4 @@ async def start_rpc_server(*args, **kwargs) -> AsyncGenerator: try: yield rpc finally: - proc.terminate() + await rpc.close() From 2ccf39800db78daea053ab8f4c53072dd6595d6f Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 4 Dec 2022 00:02:32 +0000 Subject: [PATCH 20/24] Remove start_rpc_server() and make Rpc a context manager Rpc now has a start() method. This way it is possible to use Rpc from IPython without calling __aenter__() --- deltachat-rpc-client/README.md | 15 +++--- deltachat-rpc-client/examples/echobot.py | 2 +- .../src/deltachat_rpc_client/__init__.py | 2 +- .../src/deltachat_rpc_client/pytestplugin.py | 5 +- .../src/deltachat_rpc_client/rpc.py | 50 +++++++++---------- 5 files changed, 38 insertions(+), 36 deletions(-) diff --git a/deltachat-rpc-client/README.md b/deltachat-rpc-client/README.md index 559806391e..647dec7c24 100644 --- a/deltachat-rpc-client/README.md +++ b/deltachat-rpc-client/README.md @@ -30,11 +30,12 @@ from the REPL. $ pip install ipython $ PATH="../target/debug:$PATH" ipython ... -In [1]: from deltachat_rpc_client import * -In [2]: rpc_generator = start_rpc_server() -In [3]: rpc = await rpc_generator.__aenter__() -In [4]: dc = Deltachat(rpc) -In [5]: system_info = await dc.get_system_info() -In [6]: system_info["level"] -Out [6]: 'awesome' +In [1]: from deltachat_rpc_client import * +In [2]: rpc = Rpc() +In [3]: await rpc.start() +In [4]: dc = Deltachat(rpc) +In [5]: system_info = await dc.get_system_info() +In [6]: system_info["level"] +Out[6]: 'awesome' +In [7]: await rpc.close() ``` diff --git a/deltachat-rpc-client/examples/echobot.py b/deltachat-rpc-client/examples/echobot.py index 18ec47d8a4..136b657959 100755 --- a/deltachat-rpc-client/examples/echobot.py +++ b/deltachat-rpc-client/examples/echobot.py @@ -7,7 +7,7 @@ async def main(): - async with dc.start_rpc_server() as rpc: + async with dc.Rpc() as rpc: deltachat = dc.Deltachat(rpc) system_info = await deltachat.get_system_info() logging.info("Running deltachat core %s", system_info["deltachat_core_version"]) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py b/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py index 11a168a07f..4b25df47da 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py @@ -2,4 +2,4 @@ from .contact import Contact from .deltachat import Deltachat from .message import Message -from .rpc import Rpc, start_rpc_server +from .rpc import Rpc diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py index bda51cdd54..a3eb4341c3 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py @@ -8,7 +8,7 @@ from .account import Account from .deltachat import Deltachat -from .rpc import start_rpc_server +from .rpc import Rpc async def get_temp_credentials() -> dict: @@ -42,7 +42,8 @@ async def get_online_accounts(self, num: int) -> List[Account]: @pytest_asyncio.fixture async def rpc(tmp_path) -> AsyncGenerator: env = {**os.environ, "DC_ACCOUNTS_PATH": str(tmp_path / "accounts")} - async with start_rpc_server(env=env) as rpc: + rpc_server = Rpc(env=env) + async with rpc_server as rpc: yield rpc diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py index bd42044a59..d679770bc7 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py @@ -1,6 +1,5 @@ import asyncio import json -from contextlib import asynccontextmanager from typing import Any, AsyncGenerator, Dict, Optional @@ -9,8 +8,19 @@ class JsonRpcError(Exception): class Rpc: - def __init__(self, process: asyncio.subprocess.Process) -> None: - self.process = process + def __init__(self, *args, **kwargs): + """The given arguments will be passed to asyncio.create_subprocess_exec()""" + self.args = args + self.kwargs = kwargs + + async def start(self) -> None: + self.process = await asyncio.create_subprocess_exec( + "deltachat-rpc-server", + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + *self.args, + **self.kwargs + ) self.event_queues: Dict[int, asyncio.Queue] = {} self.id = 0 self.reader_task = asyncio.create_task(self.reader_loop()) @@ -18,6 +28,18 @@ def __init__(self, process: asyncio.subprocess.Process) -> None: # Map from request ID to `asyncio.Future` returning the response. self.request_events: Dict[int, asyncio.Future] = {} + async def close(self) -> None: + """Terminate RPC server process and wait until the reader loop finishes.""" + self.process.terminate() + await self.reader_task + + async def __aenter__(self): + await self.start() + return self + + async def __aexit__(self, exc_type, exc, tb): + await self.close() + async def reader_loop(self) -> None: while True: line = await self.process.stdout.readline() @@ -37,11 +59,6 @@ async def reader_loop(self) -> None: else: print(response) - async def close(self) -> None: - """Terminate RPC server process and wait until the reader loop finishes.""" - self.process.terminate() - await self.reader_task - async def wait_for_event(self, account_id: int) -> Optional[dict]: """Waits for the next event from the given account and returns it.""" if account_id in self.event_queues: @@ -73,20 +90,3 @@ async def method(*args, **kwargs) -> Any: return response["result"] return method - - -@asynccontextmanager -async def start_rpc_server(*args, **kwargs) -> AsyncGenerator: - """The given arguments will be passed to asyncio.create_subprocess_exec()""" - proc = await asyncio.create_subprocess_exec( - "deltachat-rpc-server", - stdin=asyncio.subprocess.PIPE, - stdout=asyncio.subprocess.PIPE, - *args, - **kwargs - ) - rpc = Rpc(proc) - try: - yield rpc - finally: - await rpc.close() From 3cdbe213a36c793af37ee48ef73fb1565e00208b Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 4 Dec 2022 13:56:53 +0000 Subject: [PATCH 21/24] python: rename Deltachat class into DeltaChat --- deltachat-rpc-client/README.md | 2 +- deltachat-rpc-client/examples/echobot.py | 2 +- deltachat-rpc-client/src/deltachat_rpc_client/__init__.py | 2 +- deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py | 2 +- .../src/deltachat_rpc_client/pytestplugin.py | 6 +++--- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/deltachat-rpc-client/README.md b/deltachat-rpc-client/README.md index 647dec7c24..0973244f41 100644 --- a/deltachat-rpc-client/README.md +++ b/deltachat-rpc-client/README.md @@ -33,7 +33,7 @@ $ PATH="../target/debug:$PATH" ipython In [1]: from deltachat_rpc_client import * In [2]: rpc = Rpc() In [3]: await rpc.start() -In [4]: dc = Deltachat(rpc) +In [4]: dc = DeltaChat(rpc) In [5]: system_info = await dc.get_system_info() In [6]: system_info["level"] Out[6]: 'awesome' diff --git a/deltachat-rpc-client/examples/echobot.py b/deltachat-rpc-client/examples/echobot.py index 136b657959..3ca4f6bf85 100755 --- a/deltachat-rpc-client/examples/echobot.py +++ b/deltachat-rpc-client/examples/echobot.py @@ -8,7 +8,7 @@ async def main(): async with dc.Rpc() as rpc: - deltachat = dc.Deltachat(rpc) + deltachat = dc.DeltaChat(rpc) system_info = await deltachat.get_system_info() logging.info("Running deltachat core %s", system_info["deltachat_core_version"]) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py b/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py index 4b25df47da..560d4b7d1c 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py @@ -1,5 +1,5 @@ from .account import Account from .contact import Contact -from .deltachat import Deltachat +from .deltachat import DeltaChat from .message import Message from .rpc import Rpc diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py b/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py index 5a9a1ec804..2414fbb94a 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py @@ -4,7 +4,7 @@ from .rpc import Rpc -class Deltachat: +class DeltaChat: """ Delta Chat account manager. This is the root of the object oriented API. diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py index a3eb4341c3..0e1be118bc 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py @@ -7,7 +7,7 @@ import pytest_asyncio from .account import Account -from .deltachat import Deltachat +from .deltachat import DeltaChat from .rpc import Rpc @@ -20,7 +20,7 @@ async def get_temp_credentials() -> dict: class ACFactory: - def __init__(self, deltachat: Deltachat) -> None: + def __init__(self, deltachat: DeltaChat) -> None: self.deltachat = deltachat async def new_configured_account(self) -> Account: @@ -49,4 +49,4 @@ async def rpc(tmp_path) -> AsyncGenerator: @pytest_asyncio.fixture async def acfactory(rpc) -> AsyncGenerator: - yield ACFactory(Deltachat(rpc)) + yield ACFactory(DeltaChat(rpc)) From bad5a1de558a730615ef632d2407a4017763e8bd Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 4 Dec 2022 13:57:38 +0000 Subject: [PATCH 22/24] Ignore .tox everywhere, not only in python/ --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 17d3f67c8f..ff914ab6b4 100644 --- a/.gitignore +++ b/.gitignore @@ -12,8 +12,8 @@ include *.db *.db-blobs +.tox python/.eggs -python/.tox *.egg-info __pycache__ python/src/deltachat/capi*.so From ee19789cac8a017506493767e620a8fd4ca8403d Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 4 Dec 2022 14:03:43 +0000 Subject: [PATCH 23/24] Make _rpc private --- .../src/deltachat_rpc_client/account.py | 38 +++++++++---------- .../src/deltachat_rpc_client/chat.py | 18 ++++----- .../src/deltachat_rpc_client/contact.py | 18 ++++----- .../src/deltachat_rpc_client/message.py | 14 +++---- 4 files changed, 44 insertions(+), 44 deletions(-) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/account.py b/deltachat-rpc-client/src/deltachat_rpc_client/account.py index 5c6ec0bc97..ff339e99ff 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/account.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/account.py @@ -7,7 +7,7 @@ class Account: def __init__(self, rpc, account_id) -> None: - self.rpc = rpc + self._rpc = rpc self.account_id = account_id def __repr__(self) -> str: @@ -15,53 +15,53 @@ def __repr__(self) -> str: async def wait_for_event(self) -> dict: """Wait until the next event and return it.""" - return await self.rpc.wait_for_event(self.account_id) + return await self._rpc.wait_for_event(self.account_id) async def remove(self) -> None: """Remove the account.""" - await self.rpc.remove_account(self.account_id) + await self._rpc.remove_account(self.account_id) async def start_io(self) -> None: """Start the account I/O.""" - await self.rpc.start_io(self.account_id) + await self._rpc.start_io(self.account_id) async def stop_io(self) -> None: """Stop the account I/O.""" - await self.rpc.stop_io(self.account_id) + await self._rpc.stop_io(self.account_id) async def get_info(self) -> dict: - return await self.rpc.get_info(self.account_id) + return await self._rpc.get_info(self.account_id) async def get_file_size(self) -> int: - return await self.rpc.get_account_file_size(self.account_id) + return await self._rpc.get_account_file_size(self.account_id) async def is_configured(self) -> bool: """Return True for configured accounts.""" - return await self.rpc.is_configured(self.account_id) + return await self._rpc.is_configured(self.account_id) async def set_config(self, key: str, value: Optional[str] = None) -> None: """Set the configuration value key pair.""" - await self.rpc.set_config(self.account_id, key, value) + await self._rpc.set_config(self.account_id, key, value) async def get_config(self, key: str) -> Optional[str]: """Get the configuration value.""" - return await self.rpc.get_config(self.account_id, key) + return await self._rpc.get_config(self.account_id, key) async def configure(self) -> None: """Configure an account.""" - await self.rpc.configure(self.account_id) + await self._rpc.configure(self.account_id) async def create_contact(self, address: str, name: Optional[str] = None) -> Contact: """Create a contact with the given address and, optionally, a name.""" return Contact( - self.rpc, + self._rpc, self.account_id, - await self.rpc.create_contact(self.account_id, address, name), + await self._rpc.create_contact(self.account_id, address, name), ) async def secure_join(self, qrdata: str) -> Chat: - chat_id = await self.rpc.secure_join(self.account_id, qrdata) - return Chat(self.rpc, self.account_id, chat_id) + chat_id = await self._rpc.secure_join(self.account_id, qrdata) + return Chat(self._rpc, self.account_id, chat_id) async def get_fresh_messages(self) -> List[Message]: """Return the list of fresh messages, newest messages first. @@ -70,10 +70,10 @@ async def get_fresh_messages(self) -> List[Message]: If you are writing a bot, use get_fresh_messages_in_arrival_order instead, to process oldest messages first. """ - fresh_msg_ids = await self.rpc.get_fresh_msgs(self.account_id) - return [Message(self.rpc, self.account_id, msg_id) for msg_id in fresh_msg_ids] + fresh_msg_ids = await self._rpc.get_fresh_msgs(self.account_id) + return [Message(self._rpc, self.account_id, msg_id) for msg_id in fresh_msg_ids] async def get_fresh_messages_in_arrival_order(self) -> List[Message]: """Return fresh messages list sorted in the order of their arrival, with ascending IDs.""" - fresh_msg_ids = sorted(await self.rpc.get_fresh_msgs(self.account_id)) - return [Message(self.rpc, self.account_id, msg_id) for msg_id in fresh_msg_ids] + fresh_msg_ids = sorted(await self._rpc.get_fresh_msgs(self.account_id)) + return [Message(self._rpc, self.account_id, msg_id) for msg_id in fresh_msg_ids] diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/chat.py b/deltachat-rpc-client/src/deltachat_rpc_client/chat.py index bd5881c770..ed8561afa7 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/chat.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/chat.py @@ -8,34 +8,34 @@ class Chat: def __init__(self, rpc: Rpc, account_id: int, chat_id: int) -> None: - self.rpc = rpc + self._rpc = rpc self.account_id = account_id self.chat_id = chat_id async def block(self) -> None: """Block the chat.""" - await self.rpc.block_chat(self.account_id, self.chat_id) + await self._rpc.block_chat(self.account_id, self.chat_id) async def accept(self) -> None: """Accept the contact request.""" - await self.rpc.accept_chat(self.account_id, self.chat_id) + await self._rpc.accept_chat(self.account_id, self.chat_id) async def delete(self) -> None: - await self.rpc.delete_chat(self.account_id, self.chat_id) + await self._rpc.delete_chat(self.account_id, self.chat_id) async def get_encryption_info(self) -> str: - return await self.rpc.get_chat_encryption_info(self.account_id, self.chat_id) + return await self._rpc.get_chat_encryption_info(self.account_id, self.chat_id) async def send_text(self, text: str) -> "Message": from .message import Message - msg_id = await self.rpc.misc_send_text_message( + msg_id = await self._rpc.misc_send_text_message( self.account_id, self.chat_id, text ) - return Message(self.rpc, self.account_id, msg_id) + return Message(self._rpc, self.account_id, msg_id) async def leave(self) -> None: - await self.rpc.leave_group(self.account_id, self.chat_id) + await self._rpc.leave_group(self.account_id, self.chat_id) async def get_fresh_message_count(self) -> int: - return await self.rpc.get_fresh_msg_cnt(self.account_id, self.chat_id) + return await self._rpc.get_fresh_msg_cnt(self.account_id, self.chat_id) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/contact.py b/deltachat-rpc-client/src/deltachat_rpc_client/contact.py index 15622ec854..2e9154ef19 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/contact.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/contact.py @@ -14,39 +14,39 @@ class Contact: """ def __init__(self, rpc: Rpc, account_id: int, contact_id: int) -> None: - self.rpc = rpc + self._rpc = rpc self.account_id = account_id self.contact_id = contact_id async def block(self) -> None: """Block contact.""" - await self.rpc.block_contact(self.account_id, self.contact_id) + await self._rpc.block_contact(self.account_id, self.contact_id) async def unblock(self) -> None: """Unblock contact.""" - await self.rpc.unblock_contact(self.account_id, self.contact_id) + await self._rpc.unblock_contact(self.account_id, self.contact_id) async def delete(self) -> None: """Delete contact.""" - await self.rpc.delete_contact(self.account_id, self.contact_id) + await self._rpc.delete_contact(self.account_id, self.contact_id) async def change_name(self, name: str) -> None: - await self.rpc.change_contact_name(self.account_id, self.contact_id, name) + await self._rpc.change_contact_name(self.account_id, self.contact_id, name) async def get_encryption_info(self) -> str: - return await self.rpc.get_contact_encryption_info( + return await self._rpc.get_contact_encryption_info( self.account_id, self.contact_id ) async def get_dictionary(self) -> dict: """Return a dictionary with a snapshot of all contact properties.""" - return await self.rpc.get_contact(self.account_id, self.contact_id) + return await self._rpc.get_contact(self.account_id, self.contact_id) async def create_chat(self) -> "Chat": from .chat import Chat return Chat( - self.rpc, + self._rpc, self.account_id, - await self.rpc.create_chat_by_contact_id(self.account_id, self.contact_id), + await self._rpc.create_chat_by_contact_id(self.account_id, self.contact_id), ) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/message.py b/deltachat-rpc-client/src/deltachat_rpc_client/message.py index 8575a0444c..67d90227cd 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/message.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/message.py @@ -8,20 +8,20 @@ class Message: def __init__(self, rpc: Rpc, account_id: int, msg_id: int) -> None: - self.rpc = rpc + self._rpc = rpc self.account_id = account_id self.msg_id = msg_id async def send_reaction(self, reactions: str) -> "Message": - msg_id = await self.rpc.send_reaction(self.account_id, self.msg_id, reactions) - return Message(self.rpc, self.account_id, msg_id) + msg_id = await self._rpc.send_reaction(self.account_id, self.msg_id, reactions) + return Message(self._rpc, self.account_id, msg_id) async def get_snapshot(self) -> "MessageSnapshot": - message_object = await self.rpc.get_message(self.account_id, self.msg_id) + message_object = await self._rpc.get_message(self.account_id, self.msg_id) return MessageSnapshot( message=self, - chat=Chat(self.rpc, self.account_id, message_object["chatId"]), - sender=Contact(self.rpc, self.account_id, message_object["fromId"]), + chat=Chat(self._rpc, self.account_id, message_object["chatId"]), + sender=Contact(self._rpc, self.account_id, message_object["fromId"]), text=message_object["text"], error=message_object.get("error"), is_info=message_object["isInfo"], @@ -29,7 +29,7 @@ async def get_snapshot(self) -> "MessageSnapshot": async def mark_seen(self) -> None: """Mark the message as seen.""" - await self.rpc.markseen_msgs(self.account_id, [self.msg_id]) + await self._rpc.markseen_msgs(self.account_id, [self.msg_id]) @dataclass From 5502bff9865d0a2d9be767c9590738d0fccea5bb Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 4 Dec 2022 14:03:59 +0000 Subject: [PATCH 24/24] Make _args and _kwargs private in Rpc --- deltachat-rpc-client/src/deltachat_rpc_client/rpc.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py index d679770bc7..174a54e2d5 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py @@ -10,16 +10,16 @@ class JsonRpcError(Exception): class Rpc: def __init__(self, *args, **kwargs): """The given arguments will be passed to asyncio.create_subprocess_exec()""" - self.args = args - self.kwargs = kwargs + self._args = args + self._kwargs = kwargs async def start(self) -> None: self.process = await asyncio.create_subprocess_exec( "deltachat-rpc-server", stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, - *self.args, - **self.kwargs + *self._args, + **self._kwargs ) self.event_queues: Dict[int, asyncio.Queue] = {} self.id = 0