From e851a46c77350ddce4d21d1112c931438579f131 Mon Sep 17 00:00:00 2001 From: Nico Alt <nicoalt@posteo.org> Date: Thu, 3 Sep 2020 14:00:00 +0000 Subject: [PATCH] Add docstrings to everything --- briar_wrapper/__init__.py | 10 +++- briar_wrapper/api.py | 52 +++++++++++++++++++ briar_wrapper/api_thread.py | 27 ++++++++++ briar_wrapper/constants.py | 17 ++++++ briar_wrapper/model.py | 6 +++ briar_wrapper/models/__init__.py | 6 +++ briar_wrapper/models/contacts.py | 50 +++++++++++++++--- briar_wrapper/models/private_chat.py | 28 ++++++++-- briar_wrapper/models/socket_listener.py | 31 +++++++++-- setup.cfg | 2 + tests/briar_wrapper/models/test_contacts.py | 2 +- .../models/test_socket_listener.py | 36 ++++++------- tests/conftest.py | 2 +- 13 files changed, 236 insertions(+), 33 deletions(-) create mode 100644 briar_wrapper/models/__init__.py create mode 100644 setup.cfg diff --git a/briar_wrapper/__init__.py b/briar_wrapper/__init__.py index b72a85a..356d837 100644 --- a/briar_wrapper/__init__.py +++ b/briar_wrapper/__init__.py @@ -2,6 +2,14 @@ # SPDX-License-Identifier: AGPL-3.0-only # License-Filename: LICENSE.md -"""Wrapper for the Briar Headless REST API""" +""" +Wrapper for the Briar Headless REST API + +Before using `briar_wrapper` you need to initialize an instance of +`briar_wrapper.api.Api`. You can check with `briar_wrapper.api.Api.has_account` +whether you want want to `briar_wrapper.api.Api.login` or +`briar_wrapper.api.Api.register`. Now you can start to use the wrappers in + `briar_wrapper.models`. +""" __version__ = "0.0.3" diff --git a/briar_wrapper/api.py b/briar_wrapper/api.py index 9ab7457..d20de97 100644 --- a/briar_wrapper/api.py +++ b/briar_wrapper/api.py @@ -1,6 +1,9 @@ # Copyright (c) 2019 Nico Alt # SPDX-License-Identifier: AGPL-3.0-only # License-Filename: LICENSE.md +""" +Central API wrapper handling login and registration +""" import os from threading import Thread @@ -12,28 +15,66 @@ from briar_wrapper.models.socket_listener import SocketListener class Api: + # pylint: disable=line-too-long auth_token = None + """ + Briar's authentication token + + [Upstream documentation](https://code.briarproject.org/briar/briar/blob/master/briar-headless/README.md#how-to-use) + """ + socket_listener = None + """ + `briar_wrapper.api.Api`'s instance of + `briar_wrapper.models.socket_listener.SocketListener` + """ _api_thread = None def __init__(self, headless_jar): + """ + Initialize with path to Briar Headless JAR `headless_jar` + """ self._api_thread = ApiThread(self, headless_jar) @staticmethod def has_account(): + """ + Checks if `briar_wrapper.constants.BRIAR_DB` exists + + .. versionadded:: 0.0.3 + """ return os.path.isfile(BRIAR_DB) def is_running(self): + """ + Returns `True` if `briar_wrapper.api_thread.ApiThread` is running + + .. versionadded:: 0.0.3 + """ return self._api_thread.is_running() def login(self, password, callback): + """ + Login to Briar API with `password`. + + Calls `callback` once login process finished. + + .. versionadded:: 0.0.3 + """ self._start_and_watch(callback) startup_thread = Thread(target=self._api_thread.login, args=(password,), daemon=True) startup_thread.start() def register(self, credentials, callback): + """ + Register at Briar API with 2-tuple `credentials`. + + Calls `callback` once registration process finished. + + .. versionadded:: 0.0.3 + """ if len(credentials) != 2: raise Exception("Can't process credentials") self._start_and_watch(callback) @@ -42,6 +83,11 @@ class Api: startup_thread.start() def stop(self): + """ + Stops API wrapper + + .. versionadded:: 0.0.3 + """ self._api_thread.stop() def _start_and_watch(self, callback): @@ -49,6 +95,12 @@ class Api: self._api_thread.watch(callback) def on_successful_startup(self, callback): + """ + Called by `briar_wrapper.api_thread.ApiThread` if startup finished + successfully. + + Should not be called from outside `briar_wrapper`. + """ self._load_auth_token() self.socket_listener = SocketListener(self) callback(True) diff --git a/briar_wrapper/api_thread.py b/briar_wrapper/api_thread.py index 18b5928..2a9b32d 100644 --- a/briar_wrapper/api_thread.py +++ b/briar_wrapper/api_thread.py @@ -1,6 +1,11 @@ # Copyright (c) 2020 Nico Alt # SPDX-License-Identifier: AGPL-3.0-only # License-Filename: LICENSE.md +""" +Handles background thread for `briar_wrapper.api.Api` + +**Should not be called from outside `briar_wrapper`.** +""" from subprocess import Popen, PIPE, STDOUT from threading import Thread @@ -17,29 +22,48 @@ class ApiThread: _process = None def __init__(self, api, headless_jar): + """ + Initialize with `briar_wrapper.api.Api` instance `api` and + path to Briar Headless JAR `headless_jar` + """ self._api = api self._command = ["java", "-jar", headless_jar] def is_running(self): + """ + Returns `True` if background thread is runnning + """ return (self._process is not None) and (self._process.poll() is None) def start(self): + """ + Starts background thread + """ if self.is_running(): raise Exception("API already running") self._process = Popen(self._command, stdin=PIPE, stdout=PIPE, stderr=STDOUT) def watch(self, callback): + """ + Watches startup of background thread and calls `callback` once finished + """ watch_thread = Thread(target=self._watch_thread, args=(callback,), daemon=True) watch_thread.start() def login(self, password): + """ + Actually logins to Briar API with `password` + """ if not self.is_running(): raise Exception("Can't login; API not running") self._process.communicate((f"{password}\n").encode("utf-8")) def register(self, credentials): + """ + Actually registers new account at Briar API with 2-tuple `credentials` + """ if not self.is_running(): raise Exception("Can't register; API not running") self._process.communicate((credentials[0] + '\n' + @@ -47,6 +71,9 @@ class ApiThread: credentials[1] + '\n').encode("utf-8")) def stop(self): + """ + Stops background thread + """ if not self.is_running(): raise Exception("Nothing to stop") self._process.terminate() diff --git a/briar_wrapper/constants.py b/briar_wrapper/constants.py index 34dd83f..06ac4f0 100644 --- a/briar_wrapper/constants.py +++ b/briar_wrapper/constants.py @@ -1,6 +1,9 @@ # Copyright (c) 2019 Nico Alt # SPDX-License-Identifier: AGPL-3.0-only # License-Filename: LICENSE.md +""" +Constants used in `briar_wrapper` +""" from os.path import join from pathlib import Path @@ -14,7 +17,21 @@ _HOST = "%s://localhost:7000" _VERSION_SUFFIX = "v1/" BRIAR_AUTH_TOKEN = join(_BRIAR_DIR, "auth_token") +""" +Path to Briar's authentication token +""" + BRIAR_DB = join(_BRIAR_DIR, "db", "db.mv.db") +""" +Path to Briar's database +""" BASE_HTTP_URL = urljoin(_HOST % "http", _VERSION_SUFFIX) +""" +Base URL to construct resource's URLs +""" + WEBSOCKET_URL = urljoin(_HOST % "ws", "%s/ws" % _VERSION_SUFFIX) +""" +Websocket URL used in `briar_wrapper.models.socket_listener.SocketListener` +""" diff --git a/briar_wrapper/model.py b/briar_wrapper/model.py index f432c2b..cd53777 100644 --- a/briar_wrapper/model.py +++ b/briar_wrapper/model.py @@ -1,6 +1,9 @@ # Copyright (c) 2019 Nico Alt # SPDX-License-Identifier: AGPL-3.0-only # License-Filename: LICENSE.md +""" +Abstract base class for all `briar_wrapper.models` +""" class Model: # pylint: disable=too-few-public-methods @@ -8,6 +11,9 @@ class Model: # pylint: disable=too-few-public-methods _headers = {} def __init__(self, api): + """ + Initialize with `briar_wrapper.api.Api` instance `api` + """ self._api = api self._initialize_headers() diff --git a/briar_wrapper/models/__init__.py b/briar_wrapper/models/__init__.py new file mode 100644 index 0000000..c7175c7 --- /dev/null +++ b/briar_wrapper/models/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) 2020 Nico Alt +# SPDX-License-Identifier: AGPL-3.0-only +# License-Filename: LICENSE.md +""" +Contains wrappers around different resources +""" diff --git a/briar_wrapper/models/contacts.py b/briar_wrapper/models/contacts.py index 81d6304..dc88afb 100644 --- a/briar_wrapper/models/contacts.py +++ b/briar_wrapper/models/contacts.py @@ -1,6 +1,9 @@ # Copyright (c) 2019 Nico Alt # SPDX-License-Identifier: AGPL-3.0-only # License-Filename: LICENSE.md +""" +Wrapper around Briar API's _/contacts/_ resource +""" from operator import itemgetter from urllib.parse import urljoin @@ -14,32 +17,66 @@ from briar_wrapper.model import Model class Contacts(Model): - API_ENDPOINT = "contacts/" - CONNECTION_EVENTS = ("ContactConnectedEvent", "ContactDisconnectedEvent") + _API_ENDPOINT = "contacts/" + _CONNECTION_EVENTS = ("ContactConnectedEvent", "ContactDisconnectedEvent") _connections_callback = None def add_pending(self, link, alias): - url = urljoin(BASE_HTTP_URL, self.API_ENDPOINT + "add/pending/") + # pylint: disable=line-too-long + """ + + Adds pending contact to Briar with `link` URL and `alias` + + [Upstream documentation](https://code.briarproject.org/briar/briar/blob/master/briar-headless/README.md#adding-a-contact) + + .. versionadded:: 0.0.3 + """ + url = urljoin(BASE_HTTP_URL, self._API_ENDPOINT + "add/pending/") _post(url, headers=self._headers, json={"link": link, "alias": alias}) def get(self): - url = urljoin(BASE_HTTP_URL, self.API_ENDPOINT) + # pylint: disable=line-too-long + """ + Returns sorted list containing all contacts + + [Upstream documentation](https://code.briarproject.org/briar/briar/blob/master/briar-headless/README.md#listing-all-contacts) + + .. versionadded:: 0.0.3 + .. versionchanged:: 0.0.4 + """ + url = urljoin(BASE_HTTP_URL, self._API_ENDPOINT) request = _get(url, headers=self._headers) contacts = request.json() contacts = Contacts._sort_contact_list(contacts) return contacts def get_link(self): - url = urljoin(BASE_HTTP_URL, self.API_ENDPOINT + "add/link/") + # pylint: disable=line-too-long + """ + Returns _briar://_ link + + [Upstream documentation](https://code.briarproject.org/briar/briar/blob/master/briar-headless/README.md#adding-a-contact) + + .. versionadded:: 0.0.3 + """ + url = urljoin(BASE_HTTP_URL, self._API_ENDPOINT + "add/link/") request = _get(url, headers=self._headers).json() return request['link'] def watch_connections(self, callback): + # pylint: disable=line-too-long + """ + Calls `callback` whenever a contact's connection status changes + + [Upstream documentation](https://code.briarproject.org/briar/briar/blob/master/briar-headless/README.md#a-contact-connected-or-disconnected) + + .. versionadded:: 0.0.4 + """ self._connections_callback = callback signal_ids = list() event_callback = self._handle_connections_callback - for event in self.CONNECTION_EVENTS: + for event in self._CONNECTION_EVENTS: signal_id = self._api.socket_listener.connect(event, event_callback) signal_ids.append(signal_id) @@ -54,6 +91,7 @@ class Contacts(Model): else: raise Exception(f"Wrong event in callback: {message['name']}") + @staticmethod def _sort_contact_list(contacts): contacts.sort(key=itemgetter("lastChatActivity"), reverse=True) diff --git a/briar_wrapper/models/private_chat.py b/briar_wrapper/models/private_chat.py index 129cc2f..d9bf4fe 100644 --- a/briar_wrapper/models/private_chat.py +++ b/briar_wrapper/models/private_chat.py @@ -1,6 +1,9 @@ # Copyright (c) 2019 Nico Alt # SPDX-License-Identifier: AGPL-3.0-only # License-Filename: LICENSE.md +""" +Wrapper around Briar API's _/messages/_ resource +""" from urllib.parse import urljoin @@ -12,19 +15,38 @@ from briar_wrapper.model import Model class PrivateChat(Model): - API_ENDPOINT = "messages/" + _API_ENDPOINT = "messages/" def __init__(self, api, contact_id): + """ + Initialize with `briar_wrapper.api.Api` instance `api` and `contact_id` + """ super().__init__(api) self._contact_id = contact_id def get(self): + # pylint: disable=line-too-long + """ + Returns list containing all messages from contact + + [Upstream documentation](https://code.briarproject.org/briar/briar/blob/master/briar-headless/README.md#listing-all-private-messages) + + .. versionadded:: 0.0.3 + """ url = urljoin(BASE_HTTP_URL, - self.API_ENDPOINT + str(self._contact_id)) + self._API_ENDPOINT + str(self._contact_id)) request = requests.get(url, headers=self._headers) return request.json() def send(self, message): + # pylint: disable=line-too-long + """ + Sends `message` to contact + + [Upstream documentation](https://code.briarproject.org/briar/briar/blob/master/briar-headless/README.md#writing-a-private-message) + + .. versionadded:: 0.0.3 + """ url = urljoin(BASE_HTTP_URL, - self.API_ENDPOINT + str(self._contact_id)) + self._API_ENDPOINT + str(self._contact_id)) requests.post(url, headers=self._headers, json={"text": message}) diff --git a/briar_wrapper/models/socket_listener.py b/briar_wrapper/models/socket_listener.py index 91274e5..3522ee4 100644 --- a/briar_wrapper/models/socket_listener.py +++ b/briar_wrapper/models/socket_listener.py @@ -1,6 +1,9 @@ # Copyright (c) 2019 Nico Alt # SPDX-License-Identifier: AGPL-3.0-only # License-Filename: LICENSE.md +""" +Wrapper around Briar API's websocket stream +""" import asyncio import json @@ -12,22 +15,44 @@ from briar_wrapper.constants import WEBSOCKET_URL from briar_wrapper.model import Model -class SocketListener(): # pylint: disable=too-few-public-methods +class SocketListener(Model): # pylint: disable=too-few-public-methods def __init__(self, api): - self._api = api + super().__init__(api) self._signals = dict() self._signals_lock = Lock() self._highest_signal_id = -1 self._start_websocket_thread() def connect(self, event, callback): + """ + Connects to one of websocket API's `event`s. If the websocket API sends + out a message with given `event`, `callback` will be called. + + Returns + ------- + int + Signal ID used for + `briar_wrapper.models.socket_listener.SocketListener.disconnect` + later on + + .. versionadded:: 0.0.3 + """ self._signals_lock.acquire() signal_id = self._add_signal(event, callback) self._signals_lock.release() return signal_id def disconnect(self, signal_id): + """ + Disconnect signal with `signal_id` from + `briar_wrapper.models.socket_listener.SocketListener`. The `callback` + given at + `briar_wrapper.models.socket_listener.SocketListener.connect` will not + be called anymore. + + .. versionadded:: 0.0.3 + """ self._signals_lock.acquire() self._remove_signal(signal_id) self._signals_lock.release() @@ -38,7 +63,7 @@ class SocketListener(): # pylint: disable=too-few-public-methods self._signals[signal_id] = { "event": event, "callback": callback - } + } return signal_id def _remove_signal(self, signal_id): diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..42fc97e --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[pycodestyle] +ignore = E501, W504 diff --git a/tests/briar_wrapper/models/test_contacts.py b/tests/briar_wrapper/models/test_contacts.py index 3af6d2e..68029ab 100644 --- a/tests/briar_wrapper/models/test_contacts.py +++ b/tests/briar_wrapper/models/test_contacts.py @@ -101,7 +101,7 @@ def test_get_link(api, request_headers, requests_mock): def test_watch_signal_added(api, mocker): contacts = Contacts(api) - contacts._api.socket_listener = SocketListener(None) + contacts._api.socket_listener = SocketListener(api) contacts._api.socket_listener._highest_signal_id = 136 assert contacts._api.socket_listener._signals == dict() diff --git a/tests/briar_wrapper/models/test_socket_listener.py b/tests/briar_wrapper/models/test_socket_listener.py index 24256ef..60e0a99 100644 --- a/tests/briar_wrapper/models/test_socket_listener.py +++ b/tests/briar_wrapper/models/test_socket_listener.py @@ -9,30 +9,30 @@ from briar_wrapper.models.socket_listener import SocketListener MODULE = "briar_wrapper.models.socket_listener.%s" -def test_init_websocket_thread(mocker): +def test_init_websocket_thread(api, mocker): thread_mock = mocker.patch(MODULE % "Thread") - SocketListener(None) + SocketListener(api) thread_mock.assert_called_once() -def test_init_watch_loop(mocker): +def test_init_watch_loop(api, mocker): watch_loop_mock = mocker.patch(MODULE % "SocketListener._start_watch_loop") - SocketListener(None) + SocketListener(api) watch_loop_mock.assert_called_once() -def test_connect_lock(mocker): +def test_connect_lock(api, mocker): lock_mock = mocker.Mock() manager = mocker.Mock() manager.attach_mock(lock_mock.acquire, 'acquire_mock') manager.attach_mock(lock_mock.release, 'release_mock') - socket_listener = SocketListener(None) + socket_listener = SocketListener(api) socket_listener._signals_lock = lock_mock socket_listener.connect(None, None) @@ -45,11 +45,11 @@ def test_connect_lock(mocker): assert manager.mock_calls == expected_calls -def test_connect_signal_added(mocker): +def test_connect_signal_added(api, mocker): event_mock = mocker.MagicMock() callback_mock = mocker.MagicMock() - socket_listener = SocketListener(None) + socket_listener = SocketListener(api) socket_listener._highest_signal_id = 136 assert socket_listener._signals == dict() @@ -66,11 +66,11 @@ def test_connect_signal_added(mocker): assert socket_listener._signals == expected_signals -def test_connect_signal_id(mocker): +def test_connect_signal_id(api, mocker): event_mock = mocker.MagicMock() callback_mock = mocker.MagicMock() - socket_listener = SocketListener(None) + socket_listener = SocketListener(api) socket_listener._highest_signal_id = 137 signal_id = socket_listener.connect(event_mock, callback_mock) @@ -78,14 +78,14 @@ def test_connect_signal_id(mocker): assert signal_id == 137 + 1 -def test_disconnect_lock(mocker): +def test_disconnect_lock(api, mocker): lock_mock = mocker.Mock() manager = mocker.Mock() manager.attach_mock(lock_mock.acquire, 'acquire_mock') manager.attach_mock(lock_mock.release, 'release_mock') - socket_listener = SocketListener(None) + socket_listener = SocketListener(api) socket_listener._signals_lock = lock_mock socket_listener._signals = mocker.MagicMock() @@ -99,8 +99,8 @@ def test_disconnect_lock(mocker): assert manager.mock_calls == expected_calls -def test_disconnect_signal_removed(mocker): - socket_listener = SocketListener(None) +def test_disconnect_signal_removed(api, mocker): + socket_listener = SocketListener(api) socket_listener._signals = { 137: { "event": None, @@ -123,13 +123,13 @@ def test_watch_messages(): pass -def test_call_signal_callbacks(mocker): +def test_call_signal_callbacks(api, mocker): message = { 'name': 'VeryImportantEvent' } callback_mock = mocker.MagicMock() - socket_listener = SocketListener(None) + socket_listener = SocketListener(api) socket_listener._signals = { 137: { "event": 'VeryImportantEvent', @@ -142,14 +142,14 @@ def test_call_signal_callbacks(mocker): callback_mock.assert_called_once_with(message) -def test_call_signal_callbacks_lock(mocker): +def test_call_signal_callbacks_lock(api, mocker): lock_mock = mocker.Mock() manager = mocker.Mock() manager.attach_mock(lock_mock.acquire, 'acquire_mock') manager.attach_mock(lock_mock.release, 'release_mock') - socket_listener = SocketListener(None) + socket_listener = SocketListener(api) socket_listener._signals_lock = lock_mock socket_listener._signals = mocker.MagicMock() diff --git a/tests/conftest.py b/tests/conftest.py index 91a2eb2..9fa58b9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,5 +23,5 @@ def auth_token(): def request_headers(auth_token): request_headers = { "Authorization": 'Bearer %s' % auth_token - } + } return request_headers -- GitLab