Skip to content

Commit

Permalink
[Web] Add new jsonwebapi endpoint and interactive webapi docs with Sw…
Browse files Browse the repository at this point in the history
…agger-UI

The new webapi served on /jsonwebapi creates separate endpoints for each
exported python function, e.g. jsonwebapi/web/login

Webapi functions are exported with

webapi = WebapiNamespace("Webapi")

@webapi.post
def exported_post_func(self, arg):
   ...

@webapi.get
def exported_get_func(self):
   ...

The interactive webapi documenation is generated with swagger-ui and served
on http://0.0.0.0:8112/webapidoc
  • Loading branch information
bendikro committed Apr 12, 2020
1 parent d61e822 commit e829120
Show file tree
Hide file tree
Showing 31 changed files with 1,818 additions and 206 deletions.
9 changes: 8 additions & 1 deletion deluge/plugins/WebUi/deluge_webui/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,23 @@

log = logging.getLogger(__name__)

DEFAULT_PREFS = {'enabled': False, 'ssl': False, 'port': 8112}
DEFAULT_PREFS = {'enabled': False, 'ssl': False, 'webapidoc_enabled': False, 'port': 8112}


class Core(CorePluginBase):
server = None

def enable(self):
self.config = configmanager.ConfigManager('web_plugin.conf', DEFAULT_PREFS)
self.config.run_converter((0, 1), 2, self.__migrate_config_1_to_2)
if self.config['enabled']:
self.start_server()

def __migrate_config_1_to_2(self, config):
if 'webapidoc_enabled' not in config:
config['webapidoc_enabled'] = DEFAULT_PREFS['webapidoc_enabled']
return config

def disable(self):
self.stop_server()

Expand Down Expand Up @@ -75,6 +81,7 @@ def start_server(self):

self.server.port = self.config['port']
self.server.https = self.config['ssl']
self.server.setup_webapidoc(enabled=self.config['webapidoc_enabled'])
try:
self.server.start()
except CannotListenError as ex:
Expand Down
18 changes: 16 additions & 2 deletions deluge/plugins/WebUi/deluge_webui/data/config.ui
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@
<object class="GtkBox" id="settings_vbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkCheckButton" id="enabled_checkbutton">
<property name="label" translatable="yes">Enable web interface</property>
Expand Down Expand Up @@ -64,6 +64,20 @@
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="webapidoc_checkbutton">
<property name="label" translatable="yes">Webapi Doc</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkBox" id="port_hbox">
<property name="visible">True</property>
Expand Down Expand Up @@ -101,7 +115,7 @@
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
<property name="position">3</property>
</packing>
</child>
</object>
Expand Down
2 changes: 2 additions & 0 deletions deluge/plugins/WebUi/deluge_webui/gtkui.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ def on_apply_prefs(self):
config = {
'enabled': self.builder.get_object('enabled_checkbutton').get_active(),
'ssl': self.builder.get_object('ssl_checkbutton').get_active(),
'webapidoc_enabled': self.builder.get_object('webapidoc_checkbutton').get_active(),
'port': self.builder.get_object('port_spinbutton').get_value_as_int(),
}
client.webui.set_config(config)
Expand All @@ -70,6 +71,7 @@ def cb_get_config(self, config):
"""Callback for on show_prefs."""
self.builder.get_object('enabled_checkbutton').set_active(config['enabled'])
self.builder.get_object('ssl_checkbutton').set_active(config['ssl'])
self.builder.get_object('webapidoc_checkbutton').set_active(config['webapidoc_enabled'])
self.builder.get_object('port_spinbutton').set_value(config['port'])

def cb_chk_deluge_web(self, have_web):
Expand Down
3 changes: 2 additions & 1 deletion deluge/tests/test_httpdownloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from __future__ import unicode_literals

import os
import tempfile
from email.utils import formatdate

Expand Down Expand Up @@ -105,7 +106,7 @@ def __init__(self):
self.putChild(b'rename', RenameResource())
self.putChild(b'attachment', AttachmentResource())
self.putChild(b'partial', PartialDownloadResource())
self.putChild(b'torrents', LocalFilesResource('deluge/tests/data/'))
self.putChild(b'torrents', LocalFilesResource(os.path.join(os.path.dirname(__file__), 'data')))

def getChild(self, path, request): # NOQA: N802
if not path:
Expand Down
28 changes: 16 additions & 12 deletions deluge/tests/test_json_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from deluge.error import DelugeError
from deluge.ui.client import client
from deluge.ui.web.auth import Auth
from deluge.ui.web.json_api import JSON, JSONException
from deluge.ui.web.json_api import ERROR_RESPONSE_CODE, JSON, JsonAPIException

