From 002fac7fa9ce374157a78ad41e5d31bd6db06d07 Mon Sep 17 00:00:00 2001 From: c-bata Date: Wed, 16 Feb 2022 16:14:31 +0900 Subject: [PATCH 1/5] Provide stable Python interface --- optuna_dashboard/__init__.py | 2 ++ optuna_dashboard/app.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/optuna_dashboard/__init__.py b/optuna_dashboard/__init__.py index e69de29b..77590db9 100644 --- a/optuna_dashboard/__init__.py +++ b/optuna_dashboard/__init__.py @@ -0,0 +1,2 @@ +from .app import run_server # noqa +from .app import wsgi # noqa diff --git a/optuna_dashboard/app.py b/optuna_dashboard/app.py index c0b2a8af..a320de63 100644 --- a/optuna_dashboard/app.py +++ b/optuna_dashboard/app.py @@ -6,11 +6,13 @@ import logging import os import threading import traceback +import typing from typing import Any from typing import Callable from typing import cast from typing import Dict from typing import List +from typing import NoReturn from typing import Optional from typing import TypeVar from typing import Union @@ -20,10 +22,13 @@ from bottle import Bottle from bottle import redirect from bottle import request from bottle import response +from bottle import run from bottle import static_file import optuna from optuna.exceptions import DuplicatedStudyError from optuna.storages import BaseStorage +from optuna.storages import RedisStorage +from optuna.storages import RDBStorage from optuna.study import Study from optuna.study import StudyDirection from optuna.study import StudySummary @@ -33,6 +38,8 @@ from optuna.trial import TrialState from . import serializer from .search_space import get_search_space +if typing.TYPE_CHECKING: + from _typeshed.wsgi import WSGIApplication BottleViewReturn = Union[str, bytes, Dict[str, Any], BaseResponse] BottleView = TypeVar("BottleView", bound=Callable[..., BottleViewReturn]) @@ -274,3 +281,31 @@ def create_app(storage: BaseStorage) -> Bottle: return static_file(filename, root=STATIC_DIR) return app + + +def get_storage(storage: Union[str, BaseStorage]) -> BaseStorage: + if isinstance(storage, str): + if storage.startswith("redis"): + return RedisStorage(storage) + else: + return RDBStorage(storage) + return storage + + +def run_server( # type: ignore + storage: Union[str, BaseStorage], host: str = "localhost", port: int = 8080 +) -> NoReturn: + """Start running optuna-dashboard and blocks until the server terminates. + This function uses wsgiref module which is not intended for the production + use. If you want to run optuna-dashboard more secure and/or more fast, + please use WSGI server like Gunicorn or uWSGI via `wsgi()` function. + """ + app = create_app(get_storage(storage)) + run(app, host=host, port=port) + + +def wsgi(storage: Union[str, BaseStorage]) -> "WSGIApplication": + """This function exposes WSGI interface for people who want to run on the + production-class WSGI servers like Gunicorn or uWSGI. + """ + return create_app(get_storage(storage)) From 25679d518d70d9e767a39afeb4aacc1fe18d7f5f Mon Sep 17 00:00:00 2001 From: c-bata Date: Wed, 16 Feb 2022 16:14:55 +0900 Subject: [PATCH 2/5] Avoid using create_app() directly --- python_tests/wsgi_client.py | 20 +++++++++----------- visual_regression_test.py | 4 ++-- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/python_tests/wsgi_client.py b/python_tests/wsgi_client.py index eb935d55..d3170e6a 100644 --- a/python_tests/wsgi_client.py +++ b/python_tests/wsgi_client.py @@ -1,17 +1,15 @@ import io -from typing import Any -from typing import Callable +import typing from typing import Dict -from typing import Iterable from typing import List from typing import Optional from typing import Tuple from typing import Union +from bottle import Bottle -WSGIEnv = Dict[str, Any] # Cannot use TypedDict because of 'HTTP_' variables -StartResponse = Callable[[str, List[Tuple[str, str]]], None] -WSGIApp = Callable[[WSGIEnv, StartResponse], Iterable[bytes]] +if typing.TYPE_CHECKING: + from _typeshed.wsgi import WSGIEnvironment def create_wsgi_env( @@ -21,7 +19,7 @@ def create_wsgi_env( body: bytes, queries: Dict[str, str], headers: Dict[str, str], -) -> WSGIEnv: +) -> "WSGIEnvironment": # 'key1=value1&key2=value2' query_string = "&".join([f"{k}={v}" for k, v in queries.items()]) @@ -48,7 +46,7 @@ def create_wsgi_env( def send_request( - app: WSGIApp, + app: Bottle, path: str, method: str, body: Union[str, bytes] = b"", @@ -68,10 +66,10 @@ def send_request( headers = headers or {} queries = queries or {} env = create_wsgi_env(path, method, content_type, bytes_body, queries, headers) - body = b"" + response_body = b"" iterable_body = app(env, start_response) for b in iterable_body: - body += b + response_body += b status_code = int(status.split()[0]) - return status_code, response_headers, body + return status_code, response_headers, response_body diff --git a/visual_regression_test.py b/visual_regression_test.py index dab4b50d..6e01881b 100644 --- a/visual_regression_test.py +++ b/visual_regression_test.py @@ -12,7 +12,7 @@ import optuna from pyppeteer import launch from pyppeteer.page import Page -from optuna_dashboard.app import create_app +from optuna_dashboard import wsgi parser = argparse.ArgumentParser() @@ -201,7 +201,7 @@ def main() -> None: else: storage = optuna.storages.RDBStorage(args.storage) - app = create_app(storage) + app = wsgi(storage) httpd = make_server(args.host, args.port, app) thread = threading.Thread(target=httpd.serve_forever) thread.start() From c8a795078445e7d2935063a0d1fe68242d9e7aac Mon Sep 17 00:00:00 2001 From: c-bata Date: Wed, 16 Feb 2022 16:22:59 +0900 Subject: [PATCH 3/5] Add 'Python Interface' section in README --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index f517aa69..8f4865cf 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,19 @@ You can walk-through trials by filtering and sorting. ![optuna-dashboard-trials-datagrid](https://user-images.githubusercontent.com/5564044/114265667-20d57d00-9a2d-11eb-8b9c-69541c9b4a28.gif) +## Python Interface + +### `run_server(storage: Union[str, BaseStorage], host: str = 'localhost', port: int = 8080) -> NoReturn` + +Start running optuna-dashboard and blocks until the server terminates. +This function uses wsgiref module which is not intended for the production +use. If you want to run optuna-dashboard more secure and/or more fast, +please use WSGI server like Gunicorn or uWSGI via `wsgi()` function. + +### `wsgi(storage: Union[str, BaseStorage]) -> WSGIApplication` + +This function exposes WSGI interface for people who want to run on the +production-class WSGI servers like Gunicorn or uWSGI. ## Submitting patches From 46025b15a7f84617df9f924985cebd8b752e98d0 Mon Sep 17 00:00:00 2001 From: c-bata Date: Thu, 17 Feb 2022 17:27:29 +0900 Subject: [PATCH 4/5] Fix lint errors --- docker/optuna_example.py | 2 +- optuna_dashboard/app.py | 3 ++- python_tests/wsgi_client.py | 1 + visual_regression_test.py | 8 ++++---- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docker/optuna_example.py b/docker/optuna_example.py index 3b75e90c..66cda2df 100644 --- a/docker/optuna_example.py +++ b/docker/optuna_example.py @@ -6,7 +6,7 @@ import optuna def objective(trial): x = trial.suggest_float("x", -100, 100) y = trial.suggest_categorical("y", [-1, 0, 1]) - return x ** 2 + y + return x**2 + y if __name__ == "__main__": diff --git a/optuna_dashboard/app.py b/optuna_dashboard/app.py index a320de63..70094be5 100644 --- a/optuna_dashboard/app.py +++ b/optuna_dashboard/app.py @@ -27,8 +27,8 @@ from bottle import static_file import optuna from optuna.exceptions import DuplicatedStudyError from optuna.storages import BaseStorage -from optuna.storages import RedisStorage from optuna.storages import RDBStorage +from optuna.storages import RedisStorage from optuna.study import Study from optuna.study import StudyDirection from optuna.study import StudySummary @@ -38,6 +38,7 @@ from optuna.trial import TrialState from . import serializer from .search_space import get_search_space + if typing.TYPE_CHECKING: from _typeshed.wsgi import WSGIApplication diff --git a/python_tests/wsgi_client.py b/python_tests/wsgi_client.py index d3170e6a..2a0a860a 100644 --- a/python_tests/wsgi_client.py +++ b/python_tests/wsgi_client.py @@ -8,6 +8,7 @@ from typing import Union from bottle import Bottle + if typing.TYPE_CHECKING: from _typeshed.wsgi import WSGIEnvironment diff --git a/visual_regression_test.py b/visual_regression_test.py index 6e01881b..2da2ab31 100644 --- a/visual_regression_test.py +++ b/visual_regression_test.py @@ -94,7 +94,7 @@ def create_dummy_storage() -> optuna.storages.InMemoryStorage: def objective_multi(trial: optuna.Trial) -> Tuple[float, float]: x = trial.suggest_float("x", 0, 5) y = trial.suggest_float("y", 0, 3) - v0 = 4 * x ** 2 + 4 * y ** 2 + v0 = 4 * x**2 + 4 * y**2 v1 = (x - 5) ** 2 + (y - 5) ** 2 return v0, v1 @@ -110,13 +110,13 @@ def create_dummy_storage() -> optuna.storages.InMemoryStorage: if category == "foo": x = trial.suggest_float("x1", 0, 5) y = trial.suggest_float("y1", 0, 3) - v0 = 4 * x ** 2 + 4 * y ** 2 + v0 = 4 * x**2 + 4 * y**2 v1 = (x - 5) ** 2 + (y - 5) ** 2 return v0, v1 else: x = trial.suggest_float("x2", 0, 5) y = trial.suggest_float("y2", 0, 3) - v0 = 2 * x ** 2 + 2 * y ** 2 + v0 = 2 * x**2 + 2 * y**2 v1 = (x - 2) ** 2 + (y - 3) ** 2 return v0, v1 @@ -130,7 +130,7 @@ def create_dummy_storage() -> optuna.storages.InMemoryStorage: def objective_prune_without_report(trial: optuna.Trial) -> float: x = trial.suggest_float("x", -15, 30) y = trial.suggest_float("y", -15, 30) - v = x ** 2 + y ** 2 + v = x**2 + y**2 if v > 100: raise optuna.TrialPruned() return v From 35cf7b68ff573093a26c07685356b640e2ea9de6 Mon Sep 17 00:00:00 2001 From: c-bata Date: Thu, 17 Feb 2022 17:56:11 +0900 Subject: [PATCH 5/5] Fix setuptools attr: error --- optuna_dashboard/__init__.py | 3 +++ optuna_dashboard/cli.py | 2 +- optuna_dashboard/version.py | 1 - setup.cfg | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) delete mode 100644 optuna_dashboard/version.py diff --git a/optuna_dashboard/__init__.py b/optuna_dashboard/__init__.py index 77590db9..ebd9110f 100644 --- a/optuna_dashboard/__init__.py +++ b/optuna_dashboard/__init__.py @@ -1,2 +1,5 @@ from .app import run_server # noqa from .app import wsgi # noqa + + +__version__ = "0.5.0" diff --git a/optuna_dashboard/cli.py b/optuna_dashboard/cli.py index ae4b91b5..146b3218 100644 --- a/optuna_dashboard/cli.py +++ b/optuna_dashboard/cli.py @@ -6,8 +6,8 @@ from optuna.storages import BaseStorage from optuna.storages import RDBStorage from optuna.storages import RedisStorage +from . import __version__ from .app import create_app -from .version import __version__ AUTO_RELOAD = os.environ.get("OPTUNA_DASHBOARD_AUTO_RELOAD") == "1" diff --git a/optuna_dashboard/version.py b/optuna_dashboard/version.py deleted file mode 100644 index 3d187266..00000000 --- a/optuna_dashboard/version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "0.5.0" diff --git a/setup.cfg b/setup.cfg index 1abc769b..cd9f94e9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = optuna-dashboard -version = attr: optuna_dashboard.version.__version__ +version = attr: optuna_dashboard.__version__ url = https://github.com/optuna/optuna-dashboard author = Masashi Shibata