From f8272db533ec305aa95590e1fd1de444bc0922b7 Mon Sep 17 00:00:00 2001 From: Pedro Crespo <32402063+pcrespov@users.noreply.github.com> Date: Fri, 15 Nov 2019 15:04:37 +0100 Subject: [PATCH 1/7] Cleanup of config/fixtures in services/web/server/tests (#1158) Refactoring ``services/web/server/tests`` fixtures and tests: - Optimizes re-usability: ``conftest`` hierarchies and helper functions - Simplified ``client``-type of fixture - Cleanup naming, doc and remove deprecated fixtures --- services/web/server/Makefile | 21 ++-- services/web/server/tests/conftest.py | 70 +++++++++++ .../mock => data}/parametrized_project.json | 0 .../{unit/mock => data}/static/index.html | 0 .../mock => data}/static/osparc/.gitkeep | 0 .../mock => data}/static/resource/.gitkeep | 0 .../mock => data}/static/transpiled/.gitkeep | 0 .../web/server/tests/helpers/utils_docker.py | 73 +++++++++++ .../computation/test_computation.py | 50 +++----- .../integration/computation/test_rabbit.py | 22 ++-- .../web/server/tests/integration/conftest.py | 113 ++++++++---------- .../tests/integration/fixtures/__init__.py | 1 + .../integration/fixtures/celery_service.py | 8 +- .../integration/fixtures/docker_compose.py | 91 ++++++-------- .../integration/fixtures/docker_swarm.py | 31 +++-- .../integration/fixtures/postgres_service.py | 6 +- .../integration/fixtures/rabbit_service.py | 5 +- .../fixtures/standard_directories.py | 89 -------------- .../integration/test_project_workflow.py | 64 ++++------ .../TODO - integration-proxy}/Makefile | 0 .../TODO - integration-proxy}/conftest.py | 0 .../test_application_proxy.py | 0 services/web/server/tests/unit/conftest.py | 77 +++--------- .../tests/unit/mock/configs/light-test.yaml | 46 ------- .../tests/unit/mock/configs/minimum.yaml | 48 -------- .../unit/mock/configs/server-host-test.yaml | 46 ------- .../server/tests/unit/mock/docker-compose.yml | 24 ---- .../tests/unit/test_template_projects.py | 4 +- .../tests/unit/with_postgres/config.yaml | 2 +- .../tests/unit/with_postgres/conftest.py | 61 +++------- .../with_postgres/docker-compose.debug.yml | 2 +- .../unit/with_postgres/docker-compose.yml | 2 +- .../with_postgres/test_access_to_studies.py | 7 +- .../tests/unit/with_postgres/test_db.py | 15 ++- 34 files changed, 377 insertions(+), 601 deletions(-) create mode 100644 services/web/server/tests/conftest.py rename services/web/server/tests/{unit/mock => data}/parametrized_project.json (100%) rename services/web/server/tests/{unit/mock => data}/static/index.html (100%) rename services/web/server/tests/{unit/mock => data}/static/osparc/.gitkeep (100%) rename services/web/server/tests/{unit/mock => data}/static/resource/.gitkeep (100%) rename services/web/server/tests/{unit/mock => data}/static/transpiled/.gitkeep (100%) create mode 100644 services/web/server/tests/helpers/utils_docker.py create mode 100644 services/web/server/tests/integration/fixtures/__init__.py delete mode 100644 services/web/server/tests/integration/fixtures/standard_directories.py rename services/web/server/tests/{integration-proxy => sandbox/TODO - integration-proxy}/Makefile (100%) rename services/web/server/tests/{integration-proxy => sandbox/TODO - integration-proxy}/conftest.py (100%) rename services/web/server/tests/{integration-proxy => sandbox/TODO - integration-proxy}/test_application_proxy.py (100%) delete mode 100644 services/web/server/tests/unit/mock/configs/light-test.yaml delete mode 100644 services/web/server/tests/unit/mock/configs/minimum.yaml delete mode 100644 services/web/server/tests/unit/mock/configs/server-host-test.yaml delete mode 100644 services/web/server/tests/unit/mock/docker-compose.yml diff --git a/services/web/server/Makefile b/services/web/server/Makefile index e2e94d5ac93..5a2caebc2b7 100644 --- a/services/web/server/Makefile +++ b/services/web/server/Makefile @@ -10,30 +10,31 @@ ROOT_DIR = $(realpath $(CURDIR)/../../../) VENV_DIR ?= $(realpath $(ROOT_DIR)/.venv) -.PHONY: install-dev -install-dev: ## install app in edit mode for development +.PHONY: install +install: ## install app in edit mode for development [DEV] # installing in edit mode @$(VENV_DIR)/bin/pip3 install -r requirements/dev.txt + .PHONY: tests -tests: ## runs tests +tests: ## runs all tests [DEV] # running unit tests - @$(VENV_DIR)/bin/pytest --cov=simcore_service_${APP_NAME} --cov-append -v -m "not travis" $(CURDIR)/tests/unit + @$(VENV_DIR)/bin/pytest -vv -x --ff --pdb $(CURDIR)/tests/unit + # running integration tests + @$(VENV_DIR)/bin/pytest -vv -x --ff --pdb $(CURDIR)/tests/integration .PHONY: build -build: ## builds docker image +build: ## builds docker image (using main services/docker-compose-build.yml) @$(MAKE) -C ${ROOT_DIR} target=${APP_NAME} build -.PHONY: clean .check-clean -.check-clean: +.PHONY: clean +clean: ## cleans all unversioned files in project and temp files create by this makefile + # Cleaning unversioned @git clean -ndxf -e .vscode/ @echo -n "Are you sure? [y/N] " && read ans && [ $${ans:-N} = y ] @echo -n "$(shell whoami), are you REALLY sure? [y/N] " && read ans && [ $${ans:-N} = y ] - -clean: .check-clean ## cleans all unversioned files in project and temp files create by this makefile - # Cleaning unversioned @git clean -dxf -e .vscode/ diff --git a/services/web/server/tests/conftest.py b/services/web/server/tests/conftest.py new file mode 100644 index 00000000000..876d126aca9 --- /dev/null +++ b/services/web/server/tests/conftest.py @@ -0,0 +1,70 @@ +""" Main test configuration + + EXPECTED: simcore_service_webserver installed + +""" +# pylint: disable=unused-argument +# pylint: disable=bare-except +# pylint:disable=redefined-outer-name + +import logging +import sys +from pathlib import Path + +import pytest + +import simcore_service_webserver + +current_dir = Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent +log = logging.getLogger(__name__) + +# mute noisy loggers +logging.getLogger("openapi_spec_validator").setLevel(logging.WARNING) +logging.getLogger("sqlalchemy").setLevel(logging.WARNING) + +## HELPERS +sys.path.append(str(current_dir / 'helpers')) + + +## FIXTURES: standard paths + +@pytest.fixture(scope='session') +def package_dir() -> Path: + """ osparc-simcore installed directory """ + dirpath = Path(simcore_service_webserver.__file__).resolve().parent + assert dirpath.exists() + return dirpath + +@pytest.fixture(scope='session') +def osparc_simcore_root_dir() -> Path: + """ osparc-simcore repo root dir """ + WILDCARD = "services/web/server" + + root_dir = Path(current_dir) + while not any(root_dir.glob(WILDCARD)) and root_dir != Path("/"): + root_dir = root_dir.parent + + msg = f"'{root_dir}' does not look like the git root directory of osparc-simcore" + assert root_dir.exists(), msg + assert any(root_dir.glob(WILDCARD)), msg + assert any(root_dir.glob(".git")), msg + + return root_dir + +@pytest.fixture(scope="session") +def env_devel_file(osparc_simcore_root_dir) -> Path: + env_devel_fpath = osparc_simcore_root_dir / ".env-devel" + assert env_devel_fpath.exists() + return env_devel_fpath + +@pytest.fixture(scope='session') +def api_specs_dir(osparc_simcore_root_dir: Path) -> Path: + specs_dir = osparc_simcore_root_dir/ "api" / "specs" / "webserver" + assert specs_dir.exists() + return specs_dir + +@pytest.fixture(scope='session') +def fake_data_dir() -> Path: + dirpath = (current_dir / "data").resolve() + assert dirpath.exists() + return dirpath diff --git a/services/web/server/tests/unit/mock/parametrized_project.json b/services/web/server/tests/data/parametrized_project.json similarity index 100% rename from services/web/server/tests/unit/mock/parametrized_project.json rename to services/web/server/tests/data/parametrized_project.json diff --git a/services/web/server/tests/unit/mock/static/index.html b/services/web/server/tests/data/static/index.html similarity index 100% rename from services/web/server/tests/unit/mock/static/index.html rename to services/web/server/tests/data/static/index.html diff --git a/services/web/server/tests/unit/mock/static/osparc/.gitkeep b/services/web/server/tests/data/static/osparc/.gitkeep similarity index 100% rename from services/web/server/tests/unit/mock/static/osparc/.gitkeep rename to services/web/server/tests/data/static/osparc/.gitkeep diff --git a/services/web/server/tests/unit/mock/static/resource/.gitkeep b/services/web/server/tests/data/static/resource/.gitkeep similarity index 100% rename from services/web/server/tests/unit/mock/static/resource/.gitkeep rename to services/web/server/tests/data/static/resource/.gitkeep diff --git a/services/web/server/tests/unit/mock/static/transpiled/.gitkeep b/services/web/server/tests/data/static/transpiled/.gitkeep similarity index 100% rename from services/web/server/tests/unit/mock/static/transpiled/.gitkeep rename to services/web/server/tests/data/static/transpiled/.gitkeep diff --git a/services/web/server/tests/helpers/utils_docker.py b/services/web/server/tests/helpers/utils_docker.py new file mode 100644 index 00000000000..b0988a6a24e --- /dev/null +++ b/services/web/server/tests/helpers/utils_docker.py @@ -0,0 +1,73 @@ + +import logging +import os +import subprocess +import tempfile +from pathlib import Path +from typing import Dict, List, Optional, Union + +import docker +import yaml +from tenacity import after_log, retry, stop_after_attempt, wait_fixed + +log = logging.getLogger(__name__) + +@retry( + wait=wait_fixed(2), + stop=stop_after_attempt(10), + after=after_log(log, logging.WARN)) +def get_service_published_port(service_name: str) -> str: + """ + WARNING: ENSURE that service name exposes a port in Dockerfile file or docker-compose config file + """ + # NOTE: retries since services can take some time to start + client = docker.from_env() + + services = [x for x in client.services.list() if service_name in x.name] + if not services: + raise RuntimeError(f"Cannot find published port for service '{service_name}'. Probably services still not started.") + + service_ports = services[0].attrs["Endpoint"].get("Ports") + if not service_ports: + raise RuntimeError(f"Cannot find published port for service '{service_name}' in endpoint. Probably services still not started.") + + if len(service_ports)>1: + log.warning("Multiple porst published in service '%s'. Defaulting to first from %s", service_name, service_ports) + + published_port = service_ports[0]["PublishedPort"] + return str(published_port) + + +def run_docker_compose_config( + docker_compose_paths: Union[List[Path], Path], + workdir: Path, + destination_path: Optional[Path]=None) -> Dict: + """ Runs docker-compose config to validate and resolve a compose file configuration + + - Composes all configurations passed in 'docker_compose_paths' + - Takes 'workdir' as current working directory (i.e. all '.env' files there will be captured) + - Saves resolved output config to 'destination_path' (if given) + """ + + if not isinstance(docker_compose_paths, List): + docker_compose_paths = [docker_compose_paths, ] + + temp_dir = None + if destination_path is None: + temp_dir = Path(tempfile.mkdtemp(prefix='')) + destination_path = temp_dir / 'docker-compose.yml' + + config_paths = [ f"-f {os.path.relpath(docker_compose_path, workdir)}" for docker_compose_path in docker_compose_paths] + configs_prefix = " ".join(config_paths) + + subprocess.run( f"docker-compose {configs_prefix} config > {destination_path}", + shell=True, check=True, + cwd=workdir) + + with destination_path.open() as f: + config = yaml.safe_load(f) + + if temp_dir: + temp_dir.unlink() + + return config diff --git a/services/web/server/tests/integration/computation/test_computation.py b/services/web/server/tests/integration/computation/test_computation.py index ef69f2a4676..7ee04527fc5 100644 --- a/services/web/server/tests/integration/computation/test_computation.py +++ b/services/web/server/tests/integration/computation/test_computation.py @@ -1,5 +1,3 @@ -# pylint:disable=wildcard-import -# pylint:disable=unused-import # pylint:disable=unused-variable # pylint:disable=unused-argument # pylint:disable=redefined-outer-name @@ -8,20 +6,16 @@ import sys import time import uuid -from contextlib import contextmanager from pathlib import Path from pprint import pprint -from typing import Dict import pytest -import yaml from aiohttp import web from yarl import URL from servicelib.application import create_safe_application from servicelib.application_keys import APP_CONFIG_KEY -from servicelib.rest_responses import unwrap_envelope -from simcore_sdk.models.pipeline_models import ( # uses legacy TODO: upgrade test +from simcore_sdk.models.pipeline_models import ( SUCCESS, ComputationalPipeline, ComputationalTask) from simcore_service_webserver.computation import setup_computation from simcore_service_webserver.db import setup_db @@ -31,11 +25,12 @@ from simcore_service_webserver.security import setup_security from simcore_service_webserver.security_roles import UserRole from simcore_service_webserver.session import setup_session -from simcore_service_webserver.users import setup_users from utils_assert import assert_status from utils_login import LoggedUser from utils_projects import NewProject +current_dir = Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent + API_VERSION = "v0" API_PREFIX = "/" + API_VERSION @@ -56,24 +51,18 @@ # 'adminer', # 'portainer' ] -@pytest.fixture(scope='session') -def here() -> Path: - return Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent @pytest.fixture -def client(loop, aiohttp_unused_port, aiohttp_client, app_config, here, docker_compose_file): - port = app_config["main"]["port"] = aiohttp_unused_port() - host = app_config['main']['host'] = '127.0.0.1' - +def client(loop, aiohttp_client, + app_config, ## waits until swarm with *_services are up + ): assert app_config["rest"]["version"] == API_VERSION assert API_VERSION in app_config["rest"]["location"] app_config['storage']['enabled'] = False app_config["db"]["init_tables"] = True # inits postgres_service - final_config_path = here / "config.app.yaml" - with final_config_path.open('wt') as f: - yaml.dump(app_config, f, default_flow_style=False) + pprint(app_config) # fake config app = create_safe_application() @@ -90,33 +79,28 @@ def client(loop, aiohttp_unused_port, aiohttp_client, app_config, here, docker_c setup_computation(app) yield loop.run_until_complete(aiohttp_client(app, server_kwargs={ - 'port': port, - 'host': 'localhost' + 'port': app_config["main"]["port"], + 'host': app_config['main']['host'] })) - # cleanup - final_config_path.unlink() - @pytest.fixture def project_id() -> str: return str(uuid.uuid4()) - - -@pytest.fixture -def mock_workbench_payload(here): - file_path = here / "workbench_sleeper_payload.json" +@pytest.fixture(scope='session') +def mock_workbench_payload(): + file_path = current_dir / "workbench_sleeper_payload.json" with file_path.open() as fp: return json.load(fp) -@pytest.fixture -def mock_workbench_adjacency_list(here): - file_path = here / "workbench_sleeper_dag_adjacency_list.json" +@pytest.fixture(scope='session') +def mock_workbench_adjacency_list(): + file_path = current_dir / "workbench_sleeper_dag_adjacency_list.json" with file_path.open() as fp: return json.load(fp) -@pytest.fixture +@pytest.fixture(scope='session') def mock_project(fake_data_dir, mock_workbench_payload): with (fake_data_dir / "fake-project.json").open() as fp: project = json.load(fp) @@ -154,6 +138,7 @@ def assert_db_contents(project_id, postgres_session, mock_workbench_payload, mock_workbench_adjacency_list, check_outputs:bool ): + # pylint: disable=no-member pipeline_db = postgres_session.query(ComputationalPipeline)\ .filter(ComputationalPipeline.project_id == project_id).one() assert pipeline_db.project_id == project_id @@ -179,6 +164,7 @@ def assert_db_contents(project_id, postgres_session, assert task_db.image["tag"] == mock_pipeline[task_db.node_id]["version"] def assert_sleeper_services_completed(project_id, postgres_session): + # pylint: disable=no-member # we wait 15 secs before testing... time.sleep(15) pipeline_db = postgres_session.query(ComputationalPipeline)\ diff --git a/services/web/server/tests/integration/computation/test_rabbit.py b/services/web/server/tests/integration/computation/test_rabbit.py index 51d38655724..1f511d25df7 100644 --- a/services/web/server/tests/integration/computation/test_rabbit.py +++ b/services/web/server/tests/integration/computation/test_rabbit.py @@ -1,4 +1,3 @@ -# pylint:disable=wildcard-import # pylint:disable=unused-variable # pylint:disable=unused-argument # pylint:disable=redefined-outer-name @@ -33,15 +32,14 @@ def here() -> Path: return Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent @pytest.fixture -def webserver_service(loop, aiohttp_unused_port, aiohttp_server, app_config, rabbit_service): - port = app_config["main"]["port"] = aiohttp_unused_port() - host = app_config['main']['host'] = '127.0.0.1' - +def client(loop, aiohttp_client, + app_config, ## waits until swarm with *_services are up + rabbit_service ## waits until rabbit is responsive + ): assert app_config["rest"]["version"] == API_VERSION assert API_VERSION in app_config["rest"]["location"] app_config['storage']['enabled'] = False - app_config["db"]["init_tables"] = True # inits postgres_service # fake config @@ -50,16 +48,12 @@ def webserver_service(loop, aiohttp_unused_port, aiohttp_server, app_config, rab setup_computation(app) - server = loop.run_until_complete(aiohttp_server(app, port=port)) - yield server - # cleanup + yield loop.run_until_complete(aiohttp_client(app, server_kwargs={ + 'port': app_config["main"]["port"], + 'host': app_config['main']['host'] + })) -@pytest.fixture -def client(loop, webserver_service, aiohttp_client): - client = loop.run_until_complete(aiohttp_client(webserver_service)) - return client - @pytest.fixture def rabbit_config(app_config): rb_config = app_config[CONFIG_SECTION_NAME] diff --git a/services/web/server/tests/integration/conftest.py b/services/web/server/tests/integration/conftest.py index 87f6a9ed465..66e22f878ff 100644 --- a/services/web/server/tests/integration/conftest.py +++ b/services/web/server/tests/integration/conftest.py @@ -1,27 +1,35 @@ +""" Configuration for integration testing + + During integration testing, + - the app under test (i.e. the webserver) will be installed and started in the host + - every test module (i.e. integration/**/test_*.py) deploys a stack in a swarm fixture with a seleciton of core and op-services + - the selection of core/op services are listed in the 'core_services' and 'ops_serices' variables in each test module + + NOTE: services/web/server/tests/conftest.py is pre-loaded + +""" # pylint: disable=unused-argument # pylint: disable=bare-except # pylint:disable=redefined-outer-name - import logging import sys +from copy import deepcopy from pathlib import Path from pprint import pprint from typing import Dict -import docker import pytest import trafaret_config import yaml -from tenacity import after_log, retry, stop_after_attempt, wait_fixed from simcore_service_webserver.application_config import app_schema from simcore_service_webserver.cli import create_environ from simcore_service_webserver.resources import resources as app_resources +from utils_docker import get_service_published_port # imports the fixtures for the integration tests pytest_plugins = [ - "fixtures.standard_directories", "fixtures.docker_compose", "fixtures.docker_swarm", "fixtures.docker_registry", @@ -30,24 +38,19 @@ "fixtures.postgres_service" ] -log = logging.getLogger(__name__) +current_dir = Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent -# mute noisy loggers -logging.getLogger("openapi_spec_validator").setLevel(logging.WARNING) -logging.getLogger("sqlalchemy").setLevel(logging.WARNING) - -sys.path.append(str(Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent.parent / 'helpers')) -API_VERSION = "v0" - - -@pytest.fixture(scope='session') -def here(): - return Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent +log = logging.getLogger(__name__) @pytest.fixture(scope="module") -def webserver_environ(request, simcore_docker_compose, docker_stack) -> Dict[str, str]: - """ Environment variables for the webserver application +def webserver_environ(request, docker_stack: Dict, simcore_docker_compose: Dict) -> Dict[str, str]: + """ + Started already swarm with integration stack (via dependency with 'docker_stack') + Environment variable expected for the web-server application in + an test-integration context, i.e. web-server runs in host and the + remaining services (defined in variable 'core_services') are deployed + in containers """ assert "webserver" not in docker_stack["services"] @@ -78,65 +81,53 @@ def webserver_environ(request, simcore_docker_compose, docker_stack) -> Dict[str environ['%s_PORT' % name.upper()] = published_port # to swarm boundary since webserver is installed in the host and therefore outside the swarm's network - pprint(environ) + pprint(environ) # NOTE: displayed only if error return environ @pytest.fixture(scope='module') -def app_config(here, webserver_environ) -> Dict: - config_file_path = here / "config.yaml" - def _recreate_config_file(): - with app_resources.stream("config/server-docker-dev.yaml") as f: - cfg = yaml.safe_load(f) - # test webserver works in host - cfg["main"]['host'] = '127.0.0.1' +def _webserver_dev_config(webserver_environ: Dict, docker_stack: Dict) -> Dict: + """ + Swarm with integration stack already started + + Configuration for a webserver provided it runs in host + + NOTE: Prefer using 'app_config' below instead of this as a function-scoped fixture + """ + config_file_path = current_dir / "webserver_dev_config.yaml" - with config_file_path.open('wt') as f: - yaml.dump(cfg, f, default_flow_style=False) + # recreate config-file + with app_resources.stream("config/server-docker-dev.yaml") as f: + cfg = yaml.safe_load(f) + # test webserver works in host + cfg["main"]['host'] = '127.0.0.1' - _recreate_config_file() + with config_file_path.open('wt') as f: + yaml.dump(cfg, f, default_flow_style=False) # Emulates cli config_environ = {} config_environ.update(webserver_environ) config_environ.update( create_environ(skip_host_environ=True) ) # TODO: can be done monkeypathcing os.environ and calling create_environ as well + # validates cfg_dict = trafaret_config.read_and_validate(config_file_path, app_schema, vars=config_environ) + # WARNING: changes to this fixture during testing propagates to other tests. Use cfg = deepcopy(cfg_dict) + # FIXME: freeze read/only json obj yield cfg_dict # clean up # to debug configuration uncomment next line config_file_path.unlink() -## HELPERS -def resolve_environ(service, environ): - _environs = {} - for item in service.get("environment", list()): - key, value = item.split("=") - if value.startswith("${") and value.endswith("}"): - value = value[2:-1] - if ":" in value: - variable, default = value.split(":") - value = environ.get(variable, default[1:]) - else: - value = environ.get(value, value) - _environs[key] = value - return _environs - - - -@retry(wait=wait_fixed(2), stop=stop_after_attempt(10), after=after_log(log, logging.WARN)) -def get_service_published_port(service_name: str) -> str: - # WARNING: ENSURE that service name defines a port - # NOTE: retries since services can take some time to start - client = docker.from_env() - services = [x for x in client.services.list() if service_name in x.name] - if not services: - raise RuntimeError("Cannot find published port for service '%s'. Probably services still not up" % service_name) - service_endpoint = services[0].attrs["Endpoint"] - - if "Ports" not in service_endpoint or not service_endpoint["Ports"]: - raise RuntimeError("Cannot find published port for service '%s' in endpoint. Probably services still not up" % service_name) - - published_port = service_endpoint["Ports"][0]["PublishedPort"] - return str(published_port) + return cfg_dict + +@pytest.fixture(scope="function") +def app_config(_webserver_dev_config: Dict, aiohttp_unused_port) -> Dict: + """ + Swarm with integration stack already started + This fixture can be safely modified during test since it is renovated on every call + """ + cfg = deepcopy(_webserver_dev_config) + cfg["main"]["port"] = aiohttp_unused_port() + return cfg diff --git a/services/web/server/tests/integration/fixtures/__init__.py b/services/web/server/tests/integration/fixtures/__init__.py new file mode 100644 index 00000000000..b472f27c93f --- /dev/null +++ b/services/web/server/tests/integration/fixtures/__init__.py @@ -0,0 +1 @@ +# Collection of tests fixtures for integration testing diff --git a/services/web/server/tests/integration/fixtures/celery_service.py b/services/web/server/tests/integration/fixtures/celery_service.py index 8428824d10d..8a51d5634ed 100644 --- a/services/web/server/tests/integration/fixtures/celery_service.py +++ b/services/web/server/tests/integration/fixtures/celery_service.py @@ -1,9 +1,9 @@ -# pylint:disable=wildcard-import -# pylint:disable=unused-import # pylint:disable=unused-variable # pylint:disable=unused-argument # pylint:disable=redefined-outer-name +from copy import deepcopy + import celery import celery.bin.base import celery.bin.celery @@ -13,8 +13,8 @@ @pytest.fixture(scope="module") -def celery_service(app_config, docker_stack): - cfg = app_config["rabbit"] +def celery_service(_webserver_dev_config, docker_stack): + cfg = deepcopy(_webserver_dev_config["rabbit"]) host = cfg["host"] port = cfg["port"] user = cfg["user"] diff --git a/services/web/server/tests/integration/fixtures/docker_compose.py b/services/web/server/tests/integration/fixtures/docker_compose.py index d24ce60d951..a601d667f55 100644 --- a/services/web/server/tests/integration/fixtures/docker_compose.py +++ b/services/web/server/tests/integration/fixtures/docker_compose.py @@ -1,9 +1,17 @@ +""" + + Main Makefile produces a set of docker-compose configuration files + Here we can find fixtures of most of these configurations + +""" + # pylint:disable=wildcard-import # pylint:disable=unused-import # pylint:disable=unused-variable # pylint:disable=unused-argument # pylint:disable=redefined-outer-name + import os import re import shutil @@ -13,14 +21,15 @@ from collections import defaultdict from copy import deepcopy from pathlib import Path -from typing import Dict, List +from typing import Dict, List, Union import pytest import yaml +from utils_docker import run_docker_compose_config @pytest.fixture("session") -def devel_environ(env_devel_file) -> Dict[str, str]: +def devel_environ(env_devel_file: Path) -> Dict[str, str]: """ Loads and extends .env-devel """ @@ -50,16 +59,21 @@ def devel_environ(env_devel_file) -> Dict[str, str]: return env_devel + @pytest.fixture(scope="module") def temp_folder(request, tmpdir_factory) -> Path: tmp = Path(tmpdir_factory.mktemp("docker_compose_{}".format(request.module.__name__))) yield tmp + @pytest.fixture(scope="module") -def env_file(osparc_simcore_root_dir, devel_environ): - # ensures .env at git_root_dir +def env_file(osparc_simcore_root_dir: Path, devel_environ: Dict[str, str]) -> Path: + """ + Creates a .env file from the .env-devel + """ + # preserves .env at git_root_dir after test if already exists env_path = osparc_simcore_root_dir / ".env" - backup_path = osparc_simcore_root_dir / ".env-bak" + backup_path = osparc_simcore_root_dir / ".env.bak" if env_path.exists(): shutil.copy(env_path, backup_path) @@ -76,15 +90,18 @@ def env_file(osparc_simcore_root_dir, devel_environ): backup_path.unlink() + @pytest.fixture("module") -def simcore_docker_compose(osparc_simcore_root_dir, env_file, temp_folder) -> Dict: +def simcore_docker_compose(osparc_simcore_root_dir: Path, env_file: Path, temp_folder: Path) -> Dict: """ Resolves docker-compose for simcore stack in local host + Produces same as `make .stack-simcore-version.yml` in a temporary folder """ COMPOSE_FILENAMES = [ "docker-compose.yml", "docker-compose.local.yml" ] + # ensures .env at git_root_dir assert env_file.exists() assert env_file.parent == osparc_simcore_root_dir @@ -94,19 +111,19 @@ def simcore_docker_compose(osparc_simcore_root_dir, env_file, temp_folder) -> Di for filename in COMPOSE_FILENAMES] assert all(docker_compose_path.exists() for docker_compose_path in docker_compose_paths) - # path to resolved docker-compose - destination_path = temp_folder / "simcore_docker_compose.yml" + config = run_docker_compose_config(docker_compose_paths, + workdir=env_file.parent, + destination_path=temp_folder / "simcore_docker_compose.yml") - config = _run_docker_compose_config(docker_compose_paths, destination_path, osparc_simcore_root_dir) return config - @pytest.fixture("module") -def ops_docker_compose(osparc_simcore_root_dir, env_file, temp_folder) -> Dict: +def ops_docker_compose(osparc_simcore_root_dir: Path, env_file: Path, temp_folder: Path) -> Dict: """ Filters only services in docker-compose-ops.yml and returns yaml data + Produces same as `make .stack-ops.yml` in a temporary folder """ - # ensures .env at git_root_dir + # ensures .env at git_root_dir, which will be used as current directory assert env_file.exists() assert env_file.parent == osparc_simcore_root_dir @@ -114,24 +131,20 @@ def ops_docker_compose(osparc_simcore_root_dir, env_file, temp_folder) -> Dict: docker_compose_path = osparc_simcore_root_dir / "services" / "docker-compose-ops.yml" assert docker_compose_path.exists() - # path to resolved docker-compose - destination_path = temp_folder / "ops_docker_compose.yml" - - config = _run_docker_compose_config(docker_compose_path, destination_path, osparc_simcore_root_dir) + config = run_docker_compose_config(docker_compose_path, + workdir=env_file.parent, + destination_path=temp_folder / "ops_docker_compose.yml") return config - @pytest.fixture(scope='module') -def docker_compose_file(request, temp_folder, simcore_docker_compose): - """ A copy of simcore_docker_compose filtered with services in core_services - - Creates a docker-compose.yml with services listed in 'core_services' module variable +def core_services_config_file(request, temp_folder, simcore_docker_compose): + """ Creates a docker-compose config file for every stack of services in'core_services' module variable File is created in a temp folder - - Overrides pytest-docker fixture """ - core_services = getattr(request.module, 'core_services', []) # TODO: PC->SAN could also be defined as a fixture (as with docker_compose) + core_services = getattr(request.module, 'core_services', []) # TODO: PC->SAN could also be defined as a fixture instead of a single variable (as with docker_compose) + assert core_services, f"Expected at least one service in 'core_services' within '{request.module.__name__}'" + docker_compose_path = Path(temp_folder / 'simcore_docker_compose.filtered.yml') _filter_services_and_dump(core_services, simcore_docker_compose, docker_compose_path) @@ -139,8 +152,8 @@ def docker_compose_file(request, temp_folder, simcore_docker_compose): return docker_compose_path @pytest.fixture(scope='module') -def ops_docker_compose_file(request, temp_folder, ops_docker_compose): - """ Creates a docker-compose.yml with services listed in 'ops_services' module variable +def ops_services_config_file(request, temp_folder, ops_docker_compose): + """ Creates a docker-compose config file for every stack of services in 'ops_services' module variable File is created in a temp folder """ ops_services = getattr(request.module, 'ops_services', []) @@ -151,8 +164,6 @@ def ops_docker_compose_file(request, temp_folder, ops_docker_compose): return docker_compose_path - - # HELPERS --------------------------------------------- def _get_ip()->str: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) @@ -192,27 +203,3 @@ def _filter_services_and_dump(include: List, services_compose: Dict, docker_comp # locally we have access to file print(f"Saving config to '{docker_compose_path}'") yaml.dump(content, fh, default_flow_style=False) - - - -def _run_docker_compose_config(docker_compose_paths, destination_path: Path, osparc_simcore_root_dir: Path) -> Dict: - """ - Runs docker-compose config on multiple files 'docker_compose_paths' taking 'osparc_simcore_root_dir' - as current working directory and saves the output to 'destination_path' - """ - - - if not isinstance(docker_compose_paths, list): - docker_compose_paths = [docker_compose_paths, ] - - config_paths = [ f"-f {os.path.relpath(docker_compose_path, osparc_simcore_root_dir)}" for docker_compose_path in docker_compose_paths] - configs_prefix = " ".join(config_paths) - - # TODO: use instead python api of docker-compose! - subprocess.run( f"docker-compose {configs_prefix} config > {destination_path}", - shell=True, check=True, - cwd=osparc_simcore_root_dir) - - with destination_path.open() as f: - config = yaml.safe_load(f) - return config diff --git a/services/web/server/tests/integration/fixtures/docker_swarm.py b/services/web/server/tests/integration/fixtures/docker_swarm.py index fb7b199bd21..ddb07a1e82c 100644 --- a/services/web/server/tests/integration/fixtures/docker_swarm.py +++ b/services/web/server/tests/integration/fixtures/docker_swarm.py @@ -26,17 +26,21 @@ def docker_swarm(docker_client): # teardown assert docker_client.swarm.leave(force=True) + @pytest.fixture(scope='module') -def docker_stack(docker_swarm, docker_client, docker_compose_file: Path, ops_docker_compose_file: Path): - stacks = ['simcore', 'ops' ] +def docker_stack(docker_swarm, docker_client, core_services_config_file: Path, ops_services_config_file: Path): + stacks = { + 'simcore': core_services_config_file, + 'ops': ops_services_config_file + } # make up-version - subprocess.run( f"docker stack deploy -c {docker_compose_file.name} {stacks[0]}", - shell=True, check=True, - cwd=docker_compose_file.parent) - subprocess.run( f"docker stack deploy -c {ops_docker_compose_file.name} {stacks[1]}", - shell=True, check=True, - cwd=ops_docker_compose_file.parent) + stacks_up = [] + for stack_name, stack_config_file in stacks.items(): + subprocess.run( f"docker stack deploy -c {stack_config_file.name} {stack_name}", + shell=True, check=True, + cwd=stack_config_file.parent) + stacks_up.append(stack_name) def _print_services(msg): from pprint import pprint @@ -48,7 +52,7 @@ def _print_services(msg): _print_services("[BEFORE TEST]") yield { - 'stacks':stacks, + 'stacks': stacks_up, 'services': [service.name for service in docker_client.services.list()] } @@ -70,14 +74,15 @@ def _print_services(msg): # make down # NOTE: remove them in reverse order since stacks share common networks - stacks.reverse() - for stack in stacks: + WAIT_BEFORE_RETRY_SECS = 1 + stacks_up.reverse() + for stack in stacks_up: subprocess.run(f"docker stack rm {stack}", shell=True, check=True) while docker_client.services.list(filters={"label":f"com.docker.stack.namespace={stack}"}): - time.sleep(1) + time.sleep(WAIT_BEFORE_RETRY_SECS) while docker_client.networks.list(filters={"label":f"com.docker.stack.namespace={stack}"}): - time.sleep(1) + time.sleep(WAIT_BEFORE_RETRY_SECS) _print_services("[AFTER REMOVED]") diff --git a/services/web/server/tests/integration/fixtures/postgres_service.py b/services/web/server/tests/integration/fixtures/postgres_service.py index 9a4ade5d8cb..fda100a0df0 100644 --- a/services/web/server/tests/integration/fixtures/postgres_service.py +++ b/services/web/server/tests/integration/fixtures/postgres_service.py @@ -4,6 +4,8 @@ # pylint:disable=unused-argument # pylint:disable=redefined-outer-name +from copy import deepcopy + import pytest import sqlalchemy as sa import tenacity @@ -14,8 +16,8 @@ @pytest.fixture(scope='module') -def postgres_db(app_config, webserver_environ, docker_stack): - cfg = app_config["db"]["postgres"] +def postgres_db(_webserver_dev_config, webserver_environ, docker_stack): + cfg = deepcopy(_webserver_dev_config["db"]["postgres"]) url = DSN.format(**cfg) # NOTE: Comment this to avoid postgres_service diff --git a/services/web/server/tests/integration/fixtures/rabbit_service.py b/services/web/server/tests/integration/fixtures/rabbit_service.py index a357c2fa8fe..bee2052a840 100644 --- a/services/web/server/tests/integration/fixtures/rabbit_service.py +++ b/services/web/server/tests/integration/fixtures/rabbit_service.py @@ -4,6 +4,7 @@ # pylint:disable=unused-argument # pylint:disable=redefined-outer-name +from copy import deepcopy from typing import Dict import aio_pika @@ -12,8 +13,8 @@ @pytest.fixture(scope="function") -async def rabbit_service(app_config: Dict, docker_stack): - cfg = app_config["rabbit"] +async def rabbit_service(_webserver_dev_config: Dict, docker_stack): + cfg = deepcopy(_webserver_dev_config["rabbit"]) host = cfg["host"] port = cfg["port"] user = cfg["user"] diff --git a/services/web/server/tests/integration/fixtures/standard_directories.py b/services/web/server/tests/integration/fixtures/standard_directories.py deleted file mode 100644 index 92b49e0b5f6..00000000000 --- a/services/web/server/tests/integration/fixtures/standard_directories.py +++ /dev/null @@ -1,89 +0,0 @@ -# pylint:disable=wildcard-import -# pylint:disable=unused-import -# pylint:disable=unused-variable -# pylint:disable=unused-argument -# pylint:disable=redefined-outer-name - -import sys -from pathlib import Path - -import pytest - -import simcore_service_webserver - - -@pytest.fixture(scope='session') -def fixture_dir() -> Path: - return Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent - -@pytest.fixture(scope='session') -def package_dir() -> Path: - dirpath = Path(simcore_service_webserver.__file__).resolve().parent - assert dirpath.exists() - return dirpath - -@pytest.fixture(scope='session') -def osparc_simcore_root_dir(fixture_dir: Path) -> Path: - root_dir = fixture_dir.parent.parent.parent.parent.parent.parent.resolve() - assert root_dir.exists(), "Is this service within osparc-simcore repo?" - assert any(root_dir.glob("services/web/server")), "%s not look like rootdir" % root_dir - return root_dir - -@pytest.fixture(scope='session') -def api_specs_dir(osparc_simcore_root_dir: Path) -> Path: - specs_dir = osparc_simcore_root_dir/ "api" / "specs" / "webserver" - assert specs_dir.exists() - return specs_dir - -@pytest.fixture(scope='session') -def integration_test_dir(fixture_dir: Path) -> Path: - tests_dir = fixture_dir.parent.resolve() - assert tests_dir.exists() - return tests_dir - -@pytest.fixture(scope='session') -def tests_dir(integration_test_dir: Path) -> Path: - tests_dir = integration_test_dir.parent.resolve() - assert tests_dir.exists() - return tests_dir - -@pytest.fixture(scope='session') -def fake_data_dir(tests_dir: Path) -> Path: - fake_data_dir = tests_dir / "data" - assert fake_data_dir.exists() - return fake_data_dir - -# @pytest.fixture(scope='session') -# def mock_dir(fixture_dir): -# dirpath = fixture_dir / "mock" -# assert dirpath.exists() -# return dirpath - -# @pytest.fixture(scope='session') -# def docker_compose_file(mock_dir): -# """ -# Path to docker-compose configuration files used for testing - -# - fixture defined in pytest-docker -# """ -# fpath = mock_dir / 'docker-compose.yml' -# assert fpath.exists() -# return str(fpath) - -@pytest.fixture(scope="session") -def server_test_configfile(mock_dir): - fpath = mock_dir / "configs/server-host-test.yaml" - assert fpath.exists() - return fpath - -@pytest.fixture(scope="session") -def light_test_configfile(mock_dir): - fpath = mock_dir / "configs/light-test.yaml" - assert fpath.exists() - return fpath - -@pytest.fixture("session") -def env_devel_file(osparc_simcore_root_dir) -> Path: - env_devel_fpath = osparc_simcore_root_dir / ".env-devel" - assert env_devel_fpath.exists() - return env_devel_fpath diff --git a/services/web/server/tests/integration/test_project_workflow.py b/services/web/server/tests/integration/test_project_workflow.py index 1961151403d..3889f8c57f1 100644 --- a/services/web/server/tests/integration/test_project_workflow.py +++ b/services/web/server/tests/integration/test_project_workflow.py @@ -3,27 +3,20 @@ e.g. run, pull, push ,... pipelines This one here is too similar to unit/with_postgres/test_projects.py """ - -# pylint:disable=wildcard-import -# pylint:disable=unused-import # pylint:disable=unused-variable # pylint:disable=unused-argument # pylint:disable=redefined-outer-name import json -import sys from asyncio import Future from copy import deepcopy from pathlib import Path -from pprint import pprint from typing import Dict, List import pytest from aiohttp import web from servicelib.application import create_safe_application -from servicelib.application_keys import APP_CONFIG_KEY -from servicelib.rest_responses import unwrap_envelope from simcore_service_webserver.db import setup_db from simcore_service_webserver.login import setup_login from simcore_service_webserver.projects import setup_projects @@ -48,9 +41,31 @@ # 'adminer' ] -@pytest.fixture(scope='session') -def here() -> Path: - return Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent + +@pytest.fixture +def client(loop, aiohttp_client, + app_config, ## waits until swarm with *_services are up + ): + assert app_config["rest"]["version"] == API_VERSION + assert API_VERSION in app_config["rest"]["location"] + + app_config['storage']['enabled'] = False + app_config['rabbit']['enabled'] = False + + app = create_safe_application(app_config) + + setup_db(app) + setup_session(app) + setup_security(app) + setup_rest(app) + setup_login(app) + assert setup_projects(app) + + yield loop.run_until_complete(aiohttp_client(app, server_kwargs={ + 'port': app_config["main"]["port"], + 'host': app_config['main']['host'] + })) + @pytest.fixture(scope="session") def fake_template_projects(package_dir: Path) -> Dict: @@ -84,35 +99,6 @@ def fake_project_data(fake_data_dir: Path) -> Dict: with (fake_data_dir / "fake-project.json").open() as fp: return json.load(fp) -@pytest.fixture -def webserver_service(loop, docker_stack, aiohttp_server, aiohttp_unused_port, api_specs_dir, app_config): -# def webserver_service(loop, aiohttp_server, aiohttp_unused_port, api_specs_dir, app_config): # <<< DEVELOPMENT - port = app_config["main"]["port"] = aiohttp_unused_port() - app_config['main']['host'] = '127.0.0.1' - - assert app_config["rest"]["version"] == API_VERSION - assert API_VERSION in app_config["rest"]["location"] - - app_config['storage']['enabled'] = False - app_config['rabbit']['enabled'] = False - - app = create_safe_application(app_config) - - setup_db(app) - setup_session(app) - setup_security(app) - setup_rest(app) - setup_login(app) - assert setup_projects(app) - - yield loop.run_until_complete( aiohttp_server(app, port=port) ) - -@pytest.fixture -def client(loop, webserver_service, aiohttp_client): - client = loop.run_until_complete(aiohttp_client(webserver_service)) - yield client - - @pytest.fixture async def logged_user(client): #, role: UserRole): """ adds a user in db and logs in with client diff --git a/services/web/server/tests/integration-proxy/Makefile b/services/web/server/tests/sandbox/TODO - integration-proxy/Makefile similarity index 100% rename from services/web/server/tests/integration-proxy/Makefile rename to services/web/server/tests/sandbox/TODO - integration-proxy/Makefile diff --git a/services/web/server/tests/integration-proxy/conftest.py b/services/web/server/tests/sandbox/TODO - integration-proxy/conftest.py similarity index 100% rename from services/web/server/tests/integration-proxy/conftest.py rename to services/web/server/tests/sandbox/TODO - integration-proxy/conftest.py diff --git a/services/web/server/tests/integration-proxy/test_application_proxy.py b/services/web/server/tests/sandbox/TODO - integration-proxy/test_application_proxy.py similarity index 100% rename from services/web/server/tests/integration-proxy/test_application_proxy.py rename to services/web/server/tests/sandbox/TODO - integration-proxy/test_application_proxy.py diff --git a/services/web/server/tests/unit/conftest.py b/services/web/server/tests/unit/conftest.py index 13ab8432a6b..247b02ba059 100644 --- a/services/web/server/tests/unit/conftest.py +++ b/services/web/server/tests/unit/conftest.py @@ -1,87 +1,39 @@ +""" Configuration for unit testing + + - Any interaction with other app MUST be emulated with fakes/mocks + - ONLY external apps allowed is postgress (see unit/with_postgres) +""" + # pylint: disable=unused-argument -# pylint: disable=unused-import # pylint: disable=bare-except # pylint:disable=redefined-outer-name -import collections import json import logging -import os import sys from pathlib import Path from typing import Dict import pytest -import yaml -import simcore_service_webserver -from simcore_service_webserver.cli_config import read_and_validate +## current directory +current_dir = Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent +## Log log = logging.getLogger(__name__) -# mute noisy loggers -logging.getLogger("openapi_spec_validator").setLevel(logging.WARNING) -logging.getLogger("sqlalchemy").setLevel(logging.WARNING) - -sys.path.append(str(Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent.parent / 'helpers')) @pytest.fixture(scope='session') def here(): - return Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent - -@pytest.fixture(scope='session') -def package_dir(here): - dirpath = Path(simcore_service_webserver.__file__).resolve().parent - assert dirpath.exists() - return dirpath + cdir = Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent + assert cdir == current_dir, "Somebody changing current_dir?" + return cdir -@pytest.fixture(scope='session') -def osparc_simcore_root_dir(here): - root_dir = here.parent.parent.parent.parent.parent.resolve() - assert root_dir.exists(), "Is this service within osparc-simcore repo?" - assert any(root_dir.glob("services/web/server")), "%s not look like rootdir" % root_dir - return root_dir @pytest.fixture(scope='session') -def api_specs_dir(osparc_simcore_root_dir): - specs_dir = osparc_simcore_root_dir/ "api" / "specs" / "webserver" - assert specs_dir.exists() - return specs_dir +def fake_static_dir(fake_data_dir: Path) -> Dict: + return fake_data_dir / "static" -@pytest.fixture(scope='session') -def mock_dir(here): - dirpath = here / "mock" - assert dirpath.exists() - return dirpath - -@pytest.fixture(scope='session') -def fake_data_dir(here): - dirpath = (here / "../data").resolve() - assert dirpath.exists() - return dirpath - -@pytest.fixture(scope='session') -def docker_compose_file(mock_dir): - """ - Path to docker-compose configuration files used for testing - - - fixture defined in pytest-docker - """ - fpath = mock_dir / 'docker-compose.yml' - assert fpath.exists() - return str(fpath) - -@pytest.fixture(scope="session") -def server_test_configfile(mock_dir): - fpath = mock_dir / "configs/server-host-test.yaml" - assert fpath.exists() - return fpath - -@pytest.fixture(scope="session") -def light_test_configfile(mock_dir): - fpath = mock_dir / "configs/light-test.yaml" - assert fpath.exists() - return fpath @pytest.fixture def fake_project(fake_data_dir: Path) -> Dict: @@ -89,6 +41,7 @@ def fake_project(fake_data_dir: Path) -> Dict: yield json.load(fp) + @pytest.fixture def project_schema_file(api_specs_dir: Path) -> Path: return api_specs_dir / "v0/components/schemas/project-v0.0.1.json" diff --git a/services/web/server/tests/unit/mock/configs/light-test.yaml b/services/web/server/tests/unit/mock/configs/light-test.yaml deleted file mode 100644 index 5f47c870180..00000000000 --- a/services/web/server/tests/unit/mock/configs/light-test.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# This config is used for testing on the host ---- -version: "1.0" -main: - host: 127.0.0.1 - port: 8080 - client_outdir: ../../../client/source-output - log_level: DEBUG - testing: True -director: - host: localhost - port: 8001 -db: - enabled: False - postgres: - database: test_db - user: test_user - password: test_pass - host: localhost - port: 0000 - # DEPRECATE OR add postgresql+psycopg2:// otherwise will fail sqlalchemy.exc.ArgumentError: Could not parse rfc1738 URL from string 'localhost:5432' - endpoint: localhost:5432 -rabbit: - enabled: False - host: ${RABBIT_HOST} - password: simcore - user: simcore - channels: - log: comp.backend.channels.log - progress: comp.backend.channels.progress -# s3: -# access_key: '12345678' -# bucket_name: simcore -# endpoint: localhost:9000 -# secret_key: '12345678' -smtp: - sender: 'OSPARC support ' - host: mail.foo.com - port: 25 - tls: False - username: None - password: None -rest: - version: v0 - location: api/specs/webserver/v0/openapi.yaml -... diff --git a/services/web/server/tests/unit/mock/configs/minimum.yaml b/services/web/server/tests/unit/mock/configs/minimum.yaml deleted file mode 100644 index ebb0ab90620..00000000000 --- a/services/web/server/tests/unit/mock/configs/minimum.yaml +++ /dev/null @@ -1,48 +0,0 @@ -# This config is used for testing on the host ---- -version: "1.0" -main: - host: 127.0.0.1 - port: 8080 - client_outdir: client/source-output - log_level: DEBUG - testing: True -director: - host: localhost - port: 8001 -db: - enabled: False - postgres: - database: test_db - user: test_user - password: test_pass - host: localhost - port: 0000 - # DEPRECATE OR add postgresql+psycopg2:// otherwise will fail sqlalchemy.exc.ArgumentError: Could not parse rfc1738 URL from string 'localhost:5432' - endpoint: localhost:5432 -rabbit: - enabled: False - host: foo - password: simcore - user: simcore - channels: - log: comp.backend.channels.log - progress: comp.backend.channels.progress -s3: - access_key: '12345678' - bucket_name: simcore - endpoint: localhost:9000 - secret_key: '12345678' -smtp: - sender: 'OSPARC support ' - host: mail.foo.com - port: 25 - tls: False - username: None - password: None -rest: - version: v0 - location: api/specs/webserver/v0/openapi.yaml -session: - secret_key: 'Thirty two length bytes key.' -... diff --git a/services/web/server/tests/unit/mock/configs/server-host-test.yaml b/services/web/server/tests/unit/mock/configs/server-host-test.yaml deleted file mode 100644 index 83ff8e6bb24..00000000000 --- a/services/web/server/tests/unit/mock/configs/server-host-test.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# This config is used for testing on the host ---- -version: "1.0" -main: - host: 127.0.0.1 - port: 8080 - client_outdir: ../../../client/source-output - log_level: DEBUG - testing: True -director: - host: localhost - port: 8001 -db: - postgres: - database: test_db - user: test_user - password: test_pass - host: localhost - port: ${POSTGRES_PORT} - # DEPRECATE OR add postgresql+psycopg2:// otherwise will fail sqlalchemy.exc.ArgumentError: Could not parse rfc1738 URL from string 'localhost:5432' - endpoint: localhost:5432 -rabbit: - host: ${RABBIT_HOST} - password: simcore - user: simcore - channels: - log: comp.backend.channels.log - progress: comp.backend.channels.progress -s3: - access_key: '12345678' - bucket_name: simcore - endpoint: localhost:9000 - secret_key: '12345678' -smtp: - sender: 'OSPARC support ' - host: mail.foo.com - port: 25 - tls: False - username: None - password: None -rest: - version: v0 - location: api/specs/webserver/v0/openapi.yaml -session: - secret_key: "Thirty two length bytes key." -... diff --git a/services/web/server/tests/unit/mock/docker-compose.yml b/services/web/server/tests/unit/mock/docker-compose.yml deleted file mode 100644 index 76eace917bc..00000000000 --- a/services/web/server/tests/unit/mock/docker-compose.yml +++ /dev/null @@ -1,24 +0,0 @@ -version: '3.4' -services: - postgres: - image: postgres:10 - #volumes: TODO: make db persistent - # - '.:/home/scu/client' - ports: - - '5432:5432' - adminer: - image: adminer - ports: - - 18080:8080 - depends_on: - - postgres - rabbit: - image: rabbitmq:3-management - environment: - - RABBITMQ_DEFAULT_USER=simcore - - RABBITMQ_DEFAULT_PASS=simcore - ports: - # NOTE: these need to added because test server runs in host! - - "15672:15672" - - "5671:5671" - - "5672:5672" diff --git a/services/web/server/tests/unit/test_template_projects.py b/services/web/server/tests/unit/test_template_projects.py index c043328173f..43d3e8ce575 100644 --- a/services/web/server/tests/unit/test_template_projects.py +++ b/services/web/server/tests/unit/test_template_projects.py @@ -40,8 +40,8 @@ def fake_db(): @pytest.fixture -def mock_parametrized_project(mock_dir): - path = mock_dir/"parametrized_project.json" +def mock_parametrized_project(fake_data_dir): + path = fake_data_dir/"parametrized_project.json" with path.open() as fh: prj = json.load(fh) diff --git a/services/web/server/tests/unit/with_postgres/config.yaml b/services/web/server/tests/unit/with_postgres/config.yaml index a7be72569bb..b68e278de1e 100644 --- a/services/web/server/tests/unit/with_postgres/config.yaml +++ b/services/web/server/tests/unit/with_postgres/config.yaml @@ -1,6 +1,6 @@ version: '1.0' main: - client_outdir: ${OSPARC_SIMCORE_REPO_ROOTDIR}/services/web/server/tests/unit/mock/static + client_outdir: ${OSPARC_SIMCORE_REPO_ROOTDIR}/services/web/server/tests/data/static host: 127.0.0.1 log_level: DEBUG port: 8080 diff --git a/services/web/server/tests/unit/with_postgres/conftest.py b/services/web/server/tests/unit/with_postgres/conftest.py index e133fc14efd..b0efc539d59 100644 --- a/services/web/server/tests/unit/with_postgres/conftest.py +++ b/services/web/server/tests/unit/with_postgres/conftest.py @@ -1,27 +1,23 @@ -""" Fixtures for this folder's tests +""" Configuration for unit testing with a postgress fixture -Notice that fixtures in ../conftest.py are also accessible here + - Unit testing of webserver app with a postgress service as fixture + - Starts test session by running a postgres container as a fixture (see postgress_service) + IMPORTANT: remember that these are still unit-tests! """ -# pylint:disable=wildcard-import -# pylint:disable=unused-import # pylint:disable=unused-variable # pylint:disable=unused-argument # pylint:disable=redefined-outer-name - -import json import os import sys from asyncio import Future from copy import deepcopy from pathlib import Path -from typing import Dict import pytest import sqlalchemy as sa import trafaret_config -import yaml import simcore_service_webserver.utils from simcore_service_webserver.application import create_application @@ -30,40 +26,14 @@ from simcore_service_webserver.db import DSN from simcore_service_webserver.db_models import confirmations, metadata, users -tests_folder = Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent.parent.parent -sys.path.append(str(tests_folder/ 'helpers')) - - -@pytest.fixture(scope="session") -def here(): - return Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent - -@pytest.fixture(scope="session") -def mock_dir(here): - return here / "../mock" +## current directory +current_dir = Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent -@pytest.fixture(scope='session') -def fake_data_dir(here): - dirpath = (here / "../../data").resolve() - assert dirpath.exists() - return dirpath - -@pytest.fixture -def fake_project(fake_data_dir: Path) -> Dict: - with (fake_data_dir / "fake-project.json").open() as fp: - yield json.load(fp) - -@pytest.fixture(scope='session') -def osparc_simcore_root_dir(here): - root_dir = here.parent.parent.parent.parent.parent.parent.resolve() - assert root_dir.exists(), "Is this service within osparc-simcore repo?" - assert any(root_dir.glob("services/web/server")), "%s not look like rootdir" % root_dir - return root_dir @pytest.fixture(scope="session") -def default_app_cfg(here, osparc_simcore_root_dir): +def default_app_cfg(osparc_simcore_root_dir, fake_static_dir): # NOTE: ONLY used at the session scopes - cfg_path = here / "config.yaml" + cfg_path = current_dir / "config.yaml" assert cfg_path.exists() variables = dict(os.environ) @@ -74,11 +44,12 @@ def default_app_cfg(here, osparc_simcore_root_dir): # validates and fills all defaults/optional entries that normal load would not do cfg_dict = trafaret_config.read_and_validate(cfg_path, app_schema, vars=variables) + assert Path(cfg_dict["main"]["client_outdir"]) == fake_static_dir + # WARNING: changes to this fixture during testing propagates to other tests. Use cfg = deepcopy(cfg_dict) # FIXME: free cfg_dict but deepcopy shall be r/w return cfg_dict - @pytest.fixture(scope="function") def app_cfg(default_app_cfg, aiohttp_unused_port): cfg = deepcopy(default_app_cfg) @@ -90,10 +61,10 @@ def app_cfg(default_app_cfg, aiohttp_unused_port): # this fixture can be safely modified during test since it is renovated on every call return cfg - @pytest.fixture(scope='session') -def docker_compose_file(here, default_app_cfg): +def docker_compose_file(default_app_cfg): """ Overrides pytest-docker fixture + """ old = os.environ.copy() @@ -104,7 +75,7 @@ def docker_compose_file(here, default_app_cfg): os.environ['TEST_POSTGRES_USER']=cfg['user'] os.environ['TEST_POSTGRES_PASSWORD']=cfg['password'] - dc_path = here / 'docker-compose.yml' + dc_path = current_dir / 'docker-compose.yml' assert dc_path.exists() yield str(dc_path) @@ -127,7 +98,6 @@ def postgres_service(docker_services, docker_ip, default_app_cfg): ) return url - @pytest.fixture def postgres_db(app_cfg, postgres_service): cfg = app_cfg["db"]["postgres"] @@ -145,7 +115,6 @@ def postgres_db(app_cfg, postgres_service): metadata.drop_all(engine) engine.dispose() - @pytest.fixture def server(loop, aiohttp_server, app_cfg, monkeypatch, postgres_db): #pylint: disable=R0913 app = create_application(app_cfg) @@ -153,13 +122,11 @@ def server(loop, aiohttp_server, app_cfg, monkeypatch, postgres_db): #pylint: di server = loop.run_until_complete( aiohttp_server(app, port=app_cfg["main"]["port"]) ) return server - @pytest.fixture def client(loop, aiohttp_client, server): client = loop.run_until_complete(aiohttp_client(server)) return client - @pytest.fixture async def storage_subsystem_mock(loop, mocker): """ @@ -180,7 +147,9 @@ async def _mock_copy_data_from_project(*args): mock1.return_value.set_result("") return mock, mock1 + # helpers --------------- + def path_mail(monkeypatch): async def send_mail(*args): print('=== EMAIL TO: {}\n=== SUBJECT: {}\n=== BODY:\n{}'.format(*args)) diff --git a/services/web/server/tests/unit/with_postgres/docker-compose.debug.yml b/services/web/server/tests/unit/with_postgres/docker-compose.debug.yml index fc1ef184537..d6eeb261d20 100644 --- a/services/web/server/tests/unit/with_postgres/docker-compose.debug.yml +++ b/services/web/server/tests/unit/with_postgres/docker-compose.debug.yml @@ -1,7 +1,7 @@ version: '3.4' services: postgres: - image: postgres:10 + image: postgres:10.10 restart: always environment: # defaults are the same as in conftest.yaml so we start compose from command line for debugging diff --git a/services/web/server/tests/unit/with_postgres/docker-compose.yml b/services/web/server/tests/unit/with_postgres/docker-compose.yml index e49fb5cdfd7..7b0c3fe4a51 100644 --- a/services/web/server/tests/unit/with_postgres/docker-compose.yml +++ b/services/web/server/tests/unit/with_postgres/docker-compose.yml @@ -1,7 +1,7 @@ version: '3.4' services: postgres: - image: postgres:10 + image: postgres:10.10 restart: always environment: POSTGRES_DB: ${TEST_POSTGRES_DB} diff --git a/services/web/server/tests/unit/with_postgres/test_access_to_studies.py b/services/web/server/tests/unit/with_postgres/test_access_to_studies.py index 2fbf94d26c4..70e38ad8d59 100644 --- a/services/web/server/tests/unit/with_postgres/test_access_to_studies.py +++ b/services/web/server/tests/unit/with_postgres/test_access_to_studies.py @@ -64,9 +64,6 @@ def client(loop, aiohttp_client, app_cfg, postgres_service, qx_client_outdir, mo #def client(loop, aiohttp_client, app_cfg, qx_client_outdir, monkeypatch): # <<<< FOR DEVELOPMENT. DO NOT REMOVE. cfg = deepcopy(app_cfg) - port = cfg["main"]["port"] - cfg['main']['host'] = '127.0.0.1' - cfg["db"]["init_tables"] = True # inits tables of postgres_service upon startup cfg['projects']['enabled'] = True cfg['storage']['enabled'] = False @@ -86,8 +83,8 @@ def client(loop, aiohttp_client, app_cfg, postgres_service, qx_client_outdir, mo # server and client yield loop.run_until_complete(aiohttp_client(app, server_kwargs={ - 'port': port, - 'host': 'localhost' + 'port': cfg["main"]["port"], + 'host': cfg['main']['host'] })) diff --git a/services/web/server/tests/unit/with_postgres/test_db.py b/services/web/server/tests/unit/with_postgres/test_db.py index 72dc0c04137..4a55c5152c8 100644 --- a/services/web/server/tests/unit/with_postgres/test_db.py +++ b/services/web/server/tests/unit/with_postgres/test_db.py @@ -1,5 +1,18 @@ -from simcore_service_webserver.db import is_service_enabled, is_service_responsive +import io +import yaml + +from simcore_service_webserver.db import (is_service_enabled, + is_service_responsive) + +def test_uses_same_postgres_version(docker_compose_file, osparc_simcore_root_dir): + with io.open(docker_compose_file) as fh: + fixture = yaml.safe_load(fh) + + with io.open(osparc_simcore_root_dir / "services" / "docker-compose.yml") as fh: + expected = yaml.safe_load(fh) + + assert fixture['services']['postgres']['image'] == expected['services']['postgres']['image'] async def test_responsive(server): From 18a2f035da7c6a387d567637362fb0daf83104ac Mon Sep 17 00:00:00 2001 From: Pedro Crespo <32402063+pcrespov@users.noreply.github.com> Date: Tue, 19 Nov 2019 18:19:52 +0100 Subject: [PATCH 2/7] Is880/fix hanging (#1160) Fixes #880: authorization module retries call to db upon failure. If still fails, then it responds 503. enhanced log info on request that produced error - Adds application name in db connections - General cleanup of engine disposal - Graceful services shutdown (fixes based on input from https://hynek.me/articles/docker-signals/ ) - Modified boot and entrypoints of services in production - added new test - changes in system tests: deploy done as fixture in tests - adds watchdog in development mode to auto reboot storage and webserver when files changed (thanks to @sanderegg ) --- .../webserver/v0/openapi-diagnostics.yaml | 1 + bug - login webserver 2019-11-13 14:19:39.png | Bin 0 -> 168243 bytes ci/travis/integration-testing/simcore-sdk | 2 +- ci/travis/system-testing/swarm-deploy | 3 - .../src/simcore_postgres_database/cli.py | 10 +- .../src/servicelib/aiopg_utils.py | 72 +------- .../src/servicelib/application.py | 11 +- .../src/servicelib/application_keys.py | 29 ++-- .../src/servicelib/monitoring.py | 16 +- .../src/servicelib/rest_middlewares.py | 21 +-- .../src/simcore_sdk/models/pipeline_models.py | 2 +- .../src/simcore_sdk/node_ports/dbmanager.py | 6 +- packages/simcore-sdk/tests/__init__.py | 0 packages/simcore-sdk/tests/conftest.py | 13 +- .../simcore-sdk/tests/fixtures/postgres.py | 3 +- .../simcore-sdk/tests/node_ports/conftest.py | 31 ++-- .../tests/node_ports/helpers/__init__.py | 0 .../{helpers/helpers.py => np_helpers.py} | 0 .../tests/node_ports/test_nodeports.py | 17 +- packages/simcore-sdk/tests/test_alchemy.py | 22 +-- services/sidecar/docker/boot.sh | 2 +- services/sidecar/docker/entrypoint.sh | 2 +- services/sidecar/src/sidecar/utils.py | 12 +- services/sidecar/tests/utils.py | 10 +- services/storage/docker/boot.sh | 11 +- services/storage/docker/entrypoint.sh | 2 +- services/storage/requirements/dev.txt | 3 + .../simcore_service_storage/application.py | 20 +-- .../storage/src/simcore_service_storage/db.py | 47 +++--- .../src/simcore_service_storage/dsm.py | 7 +- .../src/simcore_service_storage/settings.py | 1 - services/storage/tests/conftest.py | 4 +- services/storage/tests/utils.py | 3 + services/web/server/docker/boot.sh | 11 +- services/web/server/docker/entrypoint.sh | 2 +- services/web/server/requirements/dev.txt | 3 + .../src/simcore_service_webserver/db.py | 42 +++-- .../simcore_service_webserver/db_config.py | 12 ++ .../login/__init__.py | 15 +- .../security_authorization.py | 72 ++++---- .../web/server/tests/helpers/utils_tokens.py | 1 + .../tests/unit/with_postgres/conftest.py | 8 +- .../tests/unit/with_postgres/test_db.py | 4 +- .../tests/unit/with_postgres/test_login.py | 6 +- .../tests/unit/with_postgres/test_users.py | 44 ++++- tests/swarm-deploy/Makefile | 30 ++++ tests/swarm-deploy/conftest.py | 107 ++++++++++++ tests/swarm-deploy/test_service_restart.py | 114 +++++++++++++ tests/swarm-deploy/test_swarm_runs.py | 156 +++++++++--------- 49 files changed, 650 insertions(+), 360 deletions(-) create mode 100644 bug - login webserver 2019-11-13 14:19:39.png delete mode 100644 packages/simcore-sdk/tests/__init__.py delete mode 100644 packages/simcore-sdk/tests/node_ports/helpers/__init__.py rename packages/simcore-sdk/tests/node_ports/{helpers/helpers.py => np_helpers.py} (100%) create mode 100644 tests/swarm-deploy/Makefile create mode 100644 tests/swarm-deploy/conftest.py create mode 100644 tests/swarm-deploy/test_service_restart.py diff --git a/api/specs/webserver/v0/openapi-diagnostics.yaml b/api/specs/webserver/v0/openapi-diagnostics.yaml index 5593b5513ad..1f4c8aae37f 100644 --- a/api/specs/webserver/v0/openapi-diagnostics.yaml +++ b/api/specs/webserver/v0/openapi-diagnostics.yaml @@ -31,6 +31,7 @@ paths: schema: type: string - in: path + required: true name: action schema: type: string diff --git a/bug - login webserver 2019-11-13 14:19:39.png b/bug - login webserver 2019-11-13 14:19:39.png new file mode 100644 index 0000000000000000000000000000000000000000..0f5c76735fefec6a68c72b0ce20cb639f52bc81b GIT binary patch literal 168243 zcmbTdRd5{5wx(S|3oK>^3oT|wiL>PQ-}RfcyxJ zpr;}6hzKg0#2-{SdmQ3b&~?xp=ixz~5ULPNdNa^7CY}u1e^+{A^}VKvnk%=NRNHDxc3oyfe35jzfQmA-{%=m=aCFR4SF}72t5!yza1aXl5cn}74Kw+v{ZeS zDAx1=&3=IPucJ?X*NDEnNcKx%c^@PU)16!g)M8Fpq}y*xQCi-tq)#5bWp0y#Zw|K& z1vz=%g`+Q)<5?>F@HDHLHd5MkXhzm!-wU{ZG9+&|lyrwtqlcyZ9>Z;HHXlpqovKfN zFI?#LkAq~13R%h}`^1T|>Tl-uF6kyc9Uh%@y~SwvBmKC7?v= zmf$)NIXTrk_uHG*SLWn*Ug+`)hU845;uAEKt~KNNvo2|x5g~{!E;j=S3tV|22e+7h zTWK3rbc9NMBVwekj(AFMiLq5aM(Pt~N|MD)HWl=@Nog7Y^@-Bv{&_ozB~w~U8?DOQ zIW0~pxgDt-&3b)}?W;k(y_axQzmIhw1r_xx%lfrFY{}pcSufVjD5`8g_@Z5q1R=Q7 zh#B`I?Em_@*6MH$VENz2|D2A3N)PM$&x?XP`H>xn|L2|D9iBA*Jy^dooGeXuM%7t#UK{r?fc1Z^=3G&e@8sju@u?_rI zq?CTjDG9EeF$(=p_k~hZv!5&~`gAOKnVYr|uVt0xQ|%izkK3%CgTMXfdKiuvXP{7q zk4zj6&(nlPU8EKSg=U!f33}mY))miKDPy! z%_v8j|E}b=6&KTZ)jYj6)cf{(m(IzCTDkR)zT0()5Op(SWwm***sZNCBeRFOgvALu z8x*&7SIgbvMC>9X?}}_ZE%mVx^gEuC6pu@-%zIz@RQv|$ozI&*#T&|W9l8(-rTwtlQ$KiRL^ta4GeuFecjw+6}kCAto+1Y&T7~!8oPu=rg>@%K8T9-qc z3PxyJB>CmG&r;u7ZJw7R5Dio(%FEGbo2S<43kx6bA_f^05DCR%ObWl2GQ_#anLc+V zoUzNx@~cH$*+GFFhQ?E9KP<8f0e(B*E>gZ6r{lPLOU}}S`Cp#1OS||N_-9d7$EzWG z$VAwC0I0vXz&V@V_jZLqePT?0(=*uP$sCj%aG-kDHMMEvgV@05w$^)Q>mPbFi(dWo zy3NIuA&drTK3F9D_?n6j3ut|(Ov~qyVI6mg{Gq9Q+Tq!ezOb3U82T|li!Qq=K3L6k$N0iJWDlyH@P-71ea{RE%d%VxJEbhNp>`xsJW7RhRDd4oZ z@WTU2)<(P$AwhFAm79=m90T3g*|gR`K-OO%K zKI3lXrg^9LgFPhhXV`k5_`H4pr-XjI)FC6!(@X?|7H(7P7twHqLI$ndnePp2O#T3{ zv^tD@zxZd)f*tDvHJ%U?M=}JEkjL1(msU1u!G5%Zh9$l$AuMFY-Y(O+@)yS$j}U#s zl4!+RQQvW0D`$Ft-0r?}=p!ruO`a-2y`5+nF*3t4!emIF2njUT^E)lRHcs zn6NC-Gf^3afRv$^Q0Ik;GIQZ7=}}=J0{~S13e4NwAX?jkczP`odPNX3-0MzjkeJaprC`7A$(AM|F0?G*8+8?xNg9FNjMxWpE zClS_~tytdap@ht2@vA`LjNMXP{w@J{0E=>; z>4-+&Z7djh(4z0(4|&m~(vSh9px8XNclG}aZjT?jlER+a?K#`cvi04JrMtmCQ`i`Hc-b1Btzwu9Gm ze3AHk8a(soWI-EGCPyig%A2k5K#Iy*U7r#Z08lJ8jS0}R{84;`mvU}CDEzt!u?o?l z%|qE}K3FVk1^}8(@lM%Ag%&9xq##!_-0zZVh*-KB{wz+?n5$A*QgLm!1ncgQnN=@? zbJZIZ%*4qjD?Bu5wu;=V#^;wsp{?#wr0KQA24gqi01Q3(+mxtmBJ@`nlwRhU;9VRs zB6|0M8*opPFT+E{DXZ$sX8sn8WZ#nE`$36eSnzNMng=a*jLDu8H6ueS5ys(8H=chZ z{ij<#&0-kQK6mQFG;Hw?7R$%5T(3mil>9$5(SDIA;e=YJ$UBSMI6{A$l_s?)Ppbvk8Z+I!33u!p<1pEW*K;}b-Esg)-| z-%V;%>V#fEgN2Z8ahuGNMu>0d{-DP5QHmG24?(|gCUy4e(3|L&gSN%=ln>YKi!OF7i@qRX| zu2?9Sn&|#P_#mlLfWeJI98Pa#P3p&yvX=}JB$>!cyE}@-Y`moiF|?|86e%2FtJBl+ zF2}@ifF!Ycm$^(mCNP{vWEH0d8M8ieix)bNwDcwJtlZn!Q4rCHeogSxgl@Ll=hrp5 z)_n&Nm)FPmA2BkaDu+bzd3HD1deitjh2Lbi=8ZKub`qeO##ZV~153CdLMR*@UD~Y+ z8B;tkE%K$2csqLl^ECg? zUjNBD-^HNe7N;PFle&Lm`4|6pDmC)XJe=5hwd-Gp`mgx#627pT4gF#`{`KFNB=-YO z?B<03jFJ5Amg<`nhDxeNc>K7va-Fz-TWk!HU+5=?hO>-nDUF?~RXZv8xTNFs0abK1 zOLCZ!wF1ppbRH+uM^OYNIq?#TG*5`tkhVZC{|i-N9<;2)`K*wz<~&_5CM9}|f-szT zywuCmoIM0t5uG9;F%^casmkL*;}>-}2+@M$tnc1UgL#n%n`im)8(&$A)uX%2P(|nG>7Lqkf1;9xlrvuAHkPd1>2=4IYiE|e^}Ecv zG4(dOhPne}F1|HR8zoLAy!b2okuA>$N32mXBpQwpR-b>UbGB2FhLpHJ+$Fmpq?{t& zKFX@8XsXa$5x!@Os|`mv`WHDDO81{2OK?T?KHX%8h=yN{wqms}CfMJR_tYSqbMdO| zD!{`Bop3qrwaLVRpNUH-;_mPZLMY=+bg^!E&e)(#gYc`W8F0qEjaXMy_hm+*Yz z!0YQ96k^emJ)yprsGwGgyf_S6^k;E9My^zdl%ZHC6P2cl&qil~H6X~m*0A#Gxfp%l zMFqnxx`fZZYT1ZUI25H6)$|x@G$acB|G9bs_Ot)zVE>n;*v8nG>|9l85rH1Mrm3 ze5`G?9HH-UpH=!?d_8Nj-|VL&_0gCnDGtdmQxhLKjt^#*w&F=)c12aDVw_P32#Mee ze)?_>2~vnOHy2~?uDZDtT{_7O82xuqb{iTPoQi!F@J|dH!<3(H4W^GAa@%FwL>~l4 zq+6IJs2blxMraY4mmF5Q<5c~FC4Bqw-u!m5ZNCKP53zIBG_F?gz)w6tr2*>G2!5XNa0kri1f~TZg^zXs?Fg< zcLd3*1}3D`ep2XD7{oRB4MJj$<4M8cZ?f)IeqpQaz#h5qF`aIB`A}D3{v5p8LyzGSSQ8>`2P>I-p%KF*7qfM%41WcC?#xv&+=NxfM`jluELx2> z*&)JYKnZ)x8R^Mq9buT%AM=mMG6<0Oa!7LwD=euyT*w? zF%&Ryp1RT4`ijPVn#0j|)t@gaSt!V6;krxG)Z>q6%lVbheI6TRv9|De?xXve6_Q}2 z6xe5EBarIzcftwfY9=0E1OZ$&uPeQzmS?YjYeXw;}n>z!vew$ zQ!mfjP{e@on4I+cV*X$Gs?jZ)fO^k*DqIvfzq?tY^wxJnOUS-qTC#4_+Fyy8c*@zzV=JMCyA3X5_L8&3;@>?{*93LP*wAAl|G~mseQO}mz=4&y^ zejua{&y-;Q7)qpj8n3t4FCs0!zqsYc>YUwPzArn_NcC?QiD)arY4ohi`MDFrqSwtb z?0+oZ;lka~lsgJpv~OFe_33Mni>vDB=o>N+ed!QTYVAuc#w|+(bIKoBROR6tkn=%W z}<)+feHftz~kQM3N_ zqLK?TV5z#6nQ}Fhly-b@uuREFb%{b)d?@)%RR=c=Qus1GkZBBL^r)Y8XT=rPDUJ9C zqf=kq8xI;-(kEjVj|nL%Hh%f?PhTdD234Jc&cNZ|@f{PNSFi2hxBJRdVbBqT@SQ)o zi-4n6MKas1f2yLy&ycP+7IQhcl=FhpCpjiDv+L1O1AS`KPc^UM19~Y3l=#gTzt&7z z+SroM1O&!s9s=f+3)Pb>j|oXsGkELwXSPR&M`xeaGOuNIlphSEl#{YvzUzs(;XhWb zX}EDXYGn;R*HSsJj{0s}R79qJ@G+m??H}SV*gqoq(|jeC)rN8R(&7Ed*)&IUyo#VA z8MQf$gy~$6c6mUvtRSK)v-L}ZwLJKt#)B+)3oZmj3*t7%`)Box?@&l;J-V%RLEYl zDHp_|Op+UXDmLBpUCEyb5P!M4zJM#iA~Tu8-nH_xeu$IRTtQ&h%zxq+avRp93Mq*a z8(vwb-K4W=d>~VI{xGZdr^JNGO6Kl-uXK2)p2}Ph?{TCO#TAaMM{gZZ?%t1W+=Nr} zoIqaM+|1XyWSR&*={~t~TxY%D;{8K7|lW$Gb^NpyV z{bFn#D|I&(ll^ObLPHJ6(KKhu-S`uaykwBK{K$^ItQ(b4tHc|jG~2N?B9(8b&hW9y z1B)h=*KK;Dc}$NBMPpJcY&rGQZQXG%HzLpR(%XXvEWNL^Uwt{P+Dg zFyIO;*=-Gz&O%*iZk-FNv9>7-7qFT1IELauoaO7k&#Y`U_v+4`lKmk{bVFQ_18YT$Dt58E_Yav;@Z5@cH}nA@g~=OF~v6PEki?V^NUthT%2=M zfv^0rWfe^7IWhjct6u%;?fDtuy6qKN=Fg3zOM)kj=~*$M!GS0O@xsxPx-%&;lJZSl z(SbGSky$>qQQMW5a}Uq+K~Os@nVPRteeL2ymk)Ri?|nEbnj4{%uUE&rO$;Jr&PZ!E zr5-jkb)6pej*nYc)<)`9mwdQ#qI&72(dL4eIm zv9)jHyIR)1E@5viaRek+;tC5M$8+$9o2p+D6%|SM{`V~V%VG{mr!g++H~f<_z{pS6+E|!aH8CLBl+!7~<0+`d_t?8-c79B30p0V3 z8rmcNu}n>r=er{?%-+qg)svC?)84L z_pVHZx~ceUiNZA9aTuR0MMrqC7=534R`~|p~qsC1FxPrIBe5+jrmde zsP>e?@JJY<1#C56d@P1iX~c1WY}SSQMST5)SDu1l>-od3q~FvKfba(Ix0&g~<)3~v zLOrbgk`lufI)YzUaXtdcjAF80<2AH+ma4vB0RSI=t>TDnj_}@tOXdw40G4fEQsW;D zz>PkHcQk$11a-h>bCYSMP#3`$N6w#->6$Pi+D-D{;lbGsQMHTmHc2#fSiHTRpKm1t z=rSGy<=$lN&$bVUC*ceXbRM@=zYgk`9i0rHv2FR%1a`R2Tv;?Y_sbkDJ1B(YJrdOX zM3QZX_Y?}KUKC7vxkXv)(IJtptL_EOnOYOP?Mz`cKq8q}x2yMk2Vnr^fi-<~Le76* zAk4FkAyPNRVlzTTA);5|$fSe*T>6LFq1i`q8jjI%9arBO-ln&+|3TTLN7lPjSkGxK z#DyLyD{*`E*yk9WZtD-~ z=o#chz}DwI*A82ol@m1f{r+Onj2WuAzVA5!i?UZXq17^#*n*4bPdPoC-XYlZ_GfM+ z%w~I_J&N4j?fjHKl$E72D8R=~){cCsW40}KGV2cwHo~&XJtCc zoLm#@ie##CHXaGDg;PrNNTzOrA=4pEn>Ql>0|zIc&bii-1>}H-&t~B`;}4>hHI1!~ zFd(3q+jEx-LN?f&yOD|(9YK5yP{;#!+wq6$c+Q`OC@?8A8c(g~aVBE>f z!-grb6mgB3tLRLa<%juS%nBKBZ(sh_00FZ0T_NOumEW^WA|C(&?as&p0$CoRp9JSb1-6(@%lNhi_Eigs8HvdO_ zX-{6+s_hWLly<{6%h>xfuALJrP4m;rv79js9!hh4DIfy`Wf83Jz*T=|*EW=v()8=n z#|*3NgmPmRtGPvZF{xg-iCc)kWr!Cdk#5fhO7Cq>V!Kb{U%l+IgHKE71KPy4r@Zn; zKG3Fgs2)9k@m{-!5_ozEunEs=Eb5N%Ho|iWJ?i(q3Lk@$ao?rV*H3ZtV!v)Ro12j8 zU{2jIpp_P#qC`rEA%4L)uke>SH~!*bTm9abRI|cDJw<}zcD<@*2@B*2VQ%cz$eY8e zl}^5tD0#*K8>|b4PuA*KP#uXUJZ~%MreLqo*iu`S=fUC6^xdWZ-lswbe>DpbMNheO zeTw&5<&4Y)0AbRUMx&d1k`o4NX%-u>JE0cMG4yv31kHTYSeYwP^sZC1*h-DQd}|{n z{!@wI>s99C<Cn#EdzbSYdIcjXYUH}_p@$mD2Lxy%=QuZh zJhTe(w3Sc>U4pu!@I^k1Ff`=x{SMQMALir4^cB-48E%*AwYMjnou?tTxQN&^{N2?I zPO)Amzb>RpJ4Z!Sj`e)- zo2|vho1-?Rem6VwfR)#)C(^dRgovci%Fah|I6tuvi@#!9zj)){EI_<~vU+jNtE$u_ z=AzM%&F0U>mq1+(Uj1H$^A}HPae7FgGZ_g#qugVW`FZ$=ZBLGGzg{wT)-qy^#V+;k zEh+J8{U<_}mWNCsli_Fnwj(1L0FGkX$$2gAHY2la`p_knXq)#63#_1&?)#^v#-!<{ zUyo&oM<8GEdf6#QO76?O>Q$@j+fF1gviM*1ZTTb?qW+&m8RiV_H-p)ku4DYjy>B@! z%oV?2fTB^Is>4I9eGYti@v)aXPjNSALjpRbi>@fITNu$8j`_q|C)vh2d^R(3xq^dh?`tP zX&H5flL~7Z?2^ankSjwlB)B!J8(3mbFg?1AW?(cMDSn0NMvs?Mxcn~jTiBE@woRD< z{s_-zQWZ{G|IO(TsA|-UjP!S305+jup-OeIyCqaIa2}MMu%+-#KgxqMb7xkXH;PhJZZ`2{_*IbPjFM( zQg9F^i5D(dQae8OCUa#@EXZEDV+V*30EbF3oA?IJ!REBFRkp zcJ1VJg_S28$C@~QN1l-YCF!Fa>$XMwNQqnpVr6ADyV|(yVh~gSm0PFk!0M)xbT1L? z0UP?-v!Ya{g0t)ULF|c&16WIFlpC3`s*}vks*xRmn8Yi-Zo68J*VWQPU3Z)w%7yg< zZ2m+iExw!`)Jc~O=3Wbd&x>~GDz*?K` z-Pgj^Yr*O~dYJzyH!hJUYSy%I;<`|G`$>Cz>g_C4*P|MlZI}}ZYXt>!F2G!{*CP)( z2G{jhS6AE9LbcGNK^B%n2gk{@%^te&+FJg!uL!)x`2|2+^rV;dXSdr=&yeS8U1xrOA%C#5YO%?`F&ibh&bi5cdxrYMHt!>%G!wyzwUw-yrZoK%12fbY&2La~goR2tE>2bzp%U!aSEpnkQ|Z=k$jiq( z#6_m<+(;z3qH=9e+d2i^k@xB^&~grCBr+$t*abyzcZ!4O@`MWOAOqggo5#O*H-+{V zLyfgPzv1{K?cf{UsN6i&RU_a<^?9~qdjAyvUP3}ki*&H`eQ?-6!&4@A8mqARG%Hrg zcP#b77O6%{&p zhg4hzUn#`uE}Qh&tV*%Fhm%lT3I#Zv;ou5LAOK3b^`^`DYjJ9Bn!=Q{X>S;W6<*&B z3s(Y*3Tmr$8%ki_z-gAZ;+o!>2X^S8XkW2{2*jEE&vR`0@Rz*nR3bJa_n>q*=$wFn z%uqaS?ZWgIb$wmIO_UBINwD-eRUp|DFSlAp8DM38IkL=05r5#a6r`?b>v8PchNF2P z8M&ufoRoH}Se>P^iNGJM-Tx~73BtpR1(EUX9beCpd3<6Ir8H6-UR6<=v!ssqw3w@O zJBlr%?*Gu%e=;<=_AYKZK~u|P9-v1hZi)b)7@t^`En!39gV1(PU@C)exPcla;?4U2V8pk(Xz2jRFHK zxz_i3evtC_>6n-v5pniXpOMtPONVNW+*XO8GBwY?}_L^O6;-%eru=DvX|Cfu0#{aT%Oz zg~R5oJ}Vg9`%a)Q=V1k!=$wt7FqaU)dC_8RrONm&n|e)sGO5R*1(G`3rVOpD@N0+6 zA@6Vz#`W%z5o28eQWG-!7NtzhAyN(BO+N7s?tktL6+>}#au5WBvjQCKA%#ifWR)Yw zl%XG&+x`$^iYsFhrKnFAgxSb_(bq+6^cf)Ob~N7pW7^w4IJ(@c_8kB1XYv z{Tx&1SMq7jg#t?lnh8pm{n46;d;roZz+R|e$c*v75r@`bHq6Y%BQ$KPV?elZl;7cX zq@^CI+v7tBmL$3l(3u#uMEg9OUOa>PoX#F$4vDcA1pZ zY^M!Y_9$MDk1g57L3f$Asyk22%|yawofq9r&KVjz2J_ezXSLQ(%Zn|D9aVCt896QX zefD&d3FjpDzAH2FG8SaNA$vFWUGslm6BWdM@qFi^?cU%}cv)M+tIX3YH6ed9A z7P|og(&9kWIjZ`*F9} zfNQIOoq}FzOio4^S>HUpX;JX7+st<442@_q=iQR{NA(EP5&!K(B#sMKVa}1D*Y+zD zp;(yI{rM0w)Xr4jtj_)Ut|OmI-_X@(zT@XppZJl49GaoqIspy_6FI$C!}Iv-H*8>Y$t!giKZKJ7RJ;$cZ|Rp*BqM zTjKiWbaUq8KkXLe0mkDWdm@Gmnq}YJ2pL}1xzY@4@`(7c_ufeWKsVO2ZTy!Oi3$*8 zK(@J-dLjxHWiHV?oZpUua&-s^xNJ0%Axl*AH*cLe@C0F`L8e`g928xwK?lgup6eJ_ za(C{vGG~?ZN}008QVja36tmHir{MP~*EzW~CPr}mJkt@f^R2CIGDoK=2oYFlmt4It zkezUxp>j>pDboPQa%xR42-G~bZz)e$5ZZg0Z12wu#+P?^foouB~T7nFplGoOYfZE5q4U|$U;Y$Rezr`Mve1u@8}4YpI*vs zp{#=_mTKsiN4dTo5jbfkeubY(IbVd|dTaF-E-jElydq6r@?OE72BwG+F4NwvyRwNO zehW?m0P`!K?Ta;uyS6XIvF@54Pd+pi!^O@RznOP#e(x(Rdt31;{vX;e{O9#@s7 z7KrN^q_8~o3m{d{StAL#mu68Y{tT&u1%qxs2Rk#aP!i|JMlVOQUJ9e?VTHjxrAJQZ z7xZ>2ovHnvCwWyx0k0V+KGB_DT8Yt-k(qmnP{V7^(uOT1`%NwzzxOuARWm{0uiCA@B{OPT!;DSJvWeW;bLbq=?=Q8j? zq&=!p;VO3((NL&6(?VRoF3zj|+HUgC{fWFOiOHB(i?AO!9q&>z%YB%)8`!+btQ|QnRw?j!%>+gzyHov(a_Y z5rh3_n)$>ed0RW2l~CIQ2cA#Z-yic0)MQqsLXE|q?h8rhm81nxg)x(aHmvpLMbgSV;wL&vy#jxT_=K%I$ zoQp8~*$P8OyRGYC(B6x@+{F420mbw@H^J2NC&`PcVh*Jn*A1ftX&b#Mpn7_xTdII) zXvW`X?x!=9nI|e>odxWB{&6`?tt-oLhNi+)2zFp0;e8$KPRT3f~08?3^4p!1HoMDjvx`WGu zoy_6xef~%DbUiz2SRJHrD-t^NW)Eu}Kh7?`A@}4|rkAaj0;D(N)?by zlCs<%WC;(}3rub#Rx9KfmgN94>okFjkCYiU z2J9DKlGqD>+7xSbCbL7a>MN?BkdUano070*4sx$ES?IZ!mvRMtLZw`~$HTy)KLy5K z$-ni#XsGO1$YgIm<7ZbsN_KX~GVb&U)tJtG|2{6t*H&9zDF^mc!-7cIbrb#YMe~pY zOJ@*oC|s&B{26FBhXqjKXop6af43)Eue)0{Q=UfGj5G)U9Rg6-$D#R5y2L{ajQu0H zR)PI&Y+!0GBp(MG49(f&{Z|11L(K`}R8((?Ym_a`G*U=c_+7BFP5DxT&OL0IzS+9F z@iu1a?m}mRi9#>EwXmZ+&+iyzXzB{i%Sch*Gr#xSBo`&GvrsYJ{p`fE-x~xdR$<)XDqxvc|&8YZ#D62aKv&RPv=aN@licLCRbneIH#H62Y^(} z&v^|`&v9fbpX@bzH_Pp3mQZa|`p|)N`{`PlM54^OgzMYn9JndZ0?#liGr`0x=kvLy z(qugQc{}L1X5c8V=e&;z$*P4WpvtH-xv4ohPV#tte$s`hK*fE0I}(Xj^#g8Ou~VW9 z?OOfm$)$!F7>X|Er>-J#KIY*$_m<7H!uru?^PmUDc`?tCNUeC;FTc6Pn`E^%`yv*$IlPu2k2!Xo;0ogqQTLv=5jyO66(ty>}c zQ+-*2jB`s5uwjBbmlHbuQ&t$lschP& zl$S$bnnK!)_;oy7`<42P8E8ilX+VgY{e`>#>XF&N^OphF)Y=OEc$r?5b#HaG$&kQh zf3IQv8T&o#bB8I8PiWbxgC{w5kcFXj1P7A3#F_J>Ww3tqYKWYCq${_Ywb4=xxj6s? z4%mABXuZG{OaO0-))7ZjvGF0O>K9FEhEsVBu_lIZ=h*trPr%Ys@;V-_w`$hFVy z-Z<3dSzXQTShWAro0Nuc@OX7N=40vxz@s+uNn!{z}(qsGZk1cXgCc&utbT)-mk6vh1gv6b<;9x z(YQ^gZGyA=pmfD~3_^rB@irS$-QVb`nkJS)ztDuhHH(C?1N6Uq%0{7nbCcAmc2ZSx`?P z4nU7B8jmTQ*vJbHo; zrl$iFsd+-}-qyb%fPm;<+Jvj;M^&T%)GXe8%kfI;E_4MqM?Be%U#*psb697mTXLGhEMmWCIghBT;Ct{R!UB`<1Hk|89 zRlIL*>ThmM_ucw9%sGqpfHKatn4ZBL0xmXB$~?k++r|d@DWU-K!gH^l)B#(^x&!}; zM_H|Kla6TyQT?^|8*P?36$q_PuFTX_IC%RPPE>g#--=mhcm^Z{AeY3|bF!nmGbS2m z(8$odg_cm!ZHv8HXSwnO=b3DmjFA5HZSYX`B^8s#0P)O|(Q;QDcH!qL3Gs`sO>z`N z_57KVG-d=rBK~7a@hN?hGGHU#XTEEexLZcn#bq=wIx^yf4N)?ro*5tAq7Pa2OrfL?kap&O_I4*}lBWifb*Af@oW7?@YX97Q`^y%)`1cqd+hJ~n zCqm#&)`6#Mn&~B{@nwHZgaR%-$*6)txd~c;Qb9Bvg8{m}EFMXfra3IBo~cunzCm^h zBoEym!wqibyMm8I5pr0E1V5W$Bki=jtBrn}uhSmhp37o&=K3wQvWy$SZtpS>Fg<>M zoL?VxarvI7I8*%8Dn41Z8gqTtL$rjp;!@ncr;<>Xarw1Bq z0x_+H(Gb9P%BrVpZojtBZUT(LiE<@v@dPI7nYH6bsjRjwt?~TJ$i94VirP1mqsfLW zHi-YttIK_>@;J*!$qVOCJ+tMxVu=w`TN}cv*7l^o&YEq&(iRel-zE`9`B&G3K8EPR ze*`V>_LjC-aVc+_ugewo-#Ho+s9Xii?zw*b3qOCFuClXpzAmr0_Y6|?JANgRt#dm1 zS)(M}ivA*c*e5Wr4cPWQ87scfNxeave@%n75K!CL37i-55<2G`p4*}FeaZOj!%j>3 z>(QFr+(u;`28g@6ITOvbk&=zj;_6EV?d4LNG1}JOiCkk3*p?miC1RPsB#u?yhtYSt z{V`P$1^TW*&h4gU*VdrPRlm;_><|Hh_s*clLWPj&s04K?ixa%GZcE5?rM8M@oO6L9 zHmJ(fU%u;JFuzD)^1dmdxUSI#<4u(P5`c*^9XtQBJrL$FQQQ8ITf0_Oy9Ul?Sj1sl zdp}rA_X#~8?Ny*C<`O=j-sudMtr<*75=1y~at4tuX&$=J1MG4IZ53lA9 zY^4B%bvv)|K<9te-TD|;%AlJp1_eq1%zB|!As!`X_i?d%UI zki}LAOtrG0e)~e4CK6B(dvo$m(?7TjQ3(V8o|@+S0Z2cDKzOp$CC+PtJZJeRJD5)q zP`p#~QJ&$#JhzO>=1|vN9NF8teH9zKEc{F3vxcDiy_5wzL6oXy(cZyGL;J_owxxY# zgpO1DCVT=`eQ9`j{d9MNfLSx&#@jTq?eALsSn_p8%+c1!(ypZ(xF@BmAk)q}^Q}Z! z{_PVM_t9wUhN~u+iZg|mF3j2f&nW;wuryWExD<`Qs~a9^XwL) z@A_^ZJYPsAAcT9rFPygGg_XSM6SxX8tsIYH2~h2IRP$uG?^i^8wL!hb$(p zzck-ar?S>63%v#`P}-~!FfoEhBsI<+Q(@|DhN*FOa($&B%n8-~5x!Y{ybrX|3|ZcrIiyE&3v3*XYH{|80mY~B+OTO z&53F2l^kCP6LWYwTTZRskY>aFhQ(isH>e^0HeDJt7CKJfp?avlf z$H;+t`qR_ok@J9lHpBynt3pQey;T^aS-fY$GL+sjx-;Y?Hg07J#ai0R&(vobQPvq^ zwkg>2LFGEw3GM$ldmh{d&QVx?9sK$+sAGjHII`;)DNr6L&b4ARE@oW2;e{Q{jM?9> z+=!a&E&EORyw=cwfkbL*85YaZCA+A*w6rkPZy*6QdCfSeJe8)X&!lG^iy06qgA?kM zd$N~T#N;tj6cioOH0a@f`6g45sBv1Tl4SB#iq}Vc@#FBw)WE=Hp>6I&+MqGM+cu}> zDGD*Yixc|Ko-a5C8w~7OQL!GQQon9uhI_Uk;@tgB?>{DO<#&ll&l84QC0OO?(PKyS zuA!xsfqpt+le=6GfO7L|wLT}(2G_^0R1v1wd()(_TIA-VW~WK6-Np>(sT8Ymk~`gN z-h!w()&DfHE*5IGmWPllM^%v=ql&sF@O6hpgo?%zqb1E+6i&r$y5#!ypgFTVA~pTM z{P`5A?a}9lvjBSj60U%hVBOZ_2`9o0FV6Wi8!;(YB3qMxC;9U`rM?JUial{!nG+5? zJax69FVfZ5HZaq9xmNB?XJR=7?CYCr`$t}-dBvW<$1|7uB15exAnAgPzUVzcl z&kbB@cF$g1t$sBNAW8C&v3ex;a|0PcQ#L88l?CWH-o7B#z(_ni3davxUkbp}my^qYL0WXOQgfA~Q{K0iBT?^^w0hx8D-Q4)kT8OQnw#hpJi|r%+Qq-+#}!~&FVwX{Tkvr* zOSLSWeICm&`*E*oQP5XRl&s9hS+Un18KLaG*<0#X>3JK>;0m&xe9?DSR&3+aS?V1u zPSefGdT0AL3-HA>rM#eR_mdkMFTAvu)PWA)C)WRBIk$;=MiatiqM9i6o@Y311Ak2G zoD+vT5P(__>$kw$Tm~ssD$w4tj}aBxs0%;HjXpqt=|PGm+0XaN#pH7oVS_$4)!?yV zr9(sFly*4hs<~=j8OKK60`?0`-iM4KN8XH5T0sRXh5KY|q6d3#?mqumM9GCe^|03` zD*sNJDyhF0NHWrqF-ZFPRVP$*^-H0><&sqTB74`$=p@H`$9rsFS0!zBmp_8O+FO<` zFfE^s`Up%`WUFq_?)O^>-((h%{1PzTc*(e|xQXHmjy^Q0_5K7pcr~jZ zv}ylZ^LWJC+o5h1!hPCR(px}=Q386hOioHu`<$@!#22Ep>I&W_B}jD^jZ7v8vo>nD z^(n#Yu}Bn~cwA9q61=s=ikLKnvCoV1eASNFb6!#=qW4?M`OQZpd(wW}t{ zz&ysQLD@R~ie5_5O&xp7UUPK_i)hROf&e)fKz8g;*UKEW*>Q1Ce@m>WQqS_S|CdQl z{gfuK;aHF=y`ALtC!Hs}?#*YtcR#v^?qBq}pO;hI|0owgZDD?8EGeD;#Yas9F?PF!f4>y11dJ-1Ij6a5&U-h> z4{aU7jLdUA&%8IB`NYeyG+bzc-}_cE*JtJbqwFo9;%K^V;lUvU51t?)xH|+51PSgg z0fJj_x8NS!-CYOQ;O-DS=-}@9cOHGe_q%KT_p(^Cpkbvtut2bcd*T=%TPLESe6)Qqn-&HtRp!^7WFZq?52O;;_$K>84zhw^H>tv$0Q4hp+0Iog<1kMTZySG4m2JNu3r)DxDW zIWP_OY174p7?K%jET@@zb=Vo;ywilmBXAOrOui`FvGOdfz2A4ca7djAL1$6TNItww zjfs(aM}!`8kF|bTKisqV$rgk9RmiKSb3021jkM=ZE*Gb)7_3j6FM7^%?$K6nl&8$u zkRxxQCw@ zEV5Ytk9HOUsJX21dJ<(tuaaC}tJ5tPk*17y8r3;DMI5i+mM=sFA(Gm&vX7hnh<}k8 zNJrB+2Jw>(xb|?_zeXgryEOh#>mJo}JsVd5j}{Yhc#q64HFw>Rm#415gt1D_6${_`H|1og_|p9m~j^b2H0n z8~44bzr#0r!YynYSZClFnuh6VyaI>tmu&(P3e`D&O}rKCk6*1mVmiA{Z@BOOfSY~a zGAAJ5b(NzH1$!R&VL_=~eh+)q>eAF^mKMGXhJ}b|ijj3&AZg<(?Olpb2-aQuwZ-VX zO4&^K?W!*y!CUE!ua|P;87wy2V>$XG{hVIfPKKA6fjo~B^F3ZA(9CG~;Wtc@#4MTtrSbKEN3pul1jJ!KS+Z-r)}$KmqB>H(a#G zs6cW;{i0dQM;K*T0F*0>W{<{i!lcRnY`UM&_>+Mt64d#N`?R{$0UxcsO5ILQ`V%)Z zpVwptoro(IcPhKMuW;+^dWotFQ z8afe=884fv4428Q9r2k4IsijL5IGuY7sN!XP<*w9nDU1-3!7 z)WdDe&t<-Cm)^>F$*cvpw7}(B<~EQ7|M}_Y)a!<(kb;b1cfc>FOj`l{{95=IP7KTF z^I_aA{{W-Fkzo&^t$p5Vy$^WUI4k-P#w2}D!&6bpUx4$v)%`tQdAJbyOkq~-rHzQ3= zH7W?SEOCA)`aRbF2nLb>IPXk)5*;oky!{j`HePr3e*gBLx+53ql{@nN9hI_#;=0cx z@8Aw4qH{~cI-0VZQ8PeNhKn$-_<)))@zYv^c5ePeEPMMsR$moW+sC25yiGufs0dt6 zwWj{A{M&w8-rFxe0$Kd>@;aDLKh&EV9-xj}!xI#t5#)1OcWE-d+s9YMSO2!HSn$uy zCBsdt+OVRO@Nq#q;*Bl(JU=Zv@b2=B=>2?Q2~y3~ z$~F(NsUjIfS*HIk-mv7#Z8;IUW(Wl8V7X3DQMF(E-PHkmi~=V>0?k}FT7u)5Bk z9I`)Qj8%K8>1Xx$q6Ow7J&v$fH}pKs*2Rg!J45EGWFd}>3`n`PW$6obc+_AQ&!DCv3G_(~%(`3fMPQf045@{;>REN}I{>6uuBi z2Xp$>^m;a&1bGm3zO{}7sRtHV*ZYnpq=HI~I+41N+PR0y#~qt0re_7PaC%evb4~jC z;pyiVKk{3%ddU6!nAp{UR=Ue}Y7}=WH(~l|+7`MD!`EbVucwZ;vZFp(NeK}va3KAg zZr!Y3&X++6(Syk!vn9j%HqVR-i}^s_BrjM3po-VtMkAGuXxxt0bMTY}sZ4~=7Aue3 z&_(F-`{z+>_MCeb^K5NFdncy27aDKAu&#UaGyG?)n`byTs8TPHfdwBi>p}G&0k!4;kD3s3NkT+(iHE@W%1A(+M(s7>|_s9alaGbF6Kl$qTKyr?X zQxcMrXnvQqrkH`J!-EdkdP9_~+x6?xS?fPujCs;^y?67^H!Ufp9RIdyb@NN?xo1VY z>@^jre+lLrui6X2>9mKrcxx?6lqwk8noPX(@|*gM-lUB^zEZIP6VYBKP;Hpr0scOr z$e&#Y^q=dm+`BCCJSxY^Dj%}W5QdzMOM6^Gc?-t{ihkX**OmoZ&K?JM^??(>oW{ma zwDCjn#YK}H^I#Wgjnl3)iHhn}t+eU46#8q8QSD?a`V>u{E{P&QoxP5TK(_ zArdr~R*F4O9kzSEZaJ+nA*ST<7gOqW0tr%Yf6*C2{IUo&BbgJQkg!uqjD85d9cp3k zG6iXlAm+)b7_DG+jmI(3y#Ep2SP=D5BxNr3&84HsA;v^2A0Im_N7}?bGS*U?I+MBZ zp}T@KrU^y9_23Q$PN{{dSocYA#WL1%-*btWy%fnDYNkTP^Kp@4nyR4=HD7!Fd4#I& zSc752=iX68bEQM-K=s`W`dH@>^gl+`@0XpdsJ)AUR0f<3fh)(o>j2 zGd3>Ak`slpE^Y6==xYJ?GcenSKVKgnio=whXyVLXPfjp~2`>H&$`%(OM8{!Lc@mIk zn_xr9b+_)P?JIE4pEJMYL=ESDeG!t^ms=)3|GxcXc6$uy*-++(;Q1PX@nk_dp6W00 zqGf^YAbYqvNUv{O1gqCFdWU`c*r3VrA z*3}rRRfc3y1qMrgYr;*ZsixD2slgeg`YXmr{l>V2*&=SZ+eZZj^1DT@@Li=p} zVO~)pkG{i;MRxv9#))|hZ3{A@#ijAtQ1zN9PA$2Sz3a@wodx3U|KuUj&Op}G3P|$o zmZMWLQc{}9IvRHs?H$Bd2Ip`r=+;qf>WfIcPK-$*H&D=zL@QG=XANXuVd&E zJ;YUq1N&BfjLJ4OM`B})x}V4QEEPL9sOY~WOL0twG*#QBlM2oQ4@3u9~~5JDT#4c$Qr(n))H4b=-%pj74kWPTdc`5Egj0!t;W?N#|0It9!O z>GIc;hKAU|pwAB!tov?_#oN?uw$v(9Zo>!($K=HUCZukV78hE(3s{i-d^2k~Po$DU zHi_e=Gr6?S+QsN_N%qg-pu$BVLtN}J=pd0IV(jFP^6zGG;|1-hOHlc1+)u~hJyi1C ze&m15H7)Uu&Ct5^(k-TNsXTm#Wul{{DK{zK!?|Zpmbci7z2~}0W0E6KO|zGVXAWpnlOmsKsS4&XoiTr!=CB5kQbS*sJ)E;+H&8)E;XXCkh zQAmG$KRXgbA5woQ@B&O`Y0(;gq}~1Q<|5bBap!$kOYSwVNl0}{6Zfuv35G9zz?;hm z{x4(OyB0i|2fkRV$E|Y-28r^-i7N}Of^Zo)a|h<8$lw{@+9Bu9c&{}PSf+}Qt?4eFd|Nwc=w}r!b-}gI+OmVI{8i+$oo@CyMk4G8A~VIpA}mY zb{`=v#OB62(MS3l8}rRaUhkq07H2Ukb$9TSd@9!uS9S_@JQEiKYDljm-79!a$k z${31VN+j`C&9Ab=9OasF2N?4or}Jys!rD8g~T=k1m#JcY0;1R|fyocDT+6!V3EHM&5;HA(EoJ zP$~CQF7Yh*{P*3>Y7fVz{YvuguGw@F&Hp_1pFtEUnxkp!_q3NwEX%`L%mWu|$WVZK z=%J&KLZ|paKFrds38u$Z8*BBoep%()%6dSaUgXTE*fOVgZM6rvPg?OBy|22ewRFpe z5fQlUUP^#h>Ty$dZtSh|Btd+ZuAfTyc>3RC;RgK3&De@{S8RVK{ z5C-bac{%B#Z3qp4Ri|@DyXw%na1zigA3s&=?=eRCz?B$*aG6*;W)dB?`kk$DF?Knd z`>D&W8BJST2(b~Tq9{o>SSi+ILth|4>Awo!1`lHLZN_s)=E{_C)7WK;i-gEVnj(Z#FnM5fo+@&9suY%(@x!QW z2eJ^I9J3n@T?-jgSRCa`!`Y0M)s>u6QT%bwXOG7dlGS=I3A@M|Q* zatL+Uzn5B@`0PnFK%mw>u2k0<4(1n#ow>={v-i{3;2bc#Szzo*u2ZSF!-w;oa7AJJ zeG>htda(Qq8*B-CrCuv=S|Q2C!!W1iL+cc+|#H_&l_nNc}bP6bnXFv4GRsCM4-{)?WaI;I7CM75-P2$FreP}fo zE<+J)(ypyl{4eR+RVkO?_3+1KNSrOa1qt8qN4tUN0a^Y4DwE%iA| zgYm)E=+=axwE#Xw(M7d^^SH=eu zFaMd%0Cle}^O<9Qj=HCsw(6X&)3tKaLOs69BBlxhPO?ztVm(f(tI&@a_0I0aLbaET zP#z*lN@|0o+NDnIST^QjCn+tK{zF>wK-Rltc<(;b1^7$?i&6%PpcyuU0XS)=Ec0wk zOA`VrP$nL}frI6D84`ial)B7@%m>1e;gwsl(9jC6v2Z{gxV(#ktj`)!;tlUn#nj?R zC@oV$vefo?E5u$uT&>I^hmKh3yUO!0I;ebnUy-PO$ZNW_J@TPA;vkYG|5>!Um{xCL!(73*1DC94HcDZ6`>6@O4gsV-QeE3>c`2KHiT5rW6E;fk(CljZ z&?ZL*v;zmK9oDMw&Nl^>5Mo=Yuv}fm+r2ZupHlsX0rHNXn5DJzC~K}{vrLS}#o5(z zfCg#GCs@vy^}}(m>i$9%5=qYgVbTx+!0mw*CHqIw&YQ67OW_+>LG41(R7>< z;?k>9{oONqo5l*~*+_wZ7WG&CQixezD~fksbHdTJQmxdrO8Hm)kE$g+SqH6eH-PW( zyuNrTRF|ooFZJ~(LykmGqXQdRe8;5X3#=yV6&1%k0$RMYk3D3?S1Pz)ng;y7*-9oP z70$&&Ehd0>e^h$tNIu$1v(namd_jGP*wZAmx5M;maO zi-9AJHIDO9pB5>ZKre@abTvlV1@r0}Lbl#m0PPY0`Hk``RCbm#kN$cnU{g4`L3`eKaz7> z@{KUrTRs6hr7SG(OGlr-)+aB)>zPo!Mbn-jN++V}IH)LwgZcUP{WNgKBama~Qq8y$ zcwcD!sF{oUp^fiCtbMHTZbU(lp~uWM^7fbQ2=w{@IislI5-`xLGLzkYz=OPJZ&2Ig zQ?;ltLCgZDmnXHy$-z;R!^vOXw>oavhL8u#Kb#ZErb){*LxFl9{Jix-BVNE3 zLP4z@veX+PgGk~J!&6(qCsyV$7j{+)A6k(zSy(r|`SzAMdi$PsX!$Gd$eUQ9Q}l$N zdfXk%))ImQ?fc^Wx;KMh`}0s@e4R|CLA5Xy(R0h4Q*4jUNh;6zz zDU}!i;n@jD(3itb>|rqRD{3=ZEh}rmfdbBn1Lz*5&;%tEzZv(&1;GmTmS9)A)<5Oi zH`fcb9{gBNzCmc_v*F-AOh(LRu+!{4H0Fz)-D{52;N552Vs)hpn9OvMa91N42@}N!=EUqZngOt{4VRU>?iPkYQ z{Hn@&8gn-7a=fG-U1?*mE~$#cINSgSDuwuYLYxk1%c0RFo%~+98OkPs2@pYF3rNUv zKEC@rEICrH&`3Sp++0r}?skXT7$2`fg-NotI{?96St+}t;F5%{u>yHBbCPd}jl5GX zDgTVN^+TUe)iPKjdEszAJ1h({3Vk_i`oPu><$LMhknN)H%OyiOS98OoIVF8X&_mej z(e!id^_SKmFAaxVV_N@6k@h9kyvGWI?jm_glnK&SJ|c6blaTGEa_9Wz$uYBxJq)xP4LjL+mQpzIrnoEG*tD`Iga~`pyVEYGvqCu%p44qlV zF0Y7p4%;&fHoF;Yz-0*J+wX9Dh&dx#42O>T{=kI->3CnbGWh7&Y>gJbMPcR}pO&XX4ns<>>Hi6j_zkEwzSUe#bioU|?%B3RBRLr7(bQKt*Bh3A%oBT^{qF^l5{ z;RR6;(*=IntrnOBIHbiXz>}m2e!neKn}0sBkJzBJ0AEl2{C4?-I?xNSmG0N|l{X}m znRlWT!;KHKEiYSx6^7?A1inVOpm-HbGO#t$ZVKo1z zUo^69^-PU7GC2wf-)|7n20j^&!<+~b1%CXS_;^~VB;KnK+XjDGBqJCL_@kLEOMHXB zwE({l*pHnw^8SrUccQu=XXC5U_5!c!VE;Kt`lH$UqJFyAnh!v+{~YDl6L|G6Ty=d_ zN&GJkM&x@j`6vg-iHq*u7sq1*mV!`02+*61N6ika`!SQSV=GzQUymcFLm&yxs3CV+ zEyvm%;aOBxN;k%6R&HnkR(-K`?^G0aBR~;`_zO?q)aDUfiMO{l>8UW1Q)Be^XZ&O& z#&1RG+r|sWzw!=?)Rb55@z)3cwHyIpS*VGaX2>}K-Ilz!#OcOH2(gGpE640Ph|En~ zT>i&5-&Xzj$ZX;zj-GYNxf0@~Z6b6_y?L0`{I|dqMH@ELuR$04vGJp!xr5kS?PDHL zHY073g@PI9cI(zCPYx#W1JpkVY^T$7#Lgv`ccRAHdHeQbJ47vDm)AlX3FMuafmgWA z6|tdJP})wS)=Q2b%Kzg0V63Xtx8^rz%I@Fn%O>->@06i!pFwHbs$p-voJ3k6Vk;oP zOaDlMq{#py{gFFjc5mb9j);X(V z^p>9~S;NN;Joya>ez)d#jiZv{sXPIy&hB#h&;SQyiHJ9B$J%PV?&D1O&qG}s-`Z%8R-kZbCI2!S0&__q z|1;7e5lvyer(ey62Nv6K71pN`^DsUZSLJuxTc!kd{rs}5txqF)a>VaCq%4HU?D1w} zn~n97hBS9?-IX)2O4Hsccy}KUJQbU!8t$i*8mhwGnrz92p}%yz`Z$nPf*eMnU}MH# zk(hRiOxla3_m~m>-tt}U-bcU5>UTvyiH92*2iX&YKDDoA9KZ2g5)azdXbGGuneTRH zSm<)NBGum5FfH6u%k*8$UvCD(VSkEgMs;g1T%X!?)Ka=)#LZb2v%Z^sE%Rp(Z(N)&=^^yg&SULSo9QsBa7jYkT zGfJ84FU1W3I*w%V8QgOhNZ}F>5QJg(k8aY^_eDM5OTQ@Z)oQ$*rF6bP8f%;SOdIX5 zq7~*yT+3F$;LXSSI-s0^qFd>7*B)VJ@*D8hsm856`D1e=(r#y~u2k@RJ>7j{wa(1* zh*r^vPB8v#VN-JXVS0Bn{YYy;;a?%O6IJYV9X`G)w!cknoXDX32yNh=W=3q5^U98; z!VtFl(0eaWerQP29wZnNY52g!Hob}gve3@1uJ{7{QT((jY*843z�aOrC#|P2+Oz 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 literal 0 HcmV?d00001 diff --git a/ci/travis/integration-testing/simcore-sdk b/ci/travis/integration-testing/simcore-sdk index 9a22542e64b..4d0968d0a6d 100755 --- a/ci/travis/integration-testing/simcore-sdk +++ b/ci/travis/integration-testing/simcore-sdk @@ -27,7 +27,7 @@ install() { before_script() { if bash ci/travis/helpers/test_for_changes "${FOLDER_CHECKS[@]}"; then - pip freeze + pip list -v # pull the test images if registry is set up, else build the images make pull-version || ((make pull-cache || true) && make build tag-version) make info-images diff --git a/ci/travis/system-testing/swarm-deploy b/ci/travis/system-testing/swarm-deploy index 8b02d55bd9f..c6a69e32adc 100755 --- a/ci/travis/system-testing/swarm-deploy +++ b/ci/travis/system-testing/swarm-deploy @@ -26,12 +26,9 @@ install() { before_script() { pip list -v make info-images - make up-version } script() { - # wait for a minute to let the swarm warm up... - make info-swarm pytest -v tests/swarm-deploy } diff --git a/packages/postgres-database/src/simcore_postgres_database/cli.py b/packages/postgres-database/src/simcore_postgres_database/cli.py index 338489e2758..fa86e3bc64f 100644 --- a/packages/postgres-database/src/simcore_postgres_database/cli.py +++ b/packages/postgres-database/src/simcore_postgres_database/cli.py @@ -47,9 +47,13 @@ def safe_func(*args, **kargs): #@retry(wait=wait_fixed(0.1), stop=stop_after_delay(60)) def _ping(url): """checks whether database is responsive""" - engine = sa.create_engine(str(url)) - conn = engine.connect() - conn.close() + try: + engine = sa.create_engine(str(url)) + conn = engine.connect() + conn.close() + finally: + engine.dispose() + @safe(if_fails_return=None) diff --git a/packages/service-library/src/servicelib/aiopg_utils.py b/packages/service-library/src/servicelib/aiopg_utils.py index e8cd6eaebc1..577d22a4a2c 100644 --- a/packages/service-library/src/servicelib/aiopg_utils.py +++ b/packages/service-library/src/servicelib/aiopg_utils.py @@ -1,68 +1,11 @@ -""" +""" Helpers for aiopg -TODO: test! + - aiopg is used as client sdk to interact with postgres database asynchronously -SEE https://aiopg.readthedocs.io/en/stable/ -SEE asyncpg https://magicstack.github.io/asyncpg/current/index.html """ -import aiopg.sa -import attr -import psycopg2 -import sqlalchemy as sa -import logging -import warnings - -logger = logging.getLogger(__name__) - -warnings.warn("DO NOT USE IN PRODUCTION, STILL UNDER DEVELOPMENT") - -@attr.s(auto_attribs=True) -class AiopgExecutor: - """ - Executes sa statements using aiopg Engine - - SEE https://github.com/aio-libs/aiopg/issues/321 - SEE http://docs.sqlalchemy.org/en/latest/faq/metadata_schema.html#how-can-i-get-the-create-table-drop-table-output-as-a-string) - """ - engine: aiopg.sa.engine.Engine - statement: str=None - dsn: str=None # Data Source Name - - @property - def sa_engine(self): - return sa.create_engine( - self.dsn, - strategy="mock", - executor=self._compile - ) - - def _compile(self, sql, *multiparams, **params): - # pylint: disable=W0613, unused-argument - self.statement = str(sql.compile(dialect=self.sa_engine.dialect)) - - async def execute(self): - async with self.engine.acquire() as conn: - logger.debug(self.statement) - resp = await conn.execute(self.statement) - return resp +from psycopg2 import Error as DBAPIError - - -async def create_all(engine: aiopg.sa.engine.Engine, metadata: sa.MetaData, dsn: str): - executor = AiopgExecutor(engine, dsn=dsn) - metadata.create_all(executor.sa_engine, checkfirst=True) - await executor.execute() - - -async def drop_all(engine: aiopg.sa.engine.Engine, metadata: sa.MetaData): - executor = AiopgExecutor(engine) - metadata.drop_all(executor.sa_engine, checkfirst=True) - await executor.execute() - - -# EXCEPTIONS ------------------------------------- -# # aiopg reuses DBAPI exceptions # # StandardError @@ -80,11 +23,8 @@ async def drop_all(engine: aiopg.sa.engine.Engine, metadata: sa.MetaData): # SEE https://aiopg.readthedocs.io/en/stable/core.html?highlight=Exception#exceptions # SEE http://initd.org/psycopg/docs/module.html#dbapi-exceptions -# alias add prefix DBAPI -DBAPIError = psycopg2.Error -__all__ = ( - 'create_all', - 'drop_all' -) +__all__ = [ + 'DBAPIError' +] diff --git a/packages/service-library/src/servicelib/application.py b/packages/service-library/src/servicelib/application.py index 43996459b82..400a9cfb6c4 100644 --- a/packages/service-library/src/servicelib/application.py +++ b/packages/service-library/src/servicelib/application.py @@ -5,6 +5,13 @@ from .application_keys import APP_CONFIG_KEY from .client_session import persistent_client_session +async def startup_info(app: web.Application): + print(f"STARTING UP {app}...", flush=True) + + +async def shutdown_info(app: web.Application): + print(f"SHUTDOWN {app} ...", flush=True) + def create_safe_application(config: Optional[Dict]=None) -> web.Application: app = web.Application() @@ -12,10 +19,12 @@ def create_safe_application(config: Optional[Dict]=None) -> web.Application: # Enxures config entry app[APP_CONFIG_KEY] = config or {} + app.on_startup.append(startup_info) + app.on_cleanup.append(shutdown_info) + # Ensures persistent client session # NOTE: Ensures client session context is run first, # then any further get_client_sesions will be correctly closed app.cleanup_ctx.append(persistent_client_session) - return app diff --git a/packages/service-library/src/servicelib/application_keys.py b/packages/service-library/src/servicelib/application_keys.py index 52c32573379..d19f14f6f37 100644 --- a/packages/service-library/src/servicelib/application_keys.py +++ b/packages/service-library/src/servicelib/application_keys.py @@ -11,24 +11,21 @@ # REQUIREMENTS: # - guarantees all keys are unique -# TODO: facilitate key generation -# TODO: hierarchical classification +# - one place for all common keys +# - hierarchical classification # TODO: should be read-only (frozen?) +# +# web.Application keys, i.e. app[APP_*_KEY] +# +APP_CONFIG_KEY = f'{__name__ }.config' +APP_OPENAPI_SPECS_KEY = f'{__name__ }.openapi_specs' +APP_JSONSCHEMA_SPECS_KEY = f'{__name__ }.jsonschema_specs' -# APP=application -APP_CONFIG_KEY = __name__ + '.config' -APP_OPENAPI_SPECS_KEY = __name__ + '.openapi_specs' -APP_SESSION_SECRET_KEY = __name__ + '.session_secret' -APP_JSONSCHEMA_SPECS_KEY = __name__ + '.jsonschema_specs' +APP_DB_ENGINE_KEY = f'{__name__ }.db_engine' -APP_DB_ENGINE_KEY = __name__ + '.db_engine' -APP_DB_SESSION_KEY = __name__ + '.db_session' -APP_DB_POOL_KEY = __name__ + '.db_pool' +APP_CLIENT_SESSION_KEY = f'{__name__ }.session' -APP_CLIENT_SESSION_KEY = f"{__name__ }.session" - -# RSP=response - - -# TODO: tool to convert dotted __name__ to section in dict +# +# web.Response keys, i.e. app[RSP_*_KEY] +# diff --git a/packages/service-library/src/servicelib/monitoring.py b/packages/service-library/src/servicelib/monitoring.py index 547c500c450..5fe020354cb 100644 --- a/packages/service-library/src/servicelib/monitoring.py +++ b/packages/service-library/src/servicelib/monitoring.py @@ -16,12 +16,13 @@ from aiohttp import web from prometheus_client import CONTENT_TYPE_LATEST, Counter, Gauge, Histogram + log = logging.getLogger(__name__) def middleware_factory(app_name): @web.middleware - async def middleware_handler(request, handler): + async def middleware_handler(request: web.Request, handler): # See https://prometheus.io/docs/concepts/metric_types try: request['start_time'] = time.time() @@ -35,10 +36,17 @@ async def middleware_handler(request, handler): resp = exc raise except Exception as exc: #pylint: disable=broad-except - # Prevents issue #1025. FIXME: why middleware below is not non-http exception safe? - log.exception("Unexpected exception. \ - Error middleware below should only raise web.HTTPExceptions.") + # Prevents issue #1025. resp = web.HTTPInternalServerError(reason=str(exc)) + resp_time = time.time() - request['start_time'] + + # NOTE: all access to API (i.e. and not other paths as /socket, /x, etc) shall return web.HTTPErrors since processed by error_middleware_factory + log.exception('Unexpected server error "%s" from access: %s "%s %s" done in %3.2f secs. Responding with status %s', + type(exc), + request.remote, request.method, request.path, + resp_time, + resp.status + ) finally: # metrics on the same request resp_time = time.time() - request['start_time'] diff --git a/packages/service-library/src/servicelib/rest_middlewares.py b/packages/service-library/src/servicelib/rest_middlewares.py index 200167e9347..1518469c033 100644 --- a/packages/service-library/src/servicelib/rest_middlewares.py +++ b/packages/service-library/src/servicelib/rest_middlewares.py @@ -27,15 +27,16 @@ def is_api_request(request: web.Request, api_version: str) -> bool: return request.path.startswith(base_path) -def _process_and_raise_unexpected_error(err): +def _process_and_raise_unexpected_error(request: web.BaseRequest, err: Exception): # TODO: send info + trace to client ONLY in debug mode!!! - logger.exception("Unexpected exception on server side") - exc = create_error_response( - [err,], - "Unexpected Server error", - web.HTTPInternalServerError - ) - raise exc + resp = create_error_response( [err,], "Unexpected Server error", web.HTTPInternalServerError) + + logger.exception('Unexpected server error "%s" from access: %s "%s %s". Responding with status %s', + type(err), + request.remote, request.method, request.path, + resp.status + ) + raise resp def error_middleware_factory(api_version: str = DEFAULT_API_VERSION): @@ -78,7 +79,7 @@ async def _middleware(request: web.Request, handler): payload = wrap_as_envelope(data=payload) ex.text = json.dumps(payload) except Exception as err: # pylint: disable=W0703 - _process_and_raise_unexpected_error(err) + _process_and_raise_unexpected_error(request, err) raise ex except web.HTTPRedirection as ex: @@ -86,7 +87,7 @@ async def _middleware(request: web.Request, handler): raise except Exception as err: # pylint: disable=W0703 - _process_and_raise_unexpected_error(err) + _process_and_raise_unexpected_error(request, err) return _middleware diff --git a/packages/simcore-sdk/src/simcore_sdk/models/pipeline_models.py b/packages/simcore-sdk/src/simcore_sdk/models/pipeline_models.py index 7d5636b09eb..258534f2d08 100644 --- a/packages/simcore-sdk/src/simcore_sdk/models/pipeline_models.py +++ b/packages/simcore-sdk/src/simcore_sdk/models/pipeline_models.py @@ -13,7 +13,7 @@ # NOTE: All this file ises classical mapping to keep LEGACY class Base: - metadata = metadata #pylint: disable=self-assigning-variable + metadata = metadata class ComputationalPipeline: diff --git a/packages/simcore-sdk/src/simcore_sdk/node_ports/dbmanager.py b/packages/simcore-sdk/src/simcore_sdk/node_ports/dbmanager.py index a307371c691..44d969c9869 100644 --- a/packages/simcore-sdk/src/simcore_sdk/node_ports/dbmanager.py +++ b/packages/simcore-sdk/src/simcore_sdk/node_ports/dbmanager.py @@ -32,10 +32,14 @@ def session_scope(session_factory): class DbSettings: def __init__(self): self._db_settings_config = db_config() - self.db = create_engine(self._db_settings_config.endpoint, client_encoding='utf8') + # FIXME: this is a SYNCRONOUS engine! And not disposed!? + self.db = create_engine( + self._db_settings_config.endpoint + f"?application_name={__name__}_{id(self)}", + client_encoding='utf8') self.Session = sessionmaker(self.db) # self.session = self.Session() + class _NodeModelEncoder(json.JSONEncoder): def default(self, o): # pylint: disable=E0202 log.debug("Encoding object: %s", o) diff --git a/packages/simcore-sdk/tests/__init__.py b/packages/simcore-sdk/tests/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/simcore-sdk/tests/conftest.py b/packages/simcore-sdk/tests/conftest.py index c96d120a5d9..f28902327b2 100644 --- a/packages/simcore-sdk/tests/conftest.py +++ b/packages/simcore-sdk/tests/conftest.py @@ -1,10 +1,7 @@ -import pytest -import os # pylint:disable=unused-argument -pytest_plugins = ["tests.fixtures.postgres", "tests.fixtures.minio_fix", "tests.fixtures.storage"] - -@pytest.fixture(scope='session') -def docker_compose_file(pytestconfig): - my_path = os.path.join(os.path.dirname(__file__), 'docker-compose.yml') - return my_path +pytest_plugins = [ + "fixtures.postgres", + "fixtures.minio_fix", + "fixtures.storage" +] diff --git a/packages/simcore-sdk/tests/fixtures/postgres.py b/packages/simcore-sdk/tests/fixtures/postgres.py index 46eb2367321..6a3b673f770 100644 --- a/packages/simcore-sdk/tests/fixtures/postgres.py +++ b/packages/simcore-sdk/tests/fixtures/postgres.py @@ -3,7 +3,6 @@ import pytest import sqlalchemy as sa -from pytest_docker import docker_ip, docker_services # pylint:disable=W0611 from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker @@ -28,6 +27,8 @@ def is_responsive(url): except sa.exc.OperationalError: logging.exception("Connection to db failed") return False + finally: + eng.dispose() return True diff --git a/packages/simcore-sdk/tests/node_ports/conftest.py b/packages/simcore-sdk/tests/node_ports/conftest.py index 32baf7d285d..54214d809e5 100644 --- a/packages/simcore-sdk/tests/node_ports/conftest.py +++ b/packages/simcore-sdk/tests/node_ports/conftest.py @@ -1,17 +1,19 @@ #pylint: disable=W0621, unused-argument, too-many-arguments, no-name-in-module import json +import sys import uuid from pathlib import Path from typing import Any, List, Tuple import pytest import yarl - -from helpers import helpers from simcore_sdk.models.pipeline_models import (Base, ComputationalPipeline, ComputationalTask) from simcore_sdk.node_ports import node_config +import np_helpers + +current_dir = Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent @pytest.fixture def user_id()->int: @@ -20,7 +22,7 @@ def user_id()->int: @pytest.fixture def s3_simcore_location() ->str: - yield helpers.SIMCORE_STORE + yield np_helpers.SIMCORE_STORE @pytest.fixture def filemanager_cfg(storage, user_id, bucket): @@ -45,29 +47,20 @@ def create(file_path:Path, project:str=None, node:str=None): project = project_id if node is None: node = node_uuid - return helpers.file_uuid(file_path, project, node) + return np_helpers.file_uuid(file_path, project, node) yield create @pytest.fixture(scope='session') -def here()->Path: - yield Path(__file__).parent - -@pytest.fixture(scope='session') -def docker_compose_file(bucket, pytestconfig, here): # pylint:disable=unused-argument - my_path = here /'docker-compose.yml' - - yield my_path - - - +def docker_compose_file(bucket, pytestconfig): # pylint:disable=unused-argument + return current_dir /'docker-compose.yml' @pytest.fixture -def default_configuration_file(here): - yield here / "config" / "default_config.json" +def default_configuration_file(): + return current_dir / "config" / "default_config.json" @pytest.fixture -def empty_configuration_file(here): - yield here / "config" / "empty_config.json" +def empty_configuration_file(): + return current_dir / "config" / "empty_config.json" @pytest.fixture(scope='module') def postgres(engine, session): diff --git a/packages/simcore-sdk/tests/node_ports/helpers/__init__.py b/packages/simcore-sdk/tests/node_ports/helpers/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/simcore-sdk/tests/node_ports/helpers/helpers.py b/packages/simcore-sdk/tests/node_ports/np_helpers.py similarity index 100% rename from packages/simcore-sdk/tests/node_ports/helpers/helpers.py rename to packages/simcore-sdk/tests/node_ports/np_helpers.py diff --git a/packages/simcore-sdk/tests/node_ports/test_nodeports.py b/packages/simcore-sdk/tests/node_ports/test_nodeports.py index d6361db24fc..6eeafc85f23 100644 --- a/packages/simcore-sdk/tests/node_ports/test_nodeports.py +++ b/packages/simcore-sdk/tests/node_ports/test_nodeports.py @@ -9,11 +9,10 @@ from pathlib import Path import pytest - -from helpers import helpers # pylint: disable=no-name-in-module from simcore_sdk import node_ports from simcore_sdk.node_ports import exceptions +import np_helpers # pylint: disable=no-name-in-module def _check_port_valid(ports, config_dict: dict, port_type:str, key_name: str, key): @@ -151,7 +150,7 @@ def test_adding_new_ports(special_configuration, session): "displayOrder":2, "type": "integer"}}) config_dict["inputs"].update({"in_15":15}) - helpers.update_configuration(session, project_id, node_uuid, config_dict) #pylint: disable=E1101 + np_helpers.update_configuration(session, project_id, node_uuid, config_dict) #pylint: disable=E1101 check_config_valid(PORTS, config_dict) # # replace the configuration now, add an output @@ -161,7 +160,7 @@ def test_adding_new_ports(special_configuration, session): "description": "a cool output", "displayOrder":2, "type": "boolean"}}) - helpers.update_configuration(session, project_id, node_uuid, config_dict) #pylint: disable=E1101 + np_helpers.update_configuration(session, project_id, node_uuid, config_dict) #pylint: disable=E1101 check_config_valid(PORTS, config_dict) @@ -175,12 +174,12 @@ def test_removing_ports(special_configuration, session): # let's remove the first input del config_dict["schema"]["inputs"]["in_14"] del config_dict["inputs"]["in_14"] - helpers.update_configuration(session, project_id, node_uuid, config_dict) #pylint: disable=E1101 + np_helpers.update_configuration(session, project_id, node_uuid, config_dict) #pylint: disable=E1101 check_config_valid(PORTS, config_dict) # let's do the same for the second output del config_dict["schema"]["outputs"]["out_2"] del config_dict["outputs"]["out_2"] - helpers.update_configuration(session, project_id, node_uuid, config_dict) #pylint: disable=E1101 + np_helpers.update_configuration(session, project_id, node_uuid, config_dict) #pylint: disable=E1101 check_config_valid(PORTS, config_dict) @@ -239,7 +238,7 @@ async def test_get_file_from_previous_node_with_mapping_of_same_key_name(special check_config_valid(PORTS, config_dict) # add a filetokeymap config_dict["schema"]["inputs"]["in_15"]["fileToKeyMap"] = {item_alias:"in_15"} - helpers.update_configuration(session, project_id, this_node_uuid, config_dict) #pylint: disable=E1101 + np_helpers.update_configuration(session, project_id, this_node_uuid, config_dict) #pylint: disable=E1101 check_config_valid(PORTS, config_dict) file_path = await PORTS.inputs["in_15"].get() assert isinstance(file_path, item_pytype) @@ -266,7 +265,7 @@ async def test_file_mapping(special_configuration, project_id, node_uuid, filema # add a filetokeymap config_dict["schema"]["inputs"]["in_1"]["fileToKeyMap"] = {item_alias:"in_1"} config_dict["schema"]["outputs"]["out_1"]["fileToKeyMap"] = {item_alias:"out_1"} - helpers.update_configuration(session, project_id, node_uuid, config_dict) #pylint: disable=E1101 + np_helpers.update_configuration(session, project_id, node_uuid, config_dict) #pylint: disable=E1101 check_config_valid(PORTS, config_dict) file_path = await PORTS.inputs["in_1"].get() assert isinstance(file_path, item_pytype) @@ -283,5 +282,5 @@ async def test_file_mapping(special_configuration, project_id, node_uuid, filema await PORTS.set_file_by_keymap(invalid_alias) await PORTS.set_file_by_keymap(file_path) - file_id = helpers.file_uuid(file_path, project_id, node_uuid) + file_id = np_helpers.file_uuid(file_path, project_id, node_uuid) assert PORTS.outputs["out_1"].value == {"store":s3_simcore_location, "path": file_id} diff --git a/packages/simcore-sdk/tests/test_alchemy.py b/packages/simcore-sdk/tests/test_alchemy.py index a777a8f2cdc..389cd57fdba 100644 --- a/packages/simcore-sdk/tests/test_alchemy.py +++ b/packages/simcore-sdk/tests/test_alchemy.py @@ -1,14 +1,19 @@ -import pytest +# pylint:disable=redefined-outer-name # pylint:disable=unused-import + +import pytest +# FIXME: Not sure why but if this import is removed pytest_docker +# gets the docker_compose_file wrong in tests_nodes. +# Somehow the fixture in packages/simcore-sdk/tests/node_ports/conftest.py +# does not override override of docker_compose_file from pytest_docker! from pytest_docker import docker_ip, docker_services -from sqlalchemy import JSON, Column, Integer, String, create_engine +from simcore_sdk.models.pipeline_models import (ComputationalPipeline, + ComputationalTask, + comp_pipeline, comp_tasks) +from sqlalchemy import JSON, Column, Integer, String from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker from sqlalchemy.orm.attributes import flag_modified -# pylint:disable=redefined-outer-name - - BASE = declarative_base() class User(BASE): __tablename__ = 'users' @@ -17,8 +22,6 @@ class User(BASE): data = Column(JSON) - - @pytest.mark.enable_travis def test_alchemy(engine, session): BASE.metadata.create_all(engine) @@ -47,9 +50,6 @@ def test_alchemy(engine, session): assert alpha2.data['counter'] == 42 -from simcore_sdk.models.pipeline_models import ComputationalPipeline, ComputationalTask, comp_pipeline, comp_tasks - - def test_legacy_queries_with_mapper_adapter(): """Checks to ensure that LEGACY queries still work with mapper adapter diff --git a/services/sidecar/docker/boot.sh b/services/sidecar/docker/boot.sh index 2eea24a12d6..598d9ff9d68 100755 --- a/services/sidecar/docker/boot.sh +++ b/services/sidecar/docker/boot.sh @@ -41,4 +41,4 @@ else CONCURRENCY=2 fi -celery worker --app sidecar.celery:app --concurrency ${CONCURRENCY} --loglevel=${DEBUG_LEVEL} +exec celery worker --app sidecar.celery:app --concurrency ${CONCURRENCY} --loglevel=${DEBUG_LEVEL} diff --git a/services/sidecar/docker/entrypoint.sh b/services/sidecar/docker/entrypoint.sh index 5b8ed0ceb98..e18f5b1e859 100755 --- a/services/sidecar/docker/entrypoint.sh +++ b/services/sidecar/docker/entrypoint.sh @@ -74,4 +74,4 @@ chown -R $USERNAME:$GROUPNAME /home/scu/input chown -R $USERNAME:$GROUPNAME /home/scu/output chown -R $USERNAME:$GROUPNAME /home/scu/log -su-exec scu "$@" +exec su-exec scu "$@" diff --git a/services/sidecar/src/sidecar/utils.py b/services/sidecar/src/sidecar/utils.py index edad99f32c9..1e6b5e15ed7 100644 --- a/services/sidecar/src/sidecar/utils.py +++ b/services/sidecar/src/sidecar/utils.py @@ -4,17 +4,16 @@ import shutil from concurrent.futures import ThreadPoolExecutor -import tenacity -from sqlalchemy import and_, create_engine -from sqlalchemy.orm import sessionmaker - import docker +import tenacity from s3wrapper.s3_client import S3Client from simcore_sdk.config.db import Config as db_config from simcore_sdk.config.docker import Config as docker_config from simcore_sdk.config.rabbit import Config as rabbit_config from simcore_sdk.config.s3 import Config as s3_config from simcore_sdk.models.pipeline_models import SUCCESS, ComputationalTask +from sqlalchemy import and_, create_engine +from sqlalchemy.orm import sessionmaker def wrap_async_call(fct: asyncio.coroutine): @@ -94,7 +93,10 @@ def __init__(self): class DbSettings: def __init__(self): self._db_config = db_config() - self.db = create_engine(self._db_config.endpoint, client_encoding='utf8', pool_pre_ping=True) + self.db = create_engine( + self._db_config.endpoint + f"?application_name={__name__}_{id(self)}", + client_encoding='utf8', + pool_pre_ping=True) self.Session = sessionmaker(self.db, expire_on_commit=False) #self.session = self.Session() diff --git a/services/sidecar/tests/utils.py b/services/sidecar/tests/utils.py index 9d4ad5cf0ef..b831dccade9 100644 --- a/services/sidecar/tests/utils.py +++ b/services/sidecar/tests/utils.py @@ -48,17 +48,23 @@ def create_tables(url, engine=None): client_encoding="utf8", connect_args={"connect_timeout": 30}, pool_pre_ping=True) + Base.metadata.create_all(engine) + engine.dispose() + else: + Base.metadata.create_all(engine) - Base.metadata.create_all(engine) def drop_tables(url, engine=None): - if not engine: + is_owned = not engine + if is_owned: engine = create_engine(url, client_encoding="utf8", connect_args={"connect_timeout": 30}, pool_pre_ping=True) Base.metadata.drop_tables(engine) + if is_owned: + engine.dispose() def setup_sleepers(url): db_engine = create_engine(url, diff --git a/services/storage/docker/boot.sh b/services/storage/docker/boot.sh index b5b56a9de13..30feaa62afd 100755 --- a/services/storage/docker/boot.sh +++ b/services/storage/docker/boot.sh @@ -26,11 +26,14 @@ then echo " PIP :" $SC_PIP list | sed 's/^/ /' + #------------ + echo " setting entrypoint to use watchmedo autorestart..." + entrypoint='watchmedo auto-restart --recursive --pattern="*.py" --' elif [[ ${SC_BUILD_TARGET} == "production" ]] then APP_CONFIG=docker-prod-config.yaml - + entrypoint='' fi @@ -40,12 +43,12 @@ then # NOTE: needs stdin_open: true and tty: true echo "Debugger attached: https://docs.python.org/3.6/library/pdb.html#debugger-commands ..." echo "Running: import pdb, simcore_service_storage.cli; pdb.run('simcore_service_storage.cli.main([\'-c\',\'${APP_CONFIG}\'])')" - python -c "import pdb, simcore_service_storage.cli; \ + eval "$entrypoint" python -c "import pdb, simcore_service_storage.cli; \ pdb.run('simcore_service_storage.cli.main([\'-c\',\'${APP_CONFIG}\'])')" elif [[ ${SC_BOOT_MODE} == "debug-ptvsd" ]] then echo "PTVSD Debugger initializing in port 3003 with ${APP_CONFIG}" - python3 -m ptvsd --host 0.0.0.0 --port 3000 -m simcore_service_storage --config $APP_CONFIG + eval "$entrypoint" python3 -m ptvsd --host 0.0.0.0 --port 3000 -m simcore_service_storage --config $APP_CONFIG else - simcore-service-storage --config $APP_CONFIG + exec simcore-service-storage --config $APP_CONFIG fi diff --git a/services/storage/docker/entrypoint.sh b/services/storage/docker/entrypoint.sh index 7287ba763ec..eb604f21bfe 100755 --- a/services/storage/docker/entrypoint.sh +++ b/services/storage/docker/entrypoint.sh @@ -51,4 +51,4 @@ then fi echo "Starting boot ..." -su-exec scu "$@" +exec su-exec scu "$@" diff --git a/services/storage/requirements/dev.txt b/services/storage/requirements/dev.txt index 6c5e1bd9203..02aae5125ff 100644 --- a/services/storage/requirements/dev.txt +++ b/services/storage/requirements/dev.txt @@ -6,6 +6,9 @@ # pip install -r requirements/dev.txt # +# installs watchdog utility +watchdog[watchmedo] + # installs base + tests requirements -r _test.txt diff --git a/services/storage/src/simcore_service_storage/application.py b/services/storage/src/simcore_service_storage/application.py index 4b185a1f878..4c12074f6cb 100644 --- a/services/storage/src/simcore_service_storage/application.py +++ b/services/storage/src/simcore_service_storage/application.py @@ -2,37 +2,35 @@ Functions to create, setup and run an aiohttp application provided a configuration object """ +import json import logging +from typing import Dict from aiohttp import web + +from servicelib.application import create_safe_application from servicelib.monitoring import setup_monitoring -from servicelib.client_session import persistent_client_session from .db import setup_db from .dsm import setup_dsm from .rest import setup_rest from .s3 import setup_s3 -from .settings import APP_CONFIG_KEY log = logging.getLogger(__name__) -def create(config): - log.debug("Creating and setting up application") - - app = web.Application() - app[APP_CONFIG_KEY] = config +def create(config: Dict) -> web.Application: + log.debug("Initializing app with config:\n%s", + json.dumps(config, indent=2, sort_keys=True)) - # NOTE: ensure client session is context is run first, then any further get_client_sesions will be correctly closed - app.cleanup_ctx.append(persistent_client_session) + app = create_safe_application(config) setup_db(app) # -> postgres service setup_s3(app) # -> minio service setup_dsm(app) # core subsystem. Needs s3 and db setups done setup_rest(app) # lastly, we expose API to the world - monitoring = config["main"]["monitoring_enabled"] - if monitoring: + if config["main"].get("monitoring_enabled", False): setup_monitoring(app, "simcore_service_storage") return app diff --git a/services/storage/src/simcore_service_storage/db.py b/services/storage/src/simcore_service_storage/db.py index c8cd8da730c..ce601849053 100644 --- a/services/storage/src/simcore_service_storage/db.py +++ b/services/storage/src/simcore_service_storage/db.py @@ -8,7 +8,7 @@ from servicelib.aiopg_utils import DBAPIError from .models import metadata -from .settings import APP_CONFIG_KEY, APP_DB_ENGINE_KEY, APP_DB_SESSION_KEY +from .settings import APP_CONFIG_KEY, APP_DB_ENGINE_KEY log = logging.getLogger(__name__) @@ -22,36 +22,30 @@ @retry( wait=wait_fixed(RETRY_WAIT_SECS), stop=stop_after_attempt(RETRY_COUNT), - before_sleep=before_sleep_log(log, logging.INFO) ) + before_sleep=before_sleep_log(log, logging.INFO), + reraise=True) async def __create_tables(**params): - sa_engine = sa.create_engine(DSN.format(**params)) - metadata.create_all(sa_engine) - -async def pg_engine(app: web.Application): - engine = None try: - cfg = app[APP_CONFIG_KEY][THIS_SERVICE_NAME] - params = {k:cfg[k] for k in 'database user password host port'.split()} - await __create_tables(**params) - engine = await create_engine(**params) + url = DSN.format(**params) + f"?application_name={__name__}_init" + sa_engine = sa.create_engine(url) + metadata.create_all(sa_engine) + finally: + sa_engine.dispose() - except Exception: # pylint: disable=W0703 - log.exception("Could not create engine") +async def pg_engine(app: web.Application): + cfg = app[APP_CONFIG_KEY][THIS_SERVICE_NAME] + params = {key:cfg[key] for key in 'database user password host port minsize maxsize'.split()} - session = None - app[APP_DB_ENGINE_KEY] = engine - app[APP_DB_SESSION_KEY] = session + # TODO: set this as optional? + await __create_tables(**params) - yield + async with create_engine(application_name=f'{__name__}_{id(app)}', **params) as engine: + app[APP_DB_ENGINE_KEY] = engine - session = app.get(APP_DB_SESSION_KEY) - if session: - session.close() + yield # ---------- - engine = app.get(APP_DB_ENGINE_KEY) - if engine: - engine.close() - await engine.wait_closed() + if engine is not app.get(APP_DB_ENGINE_KEY): + log.error("app does not hold right db engine") async def is_service_responsive(app:web.Application): """ Returns true if the app can connect to db service @@ -68,17 +62,14 @@ async def is_service_responsive(app:web.Application): return False def setup_db(app: web.Application): - disable_services = app[APP_CONFIG_KEY].get("main", {}).get("disable_services",[]) if THIS_SERVICE_NAME in disable_services: - app[APP_DB_ENGINE_KEY] = app[APP_DB_SESSION_KEY] = None + app[APP_DB_ENGINE_KEY] = None log.warning("Service '%s' explicitly disabled in config", THIS_SERVICE_NAME) return app[APP_DB_ENGINE_KEY] = None - app[APP_DB_SESSION_KEY] = None - # app is created at this point but not yet started log.debug("Setting up %s [service: %s] ...", __name__, THIS_SERVICE_NAME) diff --git a/services/storage/src/simcore_service_storage/dsm.py b/services/storage/src/simcore_service_storage/dsm.py index 868db6408f8..2fe85dfb2ae 100644 --- a/services/storage/src/simcore_service_storage/dsm.py +++ b/services/storage/src/simcore_service_storage/dsm.py @@ -8,18 +8,19 @@ from pathlib import Path from typing import Dict, List, Tuple -import aiobotocore import aiofiles import attr import sqlalchemy as sa from aiohttp import web from aiopg.sa import Engine +from sqlalchemy.sql import and_ +from yarl import URL + +import aiobotocore from blackfynn.base import UnauthorizedException from s3wrapper.s3_client import S3Client from servicelib.aiopg_utils import DBAPIError from servicelib.client_session import get_client_session -from sqlalchemy.sql import and_ -from yarl import URL from .datcore_wrapper import DatcoreWrapper from .models import (DatasetMetaData, FileMetaData, FileMetaDataEx, diff --git a/services/storage/src/simcore_service_storage/settings.py b/services/storage/src/simcore_service_storage/settings.py index 2b10069e48b..97b9f984947 100644 --- a/services/storage/src/simcore_service_storage/settings.py +++ b/services/storage/src/simcore_service_storage/settings.py @@ -63,7 +63,6 @@ # DATABASE ---------------------------- APP_DB_ENGINE_KEY = __name__ + '.db_engine' -APP_DB_SESSION_KEY = __name__ + '.db_session' # DATA STORAGE MANAGER ---------------------------------- diff --git a/services/storage/tests/conftest.py b/services/storage/tests/conftest.py index 518c86766b6..b103bf99204 100644 --- a/services/storage/tests/conftest.py +++ b/services/storage/tests/conftest.py @@ -101,7 +101,9 @@ def postgres_service(docker_services, docker_ip): 'password': PASS, 'database': DATABASE, 'host': docker_ip, - 'port': docker_services.port_for('postgres', 5432) + 'port': docker_services.port_for('postgres', 5432), + 'minsize':1, + 'maxsize':4 } return postgres_service diff --git a/services/storage/tests/utils.py b/services/storage/tests/utils.py index c8057fcbce4..2fd099e65d9 100644 --- a/services/storage/tests/utils.py +++ b/services/storage/tests/utils.py @@ -101,6 +101,7 @@ def insert_metadata(url: str, fmd: FileMetaData): engine = sa.create_engine(url) conn = engine.connect() conn.execute(ins) + engine.dispose() def create_full_tables(url): meta = sa.MetaData() @@ -143,9 +144,11 @@ def create_full_tables(url): # with open(csv_file, 'r') as file: # data_df = pd.read_csv(file) # data_df.to_sql(t, con=engine, index=False, index_label="id", if_exists='append') + engine.dispose() def drop_all_tables(url): meta = sa.MetaData() engine = sa.create_engine(url) meta.drop_all(bind=engine, tables=[file_meta_data, projects, user_to_projects, users]) + engine.dispose() diff --git a/services/web/server/docker/boot.sh b/services/web/server/docker/boot.sh index 7ba86f1031d..8119bcc35b3 100755 --- a/services/web/server/docker/boot.sh +++ b/services/web/server/docker/boot.sh @@ -25,9 +25,14 @@ then echo " PIP :" $SC_PIP list | sed 's/^/ /' + #------------ + echo " setting entrypoint to use watchmedo autorestart..." + entrypoint='watchmedo auto-restart --recursive --pattern="*.py" --' + elif [[ ${SC_BUILD_TARGET} == "production" ]] then APP_CONFIG=server-docker-prod.yaml + entrypoint='' fi @@ -37,13 +42,13 @@ then # NOTE: needs stdin_open: true and tty: true echo "Debugger attached: https://docs.python.org/3.6/library/pdb.html#debugger-commands ..." echo "Running: import pdb, simcore_service_server.cli; pdb.run('simcore_service_server.cli.main([\'-c\',\'${APP_CONFIG}\'])')" - python -c "import pdb, simcore_service_server.cli; \ + eval "$entrypoint" python -c "import pdb, simcore_service_server.cli; \ pdb.run('simcore_service_server.cli.main([\'-c\',\'${APP_CONFIG}\'])')" elif [[ ${SC_BOOT_MODE} == "debug-ptvsd" ]] then # NOTE: needs ptvsd installed echo "PTVSD Debugger initializing in port 3000 with ${APP_CONFIG}" - python3 -m ptvsd --host 0.0.0.0 --port 3000 -m simcore_service_webserver --config $APP_CONFIG + eval "$entrypoint" python3 -m ptvsd --host 0.0.0.0 --port 3000 -m simcore_service_webserver --config $APP_CONFIG else - simcore-service-webserver --config $APP_CONFIG + exec simcore-service-webserver --config $APP_CONFIG fi diff --git a/services/web/server/docker/entrypoint.sh b/services/web/server/docker/entrypoint.sh index 2a9c4a8a1b9..9ac1c84b89b 100755 --- a/services/web/server/docker/entrypoint.sh +++ b/services/web/server/docker/entrypoint.sh @@ -47,4 +47,4 @@ then python3 -m pip install ptvsd fi -su-exec scu "$@" +exec su-exec scu "$@" diff --git a/services/web/server/requirements/dev.txt b/services/web/server/requirements/dev.txt index 5c8b3eeb176..bd571f5a243 100644 --- a/services/web/server/requirements/dev.txt +++ b/services/web/server/requirements/dev.txt @@ -6,6 +6,9 @@ # pip install -r requirements/dev.txt # +# installs watchdog utility +watchdog[watchmedo] + # installs base + tests requirements -r _test.txt diff --git a/services/web/server/src/simcore_service_webserver/db.py b/services/web/server/src/simcore_service_webserver/db.py index 745d93d2f64..ad7a00993ff 100644 --- a/services/web/server/src/simcore_service_webserver/db.py +++ b/services/web/server/src/simcore_service_webserver/db.py @@ -13,12 +13,11 @@ from servicelib.aiopg_utils import DBAPIError from servicelib.application_keys import APP_CONFIG_KEY, APP_DB_ENGINE_KEY -from servicelib.application_setup import app_module_setup,ModuleCategory +from servicelib.application_setup import ModuleCategory, app_module_setup from .db_config import CONFIG_SECTION_NAME from .db_models import metadata -# SETTINGS ---------------------------------------------------- THIS_MODULE_NAME = __name__.split(".")[-1] THIS_SERVICE_NAME = 'postgres' DSN = "postgresql://{user}:{password}@{host}:{port}/{database}" # Data Source Name. TODO: sync with config @@ -26,8 +25,6 @@ RETRY_WAIT_SECS = 2 RETRY_COUNT = 20 CONNECT_TIMEOUT_SECS = 30 -# -------------------------------------------------------------- - log = logging.getLogger(__name__) @@ -38,33 +35,34 @@ reraise=True) async def __create_tables(**params): # TODO: move _init_db.metadata here!? - sa_engine = sa.create_engine(DSN.format(**params)) - metadata.create_all(sa_engine) + try: + url = DSN.format(**params) + f"?application_name={__name__}_init" + sa_engine = sa.create_engine(url) + metadata.create_all(sa_engine) + finally: + sa_engine.dispose() + async def pg_engine(app: web.Application): - engine = None - try: - cfg = app[APP_CONFIG_KEY][CONFIG_SECTION_NAME] - params = {k:cfg["postgres"][k] for k in 'database user password host port minsize maxsize'.split()} + cfg = app[APP_CONFIG_KEY][CONFIG_SECTION_NAME] + params = {k:cfg["postgres"][k] for k in 'database user password host port minsize maxsize'.split()} + - if cfg.get("init_tables"): + if cfg.get("init_tables"): + try: # TODO: get keys from __name__ (see notes in servicelib.application_keys) await __create_tables(**params) + except DBAPIError: + log.exception("Could init db. Stopping :\n %s", cfg) + raise - engine = await create_engine(**params) - - except DBAPIError: - log.exception("Could init db. Stopping :\n %s", cfg) - raise - else: + async with create_engine(application_name=f'{__name__}_{id(app)}', **params) as engine: app[APP_DB_ENGINE_KEY] = engine - yield + yield #------------------- - engine = app.get(APP_DB_ENGINE_KEY) - if engine: - engine.close() - await engine.wait_closed() + if engine is not app.get(APP_DB_ENGINE_KEY): + log.error("app does not hold right db engine") def is_service_enabled(app: web.Application): diff --git a/services/web/server/src/simcore_service_webserver/db_config.py b/services/web/server/src/simcore_service_webserver/db_config.py index 148ee2aec93..6747d130cd1 100644 --- a/services/web/server/src/simcore_service_webserver/db_config.py +++ b/services/web/server/src/simcore_service_webserver/db_config.py @@ -10,6 +10,18 @@ CONFIG_SECTION_NAME = 'db' +# FIXME: database user password host port minsize maxsize +#CONFIG_SCHEMA = T.Dict({ +# "database": T.String(), +# "user": T.String(), +# "password": T.String(), +# "host": T.Or( T.String, T.Null), +# "port": T.Or( T.Int, T.Null), +# T.Key("minsize", default=1 ,optional=True): T.Int(), +# T.Key("maxsize", default=4, optional=True): T.Int(), +#}) + + schema = T.Dict({ T.Key("postgres"): _PG_SCHEMA, T.Key("init_tables", default=False): T.Bool(), diff --git a/services/web/server/src/simcore_service_webserver/login/__init__.py b/services/web/server/src/simcore_service_webserver/login/__init__.py index 4696f4cfed9..37f9df83107 100644 --- a/services/web/server/src/simcore_service_webserver/login/__init__.py +++ b/services/web/server/src/simcore_service_webserver/login/__init__.py @@ -41,7 +41,14 @@ async def _setup_config_and_pgpool(app: web.Application): db_cfg = app[APP_CONFIG_KEY][DB_SECTION]['postgres'] # db - pool = await asyncpg.create_pool(dsn=DSN.format(**db_cfg), loop=asyncio.get_event_loop()) + #TODO: setup lifetime of this pool? + #TODO: determin min/max size of the pool + pool = await asyncpg.create_pool( + dsn=DSN.format(**db_cfg) + f"?application_name={module_name}_{id(app)}", + min_size=db_cfg['minsize'], + max_size=db_cfg['maxsize'], + loop=asyncio.get_event_loop()) + storage = AsyncpgStorage(pool) #NOTE: this key belongs to cfg, not settings! # config @@ -65,10 +72,12 @@ async def _setup_config_and_pgpool(app: web.Application): app[APP_LOGIN_CONFIG] = cfg - yield + yield # ---------------- + if config["STORAGE"].pool is not pool: + log.error("Somebody has changed the db pool") try: - await asyncio.wait_for( pool.close(), timeout=TIMEOUT_SECS) + await asyncio.wait_for(pool.close(), timeout=TIMEOUT_SECS) except asyncio.TimeoutError: log.exception("Failed to close login storage loop") diff --git a/services/web/server/src/simcore_service_webserver/security_authorization.py b/services/web/server/src/simcore_service_webserver/security_authorization.py index 2baaaf66903..229181e34ee 100644 --- a/services/web/server/src/simcore_service_webserver/security_authorization.py +++ b/services/web/server/src/simcore_service_webserver/security_authorization.py @@ -1,11 +1,14 @@ import logging -from typing import Dict, Tuple, Union +from typing import Dict, Optional, Tuple, Union import attr +import psycopg2 import sqlalchemy as sa from aiohttp import web from aiohttp_security.abc import AbstractAuthorizationPolicy from aiopg.sa import Engine +from tenacity import (RetryCallState, after_log, retry, + retry_if_exception_type, stop_after_attempt, wait_fixed) from servicelib.application_keys import APP_DB_ENGINE_KEY @@ -15,6 +18,16 @@ log = logging.getLogger(__file__) + +def raise_http_unavailable_error(retry_state: RetryCallState): + # TODO: mark incident on db to determine the quality of service. E.g. next time we do not stop. + # TODO: add header with Retry-After + #obj, query = retry_state.args + #obj.app.register_incidents + # https://tools.ietf.org/html/rfc7231#section-7.1.3 + raise web.HTTPServiceUnavailable() + + @attr.s(auto_attribs=True, frozen=True) class AuthorizationPolicy(AbstractAuthorizationPolicy): app: web.Application @@ -25,56 +38,57 @@ def engine(self) -> Engine: """Lazy getter since the database is not available upon setup :return: database's engine - :rtype: Engine """ # TODO: what if db is not available? #return self.app.config_dict[APP_DB_ENGINE_KEY] return self.app[APP_DB_ENGINE_KEY] + @retry( + retry=retry_if_exception_type(psycopg2.DatabaseError), + wait=wait_fixed(2), + stop=stop_after_attempt(3), + after=after_log(log, logging.ERROR), + retry_error_callback=raise_http_unavailable_error) + async def _safe_execute(self, query): + # NOTE: psycopg2.DatabaseError in #880 and #1160 + # http://initd.org/psycopg/docs/module.html + async with self.engine.acquire() as conn: + ret = await conn.execute(query) + res = await ret.fetchone() + return res - async def authorized_userid(self, identity: str): + async def authorized_userid(self, identity: str) -> Optional[str]: """ Retrieve authorized user id. Return the user_id of the user identified by the identity or "None" if no user exists related to the identity. """ - # pylint: disable=E1120 - async with self.engine.acquire() as conn: - # TODO: why users.c.user_login_key!=users.c.email - query = users.select().where( - sa.and_(users.c.email == identity, - users.c.status != UserStatus.BANNED) - ) - ret = await conn.execute(query) - user = await ret.fetchone() - return user["id"] if user else None - - async def permits(self, identity: str, permission: Union[str,Tuple], context: Dict=None): + # TODO: why users.c.user_login_key!=users.c.email + user = await self._safe_execute( users.select().where( + sa.and_(users.c.email == identity, + users.c.status != UserStatus.BANNED) + )) + return user["id"] if user else None + + async def permits(self, identity: str, permission: Union[str,Tuple], context: Optional[Dict]=None) -> bool: """ Determines whether an identified user has permission :param identity: session identified corresponds to the user's email as defined in login.handlers.registration - :type identity: str :param permission: name of the operation that user wants to execute OR a tuple as (operator.and_|operator.or_, name1, name2, ...) - :type permission: str or tuple :param context: context of the operation, defaults to None - :type context: Dict, optional :return: True if user has permission to execute this operation within the given context - :rtype: bool """ if identity is None or permission is None: log.debug("Invalid indentity [%s] of permission [%s]. Denying access.", identity, permission) return False - async with self.engine.acquire() as conn: - query = users.select().where( - sa.and_(users.c.email == identity, - users.c.status != UserStatus.BANNED) + user = await self._safe_execute( users.select().where( + sa.and_(users.c.email == identity, + users.c.status != UserStatus.BANNED) ) - ret = await conn.execute(query) - user = await ret.fetchone() - - if user: - role = user.get('role') - return await check_access(self.access_model, role, permission, context) + ) + if user: + role = user.get('role') + return await check_access(self.access_model, role, permission, context) return False diff --git a/services/web/server/tests/helpers/utils_tokens.py b/services/web/server/tests/helpers/utils_tokens.py index 51a62ec198c..e1f13fb55a7 100644 --- a/services/web/server/tests/helpers/utils_tokens.py +++ b/services/web/server/tests/helpers/utils_tokens.py @@ -18,6 +18,7 @@ def create_db_tables(**kargs): url = DSN.format(**kargs) engine = sa.create_engine(url, isolation_level="AUTOCOMMIT") metadata.create_all(bind=engine, tables=[users, tokens], checkfirst=True) + engine.dispose() return url diff --git a/services/web/server/tests/unit/with_postgres/conftest.py b/services/web/server/tests/unit/with_postgres/conftest.py index b0efc539d59..e150f5d8684 100644 --- a/services/web/server/tests/unit/with_postgres/conftest.py +++ b/services/web/server/tests/unit/with_postgres/conftest.py @@ -82,6 +82,7 @@ def docker_compose_file(default_app_cfg): os.environ = old + @pytest.fixture(scope='session') def postgres_service(docker_services, docker_ip, default_app_cfg): cfg = deepcopy(default_app_cfg["db"]["postgres"]) @@ -96,6 +97,7 @@ def postgres_service(docker_services, docker_ip, default_app_cfg): timeout=30.0, pause=0.1, ) + return url @pytest.fixture @@ -116,15 +118,15 @@ def postgres_db(app_cfg, postgres_service): engine.dispose() @pytest.fixture -def server(loop, aiohttp_server, app_cfg, monkeypatch, postgres_db): #pylint: disable=R0913 +def web_server(loop, aiohttp_server, app_cfg, monkeypatch, postgres_db): app = create_application(app_cfg) path_mail(monkeypatch) server = loop.run_until_complete( aiohttp_server(app, port=app_cfg["main"]["port"]) ) return server @pytest.fixture -def client(loop, aiohttp_client, server): - client = loop.run_until_complete(aiohttp_client(server)) +def client(loop, aiohttp_client, web_server): + client = loop.run_until_complete(aiohttp_client(web_server)) return client @pytest.fixture diff --git a/services/web/server/tests/unit/with_postgres/test_db.py b/services/web/server/tests/unit/with_postgres/test_db.py index 4a55c5152c8..a7a216acaaf 100644 --- a/services/web/server/tests/unit/with_postgres/test_db.py +++ b/services/web/server/tests/unit/with_postgres/test_db.py @@ -15,7 +15,7 @@ def test_uses_same_postgres_version(docker_compose_file, osparc_simcore_root_dir assert fixture['services']['postgres']['image'] == expected['services']['postgres']['image'] -async def test_responsive(server): - app = server.app +async def test_responsive(web_server): + app = web_server.app assert is_service_enabled(app) assert await is_service_responsive(app) diff --git a/services/web/server/tests/unit/with_postgres/test_login.py b/services/web/server/tests/unit/with_postgres/test_login.py index 1c2a959273c..128d58dfa03 100644 --- a/services/web/server/tests/unit/with_postgres/test_login.py +++ b/services/web/server/tests/unit/with_postgres/test_login.py @@ -79,7 +79,7 @@ async def test_login_inactive_user(client): async def test_login_successfully(client): url = client.app.router['auth_login'].url_for() - r = await client.get(url) + async with NewUser() as user: r = await client.post(url, json={ 'email': user['email'], @@ -91,7 +91,3 @@ async def test_login_successfully(client): assert not error assert data assert cfg.MSG_LOGGED_IN in data['message'] - -if __name__ == '__main__': - import pytest - pytest.main([__file__, '--maxfail=1']) diff --git a/services/web/server/tests/unit/with_postgres/test_users.py b/services/web/server/tests/unit/with_postgres/test_users.py index 140c83cdf7e..0515be683f0 100644 --- a/services/web/server/tests/unit/with_postgres/test_users.py +++ b/services/web/server/tests/unit/with_postgres/test_users.py @@ -8,10 +8,13 @@ import random from copy import deepcopy from itertools import repeat +from unittest.mock import MagicMock import faker import pytest from aiohttp import web +from aiopg.sa.connection import SAConnection +from psycopg2 import OperationalError from yarl import URL from servicelib.application import create_safe_application @@ -140,7 +143,6 @@ async def test_get_profile(logged_user, client, role, expected): assert data['last_name'] == "" assert data['role'] == role.name.capitalize() - @pytest.mark.parametrize("role,expected", [ (UserRole.ANONYMOUS, web.HTTPUnauthorized), (UserRole.GUEST, web.HTTPForbidden), @@ -273,3 +275,43 @@ async def test_delete_token(client, logged_user, tokens_db, fake_tokens, role, e if not error: assert not (await get_token_from_db(tokens_db, token_service=sid)) + + +## BUG FIXES ####################################################### + +@pytest.fixture +def mock_failing_connection(mocker) -> MagicMock: + """ + async with engine.acquire() as conn: + await conn.execute(query) --> will raise OperationalError + """ + # See http://initd.org/psycopg/docs/module.html + conn_execute = mocker.patch.object(SAConnection, "execute") + conn_execute.side_effect=OperationalError("MOCK: server closed the connection unexpectedly") + return conn_execute + +@pytest.mark.parametrize("role,expected", [ + (UserRole.USER, web.HTTPServiceUnavailable), +]) +async def test_get_profile_with_failing_db_connection(logged_user, client, + mock_failing_connection: MagicMock, + role: UserRole, + expected: web.HTTPException): + """ + Reproduces issue https://github.com/ITISFoundation/osparc-simcore/pull/1160 + + A logged user fails to get profie because though authentication because + + i.e. conn.execute(query) will raise psycopg2.OperationalError: server closed the connection unexpectedly + + ISSUES: #880, #1160 + """ + url = client.app.router["get_my_profile"].url_for() + assert str(url) == "/v0/me" + + resp = await client.get(url) + + NUM_RETRY = 3 + assert mock_failing_connection.call_count == NUM_RETRY, "Expected mock failure raised in AuthorizationPolicy.authorized_userid after severals" + + data, error = await assert_status(resp, expected) diff --git a/tests/swarm-deploy/Makefile b/tests/swarm-deploy/Makefile new file mode 100644 index 00000000000..d4ce19fa715 --- /dev/null +++ b/tests/swarm-deploy/Makefile @@ -0,0 +1,30 @@ +.DEFAULT_GOAL := help + +ROOT_DIR = $(realpath $(CURDIR)/../../) +VENV_DIR ?= $(realpath $(ROOT_DIR)/.venv) + + +%.txt: %.in + # pip compiling $< + @$(VENV_DIR)/bin/pip-compile --output-file $@ $< + + +.PHONY: install +install: $(VENV_DIR) requirements.txt ## installs dependencies + # installing requirements + @$ Path: + WILDCARD = "services/web/server" + + root_dir = Path(current_dir) + while not any(root_dir.glob(WILDCARD)) and root_dir != Path("/"): + root_dir = root_dir.parent + + msg = f"'{root_dir}' does not look like the git root directory of osparc-simcore" + assert root_dir.exists(), msg + assert any(root_dir.glob(WILDCARD)), msg + assert any(root_dir.glob(".git")), msg + + return root_dir + + +@pytest.fixture(scope='session') +def docker_client() -> DockerClient: + client = docker.from_env() + yield client + + +@pytest.fixture(scope='session') +def docker_swarm_node(docker_client: DockerClient) -> None: + # SAME node along ALL session + docker_client.swarm.init() + yield #-------------------- + assert docker_client.swarm.leave(force=True) + + +@pytest.fixture(scope='module') +def osparc_deploy( osparc_simcore_root_dir: Path, + docker_client: DockerClient, + docker_swarm_node) -> Dict: + + environ = dict(os.environ) + if "TRAVIS" not in environ: + environ["DOCKER_REGISTRY"] = "local" + environ["DOCKER_IMAGE_TAG"] = "production" + + subprocess.run( + "make info up-version info-swarm", + shell=True, check=True, env=environ, + cwd=osparc_simcore_root_dir + ) + + with open( osparc_simcore_root_dir / ".stack-simcore-version.yml" ) as fh: + simcore_config = yaml.safe_load(fh) + + with open( osparc_simcore_root_dir / ".stack-ops.yml" ) as fh: + ops_config = yaml.safe_load(fh) + + stack_configs = { + 'simcore': simcore_config, + 'ops': ops_config + } + + yield stack_configs #------------------------------------------------- + + WAIT_BEFORE_RETRY_SECS = 1 + + subprocess.run( + "make down", + shell=True, check=True, env=environ, + cwd=osparc_simcore_root_dir + ) + + subprocess.run(f"docker network prune -f", shell=True, check=False) + + for stack in stack_configs.keys(): + while True: + online = docker_client.services.list(filters={"label":f"com.docker.stack.namespace={stack}"}) + if online: + print(f"Waiting until {len(online)} services stop: {[s.name for s in online]}") + time.sleep(WAIT_BEFORE_RETRY_SECS) + else: + break + + while True: + networks = docker_client.networks.list(filters={"label":f"com.docker.stack.namespace={stack}"}) + if networks: + print(f"Waiting until {len(networks)} networks stop: {[n.name for n in networks]}") + time.sleep(WAIT_BEFORE_RETRY_SECS) + else: + break + + (osparc_simcore_root_dir / ".stack-simcore-version.yml").unlink() + (osparc_simcore_root_dir / ".stack-ops.yml").unlink() diff --git a/tests/swarm-deploy/test_service_restart.py b/tests/swarm-deploy/test_service_restart.py new file mode 100644 index 00000000000..ecca0051021 --- /dev/null +++ b/tests/swarm-deploy/test_service_restart.py @@ -0,0 +1,114 @@ +# pylint:disable=unused-variable +# pylint:disable=unused-argument +# pylint:disable=redefined-outer-name + +import logging +import subprocess +import sys +import time +from pathlib import Path +from pprint import pformat +from typing import Dict, List + +import pytest +from docker import DockerClient +from docker.models.services import Service +from tenacity import before_log, retry, stop_after_attempt, wait_fixed + +logger = logging.getLogger(__name__) + +current_dir = Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent + + + +# time measured from command 'up' finished until *all* tasks are running +MAX_TIME_TO_DEPLOY_SECS = 60 +MAX_TIME_TO_RESTART_SERVICE = 5 + + +@pytest.fixture("module") +def deployed_simcore_stack(osparc_deploy: Dict, docker_client: DockerClient) -> List[Service]: + # NOTE: the goal here is NOT to test time-to-deplopy but + # rather guaranteing that the framework is fully deployed before starting + # tests. Obviously in a critical state in which the frameworks has a problem + # the fixture will fail + + @retry( wait=wait_fixed(MAX_TIME_TO_DEPLOY_SECS), + stop=stop_after_attempt(3), + before=before_log(logger, logging.WARNING) ) + def ensure_deployed(): + for service in docker_client.services.list(): + for task in service.tasks(): + assert task['Status']['State'] == task['DesiredState'], \ + f'{service.name} still not ready: {pformat(task)}' + + try: + ensure_deployed() + finally: + # logs table like + # ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR + # xbrhmaygtb76 simcore_sidecar.1 itisfoundation/sidecar:latest crespo-wkstn Running Running 53 seconds ago + # zde7p8qdwk4j simcore_rabbit.1 itisfoundation/rabbitmq:3.8.0-management crespo-wkstn Running Running 59 seconds ago + # f2gxmhwq7hhk simcore_postgres.1 postgres:10.10 crespo-wkstn Running Running about a minute ago + # 1lh2hulxmc4q simcore_director.1 itisfoundation/director:latest crespo-wkstn Running Running 34 seconds ago + # ... + subprocess.run("docker stack ps simcore", shell=True, check=False) + + return [service for service in docker_client.services.list() + if service.name.startswith("simcore_")] + + + +@pytest.mark.parametrize("service_name", [ + 'simcore_webserver', + 'simcore_storage' +]) +def test_graceful_restart_services( + service_name: str, + deployed_simcore_stack: List[Service], + osparc_deploy: Dict): + """ + NOTE: loop fixture makes this test async + NOTE: needs to run AFTER test_core_service_running + """ + service = next( s for s in deployed_simcore_stack if s.name == service_name ) + + # "Status": { + # "Timestamp": "2019-11-18T19:33:30.448132327Z", + # "State": "shutdown", + # "Message": "shutdown", + # "ContainerStatus": { + # "ContainerID": "f2921c983ad934b4daa0c514543bbfd1a9ea89189bd1ad98b67d63b9f98f05be", + # "PID": 0, + # "ExitCode": 143 + # }, + # "PortStatus": {} + # }, + # "DesiredState": "shutdown", + assert all( task['Status']['State'] == "running" for task in service.tasks() ) + + assert service.force_update() + + time.sleep(MAX_TIME_TO_RESTART_SERVICE) + + shutdown_tasks = service.tasks(filters={'desired-state': 'shutdown'}) + assert len(shutdown_tasks) == 1 + + task = shutdown_tasks[0] + assert task['Status']['ContainerStatus']['ExitCode'] == 0, pformat(task['Status']) + + # TODO: check ps ax has TWO processes + ## name = core_service_name.name.replace("simcore_", "") + ## cmd = f"docker exec -it $(docker ps | grep {name} | awk '{{print $1}}') /bin/sh -c 'ps ax'" + # $ docker exec -it $(docker ps | grep storage | awk '{print $1}') /bin/sh -c 'ps ax' + # PID USER TIME COMMAND + # 1 root 0:00 /sbin/docker-init -- /bin/sh services/storage/docker/entry + # 6 scu 0:02 {simcore-service} /usr/local/bin/python /usr/local/bin/sim + # 54 root 0:00 ps ax + + # $ docker exec -it $(docker ps | grep sidecar | awk '{print $1}') /bin/sh -c 'ps ax' + # PID USER TIME COMMAND + # 1 root 0:00 /sbin/docker-init -- /bin/sh services/sidecar/docker/entry + # 6 scu 0:00 {celery} /usr/local/bin/python /usr/local/bin/celery worke + # 26 scu 0:00 {celery} /usr/local/bin/python /usr/local/bin/celery worke + # 27 scu 0:00 {celery} /usr/local/bin/python /usr/local/bin/celery worke diff --git a/tests/swarm-deploy/test_swarm_runs.py b/tests/swarm-deploy/test_swarm_runs.py index a652830364b..d355d4766a8 100644 --- a/tests/swarm-deploy/test_swarm_runs.py +++ b/tests/swarm-deploy/test_swarm_runs.py @@ -1,34 +1,25 @@ -""" -PRECONDITION: - Assumes simcore stack is deployed, i.e. make ops_disabled=1 up-version - -SEE before_script() in ci/travis/system-testing/swarm-deploy -""" - -# pylint:disable=wildcard-import -# pylint:disable=unused-import # pylint:disable=unused-variable # pylint:disable=unused-argument # pylint:disable=redefined-outer-name import asyncio -import datetime import logging import os -import re import sys import urllib from pathlib import Path from pprint import pformat -from typing import Dict +from typing import Dict, List -import docker import pytest -import tenacity -import yaml +from docker import DockerClient +from docker.models.services import Service logger = logging.getLogger(__name__) +current_dir = Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent + + WAIT_TIME_SECS = 20 RETRY_COUNT = 7 MAX_WAIT_TIME=240 @@ -46,68 +37,37 @@ stack_name = os.environ.get("SWARM_STACK_NAME", 'simcore') -stack_service_names = sorted([ f"{stack_name}_{name}" for name in docker_compose_service_names ]) - - - -# UTILS -------------------------------- - -def get_tasks_summary(tasks): - msg = "" - for t in tasks: - t["Status"].setdefault("Err", '') - msg += "- task ID:{ID}, STATE: {Status[State]}, ERROR: '{Status[Err]}' \n".format( - **t) - return msg - - -def get_failed_tasks_logs(service, docker_client): - failed_states = ["COMPLETE", "FAILED", - "SHUTDOWN", "REJECTED", "ORPHANED", "REMOVE"] - failed_logs = "" - for t in service.tasks(): - if t['Status']['State'].upper() in failed_states: - cid = t['Status']['ContainerStatus']['ContainerID'] - failed_logs += "{2} {0} - {1} BEGIN {2}\n".format( - service.name, t['ID'], "="*10) - if cid: - container = docker_client.containers.get(cid) - failed_logs += container.logs().decode('utf-8') - else: - failed_logs += " log unavailable. container does not exists\n" - failed_logs += "{2} {0} - {1} END {2}\n".format( - service.name, t['ID'], "="*10) - - return failed_logs - -# FIXTURES ------------------------------------- - -@pytest.fixture(scope="session") -def here() -> Path: - return Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent +stack_service_names = sorted([ f"{stack_name}_{name}" + for name in docker_compose_service_names ]) + +# wait if running pre-state +# https://docs.docker.com/engine/swarm/how-swarm-mode-works/swarm-task-states/ +pre_states = [ + "NEW", + "PENDING", + "ASSIGNED", + "PREPARING", + "STARTING" +] -def osparc_simcore_root_dir(here) -> Path: - root_dir = here.parent.parent.resolve() - assert root_dir.exists(), "Is this service within osparc-simcore repo?" - assert any(root_dir.glob("services/web/server")), "%s not look like rootdir" % root_dir - return root_dir +failed_states = [ + "COMPLETE", + "FAILED", + "SHUTDOWN", + "REJECTED", + "ORPHANED", + "REMOVE", + "CREATED" +] -@pytest.fixture(scope='session') -def osparc_simcore_services_dir(osparc_simcore_root_dir) -> Path: - services_dir = Path(osparc_simcore_root_dir) / "services" - return services_dir @pytest.fixture(scope="session", params=stack_service_names) -def core_service_name(request): +def core_service_name(request) -> str: return str(request.param) -@pytest.fixture(scope="function") -def docker_client(): - client = docker.from_env() - yield client -@pytest.fixture(scope="function") -def core_services_running(docker_client): +@pytest.fixture +def core_services_running(docker_client: DockerClient) -> List[Service]: # Matches service names in stacks as e.g. # # 'mystack_director' @@ -117,16 +77,27 @@ def core_services_running(docker_client): # for a stack named 'mystack' # maps service names in docker-compose with actual services - running_services = [ s for s in docker_client.services.list() if s.name.startswith(stack_name) ] + running_services = [ s for s in docker_client.services.list() + if s.name.startswith(stack_name) ] return running_services -# TESTS ------------------------------- -def test_all_services_up(core_services_running): + +def test_all_services_up(core_services_running: str, osparc_deploy:Dict): running_services = sorted( [s.name for s in core_services_running] ) assert running_services == stack_service_names + expected = [ f'{stack_name}_{service_name}' + for service_name in osparc_deploy[stack_name]['services'].keys() + ] + assert running_services == sorted(expected) -async def test_core_service_running(core_service_name, core_services_running, docker_client, loop): + +async def test_core_service_running( + core_service_name: str, + core_services_running: List[Service], + docker_client: DockerClient, + loop: asyncio.BaseEventLoop, + osparc_deploy: Dict ): """ NOTE: loop fixture makes this test async """ @@ -146,9 +117,6 @@ async def test_core_service_running(core_service_name, core_services_running, do get_tasks_summary(tasks), get_failed_tasks_logs(running_service, docker_client)) - # wait if running pre-state - # https://docs.docker.com/engine/swarm/how-swarm-mode-works/swarm-task-states/ - pre_states = ["NEW", "PENDING", "ASSIGNED", "PREPARING", "STARTING"] for n in range(RETRY_COUNT): task = running_service.tasks()[0] @@ -165,8 +133,7 @@ async def test_core_service_running(core_service_name, core_services_running, do get_failed_tasks_logs(running_service, docker_client)) -async def test_check_serve_root(): - # TODO: this is +async def test_check_serve_root(osparc_deploy: Dict): req = urllib.request.Request("http://127.0.0.1:9081/") try: resp = urllib.request.urlopen(req) @@ -180,3 +147,34 @@ async def test_check_serve_root(): pytest.fail("The server could not fulfill the request.\nError code {}".format(err.code)) except urllib.error.URLError as err: pytest.fail("Failed reaching the server..\nError reason {}".format(err.reason)) + + + + +# UTILS -------------------------------- + +def get_tasks_summary(tasks): + msg = "" + for t in tasks: + t["Status"].setdefault("Err", '') + msg += "- task ID:{ID}, STATE: {Status[State]}, ERROR: '{Status[Err]}' \n".format( + **t) + return msg + + +def get_failed_tasks_logs(service, docker_client): + failed_logs = "" + for t in service.tasks(): + if t['Status']['State'].upper() in failed_states: + cid = t['Status']['ContainerStatus']['ContainerID'] + failed_logs += "{2} {0} - {1} BEGIN {2}\n".format( + service.name, t['ID'], "="*10) + if cid: + container = docker_client.containers.get(cid) + failed_logs += container.logs().decode('utf-8') + else: + failed_logs += " log unavailable. container does not exists\n" + failed_logs += "{2} {0} - {1} END {2}\n".format( + service.name, t['ID'], "="*10) + + return failed_logs From 54271218ac81514dc69ec6e6c945c989937ce319 Mon Sep 17 00:00:00 2001 From: Ignacio Pascual <4764217+ignapas@users.noreply.github.com> Date: Fri, 22 Nov 2019 10:14:21 +0100 Subject: [PATCH 3/7] Activity Manager v1 (#1102) First iteration of Activity Manager --- .env-devel | 3 + .../v0/components/schemas/activity.yaml | 38 +++ api/specs/webserver/v0/openapi-activity.yaml | 21 ++ api/specs/webserver/v0/openapi.yaml | 4 + .../src/simcore_service_director/producer.py | 12 +- services/rabbit/README.md | 2 +- services/sidecar/src/sidecar/core.py | 8 +- ...ServiceFilter.js => AutocompleteFilter.js} | 23 +- .../osparc/component/filter/TagsFilter.js | 4 + .../service/manager/ActivityManager.js | 134 ++++---- .../component/service/manager/ActivityTree.js | 300 +++++++++++++++++- .../source/class/osparc/data/Resources.js | 16 +- .../class/osparc/desktop/NavigationBar.js | 36 +-- .../source/class/osparc/ui/table/Table.js | 39 +++ .../osparc/ui/table/cellrenderer/Indented.js | 50 +++ .../ui/table/cellrenderer/Percentage.js | 45 +++ .../osparc/ui/table/cellrenderer/Unit.js | 32 ++ .../class/osparc/ui/window/SingletonWindow.js | 30 ++ services/web/server/requirements/_base.txt | 2 +- services/web/server/requirements/_test.in | 2 +- services/web/server/requirements/_test.txt | 4 +- .../activity/__init__.py | 48 +++ .../activity/config.py | 14 + .../activity/handlers.py | 115 +++++++ .../simcore_service_webserver/application.py | 2 + .../application_config.py | 2 + .../config/server-defaults.yaml | 5 + .../config/server-docker-dev.yaml | 5 + .../config/server-docker-prod.yaml | 5 + .../tests/data/test_activity_config.yml | 62 ++++ .../server/tests/data/test_activity_data.json | 54 ++++ services/web/server/tests/unit/conftest.py | 6 +- .../web/server/tests/unit/test_activity.py | 119 +++++++ .../tests/unit/with_postgres/config.yaml | 2 + 34 files changed, 1116 insertions(+), 128 deletions(-) create mode 100644 api/specs/webserver/v0/components/schemas/activity.yaml create mode 100644 api/specs/webserver/v0/openapi-activity.yaml rename services/web/client/source/class/osparc/component/filter/{ServiceFilter.js => AutocompleteFilter.js} (75%) create mode 100644 services/web/client/source/class/osparc/ui/table/Table.js create mode 100644 services/web/client/source/class/osparc/ui/table/cellrenderer/Indented.js create mode 100644 services/web/client/source/class/osparc/ui/table/cellrenderer/Percentage.js create mode 100644 services/web/client/source/class/osparc/ui/table/cellrenderer/Unit.js create mode 100644 services/web/client/source/class/osparc/ui/window/SingletonWindow.js create mode 100644 services/web/server/src/simcore_service_webserver/activity/__init__.py create mode 100644 services/web/server/src/simcore_service_webserver/activity/config.py create mode 100644 services/web/server/src/simcore_service_webserver/activity/handlers.py create mode 100644 services/web/server/tests/data/test_activity_config.yml create mode 100644 services/web/server/tests/data/test_activity_data.json create mode 100644 services/web/server/tests/unit/test_activity.py diff --git a/.env-devel b/.env-devel index ce26b5b581f..ab67f2d4002 100644 --- a/.env-devel +++ b/.env-devel @@ -43,3 +43,6 @@ WEBSERVER_LOGIN_REGISTRATION_INVITATION_REQUIRED=1 # python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key())" WEBSERVER_SESSION_SECRET_KEY=REPLACE ME with a key of at least length 32. WEBSERVER_STUDIES_ACCESS_ENABLED=0 +WEBSERVER_PROMETHEUS_HOST=http://prometheus +WEBSERVER_PROMETHEUS_PORT=9090 +WEBSERVER_PROMETHEUS_API_VERSION=v1 diff --git a/api/specs/webserver/v0/components/schemas/activity.yaml b/api/specs/webserver/v0/components/schemas/activity.yaml new file mode 100644 index 00000000000..dc75293b8a1 --- /dev/null +++ b/api/specs/webserver/v0/components/schemas/activity.yaml @@ -0,0 +1,38 @@ +ActivityEnveloped: + type: object + required: + - data + properties: + data: + $ref: '#/Activity' + additionalProperties: true + error: + nullable: true + default: null + +Activity: + type: object + properties: + stats: + $ref: '#/Status' + limits: + $ref: '#/Limits' + queued: + type: boolean + +Status: + type: object + properties: + cpuUsage: + type: number + minimum: 0 + memoryUsage: + type: number + +Limits: + type: object + properties: + cpus: + type: number + mem: + type: number \ No newline at end of file diff --git a/api/specs/webserver/v0/openapi-activity.yaml b/api/specs/webserver/v0/openapi-activity.yaml new file mode 100644 index 00000000000..5c52c112c59 --- /dev/null +++ b/api/specs/webserver/v0/openapi-activity.yaml @@ -0,0 +1,21 @@ +openapi: 3.0.0 +info: + title: activity management API + version: 0.1.0 + description: API to be consumed by the activity manager to list and run actions on services +servers: + - description: API server + url: '/v0' +paths: + /activity/status: + get: + operationId: get_status + responses: + '200': + description: Object containing queuing, CPU and Memory usage/limits information of services + content: + application/json: + schema: + $ref: './components/schemas/activity.yaml#/ActivityEnveloped' + default: + $ref: './openapi.yaml#/components/responses/DefaultErrorResponse' diff --git a/api/specs/webserver/v0/openapi.yaml b/api/specs/webserver/v0/openapi.yaml index f089a1d2e29..4ff60504ae7 100644 --- a/api/specs/webserver/v0/openapi.yaml +++ b/api/specs/webserver/v0/openapi.yaml @@ -129,6 +129,10 @@ paths: /nodes/{nodeInstanceUUID}/iframe: $ref: './openapi-node-v0.0.1.yaml#/paths/~1nodes~1{nodeInstanceUUID}~1iframe' + # ACTIVITY ------------------------------------------------------------------------- + /activity/status: + $ref: './openapi-activity.yaml#/paths/~1activity~1status' + components: responses: DefaultErrorResponse: diff --git a/services/director/src/simcore_service_director/producer.py b/services/director/src/simcore_service_director/producer.py index 3df573216ab..dc3de526cfc 100644 --- a/services/director/src/simcore_service_director/producer.py +++ b/services/director/src/simcore_service_director/producer.py @@ -96,7 +96,12 @@ async def _create_docker_service_params(app: web.Application, "SIMCORE_HOST_NAME": registry_proxy.get_service_last_names(service_key) + "_" + node_uuid }, "Hosts": get_system_extra_hosts_raw(config.EXTRA_HOSTS_SUFFIX), - "Init": True + "Init": True, + "Labels": { + "user_id": user_id, + "study_id": project_id, + "node_id": node_uuid + } } docker_params = { "auth": await _create_auth() if config.REGISTRY_AUTH else {}, @@ -186,6 +191,11 @@ async def _create_docker_service_params(app: web.Application, log.exception("Could not find swarm network") log.debug("Converted labels to docker runtime parameters: %s", docker_params) + + # set labels for CPU and Memory limits + container_spec["Labels"]["nano_cpus_limit"] = str(docker_params["task_template"]["Resources"]["Limits"]["NanoCPUs"]) + container_spec["Labels"]["mem_limit"] = str(docker_params["task_template"]["Resources"]["Limits"]["MemoryBytes"]) + return docker_params diff --git a/services/rabbit/README.md b/services/rabbit/README.md index 1dade7b4ebb..5558d4d88f5 100644 --- a/services/rabbit/README.md +++ b/services/rabbit/README.md @@ -1,6 +1,6 @@ # service -[RabbitMQ: Message broquer](https://www.rabbitmq.com/) +[RabbitMQ: Message broker](https://www.rabbitmq.com/) ## special configuration diff --git a/services/sidecar/src/sidecar/core.py b/services/sidecar/src/sidecar/core.py index c00b57dc30c..48a018dd288 100644 --- a/services/sidecar/src/sidecar/core.py +++ b/services/sidecar/src/sidecar/core.py @@ -325,7 +325,13 @@ def process(self): environment=self._docker.env, nano_cpus=config.SERVICES_MAX_NANO_CPUS, mem_limit=config.SERVICES_MAX_MEMORY_BYTES, - labels={'user_id': str(self._user_id), 'study_id': str(self._task.project_id), 'node_id': str(self._task.node_id)}) + labels={ + 'user_id': str(self._user_id), + 'study_id': str(self._task.project_id), + 'node_id': str(self._task.node_id), + 'nano_cpus_limit': str(config.SERVICES_MAX_NANO_CPUS), + 'mem_limit': str(config.SERVICES_MAX_MEMORY_BYTES) + }) except docker.errors.ImageNotFound: log.exception("Run container: Image not found") except docker.errors.APIError: diff --git a/services/web/client/source/class/osparc/component/filter/ServiceFilter.js b/services/web/client/source/class/osparc/component/filter/AutocompleteFilter.js similarity index 75% rename from services/web/client/source/class/osparc/component/filter/ServiceFilter.js rename to services/web/client/source/class/osparc/component/filter/AutocompleteFilter.js index b7d88455806..7239ae96e06 100644 --- a/services/web/client/source/class/osparc/component/filter/ServiceFilter.js +++ b/services/web/client/source/class/osparc/component/filter/AutocompleteFilter.js @@ -6,9 +6,9 @@ */ /** - * Filter used for filtering services. Gets the list of services and uses them as possible options for the dropdown. + * Filter with a dropdown and autocomplete */ -qx.Class.define("osparc.component.filter.ServiceFilter", { +qx.Class.define("osparc.component.filter.AutocompleteFilter", { extend: osparc.component.filter.UIFilter, /** @@ -20,18 +20,11 @@ qx.Class.define("osparc.component.filter.ServiceFilter", { this.base(arguments, filterId, groupId); this._setLayout(new qx.ui.layout.Canvas()); - this.__autocompleteField = this.getChildControl("autocompletefield").set({ - placeholder: this.tr("Filter by service") - }); + this.__autocompleteField = this.getChildControl("autocompletefield"); this.getChildControl("clearbutton"); - const services = osparc.store.Store.getInstance().getServices(); - const dropdownData = Object.keys(services).map(key => { - const split = key.split("/"); - return split[split.length-1]; - }); - this.__autocompleteField.setModel(new qx.data.Array(dropdownData)); + this.__attachEventHandlers(); }, properties: { @@ -68,6 +61,14 @@ qx.Class.define("osparc.component.filter.ServiceFilter", { break; } return control || this.base(arguments, id); + }, + + __attachEventHandlers: function() { + this.__autocompleteField.addListener("changeValue", e => this._filterChange(e.getData()), this); + }, + + buildMenu: function(menuData) { + this.__autocompleteField.setModel(new qx.data.Array(menuData)); } } }); diff --git a/services/web/client/source/class/osparc/component/filter/TagsFilter.js b/services/web/client/source/class/osparc/component/filter/TagsFilter.js index 88e40b78e45..bae830295a3 100644 --- a/services/web/client/source/class/osparc/component/filter/TagsFilter.js +++ b/services/web/client/source/class/osparc/component/filter/TagsFilter.js @@ -98,6 +98,10 @@ qx.Class.define("osparc.component.filter.TagsFilter", { this.__menu = new qx.ui.menu.Menu(); this._dropdown.setMenu(this.__menu); } + if (this.__menu.getChildren().find(button => button.getLabel && button.getLabel() === tagName)) { + // Don't add repeated options + return; + } const button = new qx.ui.menu.Button(tagName); button.addListener("execute", e => this.__addTag(tagName, e.getTarget())); this.__menu.add(button); diff --git a/services/web/client/source/class/osparc/component/service/manager/ActivityManager.js b/services/web/client/source/class/osparc/component/service/manager/ActivityManager.js index 0be284fb80f..7e97008a273 100644 --- a/services/web/client/source/class/osparc/component/service/manager/ActivityManager.js +++ b/services/web/client/source/class/osparc/component/service/manager/ActivityManager.js @@ -22,14 +22,24 @@ qx.Class.define("osparc.component.service.manager.ActivityManager", { this.__createFiltersBar(); this.__createActivityTree(); + this.__createFetchingView(); this.__createActionsBar(); - this.__updateTree(); + this.__reloadButton.fireEvent("execute"); + }, + + statics: { + itemTypes: { + STUDY: "study", + SERVICE: "service" + } }, members: { __tree: null, __studyFilter: null, + __fetchingView: null, + __reloadButton: null, /** * Creates the top bar that holds the filtering widgets. */ @@ -41,55 +51,45 @@ qx.Class.define("osparc.component.service.manager.ActivityManager", { const filtersContainer = new qx.ui.container.Composite(new qx.ui.layout.HBox()); const nameFilter = new osparc.component.filter.TextFilter("name", "activityMonitor"); const studyFilter = this.__studyFilter = new osparc.component.filter.StudyFilter("study", "activityMonitor"); - const serviceFilter = new osparc.component.filter.ServiceFilter("service", "activityMonitor"); filtersContainer.add(nameFilter); filtersContainer.add(studyFilter); - filtersContainer.add(serviceFilter); filtersPart.add(filtersContainer); this._add(toolbar); nameFilter.getChildControl("textfield").setPlaceholder(this.tr("Filter by name")); - // React to filter changes - const msgName = osparc.utils.Utils.capitalize("activityMonitor", "filter"); - qx.event.message.Bus.getInstance().subscribe(msgName, msg => { - const model = this.__tree.getDataModel(); - const filterText = msg.getData().name; - const filterStudy = msg.getData().study; - const filter = targetNode => { - const nameFilterFn = node => { - if (filterText && filterText.length) { - if (node.type === qx.ui.treevirtual.MTreePrimitive.Type.BRANCH) { - return true; - } else if (node.label.indexOf(filterText) === -1) { - return false; - } - } - return true; - }; - const studyFilterFn = node => { - if (filterStudy && filterStudy.length) { - if (node.type === qx.ui.treevirtual.MTreePrimitive.Type.LEAF) { - return true; - } else if (filterStudy.includes(node.label)) { - return true; - } - return false; - } - return true; - }; - return nameFilterFn(targetNode) && studyFilterFn(targetNode); - }; - model.setFilter(filter); - }, this); + osparc.data.Resources.get("studies") + .then(studies => studyFilter.buildMenu(studies)); }, /** * Creates the main view, holding an instance of {osparc.component.service.manager.ActivityTree}. */ __createActivityTree: function() { - const tree = this.__tree = new osparc.component.service.manager.ActivityTree(); - this._add(tree, { + this.__tree = new osparc.component.service.manager.ActivityTree(); + this._add(this.__tree, { + flex: 1 + }); + this.__tree.addListener("treeUpdated", () => { + osparc.data.Resources.get("studies") + .then(studies => this.__studyFilter.buildMenu(studies)); + }, this); + }, + + /** + * Creates a simple view with a fetching icon. + */ + __createFetchingView: function() { + this.__fetchingView = new qx.ui.container.Composite(new qx.ui.layout.VBox().set({ + alignX: "center", + alignY: "middle" + })).set({ + visibility: "excluded" + }); + const image = new qx.ui.basic.Image("@FontAwesome5Solid/circle-notch/26"); + image.getContentElement().addClass("rotate"); + this.__fetchingView.add(image); + this._add(this.__fetchingView, { flex: 1 }); }, @@ -99,52 +99,42 @@ qx.Class.define("osparc.component.service.manager.ActivityManager", { */ __createActionsBar: function() { const toolbar = new qx.ui.toolbar.ToolBar(); - const actionsPart = new qx.ui.toolbar.Part(); - toolbar.addSpacer(); - toolbar.add(actionsPart); + const tablePart = new qx.ui.toolbar.Part(); + // const actionsPart = new qx.ui.toolbar.Part(); + toolbar.add(tablePart); + // toolbar.addSpacer(); + // toolbar.add(actionsPart); + + const reloadButton = this.__reloadButton = new qx.ui.toolbar.Button(this.tr("Reload"), "@FontAwesome5Solid/sync-alt/14"); + tablePart.add(reloadButton); + reloadButton.addListener("execute", () => { + this.__tree.exclude(); + this.__fetchingView.show(); + this.__tree.reset().then(() => { + this.__tree.show(); + this.__fetchingView.exclude(); + }); + }, this); + /* const runButton = new qx.ui.toolbar.Button(this.tr("Run"), "@FontAwesome5Solid/play/14"); actionsPart.add(runButton); + runButton.addListener("execute", () => osparc.component.message.FlashMessenger.getInstance().logAs("Not implemented")); const stopButton = new qx.ui.toolbar.Button(this.tr("Stop"), "@FontAwesome5Solid/stop-circle/14"); actionsPart.add(stopButton); + stopButton.addListener("execute", () => osparc.component.message.FlashMessenger.getInstance().logAs("Not implemented")); const infoButton = new qx.ui.toolbar.Button(this.tr("Info"), "@FontAwesome5Solid/info/14"); actionsPart.add(infoButton); + infoButton.addListener("execute", () => osparc.component.message.FlashMessenger.getInstance().logAs("Not implemented")); - this._add(toolbar); - }, + [runButton, stopButton, infoButton].map(button => this.__tree.bind("selected", button, "enabled", { + converter: data => data.length > 0 + })); + */ - /** - * This functions updates the tree with the most recent data. - */ - __updateTree: function() { - const call = osparc.data.Resources.get("studies"); - call.then(studies => { - const model = this.__tree.getDataModel(); - model.clearData(); - studies.forEach(study => { - let parent = null; - for (let key in study.workbench) { - const node = study.workbench[key]; - const metadata = osparc.utils.Services.getNodeMetaData(node.key, node.version); - if (metadata && metadata.type === "computational") { - if (parent === null) { - parent = model.addBranch(null, study.name, true); - } - const rowId = model.addLeaf(parent, node.label); - if (metadata.key && metadata.key.length) { - const splitted = metadata.key.split("/"); - model.setColumnData(rowId, 1, splitted[splitted.length-1]); - } - } - } - }); - model.setData(); - this.__studyFilter.buildMenu(studies); - }).catch(e => { - console.error(e); - }); + this._add(toolbar); } } }); diff --git a/services/web/client/source/class/osparc/component/service/manager/ActivityTree.js b/services/web/client/source/class/osparc/component/service/manager/ActivityTree.js index fdac1e6d1e9..de3da840709 100644 --- a/services/web/client/source/class/osparc/component/service/manager/ActivityTree.js +++ b/services/web/client/source/class/osparc/component/service/manager/ActivityTree.js @@ -8,29 +8,297 @@ /** * This is a table display the status of running services in real time. Simulates, in some cases, the behavior of a tree. * Has sorting and resizing capabilities, and its UI changes depending on its display mode, that changes depending on the activated type of sorting. - * WiP */ qx.Class.define("osparc.component.service.manager.ActivityTree", { - extend: qx.ui.treevirtual.TreeVirtual, + extend: osparc.ui.table.Table, /** * Constructor sets the model and general look. */ construct: function() { - this.base(arguments, [ - "Node", - "Service", - "Status", - "CPU usage", - "GPU usage" - ], { - treeDataCellRenderer: new qx.ui.treevirtual.SimpleTreeDataCellRenderer().set({ - useTreeLines: false - }) - }); - this.set({ - decorator: "no-border", - padding: 0 + this.__model = new qx.ui.table.model.Simple(); + this.__model.setColumns([ + this.tr("Type"), + this.tr("Node"), + this.tr("Service"), + this.tr("Status"), + this.tr("CPU usage"), + this.tr("Memory usage") + ]); + this.base(arguments, this.__model, { + tableColumnModel: obj => new qx.ui.table.columnmodel.Resize(obj), + initiallyHiddenColumns: [0] }); + const columnModel = this.getTableColumnModel(); + columnModel.getBehavior().setMinWidth(1, 80); + columnModel.getBehavior().setMinWidth(2, 80); + + columnModel.setDataCellRenderer(4, new osparc.ui.table.cellrenderer.Percentage("#2c7cce")); + columnModel.setDataCellRenderer(5, new osparc.ui.table.cellrenderer.Percentage("#358475").set({ + unit: "MB" + })); + + this.getSelectionModel().setSelectionMode(qx.ui.table.selection.Model.MULTIPLE_INTERVAL_SELECTION_TOGGLE); + + this._applyMode(this.getMode()); + + this.__filters = {}; + this.__sorting = {}; + + this.__attachEventHandlers(); + }, + + properties: { + mode: { + check: "String", + nullable: false, + init: "hierarchical", + apply: "_applyMode" + }, + selected: { + check: "Array", + init: [], + event: "changeSelection" + }, + alwaysUpdate: { + check: "Boolean", + init: true, + nullable: false + } + }, + + statics: { + modes: { + HIERARCHICAL: "hierarchical", + FLAT: "flat" + } + }, + + events: { + treeUpdated: "qx.event.type.Event" + }, + + members: { + __model: null, + __filters: null, + __sorting: null, + __serviceNames: null, + + _applyMode: function(mode) { + const columnModel = this.getTableColumnModel(); + switch (mode) { + case this.self().modes.HIERARCHICAL: + columnModel.setDataCellRenderer(1, + new qx.ui.table.cellrenderer.Dynamic(cellInfo => { + if (cellInfo.rowData[0] === osparc.component.service.manager.ActivityManager.itemTypes.SERVICE) { + return new osparc.ui.table.cellrenderer.Indented(1); + } + return new osparc.ui.table.cellrenderer.Indented(0); + }) + ); + break; + case this.self().modes.FLAT: + columnModel.setDataCellRenderer(1, new qx.ui.table.cellrenderer.Default()); + break; + } + }, + + _applyFilters: function(filters) { + this.__filters = filters; + const filterText = filters.name; + const filterStudy = filters.study; + // Filtering function + // By text + const nameFilterFn = row => { + if (row[0] === osparc.component.service.manager.ActivityManager.itemTypes.STUDY) { + return true; + } + if (filterText && filterText.length > 1) { + const trimmedText = filterText.trim().toLowerCase(); + return row[1].trim().toLowerCase() + .includes(trimmedText) || + row[2].trim().toLowerCase() + .includes(trimmedText); + } + return true; + }; + const studyFilterFn = (row, index, array) => { + if (row[0] === osparc.component.service.manager.ActivityManager.itemTypes.SERVICE) { + return true; + } + if (filterStudy && filterStudy.length && !filterStudy.includes(row[1])) { + // Remove also its services + let i = index + 1; + let next = array[i]; + while (next && next[0] === osparc.component.service.manager.ActivityManager.itemTypes.SERVICE && i < array.length) { + array.splice(i, 1); + next = array[i]; + } + return false; + } + return true; + }; + // Apply filters (working on a copy of the data) + const filteredData = [...this.getData()].filter(studyFilterFn).filter(nameFilterFn); + this.getTableModel().setData(this.__removeEmptyStudies(filteredData), false); + if (this.__hasActiveSorting()) { + const { + columnIndex, + ascending + } = this.__sorting; + this.getTableModel().sortByColumn(columnIndex, ascending); + } + }, + + __removeEmptyStudies: function(data) { + return data.filter((item, index, array) => { + if (item[0] === osparc.component.service.manager.ActivityManager.itemTypes.STUDY) { + if (index === array.length-1) { + return false; + } + if (item[0] === array[index+1][0]) { + return false; + } + } + return true; + }); + }, + + __removeStudies: function(data) { + return data.filter(item => item[0] !== osparc.component.service.manager.ActivityManager.itemTypes.STUDY); + }, + + /** + * This functions updates the tree with the most recent data. + */ + update: function() { + return Promise.all([osparc.data.Resources.get("studies"), osparc.data.Resources.getOne("activity")]) + .then(async data => { + const studies = data[0] || {}; + const activity = data[1] || {}; + + // Get service names + if (this.__serviceNames === null) { + this.__serviceNames = await osparc.data.Resources.get("servicesTodo"); + } + + const rows = []; + studies.forEach(study => { + let parentAdded = false; + for (var key in study.workbench) { + const node = study.workbench[key]; + if (this.getMode() !== this.self().modes.FLAT && !parentAdded) { + rows.push([ + osparc.component.service.manager.ActivityManager.itemTypes.STUDY, + study.name, + "", + "", + -1, + -1 + ]); + parentAdded = true; + } + const row = []; + // type + row[0] = osparc.component.service.manager.ActivityManager.itemTypes.SERVICE; + // given name + row[1] = node.label; + // original name + if (this.__serviceNames[node.key]) { + row[2] = this.__serviceNames[node.key]; + } else { + const splitted = node.key.split("/"); + row[2] = splitted[splitted.length - 1]; + } + if (Object.keys(activity).includes(key)) { + const stats = activity[key].stats; + const queued = activity[key].queued; + const limits = activity[key].limits; + if (stats) { + row[4] = stats.cpuUsage == null ? null : (Math.round(stats.cpuUsage * 10) / 10) + (limits && limits.cpus ? `/${limits.cpus * 100}` : ""); // eslint-disable-line no-eq-null + row[5] = stats.memUsage == null ? null : (Math.round(stats.memUsage * 10) / 10) + (limits && limits.mem ? `/${limits.mem}` : ""); // eslint-disable-line no-eq-null + row[3] = this.tr("Running"); + } + if (queued) { + row[3] = this.tr("Queued"); + } + } else { + row[3] = this.tr("Not running"); + } + rows.push(row); + } + }); + this.setData(rows); + if (this.__hasActiveFilters()) { + this._applyFilters(this.__filters); + } + if (this.__hasActiveSorting()) { + const { + columnIndex, + ascending + } = this.__sorting; + this.__model.sortByColumn(columnIndex, ascending); + } + this.fireEvent("treeUpdated"); + }) + .catch(e => { + console.error(e); + }) + .then(() => { + // Give a 2 seconds delay + setTimeout(() => { + if (this.getAlwaysUpdate()) { + this.update(); + } + }, 2000); + }); + }, + + __hasActiveFilters: function() { + if (this.__filters.name && this.__filters.name.length) { + return true; + } + if (this.__filters.study && this.__filters.study.length) { + return true; + } + return false; + }, + + __hasActiveSorting: function() { + if (Object.keys(this.__sorting).length) { + return true; + } + return false; + }, + + reset: function() { + this.__sorting = {}; + this.getTableModel().clearSorting(); + this.setMode(this.self().modes.HIERARCHICAL); + return this.update(); + }, + + __attachEventHandlers: function() { + // React to filter changes + const msgName = osparc.utils.Utils.capitalize("activityMonitor", "filter"); + qx.event.message.Bus.getInstance().subscribe(msgName, msg => this._applyFilters(msg.getData()), this); + + this.__model.addListener("sorted", e => { + this.__sorting = e.getData(); + this.setMode(this.self().modes.FLAT); + this.getTableModel().setData(this.__removeStudies(this.getTableModel().getData()), false); + }, this); + + this.getSelectionModel().addListener("changeSelection", e => { + this.setSelected(this.getSelection()); + }, this); + + this.addListener("disappear", () => { + this.setAlwaysUpdate(false); + }, this); + this.addListener("appear", () => { + this.resetAlwaysUpdate(); + }, this); + } } }); diff --git a/services/web/client/source/class/osparc/data/Resources.js b/services/web/client/source/class/osparc/data/Resources.js index 9efc0fff528..e3acaad9a0c 100644 --- a/services/web/client/source/class/osparc/data/Resources.js +++ b/services/web/client/source/class/osparc/data/Resources.js @@ -311,6 +311,18 @@ qx.Class.define("osparc.data.Resources", { url: statics.API + "/storage/locations/{locationId}/files/{fileUuid}" } } + }, + /* + * ACTIVITY + */ + activity: { + usesCache: false, + endpoints: { + getOne: { + method: "GET", + url: statics.API + "/activity/status" + } + } } }; }, @@ -385,7 +397,7 @@ qx.Class.define("osparc.data.Resources", { } console.log(`Fetching ${resource} from server.`); } - return this.fetch(resource, "getOne", params); + return this.fetch(resource, "getOne", params || {}); }, /** @@ -403,7 +415,7 @@ qx.Class.define("osparc.data.Resources", { } console.log(`Fetching ${resource} from server.`); } - return this.fetch(resource, "get", params); + return this.fetch(resource, "get", params || {}); }, /** diff --git a/services/web/client/source/class/osparc/desktop/NavigationBar.js b/services/web/client/source/class/osparc/desktop/NavigationBar.js index a2f87cfa795..244ea01b90f 100644 --- a/services/web/client/source/class/osparc/desktop/NavigationBar.js +++ b/services/web/client/source/class/osparc/desktop/NavigationBar.js @@ -190,10 +190,9 @@ qx.Class.define("osparc.desktop.NavigationBar", { __createUserBtn: function() { const menu = new qx.ui.menu.Menu(); - // Feature OFF - // const activityManager = new qx.ui.menu.Button(this.tr("Activity manager")); - // activityManager.addListener("execute", this.__openActivityManager, this); - // menu.add(activityManager); + const activityManager = new qx.ui.menu.Button(this.tr("Activity manager")); + activityManager.addListener("execute", this.__openActivityManager, this); + menu.add(activityManager); const preferences = new qx.ui.menu.Button(this.tr("Preferences")); preferences.addListener("execute", this.__onOpenAccountSettings, this); @@ -246,21 +245,20 @@ qx.Class.define("osparc.desktop.NavigationBar", { win.center(); win.open(); } - } + }, - // FEATURE OFF - // __openActivityManager: function() { - // const activityWindow = new qx.ui.window.Window(this.tr("Activity manager")).set({ - // height: 480, - // width: 600, - // layout: new qx.ui.layout.Grow(), - // appearance: "service-window", - // showMinimize: false, - // contentPadding: 0 - // }); - // activityWindow.add(new osparc.component.service.manager.ActivityManager()); - // activityWindow.center(); - // activityWindow.open(); - // } + __openActivityManager: function() { + const activityWindow = new osparc.ui.window.SingletonWindow("activityManager", this.tr("Activity manager")).set({ + height: 600, + width: 800, + layout: new qx.ui.layout.Grow(), + appearance: "service-window", + showMinimize: false, + contentPadding: 0 + }); + activityWindow.add(new osparc.component.service.manager.ActivityManager()); + activityWindow.center(); + activityWindow.open(); + } } }); diff --git a/services/web/client/source/class/osparc/ui/table/Table.js b/services/web/client/source/class/osparc/ui/table/Table.js new file mode 100644 index 00000000000..70159dd6d5c --- /dev/null +++ b/services/web/client/source/class/osparc/ui/table/Table.js @@ -0,0 +1,39 @@ +/* + * oSPARC - The SIMCORE frontend - https://osparc.io + * Copyright: 2019 IT'IS Foundation - https://itis.swiss + * License: MIT - https://opensource.org/licenses/MIT + * Authors: Ignacio Pascual (ignapas) + */ + +/** + * Qooxdoo's table widget with some convenient methods. + */ +qx.Class.define("osparc.ui.table.Table", { + extend: qx.ui.table.Table, + + properties: { + data: { + check: "Array", + apply: "_applyData" + } + }, + + members: { + getSelection: function() { + const ret = []; + const selectionRanges = this.getSelectionModel().getSelectedRanges(); + if (selectionRanges.length > 0) { + selectionRanges.forEach(range => { + for (let i=range.minIndex; i<=range.maxIndex; i++) { + ret.push(this.getTableModel().getRowData(i)); + } + }); + } + return ret; + }, + + _applyData: function(data) { + this.getTableModel().setData(data, false); + } + } +}); diff --git a/services/web/client/source/class/osparc/ui/table/cellrenderer/Indented.js b/services/web/client/source/class/osparc/ui/table/cellrenderer/Indented.js new file mode 100644 index 00000000000..04bcf13870f --- /dev/null +++ b/services/web/client/source/class/osparc/ui/table/cellrenderer/Indented.js @@ -0,0 +1,50 @@ +/* + * oSPARC - The SIMCORE frontend - https://osparc.io + * Copyright: 2019 IT'IS Foundation - https://itis.swiss + * License: MIT - https://opensource.org/licenses/MIT + * Authors: Ignacio Pascual (ignapas) + */ + +qx.Class.define("osparc.ui.table.cellrenderer.Indented", { + extend: qx.ui.table.cellrenderer.Default, + + construct: function(indentation) { + this.base(arguments); + if (indentation) { + this.setIndentation(indentation); + } else { + this.__updateIndentation(); + } + }, + + statics: { + TAB_SIZE: 4 + }, + + properties: { + indentation: { + check: "Integer", + nullable: false, + init: 0, + apply: "_applyIndentation" + } + }, + + members: { + __indentString: null, + // overridden + _getContentHtml: function(cellInfo) { + const pre = this.base(arguments, cellInfo); + return this.__indentString + pre; + }, + + _applyIndentation: function() { + this.__updateIndentation(); + }, + + __updateIndentation: function() { + const tab = Array(this.self().TAB_SIZE + 1).join(" "); + this.__indentString = Array(this.getIndentation() + 1).join(tab); + } + } +}); diff --git a/services/web/client/source/class/osparc/ui/table/cellrenderer/Percentage.js b/services/web/client/source/class/osparc/ui/table/cellrenderer/Percentage.js new file mode 100644 index 00000000000..ea33b0af32b --- /dev/null +++ b/services/web/client/source/class/osparc/ui/table/cellrenderer/Percentage.js @@ -0,0 +1,45 @@ +/* + * oSPARC - The SIMCORE frontend - https://osparc.io + * Copyright: 2019 IT'IS Foundation - https://itis.swiss + * License: MIT - https://opensource.org/licenses/MIT + * Authors: Ignacio Pascual (ignapas) + */ + +qx.Class.define("osparc.ui.table.cellrenderer.Percentage", { + extend: qx.ui.table.cellrenderer.Html, + + construct: function(color) { + this.base(arguments, "center"); + this.setColor(color); + }, + + properties: { + color: { + check: "String", + nullable: false + }, + unit: { + check: "String", + nullable: false, + init: "%" + } + }, + + members: { + // overridden + _getContentHtml: function(cellInfo) { + if (cellInfo.value == null || cellInfo.value < 0) { // eslint-disable-line no-eq-null + return ""; + } + const splitted = cellInfo.value.split("/"); + const width = typeof parseFloat(splitted[0]) === "number" && splitted.length === 2 ? this._calculateWidthPercentage(splitted[0], splitted[1]) : 0; + return "" + + `
${splitted[0]} ${this.getUnit()}
` + + `
`; + }, + + _calculateWidthPercentage: function(value, limit) { + return (value / limit) * 100; + } + } +}); diff --git a/services/web/client/source/class/osparc/ui/table/cellrenderer/Unit.js b/services/web/client/source/class/osparc/ui/table/cellrenderer/Unit.js new file mode 100644 index 00000000000..a47a71540c5 --- /dev/null +++ b/services/web/client/source/class/osparc/ui/table/cellrenderer/Unit.js @@ -0,0 +1,32 @@ +/* + * oSPARC - The SIMCORE frontend - https://osparc.io + * Copyright: 2019 IT'IS Foundation - https://itis.swiss + * License: MIT - https://opensource.org/licenses/MIT + * Authors: Ignacio Pascual (ignapas) + */ + +qx.Class.define("osparc.ui.table.cellrenderer.Unit", { + extend: qx.ui.table.cellrenderer.Html, + + construct: function(unit) { + this.base(arguments, "center"); + this.setUnit(unit); + }, + + properties: { + unit: { + check: "String", + nullable: false + } + }, + + members: { + // overridden + _getContentHtml: function(cellInfo) { + if (cellInfo.value == null || cellInfo.value < 0) { // eslint-disable-line no-eq-null + return ""; + } + return `${cellInfo.value} ${this.getUnit()}`; + } + } +}); diff --git a/services/web/client/source/class/osparc/ui/window/SingletonWindow.js b/services/web/client/source/class/osparc/ui/window/SingletonWindow.js new file mode 100644 index 00000000000..280935ae996 --- /dev/null +++ b/services/web/client/source/class/osparc/ui/window/SingletonWindow.js @@ -0,0 +1,30 @@ +/* + * oSPARC - The SIMCORE frontend - https://osparc.io + * Copyright: 2019 IT'IS Foundation - https://itis.swiss + * License: MIT - https://opensource.org/licenses/MIT + * Authors: Ignacio Pascual (ignapas) + */ + +qx.Class.define("osparc.ui.window.SingletonWindow", { + extend: qx.ui.window.Window, + + construct: function(id, caption, icon) { + this.setId(id); + const singletonWindows = qx.core.Init.getApplication().getRoot() + .getChildren() + .filter(child => child.classname === this.classname); + const thisWindow = singletonWindows.find(win => win.getId() === id); + if (thisWindow) { + console.log(`Trying to create another SingletonWindow with id ${id}, disposing the old one...`); + thisWindow.dispose(); + } + this.base(arguments, caption, icon); + }, + + properties: { + id: { + check: "String", + nullable: false + } + } +}); diff --git a/services/web/server/requirements/_base.txt b/services/web/server/requirements/_base.txt index ba7dcdea1c3..31252eae57c 100644 --- a/services/web/server/requirements/_base.txt +++ b/services/web/server/requirements/_base.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile _base.in +# pip-compile --output-file=_base.txt _base.in # aadict==0.2.3 # via asset aio-pika==2.9.0 diff --git a/services/web/server/requirements/_test.in b/services/web/server/requirements/_test.in index bf6ae74766d..af23373a70e 100644 --- a/services/web/server/requirements/_test.in +++ b/services/web/server/requirements/_test.in @@ -20,7 +20,7 @@ pytest-runner Faker openapi-spec-validator # TODO: this library is limiting jsonschema<3 (see base.in) tenacity -docker # for integration tests +docker # tools pylint diff --git a/services/web/server/requirements/_test.txt b/services/web/server/requirements/_test.txt index f061d3c86ed..7be7e044822 100644 --- a/services/web/server/requirements/_test.txt +++ b/services/web/server/requirements/_test.txt @@ -22,7 +22,7 @@ atomicwrites==1.3.0 # via pytest attrs==19.1.0 billiard==3.6.0.0 celery==4.3.0 -certifi==2019.3.9 # via requests +certifi==2019.9.11 # via requests cffi==1.12.3 change-case==0.5.2 chardet==3.0.4 @@ -82,7 +82,7 @@ trafaret-config==2.0.2 trafaret==1.2.0 typed-ast==1.3.5 # via astroid typing-extensions==3.7.2 -urllib3==1.25.3 # via requests +urllib3==1.25.6 # via requests vine==1.3.0 wcwidth==0.1.7 # via pytest websocket-client==0.56.0 # via docker diff --git a/services/web/server/src/simcore_service_webserver/activity/__init__.py b/services/web/server/src/simcore_service_webserver/activity/__init__.py new file mode 100644 index 00000000000..7a641b323e3 --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/activity/__init__.py @@ -0,0 +1,48 @@ +import asyncio +import logging + +from aiohttp import web +from servicelib.application_keys import APP_CONFIG_KEY +from servicelib.application_setup import ModuleCategory, app_module_setup +from servicelib.rest_routing import (get_handlers_from_namespace, + iter_path_operations, + map_handlers_with_operations) + +from ..rest_config import APP_OPENAPI_SPECS_KEY +from . import handlers +from .config import CONFIG_SECTION_NAME + +logger = logging.getLogger(__name__) + +@app_module_setup( + __name__, + category=ModuleCategory.ADDON, + depends=['simcore_service_webserver.rest'], + logger=logger) +def setup(app: web.Application): + + # setup routes ------------ + specs = app[APP_OPENAPI_SPECS_KEY] + + def include_path(tup_object): + _method, path, _operation_id = tup_object + return any( tail in path for tail in ['/activity/status'] ) + + handlers_dict = { + 'get_status': handlers.get_status + } + + routes = map_handlers_with_operations( + handlers_dict, + filter(include_path, iter_path_operations(specs)), + strict=True + ) + app.router.add_routes(routes) + + +# alias +setup_activity = setup + +__all__ = ( + 'setup_activity' +) diff --git a/services/web/server/src/simcore_service_webserver/activity/config.py b/services/web/server/src/simcore_service_webserver/activity/config.py new file mode 100644 index 00000000000..0bc5d2a675f --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/activity/config.py @@ -0,0 +1,14 @@ +""" Activity manager configuration + - config-file schema + - prometheus endpoint information +""" +import trafaret as T + +CONFIG_SECTION_NAME = "activity" + +schema = T.Dict({ + T.Key("enabled", default=True, optional=True): T.Bool(), + T.Key("prometheus_host", default='http://prometheus', optional=False): T.String(), + T.Key("prometheus_port", default=9090, optional=False): T.Int(), + T.Key("prometheus_api_version", default='v1', optional=False): T.String() +}) diff --git a/services/web/server/src/simcore_service_webserver/activity/handlers.py b/services/web/server/src/simcore_service_webserver/activity/handlers.py new file mode 100644 index 00000000000..3aa85a4140d --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/activity/handlers.py @@ -0,0 +1,115 @@ +import asyncio + +import aiohttp +from servicelib.application_keys import APP_CONFIG_KEY +from servicelib.client_session import get_client_session +from servicelib.request_keys import RQT_USERID_KEY +from yarl import URL + +from ..computation_handlers import get_celery +from ..login.decorators import login_required + + +async def query_prometheus(session, url, query): + async with session.get(url.with_query(query=query)) as resp: + result = await resp.json() + return result + +def celery_reserved(app): + return get_celery(app).control.inspect().reserved() + +# +# Functions getting the data to be executed async +# +async def get_cpu_usage(session, url, user_id): + cpu_query = f'sum by (container_label_node_id) (irate(container_cpu_usage_seconds_total{{container_label_node_id=~".+", container_label_user_id="{user_id}"}}[20s])) * 100' + return await query_prometheus(session, url, cpu_query) + +async def get_memory_usage(session, url, user_id): + memory_query = f'container_memory_usage_bytes{{container_label_node_id=~".+", container_label_user_id="{user_id}"}} / 1000000' + return await query_prometheus(session, url, memory_query) + +async def get_celery_reserved(app): + return celery_reserved(app) + +async def get_container_metric_for_labels(session, url, user_id): + just_a_metric = f'container_cpu_user_seconds_total{{container_label_node_id=~".+", container_label_user_id="{user_id}"}}' + return await query_prometheus(session, url, just_a_metric) + + +def get_prometheus_result_or_default(result, default): + if (isinstance(result, Exception)): + # Logs exception + return default + return result['data']['result'] + + +@login_required +async def get_status(request: aiohttp.web.Request): + session = get_client_session(request.app) + + user_id = request.get(RQT_USERID_KEY, -1) + + config = request.app[APP_CONFIG_KEY]['activity'] + url = URL(config.get('prometheus_host')).with_port(config.get('prometheus_port')).with_path('api/' + config.get('prometheus_api_version') + '/query') + results = await asyncio.gather( + get_cpu_usage(session, url, user_id), + get_memory_usage(session, url, user_id), + get_celery_reserved(request.app), + get_container_metric_for_labels(session, url, user_id), + return_exceptions=True + ) + cpu_usage = get_prometheus_result_or_default(results[0], []) + mem_usage = get_prometheus_result_or_default(results[1], []) + metric = get_prometheus_result_or_default(results[3], []) + celery_inspect = results[2] + + res = {} + for node in cpu_usage: + node_id = node['metric']['container_label_node_id'] + usage = float(node['value'][1]) + res[node_id] = { + 'stats': { + 'cpuUsage': usage + } + } + + for node in mem_usage: + node_id = node['metric']['container_label_node_id'] + usage = float(node['value'][1]) + if node_id in res: + res[node_id]['stats']['memUsage'] = usage + else: + res[node_id] = { + 'stats': { + 'memUsage': usage + } + } + + for node in metric: + limits = { + 'cpus': 0, + 'mem': 0 + } + metric_labels = node['metric'] + limits['cpus'] = float(metric_labels.get('container_label_nano_cpus_limit', 0)) / pow(10, 9) # Nanocpus to cpus + limits['mem'] = float(metric_labels.get('container_label_mem_limit', 0)) / pow(1024, 2) # In MB + node_id = metric_labels.get('container_label_node_id') + res[node_id]['limits'] = limits + + if (hasattr(celery_inspect, 'items')): + for dummy_worker_id, worker in celery_inspect.items(): + for task in worker: + if (task['args'][1:-1].split(', ')[0] == str(user_id)): # Extracts user_id from task's args + node_id = task['args'][1:-1].split(', ')[2][1:-1] # Extracts node_id from task's args + if node_id in res: + res[node_id]['queued'] = True + else: + res[node_id] = { + 'queued': True + } + + if (not res): + raise aiohttp.web.HTTPNoContent + + return res diff --git a/services/web/server/src/simcore_service_webserver/application.py b/services/web/server/src/simcore_service_webserver/application.py index dd8df96b65c..90c9c0d5468 100644 --- a/services/web/server/src/simcore_service_webserver/application.py +++ b/services/web/server/src/simcore_service_webserver/application.py @@ -11,6 +11,7 @@ from servicelib.monitoring import setup_monitoring from servicelib.application_setup import app_module_setup, ModuleCategory +from .activity import setup_activity from .application_proxy import setup_app_proxy from .computation import setup_computation from .db import setup_db @@ -69,6 +70,7 @@ def create_application(config: Dict) -> web.Application: setup_users(app) setup_projects(app) # needs storage setup_studies_access(app) + setup_activity(app) setup_app_proxy(app) # TODO: under development!!! return app diff --git a/services/web/server/src/simcore_service_webserver/application_config.py b/services/web/server/src/simcore_service_webserver/application_config.py index 71675788853..59e9db8345e 100644 --- a/services/web/server/src/simcore_service_webserver/application_config.py +++ b/services/web/server/src/simcore_service_webserver/application_config.py @@ -25,6 +25,7 @@ from . import (computation_config, db_config, email_config, rest_config, session_config, storage_config) from .director import config as director_config +from .activity import config as activity_config from .login import config as login_config from .projects import config as projects_config from .resources import resources @@ -68,6 +69,7 @@ def create_schema() -> T.Dict: storage_config.CONFIG_SECTION_NAME: storage_config.schema, addon_section(login_config.CONFIG_SECTION_NAME, optional=True): login_config.schema, session_config.CONFIG_SECTION_NAME: session_config.schema, + activity_config.CONFIG_SECTION_NAME: activity_config.schema, #TODO: s3_config.CONFIG_SECTION_NAME: s3_config.schema #TODO: enable when sockets are refactored # BELOW HERE minimal sections until more options are needed diff --git a/services/web/server/src/simcore_service_webserver/config/server-defaults.yaml b/services/web/server/src/simcore_service_webserver/config/server-defaults.yaml index 5637a010b77..0df5824d719 100644 --- a/services/web/server/src/simcore_service_webserver/config/server-defaults.yaml +++ b/services/web/server/src/simcore_service_webserver/config/server-defaults.yaml @@ -57,3 +57,8 @@ projects: location: http://localhost:8043/api/specs/webserver/v0/components/schemas/project-v0.0.1.json session: secret_key: "TODO: Replace with a key of at least length 32" +activity: + enabled: True + prometheus_host: http://prometheus + prometheus_port: 9090 + prometheus_api_version: v1 diff --git a/services/web/server/src/simcore_service_webserver/config/server-docker-dev.yaml b/services/web/server/src/simcore_service_webserver/config/server-docker-dev.yaml index c8184baaa5b..b8595431f37 100644 --- a/services/web/server/src/simcore_service_webserver/config/server-docker-dev.yaml +++ b/services/web/server/src/simcore_service_webserver/config/server-docker-dev.yaml @@ -35,6 +35,11 @@ rabbit: # access_key: ${S3_ACCESS_KEY} # secret_key: ${S3_SECRET_KEY} # bucket_name: ${S3_BUCKET_NAME} +activity: + enabled: True + prometheus_host: ${WEBSERVER_PROMETHEUS_HOST} + prometheus_port: ${WEBSERVER_PROMETHEUS_PORT} + prometheus_api_version: ${WEBSERVER_PROMETHEUS_API_VERSION} login: enabled: True registration_invitation_required: False diff --git a/services/web/server/src/simcore_service_webserver/config/server-docker-prod.yaml b/services/web/server/src/simcore_service_webserver/config/server-docker-prod.yaml index a3a2d285522..39b07b11720 100644 --- a/services/web/server/src/simcore_service_webserver/config/server-docker-prod.yaml +++ b/services/web/server/src/simcore_service_webserver/config/server-docker-prod.yaml @@ -56,4 +56,9 @@ projects: session: # python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key())" secret_key: ${WEBSERVER_SESSION_SECRET_KEY} +activity: + enabled: True + prometheus_host: ${WEBSERVER_PROMETHEUS_HOST} + prometheus_port: ${WEBSERVER_PROMETHEUS_PORT} + prometheus_api_version: ${WEBSERVER_PROMETHEUS_API_VERSION} ... diff --git a/services/web/server/tests/data/test_activity_config.yml b/services/web/server/tests/data/test_activity_config.yml new file mode 100644 index 00000000000..8c155d9f027 --- /dev/null +++ b/services/web/server/tests/data/test_activity_config.yml @@ -0,0 +1,62 @@ +version: '1.0' +main: + client_outdir: /usr/src/app/client + host: 127.0.0.1 + log_level: DEBUG + port: 8080 + testing: true + studies_access_enabled: True + monitoring_enabled: True +director: + host: director + port: 8001 + version: v0 +db: + init_tables: False + postgres: + database: simcoredb + endpoint: postgres:5432 + host: postgres + maxsize: 5 + minsize: 1 + password: simcore + port: 5432 + user: simcore +rabbit: + channels: + log: comp.backend.channels.log + progress: comp.backend.channels.progress + password: simcore + user: simcore +# s3: +# access_key: 'Q3AM3UQ867SPQQA43P2F' +# bucket_name: simcore +# endpoint: play.minio.io:9000 +# secret_key: 'zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG' +login: + enabled: False + registration_invitation_required: False + registration_confirmation_required: True +smtp: + sender: 'OSPARC support ' + host: mail.foo.com + port: 25 + tls: False + username: Null + password: Null +storage: + host: storage + port: 11111 + version: v0 +rest: + version: v0 + location: ${OSPARC_SIMCORE_REPO_ROOTDIR}/api/specs/webserver/v0/openapi.yaml +projects: + location: ${OSPARC_SIMCORE_REPO_ROOTDIR}/api/specs/webserver/v0/components/schemas/project-v0.0.1.json +session: + secret_key: "TODO: Replace with a key of at least length 32" +activity: + enabled: True + prometheus_host: http://prometheus + prometheus_port: 9090 + prometheus_api_version: v1 diff --git a/services/web/server/tests/data/test_activity_data.json b/services/web/server/tests/data/test_activity_data.json new file mode 100644 index 00000000000..d290f45edca --- /dev/null +++ b/services/web/server/tests/data/test_activity_data.json @@ -0,0 +1,54 @@ +{ + "prometheus": { + "cpu_return": { + "data": { + "result": [ + { + "metric": { + "container_label_node_id": "894dd8d5-de3b-4767-950c-7c3ed8f51d8c" + }, + "value": [ + null, + "3.9952102200000006" + ] + } + ] + } + }, + "memory_return": { + "data": { + "result": [ + { + "metric": { + "container_label_node_id": "894dd8d5-de3b-4767-950c-7c3ed8f51d8c" + }, + "value": [ + null, + "177.664" + ] + } + ] + } + }, + "labels_return": { + "data": { + "result": [ + { + "metric": { + "container_label_node_id": "894dd8d5-de3b-4767-950c-7c3ed8f51d8c", + "container_label_nano_cpus_limit": "4000000000", + "container_label_mem_limit": "2147483648" + } + } + ] + } + } + }, + "celery": { + "celery_return": { + "celery@b02a130fc2e1": [{ + "args": "(-1, '5360fc76-09f0-11ea-a930-02420aff000a', '35f95ad4-67b8-4ed8-bd55-84a5d600e687')" + }] + } + } +} \ No newline at end of file diff --git a/services/web/server/tests/unit/conftest.py b/services/web/server/tests/unit/conftest.py index 247b02ba059..8cf0b5f48ef 100644 --- a/services/web/server/tests/unit/conftest.py +++ b/services/web/server/tests/unit/conftest.py @@ -41,7 +41,11 @@ def fake_project(fake_data_dir: Path) -> Dict: yield json.load(fp) - @pytest.fixture def project_schema_file(api_specs_dir: Path) -> Path: return api_specs_dir / "v0/components/schemas/project-v0.0.1.json" + +@pytest.fixture +def activity_data(fake_data_dir: Path) -> Dict: + with (fake_data_dir / "test_activity_data.json").open() as fp: + yield json.load(fp) diff --git a/services/web/server/tests/unit/test_activity.py b/services/web/server/tests/unit/test_activity.py new file mode 100644 index 00000000000..2ce753905fa --- /dev/null +++ b/services/web/server/tests/unit/test_activity.py @@ -0,0 +1,119 @@ +# pylint:disable=unused-variable +# pylint:disable=unused-argument +# pylint:disable=redefined-outer-name + +import importlib +from asyncio import Future +from pathlib import Path + +import yaml + +import pytest +from aiohttp import web +from aiohttp.client_exceptions import ClientConnectionError +from servicelib.application import create_safe_application +from simcore_service_webserver.activity import handlers, setup_activity +from simcore_service_webserver.rest import setup_rest +from simcore_service_webserver.security import setup_security +from simcore_service_webserver.session import setup_session +from utils_assert import assert_status + + +def future_with_result(result): + f = Future() + f.set_result(result) + return f + + +@pytest.fixture +def mocked_login_required(mocker): + mock = mocker.patch( + 'simcore_service_webserver.login.decorators.login_required', + lambda h: h) + importlib.reload(handlers) + return mock + +@pytest.fixture +def mocked_monitoring(loop, mocker, activity_data): + prometheus_data = activity_data.get('prometheus') + cpu_ret = prometheus_data.get('cpu_return') + mocker.patch('simcore_service_webserver.activity.handlers.get_cpu_usage', + return_value=future_with_result(cpu_ret)) + + mem_ret = prometheus_data.get('memory_return') + mocker.patch('simcore_service_webserver.activity.handlers.get_memory_usage', + return_value=future_with_result(mem_ret)) + + labels_ret = prometheus_data.get('labels_return') + mocker.patch('simcore_service_webserver.activity.handlers.get_container_metric_for_labels', + return_value=future_with_result(labels_ret)) + + celery_data = activity_data.get('celery') + celery_ret = celery_data.get('celery_return') + mocker.patch('simcore_service_webserver.activity.handlers.get_celery_reserved', + return_value=future_with_result(celery_ret)) + +@pytest.fixture +def mocked_monitoring_down(mocker): + mocker.patch( + 'simcore_service_webserver.activity.handlers.query_prometheus', + side_effect=ClientConnectionError) + mocker.patch( + 'simcore_service_webserver.activity.handlers.celery_reserved', + side_effect=ClientConnectionError) + return mocker + +@pytest.fixture +def app_config(fake_data_dir: Path, osparc_simcore_root_dir: Path): + with open(fake_data_dir/"test_activity_config.yml") as fh: + content = fh.read() + config = content.replace("${OSPARC_SIMCORE_REPO_ROOTDIR}", str(osparc_simcore_root_dir)) + + return yaml.load(config) + +@pytest.fixture +def client(loop, aiohttp_client, app_config): + app = create_safe_application(app_config) + + setup_session(app) + setup_security(app) + setup_rest(app) + setup_activity(app) + + cli = loop.run_until_complete(aiohttp_client(app)) + return cli + + +async def test_has_login_required(client): + resp = await client.get('/v0/activity/status') + await assert_status(resp, web.HTTPUnauthorized) + +async def test_monitoring_up(mocked_login_required, mocked_monitoring, client): + QUEUED_NODE_ID = '35f95ad4-67b8-4ed8-bd55-84a5d600e687' + RUNNING_NODE_ID = '894dd8d5-de3b-4767-950c-7c3ed8f51d8c' + + resp = await client.get('/v0/activity/status') + data, _ = await assert_status(resp, web.HTTPOk) + assert QUEUED_NODE_ID in data, 'Queued node not present' + assert RUNNING_NODE_ID in data, 'Running node not present' + + celery = data.get(QUEUED_NODE_ID) + prometheus = data.get(RUNNING_NODE_ID) + + assert 'queued' in celery, 'There is no queued key for queued node' + assert celery.get('queued'), 'Queued should be True for queued node' + + assert 'limits' in prometheus, 'There is no limits key for executing node' + assert 'stats' in prometheus, 'There is no stats key for executed node' + + limits = prometheus.get('limits') + assert limits.get('cpus') == 4.0, 'Incorrect value: Cpu limit' + assert limits.get('mem') == 2048.0, 'Incorrect value: Memory limit' + + stats = prometheus.get('stats') + assert stats.get('cpuUsage') == 3.9952102200000006, 'Incorrect value: Cpu usage' + assert stats.get('memUsage') == 177.664, 'Incorrect value: Memory usage' + +async def test_monitoring_down(mocked_login_required, mocked_monitoring_down, client): + resp = await client.get('/v0/activity/status') + await assert_status(resp, web.HTTPNoContent) diff --git a/services/web/server/tests/unit/with_postgres/config.yaml b/services/web/server/tests/unit/with_postgres/config.yaml index b68e278de1e..1e3724ebcd7 100644 --- a/services/web/server/tests/unit/with_postgres/config.yaml +++ b/services/web/server/tests/unit/with_postgres/config.yaml @@ -57,3 +57,5 @@ projects: session: # python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key())" secret_key: 'tjwiMSLe0Xd9dwMlAVQT9pYY9JEnr7rcH05fkUcukVs=' +activity: + enabled: False From 11f1f5b55127d782a1145c2c676dbd55c310e669 Mon Sep 17 00:00:00 2001 From: Pedro Crespo <32402063+pcrespov@users.noreply.github.com> Date: Sat, 23 Nov 2019 15:42:21 +0100 Subject: [PATCH 4/7] Delete bug - login webserver 2019-11-13 14:19:39.png (#1180) --- bug - login webserver 2019-11-13 14:19:39.png | Bin 168243 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 bug - login webserver 2019-11-13 14:19:39.png diff --git a/bug - login webserver 2019-11-13 14:19:39.png b/bug - login webserver 2019-11-13 14:19:39.png deleted file mode 100644 index 0f5c76735fefec6a68c72b0ce20cb639f52bc81b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 168243 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�aOrC#|P2+Oz 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
nwakpZeqmQS_aU@h`gB_>*7+y{_@mk3LgOeTm zS&CQXeY?uc_qQ#qjE-{y5ttY4YCUx{!@-|9>~aRC?jR(b3tUN1$Th_{Qhsw@0Jl8XR+` z5FbDmNL5!u$hIbhEE{|c>lUx3aU z>nCT!W1f?b#rs$Mo;p)Wd%M+V_reKoTIfRKvJaWDyURY>Asf+5*1cLzX~))v`6Oie z^%*R903491n56L`TAjT61GC#NSm%*elqZ`oWaEYzP*GZxhCO z7(*s@pL>eFRc06nQW@;lZN{?Yh(iLbFH)`zDk0JpR_>I^n?7llO4;^Y9icRWr`ttJQ?Vt%d4#N8s$+B)o?io&q=sIz*KZr0upml-P_8K-Dzx7(PR$7!@Q8QPD;^N7fMQOI4I%dF<5wF(z9b8^LU)WrhRk%6=8Q% z+tw`BhUzG`aNmB%i|6qwnnCe@fb5vR{wHL27)2R%(6l`1d^k)8v-Y|c_WDH^r02_T zJnv{^zf zc)a@o(c}1hKUGpDD0!@=Cl52%Y0G5DG6B=b34?+O>^njD&;6vkiuJy(^8AsKbE0Yu z1J+Efu{@Et(l0FQAk<9qjfc5XXN(|7=xNi>Yom>;0%~mgP?M+tZ&$BiwZ(9eGNx7l z^zP~S`-k>L^}Dk_OMopKH@#dL3?+B6?JxfI_e_6Qg3yiz0XIz}12OknoWJVIT{0Cm z=_)FeDwGd~JH81S8Xoq8Mn@PP9`5#4dCY}ukh89PKvyA$@kT;-*9n0X7(teEm!9I! z7w@vUv5R-xev7vpB`ai|h>|~PE_rm2<#W!Bj*$P@ohs;9jS>4JyD2_ZOVO_6D?L6{ z99#$Em`rccDa{Rwwy7LMHZqgQOE^GY-}v5}+E`m#Gh0J8bAkmtnyzzSC!M#IOLxk6 z2#)D6Avjs(MpvKYlYA9K{S~byMgZ3sp%v$G%Z&RllRL+Ik<;9@&05uQos#h^o6t5t zeEsxe!2<5$L|btf)XOzZu{&nyqlA7Z`)~k>Wt{e&S+e+*U35W?s0`aQnzkuh0BV zuD8GEw{o#SF=vfqwG?e!Amw?aq=Hg2_>52r=y<7i+kjsOca3LWggvXfCTjazKKf#2 zHQlvffRod`!hz8v_BkvvZgST;)4j2oSlS5hdaCWV7 zfIxVA9sfrlyy_O+mWU_b8+|B}$^r)L%zS&9$LDysV0t{=kkcau;C$z!9M{y1UVas4VyV!+lEmdB97ii%93%wRVWV}oiI8-u`1CFgJw>HKUbT57^aPCK z*u{U#SazUV@*WW3czJT&hh9@udg;D|Lj4gB#nkMMM9$J#l$N?M#Si$vrIT1s8GcwIx`6koK2r1&@uk$r0%{u_;c9}22)cHZeVhi#RApWUGclwh9 z%_DI|gWB?(-=ae*_O*!{hBcy14XJQuiM8BhI~$l> zS5tg}hU)P(IJQmu)EhxeIO%jqQP;hf3lR?5za+Bg@x=F!t4G#(jZ(Z=@M7#wAUze~ z|D!+sL+fi0+9i=l!w;%4KKGshTlwIabPMG2bM3}tO&0;MeZac+S_L+W>HPp=dnPS$X8NmJd(eD!zl$T?p*8z?P!KQ%sQ+s>2gwXolC)p*-4 z;V#OfiDKQ-j%ZX{C9E%o93-b-CH+QZB@j3UN^2n?t)8~+Niyv@G$(bnR-7StV`16uu6WR2W zu8pDlY|e99b@|=}*Sg=dG?dO7@iYZR+VdSSi#z*fxwMAz zerY(`+w#z^ug1tZ3+X2Yp-arT)v*e87_Ye{qij2n86oT4j7rlr0Il10a2GeG7*s%` zd7hcO#8Lfg&;kD_hC1eDZM&c(c-GzE=-Qz!2Cfh)LG|Xk>rXb31#Ykr1t=F1$?>nKVIBjLIsN9b5iS8Rr#U3FE4F?M{{5xiM#lCL#&0Xf}95!>H{KQ~yzNQ?E zr*~QZWfi3Tc1Xbx8J4MV*3O@V0!2ha#nC5UI((O)r@PoE{}bH0JH4Ztx=)p)FS$Md z@(zHV-qf9$$h65nWZQgBuMZ)HvbXThbRXhD`5!am%kK?y%?5aKX~8a+DJ0(6eWe0U zJd-sDKoW&mVcU)qPzEnk$wreV3+uIb+Wf_lv^R7^g~`4EHLyeVeO7Oc>5VllxWWNl zycoDX?8qn#@9I#dv--q&lrABic~y_ri| ztQiB;r*}=xZb*b&X`(*u)*DsaPrlnKRGz`q`*`$xkFAsypG1@_Rw)Fhs>T@YwX7qc4A`lo&zc($Q$WT3#XZXlk4;%LYOJP7#pP|J#_>!CA+S$u%)w z$0*UEqp~xX>jQN@-f)hvsahI4<)!Dht&ANL3I7MoG@s zenX=InrFStR;d7EvvN@W^WcsS9CY!=t72Ysc_p(Z0$M$%dr!=xtzZevYSa##we?4K z?Lu0y=lxJtQrclRSdf5=v9z}i$3_oXMVmRxVyyBoNFdR*2Nwsp>~VQHPpPOXqkH4_ z_`SQt9rgwsqi3_*!_8inSBlZQEz)g-v+O_Dx%7AUSb#*Ai_&G2%#KRLX z+-y5ydkb$WOZ!-Ct03WQZGC)~TN#ssitdABiS)R;ep+87VRU?)srsFNK3g_8=E#H9 zt-H=eSYeWK-o57$KZxI}Q$F(YsVAxBXhY6m8%Gyy6D^|=KJUo3Pl8zi1_;$MTd%d4 z_1MSuSASSo+Su7AE!(ibgAc|9eYoabk-@{3jc4U3`lq58y*OX*$po_LB?5CTTLgri zP(pMzsLkOwB|P>Cbux9{f%?}|3$2HME~9jW&5b_$;=`@oPQ$ojW0h9v&|G}v|Q>vzWnSYYY+V925& ziQmBH&{hv#KL`<0)<~slWEyEkU~yE%bFwaK^%ajC>%j%o0auLs! zksFAQp(;bwv^s^t#}*CwW@c+sEiYDHDIiHlZ=hOdNNY#A!f3l=C0lXlnjFfHH;~>C zuTB-=Ap$gf`1@_c21>6Ol2L0)WSVj@djqPwgTvxmES~{B+dh6Sjpf%qhU7YXk?F2M zfYPmiwf?cKUZm6P!8NF8JYJ`D_`Z)Y=XIcfAQaj=MlCBUX!*Ud!DJYNPF`^ko{>^7 zGu8E0vS7K+Y=Euu2d>vU@r&g6@V>wTEpJf&dI=Bv+{PcH`NXQ)P*_{Y?-wl9y~r#Z)+VhGWAy181vgItu>)kpDy4>^CQz8- z4cNvd8Ss>w*5}e`BrX{6MWtdkx0RfW?#mwD_zN^H##Ui5Nm}^8dQ!9Bw#SU%9Vc8| z-%k*6K&f6?^ssA0h?J-U^AJHk3q(DN)xk%W`Bc(w=)Wo_;<&-tN%19xjArNFB(fYI zCu&9F%MtDtWOSw*zQ{NUC1H%cbo!pq>72{=HYr-Whif-mYu7i^^(-~JK1#z?jcVZR z57;4^LW5H98$u`!w-G^gT}pQM7FbEv#g*c)$jG*Dp&Kj~86Je*6jGv%To{D#2ykb* z*Cfqu=W2b>ah5i& zs>|a0HS6)&M7Yj~dZ6S$JT60s#J?4?gqfG#pR69BNBF9yyh_9Rf&BV2Mk-G>~M z1ZRsTD3gLNEhhvG5g{)Z$IVzOENwsVRxA7NFKjG*@I%yN=>5MR zF>g2x2W=gyya}5wqxYauBhN(dh4amk^b~$O=WQpDt?@lgLVgO6^&J~x7F|P@3>N?W0@97T7O9wH+7+%mjCRG} zXat%{JWdERWXD33}=WfTHvStc(>gFAzVwme@a>pjM7f7vC?8~G+0E@1uO|nymaI|uq~`9 z2$~F}7B}L7jwnYi^m_~CgPgMJ?&WJVa=!17E=tdl;Ud;nPZM-(IC*g7hW;B??VYXe z1|m)YG%T~i!(k%Yo+=b*FKjqmE3T7#%?;|yO36KqFxC82W>e39{gmIJ=CPV}*?q_| z`Q03ew*g-W1A`aSr?fN=!T{wk@$9;FU;5s;#O)%S9XlgIA=DqxuFy6{g) zWOo%#RKJ-s^v^S^L)uey(8og)hpj$ii*h#h$eV&Kq=ZWfUqz zCu-SkTeE4q?a3>cL*M{iUApQZ=*SnaZm7PAg_v06f1=JGF|E6X3(CrQ9($k}(w}Se zE7fAAV9;6{aw~c+yWmdNcMs0}L}@V_r0!v zeaYLAf^R2nX?@;;K$u<++}orWGzl}{$Kg1XpI&rYq~=50i;p@sH6!IM4yR<_9Fqgk zhLfJ8@74CxH1+lVsrD0IpiU>Q`@JLoaRJ!3N&B)_@`)~F^Yl-+C7Ha<9m|i(XKU74 z!#~Nw1U{cR%(a8Zw-3|YMtD0&Sv{`LWnU1NFntg*|2ZVkpj_~yk_2?{NaSoRuP`0U z2V?fot#`63bhjlI%1k-CPHG(LamAlHAFux2cp{lBBK!ECM(EFr>zh?4*MI%pUd?s8 zM|csQ!@3e5@Zas#pFQq&zW4n5w0~;n5-8xz|0*S*0;-1pN`(Bcnj84&1i|772M$E- z;4`1-tm%Z;q0DgY06}BQs&HHLiHy~H(1;LzQl8k%jKgtlK{?8Wn)&+h!R{vQ_S~xH z=vVH#{g%iUs+@hzuVDRCDwEqVEx`B9=%8TT5RKlvKxy;xxjPBn6`6=}c&K>znu)eQ z`UBQ`<;Pv!uTm)4?wh@j4_~#@GD)yqbm|n%C9S$nLf=$1j%&#-@9m}S!+$1%!$%+T z{+X*S*5B#UpQJBHKAAT~9Ex%%tr(LmdpbRy)NKN0gS$rN9-LbO)`C@o_BpOPyB@ld z*=f)T5uPL0a1&UX)8$P?p8Ua?Pi_{tZ(oK&M00o1 zCTwbZ3JxrcP=z{B?xi^G-C0L_l4tG7!D6N6*k`bqzhv{s|MsbS1Ui?2)J$I2BYoCF zjU<)k@2V8Dw^K!B<{wKAF(U=Q{ z9M}&?lns<-j^j$FYwo#gnr!2AgIaHtDqY2oFC_V{p+Oxc&dyth9d|Ig?h%v`8?{;l zCR`a#D{sGQ#MfFT?{qMG9RbB2d$!Q(lQu+G)m1B`DluR_&*>Jc?Pzv3A>(@TVLm^~ zBb|qSb!rOoy8nBx3xE)7PwH`=vIQ4$ranz;Ub3f*|^p$s9H5Q+EAjb>WS?yQ3 zOSn7#-q?4r$Xn{4pM>Q7{IXKSg6y?MSCuLGy*=#XW)P%@dA=0#xHKIkbHYh(U3G@@ z-#Hta$2$QUSmD0G8jP+^!7csR$l9&tKCn=|u(_Is^h@XZGr{Zs6z-sIYQUAq^`ZxB zCu4EP$x8n5Rj092d;jP&YA4IajTKeU{@X}Q0jGDAI8(NplJRlnD?25ocLpFIi71>A zb#MO9KY~5ebTxo+E-ttJ6c8XUuJcwqvP|mmYxj*OEbqNE-5A ziAp3wdJSGR?Q*7S{(*8-a?-YSR0Pnk6U6hBN4@iq40-b(bv8;6<=BM+zt{_97N}uf z7}ge4qtM^eO%wf3)cj_zrB9@xsY9ijX1`z}%k||^;`!5}T~uq7+8P8_;sM12cu9BJ zTNz)}(n4;C>q>3Cs~I{O)5wc6p?ht|x2M-W7G@>+p&{zGvc-CQ)Lu%?G-J1!vP_0( zKxBs%Z~cw-hrT4<10k?<@l2c3|DLw2Obu7^=x)F8zocEMK(8Y+l!Rc&~ ztA3af2=(KYV^N`@X8m}u-ka&qP48a(>GtaSGJS!1&TQ^>tLF-YmT!&k0_)^Hr!O$^ zEuyr2?}uh5ZW@lrwd(~7ur5Ig(QLGyA)l|o&D{I6j3DW6&oBM0^@0P8sGTwkE+`=2 zhu)OWiNf>1&V@@x9(-zxep^5G#*>Ne6Px-{3r@>L4{dLE)fyp}ei z;qoI+wy`XhUn483FEM^}r$d9{pTAY|Lm(V$MItrG4a%P&Z-rt-I7D}_OQqR3&9gRxZ@mXU{JKmuv+!D=f9Wop3km`^Bv`2T`J_JxcNhCNuTCLqD ziQg&Syl9re5KJl+`y5_;VxOk;`x52^Hkd|3vI9;SG8Zwbrc{QmR zb2=Hw!fxP(UeSwUdW4(ipzgg_Ij)nf=*}-ri~R5i_pjyq zGvg9AQ`~)47<=4gM>~z-^L%l|gQ{&I$qj|oI{okU%;vGz`N&!X8`!TuG4j3im$H#N zOc|FHf2$%W#A3=QFB@8+zqm+>x`qMD)*~xj^OxJ7!fii2CZB#Wq+el+BW{HB+AHQ^ zPq_25bI8fFEq`?pquU-jaT45f=X5#%BSX%kf$%`{K#NtJDz zoqpbY<46C9y~j$uk+2mNhGl{|iOCXKjO1ulD^d0^|Bsd>nq{TLOLXSmv8pxBH60CPE$-;BUcw%4*f^C1zY^N6 zH1g89vM9PcXgYH-^p+um8W)3TmPh5&+X*5n*72RWv(21 zMJTkpzip>2YBQ;R&^5XxQisW;ErEP#iD?;H$^BZi3Q{4F2o|Y=vN2Kbyc)06Yn6X+ zLQE|4dNl}{p0Nogs{aOWjT=K_pZUe;)lYDy|JMHeSj+-SQIIuRl~hp5lXjQ{j33XpOd-xyhwEg-yQjm{_?)t^ zU2tO{zXcR#7At%7_`v-4R5qM7O3!cB*jP7yC12tB#1w0~8Q*e`g)A+QQO4Zai@K>) zEN?O$4h`A)iVf&V2m#C0)|r0Y5Has9H|mQ!4&J_-OGJh9&@mP)8#Uz=2dxp{9G4CB z6+-iI^Ui?v8%ae#Yh2cndV=eSSUwRl12iM3*$bexQl+nN`W{ULuI9>%0GBJacj1L=1 z(W=OD39bu&GEa+w~$PKApP*FKQeU&Y4E)jVychbe9C6Jvh3jAW>R+3N5}uDup(AL7pM;jd1LE(&{_oYIaj!;NqwWz6jN-6 z``R81`n9pF5sCo9Z~ zG5sSimylIZdSIlkz{q)om)(BMg%V|XiwCG@C%Vn;JU6L&H@DXkmoLEHyu+S29gX5o zoeDt*&JE7So0ac3X!8zprrP|Q4NlH*LDmjLvSair%1VFnJOGEyl(>?K_h*Haaj|$q z6f)&#Vk6Bc>yC=xKB(6nuJ2-KJZ4ZLXccVBWm4Hq$zv_IBdnU>F_f(q!g- z`iu}DYJJeaFT+5fha`ET9x#}4HFml9_$`iX=2*Vz;M~$2c*q@QLWWCg4#dSF^g(Uv zJQki)*d)~sR&i51Y{Q7>svlze^D|I=)uQHadOk*b7CI{PQJmPP21yS;E znr%11*l#<-dR4rz_sfn+T^S#z1`#gZzP(ovG4(Qf#SjDu?WTefsK>6LbT%;> zdL-^pN?^I;52vzBGJE1076P}1*Vwrcf0!oDqUnT+A>q#2LE3&w3l*I#w|u|y z5mHkNMV>Vq+L#h+W2I3a)QSl6F>84D&nz(+T`Rhvrfc1xJjBXp!BvO^xClz1&S1I- zuJzyo`QrSX59P|aGMQogNaNAYkR|5=?Fa3kwR|2=V*9 zE-Z5Un|n_)SfXul&e$Viam*Dvbr*dGRrDYm(S%Xlv?74ImBPTu%?=F|;kXi5FJD)A z^RvZ1*pR0&kswSl3Ko=5iT!+G@;ghE+@Jo5oPRl0)%9w8+vlQ6-6N!&BrtIF%>EcanNrqIfoVnsDttrolY*e+@}DTwbC)YTelPpYORU2IOP zuJ9XB7ZjGr^=&7jFoer;N5{vz(7P^HT@Q|X)NuvM!6HvgmQ-_3v2wIjc1-*2vWHsD zh|!cW6m1p@S2Ry`@Yvi8g`lq$r`-$P;U!XXnsJW!hsJ84BP;3Y)j@+!Q&>w~eJg5f zxqkVUbzr~+{f&|5zdRY~Cd?Wycqlu|{4A+lsYRRfhwOR*nvk{eL zYMGTnf`mcchnGYH)-46^SoI|o@WVkA#X>X{4{!}!;I-%;V|pxIa${d53xt^be7p<9 z2r}}m6s7DlkU|NuEwz>J*6Q-Dd^8`hswq!Tosk;8xPa4fTrug_<>8PId^WD@46mf& zLx@&oFMe-TzHRC=3i z)`00{yC0m>lQf#%hRI1GvYogxY`w$qSZjRYro)hvhs_u`h$AAbH5K*6bIL>k_lQ`Q zu)suE7cXv*j9gKO^QkMV_rZW{8mJWtiM7(A6b8vbt&De}362YC>l7la#P(KyT6pY3 z6o|=iPL3`dNO5g`RL~)8))-BWq?%>7(`c!-tJ!7_^77u~@U4j%sFmUl&iy;t&h|6fa zGBgOn6DPm9`g*38@NEMPOaQ?oD9hXLx=nLghGYA#U(F&FxA;$G3946&Ni-`eVYTNk z^h65Xhw~9uR!i6)>E?Dpo!y_cE$Ni|hV3z8Q*LRk?Rh-W3y1JKuIb&x3|N?of#(qg zq#JoQc7K8pLcEGPD!o?9?NRTC0Gik;?&NpvH7Y68&8ZCRQl9V^!EBfL{VGE5WRSfz zvDvN=Qrd}Ch{rp1zJigzqdp%Ig6v*pPo31LsjdO{T2WKg8O1q!$fbxetl%Ha`ABTq ztT*X9o4@E`7TWLY?Bo>7@h$rI)YO#0?RnYI4)zlP36a5Eb3@)&tKPSX0aEXy!;@+x zJ%aTGrKr|peB*RY?Nhj4-F%l~6UaqMi)R@aagli?)LYE|@kFEn*t<{7=oG?pFg&#F z%iI`ZUB6S>V3{~>;=D|XR>`u{U8$|5t*xD!l9IyJ(AelEJ*N_i$#j4*UrEg7pjqg| zZ}ZgdioHqlXM28S5k$)ktNX;HK_7dEhL)1@^?bE$OKPQ{I(=XU-nD+Z3Yqzf;4-np zooMI{IB^QCgvM0hLe*jGuX9l5Y!&R>hH@FsmmYDM>nH?DkX@X<;joC8Syb7U{Aqt8 zexd*Qp%35Rv#(Z`O$bqYiAQGKy7wNwX)S5E3@uJvjOu66&91DZd}r#wP)`RJMonJZ znQ~NZ`k%Xmm(sbZK_VMl^04Tyl_e!}$W5UrwOmapcIzTWHJN8$L~qz0-_i3*!}{qb zuW>`&RzcOFX{x(+&TCV+SqvPZLYTbH!vqUSQgWTpgkQn6oL|>uNX~*FBcjZre z3=E*~KU$mB%GgqQ$-^Mu&*!Zybar-TFHaA|dG<#d9TxX8__Y*!AEjWC~Q zCjHu_qZ;opR9I1eu+h3#npGKc0snyx`cI8qoZ?!%_uaF#={9boGO%p9g_^H~SmT{V zi`-m$4M`416Kj26qkR3dT@v4{RhSo5@nOVO*((c-hOiENPej^lBlnAFpb?*I8C!l) zWe;F0NN`-4i-X;U9FaXy%E(};hJUk3FRjwQOM;?cW$koM7E)IGU6RL3cHFrhr*2;K zB4uI4%h96x`v!-7O9aR`uxqYpN&^N`#-*jA^H)} zzk7=~uscbPkJDMrS3uEc5=Fn&^wOK0!bBW&QdcqZb7|tZV&IUUkQ?43A(w!UFqen< zAs`gPy=X6BB>Us4FTC;z2|EY#c5)0Tz@>Nn#=l>oCw&FiV`MJ=oydwet|(w`R%nk8 z561HD<5$vcl$DbH70?*ufBkqFdiVMKHv6^DUAn-Rp}@WK#;u;m6`df2%p_sSYp`?5 zehV}FlWuqm!pfn#EA58;$VX4jYUU80VMt?;_$#{y** z!ijIEI%T+D+n(pHzlCc^5Th&W2@yDasKNcjOZ(R;$L&sR@NWu4{t4_ni~rw2qRkV| zbm;#X>bYox|Nj>eTDAM|AB6s2VTw~1k^rGbr6J~|61S>fg$@@j2y}E7p3?6S#kX!m zL)5jY-#)h@(})w0IIOmBQ)|iTMJ3r~i=W^9&}HXxVrrEQpX<>S^u?4e=|4fcN~piM z_uRIinkDMn66ghvxtKkhkEtzlnyI_&@_ARuKv}E~+^bntqh~vBXCdBk%4d`2tTnz@ zzvgg}OgZ(fU!l@o>7=mf3ccuGaj-#{)w9oZt7Jm;E%p0q{P$(2*6vV+yAo#VLoF-y z$8SCMet_$YFt}8q5P_k0Y!%85hDS*swb5fOLxu_16mxY=4fk20{XOa$Ny7%CEdO*! zPIpKh|E+tHM=uOLNy;GXde*zJV#VavaE2nY3c`#(!z7r^Q z{$L*JplbF`Xl)tv!#+l)^!AE}{k(!VBb27<{_?jLCPrGWwl_{jJt3L2K#0L&%!Z4l z>L5W{tA&EfgooX}a|HB10t|^?-hIH;7CR+vpg384mrK>j_;ff-BGi~|x4(?q$*9nl zFh~lOu|`C?5td=iaMA<^nsO&^A)oM)3e^W<6hsz@BUuZt+L=^LqlhZ+v%ZSw=et8p zN;!>i6=bKDqpBPt$-G=4S@bP z{(13mUa)E8d$gkPWJ4KkO z=i<6<{nttr)T1xDb9n5gz)@z+;x2d8Om9tBd+Nrr?n<&%mjxJM8@NJjV;MhkP zT&pz4%=#}Igap0d6{ujZ9pO7Cy9Pu<*$$7)Z}3lxtX;#aAyp&N<<;NRM9!{6V}Qs-<%cjf5b!^5B$UTx^j*^9@Jxh z_j{DIfh@9i$*jBvl_v=U?2tuh1xJ_22J@1YMn5E<=e$}nAueKG zO+)PVmeHxfPrPy$nmzv5BZbD$T;W+c6Js@{p9Nb&gJmhC!ul7SFXwcf0^q#qo6h0k zCST0PM&8LGZ<)wGfchV4Br1obB3kYX7TqgWAd5&{+r$mTQ#W_p+_n2{drwOL;{uEV zvANCHCW&o9MlEzm{p=AKCcpdH|rJ9Sy{$OfogA3!OdR!l> zA*bh1>!b3YKHN^)NA))K(tRek?BRE7e+NLIolQUBA~j5D#pdjj#2O*FPqd%vpg}Z; zp1-hbiv80lFop*O++2pXKVn?$#=nZhalKj`9&APq+r>|OtcbIQ*%cBU#ul^_WC5*R zcc4lqr*u*RcluwbPg4#v-lb71lUTA2s)LpB)07SmB>;jIy?u*%WDBvyB7f6Iv&WO_ zet>yf(7KHOnoa+&yn!$Vk|O-aAGN( z%NVTvY-1Tp9Qyk|T-H!a8FwYi65_D)*^}=Br@DK1tdTZLJ0Ar|5M!)fx1WIx0H&O6 zXvj0p@F0_|$p~xL&#m^R;S~P|L>73(cXk$1at8u+hg@DjBHl`D-XrKu_56!~5~A{e z(&N=f!7LgwSOWF%KOPS3?I9qdFag6^?u$^H_cl^WPOu;)HVdc@1l>X)er&@b3fVgW4lfqV)X+|Sb{DKi%`&u4@?kTm$!i*bh*9I zZC}O*EeE88|G|-xv}q+BBFbkXcPlrS21mN+AQ4WyW?^VvfJTcI%nu;_3ZvQm7anD( zCm8t%9Osmele@|l#F#1rFbB)x>SNlu&wDy|Q+46`r1byYOy8MY?A`>}2aCzDw$^!c zXYBc#1bn8~Y_dc}W9NLjq1GrxJ`3n`Z*7&sWTF4q@P85*2&*j*i>BKFpJJUXC4-;*q_s%}I^&`IJG$Nb} z{Z#u(WAxL$e-qPRFhs$>f}EINKa+TxTJu|!+s)MLUjJZJ82FsF)APo4we(De0tq0b zF`q2!htU082E$mlDT{REKWUxtxLKdit9wma@!s_8iI684#ZfdjNV<@NfTi`0iw~D` z0}F*j!IK!yKe{R8S(R^D038=+_K@nJz>H<*|0t5aEgn|gtp2xm1DB=%|JqKpG46*DcGn?B&kup3zb|4(kh&HnjLPd;NJi+Eg8BwdXj_kw7@_1K^MqimG=H3 ze~SdlPO}hh;NrH2k--tMzc)V!1gR?bJAJ{&d%Nh{;a(~}QhM0-U@~;zwO-}-yMzYE zl1`z^VFpOau(OL8`A1o$lBGyV*pqh;bk1*tFMH8zLEg7UPD%qxaEb^=xIi4tp16aGG9ZCVq^at@`G_GXkZuh*pSkdvHuoJHwghLrgUKF%S1uh_g99z#>wU#JU!g;G0m=$^iCW7|CP^a6~CNxSJW6H?zslUARe^IdK zTJ08@oQaxB8f>-gxWM;KefZqH>I6Ov;tyG~miTG^!@+FSgN<^86c!glgBUfKYS7~? zgDx=#TR*lD$&743N##B;d++B))sfBODUGCvb9&%{q+AOV+h}_FiIHjk#&NKb^82xlGMZqKiy&l}tOQu0n0*xrO&}J%6)#km)P8fAKZejQyMHX(}1zq(qyu z3{{1P?l0cHKOp))@Dl8V>hmYT`!zz4hx~kP*&!MMa#PLy=;9el7?AMsN~w2=mO6I6 zeT$m9iwAlYC3+=L{AG;nE(?N)1UR`m-XlafUL&!F#&Kted)N6!gw}NexZ+V-vEPJm zPH}w#HT!?5)~b!84`L6bH&1EGjnB`-!P$N#2EO?jU%83ytH&^$rknhE(7{ zuowD^tNo1giCJ?W!L6@ik+ZCwRAkXcOH=Ve$=1gJ(TC!vQodlyvUZO5}j?Ekdt+wI@eTLG>@^2E1wBiyy@ z5@+~4x%*{7ufn3!lk{g=8$yBMLh)7)ffdpx9^zAL{W_Ic`N&4Ym8vnSL+Ql3WPP5y zwPfmC7Xsp|pIX@Y<~L%nQvne&sWaYBL1^*Nzjp$k!ZQXmYOgA&&Cn1sLE1WX>qUCO z{9V3s|LvVl+^^orB^XZxU1UnAgb~!QSUxyh!!_=ct37aQvPI&yoL&G-we2&2ZYd zQvW@m20nFh1062>ud@GBlWV~6p#S3?{Rab>p#j+CU$s2&E# z3Um`Z5B^Gi&*6rXtG}DnqU(}OAzck_8@vACzJvp>>z`pXA(0)j@WCB5yBY1uf^y}&*rye0Q3Jd)7MdP_*t zvZxJgYNOP%6CcO>J?cbTla$rT>Wjripv8f(DbQbBlr7Jku*!h@JJ)PEEu%7xKKW;}9<-#(TEk|69 zFM|zsPl0vBw08f5p?U0Egb9o0(G4vIg8Mz`KThj+9rMMYktRf{mfi)2pvX z2PAMiI9!^d&EdUjfl&!-zZfUEA%#$VOb9D`Fdk=hq4l-9jChtcgUM?FZ?vkxsCHK6 z5A2cV{#;fWOi$-EPkllx@}LxT65RvW+^C_bG7Ya9Dp zC-W9fMq|3WOPNP{v?&Ulg9!>+sEo>bWccNmlE)vW4JFT|n4QbK$4@yu*>Pw5Voz-4 zHLle)S7aGOz_6R zDC*>6*9D}=qSJ?)LiZ;YjlUdD0R9}X8$Gc%x8%s+ObNat^V$C-@d2{d8&6NC*EXU# zcHRR5#ZpkGB^%szy}_lwcJ(s*2qfx03{?J;@^l$6rmD=4vEeavydDj|$K`6Lq$8W& z1}AA4KEP zL$%Gmlh65&$eR5-Iz#x3wo?qZU%Y5!nqSAk%i<5(VTLhqR%LC@`Bs9A2lmLuUeAYx zzXyA;6kXUry}9vsE=$hzEiG2MNpQY>oq>Y%Odh|Ps+n_X_$E)cWu*X&-jZva%R-l2 zzLquR`^UO|0IX}q^E2?UEeP}&_I9L?^!*4TL9WWz2hm&&<%UoRHBMa-?A1kPxrqU} zX)((d?vlmy^JwF-%VmlPz_1QRgUk zZrFTM@ATs#+Y=u^VR~q|0{+o#b?D*ZxKk9C0+<&yg`2^907sKa_wrF6FCn2Xm*<1b<*fw<3Pw?Zdh*lYXYnGz1*fd?xTvl_-= zfEN+HCc&E=abI1e1M8|(9{RKJS3AERJE#i!u;IJW`8~@uQvSId@%?vgR=~H2mvZGM668%0pGNQ7&+FCeT;R zTTAwrq&nHk?;w7tAEud5*gA%aOA08&$9TDV*uuvhnU)=Fv%dboElB8q9M?5q*FwIO z?>T=e55Y~M2qbAFdl4%{G<(Q3| zA9bSRDgDAwlpbzT)7=wzkzQrP#4azx z7?e1b&kfbUP4pRopHrSo=ZEeS+Y~MpRbwH{AV9$CmyVab3X@KG?%> zdUi?=L3680Eia~)!ce`|H*Fj)5z?bxM9d!O`^Jkey3NetoGwj8Gnb>6J6)fXLTSLJ zZjWgx46N<_521{&GJyatZ3y@G#um$iE{)~IpSv!f{uVzHR1jF9L4X4p*5DBeq%LQQ zRNtkjQ=c$0-%dwk7%fx23hZk&c(CO9*y2*wtIg#%s2c895d>{wRtG^4yPrP=Hfi}wi>I@ZugjJBFp;+j9`d~sJDhWQ+86BzWT|WE81~Z-lJG?x{K1Z zjr8Le@cwGZ@HdBP8B(&F(=StHxv|R-m#77jaTlTa5hv0bPg-?A+QesfP&8|ak?t6; zruywE10;#!-$-v&Z}kN_b_a7l1?_u#?Z0|a;X zh7cq{g9mqacL@adK;sQT8+UKyUD^BD&&<3t=Q`(H)1RQPwso&ts{Zw>Dx#LQZ9MS; zdD%}H>y2Ef*H2Mpx%^w-uX2HhF{uHZHl3!ix1`y3R9UwmE$zFOT`3SleJkP-E*R7_ zs%z!fm@+ymuv5jHIf7BGE>j_ZWp$Qc=N8@ljY{ z+~>M5p?fsl(GK@KOIw@tKR8x3Wf#W~$=c1CGKy(mJ;*|~G0~i_M)y-3+vEeT?$M(1 zt&+DI9(u1V_+Kv%8`=T=w@pe8E1PR=AR#|A<^K%pkt{(=tBER6alHk@S)%PB@@Wzg^~vuOtyNfm{vfH@q)auyXDk%~M&i z&NiM6si+`1V5~1%y9}xV2msM2&RJ9nb+xwl>wUeldC$m`OS>Wy|F!wAk5%YYhBQALR(-iC!9>Qao z7xG*$(RQc9G9$D7=H`D7?GBB7q;>jVQM*i?Z*xoJFc5C}*N+YC>QVrQFXrW^14WYR zrVFQLO@=pUj)%p^O;)UP1)sAR7$D;f7ipdgSNHiTQ6{~|X(!%et4IBI1Eq}1!k2|Q zDhxIrYpm(=x+iMK0u%<;ui`c`gLeq{@*zQs;bfJOv3wr||GRqY<736emX%fcCxP4u zMJm)2F`#yQe_uY?xB>S`(N^hA+^be_8;$1j45k17C2C^^bc{2D#c}tQ5Sxk$gKOi3 zisOG4uMONroOv$~ih$SkAyf4xN5h)-&IAYIQ%bC5@p=4izj-kX1_s0yc0O@}nx5+< zTtR^h`xtKo5Vo$#KD(FB>%|6cd1iPQ)#J9MmmVlK(lcKgyhV2+)SV*tKO()LEZSeF zJ+R&zNCh^f_tW6>vH9dt1ajy@T_CBkX}$^aPAz8I6&kwfBI37Y4iA0)%YqSv;)54zF<%_T)pDc)*w0R#n!!O6WK~K#z?{d! zL=!ic-vc}i`y3lr@0tov3X_Z8y5pwRw@F^-;L6imj;Ulkd-smem?A^f8flvwV4FF9 zOQ}?oik8`Y8(V4@X%N5?S`kjh+q!NM47EHWV+X<;tY!U7i z0`Hon%OW044UJL?^4FCxlu~H?wN--RlAf5JZsMkX}&A@5lR z0%9Cqra2{VzSK1q)sR1q|M*c-h!B8ADA0966?rwkgofVfukp7_OG=3OfjG?as(Ux5 zJLj)Q_vfP)9Y?$zj@T<{Hd+#n7Zy6=3ZTr!!Km@%jFA< zHzUWkNBXU-PW*{R=$4WOJ&s&HJA3&?gI_&}YA?}%_+IS${SPjux7-MMr9nL386VLX z<-}zJK%l^QW#GdQ(%{mTi~A#OPqv-=#&)ZXA1>u6-&Vv*&2a2GxR(gYG|NSJm+H|C z-5z9?{dam8Q|f=9hiTvZO%Lx13Ci?QIoxZ7em+BN*Kgyho${}1U9P2TyjUVD#E+~B z%D*a;FjsV#kKO$L0uW!2M*gX+@N%OPz@vK}()|}(=Hg&!Wj>Wy6*w2)Ns9lH`C0T0 zuEGM{OB>nGJM7FNH)#UietHY?6rDYv<)20l;QFN8Ugb-!|B<}_oJj0(wspe8WTToV zj2?zH{*_q=H+INi9XyZUEdzvd^AJkuAAd(A3OEKf2;F~tQG)r3f09996E*alV3CEs zr!Va2ZTAjejUh<^?JtVr=X=>KOr<0I5WcrnYmSCiI=F9==B*A>W;7`;o zk!HrmL#-c{nrpG%7dC!ODc6{LsrO0w&-oYlwC?kVH%srgvx>*f3W-<0(fM4>3STbm zTE1ZnKpIW2(g>~=I$+V5FZ0V(ZMKmbfIJ-C2XPCxjw&&*{$wH|S z1jm#|XbijTRvaFGA`9GU$v==t$cKES9ffu{@3p-BO|NNF`N^8Ie5g&Ce%rqPCNoI^k)2m!E)`y}ru%7I&y zPo?ym{8bU*_WfmA=(p(ui&QjUf&99jhq|PHP-PkbRhAbTCql?T04;x@6wvGZjJ7y5 zK!AXN^%tys+Es*_=?U8`mt<)^wB-PY_bl(c^Jp?>)g_zc`z$LS?uWJ^&(7`3;K%iD zIWZ3|^)piUq_2bhHRH7l=b)!62LW$DOR?iy1W>_#(BmC}Q_v{YHf2BnLa?8LTdbqxQy|mwK#c@D&0R<2Mo*BPV?pXwy!1Qcz0Hyo23>V}i=#E6?Lm3=M7Bg0|G_Lr zc%wHB3#@%M^7HQs{`mv5SMwc`(b+A#(9W2$_=Wgeq<^u+K#2<6U4^_3@m4j1&$EwJ zhLAioN>pq@bGZrAZg^WJxarvYR`Q}uR6vOuN8!ZCg&6dQCuJ(`AsxkS3K2PxQ za;C>+UYOwvkf6oysi%>@F^Gw39@SG3^wdcZx|01=_HM^w92AU4dgh+#zjiWO1mpbn zTK=)3O1u*+$%ad<@UZbU=oQD_t$sH5lfNQSGm6UNP!}5NW%fv%6JIF(cMsivSODK& zaNEP6EMm34wZ{T3uW1%MirXOTg^zXvTcCi-g_mUY*>lxe!fyj@HyWaq2@RR?{Q~O}K{hO!eon+~Imq|WqnoKuO z3{odsbZ(;lP}V4;iY>=(KCZ~Tg>@WK7Qeqj;B}z29Cy+IEi9O+s3OOC6*%g%EZ;nU zWYl!{s5p$Ij)Sm$+CtSI!jv-kn@MuJd&udf9>XXokBH%_sejfX_HAZaN!bQMK;sfl z@iYdrk}TcUOOVm`JE`|Ac1ljMIe_S~^Ylyr0Q@IrpTGlmlR4;snjc;2P28>%X>S_l zIpa~7Ufep)Kh}C2A*iUsk-9od8(%X*vqJ?eTWYVri%l7@nyI{+c6~XFtRTu{=&n*c z+=D%04^&Ko{ye2F5;6LY$!heS4v$_~)%Wc;Ed&lyh*^?-wFIVn+fLdFSu1(;JT49y zv?J3$cHVf*qXU*UzvZ~~K3bvLVSc9KL8s^-XOjR_&3m>^DT^bC7?$CE*eQL9^97W6 zH=0b<(D`CG)OtRX(`i}kC1|^Y-|pVI?ciJK7EVJ;gPZYi^+rHfqoORvZLlE!huEuT zNP}A$6OBkj%2jcXh4qHGMwJo)*}aLICN2nT3g(iFN9J^=oq0geKH>G)d@ z6!iM>R99BIKAisEA)!>P ze^m8S|6+<-ra!SQb5CU@crl(~-sZzzZmdGqenNAk?xiLu3z71UC2!o&%{%MNI-wI) zAfg1y;&Hm~0k_E|ym}eBf{!DKmH9xAq-2Tf-4BfEWe@5-(H8;Q-$lpY@0ey-ayEFMht zRP=v}6GELvf&S~!ZbSLu40qEJKn=2%fN)iyagsC>A49s{?v(4g=VD2mo1|5C4=-20 z(}L62MTU)Gr=Q-mhAfSDg^sk~=lw5EID+05Qs2%ck|d0T#%9hq6Zbo{32%J%g%6GX z>PUtniP+W%SE#pWA8`ph&QgcQNA;dSHRuzDdV72GOXc!+x!mG{u_%+TAY|?vWeQZ6 z7j2{PSQ;ycb-nHZQFG2j!)r$f4V&%@7zgEtpOeKcNwKS>k z@$vC7Pe>2f+;Lp&eS9S@18MoNiKJ9vU1ME*p|6Wtn5Q34Ki-8Y|7E>i-A1vH5XA|& zG>L*TK^v-{lZg3w6|Tb(lWAe@Dkawee0-5%i}*j>Ng;XGyUbvm!}V-+dYyK^^wX5| zn2b+0x)p3j4QIF_Oyj_ZM1NU0W-0w_ZnfCF_0tDz&4ZsG3sD%WseGg-*1ap!EZ>?= zWA;?Z7+k$As6mS_-@CmQ0)``n3~oJkbx}C)b6#?UN}&qrrzmqbe5ef49?vJ_Q{1y) zrM4Gp8XK?*=}1@l;(C&cWyizC^OoHBz~r;feacVg?3pyh7bJbaFECi~3~!EQt~e$* zcc-X6e15=n(511nM0A;823%+4i8QlEbX6f_m zXqcVDbK1q5FtYxVROYai1?BuHC;cRv#59^9^?YR2==Tks!J=EvG>dwhHWMnff-jGl zTA!UZ_x2PiDzrG-xKR1Lur*b8MvHG{A2tYDoYOl%b5JkGD3R4| z?@;;+u@!k#mi^Y6Pm{x0(J=SuO>^bbdDqOS^;RAqE}|2oyLGN;p6hb_o`~Avsr5Y042gTP zJf>PY8$IRE20u-yzUp>iLxtLHESkOf+-c^AB31QN1 zQ!$rrmG|G>+g{C#FW{K`Tn5pw<0e6eu++dZ1#u(j*$;IIQthqj~~UU`Yk-K7hys% zpI<`nHB+g`VQRjay5IesJC;A2&5OO@=-nv61=PLw zo6P3hHQW!J&hjw(p) z_L_9c9rc0=XN1IBfDuH?85i3RwD65YR*Vh(r&VkAVjw4ysII*&jhkM<#O|jbm0Kwl z4z;8ylbuG>?yrDT!(z3~-f&f8@iIMIh{o*R^Ya_I{9>?BLS&QI9apr>DLk?K#Dy%? zxJ4>&h*P3*vXuFi2^4?9Msj8a#jQ=o z#Vy;gs`QoOh@J07NnID|q>qE=t?E4CXY+lO-p-dM?ceb>gtrf{#q(-#!H1?=^V|hb zU5PG?ktyEsJbYxwVKNVdSHZRETi9zbMw4bc%IGKiLtFjhcXpU8JKPrI}a+#S4Z)R};ZM7|hcdSwXPlaLn9G>JL71~by%t~VGPC`^Z){Rov;$55z%q?rd zcrW8~I2^Q1S1U}|V4F-<4#GOCjk-u7IFe=%o#bHbZhR{+zE3{Q?WkRDvRYgTzdcsZ z^+S(NyL$dY87W{f&KRNHfS62U0Dhk1@IIp96{uHQcm<|@sRGoFQ}8I>!|#}zD_yO6 z)>udxs=v@w_zWcTuqBk9@nY7!4NMOGEsR?jMr`))mcinU{YHn5UKM4DxrEWL?is&# zj#1`Zf8dW1#CjLl;ju=1s0e%v+hK}gZBw# ziAbQ~L9VapJowLo0vEycIk=v1&k zqzE9FP0SBOWS`+gmzK0&3tC;htR)98Wk-o#g7R%UMj`$S-{<(|d!vm6Pc5(YM^vG*k_umAnQwoq(|yu>G#!!BMqTcla$gf$-5rH!sW}T}WOi{{!QK^d8>z7K68uwh8>^-E@RAotxr1)c z=z8ac(C``mx->~2(XPeBw#A4qx!rSxDZ-WSLW)M>vvRiDw!ee<_<>9?{P8D+Bc2~t z_IGq1H7A60if+R@#mckp?@7z=WJgPIvtB2%h8p1W4~UTOKkAz)%ZpdrcwckK z2Cy#pU?)C1>>k#3>hk)?&l3o>1^1U2(v!~x*6QDg@I4dFPVNBrb}@m*_>UCZn) z=%m@LtJ=$pie$_x+?OfshSBy-9tpg8)*W3xi!%JNR=4#L!iQf__GQXl-LfFR-%a;= z?N$!d_!|&_4PO7S@zlh>_i9uY!IaJp%yIa5s)oN7dV}(0gkkf}+qG&uO7;1d2qMRc zYg9>&lzi^moh!X-IfCaY-lcs=K>6jB15-i9`Rfm4Xf# z0n^&zmKFyDdXnTQ)rd@|bmm9@IEqeSN|$V$wFZ=w5$W&(?;KwOrRCpvdb;l?jSgXB zjuWSBocLSBKCPFh`{w}R_{%DUUfd{ek4t7JCki%gLm~JM{wno1@((xdPExf9EUF!* zgBC(tK|at2&_BmK#q^UXiEM%Q1a=w$T<$BsE~d?31(y$ah>>iBww}Nb*%?nj7H@B>b#+GsyYcV!F8gn-vGr;bPN*j)paLWNw%Z{{EGcEN;+0;=UFZ!r2 zp%&gE^>^%`Z-MImGwInyG=>6;QT;2p^U=nZvtzF=zc`~$11q#+xibCsMH=<7V|RdX zEvBJ}`S;s-I}tv*3v(B$!!Y8`wv*Ds0l=(#7)dub$0otIpm{oz^4VcVnU|&feHeG9q-qhv6l&zbYWR(AUs@lOp_RGb&=J4gTS@10l*WY$0wd7iZ1^Ok`u3ec-bN1>!epeq5RfCF zR)vx<0)@C;O?nPy0KVQXSI%0%M3L4?91 zS-MC!y;x#agt3VG_dsn&H5rU=rl;+V*DzL|-M}Y_6bP!)bnv*po9QH8CAaL#PtI2` zb3!GH)j`k%)nA;P&1jrv1^XA?R8osCK{8GyhbMduot|-l$c9Rc^oT$Or*fwU!SS=U zLI=+t+myig)t)3`!8DT%0-jlvetTPXi9GkKZVgQ8&Q%!DNWDuqUHXq-f*C1SnmMNC z!)sO(aY>4$jsacLAoQ3 z`z{C3vBGQNde+Ac-*%MYn$LuzP}-HpFp)~**Y>}5(Ju+>{u&1huSAHV?-e=%NOXyC zgKabYJXvi0&MjkkGW9^EIa*$adp24nz-9#`(YdP{Dj}X{;?U<2a>H?}CTf+#&SIM} z)^F*fhM?-# z)S)ZgYwynfo+?SyQGIr1E!yJx%7UN|I_2Q*r~ZO5G|p{#b>1A!mZS3y4q%tozJzNu zo0%gMS0-N2f$e%aF=!_AEVV~j_TnMP!u8Mna0u_4Vbn_Ok&omD8n1IjnkNr>7~fbQ zyyW%J`8dJrpML0VT5G6F2HbhC${44eLDSo|xsL_oFMJs2jEH{28{5!JBLTqk&UG=l z;iNkj`~1oHnP4S)n#aW8PX@q;0-8S*1|g$d`*=ml@sneP0!>^s=@RI*K=`_{Q_MW5 z*$E$8tLXfEAB(2r2f9G*g|LYxBy+T^7o(^A@`p>CiPy8zs-QgIgZSThxfzwr;S)z zPjh`wueW+Oun}^jtbn%n99n4hxO|R3Vg>dyIjfj?v;}-D|IVTVYVo|1@oq0@TX!i_ z)KpgsmFqF^rz(WHr|62F&|lSqG2{xKb(hlqEnL3--e|D#9==4>BIbaPExs#^dG-Ap z;Lf2I^PL+`!~4q{jG*knnwZbQT(i@s`(sQOCmC@LcFyjwSU;3efvICtNFuAp3SY41 zSD7s)sj*9y(J`4B55*2i%2+Q8+7$urS-CIj4SnN&wjn!&G4xZOr<{CZOjK9B871pf-utJZ|*P1B9UxEV@rg;X4W_4O=d8q%kaMXpnT&%5Y*9=`8()R$r)v5REC6fCQun^oF}mr%tKPu8#@XeD23>&V8{ zHS>*7;N=mNJzyqI@$H$!iv6})P4+TSP z7|D@>!n~tvOhazB-@>7ik^Nnf7WRyc%(G(Xm5e$I;j8N`M=!7K4CEd_SUD?mtNwNt_Rw?cl!{hd?fBR|Xn~@5l}| zPWGLS8u187-PI##rypE5x@Lcro-u?qyuW5<^VaF9VVQ|5J_6&PdRtzFq|@zQ1(rdj z08T`OCh7* z{(ZXBf?~t5bcLXg`K05sE{(^xH0qjRppCt@9Qd8MFCB>Iap$Gr0i(L^j_SjF2^2Px z6YkKR^0qUSFwic8p*Mi)27l%?+0!gn?}N{fEou2F3 zps1Dr^O>l2r*^T)D@afBs+~V`r2;=ZMDYVZh}KOQTFOwmxWtGMkfzhTaOyjvyxZ`U z{?#s1oJ}HUX~(#}bpvVm?3LGUd99|48CQ=}eCc}&vA_3YXeppn7oqeqvM|yz{`Hu< zDo|Pk!!3T?T|6^o0U9MFrpMkKEm;4UKg5BYcLLgAjmHmbx}A#{o~BvV=U|JKR6oTI z{GnDG-yV*zib|J_A?s$mb;g;t>CHzpKj^lj_7gy1Kn;}Pu)i$YuDH@0MSO`YmbY`e z`X(&LF6JvBb0|*jxq7z?)7M^yomnD;X)$+TUAte8E>|$@A!Vt{RWDe>8+|L{>#%V^ zSy}O%`Y+#sW`D%=VJz0LAU4Z$R(qDc+-qdbMxo_ zK9BU@t6|~GN=hv>cAw#~>zh%Z`G*B)<7hX*0}BcJ*OlvdQy(ThskM_$1{BkU5Pcu% zqbBf3A`rc@Sq;_I(&#zM!|`j!^=~G3(%AH~?yFz@mKbMa{`6+|-1~l@Gqc6Se@+cY zf8Pe1PMs4vDO>bxe6F0KPrcmNu)m4Nn_Zn-%jCxlpO`mGyWx=l!%EF}-Xk^GI+wbiM?US4B#!kDS^RIU zPobpD>JkK3NC9!?J72gQ&S)@|FgESsJ}P=a-g_G-u%vS}AADa;)l-N^ znI_YU&cE#TtD?&$5f$b0i!1i}i{&!((Q_|0c(x$9 z35KFRBC#UI;wn&pmCYKuMVrjDhN;caU0wK#Y-KwQ_9$w% zuf45F7Pt@uQ1A1I2(l}^Wm4&*)f{a%Oq(euIXMK-Ok2GF9VuVu{Fm&--$hp*8(QiH z`aGBVCi@KyC~)>2B$DEy$H4Rb70nlUl~>PKU|#cZ#qMQ?~g z^+)vW7MmE1mp1$jK_C?@foef%x-Ms1i7_)3SN1n@BStmnD=|#=vx~p2jb{#3V*)PT zn`f~8ltzkjMh1zHjPffZfws~}U?{hibjTpuYl}B6#QLlrXGq!Ek%w*uv`J)g4CNs5 zXMsHwNWB5%68-xTf(RhSW+IH~$n@VkAGDr@d5mYI8@|QDDs^E$1sm7DXdDfF)#jtL zxWpPKnVpJ5dEm>j9*Ao4W9U(!f@DvcAwXZZnbl+CE?L38VerQ6M+_0eThLJYY>C71 z$2XEHS6Z6tUqIvwiebNqLX*rs;(UD~$|&Pc$6X3ma0)BX!)R`h$+9(IxMl-K`^Lm* zAcJNC>#B@hgiA9E-jxz4ObCSg*oOHHH>f|3i`(Hdjf1}QCX?aI=Fnm*pHI6y3e^;f zKB?Mp7-OuC8Z>FZ zT4%Y@@fpDuwNt#L^JtGgcxixQAllJ%ALGZ5X|!vJTA$8qpI9T~CM)~mXuW8U%UDNb z@?y1`XwmBHi97iUqtB^O$Nc>~(o+%4d+nsOeTQjX!;V^BC;5Ya34gA`DJ-ucq#WJ&I9<5YchVr~z8CgOJk-fLYQ1h9yHRT(Y5mKEa+{&O zj{d6h-Rmf#H49W(MTPT|3^f_lnEyv|L#FwEL2l5$1iTg{lRCmNi+5e|`Jq?$2E!N; zV|N{lOfu!x@wn~-ZFi-A!b7mLc?B)8M5QcOe%VC>fIx|lw=3=c$wh4fHSx98ceXCd zj8ZENrMBnc8%uC?DxPt1cGk~|24Zm*yR(DTLElP%a#AQ6`jC&GEcTk~uoPvRX*DuF zktKdBU`Ud0UWH1{T~+d>TND2T*oPO%HK%0>-wK95q3cCND-1s*V2SU_HIBR~MkiCJ01ome*8f<; zZ<*MTZll-+D|B~Jq1S8h*sx*09Y-xi1&`B{A!D;HRc*GoSY&(#*FPZ-Qj-MV?K%tGP%Z zV$U#hH_cW;nDDJfccv$dfnTt65qa7iD>oY~{KH!MHKGYO@z8S^=kiw{8)h(VCIbqD zpB-FXUVSsRU@roO?x^>y(_e`CO{BTWDqSdz*y%Y?1<54niK8$W0JzJ=gjRGMx1qLe z@m3QaKHiQicA;;M%4-d)Ptd@vw*-Fm#H9p$G?glyaeIh0k-uGuSK8-Qubf*44J0;N zS&QY+_kl>;rH@(hx_AH9f(xX@To#q$mzHb`wFA<(5^jYL`^_~8T-%WKAgo2~>xNl6 zUNb09uF<+OK{TwkJodA)L!^Ff9hOZGml#0k$QN6{jNB%4HYBLt6&U;#G=Xw8De~Q_ zsbc*)SL%NjZ0EXL@jNVBAR4F8TY=l`ZjWpMkL*9-hEXj?=}2DL-#~tg_dV}Del1HS zfzNCv{wt4Qr&OAZMbb6L(+^HVMKFKAtNk3B!;jkK7XO8EU=|&kk`gwk*%-!5>zMjY zDmC`gr{8V-j&kR+@@?>6#y~|+tHXKWc~UD0sD)-gFx{lR&RMqNs5IOWtAy)tWL+rH zfFtzYDANFLc&HcC3Rhh@K{|q}V45rt3wn=FOXx|&C%Iq%DWuki?(ghocbWeOV!`2z z9EWZ>zUgh_jSnrq&-p|9opUYC4vt@8-U!#yo!X%Pb(MPe*&&T++zw!{!zvgx#%vBnU&w|aaU>F zZU~KZM3>8-zS|Q8Q@cNIEgFTvT#g((4;%2Oq^``>|F)O!F?(RA|FVWqvjs4YIwmuJ zwF_h-{yVmXde8D-+TQ-Z!3?|@nT5Kx+%I^W!!3FMM8@CeJH)fMs^cko~k z{)b9HyEk4STt=J5$#aI0X`Lk52 z$f5+fd$#8TvQxB`Ku`!O45NP3fD-=7GZC_V9+#Ba)X`{LR8wY@ANs;vXW3&Z0-E2+ zi~ivrNAql?B({WvBcz(Kbcf922`cYELj4D|pboO(8bX$yHl)X=Al0-IGJ=<6oC7YF zze3E$cf7!=eLNWGZy@kj8Ivb~=+^NnDWit!n>twrJR%BkbjWa9n}0bK<&i@&A@9t< zXO;C+u7G%Vx6IVCwJ}YcXU6-d*^2^DBS(`Q0^dV=O_-qhF%@if&v{<`tt>F|B|m0L z0U>pK!coflxQ%w=1u;SGLB@C9kbh4Y%{QZUvd~1jI`0q1rOgF1SgMKq_7i_ZlrQQn_U3o?fQ%snC=@o0gE|E^LE1_`eME%e-*W>M z-@@sMT!?D&j!X74R7Wq-QqXkpz5*~nWvuHZ$ad&2EWwSmabt6EvOvz4CyYZ1{H9PUR)?KQIbQv|s+##}WuhdRjQ@V0mR^ z>FH4tGZGwax8n;olnr&RnSYcYng61`*-x}_T*hq*c|LxNSX-&+H)`k}_uDU7TS2Wh zeE*`#;`P7Q+rt-o7?el+s!V{NEG#D>EK2y~Oqy*r^vBqdu(Oe}FBVdkBN@lk#OlB& z6Hu0A$?itO{++YfBoF<)-y75DGqH9j#wmn=Uj!l3ICz5<{ZBw0M`>7urF;o_2ehq) z-3P@36iM8N#fYHTPA{cGiyo5w9a?prVCgZRkf%imf0U6&LuQ^a1s5h4NjYJW)AcG; zQy!2bL<#*M=5gK%4b){Y%x3@Gg>2Y{w#5cttjNt(`M(JWpr3buI%D1kpM0i#{yhAc zX>iOX9-=1+T!eT82jXjxqtEH*JO6Sv;pgOA7MVNvc)9sFS<1+g*C|oNfbgQsKD2PC zh8KF4PKfbgY`A@!h_u@SG3&YtM#{qPSR%Cb^WWRk_E^)D{LAJV-HO3j2@&+IO0Cy_ za^GwA<462wHPJ!8Ns~MHHWqG^d$c_cetKncM?@%dDl9&TsW9iPBP4MvKOJF>H#^`) z+V~UVr;KoED)cRKekX=SCuar!q)F$%T(i}T4KB*ppoim8NyrC&KnBMB8R)lg7Ht*| z2qP0;-Luc$fUUrK^Nha_`X`X^38c@4-btD%vf!?o@fA+>V~JEJ;o|d6C^Nm;J;8iW z2PG=yXcNR?&$|<1t5n(@xjxs|$?`ooGv(+)^Ft zn^yL5V^_rP{e1s?uQYqKAfd3>Ipfv!` z8XJ2+1L@Cy;=lY60$TtNjxU%df~;L?l$!VaUZY6c-Me@J4Ke`1#>c+1h?>kiC5ZnjgTs6B9E)KhWyDn)C{ z9R8lznR=dqx5pRv=Yq8pc5Sfthq)XU_xDNY_3k#`h1tdtP3-Eob4-_21qCVAyGyEL z&>z9S`9v-=?K57jJzsb=7VbdR8-TorKbFQ7qLkQ`5N_BAMl2^U;3n=;5V;wQct`R` z32M4Te`K|L4QfK}>}=QwMm2ncxVcO608T*x#TTMP>wBs`QMpzT>~2x3*xgB{!Moul z9sbM^sDqW(E`|jvilN&p9G9D}o(8W>Aw15(x1T8%)#`H8TFX{Uie2Q>OU zqd;J&ML#&1EwTHC>u+-g=&01`bA3pZy$Guq4{gDp-eS3dUG+)wqCym{F+0zkr3NA9 zdhI!yKjA`-T8Mx9|G--(+J7sFU;qbS&Wh2vXGarwc%qeL=<)gH0Fb1%d_exsU8HcW zym=#(^lwb>KDz6V+2{&TF#mpzBItqm{{lS!(~71DI(+h<7z8YFkM8m)iI`JPvXa~! z!_cu;4QLmoWiddoX~e~+f_q=TLpZ|hb&6`c=f;8puDft@VJ7tQ6<^wbFkuZ!3Jc79 z9v9;O%lu3sBkaxEYVsfOHHK*H+4YmdbC0!5+RLq5g49c9JDm<9;C)OSHNN zLT-{Vw%CN1Tz;fv6Pejo9+lrBsoX|In2d-fSndLTyQX*k2_(PYqv2PYwW}AY8CNUq z?Cc3%cCA<#F29_Sx?dxm$T6B*8hf$~oM8Vo^q!-G zR+dOqo-`vZJou|Fwa>zP2Ki(wjNOh~&}7cCBe-d-%WA5Kj(sn_8k1wqx zOm_YqB`}Nfj4p#F!fr9WrR;=`FTX^+@UhNeXLv73=e;?Km;cdHD@+x!JTJlZELVHH z?aW;E=N;>oc{Fo4PNkmb{?}PkZ-jtu8+XHCEB`=`)SI%n?IgE#SslMLG_cJqgS zEYd$Y``Uydzq|l(#mVKiSPj*BwahvJw6OmJFJ$EuooXtsVs3A_K!oj^8@UhhJu-8_ zPH^nGXE@O0oCHxiI=NzJ!%62f{X?8LE4)o~x=>QF1#Je!%4lzHw#uy#BJ|~G!*lqE zU~T@c@=~-*VI^~Wa`oF__!QX&nDx_*gdz&{*3hqoR+1-wFM`J3?)TBxgUt_st;?U; zN!C8OvdUzU7!25{iJN`}6}P{3RvHZ~Tf$X~K`jjVc~+Lr7G^{5=l3q;A{yR+zOAW4 z*IjJ=*{G4x+cLD(0f~H5Z~e|I1Lmc)avUiM%EIT#+dIHvUx66~o%sdw4MW~p)>YY5Z9r>CzX8)wrOUuPfnv+qyLk&H^|q->OY_%tR!O=WB? z8c;blTy;0?0IF~8_BP7QmH@Ei6^q+XD**X!&&>+1j%xpHVFCv9d;fM76 zzC@k`11%=3onVDvYkXS?A^+=8Ta}mYy2^9aHuu#V8vJ0NIS#EVYW0<9n3o|8KlMJ+ zF4wkJFzq)F$XI_u|5(<@TR|&GaFlXn z$flc;aAO6<7&n#j*7r@D!Y4E&7rt&fL47w;Xx>@t2cDRVg_HFWv5`z5VR-_KeX?#e4o0z{kdgO{y6V#DfI|eSyTv;(Z~!wYw-%! z(jzYX)jq z8%2+6B$lenc6-rPw4PmOi`lbs(${y^6~&wLKzi4@5Y5WM#{L);93#b}V70%5$@s9t zFl?k7FKE*|;Q9#fVY3SCha_2)M z$WF1iAQ%aRl<#w{bknG+z~j@C?XO&juwvYu8 z?pblv;v2z`sh?Z-*5SJ`KyFqUcCi3Szt7m1gEO9+S!VBzrxfgNE{AkCe1WG{uPd#N zAh+I{>*#TIBzA?+kJc8AgqQ9Nz9&M6GKWE>;fF0^V<(#{q{k`0H#U(GSCbO2SIZH5 zajTahNvXQY|7JF?CPflpTW(qsJ9PJ_Y^=A;fRv}^d|?1yPp)_=ehGy1+jj+uZKs}q zf1(68#MJPZrtvU*TgdW~je&wIG`3D|$Q`dHTaIRHZ=cIjsM_{6 zwk?x*OYW9lc8|^ODjG=SVj{iS{;3eT04y}VvYAlnKmKWq96tR2F!z>0akbmGZ)3qF z!5snwhY;Ke9tav-gF6IwNwDA!!QI`pA;F<>cY-?vclxa4efR!vIrmnbs(b6+{zO$e z-K%@8Ue7bf_{}jdYB(GI6s%x%pc1}($1~fkv|+shYr{^{r_D_`eOkS%mj*PwfIUSPKxEF+CPnDzvV`xL%FD z>o70-%?hM4s~m*5o%ETJQ$>1OX=?r4#cY42yk`hz>w4)K(q3(v#DBk+A-x^kzC2c_ zl_|0#-|gARR5~C@4_N;i+8-!?6ZZ$Csj{|T;)z#wd9GN}&Z#K#EOkF!f9GWGKuwE< z;>8Wl5D3tkC08WmBV4AoEW5m)UtxfHQ{FSrIC(zqdDUyoTCE2`)I=@isjY}v0K5X| zez~qc1vY3<^rP40p$~WOcs51=5#n`9wvA5Xm$d%|?jx@Y3aoirq!*Reglw)0S-E`} z^4zv^SS7(^U3`M1GVf%QT+Fs`lFMzW^!4zC!)t5`{w|w2eE|i+PNOON`oWl-HZEIa zXjEvDl}S>TUL+eO^cN>7uQIZs-b}S#47-;(0-eeaVw4dkQaKnpj!DPCnyzGwtli?- z;!;^TZs|xG*B@uA>-yw7#&6<{eE0B)O7d}jUKxK|`&=0+LnIVEFfAq|*%1u#yCR<~ z+?0-y)_Kg$EWYDuRe|*jUFP?F6U3kT{FxxEDg0|t#oco)Wlc&f3Nu&oCe!fKLZIv} z?)O8C^f4`+=`fFw1Sn}M{q(z?{r$UnJD_{(Daz%{?W*ZoI}8nzpFtAG(wEQS=fThE zTU-h4S6)d3;4*_iqJx1c*A7&7!8*^b5u8iUw=>67GtXe z7dLB{5N}O8HhssMCKvCwv8#OMe&rj~8{h%*E1f-T3!7iNRCpHi|0zbW0=vcc`xjC7 z`xgZmA&x1+*!!-t~5y^TQ~5ZJv9}n~>v%{8uYQW4Ug9y?(#D zq8$mu36$j&M9CQ_VG5E0tFl}5tu(D;EFa6+_^&16D zpKICB0`VUU7~^mv+#Oh5VxaH8y!q8r7q6C5{q5KSO+D_?O6|m-NtL(zo=$vx_WiA0 z0<_&+?3O0e3@X6E(&-QTX;La!$iaNHSa^HlFMMWqG+S^c*yH2SS?8C|KaVfoc6W70 zjlOt8`jxM*41qOgo<12`QI3~(B(Ed~MFoj4dE%EP#_Ex~`$JvT{~J!&S^s|mPFNVc zI{OC;0DuZj=RY|6;xAzo8S*u3I<(_P6JpiAz=Ck%V}lsKApPXynV2)szLKCwananX#@^OL3`}n&w#x%?NRaR;NUQRnE63S;<}9@a3BY zc1g+Xer1X5ah~VX%0^|>2x^Lzu;U+J$fp$BILZ9jBZkhH=sSq8aw~Klmf0;dWt`&H z{yI3l&9xbxV7Kp}p0krv7$LPJb~qvhhRL#Nzfx?d2qd@ERYf0-Q@*%Vd)WHYt(tIM zOgI}PrX^pIOt@J(d2mSu_1O=7G*D3bRLy#kt2pi?MVrkaVANPa@S*<9opfVmcR&%S ze-N~H;P=269^ez8I~I$!*g>N_f++O>R|y{U_9oweD%Zt|C*VD5{L!~ae%W&$%FOA8 zdJ+7RY?k9ja@CvL9e>tEGPEhTVMgj43alSYiV=KVa*Y;ES;I;LmPmd%bofd|2()0; zV+VK7x;LL2wa_F!T!-b%O$)!d<1KHc`-*eqv9W479g`4Wj;w@(FxCqR?6PB%(>RRI z5fhHMOZ_3V&NQn{>aAaw@yq|$iC}hFkcp-m`;~vFe&9l?_IJ`h(pWbC8tc&~Gz1BD zG&t_->nj$H$(^2|+ORtAj(EGZ*q&8*C{^BkPHo=Pj4F;q)996g`sje{8(P-S3tuk2 zy{m>?=(m}sj%1nNLLWC5(NTpjGkiBC&q;Lv*=PvQFBnDy$LEIik*7S1$PPZcMtrxk z*0!KLgf)%OiO#n*Q5ooMBYJAB-FPv1g+T5s&&P35a6%zbjra382&=SQh5KGE(yPKU zP>TYD@&|J`Vv)u_Jp@K;;tAz0%owZ(am~uB7eFwl>#r8&vlmK?N#yolgt=Be$()N8 zHDLe6IULJ+$`~}M*{X-CURMEH-)v#05`ZCZ=Ra&*_dgCxqAfp@0(VgDdyhLg^u)ib zUVOL;ZZyy|#1GmNco^<#W{cvy|L*#uJr-Wc+NE*s<>xbJIagJbz-bW>4k)KN=f$^n zw0w}?3hVLBO$hGiY6u$Qjs-nJG&^d}2Veb9Wv}b_HCzci%btNiG!pv74a-AumT#N3 zIaFlWj}y>0^kCK|W&)z|SBM;Z_CC$)#H>5l>b2r{uZIydVDpkJ9-PQetXFAC07*C2 z^$op}O4qY9qpED`T11jT`_m^Lk0gy2sWv z--5DWK$eU$`vfasX2A=!Tv(7&6(R*nCvHjOQ9457jJ6#r(C1o1){?P`@4aZWw=`u> zalEQkOg7lCvt}2YYsYrF+1WTsYh-+{ zy#mklhy@o}W7K#XS{4x{z1Eg*LWs6!QP~sKm;O#0V>DfWB+aS5^&bt3AT!K>c{|uKH9*f1Ct`W9aZ}Wc%8uND6y1uLH-;#NFyPLO7 zpiVKJOh{{4$&-NwT!WQ(re?F;4SeNM&p=K>-jEth6qi07I&;t+p znexxXPFYa#7Xmr;(;S(*zj6}+bznG3@b#m3HYwTGp0sKI!`rK+X8 zoQ0@*@n#N9$CcKwCI>A-J__|(<=cJjDd*mg7FWNEw%^~9JyL0U5|oj0#<5s363Au< z57EY~V0TE#w%fsesrx`Dz9=`0jx4RwI`>nr1Js~X#98=3EEs@D>}R1pWQ=7+SCu^02FTe|7wlNDj6)UxaqKZP`7 zc$Vt)#mi+`cYKPUo@xC4SF`ldaKZS*EWnl!LaG67CW}21Vz?t@V1(hlXtvV=SSPx&RH0Vag!FQaSg$$;im_56?S2&w>>V{;3Yx~i6##4 z!fW|T6Qb}VnJj6g>BYj=+d9b@{1@QK9Ae;nw|hDEOG_2Uo9d@zjyHLL*A)9flV>Y4 zs&*KbE?|?O?(LO^x|eSJbx(MuP(Hol@L@0j=t@4We5a&R`&Ob|))?xo|J66_xoEg5 zKU^>{f-CPTQm-HlOWD`LwW%>ISLe7N%wcyRyg2kbbs>NZ_YW@;i#73pS@0ppd+8n?qW_etlk~s29nR1G&E80d`5zCAGCw3@SR2{vo^M|sb%*BNxr;FX2 zFP@RvSD)$@znai=!s*{e?Yb0s7B}t@YsXiyP6Zm6FCKrmTJ_x9{P>ael53^ScdrDw zqW0=5*QGq=A!_=?EqO!Kl+9@Z*yCcP3!{rjIr-4Q3IYU6;8ZOxG3<|&qbR;{;h-WX zU7X_z=*qTbg1o6as!D*i-QO-8B5_X-#@>xYk^JpOGr0pFPK9l;ip(0Iv5o)Ez~H$?ikJx%ARt4pRtjrp&vnI|lPfz-Oz zDL!+yhqPhojlBhRx#Yk7lHXPs+>ARMTTA@A(cy(I?lvZ^U6Yhnag4rCOpG5&3VeO8 zj#ugfOTPf13dDhoeb$#zgSTR>H9pIL3F0pQwVw_eRS#+y`e?!m3=?0$X8U5InQWy( zh6SRNA>eiBIZcB{@0*(!;#?6R&TsgnI`%qpOGAy#d5e(DFaA}rWT5yxy7N`&YRkSV zZl9d&W8>DuzQc*1yM>A_=H{_GuIRZzgr3v&}uf4h8~;lIo$9=)R9Nki6W?xlg4 zl2a~sO2NsC3dGd|pDCBhRRsowB!Qf(yF;=G0}60N1!wr4Hx)-RT=(35dXs3f{HoiG z^ZhwnywCHsjlnnJ{whK6h;dx(dCR!|zlf^sF zv=xuVje*|W&~8?>SrL@nWiK(RR}AUXnbw+Xg$_PKXB3!OvdL4vy=MUwzWU_cjpL`9 z1qGrkg*O<6n$3&-MUQt}rkI2>jlo5C0LRp9p@yf!-RI7;5}aT!b3&Mzww`*-Ovu6Q zbF-G9&Hp-*g3L0`(wPjmz15&1PTGhH|Ajyw%dl{x##VRDp`w)|ZlJEusGLp{!<5~Z zDCIre=01}0wU0k*VfG{zMdRf;@|dwqR{{M5U1*h%v&#rPLx z#(*xeOq@ca(G)bbO>G%3&jcA`9Gn?4Y^Zo^}^O6O$D&qCxTBBBUr%_KY+tDDSfO{)gKmguaG;uU=9V ziOmPUQiX+~efh>#V&WUjc`dOXp}crgOMrJVRksH4!?kKCJQWYOz97==g6t1Jc}rG~ zzYwj+3|E#$?9Hel>Ms72e7!%&FV{GIlJ=G# z>!8yKt*DF*W#{RE6$~mhog!h5S&RKEEz~WYRpLR9-mO`p5Ty?8Xkx5n8;Wi>dZo2H z+jjcsb8?1YAD#C|l$*eED`or87Jh>6iuwIy3N6EXlYz2Z<5BgZP0pksk3P+`;87f@ zSh!VV+^mH@xi_?)mCa!~yxZ%8Epux-cf7MFf8&gFxN0ir8QTHCMn&JnZZR|h!=Em9 zumzV^`Z>uRa~r#48AI(}7&gUkS_ysFaD@oojlRFn0G4Q>@h*c0kL7ezdQMZsYj+Fq zNW5Ox%6Fg1iApZ|Rl|CsQI?-NDE&1q=scO zYS(Pn6ysK3Pg~srC(FW10|-V~$uEbdj_Lz6T7p!larAk$>=l{9g z!^1YG=}*XJSGBgj_du~6sLGGpGJk7{Np`-^u-5yQmaAd0#b`74(wF;uVnMcz`QNhmW2e0N(=&hf7XJ27Tu_s(0K6>F&WnPt!7^US`{;!+B2p|Q;cyy3R7l?LRu@czuA|*b1$^5hOm+%Ru zu?avi)|{5sbKmg6+%SGYm-q}1qOPmk(v;3aGum`NP|y#tB4w!DA?B7LNO|QfpwLaM z&Hs*Ai{He~v((;N+>kb@R_>~$=Nxu;jx%8P9@#&`jtmzhF^QvL`FTA%)E39ax-meT z3*Fg9tZ|+>zivu}H9kH_yA4F4LaN6j3s~|hB~^73+HzMm&gjoWU3zYdmjHEPr>#z3 zT1>vXf)XQ}o*(wuI+_2Av*LSQr?K^)1>!PFjdshhYJyhse#AYq%Q*|j{y0Xfxst0f z&;Ht9Vu@mk6tYa&9I#vI^x!AwyIZ)A!YjmRu2drqqj|BPuWDg~pG4QCQ9Xe;Ze(=V zy}J@dQq=boXQmb0lN&N}>F$UAm2A?CuRHj)&_uD(U1cR7*Y&Q2}_ z)v0B=uX5mMcpBgCG!_g29=cLjs5)?~>YJ-KiD-Rl6aKbmhyi*JgXCe;yh8H6s~Bmf z&}_-G9u`EC`s;Xx3jcB;&R)UV^D&owW>k*)b9qSLYI_SFQEvczMu)`s5%G=j-T3IU zJS#@x7g?(uTkoYx)A_LYrxoSaW$uAt0wI&ZsXRgmM4%gpp;>46d+AiO)2>M0Xc)+q zEA>rp!0{W0rFB|6gyfZ>zhI9#d<<|SZPNaf;lRF0P>b&xaf7-`loPsqrzhGlY4xE@ zV1kB_B=6Y4Sd0Y|BsyL3ZJt<$sRCSn;nvQ|}tB z*=SLLw!YJoSb#vt+#{{C$x~&Ppx2ux{SOkH5(f2m{eke)e$|=!)<6m_p=L~q)=Ml< zHr-Zvztbeqz&t8G?$8a>R&Bflw{XZ(0Wh8Dh=2I;;o0Q+VExWa05e~a9U29g2!tjv z@Rn*l@(ezUd-2}#T{8^GNvfTpX`07AB&UeBqz;JTh1UaFP0O%@4=(Ns#5C%IHwsz! zSb&?Ubnz$iSgp|YQcE|(6cn@i7ar_j-|~`X{umj7CzqK^-?Pc@$=Qc~hvhEhZo5DSzkIw!>cnkN%}uLD zP>xIVXgE&n8HfXkj&!OUvRA_RG4qQL2$^iH8JhfTD%Dmu+lx@tZnC|c75dR`gAt8- zm!f~P$Q>E^x2k|CzY{42Lr0~@X^Pv#xgr$Pi7;pe%r4XM*uLMiKpUapkI-&w{h3Yf+2cUnu@X5})qqcL)PRu+1KNv85g^cS>0}Z; zOqGFr^%s+rg1X^tHp{@rj-O^+_su{^vb*<`^IK)mBm%X6lC|cYMki0?#?s5MLR&rV z{BneBwuBS}?sWC|8joUy)epES8UrpKh5L4wa{qN-&4Mi7lj4zPL^;adf z98PvxI95KDzT6f{7KkjcNH)9iDSzHw3e0F-dnT-_2IQB0F4<#M^D@5!w0onr zrV+R}Tu9rzHj-su%1c<);`bb&NNB6b%WD|GtG-#(oD~dq#NUZjaTs*<`Ibh&jq|>v ztN(R)SKY;a8=VPx@7q)PmGnUz-$Vm~`LeQ7%^ziOROfCTKQW}A^;qkjsSLU$ZW!kZ zKTS(*ynQF~#^K!r1fMFy^=BLOl~U9R^XCFFrOvLbm8gC6W@Mz_>bQnf%3nsHgQ)$*a-WgUUgVhfde2~#qM`|M8UOMXzHr`2 zE5!FGf0$b|ggyGAR_@IvEWS6y>j~A*#N?7_PRZ5cH!jR%gg9U~Udk+RDA+b5gkGUm z-$)RrPMvhoR0P9nB&OE=SWz%R{W85HXwsF*i&LgN*rv9k7tJyH+z&l8&5cdI_$=d> zX|}IucU-XHP9Bu$PASA;`wM=ksH#vqHa?Vl6Fk7pH5@6}?{97&&(M9M)fol|%p?1( zX^G5g+V8W1Jc`2K|8%-R@`r4$CLxX7HtGPHoNpC-EUj{Evp>Azr z8R=`2*gozUVe1qi^mw*m!P`J=wk_K6LGGLfmOk7iYCqNGReSl?cRn^11N~^PzNvHl zXLW+F&iF#jT(ns=HP zE`=Rrc4zU4e>Bl96e&K-wO*x{i-F$s>12>cs&lA$X-B_{a?0X-_E)xthbWONx@nV} z!Sg*Byv#0Kj9`xuNIC9J$_^gqyiO|kS7m;-_UyWt``^NYl$4q9ztG}s_maNym|HO9 z|JWWOQC<}KZz%rt>_1X7`@h8p@1y-2PR`e5M;iPxVSr3U{hdF{a^c}Nv6?0oa9+Nt zU#~u9Uk@_BW$GG>e@u0ESo#K8E<>d zeM*8de+r|4{%+oAeebKa^vF+V77h7e>EX3NR>yEs3I3K*9qpGX0lnVNeL3E`Ky=$D z)HyY^)wAV6xptNxnr+k0Pg=CskO1sM%)im1A#6)>emqey>iT8gV`b4 z%qT_*PI@7*RK;wZ16eoJ#?uB05)kWWWHrmQ8?C=iy z)eCEd97Jtf-K@2|MsR(v1-sR88QbpA=O*(YHScTzY2*i!@0?O=$Yn8@-s3CjU2B;J zJ4*P_G1~FoofGd(7;dLI>FImvj*)t9+^y$V-`CNoc5LDJ1oLH--f#T4m5gT{<^NqbE zyj(jyswF*VuC@uiK7K{H1Fj0Xfe5h4l zW!>#3$E3!HQ#Y&3=oHw;whL)5X*)JR{L2a$%k1)R5#(tc3pUY|-#bl;vsngD;Z>jz z_0}8;K1?qM6?jVLzW}zp$|*snjbkl!0dTe_L5wZ1AbL4vA#$_34K8}E^{dXB7a&m4 z67E(P;Lh=SKI0xKp#OK_*hjmT@TD`M?5uG8Pcb`Ge{sO#kdNH0_;WZo()&Gzqzodek@4q}-O3xPa(b!`DBtIiZ@o(zrSnFhE*4eLC zp|o&=%}FjDj@gekYb99*B#`Pd!~H>^0miQiGHTIFOYivj>V__T+3e)C)YfZCgK1Ga zN!SK|uW1&}Exh1>uU8`bQTsj5D{vHRs_Me{O*u(>DDqPi_1x?=tKxVaC5YivS%9TQ z=A?hfHh@}HjiL=ut24k0sAu_zIvkl$j%g(+869PoDlewTGqUmiW z#TG=f{fLh#w_6*7HB5}>=HZztKDgQYqYi^gwcIo}m)<5&xv*?N%VlF_heMZAuzM9H z7J{hQN~`$Fh<7Wj($?162XH1T4BjDUV|%NPjOZz8qMwKVt1z#b;U&(%J^U+5c<*7Q z1WPln9RI~bK}FkPGsD`}u`BmXLc+3QiJ%3jig=r`LrJx#@f`kN#By}Xm>e2gJtcZn zv)em2hmrjRgxPYE?}{yg;?saGF(PNQ688|j&hNI+kSW)v|J#bX$b@0?4*9ARAhC#B zytH|6>vG(KP6S+#wvol!Ap`If*x{qX_(U_9h%FoYTM8hUy;m1 z!PjrDUAnBgLXgyxs#u)^2uh=#ISxwnG-{I;#u%9WtOEXwm|mRD6`tnuE__nq*#Wnl zMK(vvhM>&4p36anq1g_*eLaAZYxG0@gR-+L66LId7tz(ch5ueIzu8!%*uyGhrd|k` zhDvs9e;Z_K=IZl#XG|h6g0PibB5MPEvDmh=|`q)u$Q@mLe z`C4rV=)wQgED39$?q8jff19w9DabYCI z+(+tJ1xfpwc_~{BNWxJTATbwInNSH$zfkryuPCDg=8ydmG&y@r1#nqf?={cyt}L$I z5uF02GIh&#@3j(tn3t%{_8)4A_Fo#UIcORmqkm8Vzp9{TCwTWbtCZ$w?NeJ@RLMiM zwI*=iKdp8-rZ|<|?CrG?OVvw9Ldu)TJuzs@^*}k`@)4n$1jqs;XL8QTGcvz1iNAM9+)Nmxu~xh>MlwLbPp)>}X9PoxGAu+ZG|qCGYZDdXdv zl;=1z6EU!p~PvG*5VH>2Yk5s-?S~W$#QJ1V@(!qxKC( z`{adY!h|zqB>agYtscmmi|5iCkrr0Y;hu}|Fmt>>8mvoBlxgu0XC+%cdrqlb>%2Du z6Nc}mk&K2Q>SLNMtJw4<7ErBOyFVl)sUwKm-W5M(KPRh$r@ng94nx82Qw05&SZYIE z$mKb(!<{SqZ7lm1H>w%_83P^i9GD=_=d3lhrk7jXVR}+*V;LJ{o@oyUIg^1mJYc`y z+Gl+Oi2!8{&w0vWTiZwy2<#BAK|pgQddkrZ%nE-0CAD*^9vGoSNXoza?h-w5I-8Ob z6oR@LtSU6>D!W-iasQ8P%I^vW;BRnHVI86GA#% zY+{#qtfUj9sw`7me|=lKIe+FtgZba;dVNEzQYn1s3@1H*;%%6^TNXBGuBB&NZxGGa zO)q517yL3~DIB-||vT0Es*$XrDE?`q3hM++n8;$Sn_) zk^K*e-5RaIq=MRCeN%s_$eqYfxdcaHde@36y(6&I<-S4aDqoNd`U8ALSI>AIU2;R-8OoKp~p1)$v+?sL#2|lo$ ztBT@0Z(3sJIL2`ZNN^i5Ae{NJH(Y8E@?)m5;<_Tr)vtvX^m-}lnA1ZRvn!^?D7nK! zB9KQ3mNQ@Ws2b6T7nia>(B*@?>OlDiiTY_b_Q7`0JaibvE9LCo zk9Qqz6?t_y)F6uNJdUMx1kj>Eb_z=?J1b0cT6L?7OjTihEVZ(yMcCo=p6}6eQ%pnB z%--;{%w)rMQOYZw2|J%-)@RGbjTKPmGyXzmq11Qt0$XGxrvx8roicpq`cv(`jWS=n zL2t(R2HrVl)23CdtGH!!Zn%p04S_GKJsic}9+Z-~8e0=%YI@pk=m;wS;Yvi8Nnf?%?;5HCqmAk5z8f-7oA#NL@A8?Ys z^^aijj-JOG?8bl`!dk8qO6|S3>~O0?4~;Y8l^xMMHQ+7$N{uHwXb%i?VkZsyhQOCG zx9g7w0^Sk}A#d6(?S7T#$GDnJN^42U`OLLkWZkbf+YIMcXIq5M&ZdN|S3V?}nj=p; zQT1$vhVL6avmNYnBR_YsYS^;>9gEQUElCjVyw=fd~q-NtZ0;VIDBoi z-dwVA;0FS&-SUM@sRR#3tJ@aZF|yoSx+WF>2)&X&-5tJYQvP7pPPlD_rrbM`$CAyl zaefi&0m}MDnLUneyM5evaqto6;3h5mmWPH01|(+Nx%!0P($w=c`#&WAE)&qscBYZh(b3zvu3nCem-)NeoCTE6l)KJd%YtMEUbVGe_Dg`TjF+$% z^`N`q#aT1WFY{A(z`pL53(3H@;z<8Asv#TJV~OkLSdYZ%TmZ1%3R9=s;|kuYHXD>$ zjUp-82<>aPsOkVKh&=aDIyQjHXvXu~&}_*yBeUEzstk z)8RfVbI+j4l67PeU0aU~3j+h?W-2FB6LWnYU_x$^(|rbv-l`e zHwh1q5D~vA& z#%qzZaB!(;z4MA(>Vu_dAc|jYJ*_+g|K&7T8>pkmup0u@up6xCaLm*av?{chmc`L< z32|s){6-{AoR|4~({*JrJ_1(g%;Ty>kDHwq?B-cZ{(LUpsLfTX;lG&>i*7YB|MUYU z-zhN+_lHhRz4#2UL-ckM1>9vma^p@1l(TBcIOSG<+Z7jpU4cV`81a>V;k8|$i?Oj! zCX=tN=cZJFYNGGffd?JWGZ8wu`4zl`zvVs?-Sx zFw0V}w20Kf`sS+^$2;8LW?U-;8(&$EzqJ~TUMby)e{|B+%!dULu#gYjjVl=l=_;oj z8bF2yS`;R8nWF#O*#P8doz4d%F}3o0<7EdHnm5@bze^|m0Okh9(43nRq6`kbIVOaE zT$klqECkNNGxXegS_en{bBl|zHs2*J?d(o?JbJi$fkI}OTmB96ijICP36JP8T@nNr zd)v1dm$mOjSBQCnMfJ+|4fgoP=GofHz8}K7bHSyJP-Fl?4MB**b!gUM*-?QhJs)P| zCdM2-;Wymqn*ZPk%>Df^+g&i6L}{JY4`eiUh)VJ@quwUD3;RO)hX1G#*x*XtztS`aW*zLX zDVr;D@nNpLRUTYN=I|k{-2MF?toiS?(+g~;o_EO)*J9&QBX}@wMMmnK6RVx$LK04u zb2{Dcz7&O!p(JkU*=o?HVu_Y;-^*rt*@G+g?z)4{2VflNO{nT!Gnxv*IrN*CzL75N z(40TbcF0elB|x?uZhsLTw6kGNc=3MnDcqjdOy)-VjqsXv6Eq9jP}T2vZA5a=A1fj) zdOOvY6n9)ieZ-_KRv9cAI*8X_CEjOhSg`L|aRuY1>98a@vIg2b7XA>eAPm zevc|z8!B5x3zcN;*!`bZ@*-lrtA#BDn1P%qYnvTm5I z2j!#x6aX9o%ofk4YVcxh@*V8`1+ z$menP-TuxIX>ifQI#*wmb9$ytb7O2M_{0OE*=GtF4f~@;sLkSu()1qamoPt6hyuy= zBR-V9H_kY-jXihQhOHrb8fd{UbyBXZdiUzz`&aY1uRGRaejDYp==AG9>_rPts*QAz zlEyY>UKU16)&x{R--uB%{kba;00n29=xUJGo(L&EN~@e|u~yH4i^DW?I=XUv+Z6lQD{GsBM68$b1)cErwh zirdKIoes1r9``B^g9|HeHJ{qUz!9_q?z^MMFDkF1Qgj;D7pK*HcYnG{@sRL29qr~K zez|V|v;HBFYW_-<>+)g26eJQS>v;o6{Vx~U1(5!b(q$h3Qn~`t-R%I%86Ig8Fu#eG za`p4yb-8JVKb|3$Q*gHLGQs4}iM(Nc`~vV7#k;%0Gr$zU=IHyb{vtmiOn!{Nl=HEZ zX(LR2&;{Mg|3RXOlD54A=5Bj?^GB80eG#?qpUZNsMr{?5dXI$Y@hoEBLSP;?c?Do zm&?n--1JNn^TprTOwS6d9D(3X`s7TMQVWev&s84XV8!T-jg}E&7dSJ(JDUYfY|$mN zM&G33vCly<#ETdQ>raw zO5H{OVD5C`-5fsO-uVo=0#ga%m$XPSQvERpqhXc~%D4~E$M3RIe|rxXJ6A>FV#S6H zzR$O&d*x$eN@aZ~j#3v(yLzHKof&I&)%CI9vm5TrU*FTE3H?*yo>-4qnr8GNi>ttg z|6IAik$syH8hJdya;;CE7KgTvxv$@Z{cR5KVwRpiGjyVl%Sf#cwsX_08-sAThLA+A zX{Z&b$Uz?tm%&!3>ImpYKyZnGX$5p7r=!j_eV= zQ*Cu{$$FE8+ICd*|EQzIAqZ{mIUly#dC28^YswY_ZkQm~4|#M0!3lx+kN26Wru3SL z3GS6l%|=^OOnD~MK=vM0X3Kh0J6lO0DPQ(&LGgBYdjPZ=XGAx_hJ^D2fy8cIayXCrfd;Clf zE-)qvmy0w2*iSpHJ5{SfqZk1CO+9FX<1>_zUDYI1N2Z+qZ-`WdR-rCIPGNSLGGLXs zKSIO)GnQdnd{GrCjyu5f=6Tfmu7(ght3j+tzps23Qb^tJ`7H=S52?7tYdyG)hD)c{ zdjY){gRB%nWH|;bSUceMGYrb01uZyA00LDK!^eHG;d+Wv`1=6Z()5M)lTA5$?9{H) z6K*@U#~a>XUV^jB78jfAB@N3K%3`84p@NhF1RY^JX*`*JQxmC2kiO*)FDyQn=hlqP zb>9c(19JZr%;Y#j>?^ddL$*8)Ih~iwUgKl(-Tgs6Ix|18#s0c=)K9{--b?N(&A6Xu zQ@a?61?#UgbPxdp1S21p^^nyx+)zUPHv-;XRgXJ<8k{#N%=hCSip(djBh2ue{MZVz z@h~ZNSGb#3R0ol-mV%#wTC20$6*2_0fEd24Xm%I0a~{Y5U5hJbhTUU893gXku|SK^ zMw?#MY4|+B381#!Ecqu1bamkZP$bJm{bz(^+Ei8~=hbLbLU(a&Kx*?_4l6x}HY=TT z_(f-G?!LgO$fBmGzD7Nr+S*DkxczsX=o{ulAHN9jssy%**F@kR_WE5mvxtS~0G{Bb zG7%e>k5{`;!9MAuzGCxLbma!kNH{;YB5x3&^eG+D=Z1;vwm8c~pZyn7ITN{%5}oN& z0$nl(s*YHnx0D$s$;}=h5It4-0l$&^>)CQ}Orv&bZ*SN&YS{FG?Vtf%Jp#>xzeU_X zt>W)S=^BYs+@g<9g@kuMxfv&;F452HX$1Kc0}gC)s0y1RT4hJ&U5&0i4f6cO!W(O502hdtSjaWeDmH41ML%9 zG!L>YZa;Jh&a(Q8?qwrIWfW=r6DsgI~QJ}Brz++6M9 zhG*TvQf?zGt(Y3n0Hh^zEDeVw(Yn5(?VEx~{Ik@@{jS^v&rx=Cl?UvG*1B||{Q8Pi z5%D|7uUnqQY?0Ij@he9IU$u`xwbr7n=ps9pvquK#kz(QZyskeh_*C(R;>X`iqOp$Y z-g(kl2C^PqZJQBh3gM0E0yl(7?psS(j)lxmcRxK)R%Ht;sn&@0gu}qW%KIorPUr0q zO0S5wgJVYm)PG<=a>Tev=SEI&E%JV zTq0wS)6K#Jw1jviwVT0sKsc)=&P^Tv!0*%O%u{$aS14l~}>7WJsALQBPtqzWa zpi=%c3ZMnRfgXSf(6+9TICnPho_Dvyfchf@iH3&AeDlfTC}cyQxn}`6jM@y zWGQVifyV#E-CIUQ*}iSxgMbJKh;#`E2-4l9fOL0vcMgq8H%Pa1cehAMcb5o5hs4kW z@2Qv9Ki_q)XRYV{^vnmBYt3+;GiM#gvF*QY+ueQA;PI!60vMbte2PpoXAjFs&21!9 zgpfgQZ%Su7l1X!1w$j@lD#wu0a&(qll@0+)`~?x~RVV39i}UCdYP*{60W2@DEKHSd zU2LBaM0xPc0|uso$j&nVcr0CtVAn=8-|>BDxyTF$(CYtx5h)Grmf2ue1N`f|<*d%9 zzKMqOEOeb5BqBI0mjj!Q?6O1r-IvypAcO6Pg@S=z^7IJfgxY8*^0`}$Q;Ddy#?SE6 z!^ZZ-%-vT6#DpAIO$B&jY)7?jDsVBACen)aFBh45N@r%dPFMOHOH@}o*yfa89D#?- zsl}tGAG0;EcgtESDmbopYG5EniTri<2R!pMpbTKwPtZ)x829I@t zH~^Lrcrg7kbqV_xEE3EJUh^sO_qV_hJxnC%uF=0X>#sw)N#B5fwK)t81K)Ihy~@Vm zd;ZsTa!>r?Cy?qJT_C4Rit?G!uNQEi(RaToy3P9+x~YM4u~h`sv_9#N<5M3N_cS$@ z`QZbO@FaE~~Tnx3OC)=GEg%{wIiePGe!YW4#%!Uh^Nw{#=wd6#G5b z3PJwljP}^Poz14(FQdgx5#R*KFzf`eK~J`EZ#tlB7^9St9zW?i9(ZC>b?-OaWR}5X zZ4ce(h`$d=wh&r6M&Nxs?`<>x-yO28?H6{qqn4rmE5R|Kkd% z)0H?(<&~F$E)`mfOIA&qy@qL|j+}$B-ilwVk^jq7(#x9$M%!z*!pjZ*On~fC>9u)yIk>Mk zbl_0&4Z{?|AnxZ&p{q5V9yhulUWwXbY_IdMw4bG4r{g&?Th-WcxmFH=n&-BZU0LX# zDMqwVNY5=t0j==D%_qxLYY{-Uk~8)^fos`O+k5x!%7C1c0yohZb{ov%S($XCE}c^(DSlq_>CYKKD|FZI=3 z$d)rqH49m`3LfRXD$Ldr7eA^3I!Y!ASEVh%6mkNRus7i)F+w8cEcXEmRIY>hLUnR%_v5>9Hx zTo)rvwONzI6n8w_%GvE#j5;`bTwUVDZAt0WW#MbaMQJuOjRwb6)9>@sdN*2rCE+1i&+}!XnjHhje^QoX(`6Lp`WT2B{>#!F zOAmxsdhdm86gUn8AnmB3<-;mV8jwP2Imqd(nWXHER9&`gYchTR>MZ++gVymSQ1+1= zgj@GOa}IgR!y-q4RSGHtZfKb@Ee4liuMSq!;T@_0>FHnY)XhRiShncWmD;X(_yf*) zY{9i~=1!j%rCW3Dvxjz4E-7j**1m{xw!neyjrtYuVQgYgEtX&YkyND~z0^ z&G?v`-dA2Doe!+SQ#jjmwQ;70IJhF;Dv5_w1Nuy%j*55dj3Xg|+hcTX74IdDc3puR zW!ow6o_wJ5YAM}uGHP>tk%JG{+WEuZF4JNm+yizhRd6r>u=ErfW@T_D3${2vKIHCs zXt&YbujR|yS$~maZ{g;)jpvT8t1^F8<7pn_Z?03_B@uCxLD;-ozV9WXIo5Os5H)+3 zkDUNZBHAzCq$gj5{LBh@Ux|rp7T`X$fBx{-0$Qt}`lUzC&;fk??DP35^S=zZSoK_0 z^756@;b`xiU?$kqX+E1l^*RgaVZ-fKDB7Gqlo`mO!CQlbtTtsD`zsHTI?6VE%Ih_) zSseC;S#DWiVQ~rALT^|{r71x~ft73Ia0OL>ju0+mlGxTNv)GLFk5pFw*l9n&F517v z#TR}PmaF(;v^HADRuuzHj3jlbpNo;lDB|lx?3o8e`GHv+*LB7Rk%D}f%VoY<)viL{ zUS-aQ>|+AitAe_K~%F*8R`S_fh9xMm1?rO$D@a2%0~f%?^GY!@WNPSDHXKWSb->apSiT)OU4V*Yh> zw2ns2tZGyq7#2I(01?HA7CCYXIa&?X*U(Zc)#$gGOKAR}*r_?f>4t0E`wrS8FTF4Q zBLOVL*izlDrGHeWqp=vJRfd{&(>q+36|y@xYwqc3D?-Si6QKQPh^#Nq0DyBC%X09w z!e*a(R|I@F5GeVr?FZ`OVVWdV#8j?Oc>r!`Ux$Fd{yo=zz_r9FmfeO+MU zv^K>SyM>eq(tuhu`QT5JeM1Dh;!@|2TNRIOqF%g7;ha*ERFB^2&D9osDQeZgxWa32 z@(6Bikc=^)W6Yr3@7wec6E4?aZT zf}#hUeDlqO87bmdeu@0rs}-9c{#&V-Rb#bE5T0mxW!lf`cY=c9@(85eIKM2N(uJn{ zRsuGMYECJ5yRb?l#-)6fJL{v|WJ8H)Q0l2Ts;-I*+SG5RiuOWsR|2^SI=|&!)GDf* zE;!w}i8%A@>SHrAZUc|?8E9`wvAT=h_eQ1`!qMbQov1{1^i5i7RjB=&`T|l=b6Akv zphJq`mdV`btJ*cxsuT7(>N>OQ>eAr?4SK9rNG}|JgFA0tNaFX0Z?Xen4vT=7=7z^aU{SP z=HvsqV$%wKGey$EV$o9TzVK#Bmw8D{_Ge2GBiLjxMc!d~3h}|=j!b-y>Lk5aeULt>{rtBkDJ!%@ zGgEwXX_Z;zLQke|*9%2L(96BXOdn|7N}9ytE_vm5nn*sh=VKmzeg)^7JZJFAd9MdV=$WY(AG`z&D zr$Vmw2c^)B)6sfHT0WrlA~XwXB6q6z0pIz;XPsbKBWiFKQjt-IpIqL?Dss5cyI7os zzS>}4{ILM^`PJdb!+!NS+E80b;7H^lmLFX`@z<~N^%13|5R2!DEXW^E^u+$E&y4FY zz+ZmH#~!JoU+O!hS|Mmy0~&glsnoSw*cslH*cZ0pjh8JxDbCu)6vKSM{xG#*?CCvt z?q0+ofs%z={SJ1$zh%m*GPJ~hx^jV&<-su2zQRjL9h8kIg7$?1=xitiOY-=XZLuFo zrV{^T#bZ>g)4WR>$g|({$@yITvA6r9AJ|1F=fUK^<7Ia zWOmN&uh>a!%m6JZqYggxAb!8IK$5_*=A%P)SFcj-ZO4XNdDZ&FW{m7X8pA^dAa-(! z(}&9IwZcT88B*{&Sq<2we?IeDLu>WaxC>S_WKg8F2+$BFg2(X~_QEr7AtHNdyb9wY z(1&bqtRjnr!KiX9_Pg42uA|X{9mVS4D2iXs0@CUbMv`cF&dkBf_mK#TcGg>tp$Zy^ z2=5;~Lu)}XxUFSJ1o`MMT%IWJQ@<;lAl5JW{F>|GkWK^z?uHBE)gX6*zIRB$#ZN4{IhR9v%~jH4;kCq%KWqpetT6ub4qCOqWmRX z>Z2l#CmHy2YA!f`2@f#>mkLAikuAGfUqNS6gHUa;D{m7<@B^&1(1p) zabz)D1*QcSTjsxnGxn8=`dKIS_~I`*$xIFYJ-P8(Zx`*E)8jBd!MxiONI2LP?P*C%!ht-*PnpaoZ=5)Etb{b?Dsm?7GkqSre~@AO`?nyDbHI( z%pZMTaSuG_1YfWNC-!Y27Mw8q1C3TWiDF{9&hP#x>9AfWPRO0eI{&hXr# zw*0kk`dDrjb)nj$_tgd><39t%_mf_;C;|3_qY^znid9a$fP2-V!;0^kplWW&#>+UG zFodF2t7|`J4BQY9hexZD)G%G0O|X`;$;5PizZl5ih7QLLYFV8?cS4;Vt}@CCUo0gl zsO$x=wN}Q3IyJS=r=9VZ3c!;}{4_ny5J3~9i0Id zeu2+Ye}fVmBr|^>X~2yC^s)cFMtrci%oYS8Ym}1+~Bjk@GJ`Cswtd8V*^E zlp%nOS1Zo5X5w%zZ-tWn(%E4=>`eHsOndQZjFp$MH5C;V_gHMO)ziTC>%w#0n4 zYH|G3oNlW-5%gunv%HMYpFlu`Gs zEK|`t8R}XC1P40pV@uANbMBRwe8SoY zM1yleKBRPnCjW|>)M{Ri$5}ScGkw@Q3tc4tMQHhX_KEA`18<@0%$NHZsomHEVU_AF zUPt)NbQ_e85E!nRM#hoDlQ~4Agyp&RJEs>16&*s6(!vml!wPN6E#%_FIk?@z(% zOCF$Nhi1jfywJtM!=;Uxa9w4(pH+}QTWMOALH9Of*E_GR^*Zozl&XEkY1N!{VB^xr zMPTp6vnnp; zfqqD({F}f@r4RF|@?#JhLQijD8P`SPyeTnP9e-I@zum8y1a5iuE5YTuHS(M_4%6YT6zU5Ei3&3l-!Z0`mE(~6@pFPwHaVXt9^mi- zWkI;cSin3_klz_*Cp&jbf@-z-1Fx3ZH8BW=$K8sq-%-zzFvmv-xv)7H+BMwE3>W?( zc(P*K=yANMk6*mV*=ugu`yIz?Ywf7~321u!3QL5KtO$LJ)%Z61LI@Rd@)c=G-=aAK zo4Vm);E$e)&NdWq*7am)t*MPT?s0ahGXgnOcxSy@X%o?pA#q zle}T;KIVDdKo(_6un0|9*1qH|;d4tuZd^Stb#i5!_~yBPKb~OzX63L_DVR zQY1j-v`!EJwnMO+?mwpu9$(TuS`1M+JR!ZaIc>cB8S|%9>azW!X$?-JC>QMe6lPzG z#bIQ7fEiZB^q?N(paM;UlJJ#^x2gY91b9Z@#_U;RlFq`c@hRxPblPwkC zi^J52ynwZCK|G^P_L+vWxF2Y!=ofmfX6!3B$Ae#$T(VUqb2G1R^u%XSz)4?PMW5p2 z*Mo6YZ=Xps`hrDPwB(*fY9RU;Z8>52>_ zDf&j`8JCgh-N^wr!&nQq5W8=%FyW)`JeeEV%IbcMKqn|sXi*H!1Nn* zJQ~Wv9ZPKXlw*LYS6;`n&5%drI#WU)Xb!d7&F3yINg{ms;d6&E7i|1zIY$R<%R~>! z$+M!>Oj0SLbB%_oGF>eT&iJI?*$2sqet&BnE6$Fm)gM8t-=5Dyd`pPMG8|gCaF$8j zx{Cz#?|ZE=0PMy?R+gJuW0lg=t(vLdu}uj&76;uI{MO5TTupa6%BbAxOw85OW%SaC zS8Ac(`2K@+R^{wZ)~JmF+Z$ZadleGRyYyeGGbwIEwgiAD^o>2rdLqFvkD23Qj=yM? zl9r6w3HdNCMp)qZwMa~DqalLIKZ_81%HX^D@U6K{atQe9TxH{y1PQ)ND#xFj zKkUKjopDi)5Dg7^&nBcEh?&;b?#8`>W*Tc=s&tqcb*e|f3y*wYj;@Ng2ajxVM>%JC zSK?9WrT&t)Bd-AV_UKP$J1J$eZnV0!a zmg^%y@2^M+-zu6xe%Ocvp}-B)m{b6%U*Tcm3x1i^aheCedU9NJt|gtGmZc5xMl)G4F?l;P=HgFbTDa%-9%p{lPN zL5zTy7PcX5fdb%6ABdVRlg@8oo0@d#(s6zbE{CHvI0k;M+h|+$pi*XEz z?tmXbl(7IJxI+pVd!%5uv1Q2%F~eQrvoSmpixw7o4Qk@Y+Fa@{hJKic2-qtN)8b6E zefmATZdiNKLH{vGr!VNnRluVTAM{0*++0`V;!RZ2#PG%nV%HXy9Oyf%vGu~q90Y{T zHs9E(plB)>7r>$`DLq2Nim&^wy@kVy8}0i6-)-|?^pvzL=(F^apDfXVu?KruG-prR z6I0ANc9{$4Nlx0>(~^g-Ag3<~KwTKT2SC`fO!_V`}EF>h1S zSbvqI6mKKhq=?JYGBNJ2x>c8s9( z<29qKZ^}{WB%?o$Q)%J0jcGzWvGjwhhDit?+V`AHlB=Ci|Lw)%8zuXE6@3-MY3h*} zw5RiQ_^$b?>2KW{k?Xh+@PjG>C#n@`3s$m74_!hoZnfUwy0??^aH1sKQjXvd;de_epPg~Mo zS^a6Tr^dDk?MOpFuU|_oqkDhHggORRbGeB+W`*+gxqe+Lm2N|8GyoqO83%zd9?8PX z!p-~W#q7wJBa@V?mCUTdodiO=l>bxr zNS&ayQnz?6#w0a;cA>gv)lzh%9NH!9Ejl*&TgTsz1Yk8v7q63Fg=1Vr_bXzfA%B;4nU zJLAQ~Oa5ZaPuPvGRe$M@v(FO(VnK)?YzD*cK?YFm*L?}DB^=-RvHzMk7InK?WN4YM zX>JS($}x?~4*1Z!*9_lB{niP0*Y<{=28AP292`ipJ!L>iL z3Vf?Fb@G~l+s2+8{7ACf8I_E8qp}dY`6(+iW{7Xa_0b_ZTv$#v8qVS|TAJd*zKx&t zRqBpG%l1)9f1oM-G+}^TL}{%*`M5*#vr;Qd^~c{5I69R22D^Q}jwEboUC;&XuKFC@ zJlFMkL&DgOwBV1VpBG5SrOO;2Wx+zc=vX{9Gya87wV%blNxD~bt{xULva2?gY^n4eN9 zdA=9nKe8}(eZ8-L=<345le9@h{OG^tzwvOgq5|Qgaw4GVyHBq=Jq|uG7x#i@)R%U+ z@Q$4M4N13t&sIF^pnzHu2syXWy{8G$<<^r^R&ciR>Qhy8Nmr*{+%23$p-7%9TrvE# z2mf{s^x&WDPiPv*O3j=+@cm7pdYIR5=1DN2r0@M%6AY zGs`NKZi%}Bto!a}Z0|%0a&=D?ET^QzBeG)qNSh4-^d8$@fv47)wQ}X2dV-xnMFlyJ z!Pb7!pd7a&6R-NmQ#21=P7H}j{#!cNk*Tn*!z%{)dHFuD_cx0>wd-!b+T z-$v?Gzd{93U7 zAZ)3-JC|;B+Z_gj{M!Tv=9=lkr^X5LK$Ly^kF!#uf@=CD^F>_v@m0qqy_SoD)D4gP zi?xhbme?6^R+k7Kz~?w2xu;|L>VS9Q%gaOM=18;x?wOZi zuQ8;b)DH71w!{4MdHP9Fo;ZMARIJ8N1?XOAumKO_=#-T1hF6HS83$D3Z|-sw&f;d` zl+)Y5y&T1Tg{b-7TE4lT5~M9bj5J0+@&l#n#{$$(3wuTmJ-N-~_M-FL{h7A=YEBV( zgnXahAq+VgtNB@?)3ZB`J09k;cYBU3Lasw8DYrFXF7P;7OzvZa~nQ?g1XMbO|e>`Xz1bNqyX`RAN(Q@Z3 zME7tKZ5WkwFmLw;I9j%JFG7g=&)UpI*Qd)#WzA@AMXo+{`QP6bWZ@mN2>8wKfxQo+ ztbt3+DJ>zM**)t)IBGknH(klPUDHdE=OxNlruxPd{Cc0}S{@CWg>+lw(-mKMcRijn zi_RXXVWJYarjV46xJ+#@sJw$9jk|N;+|KZxD_!ct*`GABY;aM|gi*WkL#jDCbWrOUUQ32_X> z%eJ-HI>nI&0+HIx2cx;VS;0>WQa?VBT~`$22_iSa0#+V)mC|>3bhGZDUMNJQVGCzL z3{DT7cVZ&E{EWY!X(ss@$G}9-*z>coym}hmLnUb)o#XQo^jLM$EJYx@W}~^nIQBEjs6U=n8txA5&ZNx^akKf8bKf^INkqPEw2|dfR4mwC4D& zH~sjX=;QOIF~hoy(InZe>yOy!G!U^iap#S79fAM3bU=UI&5r zliC>7*@Xl#B`d_ zuNkU9)afffbLU?opxk$u>i%89J?r*4{_l~}Dquf*Twh82KcnJgNpuVU?l%Ffv|t?T ze-Ev{bc?+w+!%jy1b=OX_tEG-09Qc1iE?N>Q^@o74?B{X*`KyO)ywV}By4`ZJ<#D$ zb&{|NS^CJGXlgO1?qpxTYl3I&v31=(CvoxUsQSJCcw4&m$cL&AQ*H!{SIP&Nwc8$% zx?miT#G4%8-kUsEuJm)ni1s6M0S&Q^uDl|}w#6x;z}skR*AoJh5WiyP^Hk_e-iYjf z`bn_!GS$DffkXYbu;cQo^rzgI>!oc?e!av@Z131f8S02cyIBQOO-ewJ541n$?s3i2 zjV*$z+`NkGLWuU=V+3=Q%j{9oiQ`HBk_E%24So%d(#W!gy=e%M_ifO213voUhj{yw z-4d-rHk|&*7@5VFbvL(#x%`<11lvc(vt0XL<#OL`XErNO{K8Dr3AlQvuQs+6P;rIC zrk+~QcAM)KBY&BMPiTwSzMjBrl_I$iZq?E`TEyUCzKtzZOXOT|;k7!?aRy(w8h4qM z*HFt&u~TF?|Mc_{4f(V7y;$~#DyTGC?)LoZHjsSp%=|ukPiTjvf%+^XHiPIQJ}`gh zT+Go*Qd^$Nkia}O5kz4~DRY*Kf41gRN@=3`Y-u)kNi?jnf*U$bwC)3Xe;P3`u^TPw zwKByvuH2x{8GlSDugoblTUfTZdw=s*Rox+3*!6rgnj&~*muD%R?o?OO@@yfiHBUY? z(|OX^*s#7x=)wu=#Z=yE@^GNK)P3*kI;fXnz>T%=`u-4JcAM3TP0ib#?M4>Om@n=y z%*o5mc6M>#AOoFd-^@0;tO2ht#mx*qD2D)fgxSTqQ!Y|N`_BLVOPjd1@8lrDYLvUl zo1MfL(kN)IH*EI&R}<2e+pCX2Z=Q*l-7Xrar4YS7MgVbGhU*WgQ{yEjU~hM5Twh41 zb=VFeYyUd59D5@heCE>S4bMcxEDtk$1`LE=zHO5r8CoW=^(aN$70|J0$H3^`=qEQes8^w)0G@$dh*8^0ovRhybFD4)Ml zI7kz8dld@W!&7zu`c`#+<(y{PE*^V@|9zqMBSD!eilXX(9(%Hp%fhRCEHm1E>tO-8i{aX-I@cVc? zdv%c(=RxxC+Et$8HBLOK$4F9Ab(!~Kfpj8n*?>M4d;GS0l(LL2tQpYij!wW?Cw7zk zza$@w%7s>X#b((M?b`X^{4dR&S!TkjmbC`uzQsohru`}}8hd3cbRG=7+wg@HxkQC;55 znL-16Rjja5)v~jO$>a}B-;UL`U^8g5)tQ^|$>=jzU|r}gZ`=J&+m6{ce3mI~a5BEu zCcX2A8IWWI${}NwIqNc^xh^Q36n~>E+q>i1i$orXslM%dmt^*$Vl!x!vsVJq# zGjgABXmcK8Ubd|_kiacpITvmRcWpP;7RwJ{G;8Ctd1m^bW%^DA&GqJiHd{cgQs~%k zQ)e*`0)z*X&^x|F4DCp#r-v+6HV!K}0V1BJt_$jyEepi|(hk#btN*m^QDDm>08KjO z`V`baOT-Jg%7U$==1X>Y=wO!x`ph;zckyDKYwzthLYv`{0(^3)dsIBwlhqj++7^Cr zqeZmB2Lu1LELkolfOU-C#I-1|w~Z~HYA43&$SFMaW*ECWMzyC6Zc~ktnOIQ%nYjh+Ux1rp*fHA$uSiNbu*BH(m$| zg*PKDag6_RBY^cwH=Kznf_)Hp@!~=*dqT8C{@rQK0zoIwV8U;GXxMNlsI!vdmx`z9 z6C`W8?{m|ZZX7k@LaQ@-r8|UsgjsnVd>$~Jp;@qVZB#H}VmJHm;^9`&i;OG7&YATufO8}cgX3>*7-)FfI**{ur)6_Ogz?UB; zxiT4kO+oHTowEtOW|B^4rre5;eP%?v!plS}>sMO+A>>4+EPC!hWXsNmw^qi|^36sH zhw~5TiiIIsmJF2b7BtONd&(Y_X5}ekC#T2(71)5u!saw#997ET%?C?f96>Vfxa7Ty zVDwNP)5~P^6gknMcYgEUO&rpno3+jEfG+OKy!qQbBc4FnuvB$WP`Oc`&BBZRh!@0+LIF z&>WOC^zmiQjjjPzSA(SICJfBi2m8=on6DMhnwFc8JUAO|0@))#_TXrgW&LYz6{oi9 zLvgfDKZL#}4-TXeiK5YxBGcS7p}cn`4VCk&V9m0@E+d*}XOm}02!UyM%2&be z`_9gE!o!jRGt@2>VcGAUGHTk%gbbge2@hY%>_rZAj%pDsy37fc~JqzFq=LFKiq*Rb_ zQo}5>VLT3eabZQzDE)aOmzjipP zjK9}XRqcEi;N94FS%0+MXPVf{hcni zLO=%m8BV96-v?OFSXw-gw6H%)Xe(9yrQg2uhaQRQ(>IV&Vu za?SF^ea^-12Qk^-*WM5`D=3mb0*yb_^>DNI)N2-oVo2WzTSKw6uN#smIy~(zZ$cC8 zh9%MmqwQo`4W4DR<#I8dddyEOd@g+TCGPR(d2{x5rYt&EMMh(htd5!u11eBVn=Vo- zKWW(xfI&rS+(P^hE=ui!;a) zdnb z#ScZ4oA&D`!7B_KtlhsAn-M@Mq4LU2Fx=6?Cn3sjA;oCu?B1XikN%y@mm#RVo`GcgBkl1QS36( zhipq}5|V!-JJ%eD8HE2wvXfcJ74iQb>@;VyEDUb@n9F{|F2TPIgQZNyZ9h%qoqA1o z;l+?zXtAvN?^xw7$ZRWq& zCwNX-xz;F1ci!$nGX|&iHZ7|?-;z)m0rX$0@9czsTl`Snd1~4W-l~Vi$T?5S9EdU( z*Iq7+aAYz*ydO2Z+bfe#>j?-VnO%8*cgo|xs*t<1Y<*Ujyn&vD-eDXc;MU^5a`h&9 zUrHJ!T32!GK--}%Z?p86ClLGnzlq=F6;IX(FW!}6x$IVc+Tc_DGey6gY;WrSOsFT^ z+tA_U2o5^Dl)7o;W%FM*+Q-wx?yJ0U8?1S6h7%wc)}shBpZe*GykSYtHK4)rh3vgbT zw?3$-TBqDTD~WhMyShhO&26h|rAPuAKlnagggobrzDPGF;ykI>S8x^&(3ySdO_#nH zx@H{45`XcP)A3_xr>CQ@WIf8(;ghlrNe_mmRP|PQ@4tQwX@86bQecdGo7r4LQAiK7(bed{H0d_qC{unE$qQn`9JjN z;v&}>hfv?SF#q;M1}jhFv8I~0Vu8XSD`zF=5yVExf0dkj`ZkqwQzM%ETr)d|@7au? z7W#)%rp3i#FX5CD61~+5CL}&w%n17Mn8tZ*eAof{%eW$gMBf~IJ#k9?n5w+QRuPut zkE3X$=lwN0o8eW2*1DOYl~%FDm*lKZ6#V6%KD6t=eW{NtPMHB2e47V5R!O~41__^x z1-l4!c%M{MeE6Ui=Lp=k^7%OLK+h1nj%qNiVmb?)J)NRJAP&tXw*5Z&i>g{Wv|dXV z7D=&3aMiRFJljI)IqRWA2NsnbwQd|`H`(!1k7RDwq&g@+YW$pd$WV*8G^8O@7u1U7 zuv*~X-)4VvC%N-7p>|ZbMCsGm^ePAw^sSde`7L-y22@8>8N(^ZJ@GG;qvOxJ)I+oS zP-q7G_k$>)ZvS56Q_We46yK-p11{?!ApdyUP5meRdyVu{$5=^h0F*Uj@)hj6qaonZ z7!a6dPLSlQEnE7&lNVy2#K zg8Dv*?5<(_R0M1vpZ}%S(C8DXJJL`*8Xd7_=x`ptypr2gz1T0f7Ow@I0zVta`I4YV zXAIl~QDq?>`vE{Z`_o2qy#!irvigM~f3tSicOYimV{a~1A*UMZ_W>I(mUF2zJ+`aN0ahNJ?Ya07*VtS<32- z3qGRk)LXFxVd7}%B`p@MKhLY33XcB+SW^M~Z?I&h&Y!wJfn1MZX$-(>@>HU0q?s2l+wk&NZubwWFPG_=sbaL?I?W$0&{lX|` z1_lOp9o5z61lho|&)&)ebwh(mIQ+~*^D`UQ9S+z7fwo&x?ILq{gtEW*!@_lx6djI9 z`*&|I*8OOkx@whuX_me-xz-P&T%J$1{o)J=qw%KF5|J7*gr}~>!hZ9h&llTfeD=%) zSdL}|S;#qA#>HPJNd+@piP{UP7YrezMDJF|VyU7;ZDeU{&uiSZ{*7v;O^T5E8QCVIRkrf>eZmi(b`wNyBeqfYNALn{Gq*tp3 zV2zU>556a-rv=3PLL*~=ziOU5CbfiYef+_hI^(7mdh#|$jZyu|q7Lal78W`XvoRivdY`(3{U5K_~4-tI2nKjwdc2l9uc^3m9kj#;xHgkRY68wS&mr?_qU)e-lp4Ulh#Fs#VE=v$@92Hu zh3qhezRcVG_$Z8ZOl~Dfl4bft!ZByt#piVH__da|3IB5ele5oQw_EI$-|x3YI<(oW z1~E>yu~f=^9-a7+@(47J2_a>2V=m;F;1n-#nTmS$Bn@nks1_rUT`TIv3MdQepKq5? zsnpu`jhAhA&U2K@GNB0nwR^YUw4s1QC%mxV6VA*{7QJg4!6C1KL7@jq?DvAkGf|^# z%J(1R6r!fLdLQ3(QtG;}(2=Kk3-InmF5CM6Vg|B6bG zzKIi%`kRIVe&+pW0PFlys{;l;SA_qOrM%i071?c+_3=HfkK#$HHmFrwh%2y6jUDB2 zhhaQdmHrp8ennD6^Z}SLm<5kcSM|DI$U0r{W|6^Z8xiH7=Y!@*c61R%<`V*lj zNsf>l8Wo$AW$3R>)l^|;bkN8#ST?N4snlCauU^~w8Zpn!500)%|H$Nl?Jent9THIg zRlo;+h=Mc;FNW%WL4Y9O;XQ|d_DjQ3W#iJ?60gZ^FYs1zpji&z;J##`3kQ~gS?tA^ z>0`sewEAq-Yjpi^polC)LChGfDhkovQRegClHF^KBOZEI%M*Gbefw3B+8dqv_LyH~ z25MC4>x+Gc+IZsr0dF;i zL7MNM{>iht7NKVZ|7czXYy|hvtZ&33ktdD0T~85#aT9rI+0-#pPCXMg>QCBs?l!>y z!iRRHP(J95*|mJ0+8vDQtZ;BpXyKYW9D=Z9MM_?4J?jA|2M}oID3AGUUvZz+Y1E8q zujQy5{xRQL0l%@6OE22?WWqM1l1l@)q%|Te-X7Tta@R90&8s_bbb?2rw?F}U&tiQo zL}#|G!}$6&PpwpJoaXpqg49LMWd330IU2E~@`xYnrHw)58It^L{VD(N8}0W$;Czwo zY>RhqUDd=Gh(Q?cDw6a`d^DkHVlB<94cj z9u>O+2*fT6zZ~o&!I1Mj;8%=x$uB%J&V!MGGKcKh=b#R+t8!oSHH^&_lhv*Pu}h1T z=fyYy+E0fT?_v^KW(iAH7hTfS{=-|1>UTpnSMS?){xBkMPEH$~;^r z&Oh(9m6hX5$baG$#XGnl&^SA+5hX(o0i?)0*3C@sEUm5t2kN6ENTE6;z#XYtLCfa_ zk+qc7eGQ`0;>=+kin`e2vrcNKRWXZLW3NO_8&3FUJ?e|!aLxt~dcB6j%T}#I33{&g zI!@!&-iz%Q5BWvYPe3;j@?$Fw{Fr8Rv{GN0>kO($VzYDh9A{_NS^ms-e*OIFTLBEj zI`DEsQbl|5mb+dt3|uIgdK0L)*L>Duhu;tM>*l-rJcL4mbhFn&xz|^d#jUG8M|6O_pg%J@=^6@F`cAd4U%XnzK zO$OAT`|N6CCNZ>2=$W<_?d9$avM3p1sMTnst1|t-tyy>j^^nt;-Oa48N}W=#E|0pA z@oSu7TKcxWQvM@B>3_MDqks`hizw_7i0pDplHG+g_7rW^_-wT$p1)(4?kvQ2zX+3n zMZZAKEVWY#GjKd&{6Yw)JmtV*!F&2oAyBT>}AvyIX+Z7F;%#kN`mg!QI{6 zCAho0ySu;5Imx;A{_k5eZ`FL6sha*q6}|WB-m6!y=VwoAz0G;F!_t&U>ZNdxaR)NJ zX5)_C*nolTyHjVEy3xLkPziH(l14 zeKLlQ8aI~Hj|@lPVi6@sJRNxrqZ+^y#YCM*N(ZGxbR)@!np`+a>YPxo!YEPMHL$L zvpzpI_~2G2PMn<=(RrkCZr>@oey-F@-z?dfJ;_7qCMP&hUB-!RO7z4nY=S(Wg7zH+ zpooV7rQB>BqGUS7C+~ktw|X-BdtN;3Ec}0reKleJclPxaeH}SyJAmeJlky(L99(o5 ze2G(NMG>Waf9M;05HVM7Q9dbaeAHK`;0N5LaxeYbtpS>s9pZ~uD0d|lM##Yc?Z_p@ zDaN>qAP^GA!r0j9!bxeluz~#d^gkCL)xA>6WSgSv%;Au>Q@_K1(HU zm#s*_B){d%FGAl_7>Sp0$KxnX{8xS;%N(ue>XcQ5vlmZ z?W#C2q3qCp@@xxy>)5xSo=Rh#IUpmH$PNE~J&S_o>J@ zeNo*Rv`y$YkBmJav8>ks%V;-HjC*}4R-Boh2~w+q0d{PQ_R1d>_iH?h*)$pr7NCG8 zuj@!ijD{Mj#_UXe&rSs+z)%tNa>+vXlQ^K(n4*&iIJv^hT#p>RXgp(?=sSmR1qz9|$VuHIW+Z)}g>RV%ls&pj1M*8GT1d4-SWI&98Ao+xG%AFLg= z`p#(Sc>dPtUFW{(6&gGiD1_13$~a%+0I{F9iN%C5i3vP*#TvdU#>4)UbM0fa1l?Fz znB1K|gTYR+`jg@85vxydyz1IY=WxG$DZodS{N^&Wn=5|(3DBZ0_!iT_fI$DEb`N&# zC!&%+Sd{!Yl@soVVzu;0b-}=Ar~P|p>*f0YGpPS>U{Fo(m3X_|sQ{Qm;jB7B{a_|d z3-=24GAU|yz2TWT9`afrj{zJM`KpT? zjX2?896LJtrIXcSvwcguJf3z;EfDjteAJVq-9>*Qrz`8QV%KKC&Zt_02YaWu$KD_${-v%XvLq}k6ahw#l7bW;l z%3M=c)8S)c!qU+g%KX_TxH46ovrQ!cw|jxl_4Fg@FXzjMy=q+M2rZ}dKKtE|!pVV` zhs=xaBgBOPy&PJ#EGlHT=Y>S6ooysgyS(uaeG7@@9{OgB1ywJM^NaewKHFSA-jfb# zB=;#-X|SF=tk$K8_GamEs*vT~RW*l34xm&$eCY=`(G{J!^~nbMdmGG}HU8RB7F3)b;-w1uflCMj?k~-l z_Bx-N7M1A-4ptua{dZ|`a3nJ{?P-^p#=5~ldr+>HsY-zC~f=l2kAS6;?J_D%@4 zYbD2PK^VyH<_e_Rt02Ne>_r^F+`t=}f+RU8)9LtQ4xp87Dit+V@|tDr%*&#R>j^J9 zp;39aF(T#Itvp14MHSP(&^M4ESqsQA)=1;;$g z-AvgPK`FO)dE}y$fe$@t!Yge*x#IyeG`NZA_&y{3629cViADLCL;*`z|GR;^T=t3@Zi=fXXS4I*&A}- z{(&0_G%5G{T@+dmE)`I|TOq>6dWH4Zap*PG)g-5f;jmP!00uN@w)5-f$rlq}3J=*l z?w%k)KV})D3OENJw&Z6|O>m(+R(u|gE_oglsE*!T1WVI4RSt%cQbx0b)mhdlJnp5o z1&>ZMG#TOEwDH8I8zi~*reOF}0e`VYgqp@Ni&Cf(#t@|{Q$UHzX(^ib24%Mh_eqGVERN(5|uSHoP@NN{D;8uJP z(INZoD*wSitC8NT7~PmR6FXAQSIN9Gi|ao+Gc9kx*$-!5_!1f*O-$}TZ26cq%3dF| z^D@TyW=<7AiM`E+1$mF1JxG&JB{A1qTa~*)(kEH0lr4_bp1{z?)v^aP>#|trZ2Yt? zFkGXskh6(-wVy9f6DjhfAaCP2qlAf{AVbTeFo-xdDV7tVHXp*?2BhNVeb9+(z%gf5 zGgt}XZk4pjRNUd^f50{%RZ*3*_}RLt&KLk9hHK{FD6^IgjyDy6gh>?Qtkrz&+ms$= zfaALjGe#m0c!2u!qHyUK)fx5}oa@+C@rwGQ${A^B- zM`q3~c#Q797b4LFye71E1A=OwYuJY9b?#)gx3V@P;odp7Fvu+&pO z$A=3Gg!1Un3t@v$zx6#CCK*!InUTUpW(rdRlGDSOi`yB9u2aFEW6CS;&D~2@R?(({ zTBpe&_MtZqE0ppI<4_JTpwd~P`ncp0U=}fMe+J3FLp_gZKYyL+ALp_bNiWLCdt}M# zS-xPhg;O=QVcuAKNurELR!}LSV<544Ym-sz*E=_2*-XtU?s>hr$Z{O5wJT|FJ%pPF z@?oXLY&3;%TM(fgT~##ff{UGXedqvUv!E~~5X{Gc~fm=hl?Pl+XvrD7k_?$o@7 z-6VAx-5F@`bN7w+H)u_PseV@U)Erzm5Df7yTV#t{8NmATFHK+UoLPu<6e9A~$;&DU z4KjR}xb^5cNgKBQFrDe?#1AOrO|zP2m9L2>zI+-p&Z%%%pNki7Li***yDFmVWwU(q z&MUC?4SDVuwgI8#-$a`VqjJOu7JHcth$taR|Fp!5wS_0w!1lP7|< z*UVvj`Ksz;^b+5vS>DqJLAIxpq`PwG&HIA%Eo5W6xxZp~Ai;q+*cecUZE;suaP+kg zx_y@)>ii1oD;QFGyk_4^lw}5Xe0IvQd9CKABQo-o%2)VzmCf&ha;s;W+ob>53b|+q z3nWx9k49A9G~Hy=Krw42btP~&I6t??9>hwktojmKC^5B?U{t`aYbagVJJk~K;DJZA~Z`E8_U1>cV^LY$GyU`~d z5J&-a&bA>aE9LTf($sme46RDrF6>_lwdvkA^+Hy;VEbjO70;@fAXE=S9qcdWI`A6y^qn=)$-6e14~i5=5eV; z+~c!Q_Fas8G?1XR+#mI986Ap6?R~<$oH%=M89e+45lw{qf`yy`Wo5W8v=ZmVUxHo| z4V@P3OM=oK49;4K2JWiaAT=ho7qhGP=?bAxX*onu+&sWWDEA7wf3yIG>78)IQCT*P z>*<6BY8_px!5x@D_u9+rh7^S(tQc(KY%@Qe;4E~Ikd24>906AD^lK^=IvP_qx$+;h zARa17M-rpUI@X6bP`p};s%sol;U4%iQ)R`A{P13#$xW=G^Y@Jap69@aMaR+`n7MO@qtKl)`T ze)fnD-u4e}xFB^I)4&#TEfwf+aoE>Lb1)GzebgPLOMbKufFluGD0MYlXNi@jSc@e2 zyT90#+#bj7&$(CBoKf^@ZbO4l2URk-p+n2{+`PcS#l^+9w!goR)>IjVYA}9aGrd2& zqa`)4=el<}6i@7I4;*KSWm(-oLiy`_fzU`PBLR^w6qK9$Pksq|*gqeYTC$G?8-$xc4K{B_q>KC+a zqvN%k}Su%GOge!dm}n2B#)$@DDa2@3+{CMiWCQedx3F*$?AiV zkz+g#n|5O#&JrJEUGE%C!XcCGo%XU3lYa7CWeSvss5fg$+#FUzeN31;=o^Z*c>;S= zKc+OB#KFd7wi;VOYc`aQKY!QyCU8vJ%)^S~s(s{CC5T%YTWEc|xPMSTzps7Q@{3x4h(AX(;15KPrR&51hi8Q*jo+@O~@ z=~tZ(x|?T>0`pnJV3B5zhdO9*#%~H`m?rwZ7L}!UwQmHA@Ocii zu}siCER*F=S|T7Ud4{H|js^V1ISsEt@5y&VhO-WV?Zmc@+2^3<1h>_P>1E%R zZ+(0RUG@YUGUyYPFW|6Ml*?*^{T2VHq{i7!I_QRM?lk+LFSRQ9Eg1Y*Q%}&UU z^PMU*X!jddm#*oz6wTPN^#Q&4r$%|*;)2=x-iIX=(6sBHjbFkt*!2%vBA?%uZF8>f z=Cbwgkv`4n1^;dloAh@47w1Vbj$4BKZ+0@xHW3BCQEOa5+pUSHb24&?w$;mpBbR*< z+5Yq0L6(q2UG4Px!bUf_`%fXZ8D^c1!)2?mSd08BrPM)&8oo}Rq(@l?Vi;);bknq- z0z0Rt70c{CCj0B~{2|f54jXQz=7^XD&}vO9(eXbSK6nIoct$+!h9_5mvm6^6V@;h3 zug6ao(((cRI#HXYixZ)5``(1;3YPoiXXCR{(RO}_F(n@a}BYV3hCUw_~oqw<^- zkSk|6G|NAtJF+8a8Mp!@tPoiw8HwcCZ#EY!16;RXhn0od+r1+5Aqr?gz?w2dD@MAGO zAmK1!S_dm$ZgR=)64Wr=nT$0#LD$PY5*ukAzmNQt?-fK1^$L%EEGy_Trh$*dQu{~m z9C&)(vh_YQ6-au@aiQ+A)fF*x=7=30x63e?3s`&D?7VC$j66-?a9;k5!nst=`p>VK zD*n!5GW9%GO7O7c+{MhytmiYgoY7V!1gK?LC0p@JGC}!cf>=7Wpm`i8*(O7xqh(8- zVsIAO#MQJG?ddy6kn9>qqU`HS%XfKV=_3(G>;q-F0|`X-@9XMvgq3W!C-SazwVr$` z7m|~Xy4WUWw%S&Cmq;y?OI=SGLRCaFGc(C~DJML2Q&=JWxe?MhE8>%jN0iqap4RiG zmdbB!-R3vb0usDV`JbXPM@-q`5OOhI*DHz+76<4a&z(2oCKkEZ-;STxT=0A$TU4M??ILM#nowMA-8clcvF!eQ!cl>XA2-WDlLs z53Xb(LAmckJlEon`w?(ub`DFQ5-&7+iKy_*Se(^zh5d%d2@Yi6V(3xL#1`I>R?`2r z)C63 zLcM;OL#VINl&MiWIZ1f>upBY0EYqs(s+Et%@GZMO3fzerKgX@7_RU0itCD>~z>s@V*KO02Z8$Zyf6G*Z!81*gj9wcM0udHKn z@vgz*Wrod;;bpEGo#!1Dpq;TDXR-xm>KMZ(D` zgV$c$-NiVc4F#&9*lyBGzhpYFu)7_)H3#ceSuj5r>ah@K(r@BY&6G&j_AQfi$@Fp3 ziC;?-yzU&CFYj3=4GexYg4h-V9z{+|fkkx?HYDv}K^#H2oJ{rCMYU$3vsh7!_-YjV z<cILVEOQBpGXCYiq0FXzfFK z&}vH=!NU@ND)X31qV`9txJaF&4TudsHqU|9G@66l(4+;yi-Y|=P~z*VDLf59wtY!w zfsy77KWm~*obU2FNLizyIb#t!?+mOlWrE!jhkb3G8YsaSc;>=s#N%T@(BAs7onmPi z&;UX2Ol|x(E%!XLW+nq1sq8#AYN)l?#6Yv(qU#qlnZ9T3WYe(Yq5+Ck=;u4e+vEC5 zh)&|EVwx&oa}{B@LL^a42_BS!{SE}ca{p0GsOu2*CS9GjC){9|m;em0cv13~y=4$c zd8wp@19oZ)Xpf|N=pU+KlOAN0s6WJ(E51)s!rth=>Idt<9~-3rbU*sFUh>u+8NPi! z1~}jM-b$mHs;l>@d#CU3@@K3X+iPG5h$?{GMlr^!q4H1%sj}By?a=eGi-AhnJodOXH@)2M4P2ZaA&`YFE?aVR_Ued?@+d`y6JDHl()EXw2{o-l`$QA$>A2ej3PBu7i zjGG<&^kYRNy$D{&XNF{_@?hwfnjQIiRLO{~P=l**{-KM@9zI46y zFM*Xw)e2t;vqQ>N7R`Q|ovr0Tw_TDp{YxPk;d@JcjcfACW)Tf5{6TlnG@k$dd zRpqCQ)th^w+RrQrAO5**{JK=a){JrTm>^Iqdj=8qY{ZCwUdKa??K%GvOYZwi59+4{ z1T#`=y_7sBxpQy1q#^EG-#Pvz^4~HpewyPQJpQO8mthFL$@e{z8nhwvrMjFGuI=F& z$IU{)zTsm1VEqNZJ;sojku6nPprG|L@nyJKXZ+;-&zew%FzDwtztDUm4-%?K4#vAY zI)O!gXSeChB&8aXa6PdnUM*8{F6#FCg!Ac^=N-r|o_|yk{PX;Hafgn(Qz<6?Oxe^BkFz~P(o>?F z)f|)oFDHL?$BuJ4P4C3(*I_3yr<|k_q)n8t0+3(?v@vX0AO^ z?MR%jFl(YH&`;%T%uVf9=Iy8&@&Yu7n9|tI(FCVUma%ALA^H0Ea+GtyFX@;w+kdm7 zm8gcTgI(!IYVmk++og)Y@UGrUvOk-Ub`Y-8B+79X=cM!U?v|J`D(R_oQJ9zA;bf$c z+4{;_{GUxvI=@?T(7b%?V}jJS{gKnPdP;-ImU#v2>YZQ-Tz6=ukt2%b(UnZT4mL`z zpsObJ(*(Jo*_HI3+tHifQy~?0(2hVdo@LhX^z>_q?OPg~BYpdFm^1gZ00LHJfVpmn zwpouFyCpwHmls#vs_7(5H-Z$nHbc0tJTFl5OG;s6d5D@ z$2sqGlajij&<4ac!Ms+DI#eUDxL_Z*-*9SX%XdJ+q8%aPp*-YUL~RQ6{_NvXi0wsG$nx zAh~u0wy3(y<|_(1B5S=R*uXWqBp*BK1#V)LFjj!yi>*XaedBE#_E_S9@WoKdJhWlo z|DsHmU-!a#1Sg(i7`#4jnTQ4i8fNG4tsy7L!h0qe#e{H}5NhB@sj{ehe0b~w6yIEn zj65@ZHzCSI{$3ibz#jH4M2^n1fgw&D=5`F;9Ef&o+SP~^oSh%JfnJJ^+D!Ho2Q&rq z-ZSEOUP@{)Xqo#1rIt@IR*@X&rm+H)`r%*f^)-sTToljbk60jkzULl(Mo=_{g+)?1 z4uB87D|3I*ECGME1@wb8T2qS|y6~uDpd&wSS9XqcokiououL2FI0#342w=fF*J&iyh%feyW6y|r% z0>a#tc4fvRlt8?qz*(JL>s@-D`c19hYsIjS*da&=lou|}T_@W+yhzsm$=vv9%b17~ z2(fDmJ2V`t;Dje(?-B^mb^Imte&NwHMvx{ciSI&ck&drN+Vjc4Ded_8!KiJIIZ1$_ znG;pf-m*wr7{E(BwSI`{m=j;E)=4?PSX!PQ7ig4b#t;tbGUWDjdPI7LXwprz4*@)2 z;E;Os?ek!O5zz8=+BAJ6&e$C1tEKh?n!H%}mauHBx8G|Twy7d@WnH>jU)n;EICdHL z;<}*=1J}!scP4tq6qLPr;&~L!@u=QJ(PGqI7ryuPH3J1qE%Mzv5r6JHA2m1ql3E}5XQPVy|It%4g?(^W`NJKHZHN0-1BScMHpV*!EScr7N3>*64A!$=8V&9k*;rNXnd%_ zp8BGR{v(oslI!UEF&whavT-w3)=Ewp_5!h)63b84Nk`t_ zOz^x4W(hY}d9wj9O>Dubvj3cRE!P=ybHtjf^0X4GR_%LaP7pSR$2_HxsSSF%wR41; zA4OY>;)gE7Oo)71YYsD(ePqW2J_lLP!mx4tkoD`C8_oiX;qbTxC9Wmz8=#Nw_yO{U zOizUgG-2ol&n-QdJ`;gdwZzPe^hgk5SC-pV?Blm6RK>FmMAvHI#%|&-p6#s`&9D-F zcOWPOLgOM@iU5#}{k6nGEKWmoA@n2`qncaShHfrz^!D0A^}Hsj8RGKg+_w!hQ!8x^ zp2AlcHN4|(HY*yy&y0=XH!S_`vT)3t?-e7#rNmk(CqX9r93}WGXD#brF}|td$RM-F z_aVc2i4#|+|D!4ZnZg_1GqA3GC>>m8J1-oM^)UsBtMvD&BpXNKKnVS;z02ca@y_}` zEJJRi?;Vg-?wbMWafXlSiu#m!{@XPq<0C)L4DOjg1QDrqq3GEXD~ zhCQ^r5f6fP%*DU`f(QPD)(Xt(HMd%dSwi9Zrvyv1oHkGxm_=#OW0FwabLr-yKy;# z1M>#rRL8B;86ve6(fddJQJRfqK?4D3K+d&N?d@bV(5ljRM|q0Zu6EX^3O(q1hrU^6 zaZIvfm9u(50+9ZQ9CA9bxuF6HBDKxos$e)vkaai?USi@*VO9Sw*)6D0Bp+%t2im~L z#2p@SDx(Ac;VPTsA^$`U`Td)pSMV3IKYFvzmc#CK!4d2e_YsexSQl zdbr-J8!U>2X#N|Z?wfwTv?SbiNbj-biJK*BdFRqSu7jZ75a`|sa~zq^>Dj*y;tHX8 zE(7H}H}jH41e`3h9uY4$&-d5SrvrD`y=E^9Qeu&(+B<0!8mV|3nE2-< zUc&>M-x2f|yD(@%HFWn4oDo0cJQ)L4O&j5c30oj$U2nUs!AS-DjrxunCl`X)W2bW;^@p^B$V1xov~OV#)>mV1tF+DE@H)YwU$gp{JYr`RV#b zy2f*nEvE#wWW7M`2toT&%eg`J(^f`Mmim_nZ^*2MJm-2WBTErjxz2zq6+G17o5_&$dO_~#B14PIXy(L%;)6AV~mM~SBpto7P~3>tm>xIZHY z4csdL1YWkdRZqRZ@L6EckssabkwGz&-}QqVoJOI@_bQanVRT?>la0se2ibWSAsgb@x-npP9IvWH~3m++3KpsFsgVJP`;0v2)!RTEC)XRH7GNbpfv`6I!-7s-f+~_yx2pGbogUAv~%_Ti- zJjW&w{#gb4Q@XZg94Rov2yVn6Of+bIa(z=!73>)Q$AwHsh=G?3n4%(M;=+GJ5KCFK2--&+e;_Ct)%F!?z8^Rk7xTOPs6xWBw&s!J|^kOPfylk~!81_=f7TW6TqbQH7 zuHWX+Vey!|ZhWmIhdvjEb}@75Q3*KvX)G+`GFf1{RyRPF3nQ8MJx zaj9;g_}@!Js^8BEo{r)h957%XeP}I!#Lh%P5TdD)ftRDffcO5Y_MSZAMk8pIQp}O3 z?&D+CWojymec!Qn5#<<59th|=jn82^Nb)%buRI^r@}rr44|_ws*6iq+8|{4u>07>2 z>u5(rEzciO_Ox+8j|pLZR-G9gx75wNPM6eBDc;L~AesZf$^4{M?4Hzwc=!U#t51#5 z59L&@te)2%BJT+gN6|%iF|&OvE#2H+DFd|qus|v3RK4alhS<*m40DH5_!9G8q}Ay z(Zf1cG1|hAFkquVhJ^=}86XN%HARhWzbTiH=m-FawVp4Fe4s$`9&M0w_}a;|AaC_r zQUPO+(!${5R*bq0_l7r9DItonzPK1{sZX^RSt@fDa$e|-pOSU-d`xZCHFu=)$D#UI zSpq~MGk91JD%hBYky0*O=1&kp13)bF=BHX_fux4ZgAdnO_%7&`HTJ8a$_v|x z{R&Ollx)dI3n#Ir9j9mw{Tx;I<)bvX#Rz}ak+157Tn_&Q-qb{=xvhVt zX>dt860Cp2!#u0(2$?7TDMRaU1et^c34uewBYI!`w!L=+fIh#O++}9HE3@+6t^YVS zN{Q|Z1XmHLu?aXHsGp5(Vm#bF+Stl_En19fG@~q`n4%EF>o&9-CdX}g{Bo{?p&cm3 z-pD$jyVEzpJo@#lsay~*HoNboSIB8BsJ{q7-#{n;f;$a=qH6lkn-~7UA5u?}^siNQdvuRlwKqrYdTMD*hbK zA=+Ok7m}J71DX;7nQ+Nh?khDHLEt}H0L)Ap({bLXKuxC2>R{&btBF?>)ZEyUqD{n4 zcfq^XbbY}ZHjkavGU@0;ug7u=thibLhnnN>GRu(4#|)$tlg&y90F5&U6c+PI7bFP zH$#JuBAmVCZ^%oRik1cNhi(Q0ij?m(rBWj4HF2ZX+og*S>H0#n9S`T)N9g)4gQh7! zT;B?{S-AuyS31Bj_j4D?cqZo%Aj*x5){Q`!rD$wV_hU-?7aD;9r*2jo@#EUx@++Q} zdLex;YP3Em8K3j#9ZIrVKKWZks@~0n31yc=;`v30pD4=io-ytY6!yJ<5CWD#eu!Za z430)8$I>rrTt}QwysjPwDK`^UU{4)&F<05xtI=<hDCLhH7Z;*8qVslJ2{l0JmC#6^?-#q;#Z7R1c$`Pl!YIXe$OkdskcYbJS5LNbwl6!CHBL{N^`Ps`qHh2;dG&}IXmo$)2JYVlt zehm?4i1mspBJ3)x`~~lZMJ2-< zD9y&29-{B%thZC`{sqvb!tUxf>0l0C#jn+EbO$A&@Sy(nkezQmqnD+IxDdi#5YBpn z+lPotg*vwFoE-g+Zz5DG!C4oCk?Jpm*u+_y&+w;@lIVTxo^(Hnz&&*EeXT}NEVbHM z%r+D`Q2Wp{p|@h{eN(*v%A-IOypx#n!wik>5Yh z0-fqe{-EsYX?QT%w_+jcm9HUDo{0xgE;Q8W3!e-60A7H6WcS*Lc*cExFT(|+UmBN=uK*Z#Nsel_ zdKaptbuDV?6|y$3r>#0aY+XRBi?caYTHvsaXb;NZ$tL;Swpuk)q@%JQle!AvdX4U+ z)e)1iN(yD{k~^&cRb2R3lF~V@9z#`deSYLbIW-e2{x z=%&n|z^z$>GFpxoH;?>RzuedXqU~vRaqm(r6CanI)x|q{X?}oKPvP|bYXuR=8{dD6 zTQ*N#_V!wNt*L@K{q7wlxik(|#`)=~h4gYLZx+%DAR)b0KnUI450wOeC<^#_8sObS zgpX~xnGEbklrI3*`{yki0jRTX_3QFmzO7XOOY0<|0^iw>&~8Y<3%SfF-k6al^Q~jl ztwZIAW46O(ETDs|lO}y7k;uk7IXT(bOLRu0>zaNzP$l-1YJypabcE`ZBy1ZiDwO1%&UuqsD_%GArr zqmXxXA@9c)Ew44Eiw^PZk6)6st6$vo@8C%l9=4b8j#u`T$gu%Jpcy-&&={X)G+}?h z$lAbL{8ECpc5%AW92^sPl<>wU?6u?EL>UCa&|(111EuhLKvA_<63PZZcAU^PLBJP~ zb@r|cO<_;z05XEL6ZIm)Ck?%Ezze}J(si*Bq-iK}U$^e9Yg@~S>scWEa@#EA>+4JL z3l5u9?zLDP_v#oQH2^>xcN~9VHDF?XrB?oZM7@cm#i%7p1s+_{}^S#gf6^WC1*T;8?5!|9hjEh zNJ4=B4T}s3e6D$}nO+oBi9=B#v#xZ+l`HR4RaTUkQA15<_SrF;m5QBrUkcXN-lZIC zBKFVI3~caV9s6uP0HR9%1(&_qN8SKradL3EGXgUmM-Pg^LF~zXHQm^+7D^atfcww@ z;D!K#>1s|j=INrj2G{U&1pqpSfTk$K8Hdx-8f|Y?$V1a~e2ZyoWrOM#Qt#~%hBOf- zeuI?gXK)VeItE|&Lw9#NLt~O$dxlH04XX+Q8Yd$Ot> z5{9{cLv5?w5J|QUrxNb2b#2PZ4^_9ZyfqxS>>*sKwu1QUwUK(aG2(tJwk>zNH|V#^ z@`j@~@x^lFY0*TT`5uAU?bWZK{)SP_UZeiJ_1Ce5!2&Cp$PQ4TUZu{tlqN|0umzKw%FOKD?L1Q36E8OaA&+^XjM-kV#q$WHHoYqi7KYcG?tYltvT+98AE(9Pc4 z#^Z^yf+AM9cE~R(cmc}p$=wjg?g3F{CHE@jRCq$D2L+f8zPdjoLLbHf)hw4nnf2Zd z?xb2Y^VIhzzxbq#90ea4iS+Y|$HmY07`PtfmnxI%*xLnP^ZNMhMmSl%+Q15(=@;{< zC0NCIbulIGx`{KWfrn+BCS^O%5ZjoNSYk=n`r#%ZgU2n#qFQ2h57TMq(5)>L0)c3{gXhT4lYI#0z?pjCFee_Z|!^W_55|i z8~DzvwibE8tefJ>voocSl5MKgl(9h;qxrhe{f=3#KK^B}0J#AmU)V69Kt?1baeE8x z-3xVQQr!IjVX8SEIU#2{{es-qKCAyOV~=563+X#x2{?SH5c!p}lI`rVK~OupIjY8n z1d80)gOuRa%HIDSir+CU`e0hYH6es2LdGRKrT{L_Tb%j5u~2mJM8zF6F(` zY9e;+#%4;;g4-^HdLU7j?)8VKdZ{Q+%sQ@yMT`CLN{jUMgMYv)4%*+~6$qcZcK(;-GAm1%Pg}KBe*6j$R=qtxg~o)Gs(hKep=6elgotO?XidmJ zMovN?)hV_vE#`dfOM|ki`$8pVP|u(Qc55rhoC|;!_R&>(vRwOM`%$CQohrzOY8!$_ zg7dm|Z#=A+U?ko&~MWylDIn5gQK%a8` zT|q8%Hr&}GWeXc;2}OfxH53!)Ps~AOyIwW&gx&u}_&+Jdokk3LwjXH9)oX(JoBtHAq4eY}h&O&iUM z_l*7rWX`Hy)~N|%;z*u0QcgDmP}z;7OV!Raux$>z=c93>LlkWEgl3!?-+SMH(WhCE z>E=eB77|EH7qmv_{@Z0C_YXMqY3m(fo<>-Zfw=gE2P0|yv3V!VL;-QtmCj@mm0K8zU>2fEQUxoG;t66Q<5YZSo@_~_048@m8O6>Oc~_ut{b&8?+nI~% zFR0LH-Y-yLQj-p;Axf?z!H9kSdhn1SC?SV0LK3e#@!{!f!Mg7ycF#qZIudwr`YK7e zZ3&-h_9)|0-Jr!?i+Z{z8cO!ETR7byXpxTa4j+ssE_s7?kl(C>}6#ZiqGq{N!b`bru39a`uY zZ3a332?u~B9?p9%a8!U*&2LMb(MG#>0kW5ytM_U?D--95VDyI&FJy^hfv{1f5bjGB zC$Wp{afC13y+(-MBu``Kiv0daJ=Pk^m{W3(>oY<7xS&!hk3AeMTV0Iv#C1Ld;;0NS zO-sxU-@)chll=)Ch;!KTm)a2mQQ74XC3z!_ayB!$%#i+>J6eo4Ietu+%3uBy&RN{- zXSQI57C-N~aZ%O$#v|z%D)r?zEdDjlC1_WER^}&nl#vd_N9BzH`5IueTYYK`Il;+w zx--5k05JJLnowdirhx^+(0`#1HigMb)4l9IXX%)_4QegTI#0E>VniU&3GT7L!XHS1 zPBy>^YWdfym`+T({EHy>wf&l}xd&H6p7d)bAW&YjS6A>`Nyg$Zv&Lx3uS}TK(u#Hr z1%d8wAFuEx7SyBg$``fqZIwlXhBqFW*v$POl(?y){Uh2v>%ZK6;@jf9t5H1YoAvMg zwRIp#OqDHbk<6E%|2o%ZUpIKdhTI;v+I%f0jY_xIJ|L#8C>7#Rb!|&GU^fgS?|9X< z>1jV;yWJEAfHhBYrFb%emR)pt_8%;0Di0(Wq~)($zB~xRE=YdpgDN4Mj){GYdp{Yb ze=|bliF7IM2rE1|sN`UxG3mV1Bh`bU9(55@GSXY75uOq|!WpisnD@m%U@N*G&)sfu z{lZ9$H!bZxnyk@Xf7hPDy`?#)1#L+FLm{RpQb2NBrZ2KQ%(sU*W~Y-MyC@VuJ7JLb z9_q%_*gaN*!N`VxJ2zATh2-sQ*1MU@ltsQ`IKgpRGntCYfXIzd+Fx7zdR(HpFL5sh zpVD3h-B=m+?|VRUGmqu-_LY&UCEjt^vh|e#$fAZdn^n8nIcD-{db12-Saqez)}M*6 z57hB@YviI3pdWW^GHYQJhwwkG@VpMoe=wl%gd2B<()xwmnRUdzBXSQA+YY5lIO6Pb z>oc}XKg%s`mww$7dB8G7VOSdF;3Ei>hkGCaG^L}Ba$SVw;7PncPk%k1 z*wMxVd)Tf_tNYV8_OU7HX#Q<=R5#4XKjrn&sU-F9|4tP|S?c?{SsU<8ngjwK>0RiL zcY=RD?0+Nn`Oh-?!@r7fVYo_P@h0R<8JvZ+N)P%Yy)tUm9>*-R*&fICSHUNbFiuBd z7)lE&L|WguDVuDHXT)tT=%p2H8R%pLo44~P1BxS252{*#O4;G+bl&VwaqSe6?}th) zf&1#plnCM z5_5=k!Oq0}#RqvY8;BA1;}s;}fxrG}o=8NtH}|Fl!B<~u!01+}QFC7|;8Z)wl_ z4GduPFS%-znuG*I+u?8V+gVo)!hxP2>7Bkk$$Tdnf_B@4ilHDrV$cVwd0aqrVVf|{ z`$_+DC@b`5f(BvwRKg;wcto3j#| z;`*+bEACm3McIW+S_P+y0q1L@`0E~6qcmo&vBVv-!~)^TCSX_fRF8qxsc8)Z0otJP zq&|XJ4v2QT8=9VDA_~C#5A}_wx;)U0h44BXxJM0g*Lf-mhj|y1TXGxiS47oS$AQz2 zK;A@iO+iiSg1TA*OMkMV#?CNK7RmT2v?lMAZ+M{sAUs-gv2pw;DT`V6W6;pg@ z9Jfn;2ybr@la{%JcE3*xE}n~Q`A^aJoxjY(pQ(GUCJN|B;y4A`0ZGLOL)%oOHl^6Y zq5Na*Hxqc0&Ql}^1`$04+bjKn>dmJX>M-j=E>>diBFd1$%`Y&X7J3a1Oe+<2!Yn>!T(jO#?4-dUpTCZ zOAj@CEOC_feMlw5VDSX%!pTWoIOe3dE%yDNFV~_=wWA-$nup_r4=&BCQkgjvlg zG{g_5hk=SHxSryF)R8lRdkh+ZEiTLohub*O;Er?ZofM!F2I3t5RWwFJj>v7$^q~y#nEv2xbk1J0r7XRC>~1N3Y}u7Gb0_sL)^NJrwG| zlK}w!N&Lpk(35yZ9JpL=cDcioYHgr$AL?Ijl+I<4LpGj8k;`XaBn&UnS>S)O$61eV z%JbF4u&D%%oq}QWIJo<>UWi>#f7$_?E5V!Cit&fZ*=#1cEyZ9wfNCdxE>W zyF+kycXxMp_iujZ+;hKs-}jw=`gt0VH`sC2D#GR-a!dmT$p37UVNe_!SKmKygsWyTg5n=e$d6LEGbY_|W& z6$t^qwVYk@oz6GUJ~$m{4mk>A#9kV{OE_x`&4i)A-;?VpU2E^e(cD%IqCexwC&9aG{b@d5`9wlIUVrthBz`}p44nbk0^tk!U2z>6Nq1a1 z*AIfUTinBUuabM08gTm*&~*0R^)_=)Z|VBmd7rw`HBXVVyrTJixcm3?#+-qg{d( znGFEw=c+}1FhTG&P7XwyQ3mL_fTv}FxaU{58wzq#Y3Sb~7s9^%#3C?Ne#0QG=H@t) z$X*u@`wvEa-Nz?@C!Q)f45VlWs$7!;kY$SG7%QmXE&q(0i39$tLM;l;29>=CXN6-T zK5Ii28n`O3`Jb|`+iI)T+;0%lS`A?e3h-n5<}>*a?-ZeK@8bJ(x|XU@5hI4*Y+lBy-yTO~cNwCKFlz(yOWB_ecnq4z^yE z0D%5}{UEGb*73R;lmfu)IIbro&v;0K^+Y+?XhQ#={Xv{_TR_xyK$6dLkrQZx79XnJRpR# z$qK5JOb3HX=?h6&jjX@1a^3%T)-j0=2f9qqjRf?iLUdA}b8Y$jQ;4gQYK35cT<%y6 zGJrb4?*)6&S!5?XS#p4H$FU;rgG`)zA0c?F&dEO=r~YM87r#4&2}YcPZ%)lV;B~5x z?0@W}gBl(+|HvnMSjAim+U4$C9!xOTEn}1i6)pb{2)o672E_{Juh~;IY+CM2wc^iN zW5?|K5JkCPw2O!nQA)1YI$)p9?}r1pUS~Wcp~v0d=g~`rI71bo__W>TBOGecFu8f< zr!$N+HVY^j$>sGl77TjC2>u_DEZqN%*I!pfo%t<)83m-t_a@p%PGVa(Cm6Q}KhA+m z(`ju{@3u>SO#94e6JY+^UJuDbO^L*yYx&aBc(v;%n8G@wnddxf2vVX-g2PpDW5uW} z4|}WNqb(2`@sXSfuFA{Y#r#dgQwq+rzR%D8KN$uV?`8XE(%AtZpsaF|qXcgEb(`bT z>mtyc2qZ8Ay0#BNIm=-i;zJY$(abePk=-eCutN>AwCkGaBmqo5jHH_OH=qkrHmd34 zcet^-^-ykGS%hNn4*NPI$B5N`24(797!;k?!NRL0e*e!oO{i{8%&FaiWzXXNFrptg z|2aQp08P|2y<4>GN#zgt7oYMR)U{#%HkrX5G;-c2cv|EKvRshCmS^ z+ABxihwo*kTjH&U_dk1BKBDR%0R6k5#~~rM=)Ay{Nu9oGEy38@_t^|6BWzih>3Grc ze*7?F-^8DFLU%~f8(q?&NsAV6=5XfJ<{P)*9ff*@*@4*BzrY%4a^AM% z%uOPXU%ykJ>dGJAGcHX8vu>Ci4pI)&5A7$?Q~bL1m~arG|8*G4+hV@hQ7O~AKM|*e z`}etj|LL>7{3l$Q>ifK~9+QuY>w?I3Rf55PUxx@iOMLYzh9jeORUX9RCky*OZko)| z9mdU+{4j0a$@gDh^Pj_keAhG|#;@OBJ8!iBLMFzF+Iyp+mk@v%qK8xL6a-im>su0D zttc`cr&~dDA%HIpsQc7ip2SQ)tEu$&bw4gHx(Nk0xrb=`1LpDSbG1o+2dd4QN-$R0b zGZL|smEH99EP&6YYg8uQ=Tf6mKcVYmksFDjo zai3CA;N3Jd))o4k&e|5J-ZCB(!xUb-RgxXIWot&+@`W)^hua-STRL`a_CHvF<`v-6VEkd^1s-*LE)P>NS|O}qETR;G{1p*}m9!|ytM%s=nP8Zbg(PWab80XwZ ziis0*H)sj1@6i56%jMY#aJLAHGwKTa)+%aM^qR_MJ5<|Ytprb~j>Lf!)S(SbyGI0o zty!9?`WqD;2RdKxnH?DbV5ujb32IkndZt=hupYST?J_tIp=cJn(;V=1I+QoGn$ozV zNpw}|)(0a!qr#}x{t8B7K9gr3Ys<^`&*|%(5UP50am5IPK#H zQoZiB!_Z9ooC_9o=$3Q@JZ+@avrn{4wtML;sU6S2}Cxr)emHV&(|a zxhVxeD4@&`@_FQU7O$uD`Ni*I+XQ{R^7bN8?1SdW74-qHyL~FPEG$(6A}z)761|YY zvvS^s&Iap+)OKZE(3rdaO##T8?hg4+r*MbgGo|LX@Zmczd}rt*_{%f3000#8Egv@H z;wDam@vxQ+7DuQYoG|v${(O%zmwgB*W|N#|WWjE_rP>W>mZ6+JD{lq(nm-gnYLC54 z6Qz8F8!#?HpM|7An5LpRTA1}zWL%}ByboT!eRMM#(FvmQXXZeHYx0!v!&`#2U|nuE z=ahSG;LH=kedTleY_uX!%`859jjA=~?9YV#vTDRa0KHHDfYaPC_7gKol?dM8Ip*lP z<^X<=;OVnjJU5l@dNJR+A zA*`YgJ-1a2ZVT*`)QY_JZ|(A>GDgsxkfO@H_qFR#N;o@w{RJ#iXj%;(uTHN}5x zy04k%S1ImOoJyUVOJ1_whkD+eF+LXa7L6&}X|NU72fP&w=vyopv*F3$qbxKa*JQ`jFBMeP2RZqB`L-s%hi6Sw9~W)2v`Vdsq)V;@r!a}F(3cN3b3 zD|1?DA}EymJNPFigZo=wF{`97=ez3@xU$*y7zn9ad0 zCWnf*%=*grdW>S4;jqk@>(=8Jspk68l+w?0dFi5}jXC`L#YQM*h>bDMRGA{uOv2~P z#MW5l@9W~1#e3{^i9t!0DoC#UL7U7CzmlE#uZWrF32wYrIL9qV3#DVidnOnVJ?7eV zI$e?rPK&r@f#urjb^7u>?t%o#L!zjCW|bJ(1>{%i~>j{>iEP3*5ZOv&uno>SN&I z)spOyh>>`+Uzvo(jf6qLY7Y0#R%Km?$5Lq+fv+6&SWWxuC#;hSwYGke7y^cZECPWY z2>Ri6X*L`xwqAKjdj)l=uf^NuQQK;siZZ8)vsoei^T$poSXcV`asXj0%j)T2H?WTteGUQtG z_PDo}wT0!p0<#8m^8Qv6_fI);a&nV0{7c>*W-221{8K=evCg1QZS=erer4B)QAcW} zTys#5&+2(#z4*C+{;<)jgBIs0$l@7`uJ>%5DY|f1Rwn!({SiaBCogfEAXHyhSKJe* zE0tZA8LXsJWahIVC5@O<1Xr<19Fl7}4-eepsXZ#y$?#YX<3W~V^;fZJ8!T{@Fj*ShN9+B|qJ>3l(hCVwl z%F^%t@#W`eFD-H@`G*-haujC*x$AvOhLN?@?)6rORiE9US{oW;Em4E5#eH`(;pO`J zYI#>&j4IR((%UDnDefV#tXitRd9cBUZT)xp?A<&`^XZ)zX9TQDR%Alz;5r|KdgYf~ zW?u8Hspq6S*qj!JUo6XSp5hlR8A>T90&w=sL?L*UHo$4W)lE^Y*=i6&gnIs+;f!x} zVOJmM$aUU2eN8(sL$=TCprOz>3uA9Zi{0P`hx2+UT72*`eNrg!7fy(pOrVhxRz?b~ z0Zxc8x78?StGmTNK}I(dA}Rit>WU?vZF6%fIb_`asX9ZM@@GZ9(zWx$cjJkQ3@}ig zwq}{~@&+@NNPkQ^67KF+nI@j~fmf5KkD5m4J)qAFejuPQg?Kiu9L19hA?Kz~m?*{n z7tUmeY#B67RNu>xA!yC(wdM}5wpIIU|Bo~cHPCs{ygb-3iHdepQZ zV!|agzsZzLnOdvUdNH#Fhc3rUh;<0=mX7*%U%6mm}nvQ2`>xb zaY~H_aq}b1ws1US53!Nia#7qx=pT$q(~#RNzf|G}`{SM)1U!uk_o!c0>;V9U7PJ0E zVjGw#)z!n;P3K+u@`N%CP?_OlE;cGNBntll(&m)C``~;%T54JJ7vJdT z69BD?{0ixlaw3baV^U)YnflBwx46h(7`!+$KRVYJ01!FYo?O?Fhb5GVi8r}RL~lKm z7#6$=_ zC~x0_V7h%#zQjpuE5dj6-VmVbLYuUul3?TE2Jg?Xd{vZ`{b-qlEO0P&p8m13x^ zhs35DrI(2%V6l@yV2Z}T@m1o^6t|Zty!^g&!r5UA0I=NH`7+tw-AaQ#pAP?U zwX9?eeycB!GA~yFx3ns{$zP^)4-WWkU$>@SIZaIHG8jR8dvz#+#qfB7>~A{KQC38< zUcT7cZZkY+peh!YkxFn0lHjdxiASPmVMDgF$v57}#Nn^?uwKzvmC`|W8MNx*b6U{( z8_75@@U;~D8`YQf3e_@^t_5>8b~;}l4OUKCd zIHnoP@q>4^Rt|zXb5fEd_oEx9Ll$ z2TnjPsEObN#<)RS+FlKyv@|g8I_tejVbY^YP?Spxb}TgV0>n)qd>eM4w33fFxAxZ$ z{J8UQ0I0D^yI!3hk3!oJX1>xi){{T8l5oBm-~_mIwcV{oTI|nZKEgIS+5f35FkVCc zX{Nd*o%UemAxpzVwCcV!i5|f1s8Z`v>LLOBYu^>ky{eQuI7&uSd+M-ME-TsUWp@04 znHBY(8-}9U4+Ak`KhXZB4xz%Rw=q7`*jDTps ze5BK0FYy=tv@Z4OPnE*@4@C)9w)s7T<@EX6nxKm*)A|CBwp#4+Nuw_SRCG?Wx|OyB z((|$k$qe4t+(B;;VxHwu_C0ag#>b4IGc9e_ZCJxf_8NY>#3eEj96+3s;dY=~<6UK9 zTCZ7Q+~}j$Aq)UGzRyh7DzUQOPPDn19M9yn4U;q$zo0vqc#$kC0xQa>)vaHy=8cx= z^&>q(hd^4p*3jYbDZx9w*9*mUYYx+d*mSh4PpNvonyRleF#)$3fftgPjG9;!ELu1e zErqd^zI9!c23-pol{%g<-VculmS(N}!{YuXTL&OCL=p5z&1LSkpCiCeze!4`aJIyh z_9%qr{Z@D?IFbLP?e;phL^^2DYLrFCVds%14tPb7j&}A7dzqVm&CrKyZnZAT-k2_+(k zWww>l5(S<>h-l!EmqU6HSq1|R0A{}HcO7_I$vRsWxh&@6J>}vsn@VnM+lJW7dIzYM zy|b95t1v}QsW#i}4_c>%vVl~Orp;dD;m^=67B0ZEjZA81$~BtvaEgFO11)Z=EfpPw z^X8E-?{5n==m+2J4^EzSBiE{?F$jDr_~?rwAt|>cApqGdoSRnbC1Y=I{vC%K0t(;& z_L8vAdu544RfqS#Hr`@jTwp}S_F8@Ty0Y?}1)v+g`f%DfOJM?ZyT~PH=Pl&Rna(igCTD@@dG*sXTXn7FJ(D57q0zw8Nl2J2$GfLs=#yO5t&6l*@vE z!M>T70sTNMtHX6cWB4Ff%4&akv2-)D{Z`AjBebJ4L527|#BGDIFT*IN@KJmqth5dyq7w-uGe$WOxGwtlcs}q=60EC{2vd-BAhRUAN5&Ezy!#rOfmqfw>1$;3b zPj>LPbmC(&Gq1C*!DKuL9R;Y;<(A+}`Pf(~d=@s&!5k@tI5}i>?^n}n@hUiM(gwG2 z!yMcKPls%^UJY!TolbsqAtud&Z(Ueg^&z~cxYY{awz)jgG239Uio8IUU3?wi^Y)k{ zUQ@DjGX}PJY%mnDKVyojIhUG;eQmEFs-V5_UFG5a9=M9<0Y&rX*WQx$*?HfU#R~6m z5(@eB;_*0d!f_|>@t8IhPTJj>HCAf7R{kLAfj)41D!|sy@bx>mqv=0MnDQ1O4!a^e zKtZ?3>riFU%)Ai2ixFDn{JJTIWo7w-l=~;?58=H$XL^~@Ik4)zoD3z*~bcnplIasM}K{No&@0c4OB@^X(*?%!7Xbg5^P9(93*OP zm+fWh+D`|3!3zRH>Pv9{A@TiES`XjLf;&|hw^#t=`=1sY$?wdYJz#Q*%jtE})~tDL zRN%1F@m>Lo9Fe5W2;I@QW+5(y`E3XQ>a=Ig&p+W|bl^%7DI>G9EMZP!hs+2MeHw(x z(R-HxX2;EV{M#&Uvke-uVC4ni+$u(TpCYkxx_zU?zwkTAsAJi$@^yO919}bde9-i}=<9p>W&eUGO{CH6SZ1H&)>Mn+v zNP`LCekd)L*MVb!(a6ivszJf5&`_^;Jls}~l4`+Kb)pizd>B|P%H;r-+_BPOZ~dka zAK+$6VyyNO)1#4;`9GEu-^uCzCX^5v0yoU+2qY0TV;yd1mwqSu+_04izk3cpAT{$q zzF4F=dv2JzlrX2=SQDg9-&ulYI0H8y+poy*vhB0|#-CYLwd^*k20A;bx+axYx02X2 z@(*8>G4|2y$UOQbLUUVMMGwGt<}e$h<=Y61YE2?BVqlYkfu+uf?Eh+lPGmE}4`uOB=8)}Vi)xm|ExeSHyRr6W2s zZEW2I34{aWU&R7jV*xA$ZQ>P`4PJOPex!9V7#bNaY^0qtLQUfNmhgY^Q|cVC6JPgj zUFg#iC`0Xp5H+)WEY!MY0r|qzJ~N)mN2lpKgDT%Q{z>Z3Y&GzeJJ#F$-jC$~!U1ZR zE2Wx%DUA45=qE`q*refU?{|XqvQci`9Re(}Q*faXVI8kZuCgu`oqfcnC^yLBet=NZ zD^);0w)8VA7BPMKB|$1G#i@Qfh6~=5cl5!}i%HU6h_Nz7W5~y0L63R!w2J>)qP}Ri z2?wfGta^&zZXK(u8PhzDYdl!&`9;uc?S5@3+Nm{C0{p={+qEvPIX$gd(-tHmGCDdK z<T(oiGHN|oklS>o+cM{j z8ioMRCj$d_33y6c(fmk5L(FVFpJzOrfX}#{9@;a++?Rk(>TBOa>_+%pn-W?d$`P*R(Cvd#gk>L`gYx7ZNigEL}h68wE{3?Sj1L<%$wU zo7*AtaOSe9?dxpevZ>C`P)780v8zx8U-V~lAtq!x>fs!uQ?s0h>3QTtzkn)@aE}Xy zdq=^Uh_mWi7E~keIw#J&jm2U`t&~{2w}T5C0^QyizCz60+47Q=8oq&5s^hB_^GTR$ zPFL5-1oSe#q;(omdlUZC_2<0zRicrmvp_AVa^yO8z!BTNi=ocVLBOL;yP%2J@9$dW z)whI?9j>ckpdOmexV44pf-B$v)Oh);F50rjPb}1~H_^1IoWQa$5l(^DW7l$!FZ(v$ z9Ih+Y_m90zjA(T8*T`dC8OqX)f=kBw(zul2VuRqt9=og;69-H>M^&9fEp^tz+fF(d zU{-F%3W%pw^i33U-N%m?FkLLjpY27o%c!F7pK}$<~C_IjbxWG0j33UQp7c zFaZZR8vB}>EoM12xon;}aZz%oEE~=Z%r9$>{l-tgeCj*V$0>Z5-1?EARg0TyXXmwh z7#x9&HjOMHim2(c{bqkW>5(@$%`j7Px_bXhUx4SU23&vm-`Ai=Y<7N?Z!L05N?|n8rL^@F|;~R<6Lv*o> z?wijB?kbp6I!gbAeJi>A+4UE99o(iLR%|%xp*0%>W|`i*bdY}ANxA}B*?KsEzsNGB z!%!`lzI_X<;B7DT+3@IQ!CpO#2J=@|LT-DWJ9ZW1G(ow4tFz(dSR<#tflD;yhzE>7ETcXLR5t?E~5gPQ@(6PGu{s#>x(;!pzW~Ij6%_hM9#(Z z5Pa5F(}fW9gV7igFntXr4r=oTA_%(GqaW~YKPUq$c-r==|C!`e%VDR`YxJ`rG5^`{QkF2k)(YS^xmz_7N z0^Ll9o44_;Y?9P%b0OC~bB#x+|A3-muC^qrx>+!n#ZpkN6D>T$=SeP!mfahd`7>wS z$ao|t`_&^)oQ2`0#L9~!VRgOUTJ?fz08Y!*@K^SXatY9VQt}-x<+3D`ohxL_&rtY> zB63y@fQwzDiF^kiu@k-mED4KO)7WAPSHi-Q!ZcL)9vZ?P)z)f0mgM&`@s%;1e=Tri zNjsDm=?D-=bV?mF6#E2kHe&c!Kz;|&>bwr&RV0<@Bp*?g<3oUNX%y|7D~M#GM<)aC z^;q4v?$CEN=xfeEKKZo_NR_bh9V8^Ks$F;S+kdcZ^|2Bq(FI0{&3L~Do6W@GS%>FB z!BprTy4Nez8h0?6`t#Qvlx=0ouKYbGcdPxPis`v_CSN5x- z#4Np4dEDzvdRzVbB!haQmBqq@2)1$ZV7Peh+H+D>s@;Q|#tXvnDJ}8m(C`}a)qbfP zZz&=6Dxmk#VIPsFCI2xU<>8m`x`(&achY&?F?cYN#IZO{(yk#e<6Ds=943)Hm;QNM?72#dCmrRXdk&j%Mt5Gy;Rh75H-DY@2S*Fh1Ocqfl^*|z3DSw;m*d+P8-jK={Zf<8lw5|hB z_g^0*MvSS2#NkYLm*Sq)hEYpI6>rRLhQ)0mbW9z9JKy8Qpc7dY2 zjU69FC|4eQlUjT6>IJlfiV=z(Bu&4ZULqWlVak&V z2R5El@l&oG(@=Jtv1PUyTtxq97RpCHu%!C02$oj-)VYLQ2Gs>%1imNKY;y~m1C{qY zB9u~OfS{an;xFOLz?Xe2t-rhT2_G{%8blE?l5nP@2=(|J4w7RzMM*|jAYEsI2sO5V zoCW@=;`US(paE|T`IFaeZxA%F!)&8Er%b7Vvh!v9gdDjs@NLu#FMK&_^)3ZY3-8^; ziMOqepw-k$(|&^0d1qWjdQ2DRhiDOlGag~SJ01YCW*96N-=vepzLl5I(7t-wJ6tZ> z*E-)A%Nvh9y3h>!5~S!$2YHyBA~)pN?Pcy_fE67uaKPHx*DK&5LHMH91FMfVk*wq+-^!;S`%)kWiOXQ(eV+4B<1~sK zUbvd&kee#~>YP3esw&2YjOMOYWwosUELG&JOH@!IR1YV)h1*ix6j4$4%svEw*{L*K zqiMknx-?$X!vqUea=acN2G)9vR(9lvG3VRgXJC137qZ_R)Se+=a+1HU9qzoat08tJ zUU*y{UB>kQ7xu{#TIbRFYYMSfe3<71$&;o5mS!iNVW21&w2hU~GrggQ8*fOy{fUK(pHlM$>#=M#PDh@IGMEysrS9q11Dg zJdvvfQXa^qKYY+<3TMCR5ZZecJ&%^Zw4snBdY7_xI`Q$Bgo^@%Q-n##ByJ1hipKar zOF}dQOY;(gV}sH+6No05moS!wd~*#$I{rH9M)7Ey*+y^1F@hNzv03D`&@DK@%hmJ( z*54m1594YTy?Ow^Zis1!mf@^G)zL#oFqF#y!79O;J~to;7;u`qdD`-{n~!}?$jEa% zQJ&dJp#=kYv3RZN*v=w21>|ps#k+-}nBoDJM(+I7s7vBW084N8_&&+Q{68rTS#y6Z z){ODyp!tE%XRtmT;s8K-pRTH3RcFvMlg8F)JENsWmH{H{V>O*<33uI#VHRdsDiWwV z47@8}!&Z*^u@WFTbGRQvPSl~L0Hcsq`_VOdqQ}^=%}*nL_mzwH6vp?ZP4?33g5cQZ zHkRJ~n|$qw{Ew!{RrLW9pqu6O#HJ3yZNvO}ZUyDV>&n0S0BMgmbD=~%YmnosWN?Ev zPdW+4rCj^=PRz&ZRSRX@4*Dw^1v1wkR;YNnocc8+rgr_ZXqVbL{h^9w)?I6EX}{l0 zqz4c*3vhyWE;Ft#w>uuvL;PibhBES*g8h+|@&xJ$LaW)3%Sm-edr(7pynl;ZBLL`PIFMv`)0MFZ z*Ul{DM6`p5y3e>S0HH2KRVMir!BFdMh9+KoHIRn|)sUBnkhJ@hGy%mNPM--cVtQb{ zM`iIJl#hnQ2-%Z@@uhb03--BJ_EkeerQkyBMGr*vy;o0IbMaHUV#e3fbBV3#3Pq9z|rR3LnrKNSY#rF`5d z2XoH1tFz{|r&Z2FxIf=m92Yg`iMTq~Oqu@H{geX3x&6#2ErCgd>L65i<1#n@nc4uR za2_y!=|lXerlB$|Tn5^`@Q==w@T*KoD_=-vUv#9mLKXd4}dt-ZAX-t&)EPBBH zk9UxJ&_huEz>RyL2PnwsDtlir+C!1z3p5?PiGJ5*mb@BVuxSoL1AN2b<20IX-@~~* zjD9x!HcVMsr-=n9XNCREQ~4-rktnRjYy}ft+`C1(2&@m(ne>O$eX6O1k*uM950%BL}8~b4CroA6%lVu|wY3QZT@L zy4Kg2u17DbZ)+kXFeX)6v1P1(a!QN0f<6IPjvO%8J(4oB2CCMCDese@0)+1*tJe2G zP3^qsi>on=BH&tPic;hW2o$4g0`AD`CouOP9MeWoH^<0#f}Ftg)! zY7NWN0O+r-T)|3UG=uTYE#pkywr^ee8eXVHriN@kk=P8T;bT1CW`w(00{}&BMOcrZ zm}cln)L!DKrkv;>fAi?<$-m~))|8~C|@?}~p z^aMnhbhFlt)Zs{yn%7$n=vb46V@lxtg0M z^+8Z#kIutA9csoKdiG#iwZxN-v3hP{(UytvEn!6E#Mez+U={saLbxmCrlV6#8>rta z8l0ppTIdgwQGslV_2Z{u`w}2cLS%}Af&p93!78UJnXKiC>>ALC%QbmwSdt1YO5HxC zaSDVm+ri90&50fD+v%QtWVc!llTpK#44C9R&mSc(6)jEL*RNKmd&q3AA8IQiH6a$p z9NfG~V9_s8FGO6)^cnOtY`8Bp9LXL(5ca2u>@0{K%~vV`a`5PH%C267&|{j?NGsjf zA4^f8-DlqY#v-0mXaM0N)v5B6r*CJ{tcKlq(vKG`rPX>9^40c9q^Rt(n)ce>RjZtW zM24+pBP>cmk4~21PeOaa!K^9i#I9=YlW)5B!iXwd7#df70!Bi7{G)kQGa5y?UsdJy zJOy7)JWP}%M&fAC@B(`$x!eO5x6;OAT>9*6k1QYal|T~5!Bj8CfqwxTVg6hT$J@Cg9{^Rz+t3-Ur zktcVVMWv=H!#?$#b1b5S?)x#f;3sEfCkU7LgMXoFxnZONDXtYJ6Zw*pbeB_kSZqJo zg^l8U8*qG6&L1RF+LgeX3}|mwh8%w%Vw2^P&8DS8iTN&rWR$2$7@tLF6N?NpR!%r(Fdv02n6r*O z-V@_spEv=O;`cZ{3gEoPZ zprHqDKN`2p>otQ{Uc1_EGTXs*(*yuPGr(cY(vQzfNF}x=OIzPS5y1p+O0)EYpBSr^ zX_JQEwZm=p?sQ?m9%qFxGuEG(api#m1cs#Y8snX6Tu9(XLJjT1N}dJ*h*P8*O8e^8 zk%9*+KfzNN`bXSBwh|K;7pE*ABeBQvo}F2Xn$vQ&Pg61{U4X1%Ps1xYKBTjv=$=#y zLt@wz%O=Z^3$nHzn_q?N$omzmE8k5`WkqM21e|$Q(m;?%{XGs7(^7$>$?%QxW*>4n zYP66a!lUgo?S%!$vdDa^AoM86u!g<_q&e`US0m8NF<&Ww*(HepaJ zk4-)!5d-bs=g5e`>z5 z(T8#6Nr`0Z-9nmq!mw^=eDMw%2~dZn+SRtNBdu7}Qgq-H&cHpwOTL%0>wEFVhzCt| zZJfdoRbhe3#-T%xXHJC^MR+v#@~vsRhDTX7j(pdNgjQN*c{GwM>t@HEW8W<4X6+r1 z@;pbq>(QGul5;>|Y=r%*z!YB;i_s*39WtY!w zDp|r>8rx*)O*+`zb?5<|j99%U5FP@)Uw#woVEg3sR}dMVmY4@r18EW?GHWj>1~I{+ z-8`*kG9uIHFk^*dS|`UMQWB70i6Dg;v-yJOdYnB=-&o`=D)BQQa*v)px8Ek7MnQp4gpe>>a$}%chA0@ zoJ`B?bzA{SEJm~ZPx}!|YLHx#3C6*U%6caCyZ!{}Y=K0`rFVM?iP_Fn!D5r>zWfS4 za?rzH9!T1@yyFD}=8p7QCycTQ??v4W>7%_L%(5bLC$pb72`#Vz!eY!;apOzPV!=q{ zM53k8!JuJN=NV8OIg}_>#i_D+4U#&eb3{`byP9%3Tt&1+{)W1fI>_#W6nQKEX^=gt zX#I(ooI&t9=*5bE8{|vV7?Gk-_mW0X+If^=JN5~X_`Lq{SeKkN^Jd4`^VYf3$yS%M z(vF^xHTh|rbO}*uPAB{Ib;Sn1L;dwlB&;VBHij_O{at-Z_g0_!;ENwkqg&LZu7gGA zrHJMGZ0XD@wZVW-b-rz5OT{E84Fx1nBB$KF7w%M8d5@RwhooO1r(NYiX@?P^dfRM9 zL`1qMz06h;a*{Q;oE(M`I!gZ*jP0~U9oGpatRA}WwE)2YZln#EPpaj2Q<@B}up>y9n<=97p7WYWiIfxm^+AE! zd%zGIu@c>KwVU`_DF7b*p!w|UQ~ALwBmC}^JrcE;bzXZFnzu;FoZN}&N(g`Z*#Fck z-aV#KGkb-!IQxg^gFs;H(?sF8ff;>s{!dPWo$#10J&2uV=Ucn^7GTCYclY`YHR5Oe z;5FyHmPnIKqz^x-@Uq7jSZ&IR)mLmUJfd6ONG2w?=a1@Jl&35+kIg0Lo`(=QowNJF zx%);@F5~M;W)$6skGCE60@g>X_dJeYeSGdejVoUdKS~f-Jdx|0a~_D0h(ov?j=t#L z+CZ1eW;A|O_fvl4SHb8r`9ObOZjSc;{{%h61LAH!Q^Z2AElK77u;~6n-2+UD{Q4h^ zJP-ws5DN6rpyU5IdrQ@ItMDJX-@osWEdD*{Fh}Z9j3(uROp@pEhCiF#>FG}~-*lY! zz2PW<4j90<9hKkkEzt&$ZF_x7COK{FUeXYs0>T-er}Q%po0`O;p`>)wCyN528(kr- z{LmB_iS(Ztb==4nf{x{4;|*mDH8uQH@>j)k#aS`->8?YEpB9uI?QRenA{f}CW*Iz z`F3|juGlM^6!LJYuD}7zl@2F5j;iyDBP+Nc-Pc<`m+TP$ZHwB~Ukhoa;lAGKN23f= zdh9m@Lm4U0cK%u}?bX@0={uVTv2KrBQZC={67pu%h)@_b+69tC0f2a43VM6Olze()C7n-A>GF9T4;q5K*U8k&cs8SyIVWd-YMM z_L*Wg=)QMum}Sz^)M4*7v*X>$laYAGs6t{5)gk@1?6d+i?TK^P?g#`F@ zE4W;*G3JzrVg{^Ys2%iuodQjoV8yh~wQU4{J*koXh`bdC^#5&8I51qPK}w(|eV5ty zqR4_X&NHfsZSCU&gboG->2N?$r1y>pNbkKV zy@LW$q$hNwR}lp1y+i075$T{*k*1W;gAy>50B=0&+v(YsH*IhW|_UwauQvUf}KL0o2z0Up4n>dnZ+B$9ET&@u1)J3?&ILgZ}{ zo9guO+OK7t|HOc4NItTFXTN~XT`ezQHtZjX zXKifsHMAK-GY-YdLF&9;<#-{ZSD{yO%iMEMKGp1QpmP~qh8a|Mv zQB113okbCQl-sz(6(6pU-^)Ez2$>|~)j=`Xdwu|5F=(7q)|k0Vz6C1QQHcAnF%QKm zZB!`P4nwc?Ayi}u4AD|*TS|nHljA>3mwK%xwn*)j3qOu1P;yZEpYe=Ww!6Q!Zq+_= zc1)>Iymv8B5au?cCHk74sI|M4QYuYLo;va-Dn}~gxT9*Me`-d)Ku9P60!Q(1_CG=o z{Ljbrvejjdm1)YB|0cns=S}oK@@{xPx?lW4OX8mw6A!5zx?S)e>G}5sO~RH2NB*<1 zKjk<7e~xUL@uL4{zW0moJiE?R_P&~=X8IiYUwuJ2miG&esSL=Hw!T2jKf;~+BPQv} zhdhUCCF~X$wz1y8i-U)-J*Y4R^<3iVd=vKGJ_fgUwST(>5XaHvCZ0Io=i?LW4^=D zta0M1eUs2q-Ic|okt!(lxqo1OI3&^;e2dcLt@z7NVPi>V%zq~Y ze{sLHG32XYx)r3r4P9e5?G6aQyy+G3RfhQntLfpHCsi!hUA?aJSUJfSi}IaY_W%BV z88R~CZr^Z&d3P0a_2|K3r_2SH#X}h6UEZ$&)(&+kjPfXZ%KcZ_ADfX1@4X<2E%5PW z76vFC>=+gf`vB%y4KLuS4H%|lQi7-|RJN3g>v9CC&|cq!xw($gUAwR_5%n;gEqook z0Zc;wod@91czm=>dR(mDw08QOs3%q7dZ+W(&YxVDpY0rz_$4#%qP9h!aIui3v5X=N z&5E|x{v?B1zB|m2U%1AEPGL%OXK4svCM3x*>t<@I-eQbwBPAh0!jATg?hz?O^g+!J z*|ztu3y0;1d2wd*2zD0?=sU>kJhI>UQ0w@|=1C2xJX9XcZF&N>=f3hy$M9mM=F$d8 zA&f96nL+UPYzG$!9(u#w%b9$=VHFdR!eQa(lXPXWJgbvKBd+(ctu4_!-BlL7y-<4A zHe?4c0m*3V8;I?XvfA$Udz8>)Sd^&{Qd}P@U1DEcF8tpxnEV~vJE#8*T69r&P;Kvu z3Av=cf86bMbRkEFf+ndri9II3( z{lg|DkzaXkJzlEGJ*NfUDzX$Jxl=`v+fLPy>(5#(dgHtMDDY;>I#BC{$8Zmwzd zvf-Adsb%QnkN=?GExeD9As!?~4ZGRwA^B@Gks3c6AG;og*g8MY9PT?b@vCm;e=%Er znrE&;lKk1w!=e=cz^A{p0FzezX<7JIq1NsrE(v1#Tj0W|M-~C@P3Ipr`U}bF z^F&r?x72;Ku1sSP=fcZ$Ol**#Nk^uP{j44}9nzep^HR-_*y5PS+m?cNBQ)#-3pb`+ z5v&n=iBopcYvZ4@lPg@bL%f$0-N&~v!0~8M7-pxa`Q56A08FPD}JQqDhL*e}yYfunQtVgsw)#Gn_Z}>l zA}&9^(ph=!*RBW|_#Pq$6ND-eWMQVWY}itO#BW5Duk7mJ+ieVK+d;>*|Bws0y-&y* zsPWZM-w&=#EOwYhch&DLDkTb+Haae3W*!v$)2AhB9aQ3oiH>wZEWp1fw@A#i@=ja! zoStt`soiFvnT&3`6umr#sJ=Z1AH6ImoMdv6W;zhH*%iUWdXHW?Z8p9-@D;w>d5VCP zmIUT&+m%6;$c2l-PKTPxdp+6eM|bVG7pa^C7!bc(F7NL){NY3{U)l)bYcTSS=U9gS1$Al3MQAL$sObDGQ1&3;(-*wca{YF!fU9B5BoNl;?tm(1p zn~t~!PwSiWH)NKMe=p85d+&Aa7pS!^Hw2AUxK0*dnT-|C&8^+hw!GU3Cowgb+&=3A zVmG<%7B4}IF-=B_sqf-DFdk)8k)3yR%)L6a(S?_>j%fyW$H+hA-;)q~l)&iJDXE$= z=dUbu%taazs4!XzbWi}<|0B!civ_K6X>Ua zP{KLzrysRh?}nRW$zs+6Wo)eR2+)pF0AeHbMW`_FU693Wg+r{WsfZ#-I{*)Vqyu0% zCN`SiqTV@6DlRXh!;82FYFiYJ65bYs(2~<5)Ud_59C1;@GNxA8Kq|VMpXZ@*5&P5o z?q5aKzv^b?=RMui(3g+3c8a|%I#2JHkw|mQXZ!#gy{jer3LR#+GOp)bcLy!QsWQh1 zWXj%rm*&I;v~cy}&t72s%ltUB7NO`ROBx@;<|YHR+mi)>oH9@D<1&}`%OIjE&Ajd~ zOt=i6H+_&2)LX?3NuLO+fy#YUq{8AT*4Wy0&Ki^~LT2Iyt}A_=RZ(%UONZB;?k#7S zF{R;yR8+A+2%?{S&CT5Ijuii`ueG0(^{oR8eS!R-`zH&VRIpe228hXdH(czF6#eB1 z_F;?+f7pUBJ#Bd#`v*O-#QmQu+uFUff~YT(mWP{4{1x5hkf$VsMG@g^FZ z0}2NDjkC(~-~sPflsUM2zOZy!{g0@BOfAo%lGz8-u{~nN#phIdn{}dnIZrtX|Kv9d zspOG|h8a)jVeGC0(|`yKUd3j!>(|PRFSr?^Ms`j7YF__)5i?Ev#$cIpYxM`gFAAIx zBr%JOn-r!HlF_Y)OB7tDZVn;;hDnwmI!KRr&(G^ zXO|5_npMTzY^UWzj8EHd4W6)+<&U6O7pO>OV(NC-rJ%<3?rN8@AlihFOxG$)hnWMW z(@W!Rc?9;5vTLVYdagw8UeK&0JCk0bH5wygqOes^r=ceDX7tCvh?SDOS?yGh$rLt0 zSuq2DwFUK2YrO_Px6NEjgq0Y+ia`Nm|74gZw;%~h%F?#KMz3XejR>4=Iv8v^%YPg?DK@dbNiMSL9w*D zg`^F=J?$2A&AUM?_k^n=DfK=%S@|n$A)Xx?F#}6GoA-tD5DF5K@77E}Merye#`>oEs0DnM)5kPp*%{%GJ;f3k_OTbM0ya(6mpCK1K)$upsogKJo=E$H z4+c+7(l0p)fcyIjEPXGZ=a*BCq_WL4wUW_~3H;y#=KNo5U_2N8G(h|GL4PHhs4rQ} z>$(ft^>yCz0Ytwc1VIJQ5Ag?*r|e}`qz`*a!Hs-AHKUn1{-Y%Uh=(N&WBPS7hpz7?tTjCOtCGrF*Wl% z!O_40O!qFqQoc{~P?JVkKn<3MWU-Wq+i@E&|7zscMNCCoeffkDFrkT{;8MWUF7Xu} zn`XT_+ChXf1E>C>e|adj7EGPOUW}XfjuHjBBG)?lYQB_{hwJ&sDe^(su-fZ~tbNSQ zpXVZ=ks^_>0E?>+qQ^!w5-8W1gL_4a)mygbT%uojar*4c)8%}Vw)tOkIO~=TPT!{h z&Yvv`xzc*h3^ThY>ZSkdPfm*LNBd3lh)DMA)O7{kjyWP65%0w-oo^bp0>eE~yK{H(|tfv#u zau?sQ>I={T_|25-;uc5o%32d^(YIiHpxvMWKJuJV5XF{xFx<^4jtm_B$4j*~3TF z_LR;EHszxuc$g}g2A0aL#6e4MMj6ZfnU&Bd$<3yCJMOjR-Nl}5T(aomR?f?k0`&Cy zdgC8?YbQB}f`F54#-#ICSY0rgY|n|2(#aHO=bRuJU{yZnu%ArWre!Qb68q6(>d=iB zXE(DL_nnbzwnoUwyxH6L8yiEX>K?1Nq-Sw}S~$_tBh8rgHlz9{jrHZIIqDd-tbTD+ z*u*>wg$=Z>vj0+wJa#1NU!#a)uu_+OKGGbiOgRhr_3$Qj6A?PNi!P2w;SMSZQf^#V z3Cu8j(CJfLX-49tkPMzJaIilXc&&<3f@>hDfR}k++4$FZB*kv`eLMG^PDlx3CI~y* ziQ40JhZ$J5$#43HQj?6eh<*viEz^WuaoQ!haKx7$T|?UIcHF0qru{G#xudXSzl&?p zH#b;2ZCYTwkuTaNeDcojI&o!TBI14qqTQ;exS^A5G+866>uu~wG@yiAXzNQmu89f$AXP%x*mJ8vr(l`bRT zyYXUO<}Fi-lMgB9N9|VzmYKs|Ig?~>T6vbfRi!WMBfC--Tq1F~THUn?lVD$>S9t2^ z{c9{+kJW?kVf0k8>yq_m)aY==m)g!QZ&5~|I%q#Pfs$9yS2${~b#VVA=}jxRiqzFn z(=8`|1J}Y`ZE-Oui%fnw@uF);L5>V0=dR{IFtg*oKADOud<0F&y`Wx?Ad4+D30}rK zGPzv%lEIngP}}J5;+~`_x+|N((y*fVsC;Jk4``8(tcXFg z9cyjIG-YT3M^v1~m(R5sp%HCUy=lBro?Z1GnyS`8!<&!9=TLtsx%oYhyAPN z&GdE!IMU61(zZ_8(fN;#+S7vgR3C@ILZKb?A0=J4V*dIgL2&nXnb`#}MRsps;Uw%Y n^HI!?E8IbR;J;5bBW|!h-1;TZwP?l#U=CG9ErnWntFZqABL6q_ From d672e250977a7203b7fee288432764011de1b1d5 Mon Sep 17 00:00:00 2001 From: Odei Maiz <33152403+odeimaiz@users.noreply.github.com> Date: Mon, 25 Nov 2019 00:19:05 -0800 Subject: [PATCH 5/7] Fix minor error when uploading file through frontend (#1179) "presignedLink" data Event added force tree reload when uploading file --- services/web/client/source/class/osparc/file/FilesTree.js | 6 +++--- services/web/client/source/class/osparc/store/Data.js | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/services/web/client/source/class/osparc/file/FilesTree.js b/services/web/client/source/class/osparc/file/FilesTree.js index e00e7666173..afba3bf894d 100644 --- a/services/web/client/source/class/osparc/file/FilesTree.js +++ b/services/web/client/source/class/osparc/file/FilesTree.js @@ -136,10 +136,10 @@ qx.Class.define("osparc.file.FilesTree", { populateTree: function(nodeId = null, locationId = null) { if (nodeId) { this.__populateNodeFiles(nodeId); - } else if (locationId) { - this.__populateMyLocation(locationId); - } else { + } else if (locationId === null) { this.__populateMyData(); + } else { + this.__populateMyLocation(locationId); } this.getDelegate().configureItem = item => { diff --git a/services/web/client/source/class/osparc/store/Data.js b/services/web/client/source/class/osparc/store/Data.js index c9a5b181bb3..61f11bc9129 100644 --- a/services/web/client/source/class/osparc/store/Data.js +++ b/services/web/client/source/class/osparc/store/Data.js @@ -50,7 +50,8 @@ qx.Class.define("osparc.store.Data", { "myDocuments": "qx.event.type.Data", "nodeFiles": "qx.event.type.Data", "fileCopied": "qx.event.type.Data", - "deleteFile": "qx.event.type.Data" + "deleteFile": "qx.event.type.Data", + "presignedLink": "qx.event.type.Data" }, members: { From 771da8e18012bbf35806c60aa3ea939697407dc3 Mon Sep 17 00:00:00 2001 From: Pedro Crespo <32402063+pcrespov@users.noreply.github.com> Date: Mon, 25 Nov 2019 10:31:52 +0100 Subject: [PATCH 6/7] Is1178/logs format (#1181) Fixes #1178: changes logger formatters Format complies with osparc-ops logspout multiline regex * sidecar scripts logs * storage scripts logs * webserver scripts logs * director scripts logs * director: adds exec for graceful shutdown --- services/director/docker/boot.sh | 17 ++++---- services/director/docker/entrypoint.sh | 14 ++++--- services/sidecar/docker/boot.sh | 10 +++-- services/sidecar/docker/entrypoint.sh | 17 ++++---- services/sidecar/src/sidecar/celery.py | 13 ++----- .../sidecar/src/sidecar/celery_log_setup.py | 39 +++++++++++++++++++ services/storage/docker/boot.sh | 25 ++++++------ services/storage/docker/entrypoint.sh | 13 ++++--- services/web/server/docker/boot.sh | 22 ++++++----- services/web/server/docker/entrypoint.sh | 7 +++- 10 files changed, 116 insertions(+), 61 deletions(-) create mode 100644 services/sidecar/src/sidecar/celery_log_setup.py diff --git a/services/director/docker/boot.sh b/services/director/docker/boot.sh index ec22a37467c..b57aae24f65 100755 --- a/services/director/docker/boot.sh +++ b/services/director/docker/boot.sh @@ -1,15 +1,17 @@ #!/bin/sh # +INFO="INFO: [`basename "$0"`] " +ERROR="ERROR: [`basename "$0"`] " # BOOTING application --------------------------------------------- -echo "Booting in ${SC_BOOT_MODE} mode ..." +echo $INFO "Booting in ${SC_BOOT_MODE} mode ..." echo " User :`id $(whoami)`" echo " Workdir :`pwd`" LOG_LEVEL=info if [[ ${SC_BUILD_TARGET} == "development" ]] then - echo " Environment :" + echo $INFO "Environment :" printenv | sed 's/=/: /' | sed 's/^/ /' | sort #-------------------- @@ -20,10 +22,10 @@ then cd /devel #-------------------- - echo " Python :" + echo $INFO "Python :" python --version | sed 's/^/ /' which python | sed 's/^/ /' - echo " PIP :" + echo $INFO "PIP :" $SC_PIP list | sed 's/^/ /' fi @@ -31,8 +33,9 @@ fi if [[ ${SC_BOOT_MODE} == "debug-ptvsd" ]] then echo - echo "PTVSD Debugger initializing in port 3004" - python3 -m ptvsd --host 0.0.0.0 --port 3000 -m simcore_service_director --loglevel=$LOG_LEVEL + echo $INFO "PTVSD Debugger initializing in port 3004" + python3 -m ptvsd --host 0.0.0.0 --port 3000 -m \ + simcore_service_director --loglevel=$LOG_LEVEL else - simcore-service-director --loglevel=$LOG_LEVEL + exec simcore-service-director --loglevel=$LOG_LEVEL fi diff --git a/services/director/docker/entrypoint.sh b/services/director/docker/entrypoint.sh index b515f26b678..223ac9ef329 100755 --- a/services/director/docker/entrypoint.sh +++ b/services/director/docker/entrypoint.sh @@ -1,4 +1,7 @@ #!/bin/sh +# +INFO="INFO: [`basename "$0"`] " +ERROR="ERROR: [`basename "$0"`] " # This entrypoint script: # @@ -6,19 +9,18 @@ # - Notice that the container *starts* as --user [default root] but # *runs* as non-root user [scu] # -echo "Entrypoint for stage ${SC_BUILD_TARGET} ..." -echo " User :`id $(whoami)`" -echo " Workdir :`pwd`" +echo $INFO "Entrypoint for stage ${SC_BUILD_TARGET} ..." +echo $INFO "User :`id $(whoami)`" +echo $INFO "Workdir :`pwd`" if [[ ${SC_BUILD_TARGET} == "development" ]] then - # NOTE: expects docker run ... -v $(pwd):/devel/services/director DEVEL_MOUNT=/devel/services/director stat $DEVEL_MOUNT &> /dev/null || \ - (echo "ERROR: You must mount '$DEVEL_MOUNT' to deduce user and group ids" && exit 1) # FIXME: exit does not stop script + (echo $ERROR "You must mount '$DEVEL_MOUNT' to deduce user and group ids" && exit 1) # FIXME: exit does not stop script USERID=$(stat -c %u $DEVEL_MOUNT) GROUPID=$(stat -c %g $DEVEL_MOUNT) @@ -67,4 +69,4 @@ then addgroup scu $GROUPNAME fi -su-exec scu "$@" +exec su-exec scu "$@" diff --git a/services/sidecar/docker/boot.sh b/services/sidecar/docker/boot.sh index 598d9ff9d68..0eef90f9424 100755 --- a/services/sidecar/docker/boot.sh +++ b/services/sidecar/docker/boot.sh @@ -1,14 +1,16 @@ #!/bin/sh # +INFO="INFO: [`basename "$0"`] " +ERROR="ERROR: [`basename "$0"`] " # BOOTING application --------------------------------------------- -echo "Booting in ${SC_BOOT_MODE} mode ..." +echo $INFO "Booting in ${SC_BOOT_MODE} mode ..." echo " User :`id $(whoami)`" echo " Workdir :`pwd`" if [[ ${SC_BUILD_TARGET} == "development" ]] then - echo " Environment :" + echo $INFO "Environment :" printenv | sed 's/=/: /' | sed 's/^/ /' | sort #-------------------- @@ -18,10 +20,10 @@ then DEBUG_LEVEL=debug #-------------------- - echo " Python :" + echo $INFO "Python :" python --version | sed 's/^/ /' which python | sed 's/^/ /' - echo " PIP :" + echo $INFO "PIP :" $SC_PIP list | sed 's/^/ /' diff --git a/services/sidecar/docker/entrypoint.sh b/services/sidecar/docker/entrypoint.sh index e18f5b1e859..0002104f7e1 100755 --- a/services/sidecar/docker/entrypoint.sh +++ b/services/sidecar/docker/entrypoint.sh @@ -1,4 +1,7 @@ #!/bin/sh +# +INFO="INFO: [`basename "$0"`] " +ERROR="ERROR: [`basename "$0"`] " # This entrypoint script: # @@ -6,7 +9,7 @@ # - Notice that the container *starts* as --user [default root] but # *runs* as non-root user [scu] # -echo "Entrypoint for stage ${SC_BUILD_TARGET} ..." +echo $INFO "Entrypoint for stage ${SC_BUILD_TARGET} ..." echo " User :`id $(whoami)`" echo " Workdir :`pwd`" echo " scuUser :`id scu`" @@ -17,12 +20,12 @@ GROUPNAME=scu if [[ ${SC_BUILD_TARGET} == "development" ]] then - echo "development mode detected..." + echo $INFO "development mode detected..." # NOTE: expects docker run ... -v $(pwd):/devel/services/sidecar DEVEL_MOUNT=/devel/services/sidecar stat $DEVEL_MOUNT &> /dev/null || \ - (echo "ERROR: You must mount '$DEVEL_MOUNT' to deduce user and group ids" && exit 1) # FIXME: exit does not stop script + (echo $ERROR "You must mount '$DEVEL_MOUNT' to deduce user and group ids" && exit 1) # FIXME: exit does not stop script USERID=$(stat -c %u $DEVEL_MOUNT) GROUPID=$(stat -c %g $DEVEL_MOUNT) @@ -30,17 +33,17 @@ then if [[ $USERID -eq 0 ]] then - echo "mounted folder from root, adding scu to root..." + echo $INFO "mounted folder from root, adding scu to root..." addgroup scu root else # take host's credentials in scu if [[ -z "$GROUPNAME" ]] then - echo "mounted folder from $USERID, creating new group..." + echo $INFO "mounted folder from $USERID, creating new group..." GROUPNAME=host_group addgroup -g $GROUPID $GROUPNAME else - echo "mounted folder from $USERID, adding scu to $GROUPNAME..." + echo $INFO "mounted folder from $USERID, adding scu to $GROUPNAME..." addgroup scu $GROUPNAME fi @@ -69,7 +72,7 @@ then addgroup scu $GROUPNAME fi -echo "Starting boot ..." +echo $INFO "Starting boot ..." chown -R $USERNAME:$GROUPNAME /home/scu/input chown -R $USERNAME:$GROUPNAME /home/scu/output chown -R $USERNAME:$GROUPNAME /home/scu/log diff --git a/services/sidecar/src/sidecar/celery.py b/services/sidecar/src/sidecar/celery.py index 54b364f4b9b..9a0287a1264 100644 --- a/services/sidecar/src/sidecar/celery.py +++ b/services/sidecar/src/sidecar/celery.py @@ -1,27 +1,20 @@ -import logging - from celery import Celery -from celery.utils.log import get_task_logger from simcore_sdk.config.rabbit import Config as RabbitConfig -# TODO: configure via command line or config file. Add in config.yaml -logging.basicConfig(level=logging.DEBUG) +from .celery_log_setup import get_task_logger log = get_task_logger(__name__) -log.setLevel(logging.DEBUG) - rabbit_config = RabbitConfig() +log.info("Inititalizing celery app ...") + # TODO: make it a singleton? app= Celery(rabbit_config.name, broker=rabbit_config.broker, backend=rabbit_config.backend) - - - __all__ = [ "rabbit_config", "app" diff --git a/services/sidecar/src/sidecar/celery_log_setup.py b/services/sidecar/src/sidecar/celery_log_setup.py new file mode 100644 index 00000000000..98b22339e62 --- /dev/null +++ b/services/sidecar/src/sidecar/celery_log_setup.py @@ -0,0 +1,39 @@ +""" setup logging formatters to fit logspout's multiline pattern "^(ERROR|WARNING|INFO|DEBUG|CRITICAL)[:]" + + NOTE: import to connect signals! + + SEE https://github.com/ITISFoundation/osparc-ops/blob/master/services/graylog/docker-compose.yml#L113 +""" +# NOTES: +# https://docs.celeryproject.org/en/latest/userguide/signals.html#setup-logging +# https://www.distributedpython.com/2018/08/28/celery-logging/ +# https://www.distributedpython.com/2018/11/06/celery-task-logger-format/ + +import logging + +from celery.app.log import TaskFormatter +from celery.signals import after_setup_logger, after_setup_task_logger +from celery.utils.log import get_task_logger + +@after_setup_logger.connect +def setup_loggers(logger, *_args, **_kwargs): + """ Customizes global loggers """ + for handler in logger.handlers: + handler.setFormatter(logging.Formatter('%(levelname)s: [%(asctime)s/%(processName)s] %(message)s')) + + +@after_setup_task_logger.connect +def setup_task_logger(logger, *_args, **_kwargs): + """ Customizes task loggers """ + for handler in logger.handlers: + handler.setFormatter(TaskFormatter('%(levelname)s: [%(asctime)s/%(processName)s][%(task_name)s(%(task_id)s)] %(message)s')) + + +# TODO: configure via command line or config file. Add in config.yaml +logging.basicConfig(level=logging.DEBUG) +log = get_task_logger(__name__) +log.info("Setting up loggers") + +__all__ = [ + 'get_task_logger' +] diff --git a/services/storage/docker/boot.sh b/services/storage/docker/boot.sh index 30feaa62afd..96bd6ce3a8e 100755 --- a/services/storage/docker/boot.sh +++ b/services/storage/docker/boot.sh @@ -1,15 +1,17 @@ #!/bin/sh # +INFO="INFO: [`basename "$0"`] " +ERROR="ERROR: [`basename "$0"`] " # BOOTING application --------------------------------------------- -echo "Booting in ${SC_BOOT_MODE} mode ..." +echo $INFO "Booting in ${SC_BOOT_MODE} mode ..." if [[ ${SC_BUILD_TARGET} == "development" ]] then - echo " User :`id $(whoami)`" - echo " Workdir :`pwd`" - echo " Environment :" + echo $INFO "User :`id $(whoami)`" + echo $INFO "Workdir :`pwd`" + echo $INFO "Environment :" printenv | sed 's/=/: /' | sed 's/^/ /' | sort #-------------------- @@ -20,10 +22,10 @@ then cd /devel #-------------------- - echo " Python :" + echo $INFO "Python :" python --version | sed 's/^/ /' which python | sed 's/^/ /' - echo " PIP :" + echo $INFO "PIP :" $SC_PIP list | sed 's/^/ /' #------------ @@ -41,14 +43,15 @@ fi if [[ ${SC_BOOT_MODE} == "debug-pdb" ]] then # NOTE: needs stdin_open: true and tty: true - echo "Debugger attached: https://docs.python.org/3.6/library/pdb.html#debugger-commands ..." - echo "Running: import pdb, simcore_service_storage.cli; pdb.run('simcore_service_storage.cli.main([\'-c\',\'${APP_CONFIG}\'])')" - eval "$entrypoint" python -c "import pdb, simcore_service_storage.cli; \ + echo $INFO "Debugger attached: https://docs.python.org/3.6/library/pdb.html#debugger-commands ..." + echo $INFO "Running: import pdb, simcore_service_storage.cli; pdb.run('simcore_service_storage.cli.main([\'-c\',\'${APP_CONFIG}\'])')" + eval $INFO "$entrypoint" python -c "import pdb, simcore_service_storage.cli; \ pdb.run('simcore_service_storage.cli.main([\'-c\',\'${APP_CONFIG}\'])')" elif [[ ${SC_BOOT_MODE} == "debug-ptvsd" ]] then - echo "PTVSD Debugger initializing in port 3003 with ${APP_CONFIG}" - eval "$entrypoint" python3 -m ptvsd --host 0.0.0.0 --port 3000 -m simcore_service_storage --config $APP_CONFIG + echo $INFO "PTVSD Debugger initializing in port 3003 with ${APP_CONFIG}" + eval "$entrypoint" python3 -m ptvsd --host 0.0.0.0 --port 3000 -m \ + simcore_service_storage --config $APP_CONFIG else exec simcore-service-storage --config $APP_CONFIG fi diff --git a/services/storage/docker/entrypoint.sh b/services/storage/docker/entrypoint.sh index eb604f21bfe..b042885f16b 100755 --- a/services/storage/docker/entrypoint.sh +++ b/services/storage/docker/entrypoint.sh @@ -1,4 +1,7 @@ #!/bin/sh +# +INFO="INFO: [`basename "$0"`] " +ERROR="ERROR: [`basename "$0"`] " # This entrypoint script: # @@ -6,13 +9,13 @@ # - Notice that the container *starts* as --user [default root] but # *runs* as non-root user [scu] # -echo "Entrypoint for stage ${SC_BUILD_TARGET} ..." +echo $INFO "Entrypoint for stage ${SC_BUILD_TARGET} ..." echo " User :`id $(whoami)`" echo " Workdir :`pwd`" -echo "updating certificates..." +echo $INFO "updating certificates..." update-ca-certificates -echo "certificates updated" +echo $INFO "certificates updated" if [[ ${SC_BUILD_TARGET} == "development" ]] then @@ -20,7 +23,7 @@ then DEVEL_MOUNT=/devel/services/storage stat $DEVEL_MOUNT &> /dev/null || \ - (echo "ERROR: You must mount '$DEVEL_MOUNT' to deduce user and group ids" && exit 1) # FIXME: exit does not stop script + (echo $ERROR "You must mount '$DEVEL_MOUNT' to deduce user and group ids" && exit 1) # FIXME: exit does not stop script USERID=$(stat -c %u $DEVEL_MOUNT) GROUPID=$(stat -c %g $DEVEL_MOUNT) @@ -50,5 +53,5 @@ then python3 -m pip install ptvsd fi -echo "Starting boot ..." +echo $INFO "Starting boot ..." exec su-exec scu "$@" diff --git a/services/web/server/docker/boot.sh b/services/web/server/docker/boot.sh index 8119bcc35b3..a29565bcdec 100755 --- a/services/web/server/docker/boot.sh +++ b/services/web/server/docker/boot.sh @@ -1,14 +1,17 @@ #!/bin/sh # +INFO="INFO: [`basename "$0"`] " +ERROR="ERROR: [`basename "$0"`] " + # BOOTING application --------------------------------------------- -echo "Booting in ${SC_BOOT_MODE} mode ..." +echo $INFO "Booting in ${SC_BOOT_MODE} mode ..." echo " User :`id $(whoami)`" echo " Workdir :`pwd`" if [[ ${SC_BUILD_TARGET} == "development" ]] then - echo " Environment :" + echo $INFO "Environment :" printenv | sed 's/=/: /' | sed 's/^/ /' | sort #------------ @@ -19,14 +22,14 @@ then cd /devel #------------ - echo " Python :" + echo $INFO "Python :" python --version | sed 's/^/ /' which python | sed 's/^/ /' - echo " PIP :" + echo $INFO "PIP :" $SC_PIP list | sed 's/^/ /' #------------ - echo " setting entrypoint to use watchmedo autorestart..." + echo $INFO "setting entrypoint to use watchmedo autorestart..." entrypoint='watchmedo auto-restart --recursive --pattern="*.py" --' elif [[ ${SC_BUILD_TARGET} == "production" ]] @@ -40,15 +43,16 @@ fi if [[ ${SC_BOOT_MODE} == "debug-pdb" ]] then # NOTE: needs stdin_open: true and tty: true - echo "Debugger attached: https://docs.python.org/3.6/library/pdb.html#debugger-commands ..." - echo "Running: import pdb, simcore_service_server.cli; pdb.run('simcore_service_server.cli.main([\'-c\',\'${APP_CONFIG}\'])')" + echo $INFO "Debugger attached: https://docs.python.org/3.6/library/pdb.html#debugger-commands ..." + echo $INFO "Running: import pdb, simcore_service_server.cli; pdb.run('simcore_service_server.cli.main([\'-c\',\'${APP_CONFIG}\'])')" eval "$entrypoint" python -c "import pdb, simcore_service_server.cli; \ pdb.run('simcore_service_server.cli.main([\'-c\',\'${APP_CONFIG}\'])')" elif [[ ${SC_BOOT_MODE} == "debug-ptvsd" ]] then # NOTE: needs ptvsd installed - echo "PTVSD Debugger initializing in port 3000 with ${APP_CONFIG}" - eval "$entrypoint" python3 -m ptvsd --host 0.0.0.0 --port 3000 -m simcore_service_webserver --config $APP_CONFIG + echo $INFO "PTVSD Debugger initializing in port 3000 with ${APP_CONFIG}" + eval "$entrypoint" python3 -m ptvsd --host 0.0.0.0 --port 3000 -m \ + simcore_service_webserver --config $APP_CONFIG else exec simcore-service-webserver --config $APP_CONFIG fi diff --git a/services/web/server/docker/entrypoint.sh b/services/web/server/docker/entrypoint.sh index 9ac1c84b89b..2b5dee218b5 100755 --- a/services/web/server/docker/entrypoint.sh +++ b/services/web/server/docker/entrypoint.sh @@ -1,4 +1,7 @@ #!/bin/sh +# +INFO="INFO: [`basename "$0"`] " +ERROR="ERROR: [`basename "$0"`] " # This entrypoint script: # @@ -6,7 +9,7 @@ # - Notice that the container *starts* as --user [default root] but # *runs* as non-root user [scu] # -echo "Entrypoint for stage ${SC_BUILD_TARGET} ..." +echo $INFO "Entrypoint for stage ${SC_BUILD_TARGET} ..." echo " User :`id $(whoami)`" echo " Workdir :`pwd`" @@ -17,7 +20,7 @@ then DEVEL_MOUNT=/devel/services/web/server stat $DEVEL_MOUNT &> /dev/null || \ - (echo "ERROR: You must mount '$DEVEL_MOUNT' to deduce user and group ids" && exit 1) # FIXME: exit does not stop script + (echo $ERROR "You must mount '$DEVEL_MOUNT' to deduce user and group ids" && exit 1) # FIXME: exit does not stop script USERID=$(stat -c %u $DEVEL_MOUNT) GROUPID=$(stat -c %g $DEVEL_MOUNT) From b09308d96dee8dfe61ad807b235f499485194095 Mon Sep 17 00:00:00 2001 From: Pedro Crespo <32402063+pcrespov@users.noreply.github.com> Date: Mon, 25 Nov 2019 14:41:22 +0100 Subject: [PATCH 7/7] Fixes rabbitmq config in server (#1183) * Fixes rabbitmq config in server * Renamed RABBITMQ_ by RABBIT_ Variables needs to be prefixed with service name in docker-compose * Updates utils to find published port Ensures environ variable exist before replacing them * Fixes auth of rabbit --- .env-devel | 9 +++--- .../src/simcore_sdk/config/rabbit.py | 28 +++++++++---------- services/docker-compose.yml | 12 ++++---- services/sidecar/tests/conftest.py | 8 +++--- services/sidecar/tests/docker-compose.yml | 4 +-- .../config/server-defaults.yaml | 7 +++-- .../config/server-docker-dev.yaml | 8 +++--- .../config/server-docker-prod.yaml | 10 ++++--- .../web/server/tests/helpers/utils_docker.py | 22 ++++++++++++--- .../computation/test_computation.py | 2 +- .../web/server/tests/integration/conftest.py | 12 ++++++-- 11 files changed, 73 insertions(+), 49 deletions(-) diff --git a/.env-devel b/.env-devel index ab67f2d4002..587cb23b55e 100644 --- a/.env-devel +++ b/.env-devel @@ -13,13 +13,12 @@ POSTGRES_PASSWORD=adminadmin POSTGRES_PORT=5432 POSTGRES_USER=scu -RABBITMQ_USER=admin -RABBITMQ_LOG_CHANNEL=comp.backend.channels.log -RABBITMQ_PASSWORD=adminadmin -RABBITMQ_PROGRESS_CHANNEL=comp.backend.channels.progress - RABBIT_HOST=rabbit +RABBIT_LOG_CHANNEL=comp.backend.channels.log +RABBIT_PASSWORD=adminadmin RABBIT_PORT=5672 +RABBIT_PROGRESS_CHANNEL=comp.backend.channels.progress +RABBIT_USER=admin REGISTRY_AUTH=True REGISTRY_PW=adminadmin diff --git a/packages/simcore-sdk/src/simcore_sdk/config/rabbit.py b/packages/simcore-sdk/src/simcore_sdk/config/rabbit.py index be39d71bd85..419e672fa8c 100644 --- a/packages/simcore-sdk/src/simcore_sdk/config/rabbit.py +++ b/packages/simcore-sdk/src/simcore_sdk/config/rabbit.py @@ -10,7 +10,7 @@ # TODO: adapt all data below! -# TODO: can use venv as defaults? e.g. $RABBITMQ_LOG_CHANNEL +# TODO: can use venv as defaults? e.g. $RABBIT_LOG_CHANNEL CONFIG_SCHEMA = T.Dict({ T.Key("name", default="tasks", optional=True): T.String(), T.Key("enabled", default=True, optional=True): T.Bool(), @@ -75,12 +75,12 @@ def __init__(self, config=None): else: config = {} - RABBITMQ_USER = env.get('RABBITMQ_USER','simcore') - RABBITMQ_PASSWORD = env.get('RABBITMQ_PASSWORD','simcore') - RABBITMQ_HOST=env.get('RABBITMQ_HOST','rabbit') - RABBITMQ_PORT=int(env.get('RABBITMQ_PORT', 5672)) - RABBITMQ_LOG_CHANNEL = env.get('RABBITMQ_LOG_CHANNEL','comp.backend.channels.log') - RABBITMQ_PROGRESS_CHANNEL = env.get('RABBITMQ_PROGRESS_CHANNEL','comp.backend.channels.progress') + RABBIT_USER = env.get('RABBIT_USER','simcore') + RABBIT_PASSWORD = env.get('RABBIT_PASSWORD','simcore') + RABBIT_HOST=env.get('RABBIT_HOST','rabbit') + RABBIT_PORT=int(env.get('RABBIT_PORT', 5672)) + RABBIT_LOG_CHANNEL = env.get('RABBIT_LOG_CHANNEL','comp.backend.channels.log') + RABBIT_PROGRESS_CHANNEL = env.get('RABBIT_PROGRESS_CHANNEL','comp.backend.channels.progress') CELERY_RESULT_BACKEND=env.get('CELERY_RESULT_BACKEND','rpc://') # FIXME: get variables via config.get('') or # rabbit @@ -88,23 +88,23 @@ def __init__(self, config=None): try: self._broker_url = eval_broker(config) except: # pylint: disable=W0702 - self._broker_url = 'amqp://{user}:{pw}@{url}:{port}'.format(user=RABBITMQ_USER, pw=RABBITMQ_PASSWORD, url=RABBITMQ_HOST, port=RABBITMQ_PORT) + self._broker_url = 'amqp://{user}:{pw}@{url}:{port}'.format(user=RABBIT_USER, pw=RABBIT_PASSWORD, url=RABBIT_HOST, port=RABBIT_PORT) self._result_backend = config.get("celery", {}).get("result_backend") or CELERY_RESULT_BACKEND self._module_name = config.get("name") or "tasks" # pika self._pika_credentials = pika.PlainCredentials( - config.get("user") or RABBITMQ_USER, - config.get("password") or RABBITMQ_PASSWORD) + config.get("user") or RABBIT_USER, + config.get("password") or RABBIT_PASSWORD) self._pika_parameters = pika.ConnectionParameters( - host=config.get("host") or RABBITMQ_HOST, - port=config.get("port") or RABBITMQ_PORT, + host=config.get("host") or RABBIT_HOST, + port=config.get("port") or RABBIT_PORT, credentials=self._pika_credentials, connection_attempts=100) - self._log_channel = config.get("celery", {}).get("result_backend") or RABBITMQ_LOG_CHANNEL - self._progress_channel = config.get("celery", {}).get("result_backend") or RABBITMQ_PROGRESS_CHANNEL + self._log_channel = config.get("celery", {}).get("result_backend") or RABBIT_LOG_CHANNEL + self._progress_channel = config.get("celery", {}).get("result_backend") or RABBIT_PROGRESS_CHANNEL @property def parameters(self): diff --git a/services/docker-compose.yml b/services/docker-compose.yml index 69978b5de28..0722aaa97aa 100644 --- a/services/docker-compose.yml +++ b/services/docker-compose.yml @@ -87,10 +87,10 @@ services: - log:/home/scu/log - /var/run/docker.sock:/var/run/docker.sock environment: - - RABBITMQ_USER=${RABBITMQ_USER} - - RABBITMQ_PASSWORD=${RABBITMQ_PASSWORD} - - RABBITMQ_LOG_CHANNEL=${RABBITMQ_LOG_CHANNEL} - - RABBITMQ_PROGRESS_CHANNEL=${RABBITMQ_PROGRESS_CHANNEL} + - RABBIT_USER=${RABBIT_USER} + - RABBIT_PASSWORD=${RABBIT_PASSWORD} + - RABBIT_LOG_CHANNEL=${RABBIT_LOG_CHANNEL} + - RABBIT_PROGRESS_CHANNEL=${RABBIT_PROGRESS_CHANNEL} - POSTGRES_ENDPOINT=${POSTGRES_ENDPOINT} - POSTGRES_USER=${POSTGRES_USER} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} @@ -149,8 +149,8 @@ services: image: itisfoundation/rabbitmq:3.8.0-management init: true environment: - - RABBITMQ_DEFAULT_USER=${RABBITMQ_USER} - - RABBITMQ_DEFAULT_PASS=${RABBITMQ_PASSWORD} + - RABBITMQ_DEFAULT_USER=${RABBIT_USER} + - RABBITMQ_DEFAULT_PASS=${RABBIT_PASSWORD} networks: - default - computational_services_subnet diff --git a/services/sidecar/tests/conftest.py b/services/sidecar/tests/conftest.py index 9a2b6f34f8b..3e5b28154b7 100644 --- a/services/sidecar/tests/conftest.py +++ b/services/sidecar/tests/conftest.py @@ -43,8 +43,8 @@ def docker_compose_file(here): os.environ['POSTGRES_ENDPOINT']="FOO" # TODO: update config schema!! os.environ['MINIO_ACCESS_KEY']=ACCESS_KEY os.environ['MINIO_SECRET_KEY']=SECRET_KEY - os.environ['RABBITMQ_USER']=RABBIT_USER - os.environ['RABBITMQ_PASSWORD']=RABBIT_PWD + os.environ['RABBIT_USER']=RABBIT_USER + os.environ['RABBIT_PASSWORD']=RABBIT_PWD dc_path = here / 'docker-compose.yml' @@ -85,8 +85,8 @@ def postgres_service(docker_services, docker_ip): @pytest.fixture(scope='session') def rabbit_service(docker_services, docker_ip): # set env var here that is explicitly used from sidecar - os.environ['RABBITMQ_HOST'] = "{host}".format(host=docker_ip) - os.environ['RABBITMQ_PORT'] = "{port}".format(port=docker_services.port_for('rabbit', 15672)) + os.environ['RABBIT_HOST'] = "{host}".format(host=docker_ip) + os.environ['RABBIT_PORT'] = "{port}".format(port=docker_services.port_for('rabbit', 15672)) rabbit_service = "dummy" return rabbit_service diff --git a/services/sidecar/tests/docker-compose.yml b/services/sidecar/tests/docker-compose.yml index 84378bb3c06..8ed270c4a0d 100644 --- a/services/sidecar/tests/docker-compose.yml +++ b/services/sidecar/tests/docker-compose.yml @@ -27,7 +27,7 @@ services: rabbit: image: rabbitmq:3-management environment: - - RABBITMQ_DEFAULT_USER=${RABBITMQ_USER:-rabbit} - - RABBITMQ_DEFAULT_PASS=${RABBITMQ_PASSWORD:-carrot} + - RABBIT_DEFAULT_USER=${RABBIT_USER:-rabbit} + - RABBIT_DEFAULT_PASS=${RABBIT_PASSWORD:-carrot} ports: - "15672:15672" diff --git a/services/web/server/src/simcore_service_webserver/config/server-defaults.yaml b/services/web/server/src/simcore_service_webserver/config/server-defaults.yaml index 0df5824d719..94a8cfe177e 100644 --- a/services/web/server/src/simcore_service_webserver/config/server-defaults.yaml +++ b/services/web/server/src/simcore_service_webserver/config/server-defaults.yaml @@ -23,11 +23,14 @@ db: port: 5432 user: simcore rabbit: + enabled: True + host: rabbit + port: 5672 + user: simcore + password: simcore channels: log: comp.backend.channels.log progress: comp.backend.channels.progress - password: simcore - user: simcore # s3: # access_key: 'Q3AM3UQ867SPQQA43P2F' # bucket_name: simcore diff --git a/services/web/server/src/simcore_service_webserver/config/server-docker-dev.yaml b/services/web/server/src/simcore_service_webserver/config/server-docker-dev.yaml index b8595431f37..a182fd28fcf 100644 --- a/services/web/server/src/simcore_service_webserver/config/server-docker-dev.yaml +++ b/services/web/server/src/simcore_service_webserver/config/server-docker-dev.yaml @@ -25,11 +25,11 @@ db: rabbit: host: ${RABBIT_HOST} port: ${RABBIT_PORT} - user: ${RABBITMQ_USER} - password: ${RABBITMQ_PASSWORD} + user: ${RABBIT_USER} + password: ${RABBIT_PASSWORD} channels: - progress: ${RABBITMQ_PROGRESS_CHANNEL} - log: ${RABBITMQ_LOG_CHANNEL} + progress: ${RABBIT_PROGRESS_CHANNEL} + log: ${RABBIT_LOG_CHANNEL} # s3: # endpoint: ${S3_ENDPOINT} # access_key: ${S3_ACCESS_KEY} diff --git a/services/web/server/src/simcore_service_webserver/config/server-docker-prod.yaml b/services/web/server/src/simcore_service_webserver/config/server-docker-prod.yaml index 39b07b11720..838103380a3 100644 --- a/services/web/server/src/simcore_service_webserver/config/server-docker-prod.yaml +++ b/services/web/server/src/simcore_service_webserver/config/server-docker-prod.yaml @@ -23,11 +23,13 @@ db: host: ${POSTGRES_HOST} port: ${POSTGRES_PORT} rabbit: - user: ${RABBITMQ_USER} - password: ${RABBITMQ_PASSWORD} + host: ${RABBIT_HOST} + port: ${RABBIT_PORT} + user: ${RABBIT_USER} + password: ${RABBIT_PASSWORD} channels: - progress: ${RABBITMQ_PROGRESS_CHANNEL} - log: ${RABBITMQ_LOG_CHANNEL} + progress: ${RABBIT_PROGRESS_CHANNEL} + log: ${RABBIT_LOG_CHANNEL} # s3: # endpoint: ${S3_ENDPOINT} # access_key: ${S3_ACCESS_KEY} diff --git a/services/web/server/tests/helpers/utils_docker.py b/services/web/server/tests/helpers/utils_docker.py index b0988a6a24e..2b0bcb4de33 100644 --- a/services/web/server/tests/helpers/utils_docker.py +++ b/services/web/server/tests/helpers/utils_docker.py @@ -16,7 +16,7 @@ wait=wait_fixed(2), stop=stop_after_attempt(10), after=after_log(log, logging.WARN)) -def get_service_published_port(service_name: str) -> str: +def get_service_published_port(service_name: str, target_port: Optional[int]=None) -> str: """ WARNING: ENSURE that service name exposes a port in Dockerfile file or docker-compose config file """ @@ -31,10 +31,24 @@ def get_service_published_port(service_name: str) -> str: if not service_ports: raise RuntimeError(f"Cannot find published port for service '{service_name}' in endpoint. Probably services still not started.") - if len(service_ports)>1: - log.warning("Multiple porst published in service '%s'. Defaulting to first from %s", service_name, service_ports) + published_port = None + msg = ", ".join( f"{p.get('TargetPort')} -> {p.get('PublishedPort')}" for p in service_ports ) + + if target_port is None: + if len(service_ports)>1: + log.warning("Multiple ports published in service '%s': %s. Defaulting to first", service_name, msg) + published_port = service_ports[0]["PublishedPort"] + + else: + target_port = int(target_port) + for p in service_ports: + if p['TargetPort'] == target_port: + published_port = p['PublishedPort'] + break + + if published_port is None: + raise RuntimeError(f"Cannot find published port for {target_port}. Got {msg}") - published_port = service_ports[0]["PublishedPort"] return str(published_port) diff --git a/services/web/server/tests/integration/computation/test_computation.py b/services/web/server/tests/integration/computation/test_computation.py index 7ee04527fc5..193e599b6a4 100644 --- a/services/web/server/tests/integration/computation/test_computation.py +++ b/services/web/server/tests/integration/computation/test_computation.py @@ -47,7 +47,7 @@ ] ops_services = [ - 'minio' + 'minio', # 'adminer', # 'portainer' ] diff --git a/services/web/server/tests/integration/conftest.py b/services/web/server/tests/integration/conftest.py index 66e22f878ff..7a69e9e8a28 100644 --- a/services/web/server/tests/integration/conftest.py +++ b/services/web/server/tests/integration/conftest.py @@ -74,12 +74,18 @@ def webserver_environ(request, docker_stack: Dict, simcore_docker_compose: Dict) if 'ports' in simcore_docker_compose['services'][name] ] for name in services_with_published_ports: + + host_key = f'{name.upper()}_HOST' + port_key = f'{name.upper()}_PORT' + # published port is sometimes dynamically defined by the swarm - published_port = get_service_published_port(name) + assert host_key in environ, "Variables names expected to be prefix with service names in docker-compose" + assert port_key in environ - environ['%s_HOST' % name.upper()] = '127.0.0.1' - environ['%s_PORT' % name.upper()] = published_port # to swarm boundary since webserver is installed in the host and therefore outside the swarm's network + published_port = get_service_published_port(name, int(environ.get(port_key))) + environ[host_key] = '127.0.0.1' + environ[port_key] = published_port pprint(environ) # NOTE: displayed only if error return environ