Commits (2)
......@@ -24,7 +24,7 @@ function dispatchApiErrorEvent(error) {
function postChallenge(challengeId, challengeData = {}) {
let url = 'challenge/' + challengeId;
let url = 'api/challenge/' + challengeId;
return apiRequest(url, {
method: 'POST',
cache: 'no-cache',
......@@ -40,7 +40,7 @@ function postChallenge(challengeId, challengeData = {}) {
export default {
getChallenge: () => {
let url = 'challenge';
let url = 'api/challenge';
return new Promise(function(resolve) {
apiRequest(url, {
method: 'GET',
......@@ -12,8 +12,7 @@ enabled_plugins = [
......@@ -3,27 +3,40 @@ from typing import Any
from argparse import ArgumentParser
from .misc import MAIN_LOG, get_kegtap_title_description_version
from .config import Config
from urllib3.util import parse_url, Url
from typing import Optional
from fastapi import FastAPI
from starlette.staticfiles import StaticFiles
import os
import uvicorn
def _locate_frontend() -> Optional[str]:
folder = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '../kegtap-ui/dist'))
return folder if os.path.isdir(folder) else None
API_ROOT_PATH = '/api'
DESCRIPTION = f'''{get_kegtap_title_description_version()[1]}
All the command line arguments are optional. If omitted, KegTap will fall back on the TOML config file, and then on its
internal defaults.
PARSER = ArgumentParser(description=DESCRIPTION)
PARSER = ArgumentParser(description=DESCRIPTION, prog='python -m kegtap')
PARSER.add_argument('-d', '--debug', action='store_true', required=False,
help='Enables debug mode in KegTap, FastAPI and Starlette.')
PARSER.add_argument('-H', '--host', required=False,
PARSER.add_argument('-H', '--host', required=False, default='localhost',
help='Host on which to listen.')
PARSER.add_argument('-P', '--port', type=int, required=False,
PARSER.add_argument('-P', '--port', type=int, required=False, default=8000,
help='Port on which to listen.')
PARSER.add_argument('-f', '--plugins-folder', required=False,
help='Custom plugin folder.')
PARSER.add_argument('-p', '--plugins', nargs='*', required=False,
help='List of plugins to enable and start.')
PARSER.add_argument('--frontend', dest='frontend_source', type=str, required=False, default=_locate_frontend(),
help='Folder containing the source of the frontend.')
PARSER.add_argument('--no-frontend', dest='frontend_enable', action='store_false', default=True,
required=False, help='Disable the frontend.')
PARSER.add_argument('config_file', nargs='?', default='kegtap.toml',
help='TOML config file which will provide defaults for all the above parameters and '
'plugin-specific configuration.')
......@@ -43,18 +56,6 @@ def configure(args) -> Config:
if args.debug is not None:
_warn_override('debug', args.debug)
config['debug'] = args.debug
if args.host is not None or args.port is not None:
if 'base_url' in config:
url = parse_url(config['base_url'])
url = Url(scheme='http', host='localhost', port=8000)
host = args.host if args.host is not None else url.host
port = args.port if args.port is not None else url.port
if host != args.host or port != args.port:
_warn_override('base_url', None)
url = Url(scheme=url.scheme, auth=url.auth, host=host, port=port, path=url.path, query=url.query,
config['base_url'] = url
if args.plugins_folder is not None:
_warn_override('user_plugin_folder', args.plugins_folder)
config['user_plugin_folder'] = args.plugins_folder
......@@ -66,10 +67,25 @@ def configure(args) -> Config:
def main():
args = PARSER.parse_args()
frontend_static_folder: Optional[str] = None
if args.frontend_enable is True and args.frontend_source is not None:
if not os.path.isdir(args.frontend_source):
MAIN_LOG.warning(f'Frontend directory does not exist: {os.path.realpath(args.frontend_source)}')
frontend_static_folder = os.path.realpath(args.frontend_source)
# Specify the right root path when building KegTap
config = configure(args)
application = KegTap(config)
url = parse_url(application.config.base_url)
uvicorn.run(application, host=url.host, port=url.port)
kt = KegTap(config, root_path=API_ROOT_PATH)
# Build an app from scratch so that Kegtap can be served in its own root path.
root_app = FastAPI(docs_url=None, redoc_url=None)
root_app.mount(API_ROOT_PATH, kt)
if frontend_static_folder is not None:
root_app.mount('/', StaticFiles(directory=frontend_static_folder, html=True), name='UI')
# Make sure the startup and shutdown events are forwarded to the sub app, which is not done by FastAPI
root_app.add_event_handler('startup', kt.router.startup)
root_app.add_event_handler('shutdown', kt.router.shutdown)
uvicorn.run(root_app, host=args.host, port=args.port)
......@@ -28,7 +28,6 @@ def load_config(data_or_file: Optional[Union[str, dict, io.IOBase, Config]]) ->
class KegTap(FastAPI):
class ConfigModel(BaseModel):
base_url: str = 'http://localhost:8000'
debug: bool = False
user_plugin_folder: Optional[str] = None
user_plugin_packages: List[str] = []
......@@ -71,11 +70,12 @@ class KegTap(FastAPI):
for plugin in self._plugins.values():
await ensure_awaitable(plugin.on_deactivate())
def __init__(self, config: Optional[Union[str, io.IOBase, Config]] = None):
def __init__(self, config: Optional[Union[str, io.IOBase, Config]] = None, root_path: str = None):
title, description, version = get_kegtap_title_description_version()
super(KegTap, self).__init__(title=title,
description='' if description is None else description,
version='' if version is None else version)
version='' if version is None else version,
self._config = load_config(config)
self._config_model_view = self._config.model_view(KegTap.ConfigModel)
self._debug = self.config.debug
from ..plugin import register
from ..fastapi_plugin import FastAPIPlugin
from typing import Optional
from fastapi import APIRouter, FastAPI
from starlette.staticfiles import StaticFiles
from pydantic import BaseModel
from ..misc import MAIN_LOG
from ..config import ConfigPlugin
import os
DEFAULT_UI_FOLDER = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../kegtap-ui/dist'))
class UIPlugin(FastAPIPlugin, ConfigPlugin['UIPlugin.ConfigModel']):
class ConfigModel(BaseModel):
custom_ui_folder: Optional[str] = None
def ui_folder(self):
return os.path.realpath(self.config.custom_ui_folder if self.config.custom_ui_folder else DEFAULT_UI_FOLDER)
Handles static web resources (HTML/CSS/JavaScript files)
def _build_subapp(self) -> APIRouter:
return APIRouter()
def mount(self, root_app: FastAPI):
super(UIPlugin, self).mount(root_app)
if os.path.isdir(self.ui_folder):
MAIN_LOG.info(f'Mounting static files from {self.ui_folder}.')
root_app.mount('/', StaticFiles(directory=self.ui_folder, html=True), name='UI')
MAIN_LOG.error(f'Cannot mount static files from {self.ui_folder}, not a folder!')