import typing as T from glob import glob from pathlib import Path import eventlet import munch import socketio from flask import send_from_directory from pycs.database.LabelProvider import LabelProvider from pycs.database.Model import Model from pycs.frontend.endpoints.ListJobs import ListJobs from pycs.frontend.endpoints.ListLabelProviders import ListLabelProviders from pycs.frontend.endpoints.ListModels import ListModels from pycs.frontend.endpoints.ListProjects import ListProjects from pycs.frontend.endpoints.additional.FolderInformation import FolderInformation from pycs.frontend.endpoints.additional.Authenticate import Authenticate from pycs.frontend.endpoints.data.GetCroppedFile import GetCroppedFile from pycs.frontend.endpoints.data.GetFile import GetFile from pycs.frontend.endpoints.data.GetPreviousAndNextFile import GetPreviousAndNextFile from pycs.frontend.endpoints.data.GetResizedFile import GetResizedFile from pycs.frontend.endpoints.data.RemoveFile import RemoveFile from pycs.frontend.endpoints.data.UploadFile import UploadFile from pycs.frontend.endpoints.jobs.RemoveJob import RemoveJob from pycs.frontend.endpoints.labels.CreateLabel import CreateLabel from pycs.frontend.endpoints.labels.EditLabelName import EditLabelName from pycs.frontend.endpoints.labels.EditLabelParent import EditLabelParent from pycs.frontend.endpoints.labels.ListLabelTree import ListLabelTree from pycs.frontend.endpoints.labels.ListLabels import ListLabels from pycs.frontend.endpoints.labels.RemoveLabel import RemoveLabel from pycs.frontend.endpoints.pipelines.EstimateBoundingBox import EstimateBoundingBox from pycs.frontend.endpoints.pipelines.FitModel import FitModel from pycs.frontend.endpoints.pipelines.PredictBoundingBox import PredictBoundingBox from pycs.frontend.endpoints.pipelines.PredictFile import PredictFile from pycs.frontend.endpoints.pipelines.PredictModel import PredictModel from pycs.frontend.endpoints.projects.CreateProject import CreateProject from pycs.frontend.endpoints.projects.EditProjectDescription import EditProjectDescription from pycs.frontend.endpoints.projects.EditProjectName import EditProjectName from pycs.frontend.endpoints.projects.ExecuteExternalStorage import ExecuteExternalStorage from pycs.frontend.endpoints.projects.ExecuteLabelProvider import ExecuteLabelProvider from pycs.frontend.endpoints.projects.GetProjectModel import GetProjectModel from pycs.frontend.endpoints.projects.ListProjectCollections import ListProjectCollections from pycs.frontend.endpoints.projects.ListProjectFiles import ListProjectFiles from pycs.frontend.endpoints.projects.RemoveProject import RemoveProject from pycs.frontend.endpoints.results.ConfirmResult import ConfirmResult from pycs.frontend.endpoints.results.ConfirmAllResults import ConfirmAllResults from pycs.frontend.endpoints.results.CopyResults import CopyResults from pycs.frontend.endpoints.results.CreateResult import CreateResult from pycs.frontend.endpoints.results.EditResultData import EditResultData from pycs.frontend.endpoints.results.EditResultLabel import EditResultLabel from pycs.frontend.endpoints.results.GetProjectResults import GetProjectResults from pycs.frontend.endpoints.results.GetResults import GetResults from pycs.frontend.endpoints.results.RemoveResult import RemoveResult from pycs.frontend.endpoints.results.ResetResults import ResetResults from pycs.frontend.notifications.NotificationManager import NotificationManager from pycs.jobs.JobRunner import JobRunner from pycs.util.PipelineCache import PipelineCache class WebServer: """ wrapper class for flask and socket.io which initializes most networking """ index: Path = Path.cwd() / 'webui' / 'index.html' def __init__(self, app, htpasswd, settings: munch.Munch, discovery: bool = True): self.app = app self.htpasswd = htpasswd # initialize web server if self.is_production: app.logger.info('production build') # overwrite root path to serve index.html @self.app.route('/', methods=['GET']) def index(): # pylint: disable=unused-variable return send_from_directory(str(self.index.parent), self.index.name) else: app.logger.info('development build') # set access control header to allow requests from Vue.js development server @self.app.after_request def after_request(response): # pylint: disable=unused-variable response.headers['Access-Control-Allow-Origin'] = 'http://localhost:8080' response.headers['Access-Control-Allow-Credentials'] = 'true' response.headers['Access-Control-Allow-Methods'] = 'POST, GET' response.headers['Access-Control-Allow-Headers'] = 'Authorization' return response # create service objects self.sio = socketio.Server(**self.sio_kwargs(settings.allowedOrigins)) self.wsgi_app = socketio.WSGIApp(self.sio, app, static_files=self.static_files) self.host = settings.host self.port = settings.port # create notification manager self.jobs = JobRunner() self.pipelines = PipelineCache(self.jobs, settings.get("pipeline_cache_time")) self.notifications = NotificationManager(self.sio) self.jobs.on_create(self.notifications.create_job) self.jobs.on_start(self.notifications.edit_job) self.jobs.on_progress(self.notifications.edit_job) self.jobs.on_finish(self.notifications.edit_job) self.jobs.on_remove(self.notifications.remove_job) self.define_routes() if discovery: Model.discover("models/") LabelProvider.discover("labels/") def sio_kwargs(self, allowed_origins) -> T.Dict[str, T.Union[str, list]]: """keyword arguments for the socketio.Server depending on the mode""" kwargs: T.Dict[str, T.Union[str, list]] = dict(async_mode="eventlet") if self.is_production: if isinstance(allowed_origins, list) and len(allowed_origins) > 0: kwargs["cors_allowed_origins"] = allowed_origins else: kwargs["cors_allowed_origins"] = "*" return kwargs @property def is_production(self) -> bool: """property checking, whether the UI is built (production mode) or served by npm serve (development mode)""" return self.index.exists() @property def static_files(self) -> T.Optional[T.Dict[str, T.Union[str, dict]]]: """returns a dictionary of static files (production mode) or None (development mode)""" if not self.is_production: return None # find static files and folders static_files: T.Dict[str, T.Union[str, dict]] = {} for file_path in glob('webui/*'): file_path = file_path.replace('\\', '/') static_files[file_path[5:]] = file_path # separately add svg files and set their correct mime type for svg_path in glob('webui/img/*.svg'): svg_path = svg_path.replace('\\', '/') static_files[svg_path[5:]] = {'content_type': 'image/svg+xml', 'filename': svg_path} return static_files def define_routes(self): """ defines app routes """ # authentication # additional self.app.add_url_rule( '/authenticate', view_func=self.htpasswd.required( Authenticate.as_view('authenticate') ) ) # additional self.app.add_url_rule( '/folder', view_func=self.htpasswd.required( FolderInformation.as_view('folder_information') ) ) # jobs self.app.add_url_rule( '/jobs', view_func=self.htpasswd.required( ListJobs.as_view('list_jobs', self.jobs) ) ) self.app.add_url_rule( '/jobs//remove', view_func=self.htpasswd.required( RemoveJob.as_view('remove_job', self.jobs) ) ) # models self.app.add_url_rule( '/models', view_func=self.htpasswd.required( ListModels.as_view('list_models') ) ) self.app.add_url_rule( '/projects//model', view_func=self.htpasswd.required( GetProjectModel.as_view('get_project_model') ) ) # labels self.app.add_url_rule( '/label_providers', view_func=self.htpasswd.required( ListLabelProviders.as_view('label_providers') ) ) self.app.add_url_rule( '/projects//labels', view_func=self.htpasswd.required( ListLabels.as_view('list_labels') ) ) self.app.add_url_rule( '/projects//labels/tree', view_func=self.htpasswd.required( ListLabelTree.as_view('list_label_tree') ) ) self.app.add_url_rule( '/projects//labels', view_func=self.htpasswd.required( CreateLabel.as_view('create_label', self.notifications) ) ) self.app.add_url_rule( '/projects//labels//remove', view_func=self.htpasswd.required( RemoveLabel.as_view('remove_label', self.notifications) ) ) self.app.add_url_rule( '/projects//labels//name', view_func=self.htpasswd.required( EditLabelName.as_view('edit_label_name', self.notifications) ) ) self.app.add_url_rule( '/projects//labels//parent', view_func=self.htpasswd.required( EditLabelParent.as_view('edit_label_parent', self.notifications) ) ) # collections self.app.add_url_rule( '/projects//collections', view_func=self.htpasswd.required( ListProjectCollections.as_view('list_collections') ) ) self.app.add_url_rule( '/projects//data///', view_func=self.htpasswd.required( ListProjectFiles.as_view('list_collection_files') ) ) # data self.app.add_url_rule( '/projects//data', view_func=self.htpasswd.required( UploadFile.as_view('upload_file', self.notifications) ) ) self.app.add_url_rule( '/projects//data', view_func=self.htpasswd.required( ListProjectFiles.as_view('list_all_files') ) ) self.app.add_url_rule( '/projects//data//', view_func=self.htpasswd.required( ListProjectFiles.as_view('list_files') ) ) self.app.add_url_rule( '/data//remove', view_func=self.htpasswd.required( RemoveFile.as_view('remove_file', self.notifications) ) ) self.app.add_url_rule( '/data/', view_func=GetFile.as_view('get_file') ) self.app.add_url_rule( '/data//', view_func=GetResizedFile.as_view('get_resized_file') ) self.app.add_url_rule( '/data///', view_func=GetCroppedFile.as_view('get_cropped_file') ) self.app.add_url_rule( '/data//previous_next', view_func=GetPreviousAndNextFile.as_view('get_previous_and_next_file') ) # results self.app.add_url_rule( '/projects//results', view_func=self.htpasswd.required( GetProjectResults.as_view('get_project_results') ) ) self.app.add_url_rule( '/data//results', view_func=self.htpasswd.required( GetResults.as_view('get_results') ) ) self.app.add_url_rule( '/data//results', view_func=self.htpasswd.required( CreateResult.as_view('create_result', self.notifications) ) ) self.app.add_url_rule( '/data//copy_results', view_func=self.htpasswd.required( CopyResults.as_view('copy_results', self.notifications) ) ) self.app.add_url_rule( '/data//confirm_all', view_func=self.htpasswd.required( ConfirmAllResults.as_view('confirm_all', self.notifications) ) ) self.app.add_url_rule( '/data//reset', view_func=self.htpasswd.required( ResetResults.as_view('reset_results', self.notifications) ) ) self.app.add_url_rule( '/results//remove', view_func=self.htpasswd.required( RemoveResult.as_view('remove_result', self.notifications) ) ) self.app.add_url_rule( '/results//confirm', view_func=self.htpasswd.required( ConfirmResult.as_view('confirm_result', self.notifications) ) ) self.app.add_url_rule( '/results//label', view_func=self.htpasswd.required( EditResultLabel.as_view('edit_result_label', self.notifications) ) ) self.app.add_url_rule( '/results//data', view_func=self.htpasswd.required( EditResultData.as_view('edit_result_data', self.notifications) ) ) # projects self.app.add_url_rule( '/projects', view_func=self.htpasswd.required( ListProjects.as_view('list_projects') ) ) self.app.add_url_rule( '/projects', view_func=self.htpasswd.required( CreateProject.as_view('create_project', self.notifications, self.jobs) ) ) self.app.add_url_rule( '/projects//label_provider', view_func=self.htpasswd.required( ExecuteLabelProvider.as_view('execute_label_provider', self.notifications, self.jobs) ) ) self.app.add_url_rule( '/projects//external_storage', view_func=self.htpasswd.required( ExecuteExternalStorage.as_view('execute_external_storage', self.notifications, self.jobs) ) ) self.app.add_url_rule( '/projects//remove', view_func=self.htpasswd.required( RemoveProject.as_view('remove_project', self.notifications) ) ) self.app.add_url_rule( '/projects//name', view_func=self.htpasswd.required( EditProjectName.as_view('edit_project_name', self.notifications) ) ) self.app.add_url_rule( '/projects//description', view_func=self.htpasswd.required( EditProjectDescription.as_view('edit_project_description', self.notifications) ) ) # pipelines self.app.add_url_rule( '/projects//pipelines/fit', view_func=self.htpasswd.required( FitModel.as_view('fit_model', self.jobs, self.pipelines) ) ) self.app.add_url_rule( '/projects//pipelines/predict', view_func=self.htpasswd.required( PredictModel.as_view('predict_model', self.notifications, self.jobs, self.pipelines) ) ) self.app.add_url_rule( '/data//predict', view_func=self.htpasswd.required( PredictFile.as_view('predict_file', self.notifications, self.jobs, self.pipelines) ) ) self.app.add_url_rule( '/data///predict_bounding_box', view_func=self.htpasswd.required( PredictBoundingBox.as_view('predict_bounding_box', self.notifications, self.jobs, self.pipelines) ) ) self.app.add_url_rule( '/data//estimate', view_func=EstimateBoundingBox.as_view('estimate_result', self.notifications, self.jobs) ) def run(self): """ start web server """ self.pipelines.start() eventlet.wsgi.server(eventlet.listen((self.host, self.port)), self.wsgi_app)