from . import common
from .basetest import BaseTestCase
Expand Down Expand Up @@ -85,9 +85,9 @@ def write(response_str):
self.assertEqual(response['result'], None)
self.assertEqual(response['id'], None)
self.assertEqual(
response['error']['message'], 'JSONException: JSON not decodable'
response['error']['message'], 'JsonAPIException: JSON not decodable'
)
self.assertEqual(response['error']['code'], 5)
self.assertEqual(response['error']['code'], ERROR_RESPONSE_CODE.RPC_ERROR)

request.write = write
request.write_was_called = False
Expand All @@ -102,20 +102,23 @@ def test_handle_request_invalid_method(self):
json_data = {'method': 'no-existing-module.test', 'id': 0, 'params': []}
request.json = json_lib.dumps(json_data).encode()
request_id, result, error = json._handle_request(request)
self.assertEqual(error, {'message': 'Unknown method', 'code': 2})
self.assertEqual(error, {'message': 'Unknown method', 'code': ERROR_RESPONSE_CODE.RPC_UNKNOWN_METHOD})

def test_handle_request_invalid_json_request(self):
json = JSON()
request = MagicMock()
# Missing method
json_data = {'id': 0, 'params': []}
request.json = json_lib.dumps(json_data).encode()
self.assertRaises(JSONException, json._handle_request, request)
json_data = {'method': 'some.method', 'params': []}
request.json = json_lib.dumps(json_data).encode()
self.assertRaises(JSONException, json._handle_request, request)
self.assertRaises(JsonAPIException, json._handle_request, request)
# Missing param
json_data = {'method': 'some.method', 'id': 0}
request.json = json_lib.dumps(json_data).encode()
self.assertRaises(JSONException, json._handle_request, request)
self.assertRaises(JsonAPIException, json._handle_request, request)
# No id is valid
json_data = {'method': 'system.listMethods', 'params': []}
request.json = json_lib.dumps(json_data).encode()
json._handle_request(request)

def test_on_json_request_invalid_content_type(self):
"""Test for exception with content type not application/json"""
Expand All @@ -124,7 +127,7 @@ def test_on_json_request_invalid_content_type(self):
request.getHeader.return_value = b'text/plain'
json_data = {'method': 'some.method', 'id': 0, 'params': []}
request.json = json_lib.dumps(json_data).encode()
self.assertRaises(JSONException, json._on_json_request, request)
self.assertRaises(JsonAPIException, json._on_json_request, request)


class JSONCustomUserTestCase(JSONBase):
Expand All @@ -148,7 +151,7 @@ def test_handle_request_auth_error(self):
json_data = {'method': 'core.get_libtorrent_version', 'id': 0, 'params': []}
request.json = json_lib.dumps(json_data).encode()
request_id, result, error = json._handle_request(request)
self.assertEqual(error, {'message': 'Not authenticated', 'code': 1})
self.assertEqual(error, {'message': 'Not authenticated', 'code': ERROR_RESPONSE_CODE.RPC_NOT_AUTHENTICATED})


class RPCRaiseDelugeErrorJSONTestCase(JSONBase):
Expand Down Expand Up @@ -181,6 +184,7 @@ def get_session_id(s_id):
auth = Auth(auth_conf)
request = Request(MagicMock(), False)
request.base = b''
request.path = b'json'
auth._create_session(request)
methods = yield json.get_remote_methods()
# Verify the function has been registered
Expand Down Expand Up @@ -273,7 +277,7 @@ def write(response_str):
'Failure: [Failure instance: Traceback (failure with no frames):'
" <class 'deluge.error.DelugeError'>: DelugeERROR\n]",
)
self.assertEqual(response['error']['code'], 4)
self.assertEqual(response['error']['code'], ERROR_RESPONSE_CODE.RPC_ERROR)

request.write = write
request.write_was_called = False
Expand Down
7 changes: 5 additions & 2 deletions deluge/tests/test_web_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,11 @@ def test_add_host(self):

# Add invalid port
conn[2] = 'bad port'
ret = self.deluge_web.web_api.add_host(conn[1], conn[2], conn[3], conn[4])
self.assertEqual(ret, (False, 'Invalid port. Must be an integer'))

from deluge.ui.hostlist import InvalidHostPort
with self.assertRaises(InvalidHostPort) as context:
self.deluge_web.web_api.add_host(conn[1], conn[2], conn[3], conn[4])
self.assertEqual('Invalid port: "bad port" Must be an integer', str(context.exception))

def test_remove_host(self):
conn = ['connection_id', '', 0, '', '']
Expand Down
Loading

0 comments on commit e829120

Please sign in to comment.