DISK_DEBOUNCE_IN_MILLISECONDS = 1000 DISK_POLL_IN_MILLISECONDS = 1000 HOST = '127.0.0.1' PORT = 7000 class CrossComputeError(Exception):

    def __str__(self):
        text = super().__str__()
        if hasattr(self, 'automation_definition'):
            automation_definition = self.automation_definition
            automation_name = automation_definition.name
            automation_version = automation_definition.version
            text += f' for {automation_name} {automation_version}'
        if hasattr(self, 'path'):
            text += f' in {format_path(self.path)}'
        return text def is_port_in_use(port):
    # https://stackoverflow.com/a/52872579
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        return s.connect_ex(('127.0.0.1', int(port))) == 0 def open_browser(uri, check_interval_in_seconds=1):

    def wait_then_run():
        try:
            while True:
                try:
                    open_uri(uri)
                except HTTPError as e:
                    L.error(e)
                    return
                except URLError:
                    sleep(check_interval_in_seconds)
                else:
                    break
            webbrowser.open(uri)
        except KeyboardInterrupt:
            pass

    process = LoggableProcess(name='browser', target=wait_then_run)
    process.daemon = True
    process.start() class DiskAutomation(Automation):

    @classmethod
    def load(Class, path_or_folder=None):
        instance = Class()
        path_or_folder = Path(path_or_folder)
        if path_or_folder.is_dir():
            instance._initialize_from_folder(path_or_folder)
        else:
            instance._initialize_from_path(path_or_folder)
        return instance

    def _reload(self):
        path = self.path
        if path.exists():
            self._initialize_from_path(path)
        else:
            self._initialize_from_folder(self.folder)

    def _initialize_from_folder(self, folder):
        paths = list(folder.iterdir())
        default_automation_path = folder / AUTOMATION_PATH
        if default_automation_path in paths:
            paths.remove(default_automation_path)
            paths.insert(0, default_automation_path)
        for path in paths:
            if path.is_dir():
                continue
            try:
                self._initialize_from_path(path)
            except CrossComputeConfigurationFormatError:
                continue
            except (CrossComputeConfigurationError, CrossComputeDataError):
                raise
            except CrossComputeError:
                continue
            break
        else:
            raise CrossComputeConfigurationNotFoundError(
                'configuration not found')

    def _initialize_from_path(self, path):
        configuration = load_configuration(path)
        self.configuration = configuration
        self.path = path
        self.folder = configuration.folder
        self.definitions = configuration.automation_definitions
        self._file_code_by_path = self._get_file_code_by_path()
        self._timestamp_object = Value('d', time())

    def serve(
            self,
            host=HOST,
            port=PORT,
            is_static=False,
            is_production=False,
            base_uri='',
            allowed_origins=None,
            disk_poll_in_milliseconds=DISK_POLL_IN_MILLISECONDS,
            disk_debounce_in_milliseconds=DISK_DEBOUNCE_IN_MILLISECONDS,
            automation_queue=None):
        if automation_queue is None:
            automation_queue = Queue()
        if getLogger().level > logging.DEBUG:
            getLogger('waitress').setLevel(logging.ERROR)
            getLogger('watchgod.watcher').setLevel(logging.ERROR)

        def run_server():
            worker_process = LoggableProcess(
                name='worker', target=self.work, args=(automation_queue,))
            worker_process.daemon = True
            worker_process.start()
            L.info('serving at http://%s:%s%s', host, port, base_uri)
            # TODO: Decouple from pyramid and waitress
            app = self._get_app(
                automation_queue, is_static, is_production, base_uri,
                allowed_origins)
            try:
                serve(
                    app, host=host, port=port, url_prefix=base_uri, threads=8)
            except OSError as e:
                L.error(e)

        if is_static and is_production:
            run_server()
            return

        self.watch(
            run_server, disk_poll_in_milliseconds,
            disk_debounce_in_milliseconds)

    def run(self):
        for automation_definition in self.definitions:
            batch_definitions = automation_definition.batch_definitions
            try:
                for batch_definition in batch_definitions:
                    _run_automation(
                        automation_definition, batch_definition,
                        process_data=load_variable_data)
            except CrossComputeError as e:
                e.automation_definition = automation_definition
                L.error(e)

    def work(self, automation_queue):
        try:
            while automation_pack := automation_queue.get():
                automation_definition, batch_definition = automation_pack
                try:
                    _run_automation(
                        automation_definition, batch_definition,
                        process_data=load_variable_data)
                except CrossComputeError as e:
                    e.automation_definition = automation_definition
                    L.error(e)
        except KeyboardInterrupt:
            pass

    def watch(
            self, run_server, disk_poll_in_milliseconds,
            disk_debounce_in_milliseconds):
        server_process = StoppableProcess(name='server', target=run_server)
        server_process.start()
        for changes in watch(
                self.folder, min_sleep=disk_poll_in_milliseconds,
                debounce=disk_debounce_in_milliseconds):
            should_restart_server = False
            for changed_type, changed_path in changes:
                try:
                    file_code = self._get_file_code(changed_path)
                except KeyError:
                    continue
                L.debug('%s %s %s', changed_type, changed_path, file_code)
                if file_code == 'c':
                    should_restart_server = True
            if should_restart_server:
                try:
                    self._reload()
                except CrossComputeError as e:
                    L.error(e)
                    continue
                server_process.stop()
                server_process = StoppableProcess(
                    name='server', target=run_server)
                server_process.start()
            else:
                self._timestamp_object.value = time()

    def _get_app(
            self, automation_queue, is_static, is_production, base_uri,
            allowed_origins):
        automation_routes = AutomationRoutes(
            self.configuration, self.definitions, automation_queue,
            self._timestamp_object)
        mutation_routes = MutationRoutes(self._timestamp_object)
        settings = {
            'base_uri': base_uri,
            'jinja2.trim_blocks': True,
            'jinja2.lstrip_blocks': True,
        }
        if not is_static and not is_production:
            settings.update({
                'pyramid.reload_templates': True,
            })
        with Configurator(settings=settings) as config:
            config.include('pyramid_jinja2')
            config.include(automation_routes.includeme)
            if not is_static:
                config.include(mutation_routes.includeme)
            _configure_renderer_globals(
                config, is_static, is_production, base_uri)
            _configure_cache_headers(config, is_production)
            _configure_allowed_origins(config, allowed_origins)
        return config.make_wsgi_app()

    def _get_file_code(self, path):
        path = Path(path)
        real_path = path.resolve()
        for automation_definition in self.definitions:
            automation_folder = automation_definition.folder
            runs_folder = automation_folder / 'runs'
            if not is_path_in_folder(path, runs_folder):
                continue
            run_id = path.absolute().relative_to(runs_folder).parts[0]
            run_folder = runs_folder / run_id
            expected_paths = self._get_variable_paths_from_folder(run_folder)
            for expected_path in expected_paths:
                if real_path == expected_path.resolve():
                    return 'v'
        return self._file_code_by_path[real_path]

    def _get_file_code_by_path(self):
        'Set c = configuration, s = style, t = template, v = variable'
        packs = []
        packs.extend(zip(self._get_configuration_paths(), repeat('c')))
        packs.extend(zip(self._get_variable_paths(), repeat('v')))
        packs.extend(zip(self._get_template_paths(), repeat('t')))
        packs.extend(zip(self._get_style_paths(), repeat('s')))
        return {path.resolve(): code for path, code in packs}

    def _get_configuration_paths(self):
        paths = set()
        # Get automation configuration paths
        paths.update([self.path] + [_.path for _ in self.definitions])
        # Get batch configuration paths
        for automation_definition in self.definitions:
            automation_folder = automation_definition.folder
            # Use raw batch definitions
            raw_batch_definitions = automation_definition.get('batches', [])
            for raw_batch_definition in raw_batch_definitions:
                batch_configuration = raw_batch_definition.get(
                    'configuration', {})
                if 'path' not in batch_configuration:
                    continue
                paths.add(automation_folder / batch_configuration['path'])
        return paths

    def _get_variable_paths(self):
        paths = set()
        for automation_definition in self.definitions:
            automation_folder = automation_definition.folder
            # Use computed batch definitions
            for batch_definition in automation_definition.batch_definitions:
                paths.update(self._get_variable_paths_from_folder(
                    automation_folder / batch_definition.folder))
        return paths

    def _get_template_paths(self):
        paths = set()
        for automation_definition in self.definitions:
            automation_folder = automation_definition.folder
            d = automation_definition.template_definitions_by_mode_name
            for template_definitions in d.values():
                for template_definition in template_definitions:
                    if 'path' not in template_definition:
                        continue
                    paths.add(automation_folder / template_definition.path)
            d = automation_definition.template_path_by_id
            for path in d.values():
                paths.add(automation_folder / path)
        return paths

    def _get_style_paths(self):
        paths = set()
        for automation_definition in self.definitions:
            automation_folder = automation_definition.folder
            for style_definition in automation_definition.style_definitions:
                if 'path' not in style_definition:
                    continue
                paths.add(automation_folder / style_definition['path'])
        return paths

    def _get_variable_paths_from_folder(self, absolute_batch_folder):
        paths = set()
        for automation_definition in self.definitions:
            d = automation_definition.variable_definitions_by_mode_name
            for mode_name, variable_definitions in d.items():
                folder = absolute_batch_folder / mode_name
                for variable_definition in variable_definitions:
                    variable_configuration = variable_definition.configuration
                    if 'path' in variable_configuration:
                        paths.add(folder / variable_configuration['path'])
                    paths.add(folder / variable_definition.path)
        return paths def configure_argument_parser_for_logging(argument_parser):
    argument_parser.add_argument(
        '--quiet', '-q', dest='quietness', action='count', default=0,
        help='decrease logging level; stack as -qq')
    argument_parser.add_argument(
        '--verbose', '-v', dest='verbosity', action='count', default=0,
        help='increase logging level; stack as -vv') def configure_logging_from(args):
    configure_logging(args.verbosity - args.quietness) def configure_argument_parser_for_configuring(a):
    a.add_argument(
        'path_or_folder', nargs='?',
        default='.',
        help='configuration path or folder')
