http: Simplify app registration

This commit is contained in:
Stein Magnus Jodal 2014-06-04 21:32:19 +02:00
parent b6ab29eed4
commit 33228f2528
6 changed files with 99 additions and 83 deletions

View File

@ -17,9 +17,9 @@ The HTTP server side API can be used to:
To host static files using the web server, an extension needs to register a To host static files using the web server, an extension needs to register a
name and a file path in the extension registry under the ``http:static`` key. name and a file path in the extension registry under the ``http:static`` key.
To extend the web server with a web application, an extension must create a To extend the web server with a web application, an extension must register a
subclass of :class:`mopidy.http.Router` and register the subclass with the name and a factory function in the extension registry under the ``http:app``
extension registry under the ``http:router`` key. key.
For details on how to make a Mopidy extension, see the :ref:`extensiondev` For details on how to make a Mopidy extension, see the :ref:`extensiondev`
guide. guide.
@ -31,7 +31,7 @@ Static web client example
To serve static files, you just need to register an ``http:static`` dictionary To serve static files, you just need to register an ``http:static`` dictionary
in the extension registry. The dictionary must have two keys: ``name`` and in the extension registry. The dictionary must have two keys: ``name`` and
``path``. The ``name`` is used to build the URL the static files will be ``path``. The ``name`` is used to build the URL the static files will be
served on. By convention, it should be identical with the extension's served on. By convention, it should be identical with the extension's
:attr:`~mopidy.ext.Extension.ext_name`, like in the following example. The :attr:`~mopidy.ext.Extension.ext_name`, like in the following example. The
``path`` tells Mopidy where on the disk the static files are located. ``path`` tells Mopidy where on the disk the static files are located.
@ -55,7 +55,7 @@ available at http://localhost:6680/mywebclient/foo.html.
def setup(self, registry): def setup(self, registry):
registry.add('http:static', { registry.add('http:static', {
'name': 'mywebclient', 'name': self.ext_name,
'path': os.path.join(os.path.dirname(__file__), 'static'), 'path': os.path.join(os.path.dirname(__file__), 'static'),
}) })
@ -71,18 +71,27 @@ for Tornado request handlers.
In the following example, we create a :class:`tornado.web.RequestHandler` In the following example, we create a :class:`tornado.web.RequestHandler`
called :class:`MyRequestHandler` that responds to HTTP GET requests with the called :class:`MyRequestHandler` that responds to HTTP GET requests with the
string ``Hello, world! This is Mopidy $version``. string ``Hello, world! This is Mopidy $version``, where it gets the Mopidy
version from Mopidy's core API.
Then a :class:`~mopidy.http.Router` subclass registers this Tornado request To hook the request handler into Mopidy's web server, we must register a
handler on the root URL, ``/``. The URLs returned from dictionary under the ``http:app`` key in the extension registry. The
:meth:`~mopidy.http.Router.get_request_handlers` are combined with the dictionary must have two keys: ``name`` and ``factory``.
:attr:`~mopidy.http.Router.name`` attribute of the router, so the full absolute
URL for the request handler becomes ``/mywebclient/``.
The router is added to the extension registry by The ``name`` is used to build the URL the app will be served on. By convention,
:meth:`MyWebClientExtension.setup` under the key ``http:router``. When the it should be identical with the extension's
extension is installed, Mopidy will respond to requests to :attr:`~mopidy.ext.Extension.ext_name`, like in the following example.
http://localhost:6680/mywebclient/ with the string ``Hello, world!``.
The ``factory`` must be a function that accepts two arguments, ``config`` and
``core``, respectively a dict structure of Mopidy's config and a
:class:`pykka.ActorProxy` to the full Mopidy core API. The ``factory`` function
must return a list of Tornado request handlers. The URL patterns of the request
handlers should not include the ``name``, as that will be prepended to the URL
patterns by the web server.
When the extension is installed, Mopidy will respond to requests to
http://localhost:6680/mywebclient/ with the string ``Hello, world! This is
Mopidy $version``.
:: ::
@ -92,7 +101,7 @@ http://localhost:6680/mywebclient/ with the string ``Hello, world!``.
import tornado.web import tornado.web
from mopidy import ext, http from mopidy import ext
class MyRequestHandler(tornado.web.RequestHandler): class MyRequestHandler(tornado.web.RequestHandler):
@ -105,20 +114,20 @@ http://localhost:6680/mywebclient/ with the string ``Hello, world!``.
self.core.get_version().get()) self.core.get_version().get())
class MyTornadoRouter(http.Router): def my_app_factory(config, core):
name = 'mywebclient' return [
('/', MyRequestHandler, {'core': core})
def get_request_handlers(self): ]
return [
('/', MyRequestHandler, dict(core=self.core))
]
class MyWebClientExtension(ext.Extension): class MyWebClientExtension(ext.Extension):
ext_name = 'mywebclient' ext_name = 'mywebclient'
def setup(self, registry): def setup(self, registry):
registry.add('http:router', MyTornadoRouter) registry.add('http:app', {
'name': self.ext_name,
'factory': my_app_factory,
})
# See the Extension API for the full details on this class # See the Extension API for the full details on this class
@ -147,34 +156,35 @@ http://localhost:6680/mywebclient/.
import tornado.web import tornado.web
import tornado.wsgi import tornado.wsgi
from mopidy import ext, http from mopidy import ext
class MyWSGIRouter(http.Router): def my_app_factory(config, core):
name = 'mywebclient'
def get_request_handlers(self):
def wsgi_app(environ, start_response):
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers)
return [
'Hello, world! This is Mopidy %s\n' %
self.core.get_version().get()
]
def wsgi_app(environ, start_response):
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers)
return [ return [
('(.*)', tornado.web.FallbackHandler, { 'Hello, world! This is Mopidy %s\n' %
'fallback': tornado.wsgi.WSGIContainer(wsgi_app), self.core.get_version().get()
}),
] ]
return [
('(.*)', tornado.web.FallbackHandler, {
'fallback': tornado.wsgi.WSGIContainer(wsgi_app),
}),
]
class MyWebClientExtension(ext.Extension): class MyWebClientExtension(ext.Extension):
ext_name = 'mywebclient' ext_name = 'mywebclient'
def setup(self, registry): def setup(self, registry):
registry.add('http:router', MyWSGIRouter) registry.add('http:app', {
'name': self.ext_name,
'factory': my_app_factory,
})
# See the Extension API for the full details on this class # See the Extension API for the full details on this class

View File

@ -35,13 +35,16 @@ class Extension(ext.Extension):
def setup(self, registry): def setup(self, registry):
from .actor import HttpFrontend from .actor import HttpFrontend
from .handlers import MopidyHttpRouter from .handlers import mopidy_app_factory
HttpFrontend.routers = registry['http:router'] HttpFrontend.apps = registry['http:app']
HttpFrontend.statics = registry['http:static'] HttpFrontend.statics = registry['http:static']
registry.add('frontend', HttpFrontend) registry.add('frontend', HttpFrontend)
registry.add('http:router', MopidyHttpRouter) registry.add('http:app', {
'name': 'mopidy',
'factory': mopidy_app_factory,
})
class Router(object): class Router(object):

View File

@ -20,7 +20,7 @@ logger = logging.getLogger(__name__)
class HttpFrontend(pykka.ThreadingActor, CoreListener): class HttpFrontend(pykka.ThreadingActor, CoreListener):
routers = [] apps = []
statics = [] statics = []
def __init__(self, config, core): def __init__(self, config, core):
@ -64,7 +64,7 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener):
def _get_request_handlers(self): def _get_request_handlers(self):
request_handlers = [] request_handlers = []
request_handlers.extend(self._get_router_request_handlers()) request_handlers.extend(self._get_app_request_handlers())
request_handlers.extend(self._get_static_request_handlers()) request_handlers.extend(self._get_static_request_handlers())
# Either default Mopidy or user defined path to files # Either default Mopidy or user defined path to files
@ -81,16 +81,15 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener):
list((l[0], l[1]) for l in request_handlers)) list((l[0], l[1]) for l in request_handlers))
return request_handlers return request_handlers
def _get_router_request_handlers(self): def _get_app_request_handlers(self):
result = [] result = []
for router_class in self.routers: for app in self.apps:
router = router_class(self.config, self.core) request_handlers = app['factory'](self.config, self.core)
request_handlers = router.get_request_handlers()
for handler in request_handlers: for handler in request_handlers:
handler = list(handler) handler = list(handler)
handler[0] = '/%s%s' % (router.name, handler[0]) handler[0] = '/%s%s' % (app['name'], handler[0])
result.append(tuple(handler)) result.append(tuple(handler))
logger.debug('Loaded HTTP extension: %s', router.name) logger.debug('Loaded HTTP extension: %s', app['name'])
return result return result
def _get_static_request_handlers(self): def _get_static_request_handlers(self):

View File

@ -8,25 +8,26 @@ import tornado.web
import tornado.websocket import tornado.websocket
import mopidy import mopidy
from mopidy import core, http, models from mopidy import core, models
from mopidy.utils import jsonrpc from mopidy.utils import jsonrpc
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class MopidyHttpRouter(http.Router): def mopidy_app_factory(config, core):
name = 'mopidy' return [
(r'/ws/?', WebSocketHandler, {
def get_request_handlers(self): 'core': core,
data_dir = os.path.join(os.path.dirname(__file__), 'data') }),
return [ (r'/rpc', JsonRpcHandler, {
(r'/ws/?', WebSocketHandler, {'core': self.core}), 'core': core,
(r'/rpc', JsonRpcHandler, {'core': self.core}), }),
(r'/(.*)', StaticFileHandler, { (r'/(.*)', StaticFileHandler, {
'path': data_dir, 'default_filename': 'mopidy.html' 'path': os.path.join(os.path.dirname(__file__), 'data'),
}), 'default_filename': 'mopidy.html'
] }),
]
def make_jsonrpc_wrapper(core_actor): def make_jsonrpc_wrapper(core_actor):

View File

@ -14,12 +14,12 @@ class StaticFileHandlerTest(tornado.testing.AsyncHTTPTestCase):
return tornado.web.Application([ return tornado.web.Application([
(r'/(.*)', handlers.StaticFileHandler, { (r'/(.*)', handlers.StaticFileHandler, {
'path': os.path.dirname(__file__), 'path': os.path.dirname(__file__),
'default_filename': 'test_router.py' 'default_filename': 'test_handlers.py'
}) })
]) ])
def test_static_handler(self): def test_static_handler(self):
response = self.fetch('/test_router.py', method='GET') response = self.fetch('/test_handlers.py', method='GET')
self.assertEqual(200, response.code) self.assertEqual(200, response.code)
self.assertEqual( self.assertEqual(

View File

@ -8,7 +8,6 @@ import tornado.testing
import tornado.wsgi import tornado.wsgi
import mopidy import mopidy
from mopidy import http
from mopidy.http import actor, handlers from mopidy.http import actor, handlers
@ -27,7 +26,10 @@ class HttpServerTest(tornado.testing.AsyncHTTPTestCase):
core.get_version.return_value = mopidy.__version__ core.get_version.return_value = mopidy.__version__
http_frontend = actor.HttpFrontend(config=config, core=core) http_frontend = actor.HttpFrontend(config=config, core=core)
http_frontend.routers = [handlers.MopidyHttpRouter] http_frontend.apps = [{
'name': 'mopidy',
'factory': handlers.mopidy_app_factory,
}]
return tornado.web.Application(http_frontend._get_request_handlers()) return tornado.web.Application(http_frontend._get_request_handlers())
@ -148,21 +150,19 @@ class HttpServerWithStaticFilesTest(tornado.testing.AsyncHTTPTestCase):
response.headers['Cache-Control'], 'no-cache') response.headers['Cache-Control'], 'no-cache')
class WsgiAppRouter(http.Router): def wsgi_app_factory(config, core):
name = 'wsgi'
def get_request_handlers(self): def wsgi_app(environ, start_response):
def wsgi_app(environ, start_response): status = '200 OK'
status = '200 OK' response_headers = [('Content-type', 'text/plain')]
response_headers = [('Content-type', 'text/plain')] start_response(status, response_headers)
start_response(status, response_headers) return ['Hello, world!\n']
return ['Hello, world!\n']
return [ return [
('(.*)', tornado.web.FallbackHandler, { ('(.*)', tornado.web.FallbackHandler, {
'fallback': tornado.wsgi.WSGIContainer(wsgi_app), 'fallback': tornado.wsgi.WSGIContainer(wsgi_app),
}), }),
] ]
class HttpServerWithWsgiAppTest(tornado.testing.AsyncHTTPTestCase): class HttpServerWithWsgiAppTest(tornado.testing.AsyncHTTPTestCase):
@ -178,7 +178,10 @@ class HttpServerWithWsgiAppTest(tornado.testing.AsyncHTTPTestCase):
core = mock.Mock() core = mock.Mock()
http_frontend = actor.HttpFrontend(config=config, core=core) http_frontend = actor.HttpFrontend(config=config, core=core)
http_frontend.routers = [WsgiAppRouter] http_frontend.apps = [{
'name': 'wsgi',
'factory': wsgi_app_factory,
}]
return tornado.web.Application(http_frontend._get_request_handlers()) return tornado.web.Application(http_frontend._get_request_handlers())