from argparse import ArgumentParser
from logging import getLogger
from os import environ
from crosscompute.constants import (
    DISK_DEBOUNCE_IN_MILLISECONDS,
    DISK_POLL_IN_MILLISECONDS,
    HOST,
    PORT)
from crosscompute.exceptions import (
    CrossComputeError)
from crosscompute.macros.web import is_port_in_use, open_browser
from crosscompute.routines.automation import DiskAutomation
from crosscompute.routines.log import (
    configure_argument_parser_for_logging,
    configure_logging_from)
from crosscompute.scripts.configure import (
    configure_argument_parser_for_configuring)



def do(arguments=None):
    a = ArgumentParser()
    configure_argument_parser_for_logging(a)
    configure_argument_parser_for_configuring(a)
    configure_argument_parser_for_serving(a)
    args = a.parse_args(arguments)
    configure_logging_from(args)
    try:
        automation = DiskAutomation.load(args.path_or_folder)
    except CrossComputeError as e:
        L.error(e)
        return
    serve_with(automation, args)


def configure_argument_parser_for_serving(a):
    a.add_argument(
        '--host', metavar='X',
        default=HOST,
        help='specify * to listen for requests from all ip addresses')
    a.add_argument(
        '--port', metavar='X',
        default=PORT,
        help='specify port to listen to for requests')
    a.add_argument(
        '--no-browser', dest='with_browser', action='store_false',
        help='do not open browser')
    a.add_argument(
        '--static', dest='is_static', action='store_true',
        help='disable page update on file change')
    a.add_argument(
        '--production', dest='is_production', action='store_true',
        help='disable server restart on file change')
    a.add_argument(
        '--base-uri', metavar='X',
        default='',
        help='specify base uri for all routes')
    a.add_argument(
        '--origins', metavar='X', nargs='+', dest='allowed_origins',
        default=[],
        help='specify allowed origins')
    a.add_argument(
        '--disk-poll', metavar='X', type=int,
        default=DISK_POLL_IN_MILLISECONDS,
        help='interval in milliseconds to check disk for changes')
    a.add_argument(
        '--disk-debounce', metavar='X', type=int,
        default=DISK_DEBOUNCE_IN_MILLISECONDS,
        help='interval in milliseconds to wait until the disk stops changing')


def serve_with(automation, args):
    return serve(
        automation,
        args.host,
        args.port,
        args.with_browser,
        args.is_static,
        args.is_production,
        args.base_uri,
        args.allowed_origins,
        args.disk_poll,
        args.disk_debounce)


def serve(
        automation, host=HOST, port=PORT, with_browser=True,
        is_static=False, is_production=False,
        base_uri='', allowed_origins=None,

        disk_poll_in_milliseconds=DISK_POLL_IN_MILLISECONDS,