From 81de145dc656b13b6d17b9740dc722ffd5cc38ac Mon Sep 17 00:00:00 2001 From: Bingchang Chen <19990626.love@163.com> Date: Fri, 11 Jun 2021 12:54:53 +0800 Subject: [PATCH 001/149] fix(deloy): operator ingress tls (#844) --- .../fedlearner-apiserver/templates/deployment.yaml | 1 + .../charts/fedlearner-operator/templates/deployment.yaml | 1 + .../charts/fedlearner-operator/templates/ingress.yaml | 5 +++-- .../fedlearner/charts/fedlearner-operator/values.yaml | 9 ++++++--- .../charts/fedlearner-web-console-v2/values.yaml | 2 +- 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/deploy/charts/fedlearner/charts/fedlearner-apiserver/templates/deployment.yaml b/deploy/charts/fedlearner/charts/fedlearner-apiserver/templates/deployment.yaml index e6135bcc9..bac423ede 100644 --- a/deploy/charts/fedlearner/charts/fedlearner-apiserver/templates/deployment.yaml +++ b/deploy/charts/fedlearner/charts/fedlearner-apiserver/templates/deployment.yaml @@ -31,6 +31,7 @@ spec: name: fedlearner-apiserver ports: - containerPort: 8101 + protocol: TCP resources: {{- toYaml .Values.resources | nindent 12 }} restartPolicy: Always diff --git a/deploy/charts/fedlearner/charts/fedlearner-operator/templates/deployment.yaml b/deploy/charts/fedlearner/charts/fedlearner-operator/templates/deployment.yaml index 02ecf7074..0f3ec910c 100644 --- a/deploy/charts/fedlearner/charts/fedlearner-operator/templates/deployment.yaml +++ b/deploy/charts/fedlearner/charts/fedlearner-operator/templates/deployment.yaml @@ -41,6 +41,7 @@ spec: name: fedlearner-operator ports: - containerPort: 8100 + protocol: TCP resources: {{- toYaml .Values.resources | nindent 12 }} restartPolicy: Always diff --git a/deploy/charts/fedlearner/charts/fedlearner-operator/templates/ingress.yaml b/deploy/charts/fedlearner/charts/fedlearner-operator/templates/ingress.yaml index 0ed2b6584..a5c8005a8 100644 --- a/deploy/charts/fedlearner/charts/fedlearner-operator/templates/ingress.yaml +++ b/deploy/charts/fedlearner/charts/fedlearner-operator/templates/ingress.yaml @@ -1,4 +1,5 @@ {{- if .Values.ingress.enabled -}} +{{- $ingressSuffix := index .Values.extraArgs "ingress-extra-host-suffix" -}} {{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} apiVersion: networking.k8s.io/v1beta1 {{- else -}} @@ -20,13 +21,13 @@ spec: {{- range .Values.ingress.tls }} - hosts: {{- range .hosts }} - - {{ . | quote }} + - {{ printf "%s%s" (. | required "tls host cannot be null") $ingressSuffix | quote }} {{- end }} secretName: {{ .secretName }} {{- end }} {{- end }} rules: - - host: {{ .Values.ingress.host | quote }} + - host: {{ printf "%s%s" (.Values.ingress.host | required "host cannot be null") $ingressSuffix | quote }} http: paths: - backend: diff --git a/deploy/charts/fedlearner/charts/fedlearner-operator/values.yaml b/deploy/charts/fedlearner/charts/fedlearner-operator/values.yaml index 0e3da62a6..07cbef9ae 100644 --- a/deploy/charts/fedlearner/charts/fedlearner-operator/values.yaml +++ b/deploy/charts/fedlearner/charts/fedlearner-operator/values.yaml @@ -6,7 +6,7 @@ replicaCount: 1 image: repository: fedlearner/fedlearner-operator - tag: "v1.5-rc2" + tag: "v2-rc1" pullPolicy: IfNotPresent imagePullSecrets: [] @@ -28,7 +28,10 @@ ingress: annotations: kubernetes.io/ingress.class: nginx nginx.ingress.kubernetes.io/backend-protocol: GRPC - tls: [] - host: default.fedlearner.operator + tls: + - secretName: fedlearner-proxy-server + hosts: + - fedlearner-operator + host: fedlearner-operator installCRD: true diff --git a/deploy/charts/fedlearner/charts/fedlearner-web-console-v2/values.yaml b/deploy/charts/fedlearner/charts/fedlearner-web-console-v2/values.yaml index 9abfa9a10..2adde9483 100644 --- a/deploy/charts/fedlearner/charts/fedlearner-web-console-v2/values.yaml +++ b/deploy/charts/fedlearner/charts/fedlearner-web-console-v2/values.yaml @@ -8,7 +8,7 @@ image: repository: fedlearner/fedlearner-web-console-v2 pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. - tag: "0c1120d" + tag: "v2-rc1" imagePullSecrets: [] nameOverride: "" From fe8f0c217487e4556a50566863296cc02906febd Mon Sep 17 00:00:00 2001 From: Fan Lin <70993766+tjulinfan@users.noreply.github.com> Date: Tue, 15 Jun 2021 16:27:53 +0800 Subject: [PATCH 002/149] [WebConsole] Sync to github. (#845) Co-authored-by: root --- web_console_v2/README.md | 2 +- web_console_v2/api/.gitignore | 2 + web_console_v2/api/command.py | 9 +- web_console_v2/api/config.py | 2 - web_console_v2/api/envs.py | 12 +- .../api/fedlearner_webconsole/app.py | 19 +- .../api/fedlearner_webconsole/auth/apis.py | 50 ++- .../composer/composer.py | 2 +- .../composer/interface.py | 5 +- .../fedlearner_webconsole/composer/runner.py | 20 +- .../composer/runner_cache.py | 6 + .../api/fedlearner_webconsole/dataset/apis.py | 170 +++++---- .../dataset/data_pipeline.py | 185 ++++++++++ .../fedlearner_webconsole/dataset/services.py | 124 +++++++ .../dataset/sparkapp/pipeline/analyzer.py | 92 +++++ .../dataset/sparkapp/pipeline/converter.py | 56 +++ .../dataset/sparkapp/pipeline/transformer.py | 50 +++ .../dataset/sparkapp/pipeline/util.py | 36 ++ .../api/fedlearner_webconsole/db.py | 77 ++++- .../api/fedlearner_webconsole/debug/apis.py | 46 +++ .../api/fedlearner_webconsole/exceptions.py | 11 +- .../api/fedlearner_webconsole/initial_db.py | 38 ++- .../api/fedlearner_webconsole/job/apis.py | 13 +- .../api/fedlearner_webconsole/job/models.py | 65 +++- .../api/fedlearner_webconsole/job/service.py | 22 +- .../api/fedlearner_webconsole/mmgr/apis.py | 61 ++-- .../api/fedlearner_webconsole/mmgr/service.py | 34 +- .../api/fedlearner_webconsole/project/apis.py | 12 +- .../api/fedlearner_webconsole/rpc/client.py | 7 +- .../api/fedlearner_webconsole/rpc/server.py | 19 +- .../scheduler/transaction.py | 8 +- .../setting/{model.py => models.py} | 9 +- .../fedlearner_webconsole/sparkapp/apis.py | 3 +- .../fedlearner_webconsole/sparkapp/schema.py | 36 +- .../fedlearner_webconsole/sparkapp/service.py | 48 +-- .../api/fedlearner_webconsole/utils/base64.py | 24 ++ .../api/fedlearner_webconsole/utils/es.py | 2 +- .../utils/file_manager.py | 169 +++------ .../api/fedlearner_webconsole/utils/hooks.py | 31 ++ .../utils/k8s_watcher.py | 13 +- .../fedlearner_webconsole/workflow/apis.py | 8 +- .../fedlearner_webconsole/workflow/models.py | 63 +++- .../workflow_template/apis.py | 6 +- ...90c1bf67a_add_completed_failed_jobstate.py | 38 +++ ...e30109949aa_add_extra_field_to_workflow.py | 24 ++ .../fedlearner_webconsole/proto/service.proto | 10 + web_console_v2/api/requirements.txt | 2 +- web_console_v2/api/server.py | 2 + web_console_v2/api/test/auth_test.py | 32 +- .../dataset/apis_test.py | 144 ++++++-- .../fedlearner_webconsole/exceptions_test.py | 19 +- .../fedlearner_webconsole/job/service_test.py | 26 +- .../job/yaml_formatter_test.py | 2 +- .../fedlearner_webconsole/mmgr/model_test.py | 113 +++--- .../project/apis_test.py | 4 + .../fedlearner_webconsole/rpc/client_test.py | 22 +- .../scheduler/scheduler_test.py | 7 +- .../scheduler/workflow_commit_test.py | 96 +++--- .../sparkapp/apis_test.py | 3 +- .../sparkapp/schema_test.py | 21 ++ .../sparkapp/service_test.py | 47 ++- .../test_data/dataset_metainfo/_FEATURES | 1 + .../test_data/dataset_metainfo/_HIST | 1 + .../test_data/dataset_metainfo/_META | 1 + .../test_data/sparkapp.tar | Bin 20480 -> 20480 bytes .../utils/base64_test.py | 37 ++ .../utils/file_manager_test.py | 159 ++------- .../workflow/apis_test.py | 322 +++++++++--------- web_console_v2/api/testing/common.py | 60 ++-- .../api/tools/local_runner/app_a.py | 9 +- .../api/tools/local_runner/app_b.py | 6 + .../api/tools/local_runner/run_a.sh | 3 +- .../api/tools/local_runner/run_b.sh | 1 + web_console_v2/client/package.json | 2 +- web_console_v2/client/src/App.tsx | 3 +- .../client/src/assets/images/avatar.jpg | Bin 0 -> 20674 bytes .../client/src/assets/images/avatar.svg | 16 - .../src/assets/images/login-illustration.png | Bin 0 -> 186771 bytes .../src/assets/images/logo-colorful.svg | 22 +- .../src/components/BackButton/index.tsx | 29 ++ .../src/components/BreadcrumbLink/index.tsx | 1 - .../client/src/components/Header/Account.tsx | 47 +-- .../src/components/Header/ProjectSelect.tsx | 98 ++++++ .../client/src/components/Header/index.tsx | 29 +- .../src/components/IconPark/icons/Struct.tsx | 15 + .../client/src/components/IconPark/index.ts | 1 + .../src/components/ListPageLayout/index.tsx | 66 ---- .../src/components/PrettyMenu/index.tsx | 30 ++ .../src/components/SharedPageLayout/index.tsx | 108 ++++++ .../client/src/components/Sidebar/index.tsx | 127 +++++-- .../components/WorkflowJobsCanvas/helpers.ts | 1 + .../src/components/_base/BlockRadio/index.tsx | 94 +++++ .../components/_base/Skeletonize/Skeleton.tsx | 12 - .../components/_base/Skeletonize/index.tsx | 8 - .../client/src/i18n/resources/modules/app.ts | 1 + .../client/src/i18n/resources/modules/menu.ts | 5 +- .../src/i18n/resources/modules/project.ts | 3 +- .../src/i18n/resources/modules/users.ts | 4 + web_console_v2/client/src/services/dataset.ts | 5 +- .../src/services/mocks/v2/auth/users/:id.ts | 2 +- .../mocks/v2/workflows/:id/peer_workflows.ts | 2 +- .../services/mocks/v2/workflows/examples.ts | 2 +- .../client/src/services/workflow.ts | 4 +- web_console_v2/client/src/shared/helpers.ts | 7 + .../client/src/shared/localStorageKeys.ts | 1 + .../client/src/shared/validator.test.ts | 37 ++ web_console_v2/client/src/shared/validator.ts | 13 + web_console_v2/client/src/shared/workflow.ts | 2 +- web_console_v2/client/src/stores/project.ts | 10 + web_console_v2/client/src/styles/_theme.ts | 7 +- .../client/src/styles/_variables.css | 7 +- .../client/src/styles/antd-overrides.less | 3 +- .../client/src/styles/variables.less | 9 +- web_console_v2/client/src/typings/auth.ts | 2 +- web_console_v2/client/src/typings/job.ts | 2 + web_console_v2/client/src/typings/project.ts | 1 + .../src/views/Datasets/DatasetList/index.tsx | 9 +- .../client/src/views/Login/index.tsx | 43 +-- .../views/Projects/CreateProject/index.tsx | 18 +- .../src/views/Projects/EditProject/index.tsx | 30 +- .../ProjectDetailDrawer/DetailBody.tsx | 2 +- .../Projects/ProjectForm/SecondaryForm.tsx | 6 +- .../src/views/Projects/ProjectForm/index.tsx | 3 + .../src/views/Projects/ProjectList/index.tsx | 6 +- .../client/src/views/Settings/index.tsx | 6 +- .../client/src/views/Users/UserForm/index.tsx | 113 +++--- .../client/src/views/Users/UserList/index.tsx | 6 +- .../WorkflowTemplates/TemplateForm/index.tsx | 22 +- .../WorkflowTemplates/TemplateList/index.tsx | 38 +-- .../CreateWorkflow/StepOneBasic/index.tsx | 10 +- .../SteptTwoConfig/InspectPeerConfig.tsx | 84 ----- .../CreateWorkflow/SteptTwoConfig/index.tsx | 4 +- .../views/Workflows/CreateWorkflow/index.tsx | 20 +- .../EditWorkflow/StepOneBasic/index.tsx | 11 +- .../EditWorkflow/SteptTwoConfig/index.tsx | 4 +- .../views/Workflows/EditWorkflow/index.tsx | 20 +- .../views/Workflows/ForkWorkflow/index.tsx | 20 +- .../SteptTwoConfig => }/InspectPeerConfig.tsx | 0 .../src/views/Workflows/WorkflowActions.tsx | 2 +- .../WorkflowDetail/GlobalConfigDrawer.tsx | 120 +++++++ .../views/Workflows/WorkflowDetail/index.tsx | 223 ++++++------ .../views/Workflows/WorkflowList/index.tsx | 34 +- web_console_v2/docker/spark/Dockerfile | 9 +- web_console_v2/docker/spark/requirements.txt | 3 +- web_console_v2/nginx.conf | 5 +- 145 files changed, 3078 insertions(+), 1510 deletions(-) create mode 100644 web_console_v2/api/fedlearner_webconsole/dataset/data_pipeline.py create mode 100644 web_console_v2/api/fedlearner_webconsole/dataset/services.py create mode 100644 web_console_v2/api/fedlearner_webconsole/dataset/sparkapp/pipeline/analyzer.py create mode 100644 web_console_v2/api/fedlearner_webconsole/dataset/sparkapp/pipeline/converter.py create mode 100644 web_console_v2/api/fedlearner_webconsole/dataset/sparkapp/pipeline/transformer.py create mode 100644 web_console_v2/api/fedlearner_webconsole/dataset/sparkapp/pipeline/util.py rename web_console_v2/api/fedlearner_webconsole/setting/{model.py => models.py} (76%) create mode 100644 web_console_v2/api/fedlearner_webconsole/utils/base64.py create mode 100644 web_console_v2/api/fedlearner_webconsole/utils/hooks.py create mode 100644 web_console_v2/api/migrations/versions/b3290c1bf67a_add_completed_failed_jobstate.py create mode 100644 web_console_v2/api/migrations/versions/fe30109949aa_add_extra_field_to_workflow.py create mode 100644 web_console_v2/api/test/fedlearner_webconsole/test_data/dataset_metainfo/_FEATURES create mode 100644 web_console_v2/api/test/fedlearner_webconsole/test_data/dataset_metainfo/_HIST create mode 100644 web_console_v2/api/test/fedlearner_webconsole/test_data/dataset_metainfo/_META create mode 100644 web_console_v2/api/test/fedlearner_webconsole/utils/base64_test.py create mode 100644 web_console_v2/client/src/assets/images/avatar.jpg delete mode 100644 web_console_v2/client/src/assets/images/avatar.svg create mode 100644 web_console_v2/client/src/assets/images/login-illustration.png create mode 100644 web_console_v2/client/src/components/BackButton/index.tsx create mode 100644 web_console_v2/client/src/components/Header/ProjectSelect.tsx create mode 100644 web_console_v2/client/src/components/IconPark/icons/Struct.tsx delete mode 100644 web_console_v2/client/src/components/ListPageLayout/index.tsx create mode 100644 web_console_v2/client/src/components/PrettyMenu/index.tsx create mode 100644 web_console_v2/client/src/components/SharedPageLayout/index.tsx create mode 100644 web_console_v2/client/src/components/_base/BlockRadio/index.tsx delete mode 100644 web_console_v2/client/src/components/_base/Skeletonize/Skeleton.tsx delete mode 100644 web_console_v2/client/src/components/_base/Skeletonize/index.tsx create mode 100644 web_console_v2/client/src/shared/validator.test.ts create mode 100644 web_console_v2/client/src/shared/validator.ts delete mode 100644 web_console_v2/client/src/views/Workflows/CreateWorkflow/SteptTwoConfig/InspectPeerConfig.tsx rename web_console_v2/client/src/views/Workflows/{EditWorkflow/SteptTwoConfig => }/InspectPeerConfig.tsx (100%) create mode 100644 web_console_v2/client/src/views/Workflows/WorkflowDetail/GlobalConfigDrawer.tsx diff --git a/web_console_v2/README.md b/web_console_v2/README.md index 7167f3a66..e91af9910 100644 --- a/web_console_v2/README.md +++ b/web_console_v2/README.md @@ -2,7 +2,7 @@ ### Docker ```shell -docker build -t fedlearner_webconsole_v2 . +docker build --memory 4G --tag fedlearner_webconsole_v2 . docker run --rm -it -p 1989:1989 -p 1990:1990 fedlearner_webconsole_v2 ``` Then visiting http://localhost:1989/ for the UI. diff --git a/web_console_v2/api/.gitignore b/web_console_v2/api/.gitignore index 1b9602cf7..b42ddd478 100644 --- a/web_console_v2/api/.gitignore +++ b/web_console_v2/api/.gitignore @@ -8,3 +8,5 @@ fedlearner_webconsole/proto/*.pyi # Coverage generated .coverage_html_report/ .coverage* + +root.log.* \ No newline at end of file diff --git a/web_console_v2/api/command.py b/web_console_v2/api/command.py index 6acc42f67..ca3fdc337 100644 --- a/web_console_v2/api/command.py +++ b/web_console_v2/api/command.py @@ -15,8 +15,11 @@ # coding: utf-8 from config import Config from fedlearner_webconsole.app import create_app -from fedlearner_webconsole.db import db +from fedlearner_webconsole.db import db_handler as db from fedlearner_webconsole.initial_db import initial_db +from flask_migrate import Migrate + +from fedlearner_webconsole.utils.hooks import pre_start_hook class CliConfig(Config): @@ -25,7 +28,10 @@ class CliConfig(Config): START_COMPOSER = False +pre_start_hook() +migrate = Migrate() app = create_app(CliConfig()) +migrate.init_app(app, db) @app.cli.command('create-initial-data') @@ -36,4 +42,3 @@ def create_initial_data(): @app.cli.command('create-db') def create_db(): db.create_all() - db.session.commit() diff --git a/web_console_v2/api/config.py b/web_console_v2/api/config.py index ef77e080a..e58492b6e 100644 --- a/web_console_v2/api/config.py +++ b/web_console_v2/api/config.py @@ -20,8 +20,6 @@ from fedlearner_webconsole.db import get_database_uri from envs import Envs -BASE_DIR = os.path.abspath(os.path.dirname(__file__)) - class Config(object): SQLALCHEMY_DATABASE_URI = get_database_uri() diff --git a/web_console_v2/api/envs.py b/web_console_v2/api/envs.py index 123620e7a..6f6f5f39d 100644 --- a/web_console_v2/api/envs.py +++ b/web_console_v2/api/envs.py @@ -6,9 +6,9 @@ class Envs(object): TZ = pytz.timezone(os.environ.get('TZ', 'UTC')) - HDFS_SERVER = os.environ.get('HDFS_SERVER', None) ES_HOST = os.environ.get('ES_HOST', 'fedlearner-stack-elasticsearch-client') + ES_READ_HOST = os.environ.get('ES_READ_HOST', ES_HOST) ES_PORT = os.environ.get('ES_PORT', 9200) ES_USERNAME = os.environ.get('ES_USERNAME', 'elastic') ES_PASSWORD = os.environ.get('ES_PASSWORD', 'Fedlearner123') @@ -38,6 +38,16 @@ class Envs(object): GRPC_CLIENT_TIMEOUT = os.environ.get('GRPC_CLIENT_TIMEOUT', 5) # storage filesystem STORAGE_ROOT = os.getenv('STORAGE_ROOT', '/data') + # BASE_DIR + BASE_DIR = os.path.abspath(os.path.dirname(__file__)) + # spark on k8s image url + SPARKAPP_IMAGE_URL = os.getenv('SPARKAPP_IMAGE_URL', None) + SPARKAPP_FILES_PATH = os.getenv('SPARKAPP_FILES_PATH', None) + SPARKAPP_VOLUMES = os.getenv('SPARKAPP_VOLUMES', None) + SPARKAPP_VOLUME_MOUNTS = os.getenv('SPARKAPP_VOLUME_MOUNTS', None) + + # Hooks + PRE_START_HOOK = os.environ.get('PRE_START_HOOK', None) class Features(object): diff --git a/web_console_v2/api/fedlearner_webconsole/app.py b/web_console_v2/api/fedlearner_webconsole/app.py index 82452d657..618b13b30 100644 --- a/web_console_v2/api/fedlearner_webconsole/app.py +++ b/web_console_v2/api/fedlearner_webconsole/app.py @@ -14,7 +14,6 @@ # coding: utf-8 # pylint: disable=wrong-import-position, global-statement -import importlib import logging import logging.config import os @@ -22,13 +21,11 @@ from http import HTTPStatus from flask import Flask, jsonify -from flask_migrate import Migrate from flask_restful import Api from flask_jwt_extended import JWTManager from envs import Envs from fedlearner_webconsole.utils import metrics -migrate = Migrate() jwt = JWTManager() from fedlearner_webconsole.auth.apis import initialize_auth_apis @@ -113,6 +110,8 @@ def user_lookup_callback(jwt_header, jwt_data): @jwt.token_in_blocklist_loader def check_if_token_invalid(jwt_header, jwt_data): + del jwt_header # unused by check_if_token_invalid + jti = jwt_data['jti'] session = Session.query.filter_by(jti=jti).first() return session is None @@ -122,18 +121,9 @@ def create_app(config): # format logging logging.config.dictConfig(LOGGING_CONFIG) - before_hook_path = os.getenv('FEDLEARNER_WEBCONSOLE_BEFORE_APP_START') - if before_hook_path: - module_path, func_name = before_hook_path.split(':') - module = importlib.import_module(module_path) - # Dynamically run the function - getattr(module, func_name)() - app = Flask('fedlearner_webconsole') app.config.from_object(config) - db.init_app(app) - migrate.init_app(app, db) jwt.init_app(app) # Error handlers @@ -142,6 +132,9 @@ def create_app(config): app.register_error_handler(WebConsoleApiException, make_response) app.register_error_handler(Exception, _handle_uncaught_exception) + # TODO(wangsen.0914): This will be removed sooner! + db.init_app(app) + api = Api(prefix='/api/v2') initialize_auth_apis(api) initialize_project_apis(api) @@ -152,7 +145,7 @@ def create_app(config): initialize_setting_apis(api) initialize_mmgr_apis(api) initialize_sparkapps_apis(api) - if os.environ.get('FLASK_ENV') != 'production': + if os.environ.get('FLASK_ENV') != 'production' or Envs.DEBUG: initialize_debug_apis(api) # A hack that use our customized error handlers # Ref: https://github.com/flask-restful/flask-restful/issues/280 diff --git a/web_console_v2/api/fedlearner_webconsole/auth/apis.py b/web_console_v2/api/fedlearner_webconsole/auth/apis.py index 01df1a17c..7206e3497 100644 --- a/web_console_v2/api/fedlearner_webconsole/auth/apis.py +++ b/web_console_v2/api/fedlearner_webconsole/auth/apis.py @@ -14,12 +14,15 @@ # coding: utf-8 # pylint: disable=cyclic-import +import re import datetime from http import HTTPStatus from flask import request from flask_restful import Resource, reqparse from flask_jwt_extended.utils import get_current_user from flask_jwt_extended import create_access_token, decode_token, get_jwt + +from fedlearner_webconsole.utils.base64 import base64decode from fedlearner_webconsole.utils.decorators import jwt_required from fedlearner_webconsole.utils.decorators import admin_required @@ -32,6 +35,28 @@ UnauthorizedException, NoAccessException) +# rule: password must have a letter, a num and a special character +PASSWORD_FORMAT_L = re.compile(r'.*[A-Za-z]') +PASSWORD_FORMAT_N = re.compile(r'.*[0-9]') +PASSWORD_FORMAT_S = re.compile(r'.*[`!@#$%^&*()\-_=+|{}\[\];:\'\",<.>/?~]') + + +def check_password_format(password: str): + if not 8 <= len(password) <= 20: + raise InvalidArgumentException( + 'Password is not legal: 8 <= length <= 20') + required_chars = [] + if PASSWORD_FORMAT_L.match(password) is None: + required_chars.append('a letter') + if PASSWORD_FORMAT_N.match(password) is None: + required_chars.append('a num') + if PASSWORD_FORMAT_S.match(password) is None: + required_chars.append('a special character') + if required_chars: + tip = ', '.join(required_chars) + raise InvalidArgumentException( + f'Password is not legal: must have {tip}.') + class SigninApi(Resource): def post(self): @@ -44,11 +69,11 @@ def post(self): help='password is empty') data = parser.parse_args() username = data['username'] - password = data['password'] + password = base64decode(data['password']) user = User.query.filter_by(username=username).filter_by( state=State.ACTIVE).first() if user is None: - raise NotFoundException() + raise NotFoundException(f'Failed to find user: {username}') if not user.verify_password(password): raise UnauthorizedException('Invalid password') token = create_access_token(identity=username) @@ -61,11 +86,11 @@ def post(self): db.session.commit() return { - 'data': { - 'user': user.to_dict(), - 'access_token': token - } - }, HTTPStatus.OK + 'data': { + 'user': user.to_dict(), + 'access_token': token + } + }, HTTPStatus.OK @jwt_required() def delete(self): @@ -105,11 +130,13 @@ def post(self): data = parser.parse_args() username = data['username'] - password = data['password'] + password = base64decode(data['password']) role = data['role'] name = data['name'] email = data['email'] + check_password_format(password) + if User.query.filter_by(username=username).first() is not None: raise ResourceConflictException( 'user {} already exists'.format(username)) @@ -129,7 +156,8 @@ class UserApi(Resource): def _find_user(self, user_id) -> User: user = User.query.filter_by(id=user_id).first() if user is None or user.state == State.DELETED: - raise NotFoundException() + raise NotFoundException( + f'Failed to find user_id: {user_id}') return user def _check_current_user(self, user_id, msg): @@ -158,7 +186,9 @@ def patch(self, user_id): if k not in mutable_attrs: raise InvalidArgumentException(f'cannot edit {k} attribute!') if k == 'password': - user.set_password(v) + password = base64decode(v) + check_password_format(password) + user.set_password(password) else: setattr(user, k, v) diff --git a/web_console_v2/api/fedlearner_webconsole/composer/composer.py b/web_console_v2/api/fedlearner_webconsole/composer/composer.py index 1094d3340..e0040ba99 100644 --- a/web_console_v2/api/fedlearner_webconsole/composer/composer.py +++ b/web_console_v2/api/fedlearner_webconsole/composer/composer.py @@ -407,4 +407,4 @@ def _build_pipeline(name: str, items: List[IItem], composer = Composer(config=ComposerConfig( - runner_fn=global_runner_fn, name='scheduler for fedlearner webconsole')) + runner_fn=global_runner_fn(), name='scheduler for fedlearner webconsole')) diff --git a/web_console_v2/api/fedlearner_webconsole/composer/interface.py b/web_console_v2/api/fedlearner_webconsole/composer/interface.py index 358eb9f5b..f9acdeb88 100644 --- a/web_console_v2/api/fedlearner_webconsole/composer/interface.py +++ b/web_console_v2/api/fedlearner_webconsole/composer/interface.py @@ -22,10 +22,13 @@ from fedlearner_webconsole.composer.models import Context, RunnerStatus +# NOTE: remember to register new item in `global_runner_fn` \ +# which defined in `runner.py` class ItemType(enum.Enum): - TASK = 'task' + TASK = 'task' # test only MEMORY = 'memory' WORKFLOW_CRON_JOB = 'workflow_cron_job' + DATA_PIPELINE = 'data_pipeline' # item interface diff --git a/web_console_v2/api/fedlearner_webconsole/composer/runner.py b/web_console_v2/api/fedlearner_webconsole/composer/runner.py index e9140e79c..46d87ec20 100644 --- a/web_console_v2/api/fedlearner_webconsole/composer/runner.py +++ b/web_console_v2/api/fedlearner_webconsole/composer/runner.py @@ -16,12 +16,14 @@ import datetime import logging import random +import sys import time from typing import Tuple from fedlearner_webconsole.composer.interface import IItem, IRunner, ItemType from fedlearner_webconsole.composer.models import Context, RunnerStatus, \ SchedulerRunner +from fedlearner_webconsole.dataset.data_pipeline import DataPipelineRunner from fedlearner_webconsole.db import get_session from fedlearner_webconsole.workflow.cronjob import WorkflowCronJob @@ -77,8 +79,16 @@ def result(self, context: Context) -> Tuple[RunnerStatus, dict]: return RunnerStatus.DONE, {} -# register runner_fn -global_runner_fn = { - ItemType.MEMORY.value: MemoryRunner, - ItemType.WORKFLOW_CRON_JOB.value: WorkflowCronJob, -} +def global_runner_fn(): + # register runner_fn + runner_fn = { + ItemType.MEMORY.value: MemoryRunner, + ItemType.WORKFLOW_CRON_JOB.value: WorkflowCronJob, + ItemType.DATA_PIPELINE.value: DataPipelineRunner, + } + for item in ItemType: + if item.value in runner_fn or item == ItemType.TASK: + continue + logging.error(f'failed to find item, {item.value}') + sys.exit(-1) + return runner_fn diff --git a/web_console_v2/api/fedlearner_webconsole/composer/runner_cache.py b/web_console_v2/api/fedlearner_webconsole/composer/runner_cache.py index fd487a74a..bd93e8bac 100644 --- a/web_console_v2/api/fedlearner_webconsole/composer/runner_cache.py +++ b/web_console_v2/api/fedlearner_webconsole/composer/runner_cache.py @@ -13,6 +13,7 @@ # limitations under the License. # coding: utf-8 +import logging import threading from fedlearner_webconsole.composer.interface import IRunner @@ -37,6 +38,11 @@ def find_runner(self, runner_id: int, runner_name: str) -> IRunner: if obj: return obj item_type, item_id = runner_name.rsplit('_', 1) + if item_type not in self.runner_fn: + logging.error( + f'failed to find item_type {item_type} in runner_fn, ' + f'please register it in global_runner_fn') + raise ValueError(f'unknown item_type {item_type} in runner') obj = self.runner_fn[item_type](int(item_id)) self._cache[key] = obj return obj diff --git a/web_console_v2/api/fedlearner_webconsole/dataset/apis.py b/web_console_v2/api/fedlearner_webconsole/dataset/apis.py index 4bc5eaef8..865f41a26 100644 --- a/web_console_v2/api/fedlearner_webconsole/dataset/apis.py +++ b/web_console_v2/api/fedlearner_webconsole/dataset/apis.py @@ -25,9 +25,10 @@ from fedlearner_webconsole.dataset.models import (Dataset, DatasetType, BatchState, DataBatch) +from fedlearner_webconsole.dataset.services import DatasetService from fedlearner_webconsole.exceptions import (InvalidArgumentException, NotFoundException) -from fedlearner_webconsole.db import db +from fedlearner_webconsole.db import db_handler as db from fedlearner_webconsole.proto import dataset_pb2 from fedlearner_webconsole.scheduler.scheduler import scheduler from fedlearner_webconsole.utils.decorators import jwt_required @@ -47,10 +48,12 @@ def _get_dataset_path(dataset_name): class DatasetApi(Resource): @jwt_required() def get(self, dataset_id): - dataset = Dataset.query.get(dataset_id) - if dataset is None: - raise NotFoundException() - return {'data': dataset.to_dict()} + with db.session_scope() as session: + dataset = session.query(Dataset).get(dataset_id) + if dataset is None: + raise NotFoundException( + f'Failed to find dataset: {dataset_id}') + return {'data': dataset.to_dict()} @jwt_required() def patch(self, dataset_id: int): @@ -65,22 +68,53 @@ def patch(self, dataset_id: int): help='dataset comment') parser.add_argument('comment') data = parser.parse_args() - dataset = db.session.query(Dataset).filter_by(id=dataset_id).first() - if not dataset: - raise NotFoundException() - if data['name']: - dataset.name = data['name'] - if data['comment']: - dataset.comment = data['comment'] - db.session.commit() - return {'data': dataset.to_dict()}, HTTPStatus.OK + with db.session_scope() as session: + dataset = session.query(Dataset).filter_by(id=dataset_id).first() + if not dataset: + raise NotFoundException( + f'Failed to find dataset: {dataset_id}') + if data['name']: + dataset.name = data['name'] + if data['comment']: + dataset.comment = data['comment'] + session.commit() + return {'data': dataset.to_dict()}, HTTPStatus.OK + + +class DatasetPreviewApi(Resource): + def get(self, dataset_id: int): + if dataset_id <= 0: + raise NotFoundException(f'Failed to find dataset: {dataset_id}') + with db.session_scope() as session: + data = DatasetService(session).get_dataset_preview(dataset_id) + return {'data': data} + + +class DatasetMetricsApi(Resource): + def get(self, dataset_id: int): + if dataset_id <= 0: + raise NotFoundException(f'Failed to find dataset: {dataset_id}') + name = request.args.get('name', None) + if not name: + raise InvalidArgumentException(f'required params name') + with db.session_scope() as session: + data = DatasetService(session).feature_metrics(name, dataset_id) + return {'data': data} class DatasetsApi(Resource): @jwt_required() def get(self): - datasets = Dataset.query.order_by(Dataset.created_at.desc()).all() - return {'data': [d.to_dict() for d in datasets]} + parser = reqparse.RequestParser() + parser.add_argument('project', + type=int, + required=False, + help='project') + data = parser.parse_args() + with db.session_scope() as session: + datasets = DatasetService(session).get_datasets( + project_id=int(data['project'] or 0)) + return {'data': [d.to_dict() for d in datasets]} @jwt_required() def post(self): @@ -104,22 +138,23 @@ def post(self): comment = body.get('comment') project_id = body.get('project_id') - try: - # Create dataset - dataset = Dataset( - name=name, - dataset_type=dataset_type, - comment=comment, - path=_get_dataset_path(name), - project_id=project_id, - ) - db.session.add(dataset) - # TODO: scan cronjob - db.session.commit() - return {'data': dataset.to_dict()} - except Exception as e: - db.session.rollback() - raise InvalidArgumentException(details=str(e)) + with db.session_scope() as session: + try: + # Create dataset + dataset = Dataset( + name=name, + dataset_type=dataset_type, + comment=comment, + path=_get_dataset_path(name), + project_id=project_id, + ) + session.add(dataset) + # TODO: scan cronjob + session.commit() + return {'data': dataset.to_dict()} + except Exception as e: + session.rollback() + raise InvalidArgumentException(details=str(e)) class BatchesApi(Resource): @@ -139,40 +174,40 @@ def post(self, dataset_id: int): files = body.get('files') move = body.get('move', False) comment = body.get('comment') - - dataset = Dataset.query.filter_by(id=dataset_id).first() - if dataset is None: - raise NotFoundException() - if event_time is None and dataset.type == DatasetType.STREAMING: - raise InvalidArgumentException( - details='data_batch.event_time is empty') - # TODO: PSI dataset should not allow multi batches - - # Use current timestamp to fill when type is PSI - event_time = datetime.fromtimestamp(event_time - or datetime.utcnow().timestamp(), - tz=timezone.utc) - batch_folder_name = event_time.strftime('%Y%m%d_%H%M%S') - batch_path = f'{dataset.path}/batch/{batch_folder_name}' - # Create batch - batch = DataBatch(dataset_id=dataset.id, - event_time=event_time, - comment=comment, - state=BatchState.NEW, - move=move, - path=batch_path) - batch_details = dataset_pb2.DataBatch() - for file_path in files: - file = batch_details.files.add() - file.source_path = file_path - file_name = file_path.split('/')[-1] - file.destination_path = f'{batch_path}/{file_name}' - batch.set_details(batch_details) - db.session.add(batch) - db.session.commit() - db.session.refresh(batch) - scheduler.wakeup(data_batch_ids=[batch.id]) - return {'data': batch.to_dict()} + with db.session_scope() as session: + dataset = session.query(Dataset).filter_by(id=dataset_id).first() + if dataset is None: + raise NotFoundException( + f'Failed to find dataset: {dataset_id}') + if event_time is None and dataset.type == DatasetType.STREAMING: + raise InvalidArgumentException( + details='data_batch.event_time is empty') + # TODO: PSI dataset should not allow multi batches + + # Use current timestamp to fill when type is PSI + event_time = datetime.fromtimestamp( + event_time or datetime.utcnow().timestamp(), tz=timezone.utc) + batch_folder_name = event_time.strftime('%Y%m%d_%H%M%S') + batch_path = f'{dataset.path}/batch/{batch_folder_name}' + # Create batch + batch = DataBatch(dataset_id=dataset.id, + event_time=event_time, + comment=comment, + state=BatchState.NEW, + move=move, + path=batch_path) + batch_details = dataset_pb2.DataBatch() + for file_path in files: + file = batch_details.files.add() + file.source_path = file_path + file_name = file_path.split('/')[-1] + file.destination_path = f'{batch_path}/{file_name}' + batch.set_details(batch_details) + session.add(batch) + session.commit() + session.refresh(batch) + scheduler.wakeup(data_batch_ids=[batch.id]) + return {'data': batch.to_dict()} class FilesApi(Resource): @@ -195,4 +230,7 @@ def initialize_dataset_apis(api: Api): api.add_resource(DatasetsApi, '/datasets') api.add_resource(DatasetApi, '/datasets/') api.add_resource(BatchesApi, '/datasets//batches') + api.add_resource(DatasetPreviewApi, '/datasets//preview') + api.add_resource(DatasetMetricsApi, + '/datasets//feature_metrics') api.add_resource(FilesApi, '/files') diff --git a/web_console_v2/api/fedlearner_webconsole/dataset/data_pipeline.py b/web_console_v2/api/fedlearner_webconsole/dataset/data_pipeline.py new file mode 100644 index 000000000..70b6d4588 --- /dev/null +++ b/web_console_v2/api/fedlearner_webconsole/dataset/data_pipeline.py @@ -0,0 +1,185 @@ +# Copyright 2021 The FedLearner Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# coding: utf-8 +import io +import logging +import os +import tarfile +import traceback + +from enum import Enum +from copy import deepcopy +from typing import Tuple, Optional, List +from uuid import uuid4 + +from envs import Envs + +from fedlearner_webconsole.composer.interface import IItem, IRunner, ItemType +from fedlearner_webconsole.composer.models import Context, RunnerStatus +from fedlearner_webconsole.sparkapp.service import SparkAppService +from fedlearner_webconsole.sparkapp.schema import SparkAppConfig + + +class DataPipelineType(Enum): + ANALYZER = 'analyzer' + CONVERTER = 'converter' + TRANSFORMER = 'transformer' + + +class DataPipelineItem(IItem): + def __init__(self, task_id: int): + self.id = task_id + + def type(self) -> ItemType: + return ItemType.DATA_PIPELINE + + def get_id(self) -> int: + return self.id + + +class DataPipelineRunner(IRunner): + TYPE_PARAMS_MAPPER = { + DataPipelineType.ANALYZER: { + 'files_dir': 'fedlearner_webconsole/dataset/sparkapp/pipeline', + 'main_application': 'pipeline/analyzer.py', + }, + DataPipelineType.CONVERTER: { + 'files_dir': 'fedlearner_webconsole/dataset/sparkapp/pipeline', + 'main_application': 'pipeline/converter.py', + }, + DataPipelineType.TRANSFORMER: { + 'files_dir': 'fedlearner_webconsole/dataset/sparkapp/pipeline', + 'main_application': 'pipeline/transformer.py', + } + } + + SPARKAPP_STATE_TO_RUNNER_STATUS = { + '': RunnerStatus.RUNNING, + 'SUBMITTED': RunnerStatus.RUNNING, + 'PENDING_RERUN': RunnerStatus.RUNNING, + 'RUNNING': RunnerStatus.RUNNING, + 'COMPLETED': RunnerStatus.DONE, + 'SUCCEEDING': RunnerStatus.DONE, + 'FAILED': RunnerStatus.FAILED, + 'SUBMISSION_FAILED': RunnerStatus.FAILED, + 'INVALIDATING': RunnerStatus.FAILED, + 'FAILING': RunnerStatus.FAILED, + 'UNKNOWN': RunnerStatus.FAILED + } + + def __init__(self, task_id: int) -> None: + self.task_id = task_id + self.task_type = None + self.files_dir = None + self.files_path = None + self.main_application = None + self.command = [] + self.sparkapp_name = None + self.args = {} + self.started = False + self.error_msg = False + + self.spark_service = SparkAppService() + + def start(self, context: Context): + try: + self.started = True + self.args = deepcopy(context.data.get(str(self.task_id), {})) + self.task_type = DataPipelineType(self.args.pop('task_type')) + name = self.args.pop('sparkapp_name') + job_id = uuid4().hex + self.sparkapp_name = f'pipe-{self.task_type.value}-{job_id}-{name}' + + params = self.__class__.TYPE_PARAMS_MAPPER[self.task_type] + self.files_dir = os.path.join(Envs.BASE_DIR, params['files_dir']) + self.files_path = Envs.SPARKAPP_FILES_PATH + self.main_application = params['main_application'] + self.command = self.args.pop('input') + + files = None + if self.files_path is None: + files_obj = io.BytesIO() + with tarfile.open(fileobj=files_obj, mode='w') as f: + f.add(self.files_dir) + files = files_obj.getvalue() + + config = { + 'name': self.sparkapp_name, + 'files': files, + 'files_path': self.files_path, + 'image_url': Envs.SPARKAPP_IMAGE_URL, + 'volumes': gen_sparkapp_volumes(Envs.SPARKAPP_VOLUMES), + 'driver_config': { + 'cores': + 1, + 'memory': + '4g', + 'volume_mounts': + gen_sparkapp_volume_mounts(Envs.SPARKAPP_VOLUME_MOUNTS), + }, + 'executor_config': { + 'cores': + 2, + 'memory': + '4g', + 'instances': + 1, + 'volume_mounts': + gen_sparkapp_volume_mounts(Envs.SPARKAPP_VOLUME_MOUNTS), + }, + 'main_application': f'${{prefix}}/{self.main_application}', + 'command': self.command, + } + config_dict = SparkAppConfig.from_dict(config) + resp = self.spark_service.submit_sparkapp(config=config_dict) + logging.info( + f'created spark app, name: {name}, ' + f'config: {config_dict.__dict__}, resp: {resp.__dict__}') + except Exception as e: # pylint: disable=broad-except + self.error_msg = f'[composer] failed to run this item, err: {e}, \ + trace: {traceback.format_exc()}' + + def result(self, context: Context) -> Tuple[RunnerStatus, dict]: + if self.error_msg: + context.set_data(f'failed_{self.task_id}', + {'error': self.error_msg}) + return RunnerStatus.FAILED, {} + if not self.started: + return RunnerStatus.RUNNING, {} + resp = self.spark_service.get_sparkapp_info(self.sparkapp_name) + logging.info(f'sparkapp resp: {resp.__dict__}') + if not resp.state: + return RunnerStatus.RUNNING, {} + return self.__class__.SPARKAPP_STATE_TO_RUNNER_STATUS.get( + resp.state, RunnerStatus.FAILED), resp.to_dict() + + +def gen_sparkapp_volumes(value: str) -> Optional[List[dict]]: + if value != 'data': + return None + # TODO: better to read from conf + return [{ + 'name': 'data', + 'persistentVolumeClaim': { + 'claimName': 'pvc-fedlearner-default' + } + }] + + +def gen_sparkapp_volume_mounts(value: str) -> Optional[List[dict]]: + if value != 'data': + return None + # TODO: better to read from conf + return [{'name': 'data', 'mountPath': '/data'}] diff --git a/web_console_v2/api/fedlearner_webconsole/dataset/services.py b/web_console_v2/api/fedlearner_webconsole/dataset/services.py new file mode 100644 index 000000000..a21b7cbeb --- /dev/null +++ b/web_console_v2/api/fedlearner_webconsole/dataset/services.py @@ -0,0 +1,124 @@ +# Copyright 2021 The FedLearner Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# coding: utf-8 +import json +import logging +from typing import List + +from sqlalchemy.orm import Session + +from fedlearner_webconsole.dataset.models import Dataset +from fedlearner_webconsole.dataset.sparkapp.pipeline.util import \ + dataset_meta_path, dataset_features_path, dataset_hist_path +from fedlearner_webconsole.exceptions import NotFoundException +from fedlearner_webconsole.utils.file_manager import FileManager + + +class DatasetService(object): + def __init__(self, session: Session): + self._session = session + self._file_manager = FileManager() + + def get_dataset_preview(self, dataset_id: int = 0) -> dict: + dataset = self._session.query(Dataset).filter( + Dataset.id == dataset_id).first() + if not dataset: + raise NotFoundException(f'Failed to find dataset: {dataset_id}') + dataset_path = dataset.path + # meta is generated from sparkapp/pipeline/analyzer.py + meta_path = dataset_meta_path(dataset_path) + # data format: + # { + # 'dtypes': { + # 'f01': 'bigint' + # }, + # 'samples': [ + # [1], + # [0], + # ], + # 'metrics': { + # 'f01': { + # 'count': '2', + # 'mean': '0.0015716767309123998', + # 'stddev': '0.03961485047808605', + # 'min': '0', + # 'max': '1', + # 'missing_count': '0' + # } + # } + # } + val = {} + try: + val = json.loads(self._file_manager.read(meta_path)) + except Exception as e: # pylint: disable=broad-except + logging.info( + f'failed to read meta file, path: {meta_path}, err: {e}') + return {} + # feature is generated from sparkapp/pipeline/analyzer.py + feature_path = dataset_features_path(dataset_path) + try: + val['metrics'] = json.loads(self._file_manager.read(feature_path)) + except Exception as e: # pylint: disable=broad-except + logging.info( + f'failed to read feature file, path: {feature_path}, err: {e}') + return val + + def feature_metrics(self, name: str, dataset_id: int = 0) -> dict: + dataset = self._session.query(Dataset).filter( + Dataset.id == dataset_id).first() + if not dataset: + raise NotFoundException(f'Failed to find dataset: {dataset_id}') + dataset_path = dataset.path + feature_path = dataset_features_path(dataset_path) + # data format: + # { + # 'name': 'f01', + # 'metrics': { + # 'count': '2', + # 'mean': '0.0015716767309123998', + # 'stddev': '0.03961485047808605', + # 'min': '0', + # 'max': '1', + # 'missing_count': '0' + # }, + # 'hist': { + # 'x': [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, + # 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], + # 'y': [12070, 0, 0, 0, 0, 0, 0, 0, 0, 19] + # } + # } + val = {} + try: + feature_data = json.loads(self._file_manager.read(feature_path)) + val['name'] = name + val['metrics'] = feature_data.get(name, {}) + except Exception as e: # pylint: disable=broad-except + logging.info( + f'failed to read feature file, path: {feature_path}, err: {e}') + # hist is generated from sparkapp/pipeline/analyzer.py + hist_path = dataset_hist_path(dataset_path) + try: + hist_data = json.loads(self._file_manager.read(hist_path)) + val['hist'] = hist_data.get(name, {}) + except Exception as e: # pylint: disable=broad-except + logging.info( + f'failed to read hist file, path: {hist_path}, err: {e}') + return val + + def get_datasets(self, project_id: int = 0) -> List[Dataset]: + q = self._session.query(Dataset).order_by(Dataset.created_at.desc()) + if project_id > 0: + q = q.filter(Dataset.project_id == project_id) + return q.all() diff --git a/web_console_v2/api/fedlearner_webconsole/dataset/sparkapp/pipeline/analyzer.py b/web_console_v2/api/fedlearner_webconsole/dataset/sparkapp/pipeline/analyzer.py new file mode 100644 index 000000000..5759b334e --- /dev/null +++ b/web_console_v2/api/fedlearner_webconsole/dataset/sparkapp/pipeline/analyzer.py @@ -0,0 +1,92 @@ +# Copyright 2021 The FedLearner Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# coding: utf-8 + +import os +import sys +import json +import logging + +import fsspec +import pandas + +from pyspark.sql import SparkSession +from pyspark.sql.functions import col, lit, sum +from util import dataset_features_path, dataset_meta_path, dataset_hist_path + + +def analyze(dataset_path: str, wildcard: str): + # for example: + # dataset_path: /data/fl_v2_fish_fooding/dataset/20210527_221741_pipeline/ + # wildcard: rds/** + spark = SparkSession.builder.getOrCreate() + files = os.path.join(dataset_path, wildcard) + logging.info(f'### loading df..., input files path: {files}') + df = spark.read.format('tfrecords').load(files) + # df_stats + df_missing = df.select(*(sum(col(c).isNull().cast('int')).alias(c) + for c in df.columns)).withColumn( + 'summary', lit('missing_count')) + df_stats = df.describe().unionByName(df_missing) + df_stats = df_stats.toPandas().set_index('summary').transpose() + features_path = dataset_features_path(dataset_path) + logging.info(f'### writing features, features path is {features_path}') + content = json.dumps(df_stats.to_dict(orient='index')) + with fsspec.open(features_path, mode='w') as f: + f.write(content) + # meta + meta = {} + # dtypes + logging.info('### loading dtypes...') + dtypes = {} + for d in df.dtypes: + k, v = d # (feature, type) + dtypes[k] = v + meta['dtypes'] = dtypes + # sample count + logging.info('### loading count...') + meta['count'] = df.count() + # sample + logging.info('### loading sample...') + meta['sample'] = df.head(20) + # meta + meta_path = dataset_meta_path(dataset_path) + logging.info(f'### writing meta, path is {meta_path}') + with fsspec.open(meta_path, mode='w') as f: + f.write(json.dumps(meta)) + # feature histogram + logging.info('### loading hist...') + hist = {} + for c in df.columns: + # TODO: histogram is too slow and needs optimization + x, y = df.select(c).rdd.flatMap(lambda x: x).histogram(10) + hist[c] = {'x': x, 'y': y} + hist_path = dataset_hist_path(dataset_path) + logging.info(f'### writing hist, path is {hist_path}') + with fsspec.open(hist_path, mode='w') as f: + f.write(json.dumps(hist)) + + spark.stop() + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + if len(sys.argv) != 3: + logging.error( + f'spark-submit {sys.argv[0]} [dataset_path] [file_wildcard]') + sys.exit(-1) + + dataset_path, wildcard = sys.argv[1], sys.argv[2] + analyze(dataset_path, wildcard) diff --git a/web_console_v2/api/fedlearner_webconsole/dataset/sparkapp/pipeline/converter.py b/web_console_v2/api/fedlearner_webconsole/dataset/sparkapp/pipeline/converter.py new file mode 100644 index 000000000..248210d4d --- /dev/null +++ b/web_console_v2/api/fedlearner_webconsole/dataset/sparkapp/pipeline/converter.py @@ -0,0 +1,56 @@ +# Copyright 2021 The FedLearner Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# coding: utf-8 + +import sys +import os +import logging + +from pyspark.sql import SparkSession +from util import dataset_rds_path + + +def convert(dataset_path: str, wildcard: str): + # for example: + # dataset_path: /data/fl_v2_fish_fooding/dataset/20210527_221741_pipeline/ + # wildcard: batch/**/*.csv + files = os.path.join(dataset_path, wildcard) + logging.info(f'### input files path: {files}') + spark = SparkSession.builder.getOrCreate() + if wildcard.endswith('*.csv'): + df = spark.read.format('csv').option('header', 'true').option( + 'inferSchema', 'true').load(files) + elif wildcard.endswith('*.rd') or wildcard.endswith('*.tfrecords'): + df = spark.read.format('tfrecords').load(files) + else: + logging.error(f'### no valid file wildcard, wildcard: {wildcard}') + return + + df.printSchema() + save_path = dataset_rds_path(dataset_path) + logging.info(f'### saving to {save_path}, in tfrecords') + df.write.format('tfrecords').save(save_path, mode='overwrite') + spark.stop() + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + if len(sys.argv) != 3: + logging.error( + f'spark-submit {sys.argv[0]} [dataset_path] [file_wildcard]') + sys.exit(-1) + + dataset_path, wildcard = sys.argv[1], sys.argv[2] + convert(dataset_path, wildcard) diff --git a/web_console_v2/api/fedlearner_webconsole/dataset/sparkapp/pipeline/transformer.py b/web_console_v2/api/fedlearner_webconsole/dataset/sparkapp/pipeline/transformer.py new file mode 100644 index 000000000..4c6620de0 --- /dev/null +++ b/web_console_v2/api/fedlearner_webconsole/dataset/sparkapp/pipeline/transformer.py @@ -0,0 +1,50 @@ +# Copyright 2021 The FedLearner Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# coding: utf-8 +import os +import json +import sys +import logging + +from pyspark.sql import SparkSession +from util import dataset_transformer_path + + +def transform(dataset_path: str, wildcard: str, conf: str): + # for example: + # dataset_path: /data/fl_v2_fish_fooding/dataset/20210527_221741_pipeline/ + # wildcard: rds/** or data_block/**/*.data + # conf: {"f00001": 0.0, "f00002": 1.0} + spark = SparkSession.builder.getOrCreate() + files = os.path.join(dataset_path, wildcard) + conf_dict = json.loads(conf) + logging.info(f'### input files path: {files}, config: {conf_dict}') + df = spark.read.format('tfrecords').load(files) + filled_df = df.fillna(conf_dict) + save_path = dataset_transformer_path(dataset_path) + logging.info(f'### saving to {save_path}') + filled_df.write.format('tfrecords').save(save_path, mode='overwrite') + spark.stop() + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + if len(sys.argv) != 4: + logging.error( + f'spark-submit {sys.argv[0]} [dataset_path] [wildcard] [config]') + sys.exit(-1) + + dataset_path, wildcard, conf = sys.argv[1], sys.argv[2], sys.argv[3] + transform(dataset_path, wildcard, conf) diff --git a/web_console_v2/api/fedlearner_webconsole/dataset/sparkapp/pipeline/util.py b/web_console_v2/api/fedlearner_webconsole/dataset/sparkapp/pipeline/util.py new file mode 100644 index 000000000..14085e93e --- /dev/null +++ b/web_console_v2/api/fedlearner_webconsole/dataset/sparkapp/pipeline/util.py @@ -0,0 +1,36 @@ +# Copyright 2021 The FedLearner Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# coding: utf-8 +import os + + +def dataset_rds_path(dataset_path: str) -> str: + return os.path.join(dataset_path, 'rds/') + + +def dataset_features_path(dataset_path: str) -> str: + return os.path.join(dataset_path, '_FEATURES') + + +def dataset_meta_path(dataset_path: str) -> str: + return os.path.join(dataset_path, '_META') + + +def dataset_hist_path(dataset_path: str) -> str: + return os.path.join(dataset_path, '_HIST') + + +def dataset_transformer_path(dataset_path: str) -> str: + return os.path.join(dataset_path, 'fe/') diff --git a/web_console_v2/api/fedlearner_webconsole/db.py b/web_console_v2/api/fedlearner_webconsole/db.py index b7afe0f6e..b40ff033b 100644 --- a/web_console_v2/api/fedlearner_webconsole/db.py +++ b/web_console_v2/api/fedlearner_webconsole/db.py @@ -15,26 +15,29 @@ # coding: utf-8 import os from contextlib import contextmanager -from typing import Generator, Callable +from typing import ContextManager, Callable + +import sqlalchemy as sa -from flask_sqlalchemy import SQLAlchemy from sqlalchemy.engine import Engine, create_engine +from sqlalchemy.ext.declarative.api import DeclarativeMeta, declarative_base from sqlalchemy.orm import sessionmaker from sqlalchemy.orm.session import Session +from flask_sqlalchemy import SQLAlchemy -BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '../')) +from envs import Envs +BASE_DIR = Envs.BASE_DIR # Explicitly set autocommit and autoflush # Disables autocommit to make developers to commit manually # Enables autoflush to make changes visible in the same session -# Enables expire_on_commit to make it possible that one session commit twice +# Disable expire_on_commit to make it possible that object can detach SESSION_OPTIONS = { 'autocommit': False, 'autoflush': True, - 'expire_on_commit': True + 'expire_on_commit': False } ENGINE_OPTIONS = {} -db = SQLAlchemy(session_options=SESSION_OPTIONS, engine_options=ENGINE_OPTIONS) def default_table_args(comment: str) -> dict: @@ -102,7 +105,8 @@ def get_database_uri() -> str: if 'SQLALCHEMY_DATABASE_URI' in os.environ: uri = os.getenv('SQLALCHEMY_DATABASE_URI') else: - uri = 'sqlite:///{}'.format(os.path.join(BASE_DIR, 'app.db')) + uri = 'sqlite:///{}?check_same_thread=False'.format( + os.path.join(BASE_DIR, 'app.db')) return _turn_db_timezone_to_utc(uri) @@ -119,7 +123,7 @@ def get_engine(database_uri: str) -> Engine: @contextmanager -def get_session(db_engine: Engine) -> Generator[Session, None, None]: +def get_session(db_engine: Engine) -> ContextManager[Session]: """Get session from database engine. Example: @@ -128,25 +132,27 @@ def get_session(db_engine: Engine) -> Generator[Session, None, None]: session.query(MODEL).filter_by(field=value).first() """ try: - session = sessionmaker(bind=db_engine, **SESSION_OPTIONS)() + session: Session = sessionmaker(bind=db_engine, **SESSION_OPTIONS)() except Exception: raise Exception('unknown db engine') - else: + + try: yield session + except Exception: + session.rollback() + raise finally: session.close() -def make_session_context() -> Callable[[], Generator[Session, None, None]]: +def make_session_context() -> Callable[[], ContextManager[Session]]: """A functional closure that will store engine Call it n times if you want to n connection pools Returns: - Callable[[], Generator[Session, None, None]]: - a function that yield a context + Callable[[], Callable[[], ContextManager[Session]]] + a function that return a contextmanager - Yields: - Generator[Session, None, None]: a session context Examples: # First initialize a connection pool, @@ -159,12 +165,47 @@ def make_session_context() -> Callable[[], Generator[Session, None, None]]: """ engine = None - @contextmanager def wrapper_get_session(): nonlocal engine if engine is None: engine = get_engine(get_database_uri()) - with get_session(engine) as session: - yield session + return get_session(engine) return wrapper_get_session + + +class DBHandler(object): + def __init__(self) -> None: + super().__init__() + + self.engine: Engine = get_engine(get_database_uri()) + self.Model: DeclarativeMeta = declarative_base(bind=self.engine) + for module in sa, sa.orm: + for key in module.__all__: + if not hasattr(self, key): + setattr(self, key, getattr(module, key)) + + def session_scope(self) -> ContextManager[Session]: + return get_session(self.engine) + + @property + def metadata(self) -> DeclarativeMeta: + return self.Model.metadata + + def rebind(self, database_uri: str): + self.engine = get_engine(database_uri) + self.Model = declarative_base(bind=self.engine, metadata=self.metadata) + + def create_all(self): + return self.metadata.create_all() + + def drop_all(self): + return self.metadata.drop_all() + + +# now db_handler and db are alive at the same time +# db will be replaced by db_handler in the near future +db_handler = DBHandler() +db = SQLAlchemy(session_options=SESSION_OPTIONS, + engine_options=ENGINE_OPTIONS, + metadata=db_handler.metadata) diff --git a/web_console_v2/api/fedlearner_webconsole/debug/apis.py b/web_console_v2/api/fedlearner_webconsole/debug/apis.py index 84d9f085c..4c74e9a80 100644 --- a/web_console_v2/api/fedlearner_webconsole/debug/apis.py +++ b/web_console_v2/api/fedlearner_webconsole/debug/apis.py @@ -13,10 +13,14 @@ # limitations under the License. # coding: utf-8 +import json + from flask_restful import Resource, Api, request from fedlearner_webconsole.composer.composer import composer from fedlearner_webconsole.composer.runner import MemoryItem +from fedlearner_webconsole.dataset.data_pipeline import DataPipelineItem, \ + DataPipelineType class ComposerApi(Resource): @@ -42,5 +46,47 @@ def get(self, name): return {'data': {'name': name}} +class DataPipelineApi(Resource): + def get(self, name: str): + # '/data/fl_v2_fish_fooding/dataset/20210527_221741_pipeline' + input_dir = request.args.get('input_dir', None) + if not input_dir: + return {'msg': 'no input dir'} + if 'pipe' in name: + composer.collect( + name, + [DataPipelineItem(1), DataPipelineItem(2)], + { # meta data + 1: { # convertor + 'sparkapp_name': '1', + 'task_type': DataPipelineType.CONVERTER.value, + 'input': [input_dir, 'batch/**/*.csv'], + }, + 2: { # analyzer + 'sparkapp_name': '2', + 'task_type': DataPipelineType.ANALYZER.value, + 'input': [input_dir, 'rds/**'], + }, + }, + ) + elif 'fe' in name: + composer.collect( + name, + [DataPipelineItem(1)], + { # meta data + 1: { # transformer + 'sparkapp_name': '1', + 'task_type': DataPipelineType.TRANSFORMER.value, + 'input': [input_dir, 'rds/**', json.dumps({ + 'f00000': 1.0, + 'f00010': 0.0, + })], + }, + }, + ) + return {'data': {'name': name}} + + def initialize_debug_apis(api: Api): api.add_resource(ComposerApi, '/debug/composer/') + api.add_resource(DataPipelineApi, '/debug/pipeline/') diff --git a/web_console_v2/api/fedlearner_webconsole/exceptions.py b/web_console_v2/api/fedlearner_webconsole/exceptions.py index a8834fe82..3de880de0 100644 --- a/web_console_v2/api/fedlearner_webconsole/exceptions.py +++ b/web_console_v2/api/fedlearner_webconsole/exceptions.py @@ -26,8 +26,8 @@ def __init__(self, status_code, error_code, message, details=None): self.details = details def __repr__(self): - return '%s(%d %s: %s)'%( - type(self).__name__, self.error_code, self.message, self.details) + return f'{type(self).__name__}({self.error_code} ' \ + f'{self.message}: {self.details})' def to_dict(self): dic = { @@ -38,15 +38,18 @@ def to_dict(self): dic['details'] = self.details return dic + class InvalidArgumentException(WebConsoleApiException): def __init__(self, details): WebConsoleApiException.__init__(self, HTTPStatus.BAD_REQUEST, 400, 'Invalid argument or payload.', details) + class NotFoundException(WebConsoleApiException): - def __init__(self): + def __init__(self, message=None): WebConsoleApiException.__init__( - self, HTTPStatus.NOT_FOUND, 404, 'Resource not existing') + self, HTTPStatus.NOT_FOUND, 404, + message if message else 'Resource not found.') class UnauthorizedException(WebConsoleApiException): diff --git a/web_console_v2/api/fedlearner_webconsole/initial_db.py b/web_console_v2/api/fedlearner_webconsole/initial_db.py index 0dc01a62b..da9997ba0 100644 --- a/web_console_v2/api/fedlearner_webconsole/initial_db.py +++ b/web_console_v2/api/fedlearner_webconsole/initial_db.py @@ -13,7 +13,7 @@ # limitations under the License. from fedlearner_webconsole.auth.models import User, Role, State -from fedlearner_webconsole.db import db +from fedlearner_webconsole.db import db_handler as db INITIAL_USER_INFO = [{ 'username': 'ada', @@ -33,20 +33,22 @@ def initial_db(): - # initial user info first - for u_info in INITIAL_USER_INFO: - username = u_info['username'] - password = u_info['password'] - name = u_info['name'] - email = u_info['email'] - role = u_info['role'] - state = u_info['state'] - if User.query.filter_by(username=username).first() is None: - user = User(username=username, - name=name, - email=email, - role=role, - state=state) - user.set_password(password=password) - db.session.add(user) - db.session.commit() + with db.session_scope() as session: + # initial user info first + for u_info in INITIAL_USER_INFO: + username = u_info['username'] + password = u_info['password'] + name = u_info['name'] + email = u_info['email'] + role = u_info['role'] + state = u_info['state'] + if session.query(User).filter_by( + username=username).first() is None: + user = User(username=username, + name=name, + email=email, + role=role, + state=state) + user.set_password(password=password) + session.add(user) + session.commit() diff --git a/web_console_v2/api/fedlearner_webconsole/job/apis.py b/web_console_v2/api/fedlearner_webconsole/job/apis.py index f84aafbaa..d9a073dbe 100644 --- a/web_console_v2/api/fedlearner_webconsole/job/apis.py +++ b/web_console_v2/api/fedlearner_webconsole/job/apis.py @@ -36,7 +36,7 @@ def _get_job(job_id): result = Job.query.filter_by(id=job_id).first() if result is None: - raise NotFoundException() + raise NotFoundException(f'Failed to find job_id: {job_id}') return result @@ -115,7 +115,8 @@ class PeerJobMetricsApi(Resource): def get(self, workflow_uuid, participant_id, job_name): workflow = Workflow.query.filter_by(uuid=workflow_uuid).first() if workflow is None: - raise NotFoundException() + raise NotFoundException( + f'Failed to find workflow: {workflow_uuid}') project_config = workflow.project.get_config() party = project_config.participants[participant_id] client = RpcClient(project_config, party) @@ -147,7 +148,7 @@ def get(self, job_id): job = _get_job(job_id) if start_time is None: start_time = job.workflow.start_at - return {'data': es.query_events('filebeat-*', job.name, + return {'data': es.query_events(Envs.ES_INDEX, job.name, 'fedlearner-operator', start_time, int(time.time() * 1000 @@ -171,7 +172,8 @@ def get(self, workflow_uuid, participant_id, job_name): max_lines = data['max_lines'] workflow = Workflow.query.filter_by(uuid=workflow_uuid).first() if workflow is None: - raise NotFoundException() + raise NotFoundException( + f'Failed to find workflow: {workflow_uuid}') if start_time is None: start_time = workflow.start_at project_config = workflow.project.get_config() @@ -306,7 +308,8 @@ def get(self, workflow_uuid, participant_id, job_name): args = parser.parse_args() workflow = Workflow.query.filter_by(uuid=workflow_uuid).first() if workflow is None: - raise NotFoundException() + raise NotFoundException( + f'Failed to find workflow: {workflow_uuid}') project_config = workflow.project.get_config() party = project_config.participants[participant_id] client = RpcClient(project_config, party) diff --git a/web_console_v2/api/fedlearner_webconsole/job/models.py b/web_console_v2/api/fedlearner_webconsole/job/models.py index 165c8b816..c9b00aff6 100644 --- a/web_console_v2/api/fedlearner_webconsole/job/models.py +++ b/web_console_v2/api/fedlearner_webconsole/job/models.py @@ -14,8 +14,10 @@ # coding: utf-8 import datetime +import logging import enum import json + from sqlalchemy.sql import func from sqlalchemy.sql.schema import Index from fedlearner_webconsole.utils.mixins import to_dict_mixin @@ -26,10 +28,20 @@ class JobState(enum.Enum): - INVALID = 0 - STOPPED = 1 - WAITING = 2 - STARTED = 3 + # VALID TRANSITION: + # 1. NEW : init by workflow + # 2. NEW/STOPPED/COMPLTED/FAILED -> WAITING: triggered by user, run workflow + # 3. WAITING -> STARTED: triggered by scheduler + # 4. WAITING -> NEW: triggered by user, stop workflow + # 4. STARTED -> STOPPED: triggered by user, stop workflow + # 5. STARTED -> COMPLETED/FAILED: triggered by k8s_watcher + INVALID = 0 # INVALID STATE + STOPPED = 1 # STOPPED BY USER + WAITING = 2 # SCHEDULED, WAITING FOR RUNNING + STARTED = 3 # RUNNING + NEW = 4 # BEFORE SCHEDULE + COMPLETED = 5 # SUCCEEDED JOB + FAILED = 6 # FAILED JOB # must be consistent with JobType in proto @@ -161,23 +173,14 @@ def get_pods_for_frontend(self, include_private_info=True): return [pod.to_dict(include_private_info) for pod in result.values()] def get_state_for_frontend(self): - if self.state == JobState.STARTED: - if self.is_complete(): - return 'COMPLETED' - if self.is_failed(): - return 'FAILED' - return 'RUNNING' - if self.state == JobState.STOPPED: - if self.get_flapp_details()['flapp'] is None: - return 'NEW' return self.state.name - def is_failed(self): + def is_flapp_failed(self): # TODO: make the getter more efficient flapp = FlApp.from_json(self.get_flapp_details()['flapp']) return flapp.state in [FlAppState.FAILED, FlAppState.SHUTDOWN] - def is_complete(self): + def is_flapp_complete(self): # TODO: make the getter more efficient flapp = FlApp.from_json(self.get_flapp_details()['flapp']) return flapp.state == FlAppState.COMPLETED @@ -188,20 +191,48 @@ def get_complete_at(self): return flapp.completed_at def stop(self): + if self.state not in [JobState.WAITING, JobState.STARTED, + JobState.COMPLETED, JobState.FAILED]: + logging.warning('illegal job state, name: %s, state: %s', + self.name, self.state) + return if self.state == JobState.STARTED: self._set_snapshot_flapp() k8s_client.delete_flapp(self.name) - self.state = JobState.STOPPED + # state change: + # WAITING -> NEW + # STARTED -> STOPPED + # COMPLETED/FAILED unchanged + if self.state == JobState.STARTED: + self.state = JobState.STOPPED + if self.state == JobState.WAITING: + self.state = JobState.NEW def schedule(self): - assert self.state == JobState.STOPPED + # COMPLETED/FAILED Job State can be scheduled since stop action + # will not change the state of completed or failed job + assert self.state in [JobState.NEW, JobState.STOPPED, + JobState.COMPLETED, JobState.FAILED] self.pods_snapshot = None self.flapp_snapshot = None self.state = JobState.WAITING def start(self): + assert self.state == JobState.WAITING self.state = JobState.STARTED + def complete(self): + assert self.state == JobState.STARTED, 'Job State is not STARTED' + self._set_snapshot_flapp() + k8s_client.delete_flapp(self.name) + self.state = JobState.COMPLETED + + def fail(self): + assert self.state == JobState.STARTED, 'Job State is not STARTED' + self._set_snapshot_flapp() + k8s_client.delete_flapp(self.name) + self.state = JobState.FAILED + class JobDependency(db.Model): __tablename__ = 'job_dependency_v2' diff --git a/web_console_v2/api/fedlearner_webconsole/job/service.py b/web_console_v2/api/fedlearner_webconsole/job/service.py index 991aa4b42..fc015dfb6 100644 --- a/web_console_v2/api/fedlearner_webconsole/job/service.py +++ b/web_console_v2/api/fedlearner_webconsole/job/service.py @@ -14,9 +14,10 @@ # coding: utf-8 +import logging from sqlalchemy.orm.session import Session from fedlearner_webconsole.rpc.client import RpcClient -from fedlearner_webconsole.job.models import Job, JobDependency +from fedlearner_webconsole.job.models import Job, JobDependency, JobState from fedlearner_webconsole.proto import common_pb2 from fedlearner_webconsole.utils.metrics import emit_counter @@ -33,7 +34,7 @@ def is_ready(self, job: Job) -> bool: src_job = self._session.query(Job).get(dep.src_job_id) assert src_job is not None, 'Job {} not found'.format( dep.src_job_id) - if not src_job.is_complete(): + if not src_job.state == JobState.COMPLETED: return False return True @@ -49,3 +50,20 @@ def is_peer_ready(job: Job) -> bool: if not resp.is_ready: return False return True + + def update_running_state(self, job_name): + job = self._session.query(Job).filter_by(name=job_name).first() + if job is None: + emit_counter('[JobService]job_not_found', 1) + return + if not job.state == JobState.STARTED: + emit_counter('[JobService]wrong_job_state', 1) + return + if job.is_flapp_complete(): + job.complete() + logging.debug('[JobService]change job %s state to %s', + job.name, JobState(job.state)) + elif job.is_flapp_failed(): + job.fail() + logging.debug('[JobService]change job %s state to %s', + job.name, JobState(job.state)) diff --git a/web_console_v2/api/fedlearner_webconsole/mmgr/apis.py b/web_console_v2/api/fedlearner_webconsole/mmgr/apis.py index ccac400a2..cbae04a07 100644 --- a/web_console_v2/api/fedlearner_webconsole/mmgr/apis.py +++ b/web_console_v2/api/fedlearner_webconsole/mmgr/apis.py @@ -17,11 +17,10 @@ from http import HTTPStatus from flask import request from flask_restful import Resource -from fedlearner_webconsole.db import db +from fedlearner_webconsole.db import db_handler from fedlearner_webconsole.exceptions import NotFoundException from fedlearner_webconsole.mmgr.models import Model, ModelType, ModelGroup from fedlearner_webconsole.mmgr.service import ModelService -from fedlearner_webconsole.db import make_session_context from fedlearner_webconsole.utils.decorators import jwt_required @@ -29,26 +28,32 @@ class ModelApi(Resource): @jwt_required() def get(self, model_id): detail_level = request.args.get('detail_level', '') - model_json = ModelService(db.session).query(model_id, detail_level) + with db_handler.session_scope() as session: + model_json = ModelService(session).query(model_id, detail_level) if not model_json: - raise NotFoundException() + raise NotFoundException( + f'Failed to find model: {model_id}') return {'data': model_json}, HTTPStatus.OK @jwt_required() def put(self, model_id): - model = db.session.query(Model).filter_by(id=model_id).one_or_none() - if not model: - raise NotFoundException() - model.extra = request.args.get('extra', model.extra) - db.session.commit() - return {'data': model.to_dict()}, HTTPStatus.OK + with db_handler.session_scope() as session: + model = session.query(Model).filter_by(id=model_id).one_or_none() + if not model: + raise NotFoundException( + f'Failed to find model: {model_id}') + model.extra = request.args.get('extra', model.extra) + session.commit() + return {'data': model.to_dict()}, HTTPStatus.OK @jwt_required() def delete(self, model_id): - model = ModelService(db.session).drop(model_id) - if not model: - raise NotFoundException() - return {'data': model.to_dict()}, HTTPStatus.OK + with db_handler.session_scope() as session: + model = ModelService(session).drop(model_id) + if not model: + raise NotFoundException( + f'Failed to find model: {model_id}') + return {'data': model.to_dict()}, HTTPStatus.OK class ModelListApi(Resource): @@ -56,13 +61,14 @@ class ModelListApi(Resource): def get(self): detail_level = request.args.get('detail_level', '') # TODO serialized query may incur performance penalty - model_list = [ - ModelService(db.session).query(m.id, detail_level) - for m in Model.query.filter( - Model.type.in_([ - ModelType.NN_MODEL.value, ModelType.TREE_MODEL.value - ])).all() - ] + with db_handler.session_scope() as session: + model_list = [ + ModelService(session).query(m.id, detail_level) + for m in Model.query.filter( + Model.type.in_([ + ModelType.NN_MODEL.value, ModelType.TREE_MODEL.value + ])).all() + ] return {'data': model_list}, HTTPStatus.OK @@ -78,8 +84,9 @@ def post(self): group.name = request.args.get('name', group.name) group.extra = request.args.get('extra', group.extra) - db.session.add(group) - db.session.commit() + with db_handler.session_scope() as session: + session.add(group) + session.commit() return {'data': group.to_dict()}, HTTPStatus.OK @@ -89,12 +96,14 @@ class GroupApi(Resource): def patch(self, group_id): group = ModelGroup.query.filter_by(id=group_id).one_or_none() if not group: - raise NotFoundException() + raise NotFoundException( + f'Failed to find group: {group_id}') group.name = request.args.get('name', group.name) group.extra = request.args.get('extra', group.extra) - db.session.add(group) - db.session.commit() + with db_handler.session_scope() as session: + session.add(group) + session.commit() return {'data': group.to_dict()}, HTTPStatus.OK diff --git a/web_console_v2/api/fedlearner_webconsole/mmgr/service.py b/web_console_v2/api/fedlearner_webconsole/mmgr/service.py index 705f31d7a..2f811c169 100644 --- a/web_console_v2/api/fedlearner_webconsole/mmgr/service.py +++ b/web_console_v2/api/fedlearner_webconsole/mmgr/service.py @@ -37,6 +37,14 @@ def __init__(self, session): JobType.TREE_MODEL_EVALUATION: ModelType.TREE_EVALUATION.value } + job_state_map = { + JobState.STARTED: ModelState.RUNNING.value, + JobState.COMPLETED: ModelState.SUCCEEDED.value, + JobState.FAILED: ModelState.FAILED.value, + JobState.STOPPED: ModelState.PAUSED.value, + JobState.WAITING: ModelState.WAITING.value + } + @staticmethod def is_model_related_job(job): job_type = job.job_type @@ -69,8 +77,8 @@ def plot_metrics(self, model, job=None): except Exception as e: return repr(e) - def is_model_quiescence(self, model): - return model.state in [ + def is_model_quiescence(self, state): + return state in [ ModelState.SUCCEEDED.value, ModelState.FAILED.value, ModelState.PAUSED.value ] @@ -79,17 +87,8 @@ def on_job_update(self, job: Job): logging.info('[ModelService][on_job_update] job name: %s', job.name) model = self._session.query(Model).filter_by(job_name=job.name).one() # see also `fedlearner_webconsole.job.models.Job.stop` - if job.state == JobState.STARTED: - if job.is_complete(): - state = ModelState.SUCCEEDED.value - elif job.is_failed(): - state = ModelState.FAILED.value - else: - state = ModelState.RUNNING.value - elif job.state == JobState.STOPPED: - state = ModelState.PAUSED.value - elif job.state == JobState.WAITING: - state = ModelState.WAITING.value + if job.state in self.job_state_map: + state = self.job_state_map[job.state] else: return logging.warning( '[ModelService][on_job_update] job state is %s', job.state) @@ -101,8 +100,8 @@ def on_job_update(self, job: Job): logging.info( '[ModelService][on_job_update] updating model(%d).state from %s to %s', model.id, model.state, state) - if self.is_model_quiescence(model): - model.metrics = self.plot_metrics(model, job) + if self.is_model_quiescence(state): + model.metrics = json.dumps(self.plot_metrics(model, job)) model.state = state self._session.add(model) @@ -137,8 +136,9 @@ def query(self, model_id, detail_level=''): model_json = model.to_dict() model_json['detail_level'] = detail_level if 'metrics' in detail_level: - if not self.is_model_quiescence(model) or not model.metrics: - model_json['metrics'] = self.plot_metrics(model) + if self.is_model_quiescence(model) and model.metrics: + model_json['metrics'] = json.loads(model.metrics) + else: model_json['metrics'] = self.plot_metrics(model) return model_json def drop(self, model_id): diff --git a/web_console_v2/api/fedlearner_webconsole/project/apis.py b/web_console_v2/api/fedlearner_webconsole/project/apis.py index af0a0e86e..c13a6a3ea 100644 --- a/web_console_v2/api/fedlearner_webconsole/project/apis.py +++ b/web_console_v2/api/fedlearner_webconsole/project/apis.py @@ -194,14 +194,16 @@ class ProjectApi(Resource): def get(self, project_id): project = Project.query.filter_by(id=project_id).first() if project is None: - raise NotFoundException() + raise NotFoundException( + f'Failed to find project: {project_id}') return {'data': project.to_dict()} @jwt_required() def patch(self, project_id): project = Project.query.filter_by(id=project_id).first() if project is None: - raise NotFoundException() + raise NotFoundException( + f'Failed to find project: {project_id}') config = project.get_config() if request.json.get('token') is not None: new_token = request.json.get('token') @@ -223,6 +225,9 @@ def patch(self, project_id): if variable.name == 'EGRESS_HOST': egress_host = variable.value + if request.json.get('participant_name'): + config.participants[0].name = request.json.get('participant_name') + if request.json.get('comment'): project.comment = request.json.get('comment') @@ -248,7 +253,8 @@ class CheckConnectionApi(Resource): def post(self, project_id): project = Project.query.filter_by(id=project_id).first() if project is None: - raise NotFoundException() + raise NotFoundException( + f'Failed to find project: {project_id}') success = True details = [] # TODO: Concurrently check diff --git a/web_console_v2/api/fedlearner_webconsole/rpc/client.py b/web_console_v2/api/fedlearner_webconsole/rpc/client.py index c55dca6c2..726568ad6 100644 --- a/web_console_v2/api/fedlearner_webconsole/rpc/client.py +++ b/web_console_v2/api/fedlearner_webconsole/rpc/client.py @@ -101,7 +101,8 @@ def check_connection(self): @catch_and_fallback(resp_class=service_pb2.UpdateWorkflowStateResponse) @retry_fn(retry_times=3, needed_exceptions=[grpc.RpcError]) def update_workflow_state(self, name, state, target_state, - transaction_state, uuid, forked_from_uuid): + transaction_state, uuid, forked_from_uuid, + extra=''): msg = service_pb2.UpdateWorkflowStateRequest( auth_info=self._auth_info, workflow_name=name, @@ -109,7 +110,9 @@ def update_workflow_state(self, name, state, target_state, target_state=target_state.value, transaction_state=transaction_state.value, uuid=uuid, - forked_from_uuid=forked_from_uuid) + forked_from_uuid=forked_from_uuid, + extra=extra + ) response = self._client.UpdateWorkflowState( request=msg, metadata=self._get_metadata(), timeout=Envs.GRPC_CLIENT_TIMEOUT) diff --git a/web_console_v2/api/fedlearner_webconsole/rpc/server.py b/web_console_v2/api/fedlearner_webconsole/rpc/server.py index e00335db8..19b9ac285 100644 --- a/web_console_v2/api/fedlearner_webconsole/rpc/server.py +++ b/web_console_v2/api/fedlearner_webconsole/rpc/server.py @@ -23,6 +23,7 @@ import traceback from concurrent import futures import grpc +from grpc_reflection.v1alpha import reflection from fedlearner_webconsole.proto import ( service_pb2, service_pb2_grpc, common_pb2, workflow_definition_pb2 @@ -76,6 +77,11 @@ def CheckConnection(self, request, context): self._server.check_connection, request, context, service_pb2.CheckConnectionResponse) + def Ping(self, request, context): + return self._try_handle_request( + self._server.ping, request, context, + service_pb2.PingResponse) + def UpdateWorkflowState(self, request, context): return self._try_handle_request( self._server.update_workflow_state, request, context, @@ -129,6 +135,10 @@ def start(self, app): futures.ThreadPoolExecutor(max_workers=20)) service_pb2_grpc.add_WebConsoleV2ServiceServicer_to_server( RPCServerServicer(self), self._server) + # reflection support server find the proto file path automatically + # when using grpcurl + reflection.enable_server_reflection( + service_pb2.DESCRIPTOR.services_by_name, self._server) self._server.add_insecure_port('[::]:%d' % listen_port) self._server.start() self._started = True @@ -177,6 +187,12 @@ def check_connection(self, request, context): status=common_pb2.Status( code=common_pb2.STATUS_SUCCESS)) + def ping(self, request, context): + return service_pb2.PingResponse( + status=common_pb2.Status( + code=common_pb2.STATUS_SUCCESS), + msg='Pong!') + def update_workflow_state(self, request, context): with self._app.app_context(): project, party = self.check_auth_info(request.auth_info, context) @@ -203,7 +219,8 @@ def update_workflow_state(self, request, context): state=state, target_state=target_state, transaction_state=transaction_state, uuid=uuid, - forked_from=forked_from + forked_from=forked_from, + extra=request.extra ) db.session.add(workflow) db.session.commit() diff --git a/web_console_v2/api/fedlearner_webconsole/scheduler/transaction.py b/web_console_v2/api/fedlearner_webconsole/scheduler/transaction.py index 909c50063..aa605e157 100644 --- a/web_console_v2/api/fedlearner_webconsole/scheduler/transaction.py +++ b/web_console_v2/api/fedlearner_webconsole/scheduler/transaction.py @@ -38,6 +38,12 @@ def project(self): return self._project def process(self): + # process local workflow + if self._workflow.is_local(): + self._workflow.update_local_state() + self._reload() + return self._workflow + # reload workflow and resolve -ing states self._workflow.update_state( self._workflow.state, self._workflow.target_state, @@ -117,7 +123,7 @@ def _broadcast_state( resp = client.update_workflow_state( self._workflow.name, state, target_state, transaction_state, self._workflow.uuid, - forked_from_uuid) + forked_from_uuid, self._workflow.extra) if resp.status.code == common_pb2.STATUS_SUCCESS: if resp.state == WorkflowState.INVALID: self._workflow.invalidate() diff --git a/web_console_v2/api/fedlearner_webconsole/setting/model.py b/web_console_v2/api/fedlearner_webconsole/setting/models.py similarity index 76% rename from web_console_v2/api/fedlearner_webconsole/setting/model.py rename to web_console_v2/api/fedlearner_webconsole/setting/models.py index d33a6bc48..7d46db01f 100644 --- a/web_console_v2/api/fedlearner_webconsole/setting/model.py +++ b/web_console_v2/api/fedlearner_webconsole/setting/models.py @@ -16,16 +16,13 @@ # pylint: disable=raise-missing-from from sqlalchemy import UniqueConstraint -from fedlearner_webconsole.db import db +from fedlearner_webconsole.db import db, default_table_args class Setting(db.Model): __tablename__ = 'settings_v2' - __table_args__ = (UniqueConstraint('key', name='uniq_key'), { - 'comment': 'workflow_v2', - 'mysql_engine': 'innodb', - 'mysql_charset': 'utf8mb4', - }) + __table_args__ = (UniqueConstraint('key', name='uniq_key'), + default_table_args('this is webconsole settings table')) id = db.Column(db.Integer, primary_key=True, comment='id') key = db.Column(db.String(255), nullable=False, comment='key') value = db.Column(db.Text, comment='value') diff --git a/web_console_v2/api/fedlearner_webconsole/sparkapp/apis.py b/web_console_v2/api/fedlearner_webconsole/sparkapp/apis.py index eea215f82..70dfc6339 100644 --- a/web_console_v2/api/fedlearner_webconsole/sparkapp/apis.py +++ b/web_console_v2/api/fedlearner_webconsole/sparkapp/apis.py @@ -34,7 +34,8 @@ def post(self): try: config = SparkAppConfig.from_dict(data) - config.files = base64.b64decode(config.files) + if config.files: + config.files = base64.b64decode(config.files) except ValueError as err: raise InvalidArgumentException(details=err) diff --git a/web_console_v2/api/fedlearner_webconsole/sparkapp/schema.py b/web_console_v2/api/fedlearner_webconsole/sparkapp/schema.py index bf25ae7b2..31d91f44a 100644 --- a/web_console_v2/api/fedlearner_webconsole/sparkapp/schema.py +++ b/web_console_v2/api/fedlearner_webconsole/sparkapp/schema.py @@ -18,7 +18,7 @@ from fedlearner_webconsole.utils.mixins import from_dict_mixin, to_dict_mixin SPARK_POD_CONFIG_SERILIZE_FIELDS = [ - 'cores', 'memory', 'instances', 'core_limit', 'envs' + 'cores', 'memory', 'instances', 'core_limit', 'envs', 'volume_mounts' ] @@ -31,6 +31,7 @@ def __init__(self): self.memory = None self.instances = None self.core_limit = None + self.volume_mounts = [] self.envs = {} def build_config(self) -> dict: @@ -48,18 +49,20 @@ def build_config(self) -> dict: config['instances'] = self.instances if self.core_limit: config['coreLimit'] = self.core_limit - if len(self.envs) > 0: + if self.envs and len(self.envs) > 0: config['env'] = [{ 'name': k, 'value': v } for k, v in self.envs.items()] + if self.volume_mounts and len(self.volume_mounts) > 0: + config['volumeMounts'] = self.volume_mounts return config SPARK_APP_CONFIG_SERILIZE_FIELDS = [ - 'name', 'files', 'image_url', 'driver_config', 'executor_config', - 'command', 'main_application' + 'name', 'files', 'files_path', 'volumes', 'image_url', 'driver_config', + 'executor_config', 'command', 'main_application', 'py_files' ] SPARK_APP_CONFIG_REQUIRED_FIELDS = ['name', 'image_url'] @@ -71,10 +74,16 @@ def build_config(self) -> dict: class SparkAppConfig(object): def __init__(self): self.name = None + # local files should be compressed to submit spark self.files = None + # if nas/hdfs has those files, such as analyzer, only need files path \ + # to submit spark + self.files_path = None self.image_url = None + self.volumes = [] self.driver_config = SparkPodConfig() self.executor_config = SparkPodConfig() + self.py_files = [] self.command = [] self.main_application = None @@ -107,6 +116,8 @@ def build_config(self, sparkapp_path: str) -> dict: self.image_url, 'imagePullPolicy': 'Always', + 'volumes': + self.volumes, 'mainApplicationFile': self._replace_placeholder_with_real_path( self.main_application, sparkapp_path), @@ -114,6 +125,12 @@ def build_config(self, sparkapp_path: str) -> dict: self._replace_placeholder_with_real_path(c, sparkapp_path) for c in self.command ], + 'deps': { + 'pyFiles': [ + self._replace_placeholder_with_real_path( + f, sparkapp_path) for f in self.py_files + ] + }, 'sparkConf': { 'spark.shuffle.service.enabled': 'false', }, @@ -144,7 +161,7 @@ def build_config(self, sparkapp_path: str) -> dict: SPARK_APP_INFO_SERILIZE_FIELDS = [ 'name', 'namespace', 'command', 'driver', 'executor', 'image_url', - 'main_application', 'spark_version', 'type' + 'main_application', 'spark_version', 'type', 'state' ] @@ -159,6 +176,14 @@ def from_k8s_resp(cls, resp): elif 'name' in resp['details']: sparkapp_info.name = resp['details']['name'] sparkapp_info.namespace = resp['metadata'].get('namespace', None) + sparkapp_info.state = None + if 'status' in resp: + if isinstance(resp['status'], str): + sparkapp_info.state = None + elif isinstance(resp['status'], dict): + sparkapp_info.state = resp.get('status', + {}).get('applicationState', + {}).get('state', None) sparkapp_info.command = resp.get('spec', {}).get('arguments', None) sparkapp_info.executor = SparkPodConfig.from_dict( resp.get('spec', {}).get('executor', {})) @@ -175,6 +200,7 @@ def from_k8s_resp(cls, resp): def __init__(self): self.name = None + self.state = None self.namespace = None self.command = None self.driver = SparkPodConfig() diff --git a/web_console_v2/api/fedlearner_webconsole/sparkapp/service.py b/web_console_v2/api/fedlearner_webconsole/sparkapp/service.py index 50554be46..21e612777 100644 --- a/web_console_v2/api/fedlearner_webconsole/sparkapp/service.py +++ b/web_console_v2/api/fedlearner_webconsole/sparkapp/service.py @@ -18,7 +18,6 @@ import tempfile from typing import Tuple -from pathlib import Path from envs import Envs from fedlearner_webconsole.utils.file_manager import FileManager @@ -87,12 +86,18 @@ def _copy_files_to_target_filesystem(self, source_filesystem_path: str, os.makedirs(temp_path) TarCli.untar_file(source_filesystem_path, temp_path) - for root, _, res in os.walk(temp_path): - for file in res: - file_path = os.path.join(root, file) - remote_file_path = os.path.join(target_filesystem_path, file) - if Path(file_path).is_file(): - self._file_client.copy(file_path, remote_file_path) + for root, dirs, files in os.walk(temp_path): + relative_path = os.path.relpath(root, temp_path) + for f in files: + file_path = os.path.join(root, f) + remote_file_path = os.path.join(target_filesystem_path, + relative_path, f) + self._file_client.copy(file_path, remote_file_path) + for d in dirs: + remote_dir_path = os.path.join(target_filesystem_path, + relative_path, d) + self._file_client.mkdir(remote_dir_path) + return True def submit_sparkapp(self, config: SparkAppConfig) -> SparkAppInfo: @@ -107,19 +112,22 @@ def submit_sparkapp(self, config: SparkAppConfig) -> SparkAppInfo: Returns: SparkAppInfo: resp of sparkapp """ - _, sparkapp_path = self._get_sparkapp_upload_path(config.name) - self._clear_and_make_an_empty_dir(sparkapp_path) - - with tempfile.TemporaryDirectory() as temp_dir: - tar_path = os.path.join(temp_dir, 'files.tar') - with open(tar_path, 'wb') as fwrite: - fwrite.write(config.files) - self._copy_files_to_target_filesystem( - source_filesystem_path=tar_path, - target_filesystem_path=sparkapp_path) - - resp = k8s_client.create_sparkapplication( - config.build_config(sparkapp_path)) + sparkapp_path = config.files_path + if config.files_path is None: + _, sparkapp_path = self._get_sparkapp_upload_path(config.name) + self._clear_and_make_an_empty_dir(sparkapp_path) + + with tempfile.TemporaryDirectory() as temp_dir: + tar_path = os.path.join(temp_dir, 'files.tar') + with open(tar_path, 'wb') as fwrite: + fwrite.write(config.files) + self._copy_files_to_target_filesystem( + source_filesystem_path=tar_path, + target_filesystem_path=sparkapp_path) + + config_dict = config.build_config(sparkapp_path) + logging.info(f'submit sparkapp, config: {config_dict}') + resp = k8s_client.create_sparkapplication(config_dict) return SparkAppInfo.from_k8s_resp(resp) def get_sparkapp_info(self, name: str) -> SparkAppInfo: diff --git a/web_console_v2/api/fedlearner_webconsole/utils/base64.py b/web_console_v2/api/fedlearner_webconsole/utils/base64.py new file mode 100644 index 000000000..06272b638 --- /dev/null +++ b/web_console_v2/api/fedlearner_webconsole/utils/base64.py @@ -0,0 +1,24 @@ +# Copyright 2021 The FedLearner Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# coding: utf-8 +from base64 import b64encode, b64decode + + +def base64encode(s: str) -> str: + return b64encode(s.encode('UTF-8')).decode('UTF-8') + + +def base64decode(s: str) -> str: + return b64decode(s).decode('UTF-8') diff --git a/web_console_v2/api/fedlearner_webconsole/utils/es.py b/web_console_v2/api/fedlearner_webconsole/utils/es.py index b4ee70892..058d7fbf6 100644 --- a/web_console_v2/api/fedlearner_webconsole/utils/es.py +++ b/web_console_v2/api/fedlearner_webconsole/utils/es.py @@ -26,7 +26,7 @@ class ElasticSearchClient(object): def __init__(self): self._es_client = None self._es_client = Elasticsearch([{ - 'host': Envs.ES_HOST, + 'host': Envs.ES_READ_HOST or Envs.ES_HOST, 'port': Envs.ES_PORT }], http_auth=(Envs.ES_USERNAME, diff --git a/web_console_v2/api/fedlearner_webconsole/utils/file_manager.py b/web_console_v2/api/fedlearner_webconsole/utils/file_manager.py index 8767fc301..be9e51110 100644 --- a/web_console_v2/api/fedlearner_webconsole/utils/file_manager.py +++ b/web_console_v2/api/fedlearner_webconsole/utils/file_manager.py @@ -16,22 +16,20 @@ import importlib import logging import os -import shutil +import re from collections import namedtuple -from pathlib import Path from typing import List -from pyarrow import fs -from pyarrow.fs import FileSystem from tensorflow.io import gfile -from envs import Envs - # path: absolute path of the file # size: file size in bytes # mtime: time of last modification, unix timestamp in seconds. File = namedtuple('File', ['path', 'size', 'mtime']) +# Currently the supported format '/' or 'hdfs://' +# TODO(chenyikan): Add oss format when verified. +SUPPORTED_FILE_PREFIXES = r'\.+\/|^\/|^hdfs:\/\/' class FileManagerBase(object): @@ -42,12 +40,16 @@ def can_handle(self, path: str) -> bool: raise NotImplementedError() def ls(self, path: str, recursive=False) -> List[str]: - """Lists files under a path.""" + """Lists files under a path. + Raises: + ValueError: When the path does not exist. + """ raise NotImplementedError() def move(self, source: str, destination: str) -> bool: """Moves a file from source to destination, if destination - is a folder then move into that folder.""" + is a folder then move into that folder. Files that already exist + will be overwritten.""" raise NotImplementedError() def remove(self, path: str) -> bool: @@ -56,136 +58,25 @@ def remove(self, path: str) -> bool: def copy(self, source: str, destination: str) -> bool: """Copies a file from source to destination, if destination - is a folder then move into that folder.""" + is a folder then move into that folder. Files that already exist + will be overwritten.""" raise NotImplementedError() def mkdir(self, path: str) -> bool: """Creates a directory. If already exists, return False""" raise NotImplementedError() - -class DefaultFileManager(FileManagerBase): - """Default file manager for native file system or NFS.""" - def can_handle(self, path): - return path.startswith('/') - - def ls(self, path: str, recursive=False) -> List[File]: - def _get_file_stats(path: str): - stat = os.stat(path) - return File(path=path, size=stat.st_size, mtime=int(stat.st_mtime)) - - if not Path(path).exists(): - raise ValueError( - f'cannot access {path}: No such file or directory') - # If it is a file - if Path(path).is_file(): - return [_get_file_stats(path)] - - files = [] - if recursive: - for root, _, res in os.walk(path): - for file in res: - if Path(os.path.join(root, file)).is_file(): - files.append(_get_file_stats(os.path.join(root, file))) - else: - for file in os.listdir(path): - if Path(os.path.join(path, file)).is_file(): - files.append(_get_file_stats(os.path.join(path, file))) - # Files only - return files - - def move(self, source: str, destination: str) -> bool: - return shutil.move(source, destination) - - def remove(self, path: str) -> bool: - if os.path.isfile(path): - return os.remove(path) - return shutil.rmtree(path) - - def copy(self, source: str, destination: str) -> bool: - return shutil.copy(source, destination) - - def mkdir(self, path: str) -> bool: - return os.makedirs(path, exist_ok=True) - - -class HdfsFileManager(FileManagerBase): - """A wrapper of snakebite client.""" - def can_handle(self, path): - return path.startswith('hdfs://') - - def __init__(self): - self._client, _ = FileSystem.from_uri(Envs.HDFS_SERVER) - - def _unwrap_path(self, path): - if path.startswith('hdfs://'): - return path[7:] - return path - - def _wrap_path(self, path): - if not path.startswith('hdfs://'): - return f'hdfs://{path}' - return path - - def ls(self, path: str, recursive=False) -> List[File]: - path = self._unwrap_path(path) - files = [] - try: - curr_path_info = self._client.get_file_info(path) - res_files = [] - if curr_path_info.type == fs.FileType.File: - res_files = [curr_path_info] - elif curr_path_info.type == fs.FileType.Directory: - res_files = self._client.get_file_info( - fs.FileSelector(path, recursive=recursive)) - else: - raise ValueError( - f'cannot access {path}: No such file or directory') - - for file in res_files: - if file.type == fs.FileType.File: - files.append( - File( - path=self._wrap_path(file.path), - size=file.size, - # ns to second - mtime=int(file.mtime_ns / 1e9))) - except RuntimeError as error: - # This is a hack that snakebite can not handle generator - if str(error) == 'generator raised StopIteration': - pass - else: - raise - return files - - def move(self, source: str, destination: str) -> bool: - source = self._unwrap_path(source) - destination = self._unwrap_path(destination) - return len(list(self._client.move(source, destination))) > 0 - - def remove(self, path: str) -> bool: - path = self._unwrap_path(path) - if self._client.get_file_info(path).is_file: - return self._client.delete_file(path) - return self._client.delete_dir(path) - - def copy(self, source: str, destination: str) -> bool: - return gfile.copy(source, destination) - - def mkdir(self, path: str) -> bool: - path = self._unwrap_path(path) - return self._client.create_dir(path) + def read(self, path: str) -> str: + raise NotImplementedError() class GFileFileManager(FileManagerBase): - """Gfile file manager for all FS supported by TF.""" + """Gfile file manager for all FS supported by TF, + currently it covers all file types we have.""" def can_handle(self, path): - # TODO: List tf support if path.startswith('fake://'): return False - if not Envs.HDFS_SERVER and path.startswith('hdfs://'): - return False - return True + return re.match(SUPPORTED_FILE_PREFIXES, path) def ls(self, path: str, recursive=False) -> List[File]: def _get_file_stats(path: str): @@ -225,18 +116,28 @@ def remove(self, path: str) -> bool: return gfile.rmtree(path) def copy(self, source: str, destination: str) -> bool: - return gfile.copy(source, destination) + if gfile.isdir(destination): + # gfile requires a file name for copy destination. + return gfile.copy(source, + os.path.join(destination, + os.path.basename(source)), + overwrite=True) + return gfile.copy(source, destination, overwrite=True) def mkdir(self, path: str) -> bool: return gfile.makedirs(path) + def read(self, path: str) -> str: + return gfile.GFile(path).read() + class FileManager(FileManagerBase): """A centralized manager to handle files. Please extend `FileManagerBase` and put the class path into `CUSTOMIZED_FILE_MANAGER`. For example, - 'fedlearner_webconsole.utils.file_manager:HdfsFileManager'""" + 'fedlearner_webconsole.utils.file_manager:HdfsFileManager' + """ def __init__(self): self._file_managers = [] cfm_path = os.environ.get('CUSTOMIZED_FILE_MANAGER') @@ -246,9 +147,6 @@ def __init__(self): # Dynamically construct a file manager customized_file_manager = getattr(module, class_name) self._file_managers.append(customized_file_manager()) - if Envs.HDFS_SERVER: - self._file_managers.append(HdfsFileManager()) - self._file_managers.append(DefaultFileManager()) self._file_managers.append(GFileFileManager()) def can_handle(self, path): @@ -268,6 +166,7 @@ def move(self, source: str, destination: str) -> bool: for fm in self._file_managers: if fm.can_handle(source) and fm.can_handle(destination): return fm.move(source, destination) + # TODO(chenyikan): Support cross FileManager move by using buffers. raise RuntimeError( f'move is not supported for {source} and {destination}') @@ -283,6 +182,7 @@ def copy(self, source: str, destination: str) -> bool: for fm in self._file_managers: if fm.can_handle(source) and fm.can_handle(destination): return fm.copy(source, destination) + # TODO(chenyikan): Support cross FileManager move by using buffers. raise RuntimeError( f'copy is not supported for {source} and {destination}') @@ -292,3 +192,10 @@ def mkdir(self, path: str) -> bool: if fm.can_handle(path): return fm.mkdir(path) raise RuntimeError(f'mkdir is not supported for {path}') + + def read(self, path: str) -> str: + logging.info(f'Read file from [{path}]') + for fm in self._file_managers: + if fm.can_handle(path): + return fm.read(path) + raise RuntimeError(f'read is not supported for {path}') diff --git a/web_console_v2/api/fedlearner_webconsole/utils/hooks.py b/web_console_v2/api/fedlearner_webconsole/utils/hooks.py new file mode 100644 index 000000000..25d0b9a6b --- /dev/null +++ b/web_console_v2/api/fedlearner_webconsole/utils/hooks.py @@ -0,0 +1,31 @@ +# Copyright 2021 The FedLearner Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# coding: utf-8 +import importlib + +from envs import Envs +from fedlearner_webconsole.db import db_handler as db, get_database_uri + + +def pre_start_hook(): + before_hook_path = Envs.PRE_START_HOOK + if before_hook_path: + module_path, func_name = before_hook_path.split(':') + module = importlib.import_module(module_path) + # Dynamically run the function + getattr(module, func_name)() + + # explicit rebind db engine to make hook work + db.rebind(get_database_uri()) diff --git a/web_console_v2/api/fedlearner_webconsole/utils/k8s_watcher.py b/web_console_v2/api/fedlearner_webconsole/utils/k8s_watcher.py index 30810cb22..22372a1ab 100644 --- a/web_console_v2/api/fedlearner_webconsole/utils/k8s_watcher.py +++ b/web_console_v2/api/fedlearner_webconsole/utils/k8s_watcher.py @@ -27,9 +27,10 @@ FEDLEARNER_CUSTOM_VERSION) from fedlearner_webconsole.mmgr.service import ModelService from fedlearner_webconsole.db import make_session_context +from fedlearner_webconsole.job.service import JobService -session_context = make_session_context() +session_context = make_session_context() class K8sWatcher(object): def __init__(self): @@ -77,6 +78,8 @@ def _event_consumer(self): try: event = self._queue.get() k8s_cache.update_cache(event) + # job state must be updated before model service + self._update_hook(event) if Features.FEATURE_MODEL_K8S_HOOK: with session_context() as session: ModelService(session).k8s_watcher_hook(event) @@ -85,6 +88,14 @@ def _event_consumer(self): logging.error(f'K8s event_consumer : {str(e)}. ' f'traceback:{traceback.format_exc()}') + def _update_hook(self, event: Event): + if event.obj_type == ObjectType.FLAPP: + logging.debug('[k8s_watcher][_update_hook]receive event %s', + event.flapp_name) + with session_context() as session: + JobService(session).update_running_state(event.flapp_name) + session.commit() + def _k8s_flapp_watcher(self): resource_version = '0' watcher = watch.Watch() diff --git a/web_console_v2/api/fedlearner_webconsole/workflow/apis.py b/web_console_v2/api/fedlearner_webconsole/workflow/apis.py index 237285f15..abc5c19a5 100644 --- a/web_console_v2/api/fedlearner_webconsole/workflow/apis.py +++ b/web_console_v2/api/fedlearner_webconsole/workflow/apis.py @@ -166,6 +166,10 @@ def post(self): type=int, required=False, help='interval for workflow cronjob in minute') + parser.add_argument('extra', + type=str, + required=False, + help='extra json string that needs send to peer') parser.add_argument('comment') data = parser.parse_args() @@ -188,7 +192,9 @@ def post(self): forked_from=data['forked_from'], state=WorkflowState.NEW, target_state=WorkflowState.READY, - transaction_state=TransactionState.READY) + transaction_state=TransactionState.READY, + extra=data['extra'] + ) workflow.set_config(template_proto) workflow.set_create_job_flags(data['create_job_flags']) diff --git a/web_console_v2/api/fedlearner_webconsole/workflow/models.py b/web_console_v2/api/fedlearner_webconsole/workflow/models.py index 58e9ce5d8..f988f93db 100644 --- a/web_console_v2/api/fedlearner_webconsole/workflow/models.py +++ b/web_console_v2/api/fedlearner_webconsole/workflow/models.py @@ -31,6 +31,7 @@ from fedlearner_webconsole.rpc.client import RpcClient from fedlearner_webconsole.mmgr.service import ModelService + class WorkflowState(enum.Enum): INVALID = 0 NEW = 1 @@ -201,6 +202,8 @@ class Workflow(db.Model): server_default=func.now(), comment='update_at') + extra = db.Column(db.Text(), comment='extra') # json string + owned_jobs = db.relationship( 'Job', primaryjoin='foreign(Job.workflow_id) == Workflow.id') project = db.relationship( @@ -208,12 +211,13 @@ class Workflow(db.Model): def get_state_for_frontend(self): if self.state == WorkflowState.RUNNING: - is_complete = all([ - job.is_disabled or job.is_complete() for job in self.owned_jobs - ]) + is_complete = all([job.is_disabled or + job.state == JobState.COMPLETED + for job in self.owned_jobs]) if is_complete: return 'COMPLETED' - is_failed = any([job.is_failed() for job in self.owned_jobs]) + is_failed = any([job.state == JobState.FAILED + for job in self.owned_jobs]) if is_failed: return 'FAILED' return self.state.name @@ -390,6 +394,17 @@ def prepare(self, target_state): def rollback(self): self.target_state = WorkflowState.INVALID + def start(self): + self.start_at = int(datetime.now().timestamp()) + for job in self.owned_jobs: + if not job.is_disabled: + job.schedule() + + def stop(self): + self.stop_at = int(datetime.now().timestamp()) + for job in self.owned_jobs: + job.stop() + # TODO: separate this method to another module def commit(self): assert self.transaction_state in [ @@ -398,23 +413,18 @@ def commit(self): 'Workflow not in prepare state' if self.target_state == WorkflowState.STOPPED: - self.stop_at = int(datetime.now().timestamp()) try: - for job in self.owned_jobs: - job.stop() + self.stop() except RuntimeError as e: # errors from k8s - logging.error('Stop workflow %d has Runtime error msg: %s', + logging.error('Stop workflow %d has error msg: %s', self.id, e.args) return elif self.target_state == WorkflowState.READY: self._setup_jobs() self.fork_proposal_config = None elif self.target_state == WorkflowState.RUNNING: - self.start_at = int(datetime.now().timestamp()) - for job in self.owned_jobs: - if not job.is_disabled: - job.schedule() + self.start() self.state = self.target_state self.target_state = WorkflowState.INVALID @@ -465,7 +475,7 @@ def _setup_jobs(self): config=job_def.SerializeToString(), workflow_id=self.id, project_id=self.project_id, - state=JobState.STOPPED, + state=JobState.NEW, is_disabled=(flag == common_pb2.CreateJobFlag.DISABLED)) db.session.add(job) jobs.append(job) @@ -522,3 +532,30 @@ def _prepare_for_ready(self): return True return bool(self.config) + + def is_local(self): + # since _setup_jobs has not been called, job_definitions is used + job_defs = self.get_config().job_definitions + flags = self.get_create_job_flags() + for i, (job_def, flag) in enumerate(zip(job_defs, flags)): + if flag != common_pb2.CreateJobFlag.REUSE and job_def.is_federated: + return False + return True + + def update_local_state(self): + if self.target_state == WorkflowState.INVALID: + return + if self.target_state == WorkflowState.READY: + self._setup_jobs() + elif self.target_state == WorkflowState.RUNNING: + self.start() + elif self.target_state == WorkflowState.STOPPED: + try: + self.stop() + except Exception as e: + # errors from k8s + logging.error('Stop workflow %d has error msg: %s', + self.id, e.args) + return + self.state = self.target_state + self.target_state = WorkflowState.INVALID diff --git a/web_console_v2/api/fedlearner_webconsole/workflow_template/apis.py b/web_console_v2/api/fedlearner_webconsole/workflow_template/apis.py index 0bbbaba5a..791f2ba89 100644 --- a/web_console_v2/api/fedlearner_webconsole/workflow_template/apis.py +++ b/web_console_v2/api/fedlearner_webconsole/workflow_template/apis.py @@ -146,7 +146,7 @@ def get(self, template_id): template = WorkflowTemplate.query.filter_by(id=template_id).first() if template is None: - raise NotFoundException() + raise NotFoundException(f'Failed to find template: {template_id}') result = template.to_dict() if download: @@ -164,7 +164,7 @@ def get(self, template_id): def delete(self, template_id): result = WorkflowTemplate.query.filter_by(id=template_id) if result.first() is None: - raise NotFoundException() + raise NotFoundException(f'Failed to find template: {template_id}') result.delete() db.session.commit() return {'data': {}}, HTTPStatus.OK @@ -192,7 +192,7 @@ def put(self, template_id): 'Workflow template {} already exists'.format(name)) template = WorkflowTemplate.query.filter_by(id=template_id).first() if template is None: - raise NotFoundException() + raise NotFoundException(f'Failed to find template: {template_id}') template_proto, editor_info_proto = _check_config_and_editor_info( config, editor_info) template_proto = _format_template_with_yaml_editor( diff --git a/web_console_v2/api/migrations/versions/b3290c1bf67a_add_completed_failed_jobstate.py b/web_console_v2/api/migrations/versions/b3290c1bf67a_add_completed_failed_jobstate.py new file mode 100644 index 000000000..90f4614e0 --- /dev/null +++ b/web_console_v2/api/migrations/versions/b3290c1bf67a_add_completed_failed_jobstate.py @@ -0,0 +1,38 @@ +"""add_completed_failed_jobstate + +Revision ID: b3290c1bf67a +Revises: fe30109949aa +Create Date: 2021-06-15 01:41:14.660466 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'b3290c1bf67a' +down_revision = 'fe30109949aa' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + bind = op.get_bind() + version = bind.execute('select version()') + # 'drop check' is invalid if mysql version is less than 8 + if version is not None and version.fetchall()[0][0] > '8.0.0': + op.execute('ALTER TABLE job_v2 drop check jobstate') + op.alter_column('job_v2', 'state', nullable=False, comment='state', type_=sa.Enum('INVALID', 'STOPPED', 'WAITING', 'STARTED', 'COMPLETED', 'FAILED', name='jobstate', native_enum=False)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + bind = op.get_bind() + version = bind.execute('select version()') + # 'drop check' is invalid if mysql version is less than 8 + if version is not None and version.fetchall()[0][0] > '8.0.0': + op.execute('ALTER TABLE job_v2 drop check jobstate') + op.alter_column('job_v2', 'state', nullable=False, comment='state', type_=sa.Enum('INVALID', 'STOPPED', 'WAITING', 'STARTED', name='jobstate', native_enum=False)) + # ### end Alembic commands ### diff --git a/web_console_v2/api/migrations/versions/fe30109949aa_add_extra_field_to_workflow.py b/web_console_v2/api/migrations/versions/fe30109949aa_add_extra_field_to_workflow.py new file mode 100644 index 000000000..11ece68f3 --- /dev/null +++ b/web_console_v2/api/migrations/versions/fe30109949aa_add_extra_field_to_workflow.py @@ -0,0 +1,24 @@ +"""add extra field to workflow + +Revision ID: fe30109949aa +Revises: 27a868485bf7 +Create Date: 2021-06-06 14:31:29.833200 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'fe30109949aa' +down_revision = '27a868485bf7' +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column('workflow_v2', sa.Column('extra', sa.Text(), nullable=True, comment='extra')) + + +def downgrade(): + op.drop_column('workflow_v2', 'extra') diff --git a/web_console_v2/api/protocols/fedlearner_webconsole/proto/service.proto b/web_console_v2/api/protocols/fedlearner_webconsole/proto/service.proto index 1f4283f78..0a3f89350 100644 --- a/web_console_v2/api/protocols/fedlearner_webconsole/proto/service.proto +++ b/web_console_v2/api/protocols/fedlearner_webconsole/proto/service.proto @@ -39,6 +39,13 @@ message CheckConnectionResponse { Status status = 1; } +message PingRequest {} + +message PingResponse { + Status status = 1; + string msg = 2; +} + message UpdateWorkflowStateRequest { ProjAuthInfo auth_info = 1; string workflow_name = 2; @@ -47,6 +54,8 @@ message UpdateWorkflowStateRequest { int64 transaction_state = 5; string forked_from_uuid = 6; string uuid = 7; + // json string + string extra = 8; } message UpdateWorkflowStateResponse { @@ -140,6 +149,7 @@ message GetJobKibanaResponse { service WebConsoleV2Service { rpc CheckConnection (CheckConnectionRequest) returns (CheckConnectionResponse) {} + rpc Ping (PingRequest) returns (PingResponse) {} rpc UpdateWorkflowState (UpdateWorkflowStateRequest) returns (UpdateWorkflowStateResponse) {} rpc GetWorkflow (GetWorkflowRequest) returns (GetWorkflowResponse) {} rpc UpdateWorkflow(UpdateWorkflowRequest) returns (UpdateWorkflowResponse) {} diff --git a/web_console_v2/api/requirements.txt b/web_console_v2/api/requirements.txt index 88beaa063..28ee70aaf 100644 --- a/web_console_v2/api/requirements.txt +++ b/web_console_v2/api/requirements.txt @@ -12,6 +12,7 @@ grpcio==1.32.0 # see details in https://github.com/grpc/grpc/issues/24897 grpcio-tools==1.32.0 grpcio-testing==1.32.0 +grpcio-reflection==1.32.0 kubernetes==12.0.1 elasticsearch==7.11.0 flatten-dict==0.3.0 @@ -23,7 +24,6 @@ mpld3==0.5.2 python-slugify==4.0.1 SQLAlchemy==1.3.20 prison==0.1.3 -pyarrow==3.0.0 tensorflow-io==0.8.1 pyspark==3.1.1 # Lint diff --git a/web_console_v2/api/server.py b/web_console_v2/api/server.py index 722f16cc8..bdb8e9e81 100644 --- a/web_console_v2/api/server.py +++ b/web_console_v2/api/server.py @@ -16,7 +16,9 @@ from config import Config from fedlearner_webconsole.app import create_app from fedlearner_webconsole.utils import middlewares +from fedlearner_webconsole.utils.hooks import pre_start_hook +pre_start_hook() app = create_app(Config()) # Middlewares app = middlewares.init_app(app) diff --git a/web_console_v2/api/test/auth_test.py b/web_console_v2/api/test/auth_test.py index aca9917d5..995a90458 100644 --- a/web_console_v2/api/test/auth_test.py +++ b/web_console_v2/api/test/auth_test.py @@ -17,9 +17,10 @@ import unittest from http import HTTPStatus +from fedlearner_webconsole.utils.base64 import base64encode from testing.common import BaseTestCase from fedlearner_webconsole.auth.models import State, User -from fedlearner_webconsole.db import db +from fedlearner_webconsole.db import db_handler as db class AuthApiTest(BaseTestCase): @@ -27,8 +28,9 @@ def test_get_all_users(self): deleted_user = User(username='deleted_one', email='who.knows@hhh.com', state=State.DELETED) - db.session.add(deleted_user) - db.session.commit() + with db.session_scope() as session: + session.add(deleted_user) + session.commit() resp = self.get_helper('/api/v2/auth/users') self.assertEqual(resp.status_code, HTTPStatus.UNAUTHORIZED) @@ -74,6 +76,12 @@ def test_partial_update_user_info(self): self.assertEqual(resp.status_code, HTTPStatus.OK) self.assertEqual(self.get_response_data(resp).get('role'), 'ADMIN') + resp = self.patch_helper(f'/api/v2/auth/users/{user_id}', + data={ + 'password': base64encode('fl@1234.'), + }) + self.assertEqual(resp.status_code, HTTPStatus.OK) + def test_create_new_user(self): new_user = { 'username': 'fedlearner', @@ -86,11 +94,27 @@ def test_create_new_user(self): self.assertEqual(resp.status_code, HTTPStatus.UNAUTHORIZED) self.signin_as_admin() + illegal_cases = ['aaaaaaaa', '11111111', '!@#$%^[]', + 'aaaA1111', 'AAAa!@#$', '1111!@#-', + 'aa11!@', 'fl@123.', + 'fl@1234567890abcdefg.'] + legal_case = 'fl@1234.' + + for case in illegal_cases: + new_user['password'] = base64encode(case) + resp = self.post_helper(f'/api/v2/auth/users', data=new_user) + self.assertEqual(resp.status_code, HTTPStatus.BAD_REQUEST) + + new_user['password'] = base64encode(legal_case) resp = self.post_helper(f'/api/v2/auth/users', data=new_user) self.assertEqual(resp.status_code, HTTPStatus.CREATED) self.assertEqual( self.get_response_data(resp).get('username'), 'fedlearner') + # test_repeat_create + resp = self.post_helper(f'/api/v2/auth/users', data=new_user) + self.assertEqual(resp.status_code, HTTPStatus.CONFLICT) + def test_delete_user(self): self.signin_as_admin() resp = self.get_helper('/api/v2/auth/users') @@ -132,7 +156,7 @@ def test_signout(self): self.signin_helper() resp = self.delete_helper(url='/api/v2/auth/signin') - self.assertEqual(resp.status_code, HTTPStatus.OK) + self.assertEqual(resp.status_code, HTTPStatus.OK, resp.json) resp = self.get_helper(url='/api/v2/auth/users/1') self.assertEqual(resp.status_code, HTTPStatus.UNAUTHORIZED) diff --git a/web_console_v2/api/test/fedlearner_webconsole/dataset/apis_test.py b/web_console_v2/api/test/fedlearner_webconsole/dataset/apis_test.py index d1a224454..23e2b4bed 100644 --- a/web_console_v2/api/test/fedlearner_webconsole/dataset/apis_test.py +++ b/web_console_v2/api/test/fedlearner_webconsole/dataset/apis_test.py @@ -13,9 +13,8 @@ # limitations under the License. # coding: utf-8 -import stat -import time import json +import time import os import shutil import tempfile @@ -26,37 +25,42 @@ from unittest import mock from unittest.mock import patch, MagicMock +from collections import namedtuple from testing.common import BaseTestCase -from fedlearner_webconsole.db import db -from fedlearner_webconsole.dataset.models import (Dataset, DatasetType, - DataBatch) +from fedlearner_webconsole.db import db_handler as db +from fedlearner_webconsole.dataset.models import (Dataset, DatasetType) +from tensorflow.io import gfile + +FakeFileStatistics = namedtuple('FakeFileStatistics', ['length', 'mtime_nsec']) class DatasetApiTest(BaseTestCase): class Config(BaseTestCase.Config): - STORAGE_ROOT = '/tmp' + STORAGE_ROOT = tempfile.gettempdir() def setUp(self): super().setUp() - self.default_dataset1 = Dataset( - name='default dataset1', - dataset_type=DatasetType.STREAMING, - comment='test comment1', - path='/data/dataset/123', - project_id=1, - ) - db.session.add(self.default_dataset1) - db.session.commit() + with db.session_scope() as session: + self.default_dataset1 = Dataset( + name='default dataset1', + dataset_type=DatasetType.STREAMING, + comment='test comment1', + path='/data/dataset/123', + project_id=1, + ) + session.add(self.default_dataset1) + session.commit() time.sleep(1) - self.default_dataset2 = Dataset( - name='default dataset2', - dataset_type=DatasetType.STREAMING, - comment='test comment2', - path='123', - project_id=2, - ) - db.session.add(self.default_dataset2) - db.session.commit() + with db.session_scope() as session: + self.default_dataset2 = Dataset( + name='default dataset2', + dataset_type=DatasetType.STREAMING, + comment='test comment2', + path=os.path.join(tempfile.gettempdir(), 'dataset/123'), + project_id=2, + ) + session.add(self.default_dataset2) + session.commit() def test_get_dataset(self): get_response = self.get_helper( @@ -89,6 +93,74 @@ def test_get_datasets(self): self.assertEqual(datasets[0]['name'], 'default dataset2') self.assertEqual(datasets[1]['name'], 'default dataset1') + def test_get_datasets_with_project_id(self): + get_response = self.get_helper('/api/v2/datasets?project=1') + self.assertEqual(get_response.status_code, HTTPStatus.OK) + datasets = self.get_response_data(get_response) + self.assertEqual(len(datasets), 1) + self.assertEqual(datasets[0]['name'], 'default dataset1') + + def test_preview_dataset_and_feature_metrics(self): + # write data + gfile.makedirs(self.default_dataset2.path) + meta_path = os.path.join(self.default_dataset2.path, '_META') + meta_data = { + 'dtypes': { + 'f01': 'bigint' + }, + 'samples': [ + [1], + [0], + ], + } + with gfile.GFile(meta_path, 'w') as f: + f.write(json.dumps(meta_data)) + + features_path = os.path.join(self.default_dataset2.path, '_FEATURES') + features_data = { + 'f01': { + 'count': '2', + 'mean': '0.0015716767309123998', + 'stddev': '0.03961485047808605', + 'min': '0', + 'max': '1', + 'missing_count': '0' + } + } + with gfile.GFile(features_path, 'w') as f: + f.write(json.dumps(features_data)) + + hist_path = os.path.join(self.default_dataset2.path, '_HIST') + hist_data = { + "f01": { + "x": [ + 0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, + 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1 + ], + "y": [12070, 0, 0, 0, 0, 0, 0, 0, 0, 19] + } + } + with gfile.GFile(hist_path, 'w') as f: + f.write(json.dumps(hist_data)) + + response = self.client.get('/api/v2/datasets/2/preview') + self.assertEqual(response.status_code, 200) + preview_data = self.get_response_data(response) + meta_data['metrics'] = features_data + self.assertEqual(preview_data, meta_data, 'should has preview data') + + feat_name = 'f01' + feature_response = self.client.get( + f'/api/v2/datasets/2/feature_metrics?name={feat_name}') + self.assertEqual(response.status_code, 200) + feature_data = self.get_response_data(feature_response) + self.assertEqual( + feature_data, { + 'name': feat_name, + 'metrics': features_data.get(feat_name, {}), + 'hist': hist_data.get(feat_name, {}) + }, 'should has feature data') + @patch('fedlearner_webconsole.dataset.apis.datetime') def test_post_datasets(self, mock_datetime): mock_datetime.now = MagicMock( @@ -106,13 +178,15 @@ def test_post_datasets(self, mock_datetime): self.assertEqual(create_response.status_code, HTTPStatus.OK) created_dataset = self.get_response_data(create_response) + dataset_path = os.path.join( + tempfile.gettempdir(), 'dataset/20200608_060606_test-post-dataset') self.assertEqual( { 'id': 3, 'name': 'test post dataset', 'dataset_type': dataset_type, 'comment': comment, - 'path': '/tmp/dataset/20200608_060606_test-post-dataset', + 'path': dataset_path, 'created_at': mock.ANY, 'updated_at': mock.ANY, 'deleted_at': None, @@ -130,7 +204,7 @@ def test_post_datasets(self, mock_datetime): 'name': 'test post dataset', 'dataset_type': dataset_type, 'comment': updated_comment, - 'path': '/tmp/dataset/20200608_060606_test-post-dataset', + 'path': dataset_path, 'created_at': mock.ANY, 'updated_at': mock.ANY, 'deleted_at': None, @@ -216,7 +290,7 @@ def setUp(self): def fake_stat(path, *arg, **kwargs): return self._get_file_stat(self._orig_os_stat, path) - os.stat = fake_stat + gfile.stat = fake_stat def tearDown(self): os.stat = self._orig_os_stat @@ -229,11 +303,9 @@ def _get_temp_path(self, file_path: str = None) -> str: def _get_file_stat(self, orig_os_stat, path): if path == self._get_temp_path('f1.txt') or \ - path == self._get_temp_path('f2.txt') or \ - path == self._get_temp_path('s/s3.txt'): - faked = list(orig_os_stat(path)) - faked[stat.ST_MTIME] = 1613982390 - return os.stat_result(faked) + path == self._get_temp_path('f2.txt') or \ + path == self._get_temp_path('s/s3.txt'): + return FakeFileStatistics(2, 1613982390 * 1e9) else: return orig_os_stat(path) @@ -241,7 +313,7 @@ def test_get_default_storage_root(self): get_response = self.get_helper('/api/v2/files') self.assertEqual(get_response.status_code, HTTPStatus.OK) files = self.get_response_data(get_response) - self.assertEqual(sorted(files, key=lambda f: f['size']), [ + self.assertEqual(sorted(files, key=lambda f: f['path']), [ { 'path': self._get_temp_path('f1.txt'), 'size': 2, @@ -249,12 +321,12 @@ def test_get_default_storage_root(self): }, { 'path': self._get_temp_path('f2.txt'), - 'size': 4, + 'size': 2, 'mtime': 1613982390 }, { 'path': self._get_temp_path('s/s3.txt'), - 'size': 6, + 'size': 2, 'mtime': 1613982390 }, ]) @@ -267,7 +339,7 @@ def test_get_specified_directory(self): self.assertEqual(files, [ { 'path': self._get_temp_path('s/s3.txt'), - 'size': 6, + 'size': 2, 'mtime': 1613982390 }, ]) diff --git a/web_console_v2/api/test/fedlearner_webconsole/exceptions_test.py b/web_console_v2/api/test/fedlearner_webconsole/exceptions_test.py index bdcbff70d..6e7af1a3c 100644 --- a/web_console_v2/api/test/fedlearner_webconsole/exceptions_test.py +++ b/web_console_v2/api/test/fedlearner_webconsole/exceptions_test.py @@ -16,7 +16,8 @@ import unittest from http import HTTPStatus -from fedlearner_webconsole.exceptions import InvalidArgumentException +from fedlearner_webconsole.exceptions import (InvalidArgumentException, + NotFoundException) class ExceptionsTest(unittest.TestCase): @@ -35,6 +36,22 @@ def test_invalid_argument_exception(self): ] }) + def test_not_found_exception(self): + exception1 = NotFoundException('User A not found.') + self.assertEqual(exception1.status_code, HTTPStatus.NOT_FOUND) + self.assertEqual( + exception1.to_dict(), { + 'code': 404, + 'message': 'User A not found.', + }) + exception2 = NotFoundException() + self.assertEqual(exception2.status_code, HTTPStatus.NOT_FOUND) + self.assertEqual( + exception2.to_dict(), { + 'code': 404, + 'message': 'Resource not found.', + }) + if __name__ == '__main__': unittest.main() diff --git a/web_console_v2/api/test/fedlearner_webconsole/job/service_test.py b/web_console_v2/api/test/fedlearner_webconsole/job/service_test.py index 5ac9d6f41..8161ea56c 100644 --- a/web_console_v2/api/test/fedlearner_webconsole/job/service_test.py +++ b/web_console_v2/api/test/fedlearner_webconsole/job/service_test.py @@ -44,7 +44,7 @@ def setUp(self): job_1 = Job(id=1, name='raw_data_1', job_type=JobType.RAW_DATA, - state=JobState.WAITING, + state=JobState.COMPLETED, workflow_id=0, project_id=0, config=config) @@ -58,7 +58,7 @@ def setUp(self): job_3 = Job(id=3, name='data_join_1', job_type=JobType.DATA_JOIN, - state=JobState.STARTED, + state=JobState.COMPLETED, workflow_id=1, project_id=0, config=config) @@ -91,6 +91,22 @@ def test_is_ready(self): job_service = JobService(db.session) self.assertTrue(job_service.is_ready(job_0)) self.assertFalse(job_service.is_ready(job_2)) - with patch('fedlearner_webconsole.job.models.Job.is_complete', - return_value=True): - self.assertTrue(job_service.is_ready(job_4)) + self.assertTrue(job_service.is_ready(job_4)) + + @patch('fedlearner_webconsole.job.models.Job.is_flapp_failed') + @patch('fedlearner_webconsole.job.models.Job.is_flapp_complete') + def test_update_running_state(self, mock_is_complete, mock_is_failed): + job_0 = db.session.query(Job).get(0) + job_2 = db.session.query(Job).get(2) + mock_is_complete.return_value = True + job_service = JobService(db.session) + job_service.update_running_state(job_0.name) + self.assertEqual(job_0.state, JobState.COMPLETED) + self.assertTrue(job_service.is_ready(job_2)) + job_0.state = JobState.STARTED + mock_is_complete.return_value = False + mock_is_failed = True + job_service.update_running_state(job_0.name) + self.assertEqual(job_0.state, JobState.FAILED) + + diff --git a/web_console_v2/api/test/fedlearner_webconsole/job/yaml_formatter_test.py b/web_console_v2/api/test/fedlearner_webconsole/job/yaml_formatter_test.py index 7b76efb0b..8ad1e6269 100644 --- a/web_console_v2/api/test/fedlearner_webconsole/job/yaml_formatter_test.py +++ b/web_console_v2/api/test/fedlearner_webconsole/job/yaml_formatter_test.py @@ -104,7 +104,7 @@ def test_generate_self_dict(self): } ] } - job = Job(name='aa', project_id=1, workflow_id=1, state=JobState.STOPPED) + job = Job(name='aa', project_id=1, workflow_id=1, state=JobState.NEW) job.set_config(ParseDict(config, JobDefinition())) self.assertEqual(generate_self_dict(job), {'id': None, 'name': 'aa', diff --git a/web_console_v2/api/test/fedlearner_webconsole/mmgr/model_test.py b/web_console_v2/api/test/fedlearner_webconsole/mmgr/model_test.py index deaef699c..84c8644e2 100644 --- a/web_console_v2/api/test/fedlearner_webconsole/mmgr/model_test.py +++ b/web_console_v2/api/test/fedlearner_webconsole/mmgr/model_test.py @@ -15,9 +15,10 @@ # coding: utf-8 import unittest -from unittest.mock import patch +from unittest.mock import MagicMock, patch + from testing.common import BaseTestCase -from fedlearner_webconsole.db import db, get_session, make_session_context +from fedlearner_webconsole.db import db, get_session from fedlearner_webconsole.mmgr.models import Model from fedlearner_webconsole.mmgr.models import ModelState from fedlearner_webconsole.mmgr.service import ModelService @@ -26,48 +27,80 @@ class ModelTest(BaseTestCase): - @patch( - 'fedlearner_webconsole.mmgr.service.ModelService.get_checkpoint_path' - ) + 'fedlearner_webconsole.mmgr.service.ModelService.get_checkpoint_path') def setUp(self, mock_get_checkpoint_path): super().setUp() self.model_service = ModelService(db.session) self.train_job = Job(name='train-job', - job_type=JobType.NN_MODEL_TRANINING) + job_type=JobType.NN_MODEL_TRANINING, + workflow_id=1, + project_id=1) self.eval_job = Job(name='eval-job', - job_type=JobType.NN_MODEL_EVALUATION) + job_type=JobType.NN_MODEL_EVALUATION, + workflow_id=1, + project_id=1) mock_get_checkpoint_path.return_value = 'output' self.model_service.create(job=self.train_job, parent_job_name=None) - model = db.session.query(Model).filter_by(job_name=self.train_job.name).one() - self.model_service.create(job=self.eval_job, parent_job_name=model.job_name) + model = db.session.query(Model).filter_by( + job_name=self.train_job.name).one() + self.model_service.create(job=self.eval_job, + parent_job_name=model.job_name) + db.session.add(self.train_job) + db.session.add(self.eval_job) db.session.commit() - @patch('fedlearner_webconsole.job.models.Job.is_complete') - @patch('fedlearner_webconsole.job.models.Job.is_failed') - def test_on_job_update(self, mock_is_failed, mock_is_complete): - model = Model.query.filter_by(job_name=self.train_job.name).one() - self.assertEqual(model.state, ModelState.COMMITTED.value) - self.train_job.state = JobState.STARTED - - mock_is_failed.return_value = False - mock_is_complete.return_value = False - self.model_service.on_job_update(self.train_job) - self.assertEqual(model.state, ModelState.RUNNING.value) - - mock_is_failed.return_value = False - mock_is_complete.return_value = True - self.model_service.on_job_update(self.train_job) - self.assertEqual(model.state, ModelState.SUCCEEDED.value) - - mock_is_failed.return_value = True - mock_is_complete.return_value = False - self.model_service.on_job_update(self.train_job) - self.assertEqual(model.state, ModelState.FAILED.value) - - @patch('fedlearner_webconsole.job.models.Job.is_complete') - @patch('fedlearner_webconsole.job.models.Job.is_failed') - def test_hook(self, mock_is_failed, mock_is_complete): + @patch('fedlearner_webconsole.mmgr.service.ModelService.plot_metrics') + def test_on_job_update(self, mock_plot_metrics: MagicMock): + mock_plot_metrics.return_value = 'plot metrics return' + + # TODO: change get_session to db.session_scope + with get_session(db.engine) as session: + model = session.query(Model).filter_by( + job_name=self.train_job.name).one() + self.assertEqual(model.state, ModelState.COMMITTED.value) + + train_job = session.query(Job).filter_by(name='train-job').one() + train_job.state = JobState.STARTED + session.commit() + + # TODO: change get_session to db.session_scope + with get_session(db.engine) as session: + train_job = session.query(Job).filter_by(name='train-job').one() + train_job.state = JobState.STARTED + model = session.query(Model).filter_by( + job_name=self.train_job.name).one() + model_service = ModelService(session) + + model_service.on_job_update(train_job) + self.assertEqual(model.state, ModelState.RUNNING.value) + session.commit() + + # TODO: change get_session to db.session_scope + with get_session(db.engine) as session: + train_job = session.query(Job).filter_by(name='train-job').one() + train_job.state = JobState.COMPLETED + model = session.query(Model).filter_by( + job_name=self.train_job.name).one() + model_service = ModelService(session) + + model_service.on_job_update(train_job) + self.assertEqual(model.state, ModelState.SUCCEEDED.value) + session.commit() + + # TODO: change get_session to db.session_scope + with get_session(db.engine) as session: + train_job = session.query(Job).filter_by(name='train-job').one() + train_job.state = JobState.FAILED + model = session.query(Model).filter_by( + job_name=self.train_job.name).one() + model_service = ModelService(session) + + model_service.on_job_update(train_job) + self.assertEqual(model.state, ModelState.FAILED.value) + session.commit() + + def test_hook(self): train_job = Job(id=0, state=JobState.STARTED, name='nn-train', @@ -85,27 +118,23 @@ def test_hook(self, mock_is_failed, mock_is_complete): self.assertEqual(model.state, ModelState.COMMITTED.value) event.event_type = EventType.MODIFIED - mock_is_failed.return_value = False - mock_is_complete.return_value = False + train_job.state = JobState.STARTED self.model_service.k8s_watcher_hook(event) self.assertEqual(model.state, ModelState.RUNNING.value) - mock_is_failed.return_value = False - mock_is_complete.return_value = True + train_job.state = JobState.COMPLETED self.model_service.k8s_watcher_hook(event) self.assertEqual(model.state, ModelState.SUCCEEDED.value) - mock_is_failed.return_value = False - mock_is_complete.return_value = False + train_job.state = JobState.STARTED self.model_service.k8s_watcher_hook(event) self.assertEqual(model.state, ModelState.RUNNING.value) self.assertEqual(model.version, 2) train_job.state = JobState.STOPPED - db.session.add(train_job) - db.session.commit() self.model_service.k8s_watcher_hook(event) self.assertEqual(model.state, ModelState.PAUSED.value) + db.session.rollback() def test_api(self): resp = self.get_helper('/api/v2/models/1') diff --git a/web_console_v2/api/test/fedlearner_webconsole/project/apis_test.py b/web_console_v2/api/test/fedlearner_webconsole/project/apis_test.py index 48d577064..c91401d73 100644 --- a/web_console_v2/api/test/fedlearner_webconsole/project/apis_test.py +++ b/web_console_v2/api/test/fedlearner_webconsole/project/apis_test.py @@ -155,14 +155,18 @@ def test_list_project(self): self.assertEqual(project, result) def test_update_project(self): + updated_name = 'updated name' updated_comment = 'updated comment' update_response = self.patch_helper( '/api/v2/projects/{}'.format(1), data={ + 'participant_name': updated_name, 'comment': updated_comment }) self.assertEqual(update_response.status_code, HTTPStatus.OK) queried_project = Project.query.filter_by(id=1).first() + participant = queried_project.get_config().participants[0] + self.assertEqual(participant.name, updated_name) self.assertEqual(queried_project.comment, updated_comment) def test_update_not_found_project(self): diff --git a/web_console_v2/api/test/fedlearner_webconsole/rpc/client_test.py b/web_console_v2/api/test/fedlearner_webconsole/rpc/client_test.py index 2cc3e9113..b2200e4ea 100644 --- a/web_console_v2/api/test/fedlearner_webconsole/rpc/client_test.py +++ b/web_console_v2/api/test/fedlearner_webconsole/rpc/client_test.py @@ -20,14 +20,15 @@ from grpc import StatusCode from grpc.framework.foundation import logging_pool -from testing.common import create_test_db +from testing.common import NoWebServerTestCase from fedlearner_webconsole.proto.service_pb2 import DESCRIPTOR from fedlearner_webconsole.rpc.client import RpcClient from fedlearner_webconsole.project.models import Project as ProjectModel from fedlearner_webconsole.job.models import Job -from fedlearner_webconsole.proto.common_pb2 import (GrpcSpec, Status, StatusCode - as FedLearnerStatusCode) +from fedlearner_webconsole.proto.common_pb2 import (GrpcSpec, Status, + StatusCode as + FedLearnerStatusCode) from fedlearner_webconsole.proto.project_pb2 import Project, Participant from fedlearner_webconsole.proto.service_pb2 import (CheckConnectionRequest, ProjAuthInfo) @@ -37,7 +38,7 @@ TARGET_SERVICE = DESCRIPTOR.services_by_name['WebConsoleV2Service'] -class RpcClientTest(unittest.TestCase): +class RpcClientTest(NoWebServerTestCase): _TEST_PROJECT_NAME = 'test-project' _TEST_RECEIVER_NAME = 'test-receiver' _TEST_URL = 'localhost:123' @@ -46,10 +47,9 @@ class RpcClientTest(unittest.TestCase): _TEST_X_HOST = 'default.fedlearner.webconsole' _TEST_SELF_DOMAIN_NAME = 'fl-test-self.com' - _DB = create_test_db() - @classmethod def setUpClass(cls): + grpc_spec = GrpcSpec( authority=cls._TEST_AUTHORITY, extra_headers={cls._X_HOST_HEADER_KEY: cls._TEST_X_HOST}) @@ -71,16 +71,6 @@ def setUpClass(cls): cls._project.set_config(project_config) cls._job = job - # Inserts the project entity - cls._DB.create_all() - cls._DB.session.add(cls._project) - cls._DB.session.commit() - - @classmethod - def tearDownClass(cls): - cls._DB.session.remove() - cls._DB.drop_all() - def setUp(self): self._client_execution_thread_pool = logging_pool.pool(1) diff --git a/web_console_v2/api/test/fedlearner_webconsole/scheduler/scheduler_test.py b/web_console_v2/api/test/fedlearner_webconsole/scheduler/scheduler_test.py index 57771bded..95caafac4 100644 --- a/web_console_v2/api/test/fedlearner_webconsole/scheduler/scheduler_test.py +++ b/web_console_v2/api/test/fedlearner_webconsole/scheduler/scheduler_test.py @@ -22,6 +22,7 @@ import logging from http import HTTPStatus +from envs import Envs from testing.common import BaseTestCase from fedlearner_webconsole.proto.common_pb2 import CreateJobFlag from fedlearner_webconsole.job.models import Job @@ -32,7 +33,7 @@ class LeaderConfig(object): - SQLALCHEMY_DATABASE_URI = 'sqlite://' + SQLALCHEMY_DATABASE_URI = f'sqlite:///{Envs.BASE_DIR}/leader.db' SQLALCHEMY_TRACK_MODIFICATIONS = False JWT_SECRET_KEY = secrets.token_urlsafe(64) PROPAGATE_EXCEPTIONS = True @@ -42,7 +43,7 @@ class LeaderConfig(object): class FollowerConfig(object): - SQLALCHEMY_DATABASE_URI = 'sqlite://' + SQLALCHEMY_DATABASE_URI = f'sqlite:///{Envs.BASE_DIR}/follower.db' SQLALCHEMY_TRACK_MODIFICATIONS = False JWT_SECRET_KEY = secrets.token_urlsafe(64) PROPAGATE_EXCEPTIONS = True @@ -65,6 +66,7 @@ def setUp(self): 'group_alias': 'test-template', 'job_definitions': [{ + 'is_federated': True, 'name': 'job1', 'variables': [{ @@ -73,6 +75,7 @@ def setUp(self): 'access_mode': 3 }] }, { + 'is_federated': True, 'name': 'job2', 'variables': [{ diff --git a/web_console_v2/api/test/fedlearner_webconsole/scheduler/workflow_commit_test.py b/web_console_v2/api/test/fedlearner_webconsole/scheduler/workflow_commit_test.py index fd06d65f9..b94468c09 100644 --- a/web_console_v2/api/test/fedlearner_webconsole/scheduler/workflow_commit_test.py +++ b/web_console_v2/api/test/fedlearner_webconsole/scheduler/workflow_commit_test.py @@ -29,6 +29,7 @@ from fedlearner_webconsole.proto import project_pb2 from workflow_template_test import make_workflow_template + class WorkflowsCommitTest(BaseTestCase): class Config(BaseTestCase.Config): START_GRPC_SERVER = False @@ -42,61 +43,56 @@ def setUp(self): super().setUp() # Inserts project config = { - 'participants': [ - { - 'name': 'party_leader', - 'url': '127.0.0.1:5000', - 'domain_name': 'fl-leader.com', - 'grpc_spec': { - 'authority': 'fl-leader.com' - } - } - ], - 'variables': [ - { - 'name': 'namespace', - 'value': 'leader' - }, - { - 'name': 'basic_envs', - 'value': '{}' - }, - { - 'name': 'storage_root_dir', - 'value': '/' - }, - { - 'name': 'EGRESS_URL', - 'value': '127.0.0.1:1991' + 'participants': [{ + 'name': 'party_leader', + 'url': '127.0.0.1:5000', + 'domain_name': 'fl-leader.com', + 'grpc_spec': { + 'authority': 'fl-leader.com' } - ] + }], + 'variables': [{ + 'name': 'namespace', + 'value': 'leader' + }, { + 'name': 'basic_envs', + 'value': '{}' + }, { + 'name': 'storage_root_dir', + 'value': '/' + }, { + 'name': 'EGRESS_URL', + 'value': '127.0.0.1:1991' + }] } - project = Project(name='test', - config=ParseDict(config, - project_pb2.Project()).SerializeToString()) + project = Project( + name='test', + config=ParseDict(config, + project_pb2.Project()).SerializeToString()) db.session.add(project) db.session.commit() @staticmethod - def _wait_until(cond): - while True: - time.sleep(1) + def _wait_until(cond, retry_times: int = 5): + for _ in range(retry_times): + time.sleep(5) + db.session.expire_all() if cond(): return - - @patch('fedlearner_webconsole.workflow.models.Job.is_failed') - @patch('fedlearner_webconsole.workflow.models.Job.is_complete') - def test_workflow_commit(self, mock_is_complete, mock_is_failed): - mock_is_complete.return_value = False - mock_is_failed.return_value = False + def test_workflow_commit(self): # test the committing stage for workflow creating workflow_def = make_workflow_template() - workflow = Workflow(id=20, name='job_test1', comment='这是一个测试工作流', - config=workflow_def.SerializeToString(), - project_id=1, forkable=True, state=WorkflowState.NEW, - target_state=WorkflowState.READY, - transaction_state=TransactionState.PARTICIPANT_COMMITTING) + workflow = Workflow( + id=20, + name='job_test1', + comment='这是一个测试工作流', + config=workflow_def.SerializeToString(), + project_id=1, + forkable=True, + state=WorkflowState.NEW, + target_state=WorkflowState.READY, + transaction_state=TransactionState.PARTICIPANT_COMMITTING) db.session.add(workflow) db.session.commit() scheduler.wakeup(20) @@ -104,8 +100,8 @@ def test_workflow_commit(self, mock_is_complete, mock_is_failed): lambda: Workflow.query.get(20).state == WorkflowState.READY) workflow = Workflow.query.get(20) self.assertEqual(len(workflow.get_jobs()), 2) - self.assertEqual(workflow.get_jobs()[0].state, JobState.STOPPED) - self.assertEqual(workflow.get_jobs()[1].state, JobState.STOPPED) + self.assertEqual(workflow.get_jobs()[0].state, JobState.NEW) + self.assertEqual(workflow.get_jobs()[1].state, JobState.NEW) # test the committing stage for workflow running workflow.target_state = WorkflowState.RUNNING @@ -118,15 +114,17 @@ def test_workflow_commit(self, mock_is_complete, mock_is_failed): self._wait_until( lambda: workflow.get_jobs()[0].state == JobState.STARTED) self.assertEqual(workflow.get_jobs()[1].state, JobState.WAITING) - mock_is_complete.return_value = True workflow = Workflow.query.get(20) + for job in workflow.owned_jobs: + job.state = JobState.COMPLETED self.assertEqual(workflow.to_dict()['state'], 'COMPLETED') - mock_is_complete.return_value = False - mock_is_failed.return_value = True + workflow.get_jobs()[0].state = JobState.FAILED self.assertEqual(workflow.to_dict()['state'], 'FAILED') # test the committing stage for workflow stopping workflow.target_state = WorkflowState.STOPPED workflow.transaction_state = TransactionState.PARTICIPANT_COMMITTING + for job in workflow.owned_jobs: + job.state = JobState.STARTED db.session.commit() scheduler.wakeup(20) self._wait_until( diff --git a/web_console_v2/api/test/fedlearner_webconsole/sparkapp/apis_test.py b/web_console_v2/api/test/fedlearner_webconsole/sparkapp/apis_test.py index 1705677c0..711425562 100644 --- a/web_console_v2/api/test/fedlearner_webconsole/sparkapp/apis_test.py +++ b/web_console_v2/api/test/fedlearner_webconsole/sparkapp/apis_test.py @@ -23,8 +23,9 @@ from fedlearner_webconsole.sparkapp.schema import SparkAppInfo from testing.common import BaseTestCase +from envs import Envs -BASE_DIR = os.path.abspath(os.path.join(dirname(__file__), '../../../')) +BASE_DIR = Envs.BASE_DIR class SparkAppApiTest(BaseTestCase): diff --git a/web_console_v2/api/test/fedlearner_webconsole/sparkapp/schema_test.py b/web_console_v2/api/test/fedlearner_webconsole/sparkapp/schema_test.py index 6db23febe..21a0f35ca 100644 --- a/web_console_v2/api/test/fedlearner_webconsole/sparkapp/schema_test.py +++ b/web_console_v2/api/test/fedlearner_webconsole/sparkapp/schema_test.py @@ -120,6 +120,27 @@ def test_sparkapp_info(self): }, 'sparkVersion': '3.0.0', 'type': 'Python', + }, + 'status': { + 'applicationState': { + 'state': 'COMPLETED' + }, + 'driverInfo': { + 'podName': 'fl-transformer-yaml-driver', + 'webUIAddress': '11.249.131.12:4040', + 'webUIPort': 4040, + 'webUIServiceName': 'fl-transformer-yaml-ui-svc' + }, + 'executionAttempts': 1, + 'executorState': { + 'fl-transformer-yaml-bdc15979a314310b-exec-1': 'PENDING', + 'fl-transformer-yaml-bdc15979a314310b-exec-2': 'COMPLETED' + }, + 'lastSubmissionAttemptTime': '2021-05-18T10:31:13Z', + 'sparkApplicationId': 'spark-a380bfd520164d828a334bcb3a6404f9', + 'submissionAttempts': 1, + 'submissionID': '5bc7e2e7-cc0f-420c-8bc7-138b651a1dde', + 'terminationTime': '2021-05-18T10:32:08Z' } } diff --git a/web_console_v2/api/test/fedlearner_webconsole/sparkapp/service_test.py b/web_console_v2/api/test/fedlearner_webconsole/sparkapp/service_test.py index 698eed74b..d59b57520 100644 --- a/web_console_v2/api/test/fedlearner_webconsole/sparkapp/service_test.py +++ b/web_console_v2/api/test/fedlearner_webconsole/sparkapp/service_test.py @@ -20,11 +20,12 @@ from unittest.mock import MagicMock, patch from os.path import dirname -from fedlearner_webconsole.sparkapp.schema import SparkAppConfig +from envs import Envs +from fedlearner_webconsole.sparkapp.schema import SparkAppConfig from fedlearner_webconsole.sparkapp.service import SparkAppService -BASE_DIR = os.path.abspath(os.path.join(dirname(__file__), '../../../')) +BASE_DIR = Envs.BASE_DIR class SparkAppServiceTest(unittest.TestCase): @@ -124,6 +125,27 @@ def test_submit_sparkapp(self, mock_create_sparkapp: MagicMock): }, 'sparkVersion': '3.0.0', 'type': 'Python', + }, + 'status': { + 'applicationState': { + 'state': 'COMPLETED' + }, + 'driverInfo': { + 'podName': 'fl-transformer-yaml-driver', + 'webUIAddress': '11.249.131.12:4040', + 'webUIPort': 4040, + 'webUIServiceName': 'fl-transformer-yaml-ui-svc' + }, + 'executionAttempts': 1, + 'executorState': { + 'fl-transformer-yaml-bdc15979a314310b-exec-1': 'PENDING', + 'fl-transformer-yaml-bdc15979a314310b-exec-2': 'COMPLETED' + }, + 'lastSubmissionAttemptTime': '2021-05-18T10:31:13Z', + 'sparkApplicationId': 'spark-a380bfd520164d828a334bcb3a6404f9', + 'submissionAttempts': 1, + 'submissionID': '5bc7e2e7-cc0f-420c-8bc7-138b651a1dde', + 'terminationTime': '2021-05-18T10:32:08Z' } } @@ -211,6 +233,27 @@ def test_get_sparkapp_info(self, mock_get_sparkapp: MagicMock): }, 'sparkVersion': '3.0.0', 'type': 'Python', + }, + 'status': { + 'applicationState': { + 'state': 'COMPLETED' + }, + 'driverInfo': { + 'podName': 'fl-transformer-yaml-driver', + 'webUIAddress': '11.249.131.12:4040', + 'webUIPort': 4040, + 'webUIServiceName': 'fl-transformer-yaml-ui-svc' + }, + 'executionAttempts': 1, + 'executorState': { + 'fl-transformer-yaml-bdc15979a314310b-exec-1': 'PENDING', + 'fl-transformer-yaml-bdc15979a314310b-exec-2': 'COMPLETED' + }, + 'lastSubmissionAttemptTime': '2021-05-18T10:31:13Z', + 'sparkApplicationId': 'spark-a380bfd520164d828a334bcb3a6404f9', + 'submissionAttempts': 1, + 'submissionID': '5bc7e2e7-cc0f-420c-8bc7-138b651a1dde', + 'terminationTime': '2021-05-18T10:32:08Z' } } diff --git a/web_console_v2/api/test/fedlearner_webconsole/test_data/dataset_metainfo/_FEATURES b/web_console_v2/api/test/fedlearner_webconsole/test_data/dataset_metainfo/_FEATURES new file mode 100644 index 000000000..c4b56a721 --- /dev/null +++ b/web_console_v2/api/test/fedlearner_webconsole/test_data/dataset_metainfo/_FEATURES @@ -0,0 +1 @@ +{"f00312": {"count": "12089", "mean": "0.0015716767309123998", "stddev": "0.03961485047808605", "min": "0", "max": "1", "missing_count": "0"}, "f00207": {"count": "12089", "mean": "7.969889982628836", "stddev": "6.298925249171136", "min": "0", "max": "17", "missing_count": "0"}, "f00197": {"count": "12089", "mean": "5.790387955993051E-4", "stddev": "0.02405725220983047", "min": "0", "max": "1", "missing_count": "0"}, "f00021": {"count": "12089", "mean": "22.46943502357515", "stddev": "62.78511338652187", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00380": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00129": {"count": "12089", "mean": "127.65687815369344", "stddev": "110.84802347935845", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00009": {"count": "12089", "mean": "50.4625692778559", "stddev": "88.88581381823657", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00333": {"count": "12089", "mean": "8.271982794275788E-5", "stddev": "0.009095044141880672", "min": "0", "max": "1", "missing_count": "0"}, "f00036": {"count": "12089", "mean": "40.138555711804116", "stddev": "81.46548446285466", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00000": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0.0", "max": "0.0", "missing_count": "0"}, "f00135": {"count": "12089", "mean": "26.622549425097194", "stddev": "68.94643988549662", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00213": {"count": "12089", "mean": "7.0170402845562085", "stddev": "6.28331287275781", "min": "0", "max": "17", "missing_count": "0"}, "f00389": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00108": {"count": "12089", "mean": "11.317561419472247", "stddev": "45.850510883253484", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00098": {"count": "12089", "mean": "98.65836711059642", "stddev": "113.51899823220415", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00120": {"count": "12089", "mean": "75.51890148068492", "stddev": "104.405877965072", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00306": {"count": "12089", "mean": "8.271982794275788E-5", "stddev": "0.009095044141880672", "min": "0", "max": "1", "missing_count": "0"}, "f00015": {"count": "12089", "mean": "146.97046902142444", "stddev": "107.57419770880752", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00234": {"count": "12089", "mean": "7.249234841591529", "stddev": "5.960881019889561", "min": "0", "max": "16", "missing_count": "0"}, "f00228": {"count": "12089", "mean": "0.30962031598974277", "stddev": "0.9148967149961357", "min": "0", "max": "4", "missing_count": "0"}, "f00114": {"count": "12089", "mean": "0.8296798742658615", "stddev": "11.826621946672537", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00296": {"count": "12089", "mean": "0.44412275622466707", "stddev": "1.2207702582037445", "min": "0", "max": "5", "missing_count": "0"}, "f00327": {"count": "12089", "mean": "0.004384150880966168", "stddev": "0.06607035037106973", "min": "0", "max": "1", "missing_count": "0"}, "f00315": {"count": "12089", "mean": "0.01091901728844404", "stddev": "0.1039263478660159", "min": "0", "max": "1", "missing_count": "0"}, "f00201": {"count": "12089", "mean": "1.6287534121929026", "stddev": "2.84809905187513", "min": "0", "max": "9", "missing_count": "0"}, "f00170": {"count": "12089", "mean": "1.573579286955083", "stddev": "16.69196932487125", "min": "0.0", "max": "255.0", "missing_count": "0"}, "raw_id": {"count": "12089", "mean": "6044.0", "stddev": "3489.938036699219", "min": "0", "max": "12088", "missing_count": "0"}, "f00278": {"count": "12089", "mean": "1.6543965588551577E-4", "stddev": "0.012861802735756753", "min": "0", "max": "1", "missing_count": "0"}, "f00117": {"count": "12089", "mean": "37.35991397137894", "stddev": "81.07098158275089", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00191": {"count": "12089", "mean": "22.46099760112499", "stddev": "62.86460287162058", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00216": {"count": "12089", "mean": "2.7689635205558774", "stddev": "4.064744417560437", "min": "0", "max": "13", "missing_count": "0"}, "f00185": {"count": "12089", "mean": "136.2555215485152", "stddev": "109.1495475656237", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00071": {"count": "12089", "mean": "115.2010091819009", "stddev": "113.15610412333592", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00284": {"count": "12089", "mean": "0.018198362147406732", "stddev": "0.13367370667048667", "min": "0", "max": "1", "missing_count": "0"}, "f00299": {"count": "12089", "mean": "0.06650674166597734", "stddev": "0.3520912962783987", "min": "0", "max": "2", "missing_count": "0"}, "f00179": {"count": "12089", "mean": "116.24807676400033", "stddev": "112.10561248326009", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00362": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00309": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00018": {"count": "12089", "mean": "121.69418479609563", "stddev": "110.4481389455011", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00383": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00377": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00086": {"count": "12089", "mean": "0.5124493341053851", "stddev": "9.554020953390804", "min": "0.0", "max": "254.0", "missing_count": "0"}, "f00300": {"count": "12089", "mean": "0.018446521631235006", "stddev": "0.13456502272330065", "min": "0", "max": "1", "missing_count": "0"}, "f00290": {"count": "12089", "mean": "0.8305070725452891", "stddev": "1.8063905280636319", "min": "0", "max": "7", "missing_count": "0"}, "f00003": {"count": "12089", "mean": "0.16982380676648193", "stddev": "5.260742308809774", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00092": {"count": "12089", "mean": "64.64372570105054", "stddev": "99.94772315175193", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00102": {"count": "12089", "mean": "124.01604764662089", "stddev": "111.03878118521327", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00344": {"count": "12089", "mean": "3.3087931177103153E-4", "stddev": "0.018187830935519945", "min": "0", "max": "1", "missing_count": "0"}, "f00095": {"count": "12089", "mean": "84.91827280999256", "stddev": "108.35978233028061", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00365": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00068": {"count": "12089", "mean": "101.33484986351229", "stddev": "110.4669075685077", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00272": {"count": "12089", "mean": "0.29133923401439327", "stddev": "0.9105375835637824", "min": "0", "max": "4", "missing_count": "0"}, "f00167": {"count": "12089", "mean": "0.020597237157746712", "stddev": "1.6019271796631058", "min": "0.0", "max": "128.0", "missing_count": "0"}, "f00371": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00194": {"count": "12089", "mean": "0.4916039374638101", "stddev": "8.90504079934277", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00080": {"count": "12089", "mean": "7.424269997518405", "stddev": "37.33719005251688", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00074": {"count": "12089", "mean": "122.3101166349574", "stddev": "111.195396244587", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00089": {"count": "12089", "mean": "24.765985606749936", "stddev": "66.94669354107191", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00293": {"count": "12089", "mean": "1.0765985606749937", "stddev": "2.2088405666718938", "min": "0", "max": "8", "missing_count": "0"}, "f00287": {"count": "12089", "mean": "0.2876995615849119", "stddev": "0.9106955529735481", "min": "0", "max": "4", "missing_count": "0"}, "f00152": {"count": "12089", "mean": "96.18181818181819", "stddev": "112.25217366871706", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00188": {"count": "12089", "mean": "85.78013069732815", "stddev": "106.30064004700931", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00251": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00350": {"count": "12089", "mean": "0.0010753577632558525", "stddev": "0.03277636700490423", "min": "0", "max": "1", "missing_count": "0"}, "f00173": {"count": "12089", "mean": "52.889486309868474", "stddev": "93.34585106719244", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00053": {"count": "12089", "mean": "2.0326743320373892", "stddev": "19.627575883927236", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00359": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00266": {"count": "12089", "mean": "3.797419141368186", "stddev": "4.630854277554116", "min": "0", "max": "13", "missing_count": "0"}, "f00386": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00353": {"count": "12089", "mean": "8.271982794275787E-4", "stddev": "0.028750346035965654", "min": "0", "max": "1", "missing_count": "0"}, "f00062": {"count": "12089", "mean": "25.966333030027297", "stddev": "68.53716697947962", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00248": {"count": "12089", "mean": "0.030854495822648688", "stddev": "0.1729305341578591", "min": "0", "max": "1", "missing_count": "0"}, "f00275": {"count": "12089", "mean": "0.023078831996029447", "stddev": "0.15016013013366547", "min": "0", "max": "1", "missing_count": "0"}, "f00347": {"count": "12089", "mean": "9.099181073703367E-4", "stddev": "0.030152369101126467", "min": "0", "max": "1", "missing_count": "0"}, "f00326": {"count": "12089", "mean": "0.0076102241707337245", "stddev": "0.08690761437388105", "min": "0", "max": "1", "missing_count": "0"}, "f00374": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00233": {"count": "12089", "mean": "7.51261477376127", "stddev": "6.336175108889156", "min": "0", "max": "17", "missing_count": "0"}, "f00077": {"count": "12089", "mean": "42.78674828356357", "stddev": "84.18417527697498", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00332": {"count": "12089", "mean": "8.271982794275788E-5", "stddev": "0.009095044141880672", "min": "0", "max": "1", "missing_count": "0"}, "f00155": {"count": "12089", "mean": "109.07047729340724", "stddev": "110.81584969724295", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00035": {"count": "12089", "mean": "24.930846223839854", "stddev": "66.37760252845683", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00161": {"count": "12089", "mean": "69.5351145669617", "stddev": "101.00911072125321", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00056": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0.0", "max": "0.0", "missing_count": "0"}, "f00083": {"count": "12089", "mean": "0.059144676979071886", "stddev": "3.061344888196", "min": "0.0", "max": "223.0", "missing_count": "0"}, "f00269": {"count": "12089", "mean": "1.6582016709405245", "stddev": "2.929709427278822", "min": "0", "max": "10", "missing_count": "0"}, "f00134": {"count": "12089", "mean": "43.70609645131938", "stddev": "85.1577461190761", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00182": {"count": "12089", "mean": "112.46033584250145", "stddev": "110.37074173570745", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00041": {"count": "12089", "mean": "122.2917528331541", "stddev": "110.3844621437854", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00368": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00176": {"count": "12089", "mean": "116.19058648358012", "stddev": "111.5950606075992", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00140": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0.0", "max": "0.0", "missing_count": "0"}, "f00260": {"count": "12089", "mean": "2.6654810157994873", "stddev": "3.8407366321952625", "min": "0", "max": "12", "missing_count": "0"}, "f00281": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00254": {"count": "12089", "mean": "0.004053271569195136", "stddev": "0.06353877960213003", "min": "0", "max": "1", "missing_count": "0"}, "f00149": {"count": "12089", "mean": "97.22565969062785", "stddev": "110.34982443353475", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00023": {"count": "12089", "mean": "4.017950202663578", "stddev": "26.91164598600779", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00158": {"count": "12089", "mean": "135.2027462982877", "stddev": "109.24927327835356", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00017": {"count": "12089", "mean": "143.10224170733724", "stddev": "107.52214831526439", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00050": {"count": "12089", "mean": "15.526759864339482", "stddev": "52.80830936027394", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00236": {"count": "12089", "mean": "7.231698238067665", "stddev": "5.998910757868576", "min": "0", "max": "16", "missing_count": "0"}, "f00143": {"count": "12089", "mean": "9.34130201009182", "stddev": "41.454427254989774", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00044": {"count": "12089", "mean": "135.7823641326826", "stddev": "109.00787635707023", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00038": {"count": "12089", "mean": "81.47365373480024", "stddev": "104.10037134701264", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00242": {"count": "12089", "mean": "3.188849367193316", "stddev": "4.430056635612022", "min": "0", "max": "13", "missing_count": "0"}, "f00116": {"count": "12089", "mean": "19.636032757051865", "stddev": "60.33937808755006", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00137": {"count": "12089", "mean": "5.4586814459425925", "stddev": "31.331337049123192", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00257": {"count": "12089", "mean": "0.3081313590867731", "stddev": "0.9153993013019526", "min": "0", "max": "4", "missing_count": "0"}, "f00320": {"count": "12089", "mean": "0.06278434940855324", "stddev": "0.3479336134540428", "min": "0", "max": "2", "missing_count": "0"}, "f00164": {"count": "12089", "mean": "13.817271900074449", "stddev": "49.910045947034355", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00215": {"count": "12089", "mean": "3.963685995533129", "stddev": "4.840533295725966", "min": "0", "max": "14", "missing_count": "0"}, "f00308": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00122": {"count": "12089", "mean": "82.65232856315659", "stddev": "108.00734853519677", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00341": {"count": "12089", "mean": "1.6543965588551577E-4", "stddev": "0.012861802735756756", "min": "0", "max": "1", "missing_count": "0"}, "f00335": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00329": {"count": "12089", "mean": "9.099181073703367E-4", "stddev": "0.03015236910112646", "min": "0", "max": "1", "missing_count": "0"}, "f00221": {"count": "12089", "mean": "0.018363801803292248", "stddev": "0.1342686257655253", "min": "0", "max": "1", "missing_count": "0"}, "f00356": {"count": "12089", "mean": "2.4815948382827364E-4", "stddev": "0.015751775297162093", "min": "0", "max": "1", "missing_count": "0"}, "f00059": {"count": "12089", "mean": "1.670609645131938", "stddev": "16.617605185022406", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00065": {"count": "12089", "mean": "67.83968897344694", "stddev": "99.88982320561412", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00314": {"count": "12089", "mean": "0.006783025891306146", "stddev": "0.08208272523502139", "min": "0", "max": "1", "missing_count": "0"}, "f00263": {"count": "12089", "mean": "4.683017619323352", "stddev": "5.112106252543775", "min": "0", "max": "14", "missing_count": "0"}, "f00323": {"count": "12089", "mean": "0.021672594921002566", "stddev": "0.14561815681841794", "min": "0", "max": "1", "missing_count": "0"}, "f00203": {"count": "12089", "mean": "5.274712548597899", "stddev": "5.491439999810407", "min": "0", "max": "15", "missing_count": "0"}, "f00026": {"count": "12089", "mean": "0.39308462238398545", "stddev": "7.996566426227542", "min": "0.0", "max": "254.0", "missing_count": "0"}, "f00391": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00125": {"count": "12089", "mean": "91.59459012325254", "stddev": "112.05550695302972", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00032": {"count": "12089", "mean": "2.2057242120936387", "stddev": "19.17599219583217", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00193": {"count": "12089", "mean": "3.351311109272893", "stddev": "23.793676933625665", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00047": {"count": "12089", "mean": "93.57473736454628", "stddev": "107.85198969607525", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00110": {"count": "12089", "mean": "0.9364711721399619", "stddev": "12.433034422804973", "min": "0.0", "max": "253.0", "missing_count": "0"}, "f00245": {"count": "12089", "mean": "0.6315658863429564", "stddev": "1.5311403179365195", "min": "0", "max": "6", "missing_count": "0"}, "f00146": {"count": "12089", "mean": "69.49028042021672", "stddev": "101.90158284684969", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00005": {"count": "12089", "mean": "2.562742989494582", "stddev": "20.401310971721", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00104": {"count": "12089", "mean": "81.10786665563735", "stddev": "104.36877459445203", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00218": {"count": "12089", "mean": "0.8396889734469353", "stddev": "1.8901700948075482", "min": "0", "max": "7", "missing_count": "0"}, "f00131": {"count": "12089", "mean": "116.78550748614443", "stddev": "109.83853395976982", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00317": {"count": "12089", "mean": "0.02109355612540326", "stddev": "0.1437022136462013", "min": "0", "max": "1", "missing_count": "0"}, "f00224": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00230": {"count": "12089", "mean": "2.335842501447597", "stddev": "3.590911567257245", "min": "0", "max": "11", "missing_count": "0"}, "f00239": {"count": "12089", "mean": "5.6979071883530485", "stddev": "5.581541304147346", "min": "0", "max": "15", "missing_count": "0"}, "f00094": {"count": "12089", "mean": "77.82893539581438", "stddev": "105.52048530589909", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00119": {"count": "12089", "mean": "68.77905533956489", "stddev": "101.61313020983097", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00302": {"count": "12089", "mean": "0.006038547439821325", "stddev": "0.0774763184270929", "min": "0", "max": "1", "missing_count": "0"}, "f00292": {"count": "12089", "mean": "1.0894201340061214", "stddev": "2.213914558722607", "min": "0", "max": "8", "missing_count": "0"}, "f00011": {"count": "12089", "mean": "100.26718504425511", "stddev": "109.47690624367854", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00338": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00020": {"count": "12089", "mean": "48.47075854082224", "stddev": "87.6721375293745", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00175": {"count": "12089", "mean": "100.43262470014062", "stddev": "110.3676589106749", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00206": {"count": "12089", "mean": "8.096451319381256", "stddev": "6.449682666545672", "min": "0", "max": "18", "missing_count": "0"}, "f00029": {"count": "12089", "mean": "0.01869468111506328", "stddev": "2.05547997606503", "min": "0.0", "max": "226.0", "missing_count": "0"}, "f00274": {"count": "12089", "mean": "0.07279344858962693", "stddev": "0.3603832071918777", "min": "0", "max": "2", "missing_count": "0"}, "f00128": {"count": "12089", "mean": "114.94449499545041", "stddev": "112.13157547309869", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00373": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00196": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00295": {"count": "12089", "mean": "0.6386797915460336", "stddev": "1.546000832656912", "min": "0", "max": "6", "missing_count": "0"}, "f00305": {"count": "12089", "mean": "4.135991397137894E-4", "stddev": "0.020333771833917866", "min": "0", "max": "1", "missing_count": "0"}, "f00289": {"count": "12089", "mean": "0.6302423690958723", "stddev": "1.4983772214095004", "min": "0", "max": "6", "missing_count": "0"}, "f00212": {"count": "12089", "mean": "6.817271900074448", "stddev": "5.974489070884012", "min": "0", "max": "16", "missing_count": "0"}, "f00181": {"count": "12089", "mean": "109.2416246174208", "stddev": "110.54913441390448", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00076": {"count": "12089", "mean": "70.24402349243114", "stddev": "100.58430391863487", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00280": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00014": {"count": "12089", "mean": "140.2613946562991", "stddev": "109.92329691158204", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00388": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00227": {"count": "12089", "mean": "0.07750847878236414", "stddev": "0.36544744196793927", "min": "0", "max": "2", "missing_count": "0"}, "f00082": {"count": "12089", "mean": "0.8301761932335181", "stddev": "11.676183114704884", "min": "0.0", "max": "254.0", "missing_count": "0"}, "f00008": {"count": "12089", "mean": "28.923153279841177", "stddev": "70.18855997812747", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00113": {"count": "12089", "mean": "0.020928116469517744", "stddev": "1.8185770446835063", "min": "0.0", "max": "197.0", "missing_count": "0"}, "f00107": {"count": "12089", "mean": "20.359665811895113", "stddev": "60.86079945589884", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00097": {"count": "12089", "mean": "95.71230043841508", "stddev": "112.9203359237509", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00311": {"count": "12089", "mean": "3.3087931177103153E-4", "stddev": "0.018187830935519955", "min": "0", "max": "1", "missing_count": "0"}, "f00184": {"count": "12089", "mean": "132.06774753908513", "stddev": "109.61468807022078", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00079": {"count": "12089", "mean": "13.677309951195301", "stddev": "50.07519014387303", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00101": {"count": "12089", "mean": "122.54561998511043", "stddev": "111.69458255685223", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00002": {"count": "12089", "mean": "0.007527504342790967", "stddev": "0.6438064000663923", "min": "0.0", "max": "69.0", "missing_count": "0"}, "f00070": {"count": "12089", "mean": "111.54016047646621", "stddev": "112.96571914608131", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00064": {"count": "12089", "mean": "53.12217718587145", "stddev": "92.07991097584222", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00209": {"count": "12089", "mean": "7.506162627181736", "stddev": "6.335218435677862", "min": "0", "max": "17", "missing_count": "0"}, "f00283": {"count": "12089", "mean": "0.00703118537513442", "stddev": "0.08356030986125715", "min": "0", "max": "1", "missing_count": "0"}, "f00277": {"count": "12089", "mean": "0.002233435354454463", "stddev": "0.047208383503186485", "min": "0", "max": "1", "missing_count": "0"}, "f00091": {"count": "12089", "mean": "54.057821159731986", "stddev": "93.49125294106308", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00163": {"count": "12089", "mean": "27.330465712631316", "stddev": "69.49360582065684", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00058": {"count": "12089", "mean": "0.32682604020183637", "stddev": "7.428200945373068", "min": "0.0", "max": "254.0", "missing_count": "0"}, "f00190": {"count": "12089", "mean": "39.01877740094301", "stddev": "80.64309515978327", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00200": {"count": "12089", "mean": "0.6344610803209529", "stddev": "1.5439106575916022", "min": "0", "max": "6", "missing_count": "0"}, "f00376": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00199": {"count": "12089", "mean": "0.16171726362809166", "stddev": "0.6209476669547512", "min": "0", "max": "3", "missing_count": "0"}, "f00085": {"count": "12089", "mean": "0.02034907767391844", "stddev": "1.4125452537479488", "min": "0.0", "max": "133.0", "missing_count": "0"}, "f00298": {"count": "12089", "mean": "0.09305980643560262", "stddev": "0.38333800268403145", "min": "0", "max": "2", "missing_count": "0"}, "f00157": {"count": "12089", "mean": "135.45338737695425", "stddev": "109.17069463834251", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00361": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00256": {"count": "12089", "mean": "0.09256348746794607", "stddev": "0.3823779459828604", "min": "0", "max": "2", "missing_count": "0"}, "f00262": {"count": "12089", "mean": "4.334270824716684", "stddev": "4.9958968239833395", "min": "0", "max": "14", "missing_count": "0"}, "f00355": {"count": "12089", "mean": "4.135991397137894E-4", "stddev": "0.020333771833917845", "min": "0", "max": "1", "missing_count": "0"}, "f00178": {"count": "12089", "mean": "120.58689717925387", "stddev": "112.42989836060832", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00382": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00364": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00358": {"count": "12089", "mean": "8.271982794275788E-5", "stddev": "0.009095044141880676", "min": "0", "max": "1", "missing_count": "0"}, "f00067": {"count": "12089", "mean": "92.11828935395815", "stddev": "107.880515215011", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00271": {"count": "12089", "mean": "0.4656299114897841", "stddev": "1.2226481099427282", "min": "0", "max": "5", "missing_count": "0"}, "f00286": {"count": "12089", "mean": "0.15824303085449581", "stddev": "0.616900267416377", "min": "0", "max": "3", "missing_count": "0"}, "f00244": {"count": "12089", "mean": "1.0981884357680536", "stddev": "2.2035418049877964", "min": "0", "max": "8", "missing_count": "0"}, "f00379": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00337": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00151": {"count": "12089", "mean": "96.78128877491935", "stddev": "111.93369626697235", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00259": {"count": "12089", "mean": "1.6998097443957316", "stddev": "2.9808512186328775", "min": "0", "max": "10", "missing_count": "0"}, "f00073": {"count": "12089", "mean": "126.48788154520639", "stddev": "110.94063872421407", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00172": {"count": "12089", "mean": "29.05037637521714", "stddev": "72.62815547455598", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00166": {"count": "12089", "mean": "0.6783025891306146", "stddev": "10.775627874334255", "min": "0.0", "max": "253.0", "missing_count": "0"}, "f00385": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00088": {"count": "12089", "mean": "11.935892133344362", "stddev": "47.22589878357941", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00139": {"count": "12089", "mean": "0.04020183638018033", "stddev": "2.305908122408037", "min": "0.0", "max": "153.0", "missing_count": "0"}, "f00343": {"count": "12089", "mean": "2.4815948382827364E-4", "stddev": "0.0157517752971621", "min": "0", "max": "1", "missing_count": "0"}, "f00052": {"count": "12089", "mean": "4.207378608652494", "stddev": "28.074171257801822", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00046": {"count": "12089", "mean": "122.67954338654975", "stddev": "110.99635048312369", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00187": {"count": "12089", "mean": "110.35875589378774", "stddev": "110.55640335116942", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00238": {"count": "12089", "mean": "6.148316651501365", "stddev": "5.678318834411271", "min": "0", "max": "15", "missing_count": "0"}, "f00250": {"count": "12089", "mean": "9.099181073703367E-4", "stddev": "0.030152369101126463", "min": "0", "max": "1", "missing_count": "0"}, "f00370": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00265": {"count": "12089", "mean": "4.134006121267268", "stddev": "4.719032514814078", "min": "0", "max": "13", "missing_count": "0"}, "f00145": {"count": "12089", "mean": "48.23409711307801", "stddev": "90.33569256677949", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00028": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0.0", "max": "0.0", "missing_count": "0"}, "f00169": {"count": "12089", "mean": "0.061295392505583586", "stddev": "3.047245179854696", "min": "0.0", "max": "185.0", "missing_count": "0"}, "f00232": {"count": "12089", "mean": "5.942426999751841", "stddev": "5.896385905213651", "min": "0", "max": "16", "missing_count": "0"}, "f00352": {"count": "12089", "mean": "9.926379353130945E-4", "stddev": "0.03149181864169439", "min": "0", "max": "1", "missing_count": "0"}, "f00127": {"count": "12089", "mean": "101.6755728348085", "stddev": "112.17723446501321", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00055": {"count": "12089", "mean": "0.05327156919513607", "stddev": "2.648309231570656", "min": "0.0", "max": "190.0", "missing_count": "0"}, "f00310": {"count": "12089", "mean": "1.6543965588551577E-4", "stddev": "0.012861802735756758", "min": "0", "max": "1", "missing_count": "0"}, "f00154": {"count": "12089", "mean": "99.49805608404334", "stddev": "110.42238643156286", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00049": {"count": "12089", "mean": "31.03631400446687", "stddev": "73.15778023500965", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00253": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00367": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00331": {"count": "12089", "mean": "8.271982794275788E-5", "stddev": "0.009095044141880676", "min": "0", "max": "1", "missing_count": "0"}, "f00325": {"count": "12089", "mean": "0.012573413847299198", "stddev": "0.11142867759187992", "min": "0", "max": "1", "missing_count": "0"}, "f00034": {"count": "12089", "mean": "13.855571180411944", "stddev": "49.713397507452996", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00247": {"count": "12089", "mean": "0.1573331127471255", "stddev": "0.6182713526833413", "min": "0", "max": "3", "missing_count": "0"}, "f00319": {"count": "12089", "mean": "0.029944577715278354", "stddev": "0.17044149441025006", "min": "0", "max": "1", "missing_count": "0"}, "f00160": {"count": "12089", "mean": "94.73463479195964", "stddev": "108.36629716102476", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00346": {"count": "12089", "mean": "8.271982794275787E-4", "stddev": "0.02875034603596567", "min": "0", "max": "1", "missing_count": "0"}, "f00040": {"count": "12089", "mean": "113.14327074199686", "stddev": "109.84226245861849", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00148": {"count": "12089", "mean": "93.87616841756969", "stddev": "109.01563028272538", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00226": {"count": "12089", "mean": "0.008437422450161304", "stddev": "0.09147089407626231", "min": "0", "max": "1", "missing_count": "0"}, "f00061": {"count": "12089", "mean": "14.143188022168914", "stddev": "50.65214934645993", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00268": {"count": "12089", "mean": "2.3289767557283483", "stddev": "3.5853110900424756", "min": "0", "max": "11", "missing_count": "0"}, "f00133": {"count": "12089", "mean": "65.5060799073538", "stddev": "99.12986246813963", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00022": {"count": "12089", "mean": "8.979650922326082", "stddev": "39.77712456709073", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00142": {"count": "12089", "mean": "1.3251716436429812", "stddev": "15.47932255385905", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00037": {"count": "12089", "mean": "59.9388700471503", "stddev": "94.72699453234434", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00241": {"count": "12089", "mean": "4.451567540739515", "stddev": "5.214539351483691", "min": "0", "max": "15", "missing_count": "0"}, "f00334": {"count": "12089", "mean": "8.271982794275788E-5", "stddev": "0.009095044141880672", "min": "0", "max": "1", "missing_count": "0"}, "f00229": {"count": "12089", "mean": "1.086442220200182", "stddev": "2.2141075688107272", "min": "0", "max": "8", "missing_count": "0"}, "f00043": {"count": "12089", "mean": "132.4968152866242", "stddev": "109.93326299918938", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00121": {"count": "12089", "mean": "78.94383323682686", "stddev": "105.7465897141154", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00001": {"count": "12089", "mean": "0.01323517247084126", "stddev": "1.057570263408246", "min": "0.0", "max": "99.0", "missing_count": "0"}, "f00115": {"count": "12089", "mean": "6.736785507486144", "stddev": "34.6562065366746", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00307": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00016": {"count": "12089", "mean": "149.63578459756803", "stddev": "106.31345045889744", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00220": {"count": "12089", "mean": "0.0876002977913806", "stddev": "0.3770193023195932", "min": "0", "max": "2", "missing_count": "0"}, "f00214": {"count": "12089", "mean": "5.902638762511374", "stddev": "5.87517974874268", "min": "0", "max": "16", "missing_count": "0"}, "f00109": {"count": "12089", "mean": "5.193977996525767", "stddev": "30.916493868435143", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00328": {"count": "12089", "mean": "0.0023988750103399784", "stddev": "0.048921553367709744", "min": "0", "max": "1", "missing_count": "0"}, "f00136": {"count": "12089", "mean": "14.2381503846472", "stddev": "50.832117307731565", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00340": {"count": "12089", "mean": "8.271982794275788E-5", "stddev": "0.009095044141880667", "min": "0", "max": "1", "missing_count": "0"}, "f00349": {"count": "12089", "mean": "9.926379353130945E-4", "stddev": "0.031491818641694355", "min": "0", "max": "1", "missing_count": "0"}, "f00100": {"count": "12089", "mean": "111.91397137893954", "stddev": "112.72662611143275", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00235": {"count": "12089", "mean": "7.245016130366449", "stddev": "5.975173808089956", "min": "0", "max": "16", "missing_count": "0"}, "f00313": {"count": "12089", "mean": "0.003556952601538589", "stddev": "0.05953649215335385", "min": "0", "max": "1", "missing_count": "0"}, "f00208": {"count": "12089", "mean": "7.521631235007031", "stddev": "6.319573669900177", "min": "0", "max": "17", "missing_count": "0"}, "f00186": {"count": "12089", "mean": "129.9644304739846", "stddev": "109.8452037535058", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00217": {"count": "12089", "mean": "1.6715195632393085", "stddev": "2.9719709285257014", "min": "0", "max": "10", "missing_count": "0"}, "f00025": {"count": "12089", "mean": "1.1352469186864091", "stddev": "14.074838590340523", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00390": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00384": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00093": {"count": "12089", "mean": "71.79386218876665", "stddev": "103.22560088342621", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00285": {"count": "12089", "mean": "0.06733393994540492", "stddev": "0.35404327285513265", "min": "0", "max": "2", "missing_count": "0"}, "f00103": {"count": "12089", "mean": "108.62867069236496", "stddev": "109.98131124810823", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00202": {"count": "12089", "mean": "3.416080734552072", "stddev": "4.423602482890862", "min": "0", "max": "13", "missing_count": "0"}, "f00192": {"count": "12089", "mean": "11.229299363057326", "stddev": "44.94778055349097", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00031": {"count": "12089", "mean": "0.5988088344776243", "stddev": "9.70173242684006", "min": "0.0", "max": "254.0", "missing_count": "0"}, "f00124": {"count": "12089", "mean": "88.7223922574241", "stddev": "111.03041701868655", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00130": {"count": "12089", "mean": "130.69021424435437", "stddev": "110.07541968527855", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00019": {"count": "12089", "mean": "85.66663909339069", "stddev": "105.4276935007043", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00087": {"count": "12089", "mean": "3.809744395731657", "stddev": "25.920786926965643", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00301": {"count": "12089", "mean": "0.009926379353130945", "stddev": "0.09913959537480871", "min": "0", "max": "1", "missing_count": "0"}, "f00010": {"count": "12089", "mean": "76.15460335842502", "stddev": "102.68836546035861", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00004": {"count": "12089", "mean": "0.7785590205972371", "stddev": "10.986462860696165", "min": "0.0", "max": "254.0", "missing_count": "0"}, "f00223": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00118": {"count": "12089", "mean": "55.26817768219042", "stddev": "94.35202600920245", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00291": {"count": "12089", "mean": "1.07858383654562", "stddev": "2.198672689278667", "min": "0", "max": "8", "missing_count": "0"}, "f00322": {"count": "12089", "mean": "0.026966663909339068", "stddev": "0.1619926962870867", "min": "0", "max": "1", "missing_count": "0"}, "f00316": {"count": "12089", "mean": "0.015551327653238481", "stddev": "0.12373661690904557", "min": "0", "max": "1", "missing_count": "0"}, "f00013": {"count": "12089", "mean": "131.31838861775168", "stddev": "110.71027810705532", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00189": {"count": "12089", "mean": "61.11332616428158", "stddev": "96.67351803163615", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00195": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0.0", "max": "0.0", "missing_count": "0"}, "f00112": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0.0", "max": "0.0", "missing_count": "0"}, "f00081": {"count": "12089", "mean": "3.595913640499628", "stddev": "25.670929304588366", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00007": {"count": "12089", "mean": "14.629415170816445", "stddev": "50.59485801451282", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00211": {"count": "12089", "mean": "6.821904210439242", "stddev": "5.978061865979983", "min": "0", "max": "16", "missing_count": "0"}, "f00180": {"count": "12089", "mean": "111.31235007031185", "stddev": "111.11268456059636", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00366": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00075": {"count": "12089", "mean": "100.55488460584002", "stddev": "109.45989244366011", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00069": {"count": "12089", "mean": "107.7127140375548", "stddev": "112.3468540877581", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00288": {"count": "12089", "mean": "0.4475969889982629", "stddev": "1.2037283759312305", "min": "0", "max": "5", "missing_count": "0"}, "f00174": {"count": "12089", "mean": "78.69972702456779", "stddev": "105.22183163444049", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00387": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00205": {"count": "12089", "mean": "8.0", "stddev": "6.331562444720613", "min": "0", "max": "17", "missing_count": "0"}, "f00060": {"count": "12089", "mean": "5.7965919430887585", "stddev": "32.18877971708031", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00304": {"count": "12089", "mean": "0.0015716767309123998", "stddev": "0.03961485047808601", "min": "0", "max": "1", "missing_count": "0"}, "f00273": {"count": "12089", "mean": "0.15973198775746547", "stddev": "0.6187933067584905", "min": "0", "max": "3", "missing_count": "0"}, "f00294": {"count": "12089", "mean": "0.8423360079411035", "stddev": "1.8570588236371768", "min": "0", "max": "7", "missing_count": "0"}, "f00168": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0.0", "max": "0.0", "missing_count": "0"}, "f00372": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00267": {"count": "12089", "mean": "3.1091074530564975", "stddev": "4.261348449492594", "min": "0", "max": "13", "missing_count": "0"}, "f00106": {"count": "12089", "mean": "34.45950864422202", "stddev": "76.81328321940498", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00096": {"count": "12089", "mean": "90.9721234179833", "stddev": "111.33220332628122", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00360": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00369": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00183": {"count": "12089", "mean": "120.96525767226404", "stddev": "110.18288051322584", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00141": {"count": "12089", "mean": "0.033005211349160395", "stddev": "1.9509163733029178", "min": "0.0", "max": "166.0", "missing_count": "0"}, "f00090": {"count": "12089", "mean": "40.18032922491521", "stddev": "83.35755041571151", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00354": {"count": "12089", "mean": "5.790387955993051E-4", "stddev": "0.024057252209830484", "min": "0", "max": "1", "missing_count": "0"}, "f00249": {"count": "12089", "mean": "0.009512780213417156", "stddev": "0.09707248168499928", "min": "0", "max": "1", "missing_count": "0"}, "f00084": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0.0", "max": "0.0", "missing_count": "0"}, "f00063": {"count": "12089", "mean": "38.99966912068823", "stddev": "82.02909344030543", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00198": {"count": "12089", "mean": "0.01166349573992886", "stddev": "0.10737044395610035", "min": "0", "max": "1", "missing_count": "0"}, "f00297": {"count": "12089", "mean": "0.18661593183886177", "stddev": "0.6334876659516991", "min": "0", "max": "3", "missing_count": "0"}, "f00156": {"count": "12089", "mean": "123.76226321449252", "stddev": "110.6793588639301", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00255": {"count": "12089", "mean": "0.02291339234014393", "stddev": "0.14963362224724616", "min": "0", "max": "1", "missing_count": "0"}, "f00078": {"count": "12089", "mean": "24.413847299197617", "stddev": "66.22492190582788", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00282": {"count": "12089", "mean": "0.0010753577632558525", "stddev": "0.032776367004904286", "min": "0", "max": "1", "missing_count": "0"}, "f00177": {"count": "12089", "mean": "122.51269749358922", "stddev": "111.89599689676162", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00042": {"count": "12089", "mean": "128.07585408222351", "stddev": "110.366261006918", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00381": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00240": {"count": "12089", "mean": "5.473571014972289", "stddev": "5.788460172878884", "min": "0", "max": "16", "missing_count": "0"}, "f00276": {"count": "12089", "mean": "0.009264620729588882", "stddev": "0.0958099517944918", "min": "0", "max": "1", "missing_count": "0"}, "f00099": {"count": "12089", "mean": "103.48225659690628", "stddev": "113.22365623216328", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00162": {"count": "12089", "mean": "46.30143105302341", "stddev": "87.10377794599654", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00348": {"count": "12089", "mean": "0.0012407974191413682", "stddev": "0.03520455028234003", "min": "0", "max": "1", "missing_count": "0"}, "f00057": {"count": "12089", "mean": "0.01646124576060882", "stddev": "1.2833803824909489", "min": "0.0", "max": "107.0", "missing_count": "0"}, "f00261": {"count": "12089", "mean": "3.9408553230209282", "stddev": "4.861386978193615", "min": "0", "max": "14", "missing_count": "0"}, "f00375": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00039": {"count": "12089", "mean": "99.77177599470593", "stddev": "108.31829419146803", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00237": {"count": "12089", "mean": "6.5093059806435605", "stddev": "5.6578573995671455", "min": "0", "max": "15", "missing_count": "0"}, "f00051": {"count": "12089", "mean": "7.6584498304243525", "stddev": "37.189378357535595", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00243": {"count": "12089", "mean": "2.0367276036065847", "stddev": "3.369183505988024", "min": "0", "max": "11", "missing_count": "0"}, "f00357": {"count": "12089", "mean": "1.6543965588551577E-4", "stddev": "0.012861802735756751", "min": "0", "max": "1", "missing_count": "0"}, "f00150": {"count": "12089", "mean": "97.63123500703118", "stddev": "111.40678708057216", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00270": {"count": "12089", "mean": "0.8702953097857556", "stddev": "1.8930413308456493", "min": "0", "max": "7", "missing_count": "0"}, "f00024": {"count": "12089", "mean": "2.2891885184878817", "stddev": "20.550395266526383", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00165": {"count": "12089", "mean": "4.835552982049798", "stddev": "29.151866610566092", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00159": {"count": "12089", "mean": "119.84432128381172", "stddev": "109.38668553919553", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00363": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00258": {"count": "12089", "mean": "0.8446521631235007", "stddev": "1.8386759859743027", "min": "0", "max": "7", "missing_count": "0"}, "f00123": {"count": "12089", "mean": "85.5991397137894", "stddev": "109.8586042350572", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00378": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00321": {"count": "12089", "mean": "0.06253618992472496", "stddev": "0.34809715324227564", "min": "0", "max": "2", "missing_count": "0"}, "f00336": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00342": {"count": "12089", "mean": "1.6543965588551577E-4", "stddev": "0.012861802735756756", "min": "0", "max": "1", "missing_count": "0"}, "f00045": {"count": "12089", "mean": "136.13441972040698", "stddev": "109.50079499209558", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00264": {"count": "12089", "mean": "4.694102076267681", "stddev": "5.135851246319809", "min": "0", "max": "14", "missing_count": "0"}, "f00072": {"count": "12089", "mean": "120.74977252047316", "stddev": "112.17942783302091", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00171": {"count": "12089", "mean": "10.538754239391181", "stddev": "43.87517272978043", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00222": {"count": "12089", "mean": "0.00272975432211101", "stddev": "0.05217784942384286", "min": "0", "max": "1", "missing_count": "0"}, "f00030": {"count": "12089", "mean": "0.10141450905782116", "stddev": "3.6217098742574323", "min": "0.0", "max": "254.0", "missing_count": "0"}, "f00066": {"count": "12089", "mean": "80.45983952353379", "stddev": "104.43754476777885", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00138": {"count": "12089", "mean": "0.6825213003556952", "stddev": "10.372143610013369", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00144": {"count": "12089", "mean": "26.195963272396394", "stddev": "68.96982859941298", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00279": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00153": {"count": "12089", "mean": "96.34485896269335", "stddev": "111.13053348852189", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00012": {"count": "12089", "mean": "118.59442468359666", "stddev": "111.38255919311455", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00111": {"count": "12089", "mean": "0.03391512945653073", "stddev": "1.5822396665547114", "min": "0.0", "max": "102.0", "missing_count": "0"}, "f00027": {"count": "12089", "mean": "0.011001737116386797", "stddev": "0.7069626758862559", "min": "0.0", "max": "52.0", "missing_count": "0"}, "f00033": {"count": "12089", "mean": "6.480602200347423", "stddev": "33.72465553421396", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00147": {"count": "12089", "mean": "85.32881131607246", "stddev": "107.40750641089546", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00246": {"count": "12089", "mean": "0.2963851435189015", "stddev": "0.9128581829116996", "min": "0", "max": "4", "missing_count": "0"}, "f00132": {"count": "12089", "mean": "90.81727190007445", "stddev": "107.22421003303963", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00345": {"count": "12089", "mean": "4.135991397137894E-4", "stddev": "0.02033377183391784", "min": "0", "max": "1", "missing_count": "0"}, "f00204": {"count": "12089", "mean": "6.807510960377202", "stddev": "5.992888723745428", "min": "0", "max": "16", "missing_count": "0"}, "f00048": {"count": "12089", "mean": "58.81553478368765", "stddev": "94.13977835736269", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00231": {"count": "12089", "mean": "4.466208950285384", "stddev": "5.257014839826631", "min": "0", "max": "15", "missing_count": "0"}, "f00303": {"count": "12089", "mean": "0.0038051120853668623", "stddev": "0.06157066504311279", "min": "0", "max": "1", "missing_count": "0"}, "f00126": {"count": "12089", "mean": "94.28621060468194", "stddev": "112.25243505297904", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00006": {"count": "12089", "mean": "6.519315079824634", "stddev": "33.39763014346889", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00330": {"count": "12089", "mean": "4.135991397137894E-4", "stddev": "0.02033377183391786", "min": "0", "max": "1", "missing_count": "0"}, "f00339": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00219": {"count": "12089", "mean": "0.3002729754322111", "stddev": "0.913988335427884", "min": "0", "max": "4", "missing_count": "0"}, "f00225": {"count": "12089", "mean": "2.4815948382827364E-4", "stddev": "0.015751775297162093", "min": "0", "max": "1", "missing_count": "0"}, "f00252": {"count": "12089", "mean": "0.0", "stddev": "0.0", "min": "0", "max": "0", "missing_count": "0"}, "f00324": {"count": "12089", "mean": "0.017371163867979156", "stddev": "0.13065534290273315", "min": "0", "max": "1", "missing_count": "0"}, "f00351": {"count": "12089", "mean": "9.926379353130945E-4", "stddev": "0.03149181864169435", "min": "0", "max": "1", "missing_count": "0"}, "f00210": {"count": "12089", "mean": "6.837786417404252", "stddev": "5.996903149368257", "min": "0", "max": "16", "missing_count": "0"}, "f00105": {"count": "12089", "mean": "54.79915625775499", "stddev": "92.83279267076065", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00054": {"count": "12089", "mean": "0.6340474811812391", "stddev": "10.467706738001793", "min": "0.0", "max": "255.0", "missing_count": "0"}, "f00318": {"count": "12089", "mean": "0.025891306146083218", "stddev": "0.15881760877519568", "min": "0", "max": "1", "missing_count": "0"}} \ No newline at end of file diff --git a/web_console_v2/api/test/fedlearner_webconsole/test_data/dataset_metainfo/_HIST b/web_console_v2/api/test/fedlearner_webconsole/test_data/dataset_metainfo/_HIST new file mode 100644 index 000000000..0f7948d5c --- /dev/null +++ b/web_console_v2/api/test/fedlearner_webconsole/test_data/dataset_metainfo/_HIST @@ -0,0 +1 @@ +{"f00312": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12070, 0, 0, 0, 0, 0, 0, 0, 0, 19]}, "f00207": {"x": [0.0, 1.7, 3.4, 5.1, 6.8, 8.5, 10.2, 11.9, 13.6, 15.299999999999999, 17], "y": [3415, 748, 754, 387, 748, 750, 394, 747, 3098, 1048]}, "f00197": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12082, 0, 0, 0, 0, 0, 0, 0, 0, 7]}, "f00021": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [10427, 222, 162, 127, 117, 128, 110, 117, 132, 547]}, "f00380": {"x": [0, 0], "y": [12089]}, "f00129": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [4384, 406, 404, 360, 370, 388, 402, 482, 598, 4295]}, "f00009": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [8594, 338, 291, 261, 227, 265, 226, 228, 283, 1376]}, "f00333": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12088, 0, 0, 0, 0, 0, 0, 0, 0, 1]}, "f00036": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [9249, 339, 243, 199, 187, 209, 167, 185, 224, 1087]}, "f00000": {"x": [0.0, 0.0], "y": [12089]}, "f00135": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [10241, 171, 147, 137, 133, 139, 135, 123, 173, 690]}, "f00213": {"x": [0.0, 1.7, 3.4, 5.1, 6.8, 8.5, 10.2, 11.9, 13.6, 15.299999999999999, 17], "y": [4165, 766, 751, 366, 770, 744, 397, 740, 2591, 799]}, "f00389": {"x": [0, 0], "y": [12089]}, "f00108": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11250, 108, 61, 67, 63, 76, 77, 56, 69, 262]}, "f00098": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [6319, 316, 277, 240, 225, 246, 208, 264, 399, 3595]}, "f00120": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [7339, 319, 340, 269, 231, 263, 256, 279, 326, 2467]}, "f00306": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12088, 0, 0, 0, 0, 0, 0, 0, 0, 1]}, "f00015": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [3377, 401, 401, 370, 404, 445, 467, 562, 593, 5069]}, "f00234": {"x": [0.0, 1.6, 3.2, 4.800000000000001, 6.4, 8.0, 9.600000000000001, 11.200000000000001, 12.8, 14.4, 16], "y": [3781, 753, 387, 759, 375, 752, 749, 384, 3120, 1029]}, "f00228": {"x": [0.0, 0.4, 0.8, 1.2000000000000002, 1.6, 2.0, 2.4000000000000004, 2.8000000000000003, 3.2, 3.6, 4], "y": [10609, 0, 346, 0, 0, 380, 0, 379, 0, 375]}, "f00114": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [12015, 13, 6, 11, 11, 9, 5, 3, 1, 15]}, "f00296": {"x": [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5], "y": [10485, 0, 93, 0, 377, 0, 390, 0, 368, 376]}, "f00327": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12036, 0, 0, 0, 0, 0, 0, 0, 0, 53]}, "f00315": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [11957, 0, 0, 0, 0, 0, 0, 0, 0, 132]}, "f00201": {"x": [0.0, 0.9, 1.8, 2.7, 3.6, 4.5, 5.4, 6.3, 7.2, 8.1, 9], "y": [8544, 145, 385, 373, 376, 380, 381, 394, 791, 320]}, "f00170": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11961, 16, 9, 17, 15, 16, 7, 13, 9, 26]}, "raw_id": {"x": [0.0, 1208.8, 2417.6, 3626.3999999999996, 4835.2, 6044.0, 7252.799999999999, 8461.6, 9670.4, 10879.199999999999, 12088], "y": [1209, 1209, 1209, 1209, 1208, 1209, 1209, 1209, 1209, 1209]}, "f00278": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12087, 0, 0, 0, 0, 0, 0, 0, 0, 2]}, "f00117": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [9624, 207, 204, 128, 135, 162, 156, 178, 208, 1087]}, "f00191": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [10446, 183, 142, 135, 152, 136, 104, 129, 115, 547]}, "f00216": {"x": [0.0, 1.3, 2.6, 3.9000000000000004, 5.2, 6.5, 7.800000000000001, 9.1, 10.4, 11.700000000000001, 13], "y": [7562, 378, 377, 757, 374, 385, 765, 407, 694, 390]}, "f00185": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [3845, 467, 409, 375, 370, 470, 464, 475, 601, 4613]}, "f00071": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [5239, 390, 338, 305, 269, 302, 333, 397, 497, 4019]}, "f00284": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [11869, 0, 0, 0, 0, 0, 0, 0, 0, 220]}, "f00299": {"x": [0.0, 0.2, 0.4, 0.6000000000000001, 0.8, 1.0, 1.2000000000000002, 1.4000000000000001, 1.6, 1.8, 2], "y": [11659, 0, 0, 0, 0, 56, 0, 0, 0, 374]}, "f00179": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [5063, 410, 380, 296, 336, 387, 333, 423, 475, 3986]}, "f00362": {"x": [0, 0], "y": [12089]}, "f00309": {"x": [0, 0], "y": [12089]}, "f00018": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [4620, 464, 405, 320, 377, 439, 395, 519, 580, 3970]}, "f00383": {"x": [0, 0], "y": [12089]}, "f00377": {"x": [0, 0], "y": [12089]}, "f00086": {"x": [0.0, 25.4, 50.8, 76.19999999999999, 101.6, 127.0, 152.39999999999998, 177.79999999999998, 203.2, 228.6, 254.0], "y": [12045, 4, 11, 2, 3, 5, 6, 0, 4, 9]}, "f00300": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [11866, 0, 0, 0, 0, 0, 0, 0, 0, 223]}, "f00290": {"x": [0.0, 0.7, 1.4, 2.0999999999999996, 2.8, 3.5, 4.199999999999999, 4.8999999999999995, 5.6, 6.3, 7], "y": [9557, 265, 387, 0, 377, 372, 0, 580, 375, 176]}, "f00003": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [12073, 5, 2, 0, 2, 1, 1, 3, 0, 2]}, "f00092": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [8001, 311, 258, 211, 212, 234, 217, 264, 296, 2085]}, "f00102": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [4550, 452, 386, 348, 341, 422, 415, 453, 546, 4176]}, "f00344": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12085, 0, 0, 0, 0, 0, 0, 0, 0, 4]}, "f00095": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [6925, 292, 291, 274, 213, 238, 304, 337, 369, 2846]}, "f00365": {"x": [0, 0], "y": [12089]}, "f00068": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [5826, 394, 362, 364, 291, 326, 312, 407, 432, 3375]}, "f00272": {"x": [0.0, 0.4, 0.8, 1.2000000000000002, 1.6, 2.0, 2.4000000000000004, 2.8000000000000003, 3.2, 3.6, 4], "y": [10828, 0, 128, 0, 0, 379, 0, 380, 0, 374]}, "f00167": {"x": [0.0, 12.8, 25.6, 38.400000000000006, 51.2, 64.0, 76.80000000000001, 89.60000000000001, 102.4, 115.2, 128.0], "y": [12087, 0, 0, 0, 0, 0, 0, 0, 0, 2]}, "f00371": {"x": [0, 0], "y": [12089]}, "f00194": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [12044, 7, 5, 4, 6, 8, 4, 5, 1, 5]}, "f00080": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11529, 73, 56, 43, 37, 54, 35, 43, 61, 158]}, "f00074": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [4684, 413, 369, 345, 357, 379, 395, 475, 601, 4071]}, "f00089": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [10356, 175, 151, 124, 121, 128, 120, 112, 138, 664]}, "f00293": {"x": [0.0, 0.8, 1.6, 2.4000000000000004, 3.2, 4.0, 4.800000000000001, 5.6000000000000005, 6.4, 7.2, 8], "y": [9360, 89, 376, 411, 0, 350, 369, 468, 440, 226]}, "f00287": {"x": [0.0, 0.4, 0.8, 1.2000000000000002, 1.6, 2.0, 2.4000000000000004, 2.8000000000000003, 3.2, 3.6, 4], "y": [10874, 0, 85, 0, 0, 375, 0, 377, 0, 378]}, "f00152": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [6389, 320, 295, 220, 241, 267, 256, 299, 376, 3426]}, "f00188": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [6646, 371, 358, 301, 293, 320, 336, 359, 423, 2682]}, "f00251": {"x": [0, 0], "y": [12089]}, "f00350": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12076, 0, 0, 0, 0, 0, 0, 0, 0, 13]}, "f00173": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [8701, 252, 245, 174, 192, 227, 178, 213, 223, 1684]}, "f00053": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11931, 21, 22, 9, 11, 10, 16, 16, 10, 43]}, "f00359": {"x": [0, 0], "y": [12089]}, "f00266": {"x": [0.0, 1.3, 2.6, 3.9000000000000004, 5.2, 6.5, 7.800000000000001, 9.1, 10.4, 11.700000000000001, 13], "y": [6426, 381, 387, 780, 346, 377, 761, 733, 1125, 773]}, "f00386": {"x": [0, 0], "y": [12089]}, "f00353": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12079, 0, 0, 0, 0, 0, 0, 0, 0, 10]}, "f00062": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [10295, 181, 141, 124, 124, 135, 129, 102, 143, 715]}, "f00248": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [11716, 0, 0, 0, 0, 0, 0, 0, 0, 373]}, "f00275": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [11810, 0, 0, 0, 0, 0, 0, 0, 0, 279]}, "f00347": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12078, 0, 0, 0, 0, 0, 0, 0, 0, 11]}, "f00326": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [11997, 0, 0, 0, 0, 0, 0, 0, 0, 92]}, "f00374": {"x": [0, 0], "y": [12089]}, "f00233": {"x": [0.0, 1.7, 3.4, 5.1, 6.8, 8.5, 10.2, 11.9, 13.6, 15.299999999999999, 17], "y": [3797, 752, 745, 375, 776, 739, 386, 776, 2760, 983]}, "f00077": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [9160, 289, 233, 196, 184, 211, 207, 195, 208, 1206]}, "f00332": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12088, 0, 0, 0, 0, 0, 0, 0, 0, 1]}, "f00155": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [5344, 444, 415, 336, 328, 347, 321, 425, 567, 3562]}, "f00035": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [10265, 241, 181, 133, 120, 129, 116, 109, 160, 635]}, "f00161": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [7603, 358, 320, 243, 249, 237, 266, 312, 376, 2125]}, "f00056": {"x": [0.0, 0.0], "y": [12089]}, "f00083": {"x": [0.0, 22.3, 44.6, 66.9, 89.2, 111.5, 133.8, 156.1, 178.4, 200.70000000000002, 223.0], "y": [12084, 1, 0, 0, 1, 0, 1, 1, 0, 1]}, "f00269": {"x": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], "y": [8553, 155, 368, 380, 367, 383, 373, 380, 603, 527]}, "f00134": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [9129, 273, 206, 203, 201, 223, 183, 199, 247, 1225]}, "f00182": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [5098, 474, 399, 367, 361, 386, 384, 432, 527, 3661]}, "f00041": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [4554, 413, 500, 374, 362, 394, 415, 499, 548, 4030]}, "f00368": {"x": [0, 0], "y": [12089]}, "f00176": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [5030, 395, 406, 330, 326, 394, 346, 429, 519, 3914]}, "f00140": {"x": [0.0, 0.0], "y": [12089]}, "f00260": {"x": [0.0, 1.2, 2.4, 3.5999999999999996, 4.8, 6.0, 7.199999999999999, 8.4, 9.6, 10.799999999999999, 12], "y": [7557, 377, 379, 376, 388, 750, 373, 706, 766, 417]}, "f00281": {"x": [0, 0], "y": [12089]}, "f00254": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12040, 0, 0, 0, 0, 0, 0, 0, 0, 49]}, "f00149": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [6118, 349, 407, 286, 273, 303, 313, 392, 373, 3275]}, "f00023": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11765, 44, 43, 26, 34, 23, 32, 23, 17, 82]}, "f00158": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [3887, 485, 416, 417, 357, 405, 444, 502, 645, 4531]}, "f00017": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [3486, 436, 431, 395, 386, 461, 460, 565, 668, 4801]}, "f00050": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [10907, 154, 129, 115, 73, 85, 78, 89, 109, 350]}, "f00236": {"x": [0.0, 1.6, 3.2, 4.800000000000001, 6.4, 8.0, 9.600000000000001, 11.200000000000001, 12.8, 14.4, 16], "y": [3801, 734, 377, 771, 366, 757, 753, 402, 3034, 1094]}, "f00143": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11390, 78, 57, 54, 58, 92, 48, 62, 55, 195]}, "f00044": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [3848, 456, 452, 360, 394, 462, 458, 486, 610, 4563]}, "f00038": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [6753, 429, 404, 326, 293, 322, 328, 360, 427, 2447]}, "f00242": {"x": [0.0, 1.3, 2.6, 3.9000000000000004, 5.2, 6.5, 7.800000000000001, 9.1, 10.4, 11.700000000000001, 13], "y": [7207, 370, 359, 759, 380, 370, 757, 427, 396, 1064]}, "f00116": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [10727, 138, 94, 99, 93, 121, 105, 98, 88, 526]}, "f00137": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11649, 54, 60, 39, 36, 39, 42, 31, 42, 97]}, "f00257": {"x": [0.0, 0.4, 0.8, 1.2000000000000002, 1.6, 2.0, 2.4000000000000004, 2.8000000000000003, 3.2, 3.6, 4], "y": [10630, 0, 325, 0, 0, 380, 0, 376, 0, 378]}, "f00320": {"x": [0.0, 0.2, 0.4, 0.6000000000000001, 0.8, 1.0, 1.2000000000000002, 1.4000000000000001, 1.6, 1.8, 2], "y": [11706, 0, 0, 0, 0, 7, 0, 0, 0, 376]}, "f00164": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11034, 132, 120, 75, 79, 94, 91, 80, 80, 304]}, "f00215": {"x": [0.0, 1.4, 2.8, 4.199999999999999, 5.6, 7.0, 8.399999999999999, 9.799999999999999, 11.2, 12.6, 14], "y": [6427, 378, 757, 382, 375, 764, 378, 1075, 1007, 546]}, "f00308": {"x": [0, 0], "y": [12089]}, "f00122": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [7084, 300, 256, 233, 226, 254, 275, 315, 345, 2801]}, "f00341": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12087, 0, 0, 0, 0, 0, 0, 0, 0, 2]}, "f00335": {"x": [0, 0], "y": [12089]}, "f00329": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12078, 0, 0, 0, 0, 0, 0, 0, 0, 11]}, "f00221": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [11867, 0, 0, 0, 0, 0, 0, 0, 0, 222]}, "f00356": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12086, 0, 0, 0, 0, 0, 0, 0, 0, 3]}, "f00059": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11936, 27, 23, 15, 19, 19, 10, 5, 9, 26]}, "f00065": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [7664, 367, 300, 263, 231, 275, 297, 292, 365, 2035]}, "f00314": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12007, 0, 0, 0, 0, 0, 0, 0, 0, 82]}, "f00263": {"x": [0.0, 1.4, 2.8, 4.199999999999999, 5.6, 7.0, 8.399999999999999, 9.799999999999999, 11.2, 12.6, 14], "y": [5669, 379, 768, 379, 372, 755, 369, 1327, 1358, 713]}, "f00323": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [11827, 0, 0, 0, 0, 0, 0, 0, 0, 262]}, "f00203": {"x": [0.0, 1.5, 3.0, 4.5, 6.0, 7.5, 9.0, 10.5, 12.0, 13.5, 15], "y": [5301, 387, 737, 389, 753, 384, 769, 353, 2301, 715]}, "f00026": {"x": [0.0, 25.4, 50.8, 76.19999999999999, 101.6, 127.0, 152.39999999999998, 177.79999999999998, 203.2, 228.6, 254.0], "y": [12054, 4, 3, 6, 6, 2, 3, 4, 4, 3]}, "f00391": {"x": [0, 0], "y": [12089]}, "f00125": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [6739, 269, 253, 186, 216, 260, 242, 278, 346, 3300]}, "f00032": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11890, 36, 32, 17, 19, 19, 17, 15, 11, 33]}, "f00193": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11795, 51, 34, 31, 32, 31, 23, 26, 14, 52]}, "f00047": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [6134, 437, 419, 252, 318, 402, 348, 383, 467, 2929]}, "f00110": {"x": [0.0, 25.3, 50.6, 75.9, 101.2, 126.5, 151.8, 177.1, 202.4, 227.70000000000002, 253.0], "y": [12009, 9, 7, 7, 11, 10, 12, 11, 5, 8]}, "f00245": {"x": [0.0, 0.6, 1.2, 1.7999999999999998, 2.4, 3.0, 3.5999999999999996, 4.2, 4.8, 5.3999999999999995, 6], "y": [10023, 178, 0, 381, 0, 380, 372, 0, 463, 292]}, "f00146": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [7706, 290, 298, 216, 249, 292, 222, 273, 306, 2237]}, "f00005": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11851, 48, 30, 29, 25, 27, 17, 11, 15, 36]}, "f00104": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [6824, 434, 338, 302, 300, 355, 322, 338, 404, 2472]}, "f00218": {"x": [0.0, 0.7, 1.4, 2.0999999999999996, 2.8, 3.5, 4.199999999999999, 4.8999999999999995, 5.6, 6.3, 7], "y": [9782, 59, 370, 0, 371, 376, 0, 382, 418, 331]}, "f00131": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [4821, 449, 403, 413, 342, 448, 456, 462, 508, 3787]}, "f00317": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [11834, 0, 0, 0, 0, 0, 0, 0, 0, 255]}, "f00224": {"x": [0, 0], "y": [12089]}, "f00230": {"x": [0.0, 1.1, 2.2, 3.3000000000000003, 4.4, 5.5, 6.6000000000000005, 7.700000000000001, 8.8, 9.9, 11], "y": [7933, 385, 380, 373, 406, 352, 383, 375, 600, 902]}, "f00239": {"x": [0.0, 1.5, 3.0, 4.5, 6.0, 7.5, 9.0, 10.5, 12.0, 13.5, 15], "y": [4928, 370, 766, 359, 760, 380, 756, 372, 2554, 844]}, "f00094": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [7248, 323, 294, 259, 240, 260, 288, 277, 324, 2576]}, "f00119": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [7742, 293, 319, 211, 235, 251, 232, 275, 334, 2197]}, "f00302": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12016, 0, 0, 0, 0, 0, 0, 0, 0, 73]}, "f00292": {"x": [0.0, 0.8, 1.6, 2.4000000000000004, 3.2, 4.0, 4.800000000000001, 5.6000000000000005, 6.4, 7.2, 8], "y": [9263, 181, 384, 372, 0, 381, 381, 441, 458, 228]}, "f00011": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [5764, 446, 429, 313, 341, 377, 338, 393, 448, 3240]}, "f00338": {"x": [0, 0], "y": [12089]}, "f00020": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [8724, 361, 277, 231, 204, 236, 228, 251, 255, 1322]}, "f00175": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [5905, 376, 382, 313, 287, 329, 343, 401, 408, 3345]}, "f00206": {"x": [0.0, 1.8, 3.6, 5.4, 7.2, 9.0, 10.8, 12.6, 14.4, 16.2, 18], "y": [3412, 753, 757, 747, 395, 747, 758, 1390, 1996, 1134]}, "f00029": {"x": [0.0, 22.6, 45.2, 67.80000000000001, 90.4, 113.0, 135.60000000000002, 158.20000000000002, 180.8, 203.4, 226.0], "y": [12088, 0, 0, 0, 0, 0, 0, 0, 0, 1]}, "f00274": {"x": [0.0, 0.2, 0.4, 0.6000000000000001, 0.8, 1.0, 1.2000000000000002, 1.4000000000000001, 1.6, 1.8, 2], "y": [11586, 0, 0, 0, 0, 126, 0, 0, 0, 377]}, "f00128": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [5125, 424, 393, 318, 305, 344, 326, 414, 524, 3916]}, "f00373": {"x": [0, 0], "y": [12089]}, "f00196": {"x": [0, 0], "y": [12089]}, "f00295": {"x": [0.0, 0.6, 1.2, 1.7999999999999998, 2.4, 3.0, 3.5999999999999996, 4.2, 4.8, 5.3999999999999995, 6], "y": [9997, 203, 0, 382, 0, 376, 376, 0, 408, 347]}, "f00305": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12084, 0, 0, 0, 0, 0, 0, 0, 0, 5]}, "f00289": {"x": [0.0, 0.6, 1.2, 1.7999999999999998, 2.4, 3.0, 3.5999999999999996, 4.2, 4.8, 5.3999999999999995, 6], "y": [9920, 285, 0, 373, 0, 379, 376, 0, 589, 167]}, "f00212": {"x": [0.0, 1.6, 3.2, 4.800000000000001, 6.4, 8.0, 9.600000000000001, 11.200000000000001, 12.8, 14.4, 16], "y": [4178, 741, 372, 780, 354, 761, 768, 388, 2747, 1000]}, "f00181": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [5291, 472, 414, 347, 321, 397, 356, 396, 500, 3595]}, "f00076": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [7483, 408, 326, 247, 281, 296, 271, 305, 364, 2108]}, "f00280": {"x": [0, 0], "y": [12089]}, "f00014": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [3738, 434, 464, 340, 369, 391, 391, 517, 510, 4935]}, "f00388": {"x": [0, 0], "y": [12089]}, "f00227": {"x": [0.0, 0.2, 0.4, 0.6000000000000001, 0.8, 1.0, 1.2000000000000002, 1.4000000000000001, 1.6, 1.8, 2], "y": [11527, 0, 0, 0, 0, 187, 0, 0, 0, 375]}, "f00082": {"x": [0.0, 25.4, 50.8, 76.19999999999999, 101.6, 127.0, 152.39999999999998, 177.79999999999998, 203.2, 228.6, 254.0], "y": [12014, 7, 17, 4, 9, 7, 10, 7, 6, 8]}, "f00008": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [9951, 267, 217, 165, 163, 154, 153, 135, 180, 704]}, "f00113": {"x": [0.0, 19.7, 39.4, 59.099999999999994, 78.8, 98.5, 118.19999999999999, 137.9, 157.6, 177.29999999999998, 197.0], "y": [12087, 1, 0, 0, 0, 0, 0, 0, 0, 1]}, "f00107": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [10633, 174, 106, 97, 108, 144, 86, 106, 116, 519]}, "f00097": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [6501, 292, 266, 208, 224, 247, 223, 273, 388, 3467]}, "f00311": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12085, 0, 0, 0, 0, 0, 0, 0, 0, 4]}, "f00184": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [4057, 463, 429, 354, 401, 429, 411, 484, 696, 4365]}, "f00079": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11075, 131, 78, 77, 83, 89, 87, 67, 87, 315]}, "f00101": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [4706, 406, 413, 326, 308, 358, 391, 466, 538, 4177]}, "f00002": {"x": [0.0, 6.9, 13.8, 20.700000000000003, 27.6, 34.5, 41.400000000000006, 48.300000000000004, 55.2, 62.1, 69.0], "y": [12086, 2, 0, 0, 0, 0, 0, 0, 0, 1]}, "f00070": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [5411, 368, 362, 308, 278, 334, 326, 337, 451, 3914]}, "f00064": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [8548, 333, 249, 220, 218, 225, 211, 235, 291, 1559]}, "f00209": {"x": [0.0, 1.7, 3.4, 5.1, 6.8, 8.5, 10.2, 11.9, 13.6, 15.299999999999999, 17], "y": [3802, 737, 750, 380, 760, 776, 367, 768, 2732, 1017]}, "f00283": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12004, 0, 0, 0, 0, 0, 0, 0, 0, 85]}, "f00277": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12062, 0, 0, 0, 0, 0, 0, 0, 0, 27]}, "f00091": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [8587, 262, 261, 207, 183, 237, 172, 252, 280, 1648]}, "f00163": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [10179, 165, 161, 155, 140, 159, 129, 136, 152, 713]}, "f00058": {"x": [0.0, 25.4, 50.8, 76.19999999999999, 101.6, 127.0, 152.39999999999998, 177.79999999999998, 203.2, 228.6, 254.0], "y": [12061, 3, 3, 3, 3, 4, 4, 3, 3, 2]}, "f00190": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [9354, 268, 252, 206, 189, 197, 179, 168, 219, 1057]}, "f00200": {"x": [0.0, 0.6, 1.2, 1.7999999999999998, 2.4, 3.0, 3.5999999999999996, 4.2, 4.8, 5.3999999999999995, 6], "y": [10018, 198, 0, 362, 0, 403, 359, 0, 391, 358]}, "f00376": {"x": [0, 0], "y": [12089]}, "f00199": {"x": [0.0, 0.3, 0.6, 0.8999999999999999, 1.2, 1.5, 1.7999999999999998, 2.1, 2.4, 2.6999999999999997, 3], "y": [11267, 0, 0, 67, 0, 0, 377, 0, 0, 378]}, "f00085": {"x": [0.0, 13.3, 26.6, 39.900000000000006, 53.2, 66.5, 79.80000000000001, 93.10000000000001, 106.4, 119.7, 133.0], "y": [12086, 0, 1, 0, 0, 1, 0, 0, 0, 1]}, "f00298": {"x": [0.0, 0.2, 0.4, 0.6000000000000001, 0.8, 1.0, 1.2000000000000002, 1.4000000000000001, 1.6, 1.8, 2], "y": [11342, 0, 0, 0, 0, 369, 0, 0, 0, 378]}, "f00157": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [3882, 449, 425, 384, 392, 441, 424, 521, 647, 4524]}, "f00361": {"x": [0, 0], "y": [12089]}, "f00256": {"x": [0.0, 0.2, 0.4, 0.6000000000000001, 0.8, 1.0, 1.2000000000000002, 1.4000000000000001, 1.6, 1.8, 2], "y": [11346, 0, 0, 0, 0, 367, 0, 0, 0, 376]}, "f00262": {"x": [0.0, 1.4, 2.8, 4.199999999999999, 5.6, 7.0, 8.399999999999999, 9.799999999999999, 11.2, 12.6, 14], "y": [6052, 373, 760, 371, 387, 761, 364, 1167, 1212, 642]}, "f00355": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12084, 0, 0, 0, 0, 0, 0, 0, 0, 5]}, "f00178": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [4830, 437, 381, 304, 340, 379, 356, 390, 449, 4223]}, "f00382": {"x": [0, 0], "y": [12089]}, "f00364": {"x": [0, 0], "y": [12089]}, "f00358": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12088, 0, 0, 0, 0, 0, 0, 0, 0, 1]}, "f00067": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [6256, 386, 391, 343, 324, 331, 365, 335, 416, 2942]}, "f00271": {"x": [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5], "y": [10223, 0, 366, 0, 367, 0, 378, 0, 380, 375]}, "f00286": {"x": [0.0, 0.3, 0.6, 0.8999999999999999, 1.2, 1.5, 1.7999999999999998, 2.1, 2.4, 2.6999999999999997, 3], "y": [11300, 0, 0, 36, 0, 0, 382, 0, 0, 371]}, "f00244": {"x": [0.0, 0.8, 1.6, 2.4000000000000004, 3.2, 4.0, 4.800000000000001, 5.6000000000000005, 6.4, 7.2, 8], "y": [9118, 345, 363, 374, 0, 383, 386, 369, 601, 150]}, "f00379": {"x": [0, 0], "y": [12089]}, "f00337": {"x": [0, 0], "y": [12089]}, "f00151": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [6312, 320, 285, 259, 275, 274, 261, 322, 391, 3390]}, "f00259": {"x": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], "y": [8358, 337, 383, 366, 385, 372, 379, 382, 391, 736]}, "f00073": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [4424, 441, 415, 338, 328, 423, 411, 474, 566, 4269]}, "f00172": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [10154, 149, 158, 111, 128, 134, 118, 173, 154, 810]}, "f00166": {"x": [0.0, 25.3, 50.6, 75.9, 101.2, 126.5, 151.8, 177.1, 202.4, 227.70000000000002, 253.0], "y": [12032, 5, 10, 5, 5, 9, 3, 7, 2, 11]}, "f00385": {"x": [0, 0], "y": [12089]}, "f00088": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11223, 93, 83, 61, 65, 79, 70, 64, 58, 293]}, "f00139": {"x": [0.0, 15.3, 30.6, 45.900000000000006, 61.2, 76.5, 91.80000000000001, 107.10000000000001, 122.4, 137.70000000000002, 153.0], "y": [12084, 1, 1, 0, 0, 0, 0, 0, 1, 2]}, "f00343": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12086, 0, 0, 0, 0, 0, 0, 0, 0, 3]}, "f00052": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11759, 52, 35, 24, 24, 33, 19, 21, 37, 85]}, "f00046": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [4627, 410, 429, 346, 368, 383, 379, 491, 583, 4073]}, "f00187": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [5262, 459, 383, 331, 332, 364, 383, 438, 551, 3586]}, "f00238": {"x": [0.0, 1.5, 3.0, 4.5, 6.0, 7.5, 9.0, 10.5, 12.0, 13.5, 15], "y": [4555, 357, 758, 382, 752, 376, 756, 417, 2569, 1167]}, "f00250": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12078, 0, 0, 0, 0, 0, 0, 0, 0, 11]}, "f00370": {"x": [0, 0], "y": [12089]}, "f00265": {"x": [0.0, 1.3, 2.6, 3.9000000000000004, 5.2, 6.5, 7.800000000000001, 9.1, 10.4, 11.700000000000001, 13], "y": [6045, 388, 368, 756, 379, 393, 749, 926, 1318, 767]}, "f00145": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [9002, 229, 222, 141, 173, 199, 187, 201, 233, 1502]}, "f00028": {"x": [0.0, 0.0], "y": [12089]}, "f00169": {"x": [0.0, 18.5, 37.0, 55.5, 74.0, 92.5, 111.0, 129.5, 148.0, 166.5, 185.0], "y": [12083, 1, 1, 0, 0, 0, 0, 1, 1, 2]}, "f00232": {"x": [0.0, 1.6, 3.2, 4.800000000000001, 6.4, 8.0, 9.600000000000001, 11.200000000000001, 12.8, 14.4, 16], "y": [4920, 755, 380, 754, 370, 760, 769, 413, 2147, 821]}, "f00352": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12077, 0, 0, 0, 0, 0, 0, 0, 0, 12]}, "f00127": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [5959, 375, 377, 256, 263, 273, 285, 361, 422, 3518]}, "f00055": {"x": [0.0, 19.0, 38.0, 57.0, 76.0, 95.0, 114.0, 133.0, 152.0, 171.0, 190.0], "y": [12083, 0, 1, 1, 2, 0, 0, 0, 1, 1]}, "f00310": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12087, 0, 0, 0, 0, 0, 0, 0, 0, 2]}, "f00154": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [5935, 413, 389, 270, 301, 368, 321, 351, 420, 3321]}, "f00049": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [9852, 272, 225, 154, 162, 168, 134, 126, 148, 848]}, "f00253": {"x": [0, 0], "y": [12089]}, "f00367": {"x": [0, 0], "y": [12089]}, "f00331": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12088, 0, 0, 0, 0, 0, 0, 0, 0, 1]}, "f00325": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [11937, 0, 0, 0, 0, 0, 0, 0, 0, 152]}, "f00034": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11018, 146, 117, 100, 91, 86, 70, 58, 87, 316]}, "f00247": {"x": [0.0, 0.3, 0.6, 0.8999999999999999, 1.2, 1.5, 1.7999999999999998, 2.1, 2.4, 2.6999999999999997, 3], "y": [11319, 0, 0, 15, 0, 0, 378, 0, 0, 377]}, "f00319": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [11727, 0, 0, 0, 0, 0, 0, 0, 0, 362]}, "f00160": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [6121, 435, 358, 294, 322, 376, 316, 395, 480, 2992]}, "f00346": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12079, 0, 0, 0, 0, 0, 0, 0, 0, 10]}, "f00040": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [5033, 400, 476, 364, 364, 427, 434, 427, 517, 3647]}, "f00148": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [6238, 375, 369, 319, 314, 298, 331, 372, 388, 3085]}, "f00226": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [11987, 0, 0, 0, 0, 0, 0, 0, 0, 102]}, "f00061": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11035, 129, 90, 89, 88, 97, 76, 69, 95, 321]}, "f00268": {"x": [0.0, 1.1, 2.2, 3.3000000000000003, 4.4, 5.5, 6.6000000000000005, 7.700000000000001, 8.8, 9.9, 11], "y": [7944, 375, 382, 369, 377, 382, 379, 375, 647, 859]}, "f00133": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [7831, 348, 304, 261, 234, 246, 251, 256, 360, 1998]}, "f00022": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11351, 119, 106, 67, 64, 65, 39, 36, 51, 191]}, "f00142": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11984, 13, 9, 10, 12, 9, 9, 11, 13, 19]}, "f00037": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [7989, 403, 368, 247, 259, 261, 254, 275, 370, 1663]}, "f00241": {"x": [0.0, 1.5, 3.0, 4.5, 6.0, 7.5, 9.0, 10.5, 12.0, 13.5, 15], "y": [6047, 382, 749, 378, 763, 373, 767, 380, 1767, 483]}, "f00334": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12088, 0, 0, 0, 0, 0, 0, 0, 0, 1]}, "f00229": {"x": [0.0, 0.8, 1.6, 2.4000000000000004, 3.2, 4.0, 4.800000000000001, 5.6000000000000005, 6.4, 7.2, 8], "y": [9283, 177, 381, 360, 0, 381, 373, 390, 566, 178]}, "f00043": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [4046, 460, 445, 382, 381, 377, 440, 497, 599, 4462]}, "f00121": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [7163, 327, 295, 290, 254, 279, 251, 305, 325, 2600]}, "f00001": {"x": [0.0, 9.9, 19.8, 29.700000000000003, 39.6, 49.5, 59.400000000000006, 69.3, 79.2, 89.10000000000001, 99.0], "y": [12087, 0, 0, 0, 0, 0, 1, 0, 0, 1]}, "f00115": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11554, 61, 59, 52, 55, 60, 39, 39, 46, 124]}, "f00307": {"x": [0, 0], "y": [12089]}, "f00016": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [3154, 446, 438, 391, 422, 460, 418, 526, 708, 5126]}, "f00220": {"x": [0.0, 0.2, 0.4, 0.6000000000000001, 0.8, 1.0, 1.2000000000000002, 1.4000000000000001, 1.6, 1.8, 2], "y": [11406, 0, 0, 0, 0, 307, 0, 0, 0, 376]}, "f00214": {"x": [0.0, 1.6, 3.2, 4.800000000000001, 6.4, 8.0, 9.600000000000001, 11.200000000000001, 12.8, 14.4, 16], "y": [4926, 745, 393, 738, 383, 755, 762, 398, 2257, 732]}, "f00109": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11696, 39, 30, 37, 33, 49, 31, 37, 43, 94]}, "f00328": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12060, 0, 0, 0, 0, 0, 0, 0, 0, 29]}, "f00136": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11038, 109, 99, 87, 81, 94, 86, 90, 98, 307]}, "f00340": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12088, 0, 0, 0, 0, 0, 0, 0, 0, 1]}, "f00349": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12077, 0, 0, 0, 0, 0, 0, 0, 0, 12]}, "f00100": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [5363, 398, 372, 293, 294, 303, 337, 395, 416, 3918]}, "f00235": {"x": [0.0, 1.6, 3.2, 4.800000000000001, 6.4, 8.0, 9.600000000000001, 11.200000000000001, 12.8, 14.4, 16], "y": [3778, 761, 382, 753, 393, 751, 747, 462, 2973, 1089]}, "f00313": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12046, 0, 0, 0, 0, 0, 0, 0, 0, 43]}, "f00208": {"x": [0.0, 1.7, 3.4, 5.1, 6.8, 8.5, 10.2, 11.9, 13.6, 15.299999999999999, 17], "y": [3793, 767, 733, 393, 749, 769, 353, 783, 2729, 1020]}, "f00186": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [4193, 442, 423, 362, 346, 413, 462, 503, 661, 4284]}, "f00217": {"x": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], "y": [8623, 67, 379, 376, 377, 385, 371, 380, 485, 646]}, "f00025": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11988, 15, 20, 11, 6, 10, 5, 6, 9, 19]}, "f00390": {"x": [0, 0], "y": [12089]}, "f00384": {"x": [0, 0], "y": [12089]}, "f00093": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [7596, 327, 256, 249, 230, 228, 252, 280, 358, 2313]}, "f00285": {"x": [0.0, 0.2, 0.4, 0.6000000000000001, 0.8, 1.0, 1.2000000000000002, 1.4000000000000001, 1.6, 1.8, 2], "y": [11653, 0, 0, 0, 0, 58, 0, 0, 0, 378]}, "f00103": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [5336, 433, 356, 361, 355, 378, 414, 448, 544, 3464]}, "f00202": {"x": [0.0, 1.3, 2.6, 3.9000000000000004, 5.2, 6.5, 7.800000000000001, 9.1, 10.4, 11.700000000000001, 13], "y": [6805, 386, 377, 747, 375, 393, 754, 882, 888, 482]}, "f00192": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11234, 92, 92, 76, 70, 79, 69, 75, 70, 232]}, "f00031": {"x": [0.0, 25.4, 50.8, 76.19999999999999, 101.6, 127.0, 152.39999999999998, 177.79999999999998, 203.2, 228.6, 254.0], "y": [12033, 11, 6, 9, 9, 4, 4, 3, 2, 8]}, "f00124": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [6866, 258, 257, 191, 249, 258, 242, 254, 343, 3171]}, "f00130": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [4186, 409, 402, 365, 366, 448, 452, 478, 621, 4362]}, "f00019": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [6535, 433, 390, 275, 316, 397, 345, 375, 428, 2595]}, "f00087": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11773, 44, 39, 30, 33, 39, 12, 29, 20, 70]}, "f00301": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [11969, 0, 0, 0, 0, 0, 0, 0, 0, 120]}, "f00010": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [7104, 403, 375, 295, 270, 344, 305, 323, 361, 2309]}, "f00004": {"x": [0.0, 25.4, 50.8, 76.19999999999999, 101.6, 127.0, 152.39999999999998, 177.79999999999998, 203.2, 228.6, 254.0], "y": [12010, 18, 19, 7, 8, 4, 4, 3, 6, 10]}, "f00223": {"x": [0, 0], "y": [12089]}, "f00118": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [8533, 253, 269, 203, 182, 239, 208, 218, 275, 1709]}, "f00291": {"x": [0.0, 0.8, 1.6, 2.4000000000000004, 3.2, 4.0, 4.800000000000001, 5.6000000000000005, 6.4, 7.2, 8], "y": [9310, 140, 375, 383, 0, 378, 369, 499, 431, 204]}, "f00322": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [11763, 0, 0, 0, 0, 0, 0, 0, 0, 326]}, "f00316": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [11901, 0, 0, 0, 0, 0, 0, 0, 0, 188]}, "f00013": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [4169, 402, 453, 375, 377, 417, 391, 501, 459, 4545]}, "f00189": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [8107, 299, 298, 257, 219, 235, 258, 268, 320, 1828]}, "f00195": {"x": [0.0, 0.0], "y": [12089]}, "f00112": {"x": [0.0, 0.0], "y": [12089]}, "f00081": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11816, 27, 22, 21, 30, 40, 21, 19, 26, 67]}, "f00007": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [10927, 174, 149, 95, 99, 74, 78, 96, 86, 311]}, "f00211": {"x": [0.0, 1.6, 3.2, 4.800000000000001, 6.4, 8.0, 9.600000000000001, 11.200000000000001, 12.8, 14.4, 16], "y": [4173, 749, 373, 765, 363, 766, 749, 383, 2766, 1002]}, "f00180": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [5213, 468, 396, 317, 338, 423, 311, 419, 483, 3721]}, "f00366": {"x": [0, 0], "y": [12089]}, "f00075": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [5770, 490, 347, 302, 331, 397, 323, 417, 492, 3220]}, "f00069": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [5619, 356, 326, 310, 280, 326, 337, 368, 447, 3720]}, "f00288": {"x": [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5], "y": [10378, 0, 200, 0, 382, 0, 383, 0, 432, 314]}, "f00174": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [7158, 305, 325, 249, 278, 294, 257, 330, 373, 2520]}, "f00387": {"x": [0, 0], "y": [12089]}, "f00205": {"x": [0.0, 1.7, 3.4, 5.1, 6.8, 8.5, 10.2, 11.9, 13.6, 15.299999999999999, 17], "y": [3418, 743, 757, 374, 756, 761, 395, 749, 2987, 1149]}, "f00060": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11619, 78, 52, 40, 30, 54, 36, 37, 27, 116]}, "f00304": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12070, 0, 0, 0, 0, 0, 0, 0, 0, 19]}, "f00273": {"x": [0.0, 0.3, 0.6, 0.8999999999999999, 1.2, 1.5, 1.7999999999999998, 2.1, 2.4, 2.6999999999999997, 3], "y": [11285, 0, 0, 53, 0, 0, 375, 0, 0, 376]}, "f00294": {"x": [0.0, 0.7, 1.4, 2.0999999999999996, 2.8, 3.5, 4.199999999999999, 4.8999999999999995, 5.6, 6.3, 7], "y": [9641, 189, 373, 0, 381, 372, 0, 384, 546, 203]}, "f00168": {"x": [0.0, 0.0], "y": [12089]}, "f00372": {"x": [0, 0], "y": [12089]}, "f00267": {"x": [0.0, 1.3, 2.6, 3.9000000000000004, 5.2, 6.5, 7.800000000000001, 9.1, 10.4, 11.700000000000001, 13], "y": [7182, 379, 398, 749, 369, 371, 754, 596, 855, 436]}, "f00106": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [9670, 270, 196, 140, 185, 185, 162, 177, 194, 910]}, "f00096": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [6688, 311, 265, 229, 227, 246, 257, 283, 335, 3248]}, "f00360": {"x": [0, 0], "y": [12089]}, "f00369": {"x": [0, 0], "y": [12089]}, "f00183": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [4581, 546, 421, 348, 379, 423, 382, 469, 575, 3965]}, "f00141": {"x": [0.0, 16.6, 33.2, 49.800000000000004, 66.4, 83.0, 99.60000000000001, 116.20000000000002, 132.8, 149.4, 166.0], "y": [12085, 0, 0, 1, 0, 2, 0, 0, 0, 1]}, "f00090": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [9452, 221, 189, 147, 169, 162, 165, 176, 221, 1187]}, "f00354": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12082, 0, 0, 0, 0, 0, 0, 0, 0, 7]}, "f00249": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [11974, 0, 0, 0, 0, 0, 0, 0, 0, 115]}, "f00084": {"x": [0.0, 0.0], "y": [12089]}, "f00063": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [9480, 243, 186, 182, 154, 158, 185, 160, 192, 1149]}, "f00198": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [11948, 0, 0, 0, 0, 0, 0, 0, 0, 141]}, "f00297": {"x": [0.0, 0.3, 0.6, 0.8999999999999999, 1.2, 1.5, 1.7999999999999998, 2.1, 2.4, 2.6999999999999997, 3], "y": [10963, 0, 0, 374, 0, 0, 374, 0, 0, 378]}, "f00156": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [4527, 475, 409, 360, 340, 411, 376, 459, 645, 4087]}, "f00255": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [11812, 0, 0, 0, 0, 0, 0, 0, 0, 277]}, "f00078": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [10356, 204, 153, 113, 121, 147, 108, 97, 129, 661]}, "f00282": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12076, 0, 0, 0, 0, 0, 0, 0, 0, 13]}, "f00177": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [4723, 404, 353, 334, 360, 405, 376, 411, 518, 4205]}, "f00042": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [4282, 425, 468, 374, 372, 381, 462, 488, 521, 4316]}, "f00381": {"x": [0, 0], "y": [12089]}, "f00240": {"x": [0.0, 1.6, 3.2, 4.800000000000001, 6.4, 8.0, 9.600000000000001, 11.200000000000001, 12.8, 14.4, 16], "y": [5291, 762, 371, 759, 384, 762, 745, 450, 1867, 698]}, "f00276": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [11977, 0, 0, 0, 0, 0, 0, 0, 0, 112]}, "f00099": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [5984, 304, 310, 279, 257, 278, 251, 324, 403, 3699]}, "f00162": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [8968, 271, 259, 188, 208, 200, 214, 214, 257, 1310]}, "f00348": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12074, 0, 0, 0, 0, 0, 0, 0, 0, 15]}, "f00057": {"x": [0.0, 10.7, 21.4, 32.099999999999994, 42.8, 53.5, 64.19999999999999, 74.89999999999999, 85.6, 96.3, 107.0], "y": [12087, 0, 0, 0, 0, 0, 0, 0, 1, 1]}, "f00261": {"x": [0.0, 1.4, 2.8, 4.199999999999999, 5.6, 7.0, 8.399999999999999, 9.799999999999999, 11.2, 12.6, 14], "y": [6432, 368, 761, 381, 387, 747, 379, 1024, 1069, 541]}, "f00375": {"x": [0, 0], "y": [12089]}, "f00039": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [5703, 470, 412, 383, 320, 403, 401, 401, 485, 3111]}, "f00237": {"x": [0.0, 1.5, 3.0, 4.5, 6.0, 7.5, 9.0, 10.5, 12.0, 13.5, 15], "y": [4173, 382, 735, 393, 744, 398, 765, 372, 3016, 1111]}, "f00051": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11475, 97, 58, 63, 57, 52, 37, 46, 46, 158]}, "f00243": {"x": [0.0, 1.1, 2.2, 3.3000000000000003, 4.4, 5.5, 6.6000000000000005, 7.700000000000001, 8.8, 9.9, 11], "y": [8314, 395, 374, 370, 373, 374, 391, 371, 390, 737]}, "f00357": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12087, 0, 0, 0, 0, 0, 0, 0, 0, 2]}, "f00150": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [6158, 350, 355, 292, 294, 299, 264, 318, 350, 3409]}, "f00270": {"x": [0.0, 0.7, 1.4, 2.0999999999999996, 2.8, 3.5, 4.199999999999999, 4.8999999999999995, 5.6, 6.3, 7], "y": [9462, 368, 372, 0, 379, 375, 0, 379, 401, 353]}, "f00024": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11902, 27, 31, 12, 14, 21, 8, 12, 16, 46]}, "f00165": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11691, 51, 46, 36, 43, 35, 41, 42, 24, 80]}, "f00159": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [4615, 454, 453, 393, 382, 458, 424, 506, 588, 3816]}, "f00363": {"x": [0, 0], "y": [12089]}, "f00258": {"x": [0.0, 0.7, 1.4, 2.0999999999999996, 2.8, 3.5, 4.199999999999999, 4.8999999999999995, 5.6, 6.3, 7], "y": [9543, 281, 392, 0, 370, 369, 0, 378, 622, 134]}, "f00123": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [6984, 294, 266, 220, 218, 245, 210, 279, 358, 3015]}, "f00378": {"x": [0, 0], "y": [12089]}, "f00321": {"x": [0.0, 0.2, 0.4, 0.6000000000000001, 0.8, 1.0, 1.2000000000000002, 1.4000000000000001, 1.6, 1.8, 2], "y": [11711, 0, 0, 0, 0, 0, 0, 0, 0, 378]}, "f00336": {"x": [0, 0], "y": [12089]}, "f00342": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12087, 0, 0, 0, 0, 0, 0, 0, 0, 2]}, "f00045": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [3911, 454, 408, 329, 363, 435, 424, 508, 679, 4578]}, "f00264": {"x": [0.0, 1.4, 2.8, 4.199999999999999, 5.6, 7.0, 8.399999999999999, 9.799999999999999, 11.2, 12.6, 14], "y": [5678, 372, 753, 379, 393, 737, 380, 1236, 1402, 759]}, "f00072": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [4799, 453, 387, 342, 317, 322, 368, 425, 502, 4174]}, "f00171": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11296, 74, 84, 56, 83, 89, 54, 64, 60, 229]}, "f00222": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12056, 0, 0, 0, 0, 0, 0, 0, 0, 33]}, "f00030": {"x": [0.0, 25.4, 50.8, 76.19999999999999, 101.6, 127.0, 152.39999999999998, 177.79999999999998, 203.2, 228.6, 254.0], "y": [12077, 3, 2, 3, 1, 2, 0, 0, 0, 1]}, "f00066": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [6902, 393, 322, 331, 295, 348, 295, 328, 393, 2482]}, "f00138": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [12021, 17, 9, 7, 6, 9, 3, 5, 5, 7]}, "f00144": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [10286, 176, 139, 112, 141, 136, 126, 123, 122, 728]}, "f00279": {"x": [0, 0], "y": [12089]}, "f00153": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [6259, 340, 350, 234, 286, 286, 279, 363, 389, 3303]}, "f00012": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [4872, 402, 424, 321, 351, 415, 387, 454, 468, 3995]}, "f00111": {"x": [0.0, 10.2, 20.4, 30.599999999999998, 40.8, 51.0, 61.199999999999996, 71.39999999999999, 81.6, 91.8, 102.0], "y": [12083, 0, 1, 0, 1, 1, 1, 0, 0, 2]}, "f00027": {"x": [0.0, 5.2, 10.4, 15.600000000000001, 20.8, 26.0, 31.200000000000003, 36.4, 41.6, 46.800000000000004, 52.0], "y": [12086, 0, 0, 0, 0, 1, 0, 0, 0, 2]}, "f00033": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11558, 86, 59, 58, 44, 45, 35, 42, 35, 127]}, "f00147": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [6773, 352, 334, 276, 270, 303, 268, 333, 402, 2778]}, "f00246": {"x": [0.0, 0.4, 0.8, 1.2000000000000002, 1.6, 2.0, 2.4000000000000004, 2.8000000000000003, 3.2, 3.6, 4], "y": [10771, 0, 186, 0, 0, 377, 0, 377, 0, 378]}, "f00132": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [6290, 443, 373, 301, 323, 374, 327, 360, 447, 2851]}, "f00345": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12084, 0, 0, 0, 0, 0, 0, 0, 0, 5]}, "f00204": {"x": [0.0, 1.6, 3.2, 4.800000000000001, 6.4, 8.0, 9.600000000000001, 11.200000000000001, 12.8, 14.4, 16], "y": [4185, 737, 380, 748, 374, 774, 742, 385, 2768, 996]}, "f00048": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [8094, 379, 315, 252, 252, 305, 249, 296, 263, 1684]}, "f00231": {"x": [0.0, 1.5, 3.0, 4.5, 6.0, 7.5, 9.0, 10.5, 12.0, 13.5, 15], "y": [6046, 395, 738, 377, 763, 382, 748, 379, 1725, 536]}, "f00303": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12043, 0, 0, 0, 0, 0, 0, 0, 0, 46]}, "f00126": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [6511, 338, 288, 220, 208, 241, 237, 292, 369, 3385]}, "f00006": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [11528, 92, 84, 57, 50, 47, 40, 43, 31, 117]}, "f00330": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12084, 0, 0, 0, 0, 0, 0, 0, 0, 5]}, "f00339": {"x": [0, 0], "y": [12089]}, "f00219": {"x": [0.0, 0.4, 0.8, 1.2000000000000002, 1.6, 2.0, 2.4000000000000004, 2.8000000000000003, 3.2, 3.6, 4], "y": [10726, 0, 230, 0, 0, 377, 0, 378, 0, 378]}, "f00225": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12086, 0, 0, 0, 0, 0, 0, 0, 0, 3]}, "f00252": {"x": [0, 0], "y": [12089]}, "f00324": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [11879, 0, 0, 0, 0, 0, 0, 0, 0, 210]}, "f00351": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [12077, 0, 0, 0, 0, 0, 0, 0, 0, 12]}, "f00210": {"x": [0.0, 1.6, 3.2, 4.800000000000001, 6.4, 8.0, 9.600000000000001, 11.200000000000001, 12.8, 14.4, 16], "y": [4173, 741, 380, 763, 379, 746, 764, 372, 2690, 1081]}, "f00105": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [8423, 354, 259, 224, 214, 257, 237, 234, 274, 1613]}, "f00054": {"x": [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0], "y": [12038, 1, 6, 8, 6, 6, 5, 8, 3, 8]}, "f00318": {"x": [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9, 1], "y": [11776, 0, 0, 0, 0, 0, 0, 0, 0, 313]}} \ No newline at end of file diff --git a/web_console_v2/api/test/fedlearner_webconsole/test_data/dataset_metainfo/_META b/web_console_v2/api/test/fedlearner_webconsole/test_data/dataset_metainfo/_META new file mode 100644 index 000000000..a8399694e --- /dev/null +++ b/web_console_v2/api/test/fedlearner_webconsole/test_data/dataset_metainfo/_META @@ -0,0 +1 @@ +{"dtypes": {"f00312": "bigint", "f00207": "bigint", "f00197": "bigint", "f00021": "float", "f00380": "bigint", "f00129": "float", "f00009": "float", "f00333": "bigint", "f00036": "float", "f00000": "float", "f00135": "float", "f00213": "bigint", "f00389": "bigint", "f00108": "float", "f00098": "float", "f00120": "float", "f00306": "bigint", "f00015": "float", "f00234": "bigint", "f00228": "bigint", "f00114": "float", "f00296": "bigint", "f00327": "bigint", "f00315": "bigint", "f00201": "bigint", "f00170": "float", "raw_id": "bigint", "f00278": "bigint", "f00117": "float", "f00191": "float", "f00216": "bigint", "f00185": "float", "f00071": "float", "f00284": "bigint", "f00299": "bigint", "f00179": "float", "f00362": "bigint", "f00309": "bigint", "f00018": "float", "f00383": "bigint", "f00377": "bigint", "f00086": "float", "f00300": "bigint", "f00290": "bigint", "f00003": "float", "f00092": "float", "f00102": "float", "f00344": "bigint", "f00095": "float", "f00365": "bigint", "f00068": "float", "f00272": "bigint", "f00167": "float", "f00371": "bigint", "f00194": "float", "f00080": "float", "f00074": "float", "f00089": "float", "f00293": "bigint", "f00287": "bigint", "f00152": "float", "f00188": "float", "f00251": "bigint", "f00350": "bigint", "f00173": "float", "f00053": "float", "f00359": "bigint", "f00266": "bigint", "f00386": "bigint", "f00353": "bigint", "f00062": "float", "f00248": "bigint", "f00275": "bigint", "f00347": "bigint", "f00326": "bigint", "f00374": "bigint", "f00233": "bigint", "f00077": "float", "f00332": "bigint", "f00155": "float", "f00035": "float", "f00161": "float", "f00056": "float", "f00083": "float", "f00269": "bigint", "f00134": "float", "f00182": "float", "f00041": "float", "f00368": "bigint", "f00176": "float", "f00140": "float", "f00260": "bigint", "f00281": "bigint", "f00254": "bigint", "f00149": "float", "f00023": "float", "f00158": "float", "f00017": "float", "f00050": "float", "f00236": "bigint", "f00143": "float", "f00044": "float", "f00038": "float", "f00242": "bigint", "f00116": "float", "f00137": "float", "f00257": "bigint", "f00320": "bigint", "f00164": "float", "f00215": "bigint", "f00308": "bigint", "f00122": "float", "f00341": "bigint", "f00335": "bigint", "f00329": "bigint", "f00221": "bigint", "f00356": "bigint", "f00059": "float", "f00065": "float", "f00314": "bigint", "f00263": "bigint", "f00323": "bigint", "f00203": "bigint", "f00026": "float", "f00391": "bigint", "f00125": "float", "f00032": "float", "f00193": "float", "f00047": "float", "f00110": "float", "f00245": "bigint", "f00146": "float", "f00005": "float", "f00104": "float", "f00218": "bigint", "f00131": "float", "f00317": "bigint", "f00224": "bigint", "f00230": "bigint", "f00239": "bigint", "f00094": "float", "f00119": "float", "f00302": "bigint", "f00292": "bigint", "f00011": "float", "f00338": "bigint", "f00020": "float", "f00175": "float", "f00206": "bigint", "f00029": "float", "f00274": "bigint", "f00128": "float", "f00373": "bigint", "f00196": "bigint", "f00295": "bigint", "f00305": "bigint", "f00289": "bigint", "f00212": "bigint", "f00181": "float", "f00076": "float", "f00280": "bigint", "f00014": "float", "f00388": "bigint", "f00227": "bigint", "f00082": "float", "f00008": "float", "f00113": "float", "f00107": "float", "f00097": "float", "f00311": "bigint", "f00184": "float", "f00079": "float", "f00101": "float", "f00002": "float", "f00070": "float", "f00064": "float", "f00209": "bigint", "f00283": "bigint", "f00277": "bigint", "f00091": "float", "f00163": "float", "f00058": "float", "f00190": "float", "f00200": "bigint", "f00376": "bigint", "f00199": "bigint", "f00085": "float", "f00298": "bigint", "f00157": "float", "f00361": "bigint", "f00256": "bigint", "f00262": "bigint", "f00355": "bigint", "f00178": "float", "f00382": "bigint", "f00364": "bigint", "f00358": "bigint", "f00067": "float", "f00271": "bigint", "f00286": "bigint", "f00244": "bigint", "f00379": "bigint", "f00337": "bigint", "f00151": "float", "f00259": "bigint", "f00073": "float", "f00172": "float", "f00166": "float", "f00385": "bigint", "f00088": "float", "f00139": "float", "f00343": "bigint", "f00052": "float", "f00046": "float", "f00187": "float", "f00238": "bigint", "f00250": "bigint", "f00370": "bigint", "f00265": "bigint", "f00145": "float", "f00028": "float", "f00169": "float", "f00232": "bigint", "f00352": "bigint", "f00127": "float", "f00055": "float", "f00310": "bigint", "f00154": "float", "f00049": "float", "f00253": "bigint", "f00367": "bigint", "f00331": "bigint", "f00325": "bigint", "f00034": "float", "f00247": "bigint", "f00319": "bigint", "f00160": "float", "f00346": "bigint", "f00040": "float", "f00148": "float", "f00226": "bigint", "f00061": "float", "f00268": "bigint", "f00133": "float", "f00022": "float", "f00142": "float", "f00037": "float", "f00241": "bigint", "f00334": "bigint", "f00229": "bigint", "f00043": "float", "f00121": "float", "f00001": "float", "f00115": "float", "f00307": "bigint", "f00016": "float", "f00220": "bigint", "f00214": "bigint", "f00109": "float", "f00328": "bigint", "f00136": "float", "f00340": "bigint", "f00349": "bigint", "f00100": "float", "f00235": "bigint", "f00313": "bigint", "f00208": "bigint", "f00186": "float", "f00217": "bigint", "f00025": "float", "f00390": "bigint", "f00384": "bigint", "f00093": "float", "f00285": "bigint", "f00103": "float", "f00202": "bigint", "f00192": "float", "f00031": "float", "f00124": "float", "f00130": "float", "f00019": "float", "f00087": "float", "f00301": "bigint", "f00010": "float", "f00004": "float", "f00223": "bigint", "f00118": "float", "f00291": "bigint", "f00322": "bigint", "f00316": "bigint", "f00013": "float", "f00189": "float", "f00195": "float", "f00112": "float", "f00081": "float", "f00007": "float", "f00211": "bigint", "f00180": "float", "f00366": "bigint", "f00075": "float", "f00069": "float", "f00288": "bigint", "f00174": "float", "f00387": "bigint", "f00205": "bigint", "f00060": "float", "f00304": "bigint", "f00273": "bigint", "f00294": "bigint", "f00168": "float", "f00372": "bigint", "f00267": "bigint", "f00106": "float", "f00096": "float", "f00360": "bigint", "f00369": "bigint", "f00183": "float", "f00141": "float", "f00090": "float", "f00354": "bigint", "f00249": "bigint", "f00084": "float", "f00063": "float", "f00198": "bigint", "f00297": "bigint", "f00156": "float", "f00255": "bigint", "f00078": "float", "f00282": "bigint", "f00177": "float", "f00042": "float", "f00381": "bigint", "f00240": "bigint", "f00276": "bigint", "f00099": "float", "f00162": "float", "f00348": "bigint", "f00057": "float", "f00261": "bigint", "f00375": "bigint", "f00039": "float", "f00237": "bigint", "f00051": "float", "f00243": "bigint", "f00357": "bigint", "f00150": "float", "f00270": "bigint", "f00024": "float", "f00165": "float", "f00159": "float", "f00363": "bigint", "f00258": "bigint", "f00123": "float", "f00378": "bigint", "f00321": "bigint", "f00336": "bigint", "f00342": "bigint", "f00045": "float", "f00264": "bigint", "f00072": "float", "f00171": "float", "f00222": "bigint", "f00030": "float", "f00066": "float", "f00138": "float", "f00144": "float", "f00279": "bigint", "f00153": "float", "f00012": "float", "f00111": "float", "f00027": "float", "f00033": "float", "f00147": "float", "f00246": "bigint", "f00132": "float", "f00345": "bigint", "f00204": "bigint", "f00048": "float", "f00231": "bigint", "f00303": "bigint", "f00126": "float", "f00006": "float", "f00330": "bigint", "f00339": "bigint", "f00219": "bigint", "f00225": "bigint", "f00252": "bigint", "f00324": "bigint", "f00351": "bigint", "f00210": "bigint", "f00105": "float", "f00054": "float", "f00318": "bigint"}, "count": 12089, "sample": [[0, 7, 0, 0.0, 0, 0.0, 0.0, 0, 5.0, 0.0, 252.0, 0, 0, 49.0, 203.0, 241.0, 0, 255.0, 1, 0, 0.0, 0, 0, 0, 6, 0.0, 0, 0, 3.0, 0.0, 0, 0.0, 208.0, 0, 0, 252.0, 0, 0, 228.0, 0, 0, 0.0, 0, 0, 0.0, 252.0, 173.0, 0, 16.0, 0, 59.0, 0, 0.0, 0, 0.0, 0.0, 252.0, 0.0, 0, 0, 244.0, 0.0, 0, 0, 231.0, 0.0, 0, 0, 0, 0, 0.0, 0, 0, 0, 0, 0, 3, 7.0, 0, 29.0, 0.0, 89.0, 0.0, 0.0, 0, 252.0, 3.0, 228.0, 0, 205.0, 0.0, 0, 0, 0, 18.0, 0.0, 0.0, 253.0, 0.0, 0, 0.0, 252.0, 227.0, 0, 0.0, 0.0, 0, 0, 37.0, 0, 0, 17.0, 0, 0, 0, 0, 0, 0.0, 252.0, 0, 0, 0, 12, 0.0, 0, 200.0, 0.0, 0.0, 117.0, 0.0, 0, 252.0, 0.0, 252.0, 0, 72.0, 0, 0, 4, 0, 180.0, 252.0, 0, 0, 53.0, 0, 0.0, 245.0, 9, 0.0, 0, 65.0, 0, 0, 0, 0, 0, 0, 124.0, 87.0, 0, 253.0, 0, 0, 0.0, 0.0, 0.0, 49.0, 21.0, 0, 0.0, 0.0, 129.0, 0.0, 18.0, 78.0, 2, 0, 0, 135.0, 180.0, 0.0, 0.0, 0, 0, 0, 0.0, 0, 0.0, 0, 0, 0, 0, 252.0, 0, 0, 0, 125.0, 0, 0, 0, 0, 0, 170.0, 0, 252.0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0.0, 231.0, 0.0, 0, 0, 0, 0, 105.0, 0.0, 0.0, 6, 0, 216.0, 0.0, 0, 126.0, 0.0, 0, 0, 0, 0, 0.0, 0, 0, 0.0, 0, 243.0, 88.0, 0, 0.0, 0, 241.0, 0.0, 0.0, 54.0, 0, 0, 2, 242.0, 106.0, 0.0, 0.0, 0, 253.0, 0, 0, 0.0, 0, 223.0, 0, 0, 247.0, 0, 0, 3, 0.0, 0, 0.0, 0, 0, 252.0, 0, 252.0, 10, 0.0, 0.0, 53.0, 14.0, 35.0, 0.0, 0, 10.0, 0.0, 0, 136.0, 0, 0, 0, 253.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 252.0, 0, 252.0, 0.0, 0, 252.0, 0, 14, 0.0, 0, 0, 0, 0.0, 0, 0, 66.0, 0.0, 0, 0, 0.0, 0.0, 5.0, 0, 0, 0.0, 6.0, 0, 0, 0.0, 0, 0.0, 0, 216.0, 170.0, 0, 0, 0, 253.0, 180.0, 0, 0.0, 0, 0, 252.0, 0, 0.0, 0, 0, 73.0, 0, 0.0, 0.0, 0.0, 0, 0, 0.0, 0, 0, 0, 0, 252.0, 0, 252.0, 0.0, 0, 0.0, 252.0, 0.0, 0.0, 0, 252.0, 179.0, 0.0, 0.0, 0.0, 242.0, 0, 163.0, 0, 13, 6.0, 7, 0, 252.0, 0.0, 0, 0, 0, 0, 0, 0, 0, 0, 184.0, 0.0, 0], [0, 8, 0, 0.0, 0, 252.0, 222.0, 0, 45.0, 0.0, 0.0, 9, 0, 0.0, 0.0, 0.0, 0, 252.0, 13, 0, 0.0, 0, 0, 0, 6, 0.0, 1, 0, 0.0, 0.0, 0, 252.0, 44.0, 0, 0, 0.0, 0, 0, 177.0, 0, 0, 0.0, 0, 4, 0.0, 0.0, 252.0, 0, 0.0, 0, 44.0, 0, 0.0, 0, 0.0, 0.0, 252.0, 0.0, 0, 3, 0.0, 0.0, 0, 0, 61.0, 0.0, 0, 6, 0, 0, 0.0, 0, 0, 0, 0, 0, 14, 0.0, 0, 98.0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 92.0, 253.0, 0, 29.0, 0.0, 9, 0, 0, 0.0, 0.0, 252.0, 252.0, 0.0, 13, 0.0, 253.0, 253.0, 0, 0.0, 0.0, 2, 0, 0.0, 0, 0, 0.0, 0, 0, 0, 0, 0, 0.0, 31.0, 0, 11, 0, 12, 0.0, 0, 0.0, 0.0, 0.0, 74.0, 0.0, 0, 5.0, 0.0, 0.0, 0, 74.0, 0, 0, 9, 10, 0.0, 0.0, 0, 1, 252.0, 0, 0.0, 252.0, 7, 0.0, 0, 86.0, 0, 0, 0, 0, 5, 13, 18.0, 0.0, 0, 253.0, 0, 0, 0.0, 45.0, 0.0, 0.0, 0.0, 0, 252.0, 0.0, 252.0, 0.0, 44.0, 0.0, 11, 0, 0, 0.0, 0.0, 0.0, 0.0, 0, 0, 0, 0.0, 0, 252.0, 0, 0, 11, 0, 0.0, 0, 0, 0, 52.0, 0, 2, 0, 0, 0, 0.0, 8, 252.0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0.0, 253.0, 65.0, 13, 0, 0, 8, 0.0, 0.0, 0.0, 13, 0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0, 0, 0, 0.0, 0, 0, 0.0, 0, 253.0, 9.0, 0, 0.0, 0, 0.0, 0.0, 0.0, 223.0, 2, 0, 6, 253.0, 0.0, 0.0, 0.0, 0, 252.0, 0, 5, 0.0, 0, 0.0, 0, 0, 15.0, 13, 0, 7, 243.0, 0, 0.0, 0, 0, 0.0, 0, 74.0, 10, 0.0, 0.0, 0.0, 252.0, 0.0, 0.0, 0, 252.0, 0.0, 0, 0.0, 3, 0, 0, 252.0, 0.0, 0.0, 0.0, 0.0, 0.0, 13, 0.0, 0, 74.0, 44.0, 4, 183.0, 0, 7, 0.0, 0, 0, 0, 0.0, 0, 3, 0.0, 0.0, 0, 0, 239.0, 0.0, 0.0, 0, 0, 0.0, 0.0, 0, 0, 242.0, 0, 0.0, 0, 0.0, 255.0, 0, 7, 0, 0.0, 0.0, 0, 0.0, 11, 0, 253.0, 12, 0.0, 0, 0, 0.0, 0, 0.0, 0.0, 74.0, 0, 5, 0.0, 0, 0, 0, 0, 253.0, 9, 143.0, 0.0, 0, 0.0, 123.0, 0.0, 0.0, 0, 0.0, 252.0, 0.0, 0.0, 0.0, 75.0, 0, 0.0, 0, 7, 0.0, 12, 0, 0.0, 0.0, 0, 0, 0, 0, 0, 0, 0, 14, 0.0, 0.0, 0], [0, 10, 0, 0.0, 0, 254.0, 0.0, 0, 0.0, 0.0, 0.0, 2, 0, 0.0, 0.0, 0.0, 0, 254.0, 15, 0, 0.0, 0, 0, 0, 0, 0.0, 2, 0, 0.0, 0.0, 0, 139.0, 1.0, 0, 0, 0.0, 0, 0, 171.0, 0, 0, 0.0, 0, 0, 0.0, 0.0, 214.0, 0, 0.0, 0, 0.0, 0, 0.0, 0, 0.0, 0.0, 219.0, 0.0, 0, 0, 0.0, 0.0, 0, 0, 0.0, 0.0, 0, 10, 0, 0, 0.0, 0, 0, 0, 0, 0, 16, 0.0, 0, 240.0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 206.0, 89.0, 0, 215.0, 0.0, 1, 0, 0, 0.0, 0.0, 34.0, 254.0, 0.0, 15, 0.0, 240.0, 0.0, 0, 0.0, 0.0, 0, 0, 0.0, 0, 0, 0.0, 0, 0, 0, 0, 0, 0.0, 0.0, 0, 7, 0, 9, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 19.0, 0.0, 0.0, 0, 0.0, 0, 0, 0, 11, 0.0, 0.0, 0, 0, 110.0, 0, 0.0, 254.0, 11, 0.0, 0, 254.0, 0, 0, 0, 0, 0, 10, 89.0, 0.0, 0, 254.0, 0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 254.0, 0.0, 254.0, 0.0, 0.0, 0.0, 16, 0, 0, 0.0, 0.0, 0.0, 0.0, 0, 0, 0, 0.0, 0, 254.0, 0, 0, 5, 0, 36.0, 0, 0, 0, 0.0, 0, 0, 0, 0, 0, 0.0, 0, 254.0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0.0, 171.0, 0.0, 14, 0, 0, 12, 0.0, 0.0, 0.0, 5, 0, 138.0, 0.0, 0, 25.0, 0.0, 0, 0, 0, 0, 0.0, 0, 0, 0.0, 0, 73.0, 90.0, 0, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 0, 0, 0, 93.0, 0.0, 0.0, 0.0, 0, 254.0, 0, 0, 0.0, 0, 0.0, 0, 0, 254.0, 15, 0, 13, 8.0, 0, 0.0, 0, 0, 0.0, 0, 28.0, 4, 0.0, 0.0, 0.0, 116.0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0.0, 0, 0, 0, 254.0, 0.0, 0.0, 0.0, 0.0, 0.0, 14, 51.0, 0, 31.0, 0.0, 0, 164.0, 0, 16, 0.0, 0, 0, 0, 0.0, 0, 3, 0.0, 0.0, 0, 0, 254.0, 0.0, 0.0, 0, 0, 0.0, 0.0, 0, 0, 254.0, 0, 0.0, 0, 63.0, 89.0, 0, 0, 0, 7.0, 0.0, 0, 0.0, 5, 0, 0.0, 14, 0.0, 0, 0, 0.0, 0, 0.0, 0.0, 0.0, 0, 0, 0.0, 0, 0, 0, 0, 254.0, 13, 128.0, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 246.0, 0.0, 0.0, 0.0, 177.0, 0, 0.0, 0, 15, 0.0, 7, 0, 0.0, 0.0, 0, 0, 0, 0, 0, 0, 0, 15, 0.0, 0.0, 0], [0, 2, 0, 22.0, 0, 0.0, 0.0, 0, 0.0, 0.0, 0.0, 16, 0, 0.0, 0.0, 0.0, 0, 254.0, 15, 0, 0.0, 3, 0, 0, 0, 0.0, 3, 0, 0.0, 0.0, 9, 137.0, 0.0, 0, 0, 0.0, 0, 0, 254.0, 0, 0, 0.0, 0, 4, 0.0, 0.0, 192.0, 0, 0.0, 0, 0.0, 0, 0.0, 0, 0.0, 0.0, 254.0, 0.0, 5, 0, 0.0, 254.0, 0, 0, 0.0, 0.0, 0, 12, 0, 0, 0.0, 0, 0, 0, 0, 0, 16, 254.0, 0, 0.0, 0.0, 250.0, 0.0, 0.0, 8, 116.0, 0.0, 50.0, 0, 209.0, 0.0, 7, 0, 0, 126.0, 0.0, 188.0, 254.0, 2.0, 9, 0.0, 253.0, 0.0, 13, 0.0, 0.0, 0, 0, 0.0, 14, 0, 0.0, 0, 0, 0, 0, 0, 0.0, 0.0, 0, 13, 0, 8, 0.0, 0, 0.0, 0.0, 0.0, 254.0, 0.0, 0, 0.0, 0.0, 254.0, 0, 254.0, 0, 0, 0, 14, 0.0, 0.0, 0, 5, 181.0, 0, 140.0, 24.0, 3, 0.0, 0, 0.0, 0, 0, 4, 0, 2, 12, 0.0, 254.0, 0, 254.0, 0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 23.0, 0.0, 4.0, 0.0, 0.0, 0.0, 0, 0, 0, 0.0, 0.0, 0.0, 0.0, 0, 0, 0, 0.0, 0, 3.0, 0, 0, 13, 0, 15.0, 0, 0, 0, 0.0, 2, 0, 3, 0, 0, 0.0, 2, 200.0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0.0, 254.0, 254.0, 9, 0, 0, 12, 0.0, 0.0, 0.0, 15, 0, 0.0, 0.0, 0, 0.0, 191.0, 0, 0, 0, 0, 0.0, 0, 0, 254.0, 0, 30.0, 25.0, 0, 0.0, 11, 254.0, 0.0, 0.0, 0.0, 14, 0, 0, 155.0, 0.0, 0.0, 0.0, 0, 254.0, 0, 15, 0.0, 0, 0.0, 0, 0, 0.0, 9, 0, 0, 254.0, 4, 0.0, 0, 0, 0.0, 0, 254.0, 0, 0.0, 0.0, 0.0, 141.0, 252.0, 0.0, 0, 58.0, 0.0, 0, 0.0, 3, 0, 0, 254.0, 209.0, 0.0, 0.0, 0.0, 0.0, 6, 0.0, 0, 254.0, 0.0, 1, 0.0, 0, 16, 0.0, 0, 0, 4, 0.0, 0, 12, 154.0, 0.0, 0, 0, 0.0, 0.0, 0.0, 0, 0, 0.0, 0.0, 0, 1, 0.0, 0, 118.0, 0, 254.0, 73.0, 0, 15, 0, 0.0, 61.0, 0, 0.0, 13, 0, 0.0, 8, 0.0, 8, 0, 86.0, 4, 0.0, 0.0, 254.0, 0, 0, 0.0, 0, 0, 0, 0, 254.0, 13, 91.0, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 234.0, 0.0, 0.0, 0.0, 0.0, 0, 254.0, 0, 15, 254.0, 6, 0, 0.0, 0.0, 0, 0, 0, 0, 0, 0, 0, 1, 254.0, 0.0, 0], [0, 6, 0, 0.0, 0, 220.0, 0.0, 0, 0.0, 0.0, 0.0, 0, 0, 0.0, 228.0, 247.0, 0, 0.0, 12, 0, 0.0, 0, 0, 0, 0, 0.0, 4, 0, 0.0, 0.0, 0, 0.0, 254.0, 0, 0, 0.0, 0, 0, 254.0, 0, 0, 0.0, 0, 0, 0.0, 214.0, 154.0, 0, 254.0, 0, 203.0, 0, 0.0, 0, 0.0, 33.0, 254.0, 0.0, 0, 0, 0.0, 0.0, 0, 0, 0.0, 160.0, 0, 0, 0, 0, 0.0, 0, 0, 0, 0, 0, 13, 223.0, 0, 244.0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 254.0, 0.0, 0, 4.0, 0.0, 0, 0, 0, 0.0, 67.0, 0.0, 254.0, 225.0, 8, 0.0, 98.0, 0.0, 0, 0.0, 0.0, 0, 0, 0.0, 0, 0, 146.0, 0, 0, 0, 0, 0, 0.0, 31.0, 0, 0, 0, 14, 0.0, 0, 28.0, 0.0, 0.0, 208.0, 0.0, 0, 255.0, 0.0, 0.0, 0, 0.0, 0, 0, 0, 0, 254.0, 254.0, 0, 0, 0.0, 0, 0.0, 206.0, 7, 0.0, 0, 254.0, 0, 0, 0, 0, 0, 0, 179.0, 223.0, 0, 0.0, 0, 0, 0.0, 0.0, 0.0, 0.0, 240.0, 0, 64.0, 52.0, 254.0, 0.0, 212.0, 9.0, 14, 0, 0, 137.0, 0.0, 0.0, 0.0, 0, 0, 0, 0.0, 0, 50.0, 0, 0, 0, 0, 0.0, 0, 0, 0, 137.0, 0, 0, 0, 0, 0, 0.0, 0, 254.0, 0.0, 0.0, 0, 0.0, 0.0, 0, 241.0, 254.0, 0.0, 0, 0, 0, 0, 0.0, 0.0, 0.0, 12, 0, 216.0, 0.0, 0, 137.0, 207.0, 0, 0, 0, 0, 0.0, 0, 0, 0.0, 0, 0.0, 49.0, 0, 0.0, 0, 0.0, 20.0, 0.0, 0.0, 0, 0, 0, 35.0, 179.0, 0.0, 0.0, 0, 39.0, 0, 0, 0.0, 0, 0.0, 0, 0, 254.0, 12, 0, 11, 0.0, 0, 39.0, 0, 0, 254.0, 0, 50.0, 9, 0.0, 0.0, 60.0, 12.0, 56.0, 0.0, 0, 0.0, 0.0, 0, 185.0, 0, 0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3, 8.0, 0, 251.0, 203.0, 0, 254.0, 0, 9, 0.0, 0, 0, 0, 0.0, 0, 0, 0.0, 254.0, 0, 0, 247.0, 0.0, 9.0, 0, 0, 0.0, 0.0, 0, 0, 232.0, 0, 127.0, 0, 0.0, 7.0, 0, 0, 0, 250.0, 0.0, 0, 0.0, 0, 0, 0.0, 4, 254.0, 0, 0, 0.0, 0, 124.0, 0.0, 0.0, 0, 0, 67.0, 0, 0, 0, 0, 254.0, 0, 254.0, 0.0, 0, 0.0, 82.0, 0.0, 0.0, 0, 4.0, 0.0, 0.0, 0.0, 0.0, 222.0, 0, 0.0, 0, 7, 157.0, 7, 0, 0.0, 0.0, 0, 0, 0, 0, 0, 0, 0, 9, 0.0, 0.0, 0], [0, 15, 0, 0.0, 0, 31.0, 5.0, 0, 0.0, 0.0, 193.0, 15, 0, 0.0, 0.0, 253.0, 0, 245.0, 14, 0, 0.0, 0, 0, 0, 0, 0.0, 5, 0, 0.0, 122.0, 11, 253.0, 0.0, 0, 0, 236.0, 0, 0, 0.0, 0, 0, 0.0, 0, 0, 0.0, 253.0, 0.0, 0, 189.0, 0, 188.0, 0, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 0, 0, 0.0, 253.0, 0, 0, 12.0, 0.0, 0, 4, 0, 0, 0.0, 0, 0, 0, 0, 0, 15, 0.0, 0, 131.0, 0.0, 253.0, 0.0, 0.0, 0, 205.0, 252.0, 192.0, 0, 253.0, 0.0, 1, 0, 0, 253.0, 0.0, 253.0, 115.0, 0.0, 14, 0.0, 0.0, 253.0, 2, 0.0, 0.0, 0, 0, 0.0, 12, 0, 186.0, 0, 0, 0, 0, 0, 0.0, 220.0, 0, 12, 0, 8, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 1, 218.0, 0.0, 0.0, 4, 174.0, 0, 0, 0, 13, 253.0, 225.0, 0, 0, 217.0, 0, 0.0, 253.0, 15, 0.0, 0, 0.0, 0, 0, 0, 0, 0, 14, 222.0, 0.0, 0, 250.0, 0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 253.0, 0.0, 0.0, 0.0, 0.0, 47.0, 15, 0, 0, 107.0, 94.0, 0.0, 253.0, 0, 0, 0, 0.0, 0, 222.0, 0, 0, 5, 0, 253.0, 0, 0, 0, 253.0, 0, 0, 1, 0, 0, 59.0, 0, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0.0, 0.0, 253.0, 13, 0, 0, 6, 48.0, 0.0, 0.0, 9, 0, 0.0, 0.0, 0, 128.0, 0.0, 0, 0, 0, 0, 0.0, 0, 0, 253.0, 0, 253.0, 253.0, 0, 0.0, 3, 205.0, 0.0, 0.0, 124.0, 4, 0, 0, 0.0, 253.0, 0.0, 0.0, 0, 245.0, 0, 14, 0.0, 0, 58.0, 0, 0, 0.0, 14, 0, 15, 253.0, 5, 0.0, 0, 0, 253.0, 0, 0.0, 2, 0.0, 0.0, 0.0, 42.0, 0.0, 0.0, 0, 41.0, 0.0, 0, 41.0, 0, 0, 0, 253.0, 253.0, 0.0, 0.0, 0.0, 0.0, 14, 222.0, 0, 0.0, 25.0, 0, 152.0, 0, 15, 0.0, 0, 0, 0, 0.0, 0, 4, 0.0, 13.0, 0, 0, 253.0, 0.0, 0.0, 0, 0, 0.0, 11.0, 0, 0, 131.0, 0, 0.0, 0, 253.0, 105.0, 0, 10, 0, 0.0, 253.0, 0, 0.0, 5, 0, 253.0, 13, 0.0, 2, 0, 150.0, 0, 0.0, 0.0, 253.0, 0, 0, 22.0, 0, 0, 0, 0, 0.0, 12, 0.0, 0.0, 0, 0.0, 253.0, 0.0, 0.0, 0, 0.0, 253.0, 0.0, 0.0, 0.0, 253.0, 0, 205.0, 0, 14, 0.0, 5, 0, 0.0, 0.0, 0, 0, 1, 0, 0, 0, 0, 14, 0.0, 0.0, 0], [0, 10, 0, 0.0, 0, 233.0, 0.0, 0, 0.0, 0.0, 0.0, 15, 0, 0.0, 0.0, 0.0, 0, 253.0, 14, 2, 0.0, 0, 0, 0, 8, 0.0, 6, 0, 0.0, 0.0, 4, 253.0, 7.0, 0, 0, 21.0, 0, 0, 253.0, 0, 0, 0.0, 0, 6, 0.0, 0.0, 253.0, 0, 0.0, 0, 0.0, 0, 0.0, 0, 0.0, 0.0, 253.0, 0.0, 3, 4, 0.0, 138.0, 0, 0, 211.0, 0.0, 0, 11, 0, 0, 0.0, 0, 0, 0, 0, 0, 15, 59.0, 0, 37.0, 0.0, 0.0, 0.0, 0.0, 2, 0.0, 151.0, 87.0, 0, 59.0, 0.0, 10, 0, 0, 0.0, 0.0, 253.0, 253.0, 0.0, 14, 0.0, 253.0, 0.0, 10, 0.0, 0.0, 3, 0, 0.0, 7, 0, 0.0, 0, 0, 0, 0, 0, 0.0, 0.0, 0, 12, 0, 13, 0.0, 0, 0.0, 0.0, 0.0, 253.0, 0.0, 0, 0.0, 0.0, 253.0, 0, 253.0, 0, 0, 10, 13, 0.0, 0.0, 0, 3, 201.0, 0, 49.0, 211.0, 15, 0.0, 0, 32.0, 0, 0, 1, 0, 5, 14, 36.0, 250.0, 0, 253.0, 0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 253.0, 0.0, 238.0, 0.0, 0.0, 0.0, 15, 0, 0, 0.0, 0.0, 0.0, 0.0, 3, 0, 0, 0.0, 0, 253.0, 0, 1, 12, 0, 36.0, 0, 0, 0, 0.0, 0, 2, 0, 0, 0, 0.0, 9, 253.0, 66.0, 0.0, 0, 0.0, 0.0, 0, 0.0, 253.0, 253.0, 13, 0, 0, 11, 0.0, 0.0, 0.0, 14, 0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0, 0, 0, 0.0, 0, 0, 138.0, 0, 87.0, 0.0, 0, 0.0, 7, 6.0, 0.0, 0.0, 0.0, 11, 0, 7, 248.0, 0.0, 0.0, 0.0, 0, 253.0, 0, 14, 0.0, 0, 0.0, 0, 0, 62.0, 14, 0, 11, 253.0, 0, 0.0, 0, 0, 0.0, 0, 253.0, 11, 0.0, 0.0, 0.0, 253.0, 230.0, 0.0, 0, 18.0, 0.0, 0, 0.0, 7, 0, 0, 253.0, 0.0, 0.0, 0.0, 0.0, 0.0, 14, 26.0, 0, 253.0, 0.0, 5, 211.0, 0, 15, 0.0, 0, 0, 2, 0.0, 0, 7, 0.0, 0.0, 0, 0, 222.0, 0.0, 0.0, 0, 0, 0.0, 0.0, 0, 0, 203.0, 0, 0.0, 0, 36.0, 87.0, 0, 14, 0, 0.0, 0.0, 0, 0.0, 12, 0, 36.0, 13, 0.0, 3, 0, 0.0, 0, 0.0, 0.0, 253.0, 0, 6, 0.0, 0, 0, 0, 0, 253.0, 12, 152.0, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 253.0, 0.0, 0.0, 0.0, 0.0, 0, 150.0, 0, 14, 138.0, 13, 0, 0.0, 0.0, 0, 0, 0, 0, 0, 0, 0, 14, 60.0, 0.0, 0], [0, 15, 0, 101.0, 0, 79.0, 0.0, 0, 0.0, 0.0, 0.0, 8, 0, 0.0, 244.0, 0.0, 0, 0.0, 12, 0, 0.0, 5, 0, 0, 0, 0.0, 7, 0, 0.0, 0.0, 4, 0.0, 253.0, 0, 2, 253.0, 0, 0, 253.0, 0, 0, 0.0, 1, 0, 0.0, 0.0, 30.0, 0, 0.0, 0, 0.0, 4, 0.0, 0, 0.0, 0.0, 253.0, 0.0, 3, 0, 253.0, 0.0, 0, 0, 0.0, 0.0, 0, 11, 0, 0, 0.0, 0, 1, 0, 0, 0, 5, 0.0, 0, 137.0, 0.0, 0.0, 0.0, 0.0, 9, 0.0, 141.0, 0.0, 0, 0.0, 0.0, 0, 0, 0, 0.0, 0.0, 0.0, 182.0, 0.0, 14, 0.0, 198.0, 0.0, 12, 0.0, 0.0, 0, 0, 0.0, 7, 0, 58.0, 0, 0, 0, 0, 0, 0.0, 0.0, 0, 8, 0, 0, 0.0, 0, 253.0, 0.0, 0.0, 247.0, 0.0, 6, 0.0, 0.0, 0.0, 2, 0.0, 0, 0, 0, 13, 0.0, 0.0, 1, 2, 0.0, 0, 251.0, 0.0, 12, 0.0, 2, 197.0, 0, 0, 4, 0, 0, 8, 253.0, 0.0, 0, 0.0, 0, 0, 0.0, 0.0, 0.0, 0.0, 224.0, 0, 8.0, 0.0, 239.0, 0.0, 99.0, 0.0, 15, 0, 0, 0.0, 0.0, 0.0, 0.0, 0, 0, 0, 0.0, 2, 0.0, 0, 0, 4, 0, 216.0, 0, 0, 0, 0.0, 5, 0, 7, 0, 0, 253.0, 0, 253.0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0.0, 253.0, 0.0, 13, 0, 0, 11, 0.0, 0.0, 0.0, 0, 0, 253.0, 0.0, 0, 242.0, 0.0, 0, 0, 0, 0, 0.0, 2, 0, 0.0, 0, 0.0, 0.0, 0, 0.0, 10, 0.0, 0.0, 0.0, 0.0, 13, 0, 0, 99.0, 0.0, 0.0, 0.0, 0, 22.0, 0, 8, 0.0, 0, 0.0, 0, 0, 253.0, 14, 0, 15, 0.0, 2, 0.0, 0, 0, 0.0, 0, 23.0, 0, 0.0, 0.0, 213.0, 0.0, 253.0, 0.0, 1, 0.0, 0.0, 0, 0.0, 0, 0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 14, 253.0, 0, 191.0, 0.0, 0, 0.0, 0, 1, 0.0, 0, 3, 3, 0.0, 0, 11, 0.0, 117.0, 0, 0, 62.0, 0.0, 0.0, 0, 0, 0.0, 0.0, 0, 3, 16.0, 0, 0.0, 0, 0.0, 3.0, 0, 14, 0, 253.0, 0.0, 0, 0.0, 0, 0, 0.0, 13, 0.0, 10, 0, 86.0, 7, 0.0, 0.0, 0.0, 0, 0, 169.0, 0, 0, 0, 0, 253.0, 9, 253.0, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 0, 253.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3, 0.0, 0, 0, 129.0, 0, 1, 253.0, 0.0, 0, 0, 0, 0, 0, 0, 0, 14, 0.0, 0.0, 0], [0, 0, 0, 3.0, 0, 0.0, 0.0, 0, 0.0, 0.0, 0.0, 15, 0, 0.0, 195.0, 0.0, 0, 253.0, 0, 0, 0.0, 0, 0, 0, 0, 0.0, 8, 0, 0.0, 0.0, 11, 223.0, 192.0, 0, 0, 0.0, 0, 0, 253.0, 0, 0, 0.0, 0, 0, 0.0, 0.0, 0.0, 0, 0.0, 0, 0.0, 0, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 0, 0, 0.0, 253.0, 0, 0, 0.0, 0.0, 0, 0, 0, 0, 0.0, 0, 0, 0, 0, 0, 0, 253.0, 0, 184.0, 0.0, 253.0, 0.0, 0.0, 0, 253.0, 253.0, 61.0, 0, 0.0, 0.0, 0, 0, 0, 0.0, 0.0, 0.0, 253.0, 4.0, 0, 0.0, 176.0, 42.0, 6, 0.0, 0.0, 0, 0, 0.0, 12, 0, 0.0, 0, 0, 0, 0, 0, 0.0, 0.0, 0, 0, 0, 0, 0.0, 0, 172.0, 0.0, 0.0, 247.0, 0.0, 1, 0.0, 0.0, 112.0, 1, 0.0, 0, 0, 0, 2, 0.0, 0.0, 0, 0, 253.0, 0, 169.0, 0.0, 0, 0.0, 0, 0.0, 0, 0, 0, 0, 0, 14, 144.0, 203.0, 0, 253.0, 0, 0, 0.0, 0.0, 0.0, 0.0, 56.0, 0, 243.0, 0.0, 0.0, 0.0, 59.0, 0.0, 2, 0, 0, 0.0, 0.0, 0.0, 96.0, 0, 0, 0, 0.0, 0, 0.0, 0, 0, 0, 0, 0.0, 0, 0, 0, 0.0, 0, 0, 3, 0, 0, 0.0, 0, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0.0, 110.0, 250.0, 1, 0, 0, 0, 0.0, 0.0, 0.0, 0, 0, 17.0, 0.0, 0, 253.0, 195.0, 0, 0, 0, 0, 0.0, 0, 0, 164.0, 0, 61.0, 0.0, 0, 0.0, 0, 248.0, 0.0, 0.0, 0.0, 7, 0, 0, 83.0, 0.0, 0.0, 0.0, 0, 253.0, 0, 14, 0.0, 0, 0.0, 0, 0, 0.0, 0, 0, 0, 223.0, 6, 0.0, 0, 0, 0.0, 0, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 253.0, 0.0, 0, 174.0, 0.0, 0, 0.0, 0, 0, 0, 253.0, 253.0, 0.0, 0.0, 0.0, 0.0, 14, 0.0, 0, 50.0, 0.0, 0, 0.0, 0, 0, 0.0, 0, 0, 0, 0.0, 0, 0, 229.0, 0.0, 0, 0, 253.0, 0.0, 0.0, 0, 0, 0.0, 0.0, 0, 0, 62.0, 0, 42.0, 0, 0.0, 61.0, 0, 6, 0, 57.0, 253.0, 0, 0.0, 0, 0, 61.0, 0, 0.0, 6, 0, 0.0, 0, 0.0, 0.0, 85.0, 0, 0, 0.0, 0, 0, 0, 0, 79.0, 0, 30.0, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 0, 192.0, 253.0, 0.0, 0.0, 0.0, 0.0, 0, 58.0, 0, 0, 253.0, 0, 0, 204.0, 0.0, 0, 0, 0, 0, 0, 0, 0, 7, 253.0, 0.0, 0], [0, 0, 0, 0.0, 0, 72.0, 0.0, 0, 0.0, 0.0, 0.0, 15, 0, 0.0, 40.0, 0.0, 0, 0.0, 0, 0, 0.0, 2, 0, 0, 0, 0.0, 9, 0, 0.0, 0.0, 0, 254.0, 254.0, 0, 0, 0.0, 0, 0, 0.0, 0, 0, 0.0, 0, 0, 0.0, 0.0, 252.0, 0, 0.0, 0, 142.0, 0, 0.0, 0, 0.0, 0.0, 253.0, 0.0, 4, 0, 0.0, 0.0, 0, 0, 0.0, 0.0, 0, 7, 0, 0, 0.0, 0, 0, 0, 0, 0, 0, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 223.0, 0, 0.0, 0.0, 0, 0, 0, 0.0, 0.0, 252.0, 0.0, 0.0, 0, 0.0, 61.0, 0.0, 0, 0.0, 0.0, 0, 2, 0.0, 0, 0, 0.0, 0, 0, 0, 0, 0, 0.0, 0.0, 0, 0, 0, 0, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 0.0, 0, 203.0, 0, 0, 0, 7, 0.0, 0.0, 0, 4, 113.0, 0, 0.0, 0.0, 0, 0.0, 0, 0.0, 0, 0, 4, 0, 0, 9, 0.0, 0.0, 0, 61.0, 0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 102.0, 0.0, 172.0, 0.0, 243.0, 0.0, 0, 0, 0, 0.0, 0.0, 0.0, 0.0, 0, 0, 0, 0.0, 0, 152.0, 0, 0, 0, 0, 0.0, 0, 0, 0, 62.0, 0, 0, 0, 0, 0, 0.0, 0, 254.0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0.0, 0.0, 0.0, 0, 0, 0, 0, 0.0, 0.0, 0.0, 0, 0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0, 0, 0, 0.0, 0, 1, 0.0, 0, 252.0, 0.0, 0, 0.0, 7, 0.0, 0.0, 0.0, 0.0, 8, 0, 0, 102.0, 0.0, 0.0, 0.0, 0, 0.0, 0, 7, 0.0, 0, 0.0, 0, 0, 111.0, 0, 0, 0, 213.0, 0, 0.0, 0, 0, 0.0, 0, 203.0, 0, 0.0, 0.0, 0.0, 253.0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0.0, 3, 1, 0, 224.0, 0.0, 0.0, 0.0, 0.0, 0.0, 2, 0.0, 0, 82.0, 203.0, 0, 0.0, 0, 0, 0.0, 0, 0, 6, 0.0, 0, 11, 0.0, 0.0, 0, 0, 0.0, 0.0, 0.0, 0, 0, 0.0, 0.0, 0, 0, 0.0, 0, 0.0, 0, 0.0, 102.0, 0, 14, 0, 71.0, 0.0, 0, 0.0, 0, 0, 152.0, 0, 0.0, 0, 0, 0.0, 0, 0.0, 0.0, 81.0, 0, 0, 0.0, 0, 2, 0, 0, 41.0, 0, 253.0, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 253.0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 0, 0, 0.0, 0, 0, 0.0, 0.0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0, 0.0, 0], [0, 9, 0, 0.0, 0, 215.0, 0.0, 0, 0.0, 0.0, 0.0, 14, 0, 0.0, 0.0, 0.0, 0, 252.0, 13, 0, 0.0, 0, 0, 0, 3, 0.0, 10, 0, 0.0, 0.0, 0, 253.0, 53.0, 0, 0, 0.0, 0, 0, 0.0, 0, 0, 0.0, 0, 3, 0.0, 0.0, 253.0, 0, 0.0, 0, 0.0, 0, 0.0, 0, 0.0, 0.0, 253.0, 0.0, 0, 2, 0.0, 0.0, 0, 0, 0.0, 0.0, 0, 5, 0, 0, 0.0, 0, 0, 0, 0, 0, 14, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 113.0, 0.0, 0, 255.0, 0.0, 10, 0, 0, 0.0, 0.0, 253.0, 63.0, 0.0, 13, 0.0, 253.0, 0.0, 0, 0.0, 0.0, 0, 0, 0.0, 0, 0, 0.0, 0, 0, 0, 0, 0, 0.0, 0.0, 0, 11, 0, 12, 0.0, 0, 0.0, 0.0, 0.0, 12.0, 0.0, 0, 0.0, 0.0, 0.0, 0, 89.0, 0, 0, 6, 11, 0.0, 0.0, 0, 0, 112.0, 0, 0.0, 253.0, 5, 0.0, 0, 19.0, 0, 0, 0, 0, 5, 13, 114.0, 0.0, 0, 252.0, 0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 253.0, 0.0, 227.0, 0.0, 0.0, 0.0, 15, 0, 0, 0.0, 0.0, 0.0, 0.0, 0, 0, 0, 0.0, 0, 252.0, 0, 0, 11, 0, 0.0, 0, 0, 0, 0.0, 0, 0, 0, 0, 0, 0.0, 7, 252.0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0.0, 114.0, 15.0, 12, 0, 0, 9, 0.0, 0.0, 0.0, 14, 0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0, 0, 0, 0.0, 0, 0, 0.0, 0, 0.0, 0.0, 0, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 4, 0, 2, 229.0, 0.0, 0.0, 0.0, 0, 239.0, 0, 6, 0.0, 0, 0.0, 0, 0, 22.0, 13, 0, 13, 204.0, 0, 0.0, 0, 0, 0.0, 0, 136.0, 10, 0.0, 0.0, 0.0, 253.0, 0.0, 0.0, 0, 25.0, 0.0, 0, 0.0, 3, 0, 0, 174.0, 0.0, 0.0, 0.0, 0.0, 0.0, 13, 63.0, 0, 27.0, 0.0, 4, 92.0, 0, 6, 0.0, 0, 0, 0, 0.0, 0, 4, 0.0, 0.0, 0, 0, 222.0, 0.0, 0.0, 0, 0, 0.0, 0.0, 0, 0, 107.0, 0, 0.0, 0, 27.0, 126.0, 0, 8, 0, 0.0, 0.0, 0, 0.0, 11, 0, 0.0, 13, 0.0, 0, 0, 0.0, 0, 0.0, 0.0, 167.0, 0, 2, 0.0, 0, 0, 0, 0, 253.0, 9, 177.0, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 112.0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 0, 14, 0.0, 12, 0, 0.0, 0.0, 0, 0, 0, 0, 0, 0, 0, 13, 0.0, 0.0, 0], [0, 6, 0, 0.0, 0, 0.0, 0.0, 0, 0.0, 0.0, 0.0, 10, 0, 0.0, 0.0, 0.0, 0, 0.0, 15, 0, 0.0, 0, 0, 0, 0, 0.0, 11, 0, 0.0, 0.0, 0, 254.0, 0.0, 0, 0, 0.0, 0, 0, 179.0, 0, 0, 0.0, 0, 0, 0.0, 0.0, 100.0, 0, 0.0, 0, 0.0, 0, 0.0, 0, 0.0, 0.0, 80.0, 0.0, 0, 0, 0.0, 0.0, 0, 0, 0.0, 0.0, 0, 4, 0, 0, 0.0, 0, 0, 0, 0, 0, 11, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 0.0, 0, 254.0, 0.0, 0, 0, 0, 5.0, 0.0, 254.0, 0.0, 0.0, 15, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0, 0.0, 0, 0, 0.0, 0, 0, 0, 0, 0, 0.0, 0.0, 0, 10, 0, 2, 0.0, 0, 0.0, 0.0, 0.0, 239.0, 0.0, 0, 0.0, 0.0, 0.0, 0, 239.0, 0, 0, 0, 14, 0.0, 0.0, 0, 0, 0.0, 0, 0.0, 64.0, 11, 0.0, 0, 0.0, 0, 0, 0, 0, 0, 15, 0.0, 20.0, 0, 0.0, 0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 188.0, 0.0, 0.0, 0.0, 0.0, 0.0, 7, 0, 0, 0.0, 0.0, 0.0, 0.0, 0, 0, 0, 0.0, 0, 150.0, 0, 0, 5, 0, 38.0, 0, 0, 0, 0.0, 0, 0, 0, 0, 0, 0.0, 0, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0.0, 80.0, 20.0, 15, 0, 0, 7, 0.0, 0.0, 0.0, 3, 0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0, 0, 0, 0.0, 0, 0, 0.0, 0, 0.0, 140.0, 0, 0.0, 1, 0.0, 0.0, 0.0, 0.0, 4, 0, 0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 0, 3, 0.0, 0, 0.0, 0, 0, 0.0, 15, 0, 5, 209.0, 0, 0.0, 0, 0, 0.0, 0, 239.0, 0, 0.0, 0.0, 0.0, 234.0, 239.0, 0.0, 0, 0.0, 0.0, 0, 0.0, 0, 0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 12, 0.0, 0, 244.0, 0.0, 0, 0.0, 0, 17, 0.0, 0, 0, 0, 0.0, 0, 2, 0.0, 0.0, 0, 0, 34.0, 0.0, 0.0, 0, 0, 0.0, 0.0, 0, 0, 3.0, 0, 0.0, 0, 181.0, 0.0, 0, 9, 0, 0.0, 0.0, 0, 0.0, 3, 0, 0.0, 14, 0.0, 0, 0, 0.0, 0, 0.0, 0.0, 129.0, 0, 0, 0.0, 0, 0, 0, 0, 0.0, 9, 0.0, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 4.0, 0, 0.0, 0, 11, 0.0, 0, 0, 0.0, 0.0, 0, 0, 0, 0, 0, 0, 0, 8, 0.0, 0.0, 0], [0, 5, 0, 0.0, 0, 252.0, 76.0, 0, 0.0, 0.0, 0.0, 12, 0, 0.0, 0.0, 0.0, 0, 252.0, 13, 0, 0.0, 2, 0, 0, 0, 0.0, 12, 0, 0.0, 0.0, 0, 253.0, 47.0, 0, 0, 0.0, 0, 0, 128.0, 0, 0, 0.0, 0, 5, 0.0, 0.0, 252.0, 0, 0.0, 0, 0.0, 0, 0.0, 0, 0.0, 0.0, 252.0, 0.0, 5, 0, 0.0, 0.0, 0, 0, 0.0, 0.0, 0, 11, 0, 0, 0.0, 0, 0, 0, 0, 0, 7, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 2, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0, 0, 0.0, 0.0, 252.0, 252.0, 0.0, 9, 0.0, 253.0, 0.0, 0, 0.0, 0.0, 0, 0, 0.0, 0, 0, 0.0, 0, 0, 0, 0, 0, 0.0, 0.0, 0, 11, 0, 0, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 0.0, 0, 178.0, 0, 0, 0, 12, 0.0, 0.0, 0, 6, 221.0, 0, 0.0, 0.0, 12, 0.0, 0, 90.0, 0, 0, 2, 0, 1, 13, 0.0, 0.0, 0, 86.0, 0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 222.0, 0.0, 252.0, 0.0, 0.0, 0.0, 3, 0, 0, 0.0, 0.0, 0.0, 0.0, 0, 0, 0, 0.0, 0, 252.0, 0, 0, 11, 0, 154.0, 0, 0, 0, 0.0, 0, 0, 0, 0, 0, 0.0, 0, 252.0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0.0, 148.0, 0.0, 13, 0, 0, 10, 0.0, 0.0, 0.0, 0, 0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0, 0, 0, 0.0, 0, 0, 0.0, 0, 0.0, 0.0, 0, 0.0, 5, 0.0, 0.0, 0.0, 0.0, 7, 0, 0, 100.0, 0.0, 0.0, 0.0, 0, 252.0, 0, 5, 0.0, 0, 0.0, 0, 0, 234.0, 13, 0, 2, 243.0, 0, 0.0, 0, 0, 0.0, 0, 199.0, 0, 0.0, 0.0, 0.0, 252.0, 0.0, 0.0, 0, 211.0, 0.0, 0, 0.0, 6, 0, 0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 10, 0.0, 0, 74.0, 0.0, 0, 0.0, 0, 7, 0.0, 0, 0, 4, 0.0, 0, 10, 0.0, 0.0, 0, 0, 36.0, 0.0, 0.0, 0, 0, 0.0, 0.0, 0, 0, 122.0, 0, 0.0, 0, 87.0, 0.0, 0, 13, 0, 34.0, 0.0, 0, 0.0, 5, 0, 0.0, 9, 0.0, 0, 0, 0.0, 0, 0.0, 0.0, 126.0, 0, 0, 0.0, 0, 0, 0, 0, 253.0, 11, 252.0, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 43.0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 0, 0, 0.0, 0, 0, 0.0, 0.0, 0, 0, 0, 0, 0, 0, 0, 4, 0.0, 0.0, 0], [0, 16, 0, 0.0, 0, 0.0, 0.0, 0, 0.0, 0.0, 0.0, 11, 0, 0.0, 0.0, 0.0, 0, 87.0, 4, 0, 0.0, 0, 0, 0, 0, 0.0, 13, 0, 0.0, 116.0, 8, 48.0, 0.0, 0, 0, 254.0, 0, 0, 0.0, 0, 0, 0.0, 0, 0, 0.0, 0.0, 0.0, 0, 249.0, 0, 192.0, 2, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 0, 0, 124.0, 16.0, 0, 0, 0.0, 0.0, 0, 4, 0, 0, 0.0, 0, 0, 0, 0, 0, 0, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 4, 0.0, 76.0, 252.0, 0, 0.0, 0.0, 0, 0, 0, 196.0, 0.0, 0.0, 0.0, 0.0, 9, 0.0, 0.0, 35.0, 13, 0.0, 0.0, 0, 0, 0.0, 8, 0, 254.0, 0, 0, 0, 0, 0, 0.0, 0.0, 0, 0, 0, 0, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 4, 0.0, 0.0, 0.0, 6, 0.0, 0, 0, 0, 14, 242.0, 0.0, 0, 0, 18.0, 0, 0.0, 0.0, 8, 0.0, 0, 0.0, 0, 0, 0, 0, 0, 14, 116.0, 0.0, 0, 196.0, 0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 112.0, 0.0, 0.0, 0.0, 0.0, 0.0, 16, 0, 0, 0.0, 0.0, 0.0, 116.0, 0, 0, 0, 0.0, 0, 0.0, 0, 0, 0, 0, 254.0, 0, 0, 0, 254.0, 3, 0, 5, 0, 0, 250.0, 0, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0.0, 0.0, 0.0, 14, 0, 0, 3, 0.0, 0.0, 0.0, 0, 0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0, 0, 0, 0.0, 0, 0, 0.0, 0, 254.0, 0.0, 0, 0.0, 4, 0.0, 0.0, 0.0, 0.0, 14, 0, 0, 0.0, 221.0, 0.0, 0.0, 0, 3.0, 2, 10, 0.0, 0, 0.0, 0, 0, 0.0, 7, 0, 16, 0.0, 10, 0.0, 0, 0, 77.0, 0, 0.0, 0, 116.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0.0, 0, 0, 0, 254.0, 116.0, 0.0, 0.0, 0.0, 0.0, 9, 196.0, 0, 0.0, 101.0, 0, 0.0, 0, 2, 0.0, 0, 0, 0, 0.0, 0, 4, 0.0, 118.0, 0, 0, 0.0, 0.0, 0.0, 0, 0, 0.0, 0.0, 0, 0, 0.0, 0, 0.0, 0, 45.0, 121.0, 0, 15, 0, 0.0, 0.0, 0, 0.0, 0, 0, 212.0, 14, 0.0, 11, 0, 254.0, 3, 0.0, 0.0, 0.0, 0, 0, 228.0, 0, 0, 0, 0, 0.0, 3, 0.0, 0.0, 0, 0.0, 124.0, 0.0, 0.0, 0, 0.0, 204.0, 0.0, 0.0, 0.0, 0.0, 2, 0.0, 0, 0, 0.0, 0, 0, 0.0, 0.0, 0, 0, 4, 0, 0, 0, 0, 12, 0.0, 0.0, 0], [0, 14, 0, 107.0, 0, 156.0, 0.0, 0, 92.0, 0.0, 0.0, 14, 0, 0.0, 252.0, 253.0, 0, 85.0, 14, 0, 0.0, 0, 0, 0, 0, 0.0, 14, 0, 0.0, 0.0, 0, 252.0, 253.0, 0, 0, 246.0, 0, 0, 0.0, 0, 0, 0.0, 0, 0, 0.0, 252.0, 252.0, 0, 252.0, 0, 252.0, 0, 0.0, 0, 0.0, 0.0, 252.0, 0.0, 0, 0, 71.0, 53.0, 0, 0, 0.0, 0.0, 0, 0, 0, 0, 0.0, 0, 0, 0, 0, 0, 14, 133.0, 0, 146.0, 2.0, 231.0, 0.0, 0.0, 0, 188.0, 249.0, 252.0, 0, 252.0, 0.0, 0, 0, 0, 252.0, 0.0, 252.0, 0.0, 0.0, 13, 0.0, 246.0, 192.0, 3, 0.0, 0.0, 0, 0, 0.0, 4, 0, 194.0, 0, 0, 0, 0, 0, 0.0, 252.0, 0, 0, 0, 12, 0.0, 0, 0.0, 0.0, 0.0, 249.0, 0.0, 0, 85.0, 0.0, 253.0, 0, 253.0, 0, 0, 0, 13, 253.0, 253.0, 0, 0, 85.0, 0, 253.0, 252.0, 15, 0.0, 0, 120.0, 0, 0, 0, 0, 0, 13, 232.0, 253.0, 0, 85.0, 0, 0, 0.0, 0.0, 0.0, 0.0, 252.0, 0, 252.0, 0.0, 252.0, 0.0, 252.0, 252.0, 14, 0, 0, 215.0, 0.0, 0.0, 72.0, 0, 0, 0, 0.0, 0, 252.0, 0, 0, 0, 0, 253.0, 0, 0, 0, 252.0, 0, 0, 0, 0, 0, 57.0, 0, 252.0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0.0, 232.0, 244.0, 12, 0, 0, 0, 0.0, 0.0, 0.0, 13, 0, 62.0, 0.0, 0, 71.0, 63.0, 0, 0, 0, 0, 0.0, 0, 0, 160.0, 0, 252.0, 252.0, 0, 0.0, 0, 253.0, 0.0, 0.0, 127.0, 11, 0, 0, 253.0, 253.0, 0.0, 0.0, 0, 57.0, 0, 12, 0.0, 0, 0.0, 0, 0, 252.0, 13, 0, 14, 252.0, 0, 0.0, 0, 0, 252.0, 0, 252.0, 4, 0.0, 0.0, 53.0, 253.0, 211.0, 0.0, 0, 15.0, 0.0, 0, 85.0, 0, 0, 0, 85.0, 51.0, 0.0, 0.0, 0.0, 0.0, 14, 249.0, 0, 252.0, 252.0, 0, 85.0, 0, 14, 0.0, 0, 0, 0, 0.0, 0, 0, 9.0, 252.0, 0, 0, 253.0, 0.0, 0.0, 0, 0, 0.0, 92.0, 0, 0, 211.0, 0, 0.0, 0, 252.0, 252.0, 0, 13, 0, 253.0, 231.0, 0, 0.0, 0, 0, 252.0, 12, 0.0, 0, 0, 129.0, 0, 0.0, 0.0, 252.0, 0, 0, 106.0, 0, 0, 0, 0, 232.0, 0, 252.0, 0.0, 0, 0.0, 253.0, 0.0, 0.0, 0, 0.0, 85.0, 0.0, 0.0, 0.0, 252.0, 0, 255.0, 0, 13, 253.0, 4, 0, 0.0, 0.0, 0, 0, 0, 0, 0, 0, 0, 13, 203.0, 0.0, 0], [0, 0, 0, 0.0, 0, 0.0, 0.0, 0, 0.0, 0.0, 0.0, 0, 0, 0.0, 0.0, 0.0, 0, 24.0, 0, 0, 0.0, 5, 1, 0, 0, 0.0, 15, 0, 0.0, 0.0, 5, 0.0, 0.0, 0, 2, 0.0, 0, 0, 253.0, 0, 0, 0.0, 1, 0, 0.0, 0.0, 0.0, 0, 0.0, 0, 0.0, 4, 0.0, 0, 0.0, 0.0, 54.0, 0.0, 5, 0, 0.0, 112.0, 0, 0, 0.0, 0.0, 0, 5, 0, 0, 0.0, 0, 0, 0, 1, 0, 0, 167.0, 0, 0.0, 0.0, 253.0, 0.0, 0.0, 5, 106.0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0, 0, 0.0, 0.0, 0.0, 245.0, 0.0, 4, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0, 0.0, 0, 0, 0.0, 0, 0, 0, 0, 0, 0.0, 0.0, 0, 2, 1, 0, 0.0, 0, 0.0, 0.0, 0.0, 253.0, 0.0, 6, 0.0, 0.0, 248.0, 5, 0.0, 0, 0, 0, 0, 0.0, 0.0, 1, 1, 0.0, 0, 32.0, 0.0, 0, 0.0, 1, 0.0, 0, 0, 6, 0, 0, 0, 0.0, 253.0, 0, 0.0, 0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 0, 0, 0.0, 0.0, 0.0, 175.0, 0, 0, 0, 0.0, 2, 0.0, 0, 0, 0, 0, 0.0, 0, 0, 0, 0.0, 4, 0, 5, 0, 0, 0.0, 0, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0.0, 246.0, 0.0, 0, 0, 0, 8, 0.0, 0.0, 0.0, 0, 0, 0.0, 0.0, 0, 0.0, 34.0, 0, 0, 0, 1, 0.0, 0, 0, 112.0, 0, 0.0, 0.0, 0, 0.0, 5, 253.0, 0.0, 0.0, 0.0, 0, 0, 0, 0.0, 0.0, 0.0, 0.0, 0, 117.0, 0, 0, 0.0, 1, 0.0, 0, 0, 0.0, 0, 0, 0, 0.0, 9, 0.0, 0, 0, 0.0, 0, 102.0, 0, 0.0, 0.0, 0.0, 0.0, 135.0, 0.0, 1, 0.0, 0.0, 0, 0.0, 0, 1, 0, 0.0, 253.0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 0, 235.0, 0.0, 0, 0.0, 0, 0, 0.0, 0, 3, 6, 0.0, 0, 5, 5.0, 0.0, 0, 0, 0.0, 0.0, 0.0, 0, 0, 0.0, 0.0, 0, 3, 0.0, 0, 2.0, 0, 0.0, 0.0, 0, 0, 0, 0.0, 129.0, 0, 0.0, 0, 0, 0.0, 2, 0.0, 0, 0, 0.0, 4, 0.0, 0.0, 0.0, 0, 0, 0.0, 0, 2, 0, 0, 104.0, 9, 0.0, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 2, 196.0, 0, 0, 169.0, 0, 0, 0.0, 0.0, 0, 0, 0, 0, 0, 1, 0, 0, 253.0, 0.0, 0], [0, 3, 0, 0.0, 0, 207.0, 0.0, 0, 0.0, 0.0, 0.0, 14, 0, 0.0, 0.0, 0.0, 0, 96.0, 13, 0, 0.0, 0, 0, 0, 0, 0.0, 16, 0, 0.0, 0.0, 0, 253.0, 0.0, 0, 0, 0.0, 0, 0, 128.0, 0, 0, 0.0, 0, 5, 0.0, 0.0, 252.0, 0, 0.0, 0, 0.0, 0, 0.0, 0, 0.0, 0.0, 252.0, 0.0, 6, 0, 0.0, 0.0, 0, 0, 0.0, 0.0, 0, 11, 0, 0, 0.0, 0, 0, 0, 0, 0, 3, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 3, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0, 0, 0.0, 0.0, 252.0, 252.0, 0.0, 5, 0.0, 70.0, 0.0, 2, 0.0, 0.0, 0, 0, 0.0, 0, 0, 0.0, 0, 0, 0, 0, 0, 0.0, 0.0, 0, 11, 0, 0, 0.0, 0, 0.0, 0.0, 0.0, 64.0, 0.0, 0, 0.0, 0.0, 0.0, 0, 116.0, 0, 0, 0, 12, 0.0, 0.0, 0, 6, 221.0, 0, 0.0, 0.0, 3, 0.0, 0, 0.0, 0, 0, 2, 0, 1, 13, 0.0, 0.0, 0, 23.0, 0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 138.0, 0.0, 207.0, 0.0, 0.0, 0.0, 0, 0, 0, 0.0, 0.0, 0.0, 0.0, 0, 0, 0, 0.0, 0, 248.0, 0, 0, 11, 0, 0.0, 0, 0, 0, 0.0, 0, 0, 0, 0, 0, 0.0, 0, 223.0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0.0, 253.0, 53.0, 8, 0, 0, 10, 0.0, 0.0, 0.0, 0, 0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0, 0, 0, 0.0, 0, 0, 0.0, 0, 0.0, 0.0, 0, 0.0, 6, 0.0, 0.0, 0.0, 0.0, 10, 0, 0, 0.0, 0.0, 0.0, 0.0, 0, 221.0, 0, 8, 0.0, 0, 0.0, 0, 0, 0.0, 11, 0, 0, 253.0, 0, 0.0, 0, 0, 0.0, 0, 116.0, 0, 0.0, 0.0, 0.0, 252.0, 0.0, 0.0, 0, 26.0, 0.0, 0, 0.0, 6, 0, 0, 137.0, 0.0, 0.0, 0.0, 0.0, 0.0, 6, 0.0, 0, 116.0, 0.0, 0, 0.0, 0, 1, 0.0, 0, 0, 4, 0.0, 0, 9, 0.0, 0.0, 0, 0, 5.0, 0.0, 0.0, 0, 0, 0.0, 0.0, 0, 0, 64.0, 0, 0.0, 0, 0.0, 0.0, 0, 13, 0, 0.0, 0.0, 0, 0.0, 2, 0, 0.0, 4, 0.0, 0, 0, 0.0, 0, 0.0, 0.0, 116.0, 0, 0, 0.0, 0, 0, 0, 0, 253.0, 11, 25.0, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 210.0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 0, 0, 0.0, 0, 0, 0.0, 0.0, 0, 0, 0, 0, 0, 0, 0, 1, 0.0, 0.0, 0], [0, 15, 0, 0.0, 0, 252.0, 0.0, 0, 0.0, 0.0, 0.0, 8, 0, 0.0, 0.0, 0.0, 0, 217.0, 13, 3, 0.0, 0, 0, 0, 7, 0.0, 17, 0, 0.0, 0.0, 0, 237.0, 0.0, 0, 0, 110.0, 0, 0, 252.0, 0, 0, 0.0, 0, 0, 0.0, 0.0, 252.0, 0, 0.0, 0, 104.0, 0, 0.0, 0, 0.0, 0.0, 253.0, 0.0, 0, 0, 0.0, 0.0, 0, 0, 79.0, 0.0, 0, 0, 0, 0, 0.0, 0, 0, 0, 0, 0, 14, 0.0, 0, 63.0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 191.0, 231.0, 0, 0.0, 0.0, 5, 0, 0, 0.0, 0.0, 252.0, 241.0, 0.0, 10, 0.0, 0.0, 144.0, 0, 0.0, 0.0, 3, 0, 0.0, 0, 0, 0.0, 0, 0, 0, 0, 0, 0.0, 0.0, 0, 4, 0, 10, 0.0, 0, 0.0, 0.0, 0.0, 144.0, 0.0, 0, 0.0, 0.0, 0.0, 0, 144.0, 0, 0, 9, 9, 0.0, 0.0, 0, 0, 253.0, 0, 0.0, 0.0, 11, 0.0, 0, 176.0, 0, 0, 0, 0, 0, 11, 109.0, 0.0, 0, 252.0, 0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 253.0, 0.0, 201.0, 0.0, 0.0, 0.0, 14, 0, 0, 0.0, 0.0, 0.0, 0.0, 4, 0, 0, 0.0, 0, 252.0, 0, 2, 4, 0, 0.0, 0, 0, 0, 145.0, 0, 0, 0, 0, 0, 0.0, 6, 182.0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0.0, 252.0, 0.0, 9, 0, 0, 0, 0.0, 0.0, 0.0, 13, 0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0, 0, 0, 0.0, 0, 0, 0.0, 0, 252.0, 0.0, 0, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 0, 0, 7, 0.0, 0.0, 0.0, 0.0, 0, 215.0, 0, 0, 0.0, 0, 0.0, 0, 0, 21.0, 14, 0, 14, 62.0, 0, 0.0, 0, 0, 0.0, 0, 144.0, 10, 0.0, 0.0, 0.0, 252.0, 144.0, 0.0, 0, 105.0, 0.0, 0, 0.0, 0, 0, 0, 252.0, 0.0, 0.0, 0.0, 0.0, 0.0, 14, 109.0, 0, 145.0, 0.0, 0, 109.0, 0, 11, 0.0, 0, 0, 0, 0.0, 0, 0, 0.0, 0.0, 0, 0, 255.0, 0.0, 0.0, 0, 0, 0.0, 0.0, 0, 0, 237.0, 0, 0.0, 0, 0.0, 108.0, 0, 5, 0, 0.0, 0.0, 0, 0.0, 5, 0, 253.0, 9, 0.0, 0, 0, 0.0, 0, 0.0, 0.0, 62.0, 0, 6, 0.0, 0, 0, 0, 0, 181.0, 2, 0.0, 0.0, 0, 0.0, 84.0, 0.0, 0.0, 0, 0.0, 252.0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 0, 10, 0.0, 12, 0, 0.0, 0.0, 0, 0, 0, 0, 0, 0, 0, 13, 0.0, 0.0, 0], [0, 0, 0, 0.0, 0, 189.0, 254.0, 0, 254.0, 0.0, 0.0, 0, 0, 59.0, 252.0, 59.0, 0, 0.0, 0, 0, 0.0, 0, 0, 0, 0, 0.0, 18, 0, 0.0, 0.0, 0, 0.0, 99.0, 0, 0, 0.0, 0, 0, 0.0, 0, 0, 0.0, 0, 0, 0.0, 187.0, 254.0, 0, 251.0, 0, 0.0, 0, 0.0, 0, 0.0, 254.0, 207.0, 0.0, 0, 0, 151.0, 0.0, 0, 0, 0.0, 254.0, 0, 0, 0, 0, 0.0, 0, 0, 0, 0, 0, 0, 254.0, 0, 144.0, 64.0, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0, 0, 0.0, 0.0, 0.0, 0.0, 168.0, 0, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0, 0.0, 0, 0, 228.0, 0, 0, 0, 0, 0, 0.0, 253.0, 0, 0, 0, 0, 13.0, 0, 254.0, 0.0, 0.0, 40.0, 0.0, 0, 0.0, 0.0, 227.0, 0, 62.0, 0, 0, 0, 0, 254.0, 0.0, 0, 0, 151.0, 0, 0.0, 0.0, 0, 0.0, 0, 213.0, 0, 0, 0, 0, 0, 0, 0.0, 254.0, 0, 0.0, 0, 0, 111.0, 184.0, 0.0, 119.0, 245.0, 0, 0.0, 254.0, 254.0, 0.0, 76.0, 254.0, 0, 0, 0, 6.0, 0.0, 0.0, 0.0, 0, 0, 0, 0.0, 0, 0.0, 0, 0, 0, 0, 0.0, 0, 0, 0, 67.0, 0, 0, 0, 0, 0, 124.0, 0, 207.0, 0.0, 0.0, 0, 0.0, 0.0, 0, 216.0, 0.0, 0.0, 0, 0, 0, 0, 0.0, 0.0, 0.0, 0, 0, 254.0, 0.0, 0, 151.0, 72.0, 0, 0, 0, 0, 0.0, 0, 0, 0.0, 0, 0.0, 0.0, 0, 0.0, 0, 0.0, 0.0, 0.0, 249.0, 0, 0, 0, 0.0, 129.0, 0.0, 0.0, 0, 0.0, 0, 0, 8.0, 0, 0.0, 0, 0, 254.0, 0, 0, 0, 0.0, 0, 22.0, 0, 0, 254.0, 0, 248.0, 0, 0.0, 0.0, 254.0, 88.0, 0.0, 0.0, 0, 233.0, 0.0, 0, 0.0, 0, 0, 0, 0.0, 0.0, 0.0, 0.0, 227.0, 4.0, 0, 0.0, 0, 238.0, 0.0, 0, 0.0, 0, 0, 0.0, 0, 0, 0, 0.0, 0, 0, 153.0, 245.0, 0, 0, 0.0, 0.0, 0.0, 0, 0, 0.0, 147.0, 0, 0, 16.0, 0, 254.0, 0, 0.0, 0.0, 0, 0, 0, 254.0, 0.0, 0, 0.0, 0, 0, 0.0, 0, 168.0, 0, 0, 26.0, 0, 13.0, 0.0, 0.0, 0, 0, 254.0, 0, 0, 0, 0, 0.0, 0, 167.0, 0.0, 0, 0.0, 141.0, 0.0, 0.0, 0, 151.0, 26.0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 0, 0, 60.0, 0, 0, 254.0, 0.0, 0, 0, 0, 0, 0, 0, 0, 0, 227.0, 218.0, 0], [0, 0, 0, 0.0, 0, 11.0, 253.0, 0, 253.0, 0.0, 0.0, 12, 0, 0.0, 0.0, 0.0, 0, 253.0, 10, 4, 0.0, 4, 0, 1, 8, 0.0, 19, 0, 0.0, 0.0, 7, 68.0, 245.0, 1, 0, 0.0, 0, 0, 253.0, 0, 0, 0.0, 0, 6, 0.0, 0.0, 253.0, 0, 0.0, 0, 81.0, 0, 0.0, 0, 0.0, 0.0, 253.0, 0.0, 5, 4, 0.0, 253.0, 0, 0, 42.0, 0.0, 0, 11, 0, 0, 0.0, 0, 0, 0, 0, 0, 11, 0.0, 0, 0.0, 124.0, 15.0, 0.0, 0.0, 8, 0.0, 0.0, 253.0, 0, 0.0, 0.0, 10, 0, 0, 0.0, 0.0, 186.0, 253.0, 0.0, 4, 0.0, 253.0, 253.0, 12, 0.0, 0.0, 4, 2, 0.0, 12, 0, 0.0, 0, 0, 0, 0, 0, 0.0, 171.0, 0, 12, 0, 6, 0.0, 0, 0.0, 0.0, 0.0, 36.0, 0.0, 0, 0.0, 0.0, 42.0, 0, 253.0, 1, 0, 10, 11, 0.0, 0.0, 0, 7, 253.0, 0, 41.0, 0.0, 0, 0.0, 0, 0.0, 0, 0, 4, 0, 5, 6, 0.0, 0.0, 0, 253.0, 0, 2, 0.0, 238.0, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 42.0, 0.0, 245.0, 81.0, 0, 1, 0, 0.0, 0.0, 0.0, 0.0, 6, 0, 2, 0.0, 0, 19.0, 0, 2, 12, 0, 0.0, 0, 0, 0, 143.0, 1, 3, 1, 0, 0, 0.0, 9, 246.0, 42.0, 0.0, 0, 0.0, 0.0, 0, 0.0, 253.0, 253.0, 10, 0, 0, 11, 0.0, 0.0, 0.0, 11, 0, 0.0, 0.0, 0, 0.0, 0.0, 0, 0, 0, 0, 0.0, 0, 1, 253.0, 0, 253.0, 0.0, 0, 0.0, 10, 12.0, 0.0, 0.0, 253.0, 13, 0, 7, 253.0, 0.0, 0.0, 0.0, 0, 253.0, 0, 14, 0.0, 0, 0.0, 0, 0, 0.0, 10, 0, 0, 253.0, 2, 0.0, 0, 0, 0.0, 2, 218.0, 9, 0.0, 0.0, 0.0, 162.0, 217.0, 0.0, 0, 253.0, 0.0, 0, 0.0, 7, 0, 1, 253.0, 15.0, 0.0, 0.0, 0.0, 77.0, 5, 0.0, 0, 143.0, 86.0, 5, 36.0, 0, 0, 0.0, 0, 0, 5, 0.0, 0, 11, 0.0, 0.0, 0, 0, 0.0, 0.0, 0.0, 0, 0, 0.0, 40.0, 0, 1, 0.0, 1, 0.0, 0, 0.0, 253.0, 0, 14, 0, 0.0, 0.0, 0, 0.0, 12, 0, 253.0, 4, 0.0, 6, 0, 0.0, 4, 0.0, 0.0, 253.0, 0, 6, 0.0, 0, 2, 0, 0, 253.0, 12, 245.0, 15.0, 0, 0.0, 245.0, 0.0, 0.0, 0, 0.0, 253.0, 0.0, 0.0, 0.0, 0.0, 0, 215.0, 0, 2, 0.0, 13, 0, 0.0, 0.0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0, 0.0, 1]]} \ No newline at end of file diff --git a/web_console_v2/api/test/fedlearner_webconsole/test_data/sparkapp.tar b/web_console_v2/api/test/fedlearner_webconsole/test_data/sparkapp.tar index 80f2b1e061690e9bc1b0fefdfcb544b7fef349fa..c7d93f210cc81cb39f6b2b28b613014dc096840f 100644 GIT binary patch delta 4322 zcmcIo&2HO95Vj625H*MXoZ12{6Bw23SWAlfbAqyf>ZnMN#xiUtXy6*eid;&Jmt2Be z$q@{LITYxjm$>Q!#OMnY$hnt3L*J*JB_)!UDMt=kU1W)w+_As;hN% zDm5t-{-Fm&2|l+6rWM?i?cs}PAM3n@y_EDk0 zCO6hMKQEfZk__gKh|if{p4!zt%m)*?zSiy&-u1V9f;lO#6{Ap$V5D0%C6Xqw0Ev_6 zIkVjhvKvxq9l^oU+T^eGS{TU*7~c9RaeqikYlwN=W^TD;yB6{Hi~(_QX=8ac7wt?w zxwE%!l}@tJoyxu$$fXU*VJ6W^3;;OxZ4S1Jlb`S0YE#zl+ioBAa9|r-%(d)(nUWEq z+U&{0?eDs4kxRBEQR0?^ae%PjAFWBZG)bNOd8c_ZYjJXO}e;H-#rLH=HC{QZHh+pf)Z9eLxk^Jg(ed5bw zptf!jkFqh;fXSFwe6rWs)$c#t)h?FCFo5I7*zE_zMatc3y|{XkQFG9Z=>EkehO!_fONTuT7fD>Oj+7v~ShRF9LdC$J@sWX=bJp=D8mAXg*XAc(l3fXCDuPJ$z^2twth&)snrgGzXkzH7ck?`5Y!-RIn;k}NW3095F&+o0e3)b+n}`U@ zAQ0gmW1F`EoyfR2JxoJeRP0nnRTL*rZhF`u z4)e!aqpIef%4o+!J|Hf)A!P(jrKdS>8wMM?d>g2y0rsJPlxH!cYvw8L5sDx{frcuz(8}P~3kiakEB_ zD2-?~%3gvoQ>V9J^pCh;mUH-$!B{@A7Yc(gr{Devk?!X-jU z@_)6_cLGn`|I@25+kmfS@;dO%D-KgCT*%Dw2rdEOl)RVVgXt=CujXE60DDKm2@Mx* zWbq1fuQC{zUH(1mBh&Wh=Dc(Tf+nsvv&; zQd*713Vc;t4R}|Oev3!qlWtg51vV_TrNFk-5bqMihV@~j>cVoipR=5S47_1bVE2k& cLqyA2B#NVjE?&-Z%+>16>~i+|9~WaYDC{v8jQ9fsvUZgMp#3si_fzg27}*#+1#BjEw4&nRJ>Yjf{*K3{4D7 z3=Pc<%}q^!%1n%n5z2H087Ir=>1(H?rYR)n=ar=vmE;#`Bo~*(r)B1(S}7El6zM3G zq!pzm=NF|Eg9Rt2>1paF78Lj;=B8?>2Nxt3Wjj^6RwU*Y+|RZ-z;go@;yoh(P`;4& diff --git a/web_console_v2/api/test/fedlearner_webconsole/utils/base64_test.py b/web_console_v2/api/test/fedlearner_webconsole/utils/base64_test.py new file mode 100644 index 000000000..2667e1025 --- /dev/null +++ b/web_console_v2/api/test/fedlearner_webconsole/utils/base64_test.py @@ -0,0 +1,37 @@ +# Copyright 2021 The FedLearner Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# coding: utf-8 +import unittest + +from fedlearner_webconsole.utils.base64 import base64encode, base64decode + + +class Base64Test(unittest.TestCase): + def test_base64encode(self): + self.assertEqual(base64encode('hello 1@2'), 'aGVsbG8gMUAy') + self.assertEqual(base64encode('😈'), '8J+YiA==') + + def test_base64decode(self): + self.assertEqual(base64decode('aGVsbG8gMUAy'), 'hello 1@2') + self.assertEqual(base64decode('JjEzOVlUKiYm'), '&139YT*&&') + + def test_base64_encode_and_decode(self): + self.assertEqual(base64decode(base64encode('test')), 'test') + self.assertEqual(base64encode(base64decode('aGVsbG8gMUAy')), + 'aGVsbG8gMUAy') + + +if __name__ == '__main__': + unittest.main() diff --git a/web_console_v2/api/test/fedlearner_webconsole/utils/file_manager_test.py b/web_console_v2/api/test/fedlearner_webconsole/utils/file_manager_test.py index 34ab41bc0..f32bf303e 100644 --- a/web_console_v2/api/test/fedlearner_webconsole/utils/file_manager_test.py +++ b/web_console_v2/api/test/fedlearner_webconsole/utils/file_manager_test.py @@ -15,20 +15,20 @@ # coding: utf-8 import os import shutil -import stat import tempfile import unittest +from collections import namedtuple from pathlib import Path -from unittest.mock import patch, MagicMock -from pyarrow import fs +from tensorflow.io import gfile -from fedlearner_webconsole.utils.file_manager import (DefaultFileManager, - HdfsFileManager, - FileManager, File) +from fedlearner_webconsole.utils.file_manager import GFileFileManager, FileManager, File +FakeFileStatistics = namedtuple('FakeFileStatistics', ['length', 'mtime_nsec']) + + +class GFileFileManagerTest(unittest.TestCase): -class DefaultFileManagerTest(unittest.TestCase): _F1_SIZE = 3 _F2_SIZE = 4 _S1_SIZE = 55 @@ -37,21 +37,21 @@ class DefaultFileManagerTest(unittest.TestCase): _S1_MTIME = 1613982392 def _get_file_stat(self, orig_os_stat, path): - faked = list(orig_os_stat(path)) + gfile_stat = FakeFileStatistics(2, 1613982390 * 1e9) if path == self._get_temp_path('f1.txt') or \ path == self._get_temp_path('subdir/f1.txt'): - faked[stat.ST_SIZE] = self._F1_SIZE - faked[stat.ST_MTIME] = self._F1_MTIME - return os.stat_result(faked) + gfile_stat = FakeFileStatistics(self._F1_SIZE, + self._F1_MTIME * 1e9) + return gfile_stat elif path == self._get_temp_path('f2.txt') or \ path == self._get_temp_path('f3.txt'): - faked[stat.ST_SIZE] = self._F2_SIZE - faked[stat.ST_MTIME] = self._F2_MTIME - return os.stat_result(faked) + gfile_stat = FakeFileStatistics(self._F2_SIZE, + self._F2_MTIME * 1e9) + return gfile_stat elif path == self._get_temp_path('subdir/s1.txt'): - faked[stat.ST_SIZE] = self._S1_SIZE - faked[stat.ST_MTIME] = self._S1_MTIME - return os.stat_result(faked) + gfile_stat = FakeFileStatistics(self._S1_SIZE, + self._S1_MTIME * 1e9) + return gfile_stat else: return orig_os_stat(path) @@ -70,9 +70,9 @@ def setUp(self): def fake_stat(path, *arg, **kwargs): return self._get_file_stat(self._orig_os_stat, path) - os.stat = fake_stat + gfile.stat = fake_stat - self._fm = DefaultFileManager() + self._fm = GFileFileManager() def tearDown(self): os.stat = self._orig_os_stat @@ -177,119 +177,16 @@ def test_mkdir(self): self._fm.mkdir(os.path.join(self._get_temp_path(), 'subdir2')) self.assertTrue(os.path.isdir(self._get_temp_path('subdir2'))) - -class HdfsFileManagerTest(unittest.TestCase): - def setUp(self): - self._envs_patcher = patch('envs.Envs.HDFS_SERVER', 'hdfs://haruna/') - self._envs_patcher.start() - - self._mock_client = MagicMock() - self._mock_client_generator = MagicMock() - self._mock_client_generator.from_uri.return_value = (self._mock_client, - '/') - self._client_patcher = patch( - 'fedlearner_webconsole.utils.file_manager.FileSystem', - self._mock_client_generator) - self._client_patcher.start() - - self._fm = HdfsFileManager() - - def tearDown(self): - self._envs_patcher.stop() - self._client_patcher.stop() - - def test_can_handle(self): - self.assertFalse(self._fm.can_handle('/data/abc')) - self.assertTrue(self._fm.can_handle('hdfs://abc')) - - def test_ls(self): - mock_ls = MagicMock() - self._mock_client.get_file_info = mock_ls - mock_ls.side_effect = [ - fs.FileInfo(type=fs.FileType.Directory, - path='/data', - size=1024, - mtime_ns=1367317325346000000), - [ - fs.FileInfo(type=fs.FileType.File, - path='/data/abc', - size=1024, - mtime_ns=1367317325346000000), - fs.FileInfo(type=fs.FileType.Directory, - path='/data', - size=1024, - mtime_ns=1367317325346000000), - ] - ] - self.assertEqual( - self._fm.ls('hdfs:///data', recursive=True), - [File(path='hdfs:///data/abc', size=1024, mtime=1367317325)]) - mock_ls.assert_called() - - mock_ls = MagicMock() - self._mock_client.get_file_info = mock_ls - mock_ls.return_value = fs.FileInfo(type=fs.FileType.File, - path='/data/abc', - size=1024, - mtime_ns=1367317325346000000) - self.assertEqual( - self._fm.ls('hdfs:///data/abc', recursive=True), - [File(path='hdfs:///data/abc', size=1024, mtime=1367317325)]) - mock_ls.assert_called_once() - - @staticmethod - def _yield_files(files): - for file in files: - yield file - - def test_move(self): - mock_rename = MagicMock() - self._mock_client.move = mock_rename - mock_rename.return_value = self._yield_files(['/data/123']) - self.assertTrue(self._fm.move('hdfs:///data/abc', 'hdfs:///data/123')) - mock_rename.assert_called_once_with('/data/abc', '/data/123') - - mock_rename.return_value = self._yield_files([]) - self.assertFalse(self._fm.move('hdfs:///data/abc', 'hdfs:///data/123')) - - def test_remove_dir(self): - mock_get_file_info = MagicMock() - self._mock_client.get_file_info = mock_get_file_info - mock_get_file_info.return_value = fs.FileInfo(type=fs.FileType.File, - path='/data/123', - size=1024) - - self.assertTrue(self._fm.remove('hdfs:///data/123')) - self._mock_client.delete_file.assert_called_once_with('/data/123') - self._mock_client.delete_dir.assert_not_called() - - def test_remove_file(self): - mock_get_file_info = MagicMock() - self._mock_client.get_file_info = mock_get_file_info - mock_get_file_info.return_value = fs.FileInfo( - type=fs.FileType.Directory, path='/data/123', size=1024) - - self.assertTrue(self._fm.remove('hdfs:///data/123')) - self._mock_client.delete_file.assert_not_called() - self._mock_client.delete_dir.assert_called_once_with('/data/123') - - @patch('tensorflow.io.gfile.copy') - def test_copy(self, mock_copy): - self.assertTrue(self._fm.copy('hdfs:///source', 'hdfs:///dest')) - mock_copy.assert_called_once_with('hdfs:///source', 'hdfs:///dest') - - def test_mkdir(self): - mock_mkdir = MagicMock() - self._mock_client.create_dir = mock_mkdir - self.assertTrue(self._fm.mkdir('hdfs:///data')) - mock_mkdir.assert_called_once_with('/data') + def test_read(self): + content = self._fm.read(self._get_temp_path('f1.txt')) + self.assertEqual('xxx', content) class FileManagerTest(unittest.TestCase): @classmethod def setUpClass(cls): - os.environ[ - 'CUSTOMIZED_FILE_MANAGER'] = 'testing.fake_file_manager:FakeFileManager' + fake_fm = 'testing.fake_file_manager:FakeFileManager' + os.environ['CUSTOMIZED_FILE_MANAGER'] = fake_fm @classmethod def tearDownClass(cls): @@ -302,7 +199,7 @@ def test_can_handle(self): self.assertTrue(self._fm.can_handle('fake://123')) # Falls back to default manager self.assertTrue(self._fm.can_handle('/data/123')) - self.assertFalse(self._fm.can_handle('hdfs:///123')) + self.assertFalse(self._fm.can_handle('unsupported:///123')) def test_ls(self): self.assertEqual(self._fm.ls('fake://data'), [{ @@ -322,7 +219,8 @@ def test_remove(self): self.assertTrue(self._fm.remove('fake://remove/123')) self.assertFalse(self._fm.remove('fake://do_not_remove/123')) # No file manager can handle this - self.assertRaises(RuntimeError, lambda: self._fm.remove('hdfs://123')) + self.assertRaises(RuntimeError, + lambda: self._fm.remove('unsupported://123')) def test_copy(self): self.assertTrue(self._fm.copy('fake://copy/123', 'fake://copy/234')) @@ -336,7 +234,8 @@ def test_mkdir(self): self.assertTrue(self._fm.mkdir('fake://mkdir/123')) self.assertFalse(self._fm.mkdir('fake://do_not_mkdir/123')) # No file manager can handle this - self.assertRaises(RuntimeError, lambda: self._fm.mkdir('hdfs:///123')) + self.assertRaises(RuntimeError, + lambda: self._fm.mkdir('unsupported:///123')) if __name__ == '__main__': diff --git a/web_console_v2/api/test/fedlearner_webconsole/workflow/apis_test.py b/web_console_v2/api/test/fedlearner_webconsole/workflow/apis_test.py index a911e30ec..2c6382658 100644 --- a/web_console_v2/api/test/fedlearner_webconsole/workflow/apis_test.py +++ b/web_console_v2/api/test/fedlearner_webconsole/workflow/apis_test.py @@ -14,6 +14,8 @@ # coding: utf-8 import logging +import random +import string import time import json import unittest @@ -36,7 +38,7 @@ from fedlearner_webconsole.proto.common_pb2 import CreateJobFlag from fedlearner_webconsole.workflow.apis import is_peer_job_inheritance_matched from testing.common import BaseTestCase - +from fedlearner_webconsole.db import db_handler class WorkflowsApiTest(BaseTestCase): class Config(BaseTestCase.Config): @@ -47,15 +49,9 @@ def setUp(self): self.maxDiff = None super().setUp() # Inserts data - workflow1 = Workflow(name='workflow_key_get1', - project_id=1 - ) - workflow2 = Workflow(name='workflow_kay_get2', - project_id=2 - ) - workflow3 = Workflow(name='workflow_key_get3', - project_id=2 - ) + workflow1 = Workflow(name='workflow_key_get1', project_id=1) + workflow2 = Workflow(name='workflow_kay_get2', project_id=2) + workflow3 = Workflow(name='workflow_key_get3', project_id=2) db.session.add(workflow1) db.session.add(workflow2) db.session.add(workflow3) @@ -77,11 +73,10 @@ def test_get_with_keyword(self): def test_get_workflows(self): time.sleep(1) - workflow = Workflow(name='last', - project_id=1 - ) + workflow = Workflow(name='last', project_id=1) db.session.add(workflow) db.session.flush() + db.session.commit() response = self.get_helper('/api/v2/workflows') data = self.get_response_data(response) self.assertEqual(data[0]['name'], 'last') @@ -91,18 +86,22 @@ def test_get_workflows(self): def test_create_new_workflow(self, mock_uuid, mock_wakeup): mock_uuid.return_value = UUID('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') with open( - Path(__file__, '../../test_data/workflow_config.json').resolve() - ) as workflow_config: + Path(__file__, '../../test_data/workflow_config.json').resolve( + )) as workflow_config: config = json.load(workflow_config) + extra = ''.join( + random.choice(string.ascii_lowercase) for _ in range(10)) + # extra should be a valid json string so we mock one + extra = f'{{"parent_job_name":"{extra}"}}' workflow = { 'name': 'test-workflow', 'project_id': 1234567, 'forkable': True, 'comment': 'test-comment', - 'config': config + 'config': config, + 'extra': extra } - response = self.post_helper('/api/v2/workflows', - data=workflow) + response = self.post_helper('/api/v2/workflows', data=workflow) self.assertEqual(response.status_code, HTTPStatus.CREATED) created_workflow = json.loads(response.data).get('data') # Check scheduler @@ -115,35 +114,36 @@ def test_create_new_workflow(self, mock_uuid, mock_wakeup): del created_workflow['updated_at'] del created_workflow['start_at'] del created_workflow['stop_at'] - self.assertEqual(created_workflow, { - 'batch_update_interval': -1, - 'name': 'test-workflow', - 'project_id': 1234567, - 'forkable': True, - 'forked_from': None, - 'metric_is_public': False, - 'comment': 'test-comment', - 'state': 'NEW', - 'target_state': 'READY', - 'transaction_state': 'READY', - 'transaction_err': None, - 'create_job_flags': [1, 1, 1], - 'peer_create_job_flags': None, - 'job_ids': [], - 'transaction_state': 'READY', - 'last_triggered_batch': None, - 'recur_at': None, - 'recur_type': 'NONE', - 'trigger_dataset': None, - 'uuid': f'u{mock_uuid().hex[:19]}' - }) + self.assertEqual( + created_workflow, { + 'batch_update_interval': -1, + 'name': 'test-workflow', + 'project_id': 1234567, + 'extra': extra, + 'forkable': True, + 'forked_from': None, + 'metric_is_public': False, + 'comment': 'test-comment', + 'state': 'NEW', + 'target_state': 'READY', + 'transaction_state': 'READY', + 'transaction_err': None, + 'create_job_flags': [1, 1, 1], + 'peer_create_job_flags': None, + 'job_ids': [], + 'transaction_state': 'READY', + 'last_triggered_batch': None, + 'recur_at': None, + 'recur_type': 'NONE', + 'trigger_dataset': None, + 'uuid': f'u{mock_uuid().hex[:19]}' + }) # Check DB self.assertEqual(len(Workflow.query.all()), 4) # Post again mock_wakeup.reset_mock() - response = self.post_helper('/api/v2/workflows', - data=workflow) + response = self.post_helper('/api/v2/workflows', data=workflow) self.assertEqual(response.status_code, HTTPStatus.CONFLICT) # Check mock mock_wakeup.assert_not_called() @@ -153,7 +153,8 @@ def test_create_new_workflow(self, mock_uuid, mock_wakeup): @patch('fedlearner_webconsole.workflow.apis.composer.get_item_status') @patch('fedlearner_webconsole.workflow.apis.composer.collect') @patch('fedlearner_webconsole.workflow.apis.scheduler.wakeup') - def test_post_batch_update_interval_job(self, mock_wakeup, mock_collect, mock_get_item_status): + def test_post_batch_update_interval_job(self, mock_wakeup, mock_collect, + mock_get_item_status): mock_get_item_status.return_value = None with open( Path(__file__, '../../test_data/workflow_config.json').resolve( @@ -194,54 +195,48 @@ def test_fork_workflow(self): class WorkflowApiTest(BaseTestCase): def test_put_successfully(self): config = { - 'participants': [ - { - 'name': 'party_leader', - 'url': '127.0.0.1:5000', - 'domain_name': 'fl-leader.com' - } - ], - 'variables': [ - { - 'name': 'namespace', - 'value': 'leader' - }, - { - 'name': 'basic_envs', - 'value': '{}' - }, - { - 'name': 'storage_root_dir', - 'value': '/' - }, - { - 'name': 'EGRESS_URL', - 'value': '127.0.0.1:1991' - } - ] + 'participants': [{ + 'name': 'party_leader', + 'url': '127.0.0.1:5000', + 'domain_name': 'fl-leader.com' + }], + 'variables': [{ + 'name': 'namespace', + 'value': 'leader' + }, { + 'name': 'basic_envs', + 'value': '{}' + }, { + 'name': 'storage_root_dir', + 'value': '/' + }, { + 'name': 'EGRESS_URL', + 'value': '127.0.0.1:1991' + }] } - project = Project(name='test', - config=ParseDict(config, - project_pb2.Project()).SerializeToString()) + project = Project( + name='test', + config=ParseDict(config, + project_pb2.Project()).SerializeToString()) db.session.add(project) workflow = Workflow( name='test-workflow', project_id=1, state=WorkflowState.NEW, transaction_state=TransactionState.PARTICIPANT_PREPARE, - target_state=WorkflowState.READY - ) + target_state=WorkflowState.READY) db.session.add(workflow) db.session.commit() db.session.refresh(workflow) - response = self.put_helper( - f'/api/v2/workflows/{workflow.id}', - data={ - 'forkable': True, - 'config': {'group_alias': 'test-template'}, - 'comment': 'test comment' - }) + response = self.put_helper(f'/api/v2/workflows/{workflow.id}', + data={ + 'forkable': True, + 'config': { + 'group_alias': 'test-template' + }, + 'comment': 'test comment' + }) self.assertEqual(response.status_code, HTTPStatus.OK) updated_workflow = Workflow.query.get(workflow.id) @@ -262,12 +257,13 @@ def test_put_resetting(self): db.session.commit() db.session.refresh(workflow) - response = self.put_helper( - f'/api/v2/workflows/{workflow.id}', - data={ - 'forkable': True, - 'config': {'group_alias': 'test-template'}, - }) + response = self.put_helper(f'/api/v2/workflows/{workflow.id}', + data={ + 'forkable': True, + 'config': { + 'group_alias': 'test-template' + }, + }) self.assertEqual(response.status_code, HTTPStatus.CONFLICT) @patch('fedlearner_webconsole.workflow.apis.scheduler.wakeup') @@ -283,11 +279,8 @@ def test_patch_successfully(self, mock_wakeup): db.session.commit() db.session.refresh(workflow) - response = self.patch_helper( - f'/api/v2/workflows/{workflow.id}', - data={ - 'target_state': 'RUNNING' - }) + response = self.patch_helper(f'/api/v2/workflows/{workflow.id}', + data={'target_state': 'RUNNING'}) self.assertEqual(response.status_code, HTTPStatus.OK) patched_data = json.loads(response.data).get('data') self.assertEqual(patched_data['id'], workflow.id) @@ -301,26 +294,22 @@ def test_patch_successfully(self, mock_wakeup): @patch('fedlearner_webconsole.workflow.apis.scheduler.wakeup') def test_patch_invalid_target_state(self, mock_wakeup): - workflow = Workflow( - name='test-workflow', - project_id=123, - config=WorkflowDefinition().SerializeToString(), - forkable=False, - state=WorkflowState.READY, - target_state=WorkflowState.RUNNING - ) + workflow = Workflow(name='test-workflow', + project_id=123, + config=WorkflowDefinition().SerializeToString(), + forkable=False, + state=WorkflowState.READY, + target_state=WorkflowState.RUNNING) db.session.add(workflow) db.session.commit() db.session.refresh(workflow) - response = self.patch_helper( - f'/api/v2/workflows/{workflow.id}', - data={ - 'target_state': 'READY' - }) + response = self.patch_helper(f'/api/v2/workflows/{workflow.id}', + data={'target_state': 'READY'}) self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST) - self.assertEqual(json.loads(response.data).get('details'), - 'Another transaction is in progress [1]') + self.assertEqual( + json.loads(response.data).get('details'), + 'Another transaction is in progress [1]') # Checks DB patched_workflow = Workflow.query.get(workflow.id) self.assertEqual(patched_workflow.state, WorkflowState.READY) @@ -332,7 +321,9 @@ def test_patch_invalid_target_state(self, mock_wakeup): @patch('fedlearner_webconsole.workflow.apis.composer.patch_item_attr') @patch('fedlearner_webconsole.workflow.apis.composer.finish') @patch('fedlearner_webconsole.workflow.apis.composer.collect') - def test_patch_batch_update_interval(self, mock_collect, mock_finish, mock_patch_item, mock_get_item_status): + def test_patch_batch_update_interval(self, mock_collect, mock_finish, + mock_patch_item, + mock_get_item_status): mock_get_item_status.side_effect = [None, ItemStatus.ON] workflow = Workflow( name='test-workflow-left', @@ -347,8 +338,9 @@ def test_patch_batch_update_interval(self, mock_collect, mock_finish, mock_patch db.session.refresh(workflow) # test create cronjob - response = self.patch_helper(f'/api/v2/workflows/{workflow.id}', - data={'batch_update_interval': batch_update_interval}) + response = self.patch_helper( + f'/api/v2/workflows/{workflow.id}', + data={'batch_update_interval': batch_update_interval}) self.assertEqual(response.status_code, HTTPStatus.OK) mock_collect.assert_called_with( @@ -359,11 +351,14 @@ def test_patch_batch_update_interval(self, mock_collect, mock_finish, mock_patch # patch new interval time for cronjob batch_update_interval = 2 - response = self.patch_helper(f'/api/v2/workflows/{workflow.id}', - data={'batch_update_interval': batch_update_interval}) + response = self.patch_helper( + f'/api/v2/workflows/{workflow.id}', + data={'batch_update_interval': batch_update_interval}) self.assertEqual(response.status_code, HTTPStatus.OK) - mock_patch_item.assert_called_with(name=f'workflow_cron_job_{workflow.id}', key='interval_time', value=batch_update_interval * 60) - + mock_patch_item.assert_called_with( + name=f'workflow_cron_job_{workflow.id}', + key='interval_time', + value=batch_update_interval * 60) # test stop cronjob response = self.patch_helper(f'/api/v2/workflows/{workflow.id}', @@ -386,56 +381,24 @@ def test_patch_batch_update_interval(self, mock_collect, mock_finish, mock_patch data={'batch_update_interval': 1}) self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST) - - def test_patch_not_found(self): - response = self.patch_helper( - '/api/v2/workflows/1', - data={ - 'target_state': 'RUNNING' - }) + response = self.patch_helper('/api/v2/workflows/1', + data={'target_state': 'RUNNING'}) self.assertEqual(response.status_code, HTTPStatus.NOT_FOUND) def test_patch_create_job_flags(self): - wd = WorkflowDefinition() - jd = wd.job_definitions.add() - workflow = Workflow( - name='test-workflow', - project_id=123, - config=wd.SerializeToString(), - forkable=False, - state=WorkflowState.READY, - ) - db.session.add(workflow) - db.session.flush() - job = Job( - name='test_job', - job_type=JobType(1), - config=jd.SerializeToString(), - workflow_id=workflow.id, - project_id=123, - state=JobState.STOPPED, - is_disabled=False) - db.session.add(job) - db.session.flush() - workflow.job_ids = str(job.id) - db.session.commit() - response = self.patch_helper( - f'/api/v2/workflows/{workflow.id}', - data={ - 'create_job_flags': [3] - }) - self.assertEqual(response.status_code, HTTPStatus.OK) - patched_job = Job.query.get(job.id) - self.assertEqual(patched_job.is_disabled, True) - response = self.patch_helper( - f'/api/v2/workflows/{workflow.id}', - data={ - 'create_job_flags': [1] - }) - self.assertEqual(response.status_code, HTTPStatus.OK) - patched_job = Job.query.get(job.id) - self.assertEqual(patched_job.is_disabled, False) + with db_handler.session_scope() as session: + workflow, job = add_fake_workflow(session) + response = self.patch_helper(f'/api/v2/workflows/{workflow.id}', + data={'create_job_flags': [3]}) + self.assertEqual(response.status_code, HTTPStatus.OK) + patched_job = Job.query.get(job.id) + self.assertEqual(patched_job.is_disabled, True) + response = self.patch_helper(f'/api/v2/workflows/{workflow.id}', + data={'create_job_flags': [1]}) + self.assertEqual(response.status_code, HTTPStatus.OK) + patched_job = Job.query.get(job.id) + self.assertEqual(patched_job.is_disabled, False) # TODO: Move it to service_test @patch('fedlearner_webconsole.rpc.client.RpcClient.get_workflow') @@ -461,14 +424,51 @@ def test_is_peer_job_inheritance_matched(self, mock_get_workflow): workflow1 = Workflow(project=project, forked_from=workflow0.id) workflow1.set_config(config) workflow1.set_create_job_flags([CreateJobFlag.REUSE]) - workflow1.set_peer_create_job_flags([CreateJobFlag.NEW, CreateJobFlag.REUSE]) - + workflow1.set_peer_create_job_flags( + [CreateJobFlag.NEW, CreateJobFlag.REUSE]) self.assertTrue(is_peer_job_inheritance_matched(workflow1)) workflow1.set_create_job_flags([CreateJobFlag.NEW]) self.assertFalse(is_peer_job_inheritance_matched(workflow1)) + def test_is_local(self): + with db_handler.session_scope() as session: + workflow, job = add_fake_workflow(session) + self.assertTrue(workflow.is_local()) + config = workflow.get_config() + config.job_definitions[ + 0].is_federated = True + workflow.set_config(config) + self.assertFalse(False, workflow.is_local()) + + +def add_fake_workflow(session): + wd = WorkflowDefinition() + jd = wd.job_definitions.add() + workflow = Workflow( + name='test-workflow', + project_id=123, + config=wd.SerializeToString(), + forkable=False, + state=WorkflowState.READY, + ) + session.add(workflow) + session.flush() + job = Job( + name='test_job', + job_type=JobType(1), + config=jd.SerializeToString(), + workflow_id=workflow.id, + project_id=123, + state=JobState.STOPPED, + is_disabled=False) + session.add(job) + session.flush() + workflow.job_ids = str(job.id) + session.commit() + return workflow, job + if __name__ == '__main__': unittest.main() diff --git a/web_console_v2/api/testing/common.py b/web_console_v2/api/testing/common.py index 6ebb41930..fff83b5a0 100644 --- a/web_console_v2/api/testing/common.py +++ b/web_console_v2/api/testing/common.py @@ -15,7 +15,6 @@ # coding: utf-8 import contextlib import json -import os import logging import unittest import secrets @@ -25,20 +24,29 @@ from flask import Flask from flask_testing import TestCase from fedlearner_webconsole.composer.composer import Composer, ComposerConfig -from fedlearner_webconsole.db import db +from fedlearner_webconsole.db import db_handler as db, get_database_uri from fedlearner_webconsole.app import create_app from fedlearner_webconsole.initial_db import initial_db +from fedlearner_webconsole.scheduler.scheduler import scheduler # NOTE: the following models imported is intended to be analyzed by SQLAlchemy from fedlearner_webconsole.auth.models import Role, User, State from fedlearner_webconsole.composer.models import SchedulerItem, SchedulerRunner, OptimisticLock +from fedlearner_webconsole.utils.base64 import base64encode -test_db_path = '/tmp/fedlearner_test.db' + +def create_all_tables(database_uri: str = None): + if database_uri: + db.rebind(database_uri) + + # If there's a db file due to some reason, remove it first. + if db.metadata.tables.values(): + db.drop_all() + db.create_all() class BaseTestCase(TestCase): class Config(object): - SQLALCHEMY_DATABASE_URI = \ - f'sqlite:///{test_db_path}?check_same_thread=False' + SQLALCHEMY_DATABASE_URI = get_database_uri() SQLALCHEMY_TRACK_MODIFICATIONS = False JWT_SECRET_KEY = secrets.token_urlsafe(64) PROPAGATE_EXCEPTIONS = True @@ -49,26 +57,20 @@ class Config(object): START_COMPOSER = False def create_app(self): + create_all_tables(self.__class__.Config.SQLALCHEMY_DATABASE_URI) + initial_db() app = create_app(self.__class__.Config) - app.app_context().push() return app def setUp(self): - try: - # keep clean database before each test - os.remove(test_db_path) - except OSError: - pass - - db.create_all() - initial_db() + super().setUp() self.signin_helper() def tearDown(self): self.signout_helper() - - db.session.remove() + scheduler.stop() db.drop_all() + super().tearDown() def get_response_data(self, response): return json.loads(response.data).get('data') @@ -81,7 +83,7 @@ def signin_helper(self, username='ada', password='fl@123.'): resp = self.client.post('/api/v2/auth/signin', data=json.dumps({ 'username': username, - 'password': password + 'password': base64encode(password) }), content_type='application/json') resp_data = self.get_response_data(resp) @@ -232,7 +234,8 @@ def new_tear_down(*args, **kwargs): ) assert result.wasSuccessful() self._result_queue.put(True) - except Exception: + except Exception as err: + logging.error('expected happened %s', err) self._result_queue.put(False) raise @@ -264,13 +267,14 @@ def multi_process_test(test_list): raise Exception(f'Subprocess failed: number {i}') -def create_test_db(): - """Creates test db for testing non flask-must units.""" - app = Flask('fedlearner_webconsole_test') - app.config['TESTING'] = True - app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:' - app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False - db.init_app(app) - # this does the binding - app.app_context().push() - return db +class NoWebServerTestCase(unittest.TestCase): + class Config(object): + SQLALCHEMY_DATABASE_URI = get_database_uri() + + def setUp(self) -> None: + super().setUp() + create_all_tables(self.__class__.Config.SQLALCHEMY_DATABASE_URI) + + def tearDown(self) -> None: + db.drop_all() + return super().tearDown() \ No newline at end of file diff --git a/web_console_v2/api/tools/local_runner/app_a.py b/web_console_v2/api/tools/local_runner/app_a.py index efa15188f..a9f9cb314 100644 --- a/web_console_v2/api/tools/local_runner/app_a.py +++ b/web_console_v2/api/tools/local_runner/app_a.py @@ -15,12 +15,14 @@ # coding: utf-8 import os import logging + +from envs import Envs from fedlearner_webconsole.app import create_app from tools.local_runner.initial_db import init_db -BASE_DIR = os.path.abspath(os.path.dirname(__file__)) BASE_DIR = os.path.abspath(os.path.dirname(__file__)) + class Config(object): SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(BASE_DIR, 'app_a.db') MYSQL_CHARSET = 'utf8mb4' @@ -31,10 +33,13 @@ class Config(object): LOGGING_LEVEL = logging.INFO GRPC_LISTEN_PORT = 1993 JWT_ACCESS_TOKEN_EXPIRES = 86400 + STORAGE_ROOT = Envs.STORAGE_ROOT START_GRPC_SERVER = True START_SCHEDULER = True - START_COMPOSER = False + START_COMPOSER = True + + app = create_app(Config) diff --git a/web_console_v2/api/tools/local_runner/app_b.py b/web_console_v2/api/tools/local_runner/app_b.py index 8c894270b..e9f7cb612 100644 --- a/web_console_v2/api/tools/local_runner/app_b.py +++ b/web_console_v2/api/tools/local_runner/app_b.py @@ -15,11 +15,14 @@ # coding: utf-8 import os import logging + +from envs import Envs from fedlearner_webconsole.app import create_app from tools.local_runner.initial_db import init_db BASE_DIR = os.path.abspath(os.path.dirname(__file__)) + class Config(object): SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(BASE_DIR, 'app_b.db') MYSQL_CHARSET = 'utf8mb4' @@ -30,10 +33,13 @@ class Config(object): LOGGING_LEVEL = logging.INFO GRPC_LISTEN_PORT = 1991 JWT_ACCESS_TOKEN_EXPIRES = 86400 + STORAGE_ROOT = Envs.STORAGE_ROOT START_GRPC_SERVER = True START_SCHEDULER = True START_COMPOSER = False + + app = create_app(Config) diff --git a/web_console_v2/api/tools/local_runner/run_a.sh b/web_console_v2/api/tools/local_runner/run_a.sh index a48b260ea..6622b5f4c 100755 --- a/web_console_v2/api/tools/local_runner/run_a.sh +++ b/web_console_v2/api/tools/local_runner/run_a.sh @@ -3,8 +3,9 @@ export FLASK_APP=app_a:app export FLASK_ENV=development flask create-db export K8S_CONFIG_PATH=$1 -export FEDLEARNER_WEBCONSOLE_POLLING_INTERVAL=1 +export FEDLEARNER_WEBCONSOLE_POLLING_INTERVAL=10 export SQLALCHEMY_DATABASE_URI="sqlite:///app_a.db" export FEATURE_MODEL_WORKFLOW_HOOK=True export FEATURE_MODEL_K8S_HOOK=True +export ES_READ_HOST=172.21.8.76 # aliyun-demo1 fedlearner-stack-elasticsearch-client flask run --host=0.0.0.0 --no-reload --eager-loading -p 9001 diff --git a/web_console_v2/api/tools/local_runner/run_b.sh b/web_console_v2/api/tools/local_runner/run_b.sh index 9eb7608aa..340f94763 100755 --- a/web_console_v2/api/tools/local_runner/run_b.sh +++ b/web_console_v2/api/tools/local_runner/run_b.sh @@ -7,4 +7,5 @@ export FEDLEARNER_WEBCONSOLE_POLLING_INTERVAL=1 export SQLALCHEMY_DATABASE_URI="sqlite:///app_b.db" export FEATURE_MODEL_WORKFLOW_HOOK=True export FEATURE_MODEL_K8S_HOOK=True +export ES_READ_HOST=172.21.14.199 # aliyun-demo2 fedlearner-stack-elasticsearch-client flask run --host=0.0.0.0 --no-reload --eager-loading -p 9002 diff --git a/web_console_v2/client/package.json b/web_console_v2/client/package.json index a6b05a876..0aa988a8a 100644 --- a/web_console_v2/client/package.json +++ b/web_console_v2/client/package.json @@ -5,7 +5,7 @@ "scripts": { "preinstall": "npx only-allow pnpm", "start": "node scripts/start.js", - "build": "node scripts/build.js", + "build": "node --max-old-space-size=4096 scripts/build.js", "test": "node scripts/test.js", "test:coverage": "npx jest --coverage", "lint": "eslint '*/**/*.{js,ts,tsx}' --fix", diff --git a/web_console_v2/client/src/App.tsx b/web_console_v2/client/src/App.tsx index 58ef482e0..1bcddae30 100644 --- a/web_console_v2/client/src/App.tsx +++ b/web_console_v2/client/src/App.tsx @@ -32,10 +32,11 @@ const AppSidebar = styled(Sidebar)` `; const AppMainContent = styled.main` + position: relative; display: flex; flex-direction: column; grid-area: main-content; - padding: var(--contentOuterPadding); + overflow: auto; overflow-anchor: auto; `; diff --git a/web_console_v2/client/src/assets/images/avatar.jpg b/web_console_v2/client/src/assets/images/avatar.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f19714c7795d86c07eb68f04c16c3ea5ebe84f88 GIT binary patch literal 20674 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 - - - - - - - - - - - - - - - diff --git a/web_console_v2/client/src/assets/images/login-illustration.png b/web_console_v2/client/src/assets/images/login-illustration.png new file mode 100644 index 0000000000000000000000000000000000000000..cdcf3146570b6db6e24044e24f7ccc140eb4d5d6 GIT binary patch literal 186771 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�{i__;hv>}3_>dhd%;{&@;V9tVO=|sVKl!v2W$gUwra;93$*>yFx>Ye zcC4l!f_@Yt;h3azsf2nfD=Y6tz)*!tOrA8r@RL9JUw&+MZtg#gRmNQMrY8!2quzm( zLyEP9j6jLO0a5sUNI^dBbEJfswUgqdMX1PZ!O9!W4(AqE_{F$<{|vrBlAZb%me*i% zyu{~ZUj#HuXzU2<8=I)$((Kdl;K^k+bxffWrcgzq;OYDnf=LRW@+RE(=s7Ht=1@bD z4zeQT!%#o~P{?QaVIbcM7vH{>^(wTwJ_CtO1RBIh5Y12Nlob$B3LIe$JaTdofx$F0 z5d@5GJW_4+4N|_KvYLr*cko zV9ZH#8yqURf{*2T-G@p(^i81uA3ocJbE`erGnHik zgFt-08=1!;{(L`gUNzW&x5!M2l?+6yn=N z!e``ou`>X$BT$hbQbzkN=tmjrQ;2>J>${|;#;{80t_oLSnKh~HN90)-{#2-Pk4ki_KuNRskL~xAYF_6rveTq!-$*k+Z z*3lXb3Z0kUIIqXdU_o0U#djI?0_r7y*iOG`! z7~b`+cinXG@R2_*6$^RtLnz|Y_LYZGu~%}au#i1KTRz#rDYbH^+XhtENXzFKNRX|J z91A&G0)LorO2_q3QD(G)?X&Xl^aQl}I1^5*hr+DqV5HaWvBTopW(#&rSKzNdbsT0U zDVib^tPNp~p+&A{(UhQ;wf5o6VilQyb-3=x3@fNCQS-=JoLi~mFcRiC{rdSenKk)eE#GH931!HpYN+;GkVAx zWc2_fVfSPXc1@&p{b;J-@JWXkwqa9Az0J=&0j_GRhrpwWOi#{ID#}5VfefhG%j|>$ zV<|E_IkERe1Pk&cYSLjd*#vNqG!`X+g<&G>fn5w&P`2{l3M?dDQsO1b;cbhT+e(Jb zlO7AJVCjIcUP}ST&uMjCL_|UvGcMc8-DY-&_2)nxv2zUUkkRtn_TQGX+9AJX>{uC| z*H-NIHTV5k7^bu$9dty51c*c!_5JWSMMIeA2m%Xz&MDpJF@kc?I6z6;`Z#7OCuauv zjuU$qw1O9Qo(9ujdUh^l1SqCG$qAg|9U*mn1J6uCXI_X|V&`Fr_7i6QhA8DB4@&?L z#jxY`^fSUn$Y|X#$ZEu$#DkNW8mtEk?~%8#u<+6MzW2Qk!6hb7IAHkq|Nh@+Z+rIh z-dijdk0M|YSp)|(8>Y0+W5pDQMN0Nn=)>YhgL54evo7Z$pxHUu(#e0}^!Zg*#>7mR zXbPIatCbX$pM#4~+?Bl`eKTiR#2GGSGF)6<;YbKlwx<_6FpY|FcEaV=F3n7t>NQag ztY8HMGk)UmC=!F5{3tO~Uzk4zT>OxFi36* z&8}K&B8Py3{3XsTwb)mJRqolqOR(ounkpvz9hJyo0*dH_|ULfImwtF5TeQjT$y*UmRtWz4BcD$ZAqu^Vg z!YUJ}D?bDUW_^~vUSNs}(4>I=x*A~6P^A{a^Enxm@3-Klt8zg)XES@YBPPN=abkTG zG7>gmAV2C#NiWzW!wiPQ9Qwk2+dYSa$41Q#gKv|x4%^9?7y)$p{@C|BtU@LpYk!8{ zW9QWmx6OQn@3xFx+n~^Otgo+s{!jkoPd*Emm^|Tm7{2?vzxxMJ**(*jjv(`alwDTz zv=|ZzDI8k2fwBfp7Sk#`5ls<9ol`g?DSOU)(CNcwy`dckJJ2QPMKn>=>-8WaflUfX zx6|TD$In#}Ec96E(Yg}^L{_Rj&ew2cPYxA+mwgMU44Fpemp9n_KsV8tCJ2p_QYWbt zlrteIS2Bkf$4mO7<*5mD)S69BuUskRVFK9`cDh6`Fu%C26DqlELg`}tr{*{CMfVXL zsk{>uUP?rsNrQc8-y~d(Yz8Up0-8}i`_Kx4sMZCbZEQ2vJy%F^G{!i#iOxfQ2m}fk z3?YJDCTy~&9UJL8v3qnL1`fPmtzJo$ zkh5{G*WpiI01wBSP6thi%^pw6!h8%m2Mj2s70q}n68kUsE>-v@y`xe_(;e?LQ=^O{ zt)LHwK|+XjVr;M2236W)MHIw|7+?y^Z22^AgHT068M7u)^cs`TLFFbVCy&4-C{HNE z@Tw*GQnDP)?_QwU`h^)f{pNLKzmrt@i8YooNw zY&HO<*O^T~g|?2$`3%m6CrVjX^2R~(k<+X2xvxD8d#B5sBD#Zb9L<0qI@#cvJ}n~U zYo74#dr!lc&SrS=r9;yyjMJDs%qo_K!B8QdDa$mFLeoPFXZ;RG7F0QTlVTkjv{W2E zDCF-jj^Lq~%`l};j*`SuaA;*A2Xj+pW?Zs5jFOZ(T^p5KMyBF(Up)a=9iG9qWto*w zFGz^ww*nBfuat2Vi-P@Fp~a=r9@q|#B=PktXf26&7Ri$f>+H)xi&O0>xUhdJA4^Y7 zt67pc$l;}R=^m?vtU?vZbY_ay+#sI|37744;oOphQ!A4F93n58gdiJc@Dc{s1<5mW z7~F1e69A#v0|sAL``BTM+8$U2W+ppOvI1a>Sxxf!O!>zG9JUz46AD9g#(&|54g8i6 z|Dx-+?-M|Tp-4JjK*Bg}^~tdNwbQ;E=qEWuZAcg}Zwy?cRarpITlM>XxCVtO?pq0% zxQq+|HZu&Mb29jPn^Yiz%W3Cmo@~${>311O^!gnRt-S7N3K`v$0yX*t;G}CJY@^L6 z02sw4BkY1*cB}+jgjPcI`h)(WR&D=4+CB#M(GX)9BJ88}f8-Dd)8N}PehAowur7%0 zANCdqtDSa^9Mk6EM-E(RSf>Tr=sV?N_{?WM^Oy7U^FCZc@`M3~x4h*oxf`Ey(=Qb( zQ{OROE(l&10v~2tl!<1$Wq*$2+7!Bq#RpX)y8nD?U+k9U+QsUdWvc3sqk|2dh#}X^Ch6snkA2+I1R_>(aY3+zLwByrE{p8sd zIJdIN;NjH5GB0?gA~#f*@1ChJ8$x}+z5)8};{;a7kWhFi+5c(9&AF*D9TGYC819sD zpaH^!N>Z5SIRtLyd`_=k7p;T#D;6SrnaEL+`;wRY?y@7N*-uW7wMG~Fu*YD47>fH( z*3mrC#Ou#-xw9)xc=&9M?_mxgC2<87uP-vl39GpxS?N92b_|?rCuQT?VEbc}l)Q-TOZv0bhOiD2 zvGc{h3#;OA;D(_h#MWho9xH}lx3n)BED#6~0OSa54TAu|v4Dh4lbeu9j(z~mS_p_T zY+!(eRQVYMg{6}jqRpmV4m}4o*=ZpiaAX%UgOe#9d(1N@^tfx1$C>Xoa4^wOc3+xz zkRl~g9y|de5&_Dva`C51K-AO%Ls*RP-4MZfqD3{hiM*5M-b!oI1%oXU(eY^~uW$l8 zQ$zOr&p-94PqpC^k|zu>{QS@V{L6}^(l0KqtYt{C7V>#eshoSf;-Z;-k}{{viv?6b zXBIYLs*+)3EhISy7T96#QKawZ*XmG637(D9F{EJ0tRTo|ft~7#|#i$&!kgAh2+9 zsRomU4A;$zS?N_O^H~*rV4@(36j32b=~AldF*G$a;FI?)z(M?8A`=LMBqv}m2p|v$ zF_|toLITvaQ|8C1u_8xZcx;4Jp3g?i!+-qTCTDykKZ>0Ih;-UxQ_%eK8Y=X`#PAQC z-axh|#UYXN2rP(kAm0h9n|($kFn9|66le_zf&H6eV6w8=Ml(kmO*N??aTKM{rU8ab zHj|~ST4YC%?}o&CCE$w>;^1b|l*pJu5h9|z;)*qX#Rg!+k-n{rMC^b8B{oEX4-*kP z)*kTU5J|W zk!Rmz(KTQ}Y8FLW=Q8_DA0>?%iD8mL47~R{d0vxDIj9GkYinzN{-Fcl=j5xfe>Mvz7i(xvILUT?0t+;o zpCJ1>&a^-O@I1nq>W`u(;HUg7Bl;Y2i}DeG0Abr@?6}0=`p=hx&p?C)G2j8h z^(4$WO_?C-!kR79!z~$6Xg>vBsaJ5M#Oiui7={ zecwa2q{;21wip0J4w9u@S}Qt$eX|p6+8{FuwbM9D_N;5+MY8I3FwT_5`I5e0jx+G- zN}A81R>@Sedm0292#~&fatW2Kc@cK@cv5ngwI;S%4w}f+>XiWHQVAx<3IqNOTMZS* z7s+`O!B-}Uyh31>QVyaSy`TEGn0MfLPs_k9hogh20S-2RP}dlK24x^RtCO)$-sgv9 z;(#Kq4xm?dt}z0u(U5*5GL;j!C_4M^nRvB~oy>-R9FKBNvlL=JC9r0fOglBB+Z z*g1FjNJq*E*jamJ!f{}4kM)y??Hk&*_;U!+XVlLA=v&XWUBBHn1CC9lwsKd$y9yBW zJNg08UH|s4{{)P_o^{DU44$|ZqR3aK=pt*t~Du@El zr_^mwgMrV=mz0_3(M(o7_%&4Y%VjvZP@0o9sTDG- z4YI$hC;@7ZEBZh|1NpQvcTj`^IXLFC>Rf8YNuLuz(Qjj3pzERJQUqjH*Oh+*rBz;S z;dSE6Ou&Q65{nS}0QlG+O=crV3>Y#QBPT0)pwrp_gO05>I}94e5oqm0uoKJztkov% zY>u|@qwRMBQ-j%0vzqypD*Iv38W}G8T*w`<7j&LZr^oCHIael$ZBwUyr|1fbs-W5b z{%IFZocDoNCU@-e2?{Yv4-ST4M$Tn@NmEd*{GxGIkB|bJ?v+RJieOl+ilv zaI+`qQ4}i-h|Q z+qNZ!48T(Qc7i3V9fnCT<;=16N|h|ONy4g+5AplVuu8qL-$%gC6Al`m@ZIuhb=?j; z?dlTyO!3&}IDrY(m?#R zBp01wc-FI?^W)h}=4wi{+-^3YR8SOrqtOnOC9k)DaQXa7jeQapme#n_Ng+;+7uZ>m zgcRmPS_y3-L*pcHV2znkT7>6&smEtaQf1zLaN%=uMLCKb8y_;7OqDa}?aZT8`hKQYfHAWDbLap5BF zK(w_`xj8gD5b#pesSK{s{^UNQ_BDIle~7biFvIqsrRs-n2RJY-!r6?LIE5Uf+yy-l zTY31+u>(wmJ?R06&WQEC$bvu;A~Qp29}LzY4}(g}{uvB;N!=(Cwo6E6DDT>?DDZVRu^YaAT749>5TC@R))7GvvY&Czi1N5Ntma1K`C19d_SBLffTvLTNvX z@%>=cr$LGz^FfGQ$FS!wMgU5kjExtVLa?CP?*ypBankyI=%_#GFdI;-b;9s-UIn-Z zCK@4J9XCwAWYDcNTczEPl-*T=zs1uhEsYutqcjeKR(;zdGW%^vcv^)RM1GMPrQS7w zC}2CZ84Wd!1FP8YNL4mSik~C7J+b6Y4w|nIz$GRZm0@_xTi$Z?#v5<=L%i6W3Kdjf zffin(nYLLv+6b+1NDA%Vht42KC~z0EqO?L8>682t*uhcHmT6XcetDB)j%kKpud9Sh zD;rJDvrx#n?3_q9%_LwV4`E^x==;L*27K+IC63t0rB&pD38kbEOqHxjDeZcbl0->H zc-6VBRL%)n&P;}tC*IKB*1m4Zh23LSgaN|aRZ7@Axt z^)+RPEaY>%wnZDw1{Bz^vQdxB*q7HflrM#VAUuldAs>+xC#^!$!2BlMa~4fNC2FkC ziJ{^T=p$$Au(*L>sX~zr5+(``OdvqZklDc5dI%;4h2q0a7D!vr%E0>`Jh1{x*mkB0>y;jmUT>ta4U)vnwboqMs2aMZ|_itBtmDi_|&f_ke5zY_?MDk4GYHwC~Xc zo~u$U{OWM9-U4r+uH|05Uk2*)0QI3~{qY0GuFNPekX?ab#MM z%GpsJ-w#ga`)ZAA1t@yFR!zUxhN})2V4|W@)T{BrWPhahSL*M2=B^(iM<HPKtePN-*@1GD}bxBbqR}3Q^Z;HD(q1zS^JHc>`$8ggj!DB{Vyb z-vpUFj-OuQJ4J}P(dzJ;6qKZByp)5pOKn!rjYgfF@EC3Py9@$|ZJ_g$@?Kb8hqa9= zD`dKsYON0I>l+M4ItW7O8VT)ZJi&PeOh^-fi@xu`(t4j64f2`bFiKZBC=p0e{tP`{ z@X2*NR={-aD|G>1I@!kdcA?Sk@nTr?N9K-Jm%b}UPs5NU^C1U; zOB!Y;%0Xm?EtE`ZepO}7q%g`ua|LF6;tw{5O}5nisbL?~X%ViqEDQ~5;G{#Ia1cScmTs6*AU#|Boj;$Kik zC%@sJ2?Mn$XNHjYAZ{xddr{0tTdaXAGBL5vC*z7L_89>LGHjEKm$yX5z7LE;5+cfM z$6|q$i!B!d6b9}E{}@sR!v2jj7WTbNs$*!q3C~KLsj&MuzMVFk#AJ@%FXG_By8j~9 zl!ybcZ|#wWiBk*jsh#EwsxUI!$SB|#w5lQ|)EMCCzEF;k;dk>Xqt_1he)UJ!)9r~! zJ0aF?kT&ohe2C)AL%gQQdleV41&GiSD3kXg{(eNhglH((V3+p;vxd6x5WuKDj!ZaA zwE-9ze9j}!PwW~ZW${O;R5}b#LUP3mk&DGJh@bqapL*Wzxm~}EcKj3#^!Z#?ul(r1 ze3jCYb(t0?zX9r~uRO58>7mJ2k7m!5GMUzEDCNC?-ZzUdQTQS$bPqo%#d8j4cjUq1#c6L0?1k7Vf)m(Pf{CoZI72K}s7H4T{ z`r{?Av7O5s9R@wbLTsSPVrDE4(?tR&ZH};@WucdwXokq)^`*ikw9O~|jiD)lA`_@x zyQWGihlNO5R#ge5x;`+I4HP)Nz_biFDR)zHVPyk0unnh|S~#P~Ky|ameNO9V9Gb0w zC!B#7n$l&A=~UR1hXI6$w8IQKoT*Oe*IKGoENNk~RC$qvK1>dxGk~VHrqtPcNsfITKjf-&S2*dr7X8;L((kz@rd33 z@-ftF06nYTE}f43wLJd>E#28h7+incufCl+ur^En$*1G3%!i?msmxD9tUlD~bHPC?IT7rGSgC~Q1@oNUvcE|!CqY!i?Vie7Mp+!ID&=#l)#man zG~wJ*gFopW(Q(Rr0SXAv`Vy{WV3LCxoL#B&S`Y0GD&;bTN8+rx*5LdMbpPtDE}xTr z6Ih^ynrJ56LPo>I7p&9n@b5)hosvLFr^QO%D1HJj#3VF{vd8o9ozB9`Z_Xq0p?nfd zs%c|hA*nXI$W+j(mLAjgt2!58sP>fQ500Rb0KLEr{3-)~sK7(rzu@_lD=nED8v#&p>RE9?dGEMK8GK6vf^|h}ADK=t1$b>;JoXfcuRAVW3 zgD=$xOl5jzHA_zA>>wcVaYn7Sndk>cgvn9JsgL{_2plOhMVkF2*pESnNs??Lg+R`N zhBYuCV62eguuuXilscOiu|hsGms2?#%$g52(_p|0x1W+PE!R1zlbz8m{=gbXr1;90 zq{wL-MNQ<=Dhh;{5L#Gkm@_1qSI)1jLph&z!zX-sL#hDpnRcZr1op;z1# z3@QnmtIU@G&DuH+bjYmK$oC=Ut*yVu*u+RVqg;eNh31Qm%}Pb$m>1a=ryZl`w8~kZ za*J&+q+F6hv`-wlUi-Hg+@ExJ61Q37=#X2wV+CL{JLc17cebm?R+Iz4AnAA+-9ONt zXt9JJ(Q=7xij@;X&L3}sF9#J^fHxzk!W>|2H-N%2$%UXqnIAZzIx`6!#WWbDue6y~ zdi0wd936Pd)kR39RdO{)FG*>9pv*YpXj&~CE9gI~@s=j0L~LVak&JG{PM?@$5wU9Q zNG`KiL<-ouMD&5N+7hswnn@u4DFZwVSl}^4gqa>qb%sr#a?Y?&b1v%yEj0Xt3b7ZF z$yvhD5y4|nb#>qW7k}{==iw5O#|;>M=4XCp?xve=`lVu_aJc9Df>0ODbV*Ut8U~bm zflLY1C3YuoGWwz9d`XI#l#y9Y(u7yCb0Rt2X~1!QZ4;`E4o3x~^#WD2kJybS&ZL)CYmmhmKkX;R zg43#Ey9H;MRDh+1;D%mWGQrSsWU4TfpS`rUiJmZWV!jGz*Sbg(rc?qa?3c+(0XC8P zmxxv~dbLfeCzVdKeP1nUEtxqmOJnPt;}PW_r@5%y(rO*bCACboEy+$k(QU*6oH)OU z=8J4Bh3xPFf$lwpf-a(&WzTelnHY+0F~?9l=sGAwmD6fF>IH1JdhqB%jlm5uLU{xw zS+@hn7LZBm=(dZ&!6bO0xdY6ma3BfH687=Yu_yex)$jN|fGA}Op~8-X0I=I9qWeTb za|8?yBQ~u+0A`Z3~wv7g(se_v0y)Z~B@*h$daddlgMkL@9I+Ja|X zH=%s2=>2sd$gg73(z~wJ^FqXQv9!RbG(})?+*00lm+Ah2#O-33j*ONaa=bcRK$4$8 z7=~%pV&4xKTm;GqR_U~_UDqQ+F9?xnzYNDjb(~@wLQp8dWRr|WL4++D_VQwRv@ZD{ zdiT5EeH<9uB0oI1b8m(I(p>hMWgyMe+YmGfeUb0Uz^FQJ8@ zf-_K3h@=3?Sx-3us)vG<^xQ;|%^T#L*YAZX{_a1qj3$9LGZ=KUO^6aX?h!-LZnc8* zkwYON{j;kLSg)y6(ZnK5A@xpaoKu3pN*gvPF9d=E%AK&zPK-S?b!@Wzo*oK&mCPEl z9}gT~;qC2qm%+;M`6@eFE-kO~YL;{jbUlU*A@6_H9-q@sZ+6uTok0+-fZ1%oXC7(7 z-N%}|=oZP!W&^6#YT(%DShIgSiUf3@+aQHR`3C?T**k^+%mdprkrdK13}ed3X`gYf3U##An1(T9`mu3OVSPWPlKt zIF&CF_S*nrClYV7ecWSFveEt?RNUq|<5dU!4?}h)NKltO_?ugta9;)+YyAzo4oKc- z*URmYGO{n}{+P1hNBbrYLiuNa9^%nI_A@0kKnS*SQWeUACx(wueVbMTVyc( zCYvG1)LX*|lA_Ih3xJsG^aJysX-n+OKtFYQGR!3b@oN$QLnb6UdDLX&euWgj3i-6p z7lqhHNvk?!2)^t#faTvHqRE(K1kMTzJb=X#m=_NMFk#wD39{zTuW)1=uiC3YMG$?W zf!7XSHxI}6eQ*iM;{ptn0=iV1{I?IEt>w?I_aUFpu|hSoUXCK*nY%*56zWK{KFuzb zg6ru7NVL*n3ZHjWf_;N_U0!U8%@@TScecqzu&~A8`rbz`&3r0C?-=pIQ zXwWgFSjpZ-5fF4kDO+Q`NeNDrvm^O12(92Vp2|{4D??IymzP(WIiT-G=@aXKErav8 zk1|QRq6ayrh2Wvd$I^R8z6$htex)8baFU!~sUoe@gJ)fy;k*!L6-;`H<|=q>x5d66 ztN6g(b?+M7dt!y7Lx{y#K~}`hkZGJImH&driKD`VEFtn$Gl;Dp(N@Rj*y=b#K_-c- z_7$Lxv;B6IDB-ryAVht9da=gsv}LaB!e&#zXCCgtW~0Md9n)!LYwh+_REnM}=(Ymv zpUSDc8~8shaZb^s*B%BqJmZ62S0@mA0F!>0cGsSj#mh!!bTX5_+#dKN z$HvYR|CDm@cfg8>@Oyl~zECA^GYXGI;>RX2E=j+mL%OwZ8-;7iBmz6DJXY6iWs(pkI>Oc4O(m|1 zkHlT;8*OG>8EW|ET^CkP23@nIa1Hpj+bZ=o8oG%k9wwpR0c=rb@ON~ZYGa(a3wD8? z)tj+|62TeZ03uNE!9U|~07943ZKR2(jymzB>Fa3S;c0ESA~Xn7tA?I9w}Seq4(D-# zdh+}R>KPvbCrw?iw4TXr93OF5_YT4(B##R)+WkWPvuDOL$j-vh9#<>{1OOQ6jObkrE)#KuRVd4-$9n_dh95qfJTpiO}7K( zjDysAE;z50Y9>!ee(!7NRt}H6I1HLI zs+(YZGun8Kiz}OO`7GKQ59QP9lmnBF>sIR7|x^6cj08U7kQZ%@x}PSr_XaF2(3UVV$&na z(E0o;9b`@r;MF|7E|ob_1Z#q*!BkaBZO&ntn~qMf|HIrwmhaKjm;*1k5t*5s9xOF$ z5HJl8RVrrC39yA6qUc3*@V4tP$Hf&+T)Eq_oiZK<8n!#$nhXtI!d9d4kn?VJ$aai} z`T?Qx6Om2PW9;n_Ws^|9b^auFMkbVz`ZHJsGU?=7Tua1M726kY7sT5K(QC5H89zH5-y{!=NL!-3LixKuTm1pp~xyKg?8uD(CC% zz6{T0gCP?U5{wo2x3ankC(bOx@_H91n-&PMfJNXWDGX4M!GlN{+0C_c=#4q>m9b?^>qR#GA)pag3iNc0m{?RROXB| z1S#a;D5Rp&DO{1)tH3gh-=)uqnn>A`V$_clheYOc8Rc_9ZP{!w>p(sZ$yNZBqGM&^B@$JTOy0#i8zfNMBjY+wL2?dETb}8g08QI==q+LZ8){mg)8>u5j1!M zW(_L8j{QDUNx^j4(*v~N$QTgjOo9_U88Zd3DQ5Alh1C|n^f`)VsY*D$7^bvUJq`iQ zYqOxi_lQEU948?*3y#m0?P+m*+mPeJ##}-o2WR+T{obhvF{l7+r5&#~012Qo1Bm2y z*vi*FfrR00HM9Q2{W9SITjX#>X6m%B%bp1i?JoflW+3UDNxOsd$iZ!aZU)dP7Erkf z-71QJp!Zr&3Y4irvjoO$K`U5twnyb4NuiZfV;&qmR8eCNsay6Pf^gYueeq!ur?A!0 z_euo)RW3Q8Q>^S*k{lR!a8UFUY!<~aa7sCR^Y!-iZ+%fV<0`^!M)8wQdica?&f-gE zb7DkMO)%5!>M_qAYfKxQV9b^9^#>k+wbdnF&%^c9k^xj^{`@&kVpz;)2d+CgKSkJ2 zILvdk_tJ$bQ$0tJ&f`aiJn}J-%Hi;k=+II0e0VeW-!39M_ z&@*F~A$#bVrA^Kyp_Gdn5NZhEhzlZL76POv<{e;DEFTCF>VDXaGEn+L#5aYMKSb*s zsA7oril0`*_J6}fZeK)facoR~4*;rR!+oI4ZSV%eu~eXxMN~m2;V^rD(m72XN#~IX zN;y(aMnOl?<0PzlY(Nw*Nad_-%fV+7dl6X{+p60D1}UwRYGe|&!DnWHfdiFumkOw@+{o|EVYGzX01)OCY=SQ0^ z@9WS<)8jN#CaR43t4j+_kvF|Ixs!r-SV{Fb2LQ1ELMuI2n;ZHf#ZHQA#7N*_v|dGv zlzY3w%7&D8S07I|%0o0wxfVZd0D5imyqUL3pRq9oT z7Ec02-_c`8fPj4|q(Yty6g;KK=~~DfW0r#MV%ch~#m<)WFp?6d$v`%vau=usOe#SW zl{e-a0s)k3f)Y~|vKcmkkYg#Cg8G`BpyLR5c%B>aX)yPJ{;J3hoIKxz$#RPOyx#1= z{l{04$w;f0!wec)SqJQeq_oqkFgsB|VCKS}=>lB7w*b2)^6>DY56gHT$~o0$7D+Yu zh}eCofP)3hB0$9K zGj2Yhm7EUm4)Z1G@Jfz~=yP5M-mXK{`8b|zbyX%>w+Gi99NTe z76UE}!xFw*>ir{^q)JH)0oj8aWlYh=j)0__s`^MzLZoV(8qd_2MiZ>hfB7r0yt>A# z64#q`c8I5m39%=htHQK(mxS*kC}!gLsZ%h&FwgzhZrAigQw<=bAC^~F7@W}96pj}n zGI*?S)Zpy=q7GLTY<4!!D1kM~<3o9UW~QcssFY!1g9j|@AsG6-FMQz(O}GT)F#(2G zzVel~PEF7L3eG&elwR$SI*BtmrW{swMA6J|;0)VS)R&pnD~3Ql?8Z4VtwRQhre@nu zll%<&YBp|#Bz7pNlaK+!aB86n4=?$gig+UHGkv6#H?aRe78NR|A69r2>82j6R=b=I zncLE9vjXmGo1R)a+h4 zKaDeyLGt&V+<;O(2XkX8pTv5t#eGvjbH}bRii{9Y%(_^{fzLm%fXY9mUqWSzBqn3E z*5&&}J{%onUnr|*3ZJPonq{(R5}BJ4@X#rVAX5f|S)k;$QpA?B7YQ2C9tUi2FfeP4 zgMoEQZuA;&p7%jwAp8Rvp}@au8%~*;D}_ccAd49Q?jj z*F}~6;JIyuoODbQSg>dQ1AS!!3H@FAQD8Hm7UR!Myh;lID=R}^Z&zqTnIih^$fIV= zelVd>tZ1J>0!UZqXz1}86J*Z4>6&pI7jvMJq=n3a%EKV78M&TlfH7xEOGk!?hv@o= ziWvIt#rsrj@tzL!1K7`>2*Ddypd2~L$L*{4-V5iK7ID&)f;^6k#bTMw)zlX>d8Khb zjT0$0r-BT|8=ClaaQwgL{s*-)zM2@3`KHsVvwxcD_m$OU9EY+zPKOf*)jDd^^4dBq zuB<2@Eq&v9s9&fb6?@%*rKJU!oS5V>Jnp|0x%ZY}o6={`p8eyGfBfUia0$o-J3Zch z`|a7u>FFQDnQ*39EZ}Uu&tCmBc$1<`A*G+ziFamj_Dbrz*E0?}#%V7M|6`{c>R4Yn z8j{i_MYX(IXG+}Ht1J>qthMj~sJB_6(~R8{eOO;O3pvr^g`{YfPRxQ@k*5Tk}ICv27BR>&`O)82KB)~yFDAydAWFTW0S7S3h zxzvJtj;+CGzH%D=>FWzT(@A;icmi!rWJ0dr+rT=EH&S6R9^oLjH5E4eh6oI@wUg$JwzW3oMR`FG{y&g6;r4( zscF_mdqzF9S+Tx0AWjasu=qWToGbPj>=Ute2hV^N#usT+^s9L;%!Wu`*!4le^~!{6 zwVs0@9|J2ER{=5!VIlp{tA(c&Nbq&$M-Hh3kJCk~GGv-NL%K7@S{_Dy&_9gaSOEhYN=4>~&z69HBq=q1&eHBnw4{O zfey(eF%L5xzq+=;Z&2D%DV~izo)Z6z7abTcq~PpYi$nafp6+}+KOswc)akr*ABaNt z96VpEkF$EU^fkRGR9C&x;d@QafC?QVjgGT(U;71kE<=6ZPWxLr2LjE23Dc0hixNjs z%mn#W2!6u_M@|rPLMvMKd^FiK+EB=-td9!OLP+^(Kt+9F97YKw1Rl(w`i;;tn@fCP z$DD~uQPXb9gh8pP$>EXK?Z~7RYoYuol!FtAF-=h0ILjqRObGSj#O^tj`@7HG4;2Ju zd6VuLfXzK#%G0xUfZ54Xuzm4UK{;WtL;-MeUEJy#1}krev5~f%o^jK^dj|J6yDf+b`Rj zMO%7_Phc2HWtvcI12YXm1ilP|>_Rw+Fwf%nxL{B}Oa^kkZ2Lw;3bP_MV?Yoh$uG78 z4?glJPA*q*Ohof+DixSYx*eLd;dtzN{F@l-bMvS18a?equYk|W>N=b}bB-svDx%Mf zQOYE)fR)da&qlopt7x9svwJtQ=AoGcUF8$!?E$McfIRfUxrISE|_7MoSb}VsZ`vflx!P{s1W8RE8KyUhLnxYj}=2DsrON zn{B0xxm`$em$E8Vv|~coPD$@F7c350*q8&@+RUn@Sit?aUEkP(7A=R65d?=)Y9H&ku72?$X8+oAJUVEoQdtAdPu16 zGDPF!i~sPM8a#Tg!A?PRJgv-09>4~GnD9%w7c12koLjC1qnR1UC^y6WN*8Xv<`7gU z=GBtVKX8&MKN6~w0KoBcD+~^|5<2g(bL((+vC60G;qyS0UbWGKyB}46ZU62u@P*4j zju@cT%@zYw@&j@4IeE&_K7x*ZbVezogRP=-)pw&{%UkQ zVj}wh+bd{k%`6AD8Lb?v6LzTd!|RP5|9HfnnGe}*KLR@eDLZOcte>QHzAgJhLWJp% zv29^Hl~$Co>SXMmM}VYN7ti@EFkzS=u|?k}QS>5qURB1p0qci4NWMg4mthqh>w^25 zX+Xxo$tb1QLt1>Lt${@N(wK3;{250*lkFkwI~jZ$*1_kA;MOmOjCG#e>BDO-#X@>g zcMw%c%ZP2TDbfui!{coywak1kV$A3Op47I2#8*>@VI_gp+%ZWvwtSmO(8S{W|;mbB;Qwb69ERb9Ie!UiVip6LB;nH zitVe=OHer=vU(Ls^&&&!tky@M_FY)mds+rbnJy5@$TduB4pE{??t7U zi!cWy12YUgeWrV7LNO^qBaipIKxUWH3&|#Bd_Qt+dr^&Q&S`GLKHe z!C_J?P7MH1TUld+dy;hu@k5GC$R%6MBNF1AC>C$v>8J859y7SwN;p&oZ#}5 z3~vP)Y?`bpb|pNYZ=Bc}04UJVLK8zSSAf~+DGe;tjU%Sw9GXT5q;!x?XgBN3D41M1 zCgU#wi35B0!NmB4^{j+_5wA-y?3kwS$|LkW!+krDtS}{4Mn8| zX^;C1LWth3lMewo6ryrFwcLi?lSRmTJyv$hYYkSiqy)QYYnm))nQ>U(L>rWkOovp7 zJg7bjC8U%meGYMyYS!+ncv@m0Ivr(-PPy4)$fP-41yUh=EIhTW}vV>AvG7w6k3B^k} z0T7@F23oC>ymt@mo>kXMj0z=#YBaI$n%IXWSX%4C{2KN_p{t!a!xbzk9X9o|84T!? z*c^&{Fwbfsqar}a|8dWBI^k)MaP^^Gu>nPd(Ghk@Ik++#`A!(K6TPU^ZUUPGZu)a$ z?8tdwcL5ex95#E<{r0p2>v$!FzzN`KSM=fGa}wDRVUpPZQb4W0>GPTPgfC#|aSjo) zg`yl2#yb5rvoe6NKC@lg16oX}ZZ#y@W`QRHJVYjQuyduY4vzhPjP2X;IU|TGdRn{R zlj=_dBnBYZ%G*-FLFF?1s_L_L%E9^X5KBdP4s*VQHgCxYS;r-n;oSjnC0f20(DR&= zAxH%cihT&O66!6qUzojWqPD)-V5*)#f=UEOf1z`quS3xdWl#5k(-7!}I*y69(r3ZX zP8*URXqm(%2pIN;MBb@jBZMb{@ZF_J8?EKT=IJ2gtqm5HGN*EFWC^@fRx<(u=I77A z{OlYY+P_cv6HxfRqrismhfJ+f@@<}M67Zlgm&W8rk39_6UU?Zuz!_G7mI_~6S>Qe= zGf0>mOPPjjz|ad)_$T#`&Z-#!Th9JlZ@o4B*0;X(8y+cfK_-d5FTMwrI~l6s;O-)4 z0CfNV?EML}F3DXV2>v7UTkgK!w{NvCs?t^^l>`EWzygT|(+1kcbc3dSJTnJxW1pV! znM0o$dQQ8iJ?D(aJ@~Y{F>TS^*cc1&0$8*FBP`HLsYTW`)5$9_$z@=%e#;R=%ssKPx=r>G_neq;lV+C<4qY{rOkkiV zd?9EAi9{Q(o1Ux_2aj!`grG~RhXHK1djt}20%A}l6nhUS5msM}fY5EIN%v}Z(B%dJ zN8&i|#Oc~GMge+3pnGJ`A~+>Jn{W@2X47%L(s5|_t7$IM_LudjH!Dqj=;=n{k zDvx9HHCSl6=NxdX1}tGiFqG&kwjU@5rWvCQnLuD9jv9&TlN~fhbqjLSJzqO&J1#}P z;?Od_tJMvTD2Wm&j4PpF=-O(Bz#QvaGg7M@U!0^DgtjFpslm*hjRsm~Bf!bw0}tlp zrWvarta5J}0vf_`JH!Rr;<-}jX<@3uUsWk@bapf+>-#753LNGg6l;&zMu|8Tp0BEI z6_*^{^L5`pu0Nt-JmkYY!y7E7V!Wg8ei)+nuN!zQbvo$%A=vOIT2Og1%mq;-i*s@TM_D|sqJpFvP~2B7`5*KLt>0VM$V%oi`d zLaN+^0^ildFq{Dfr4{J2N}=xC^R1w69Ef4jwkB70PCaPX+x>wB=V zu>);)ZzqIsz`%+9f%9mXyh9Tg?E$*okFlw+$~|MSBA$B6RO(adr&8<54F%o5{&d635}jX4iqF$Qn8V5M>EKPgFQp zSKDxXz30!P1OX#lqV#7M#^B0U4{mMl7DBLUu@x*JdQE)xsp}LWlQM&fX9|w>#!ipy zUQm*gNs=8+RJA-m<&nT(fF3yjOY;qQ?*j=On*ncwkVoDU{x3>aWh+0AK$grAh?lMW zIwXXk-+E9g8&h%N9hSGpyyKRWAG1BaDZS^)(_8%+XV z$@+UEU{I+_xV17t>P5v;Zj8^+Zts!D0(!`;uCKy|Q%&GK$iBb<0t`aeadq}~w;UbX zCL>4ma_9@8=R87vVq7UfVAS!+$w~T(`!#`tt|Tk$b6NmO0e~KFC`F#02KCa=F$~Wf z<{yJUG<q#_}z)MdK^${a&m&e62?q;_0}fbcY4`9CtLYu7Q9FwMR3cD zP90;MB*&?AFp$%?yz?ZSIlcuiUcOCfreE3U!FE!IcTRWU!tywU(4LvBv3f`*KgG## zJ^sVzufP-UxB#b@#$j!n`>(D?Cl`6{o}8}1*KRqL#Z03}hh(#j_llQqcH#6w9j@PA zgS*en!Z%)Bhj-t!w2Wz4~S8TlARI^`u7m1>I- z?eTTCa9*$XxFQ-Wui}bs5s8_MydP`y2TLO@2%{Q;)MXNMsmKI@rg7P!mUWoTxRO9(vMk>Rq_Hwht%g$JrGpNr-#D z>(newAPwcL>6oz&i++NryPoDSH^snWKuMmAq-4zq4dcXUIB5dqfWZIUHRwCbIT=zS zzUbSJA|U+3qR|-9zz+dNKIrqZ6Q5zc2WG=WH3N#JdSnN{KF|Gu6<=9aL8NJ&L{Vzb zkfBJ=rfl!vce!cU>(*g%Vw$7_dmWyXz20?LT%2~-U`Xs9UDM2%izfRG_k43RGi=P~ zCKS2{S)J&aW_M(}k87JP2RJtAc{zAhM%@1V>0yr3>#m#Juaq>qdVcPR-Wg@@>rGxdTr;xaa|cF1p2;Nw|1z3r;Rg5JDbgW8MZr z>pE-$;|`!rPBiGft~nLO?8G>%Z*9TNtrpC?e&(+3T_=~^?~Ghd&mLQFo7EU?TI9wh zV%Dh%-kh#El@3asP>uBJ^?i7Hu(lk5AJ3ou37G@O%c%FT1&u zNqE~t2%&D~YTsbMd!lL4*GHmX2)QQwMk&AjCKwTpn84Az9f`0L}X1KypBnU(ID?L{MEJV+8Wa55EiNRcTd-0$`wDHFsi>^1( zT+&0Pc#l!{EHT|sF{H!&8_LLvn-&`<^5?*}gXd35|0mqh)niuBA-mSfYLJ$n`|zV@1rplRh=wqHQWL<`>k*yBDx zpW>hL+%EzL0&`9~+c*J#49bxJ!zVuRi4&*KocXr2j)8U(pJ@^&;|n)2VxWbE$pLvb zsE8XmWpZy6gq2I~;XD$xAR(%wb*ftkX^nFwpl&>uwAUEd096$lE;&zfw()TTIq3)! zPl^mC9GIXNV=*4WfFeht(IZI>Twl<10>3v>&I^xH2Lu)<_C`H9bc9s zc}p3H^m7rBW8x-G&+J~HBZf6<59kO^#?p{Rb0yaqZn z$?J9OtA!UX-2`M%KJdsrBypiI+HR3NcGnzS-{_F|{`UGd+;w~!Ubw+g6>FmQp=WV7 zxcA&LymDoYR8DuDS|AXD5*syYL>JdBr`~wzyvB(DvcteI&hv9KaB*da>{l>W2cG-D zeNB4*^%{enh1p5EFGgUCHx@L!L@89$&5QnE4>q?FxV7bF@|bqGg9 z{kG*}qq{ZCt=Q4^hX`eKutZjeh~q)TZ`A!w#r>;}#qErg6xaeuDbyeCOO%l@mdjW> z@?FQkClJ73yHK^sL)EjCY2iV2;~)AgqCQ~GpKYsDJ?lG(L@v*zf5yZsar<#=U5>U?P=(zLzZHRM*Jclu3O-MG1$Msu{(`iv)tAzOkwS ziSUk-a5PDRj7%j^fBNL9Kt#UZ_c0OpZy?@ooH2O({9P2YMy2C5G*~o}0a$LP*E3+~ z(w^NQS{UrUP}Oku^l2Is&lr2a&?hN38n-|E&_mID=a*Ns_tJ5Q>j2EnP5-e7-gFee zVBY!8cYe?G%R_z3?S& z?e*c(^;IW!pCWKE=!Zy>JxXfj001fmI;={msh=+9rl19>gfgRrg_$t1AQ5i&1_Y9v z9E9H{85w0{7trls#7I&NZ!x%r8SaocKj42s04&j`fQjEpX4$S)X8j~h-^krODoz1?zS=eTUi!_kw# zQH*qR0&zGNPra}LuUy-PkACn1EsQBQcI(@F@chMVPUpV>4_sJgP?ED4^ZoZt$(~{g z+yH`^2QD1*lVIPC{nuZ*4)1@*Sy~JT%D(h`8$NVT4KNN2E}pZ;7h%7b6Ib%YL+3~$ zgPspKYz@h&q0))hZf?OVH#gxOcb$N9%Ni=xP@Icf>wB~?Pamtpy=RWO_fY3#V-^V1 zVPY|oxF!f3YXqtuIopTlE>F69B%Jxv2tx%BEai=IY%t8pOVNI?Xf;~e28PrL`ZX50 zdyrs?Ti=kie(Cl~#}qh>!YPMtx)D7z00rV-Rkd(`IcO`ID--uZUn&T9`6kxF5YjNE zdqmX_CB}M@Ih+dlR#luId+q4G3vnZ$NcdvFBF67N6HvKtab15cKtXEYC0%;G%*irP zl3o|)D-kKxBhkJ!03Wo;_W@(a_JKiCDsA+sGib{6 zDiS{A)oc=;(U^d0$P=N5kz_}C>Bn!lbf$oks*8v4w!8wI>U-^`52LT@75qP!y(YHR z(wshO)QDgLrJa*bTDiJ*i`>imMza7z=~_-s*y(oQ{FyUE{CVD6>7ylA$zw~$os{7f zKR!9BF)((|K78Nzejh9?E*0LjDNP^-YGJ&r(7EYp0t`pm0{$q=p>Y^~dcul z|K-x+;scn!00(v5iJM=((SgND3sa2&oLg#;C|oW1QA&Vri^z>)&VsQ<3acC2q{3ih zzRX+x6JP)!4jzLkom`xvmx1;GO-D>88Yyv<6oUb|YKJ0sU7P5_hya5Ug}8~c=l}*1 zF`QgzP=}+c5(fMaxmKKTtajko4SmwP)#*4I+o8KCsvyGdjjm zndllPX-Fo}5bsi_b?YZ@zV!3|*Vg8<&-M0RX~pAs<;n}|S1!N!o0~2BFZ+FC97=tU zVH0PA1ROyP5@K@SOky|6I^~VP^wzV}+$p!z_=dY34t==g~QjDd5(gJkFq+nXWo5uW@d$%=v`Pi zlBD6F_J+D*M;|X6^?E>9k|qsbJPZKJV9Z2w!%HH{;Lt{Opm;-D26$0B!G?m3YFIS^ z0J45rDj@a?#CEgzm9tc;ErT*`4rce;bRLbb-}%rnBCwp^I*8%Pb=jx4mUs+!6@Dh6 zU#5W6oJ6^4oaA(r4ZZQ>K6J*9Ukpu4(>h*k(jKmPG-`k0r5A}H<1tTvB+NAj2@KMK zEE|Yd8V23#z?su$;QaaXu(`PjSFT=--eVrQRkAuky&TX8@}uAN-EjQ)vBJFK`IOL} z`}EhoMiK?~km!=J z6)*xNS|ScCpy&?8-YC{f(g_Ut{KZ~t|8L!V<$w7NI0ON9_t&?kCzIc`jk~TnvDWuA zYIzOgRiK2RNnw~UR$BpcVcPEXQ;$Ud-~W6+7k67G>#~L&j*ewzl z$9SVb@8+g^x0yRX1w&^XOBMsSIAYRwKZwQRmfu!EZ%nA|D-#i>W$cKmvqC`$+fk5F1rO@;@fv<^G8gY|xzg~yH;7Iq23BIZyhQ=WRY=!d?xUuEW=pLo& zL$4^VSNTrmGp?(^B9?S{uLi4pZSKJVP}~}nDkK~lIv)tr)RW(=pR-v9FlNBJA6}+C z4H>gI#WiK%l}>UdSziB%h;vLdfn%VI%>DdK0@Wzb9{O&LDUROIw2nn~GA;wR=3f-R zph>hcF{;nvKd~cV+XV&WaT-dj~gF#eZS?} z{?xDk`mgulEi8wsBpi`?-_p|3qGtc12!{0p?m9U|F92hRk5Ou7CQg6&{4Gbc#^C(P zIk>r-!b!~5J*@|!Oi_az>pZWyyw{DDU09`Oi=X2 z8j0gD3K$9~$iBiR1Qsly6eP8SEO{x*I)8WN;y?Wy9C0gq<&{^u`Pu*Kp8jrU49v4I zZ(FcxfkI&S`oV>13^=gUhU279ASX?3mOXN*e8)YfNOGgu0_nw9ma{y~=H=t9;$e|5bm>H)z zaccvmBW&D?>B%uk78)OQvM%Wp-DAVKlPR2D$l%r6bpo`s;382f-Bj%wOduwnSlBXw z=Qd(&;JHl;1?fcPs)tftZL+dbk+{Wu8xng%1s4nDUJu{j_oJfCQGGtF+tn9G9gThu zZHjWr9tIRfNKv$(qe?+lgmvYBjXNN14uKa4(|kFI^K z%ch1(pi}9;1r0LdZ5e3o)#SY;^gYidFAqZp&$}Z*!?-e2Q_NgZG+S#S*fJ!3)+-^W zb}a~KDE1c%OJR#@yrJhDQK}6ds~fRV4crH3XJ%o4WtWW+)0*e>W9dw+j^Nlg?25iH zH&6AKmzSIrbRIUhHc1tu(iSz&_ns9_y6^)Z|9-dUjq`gz33Hkhz#syG4}HrAq1$f5 zGv9ayCMRd%i6@?b6AqXlK=VK$8m8ii6(_roLTg~zNkecmZNXbu4g?Gz{pd$07Z(@5 z(-9DS1#o+-1IOloGlNNzsBsjr7+#ER(qnN-WRy4@pLQbR^?k>IJMoXZFcWdn?6`rs zbF&m-5yk404W5{YZucB#X%g#*!z4wKBtsSqrsQ4#zCZ*RXe@_DdlQaWHN?}wh+ZBg z6%8V;EV;qSgH4d9M9zFCw$b%rr7dx%_+Bs|5Q@4<(qIQ9UPsqSHV|TKVw@bL-N4}1 zy~dSJd;727e)W?_1`Ajg?)@7x^~TJPuC>P8bKMR1WiP{g-f|Om7v^Rr$)ls+uM^=u zHt+QE*2scL$v_9~GIsa6FtI#Gp`x>$JqmTIzyJy8^|gJc2HJxM?>go{1QTr)vmy8+ zqe(aMXD91^665uxn~Ms%oj!kgje=mMu0IP7q@jAKQLEG9KotXq0KeU2s6%U~3(3r|*b)sh@wrijaqie8oLa#ChNzJUhNj1Bz1TL>y@Af9%O;n#u8|3zR9x(^%A}4hV#3SAuZBa5&02`)A z$jvD50HaHp@Wdq+dmlE@%mHm51t?T~AAJFg#!9yEF$7{_iW3u`gYH%MZVtF^h&bEI zep)yH!$p0I>+88PxYN3YF@%GQtpE{r2o!0elb|uzyQx~mBpfRSvK})~QGN+D^5#Hv z16lyBc1}5PmX~HZ31FIt^de#KKyID<8$Z}4h(|B10UO>S0PUM7&8^kSeYb}`f8}xi zPiQ9=|6{?u3e!bjzWS;=Rxj8{5ko|wu|q5dl^US5;G|O#;I&pl_&;#}1MqvF`#q@V zs`}&vQfjn_YK60B&N`{Y17h=`64;0jG3-4tGd%_0{oUUU?|kRGs85Rva}GF+lkJ!h ziD`8LE?YT1EWxQCo1UJ2%kxPb*nu;y%*@Vy(4i~GNoQthc8rLzaJTN9I}A=TMC(FSXd+T@6sAD#h1+b9boX1g{?pAXzxW66I=}*!nN#oi zn3G$a&wJajIhcayIw#@o_6uYSfX0ChGTd~az)5J**^~fUPa=fnZsL+>g=^>ft#vo1 z8C*EE%xS^hYQ^l9luyHP7x$iBBneO)Sc^FNUXSXLmr9*usN+IX34^K^@HmE|e&@X> z;eYzA=is-W--I9g;2Da;;1{hEX>M+};Oy~fXhc|Gai$K_lN?Y6+d4hhgcCEIXCyW( z)N=RfCHlle2m)2@p?x2tyc;#%;NBoZ;`IH&{;2`Z%whn2&C%R$#1 ztug?Zqn#eLHJTBG)f3$U8((Z#35tOV&`=`uxwXSUp#%)$?*#)$yJ|2s?`Fr<#k~W1Px(L1Or7AFQ$0S+2W+b z^>ymhDW^{0eJc@TcDBIkSh_#zp*d40AcMo&C) z3hqC@NarV=!~nT_tb@@YF-X%%r-LV}-lPG~T-m2gkBD;7r4mKjIN$^t9FgC3x{Td! zPAY=M`B}csjq3)6Vcuy|V5T~zIjuEl@?thik_O<*wx$eHIh6aP#+Yg}Wd7Z)fX`>AyPYY#Us-*`NdK zdrf%x=1I75bKOZ~wqavqn=_lWI*x)2`4VtQz4WZa|_LZw^aQVh6*^~Uacb|p% z2?yG0MRf&U^KJ(c>!rk0xNyem#gx{yG-QLj;lNHDa$3>44kuY&Q$)IilXFSl5sI}T zp}@&LBSqB)8g)ALjpMFzz@Sup$~}=7wjMV^VMNu-pHcKjUt%I?7m+Z9hT1@1Qx0y!G^Uz z9C5ZFWzcJ6ZDQY+mzQC7dX|DB;a=A5a#UO&UD5ra+c`=IP)gltGsAWN{r5wh;(ppm zIN$g96XapHzrW9QJ8cIp_F-x11pN6Q_>W<3cA7y6n9!~0c~shiOc6?7Q94eUgKL~* z&_DoQs+RCqoA_Z(VF0kIW0)u0y-&egTn+>b_uYHpgU6PaapsZVJnDD$+g$<#=!cJ- z9Tr4xOS2O&g_#bMgwjUifV5m46lG&5X)-9uSTuy2{?gnO>7eJP$7uKmy*6B0&*17> z#}WOycsu}c&JE&%4P}FzY9_+{CR7I#gP=Zr-HGL~-~>rX`jOI$O-Xc=lGMSF;bo1i z@{wyN{U1shYE4=ctmg`BVV3bC633uTffBLDrHC!oqrnqb8-Qt*>Xwos9$he?Twb3V^<=h+ov>md#x!xkl&^;$+ z;V*v61vn--EVQUUwlPN~8(dBvTkt7q_1c@On<5Qh9VC8kyJ!CVb2s6}+72xzlultt zasmVx=*xZXw)^Bw(R3=M=2!ynx-W&i_7sRMF5VDmbD~_`&Lza&n1J)=?}oM`@GK2r&jijc$sP(CULXH3NYlr@ z=i_vp7Lqc!h5g7Q?|>)X{~=-)dt|8Jfb(~~1HSD;A0dN9bfr%OF)H!5Q2mS)lrH61 zJCqN|81X&{Pg3MN%HuNh-AqhOoP)Qxj8sWH@x&7|sTn+O2N(dO>5n%W9Gq!xW*T0) zbPG-`PZRNtxnBxx>Svs4@ zb&rz)C);hHfoI=w+}!tD>pP^zIkqqZJLp;9#$#r@Mxwl?16do}dk(M}Xm@gW<;peq zw)b4%cad`#ZZuq_ES!RbCz~u?zzwGCUK4X1aefNS&4Hh&+Ng4E!$NMh4glhuN}}Uf z=k|6FE^in(vz!r7q7=4nfAFXG4(1PD{!Hv$UB4$wSp+wVkN!4a2%p@ z!-AeNx(nw z{>Na(F=8*g^eW8GF2I>nC*aKK6QurB&ou=@vHHYDQUffhl}L{!GF${0Vy=pFqxj3O zeQA4PVd-9Yi_1vB@X$jKJ>2dk=eJuH=I1$w0}dD(Gh%QZB19=*vJzLF=8e* zMaX?)01`}DjQ|G4)|f+rH-;Kw^oxUj=l{F6@~KsLBjwaze$dG{KGy5*QY?6ju7Vjy zB%>rR4xC(GorC4MwgU?rWb`@hChz7RhKNpKVP=eUW31C}&{$7mc`H151njyo zW^a?lxpB}mt2ed=aBXJ;zW32(2?~b^ux`P`-DlzY%1xM^VZ`lSDP1MaX!+{(+l0FB zIkQ9rT_poZz|nSP*#Q!amWkZIUaxglxAtJisk0tFw@jex>aAUJ$BkVyJID>VIe=-o zv8Z)21*6JCTzkqT;lN}s$IPf>bnVqoEw6n&V~Nl@+sf%a-b$CJ-dkkSqb>W z@2mWdWBOeMi0r6$X^Q})&><@X9909XM+Kju60Dq8wil%j67kWikj1E(zGp=ATl*|> zpPXz44&U;+GooH(qGQk`MK$c} z8vJ8X+=G&X+goiBi?Q+I@*ERCav?N8B52&SCnm;e(?RtGCzZ0IQeaUPCPd-4laRkI z>b*PtjIvyk8L%-Fi-FgPAx{MS#Aietg^?X8!XqYb!4S~7Yp2`#f84(KPd^23@P*Bu z|Ng%Wrtug1-F<@#KO3|`lgLo9C=J1&SC}hdpIirL9oLLuwi5yg95Pz{rh|KAE=1a) z(D_+VgB^}W#uIhUNnxfN7MeA-h(?LTf@>R-TcPIzZZxyxlF74FRU23|mIC$%3mYR2 zVBP6KH=W7@14@1E;;xebHRwIY(uNr)`I(y@J*Js=4Fr;)#WHeC}B= zlO-D|W~7F~zRXV4;pXN(*-xO+ErQDzuCbr_-u{jQRpYR-(Q&|%LyDz1zE;(CSOP%H)FI%0>3=}Msw zrF!N2!U5-BZ!uMz@nQY2hxFkNx56QjH&pbcG+wsq0$b7NQbX2OkEx!EfnFJO9QK?Z zJQxiN9oZE~!l{B$WS#{OfqM!WZ4bzMjZ_l|yeUpYodP)JSr6WE|FR=OW1=Hxv3UsG z#PMJlrz8dLxWF+-4~1zf7UvTft(`pB0J7qOp*5$^t3=pR4hzS$&Mm-;FI|M4oh_0k z#3`!l%?Wt)k;mX8AO5goD5vPT5DcV|-4#D4KO2t0l-su^r^YC4bweb$91BN`skJ^p zVG^iod0F)k2ZA%kj97TZ6;vKax2JD8^~x_j`Q(#tSuBQ;fZ_Xp;0OON3k&n_IeXlR zdL8G-7N8QL$_*NNG~n}KK7(6ZJs7VE617&W*?|o6FlGaCaP^dpAn`n=2gS6;m^=yz z8|EO$Gd9daLphW}q@u^}I*~T26aYAG01-=rDW&^N+#Hd=wzA8)7it_gVL(|Z`(y`@ zkl_z!V?%>Wit3ngY;qRi1A9OaIOeurze}776sXAh|!}Ovr=*} zcmJjU0RU=z}NJN_3APafshZ@gAcut^!Q) zcojIii^l3LIDdMATq`*~0Ca}4%5TGFE!#=3>M~%bq$}7$H*52OU zKmU#2_>Fydi%KP6c=+Lmr{4Y86F+tK^oiqW2cQ8ba5L8245%Z*FwH&3y}z{X5Lts0 zd+fJ5G|;MFLs9i^JA<#kwhK$sDH)-lG33r}o1y|5VN;MBCSO^J?~seakO4{>#NaYf zII&euI&us%j4u`?6>X>AM`FgaDRpX#KD})0$EKS|0-WiH@+*Yn{Z>JMG2cyiLoccu|%7+enDNjz7DrG+O*kBVZtvr=ICXC3-A1K%ut%qGxXiFEX_|+dhn?U z*7s3djDF_BIxkGu;GO4caAqlS&&4XN?T!O8hK+% zpD__Hfdqu z-)7829mZwHlAi-_cJTG_Z{Ls7c^mf+4y0{`JC{M1KDL4YQ@WrEfQ=B$LqHQ+1R#h6 zHnw3PF}%$segcTW;y|R0eIdFcX~2MXZm&GKl)~B5izNL^h2u`W1k(gg+E^i!5J?(h z03AAs5(e*D=?CQ~k_A(G*ZN$g;?*Kv#sC~hkE8J(J9Zpub*DlYn}LPJlW^CCyW#xV zogC4~1g} zwircmRoCglz;VsDxA(y@cC?B2-NZ!;{cGz^{N+>=H`ZIkiQ)@KK*9M7LRiwZjcpc* z)i~oAMiVq%90x>ri11T*&)iEIkf?9%vfB)D$F-ERNg^o79GDs8obDB`N03pMjGL^E zu$-ovKnN?Y(ZI3S?a@84t`qIq?qL7FTEF}+e*@ld`EYIF+TV{;RrMm;Dg*muH)8@Jcs^s#wZ zb?SlNfBFi61a!4Usl~KY8{j)fj}DwGiOh72iE-oh4$RMti|WL2AaivxKHb=9!>p5< zY_;+t;H%`^?%>Ljq> zR4$lU481#Q4m6@_4AkSw?^$Ts=@Ha$9EmtnP4{{y}O}VT_j~Xf( zNKIjLl{gs5A;CtFlt&`#00?2l?Ym|dxjAW`05`8z$I{?=y&T}MGw zg2!j46Gxci$qz>YU)pYLA5r*L!XRELVT%g@!_hCy&B5{IB{*?>363q#!<^gC&}b2v zA)OC@eX!d#pOQ3XaKe(BuM@H~XDUPI_DQ@nS@a}LjE^b?0q z{oK#}+zoh(N~O```Sa)BGcnd2L-FsW+ilor_2IF5mnksP&e|4i?YYP9G~wYhH8NJa z=a}R0lX1t{j?w0iNDrg(VPc@I-90#eVb&3fF^=;M%;loRDh>z&06gAopeF|Fqz07X zt53#is-q{j9Vfj&gGPO!eX;UwyM9iO)d@VH?>_k7|2C5dEx}}pj{oNF+D{wxoZMG>V1iDSu9Fc5y zI3dk5m$u>lvmA{P)eZPA@!7Ahw&D2VxZ6`2aCHM!8C@8hVBc;WTLb_Xj@97w!bw`G zDADOS-QcO^8TfZ!xdnOBfj|HLWl3sfy)i3!MmTlUxs!8#L9A}=IElx!Qz?uyljQ0j zo77CF{2K$wpz$dpbZX08*K=d`o(E1t_UaZa&1LY+#Wp18XF;>{HQjUGJvpGDSGXK! zo%EuQsjrh9E}XFN>aAS78^lEo#h*5a3G>)P0>IlQ6b!;k#49GyHX;x)p($S%w-uKm zVp1*@M5T%-R~37d_6}&vR@|==D42smOC{kPQIz->BmmZqyq@lB;7BFzV|>4ZepdmK z*hazI43^rkrS^s-5^?*5YeP+47sj_~Ab3h>YaCd_QeGbp!Oy^$W62mZAT=b`OE6Ck zxnkz9v)8BnH%-|lDbH&3-CE;;irN`5bA`}@$)5*q{vjBQbj#ZH5%0S^o+Dph)dY;j zObM^L*<-}Vg@tLd&EUN#6)=fyh&J(==$PI>GRN_R_ zlLQ>NWYp%qlNemQx&aqX%@Y@jp`1~AusFlJ%ieyMC$_jjqTZd11zlYf*CRpg4FJ?L zUv=>{n(Un52yGq^)bt&Q=qzQNprRsJ~q9Nk(g+5HViU09;pW=v^ z?k#`+`;U=6v)^Z<)6K1Is5i#o$yfH^I~}Kw-YAm|ckTQbZ4#LA6E_>Ii&oc@jS0HX zl0)n7fAAFLc=*nD9e2`#0ZcR`Ig||%92Gd!Yr(K+d5-N~)PR*2Oz^C@d~*}dotTHf z#qc7=Ifmfz1NX4L_1O3{%_(eoVQz}NC63K-Xjn`WKJB=PZO1`Pj@RMc_hsG}eJ zK3aYMuIz~OaI|L~H6{lEg~P5N1(xjVy{2?-2n+@$+aJ$u@plzq(s?5EDQZ_#r4Yi~ z2TnSVZWtLu61U8%SQf7faZ3`}d+|k~A>=Myk9+LuCI@?KHaRWyz=4J|3+vH`>4`Y{ zFv%)VGa$p3Lm|9R6=IHy4S??`bIxBBeUhXZ*ytNo+xTp_Kj^zb<|7daL7K#Xi(+2O zIO#gpHg#^8%0*@5*=($b8tS7t;(9E)tNQ(;bg^T4Q}=wQ;VmqcfT7#%JW;QY8T9=> zxjg5sLHF8yw+T-;J#ou%?;R%+Zn}+_H|E{_HW}2ZLCK|+UAM_+aNZFBjKHp zKIrZy*`O=_)X2eexK_2eHXfBQpmZnbGWh{t zy|(6bsk4;9a%pao7WH1M>qLkRis*@#?(OcAcy-(XKYZ`=)0~v6pL6(Hw5LGH3vFs> z(CPY$;m9$o<;2(k?pkWV)0dq%G8vHMNVy}N{OBEnm?+h_I4Kl3T-q0-xC|aNsA$nd z`=^aK1dwSQaT;+sOzLo0sUmh|X6N;^`FbP(g?rlSz0!{8)6wp28NdO+QM)t5{m1u* zVK7yA$0Pa~O9l?QFSVbCo8|U^zK=K|#m(~$SkCb3dp)+!a;$om@vmnu96WOY*UlSP9#rMwI5d_l~ zo|>+VW37)Ev^YMC7IhKFLp~53dh_LK^A_fY22V;<{opx-006@J6(C`tu!req92s*6 zG*~-SZ}|C3+{Gb9AH800J^*iF8H)6X0h{LLX75I>yjEj-fPsW)zO}wb&xYZbQBRKU zlpVKg-&)&bDMO;6g;Th2e8O>*O{c5gXKQ|wQgR_g%NV5(i8!YjQr z2SCuk68-IwkYl7ne9;))5Cau)&=H}i3^UDLzP>?`A2)Vd1aML@G{x+ZxPUHRzDb6lnMi3U z+ghjNZAK<1#)&{bcwvcMV%=Cyjn#;FzVO;@Se%`L6Amn5S#sR_nPXFMb!7vddf|ox zT74SZUw>*BR=4{^#4q36hky6^E3me`&yQq`mpZ7#Ar9z-$A0h0%doQ1f@fa2Nd}T= z*MfCkcy$F{xx5CPZ8WDSlpDKk&pF{S3KcCzN%*`qQAc2u%-623(KF6W)!lg1;qEiI z$+4}3grMPV)(Q}a2zq3M&aWkhf|xzp6N&^TCg8HELR9y#O(AY?sBH+>+j6^duE?Pd z*FEUIT13-#q-#boRzpPh(00N@s1e^$2=zhzDj(Cv%Fi1AE=Qs)zuUOPt@< zq^=t9$V)_XE9U~Rp?~x6Alj>L*Pks8R(U)C1fWC%6W4*bo#Augi9x^u8)d3JM!goi z8#H@wl4?q6TaX~e_6xx?1pI#E&l^9;P!+=3L?FZ#yXdbAKdKf`Xmf}e5Kd)oZss0% z3(F8-ICt*c-7_;YQy9{@Cc$;u7$g9PKzP60B!bR>f*Qo5rDY$gq!sh0dlf-bWbVT+wlTjE{ z&bh&P-gov;nlnKv9SZyGq&wSAU870E6p2%~X!owx-T1DovepGb;<3e98fzx{8nfQQ zx4-v1yzikUjwI)}l6Ri1lOZJ<8b0yxJSDyLZK$%qTRjzcDD-OHSP~PM|O# z_;V*0V19-TFHx#7(HwWGibc?r<*|D$jty=Em^TEaF6gqm;I73G+6XkRZ#s3w0>JS( z6%&iTH)lgrJcxE-dF;JbA{u@hw?V=H{#Xv7dzgxQ*{Cjz zcoroJHhKVxKr4F#Ne@KirNopzNx`0gPt7Xsm&PaK#`*&S5~TJ_nz9~q$8>ZB*yp6K z^RAkfgE+C*g{IV2156v9sYLM6XgbTo>>m!E#lNe8sf7cNRxsu2wctG)jPc+A?J;WJ zH8PPf&FocX^qS~x1%pHd3wpoEtHIg=-M7)hcmQmnaIN)|r1(U|A*dXXp)xi$e)1Ea z_{12zMP*1OarW%_M;y^GoQEMH4+4zhh*~1FG)3Z%Nu+R$FmD6Cj1!A9WDJKd1CxJ{ zh)4)?CP8=x{f^&Ut%PKv>Hr5U}P+<9=jRWyz{7S#E_c5TzGq%O=&aa1?5ArVOf{>H#ui?zCs)HyfRpt0z= z_Aw0g%+!=5yh4}9WuMtH@9w&KV`MXOd|?tk{-FoJ4LeD6szh-yV8Flk-oF69{`pI= z-_89Z#d2(M8Z?JJ0s-`rn03;Egxq5XJ`8ruE$lj=fbbTLo5n0|TmYNfd+s`?1{+V{ z^imEtH!+q&f-)%DOxcB2^pzI87#u)10`Qy%L^&wRDh6-60pML6J-~?3LyR24o?0BE z`2H1D;`63-+;jbQ#68M@;;7X5kb3zM+Og4Q={HdTKW6dp9KEgK-&K;2_&wMW*A%{w z0A5u@KbBN@Kxsy|4`^;t%d@fF4blZ~9udGh_n#%OHcdQH)G=kDo$=3vQeXT_8wV4>gTUqCfY%nrnZq*k zc;P4Kj}ZoiQ*C-+U}Cg8Vtp9>x9qPhTpk}Os#^l6F%@gJY8BgxhbwokRKrqA@8gS$ zi&O4r2i}5GkT5*<*kg5*rjH_*kD_hlKsnZUO7;s&GYtX{O4wexvcfR{NIY!F6%qsa zPzdRivkOw-A9p>6G)_p8&=%2nrU+Kv{+N?CV6Z0+(cB~H0je3$m2lF5m1)c=USp1% z>o+(n80E4^#IP}ukkHJ%<7?e%Tgp(0ZjxxjfDCY+tD##;zyb~N$R*H87Y2jQ=Xbk% z{{il(eAwZa=^x7aok?7vXt%)gO(iuoTCmDJVDP9!($8{2nYsXrL?jaK9rP#Qr7J01 zzqv|43~fWu5OpAt%_s;~yG<^p=t+U^52ZeLpPYk7E-aIJ2d`^)+H_vCSfZ3iIS0-P znZOsGxkQGKY+cQhHbIF*Q!kOc%_%1hxhEs;$5_r&LS8a?ke;Y+7E4TP{n9oiVpFlHndrQY>P4#3aD9o;@R#AX z%EgX2R^Iz!!en4{`v-38Bc2nA%^%!`-p4>B0kI>;@Qs$Zy()rZ?J$VYzfspzJy+p* z?eOyx3Af?%#LD&=XaK81nHZ+T#O!Qs zgR1_}mMoJsq48OW0846DTQInc$J7=Eq}Kw2&@DmQI)Rf%w)Xvl z-o909T$;p^9+g0Ygk$BvR4IuS+2u*1B%(`DQj_ePhCsvh?IqY)-=hVO#FG-M5Rv6S zpITf#@g1ORX-&9u@_KM0$&Eb*gSS@KoEX1F*Xk$Pt8BMmuhXXhNcets_S&$$y-R>( zAnknXV-LXM+ziCVjTkrK@{QYMuY%>)`Yv3(z0H$N#DE*FZ3J})2o~lh;mnD7pK0^z z&0A!zg2jk~ODxPd z(BOn1!hH;Av&6zKWW3M^yyr3zkk35Wlli|mA}`pXuiSSSCM2Fwk?%3l_M7iPgv%D& zs8X3|-{TI%;`yQb!Xca7@IEnB zC?OcC7u~n~u6?zG$U=a_Nvf$rxQC!= zWe|Da3=t_+imS8>lWr0h!9e|g{H&|2o{D!$TH$~-DdB&L?oQ(F2^EvCKei>fwucBm*|tm zOD9kwfku;?yB&&HSWoLdwgdH90v*-U2EC+|k_bq|G}61FPXYDu_I~&I&%hm)hMD-d zyUqP>5gQT@8lAWxI<%AnhY&#Ed=-h>E{H*7B3=|U{wcc)Wwr^g++2Y54F{}Hnm`hU z%yR_#srC%2f>45v$8fViLW*PX%Jnt)#;e!8K_YDu?(e(bb2l_=?6H8yzVgBAEseGsW8!36y>}(K!gjE+#?-s)<$= zfU)Ji#3w3F+R|}bK|p;y&@I=>|F)8bjv^nU2y3CAF#POgDNmW^6hKM|JZKv+sED`m zm_sdtCKG{%!u@QqO|4&kG-m8$;w-LzbX^{GnsY{w+CeyAo|8hrBvBRRn*f$J=*4V9 z)o1CY+fQ&0-rn1FYR5K&wDvi%UJ9OjZE%*Rt71~Y+|ukTW5UMw6~hb`}O(t{fcZ;TQEOqYztW+*ZE z+LboEc5{nD2;bgx6S8mM(#>6`tL}@`0L9`10st5}5~DnJ#b^zE==mkEAtM40ltg$@ zv?C^Xj6FCKHkdOUtxy3q#vQ;uB@)bo4C92m#T3O<7c?}qrF7hq@XFd=>oZ%Ip1vdL z!rX;V%$cO|eR;p5aZVWNB4|nKly;UR2?QJ%D6o`)j}vg9b9Dv~B$24`b?*4u{w%zD ztphJzLupKp(uY$Hg`Ir@5WMI)*%n5B1R=O+kAOBENSra{HLi*?CzjyU@;vA9aFQP! zr&q77!o*me0&ijaFI?GxwY>x`uk6xha_RaCd8Vi#BNoaEhrlFPZbl?gyDg`>S_Acz zz;}r8-Y;F-AfSU~bz`4wRIaaXQ21^fFT7UUXtlfW%*9*q>}$6P2(i0XMuDkrItj)f zytqSL9tQ7PoMDhSHD0HAf?=w00~>e1;obKnPBn##P`EeoTFHgFP_#|i^_5wR2@u5J zV*(JLi$3YQ878b{BBez$VcM#_rs}w@00IZ>Nh5(l#dAQZ%~9>za(e)gy?}rPKfc4) z*%6HzGfZd?0x9t^KPDCmT)!V%JN$fc`?hpEo&yGo$KvlX@vs%|t?J&!R6Ua|R)`)- z)sz70F@YySJ_*feptez-;6?S$nu6bzRi_b9ptGo+@W5W^!Z5zsW zgDPlgNO)q-!&_7efT5q~cUzN;BZt1_R1Py!<8W@N;eB0maktSVPnbvGop();-Wa1Q zqNHGLyAQ1%XO8^ZOE(=)FdVSJpecvsi9 z9Ej;qyEvKAcoC%t6iLzt3bI5{8g&OQnl;L-2$Bb4)3L-TrbZ$=$V5+W$0!NxVkb%1P{j(gBs=8KvX+C!M2?W;p-Py$Yul818bZ6oYfP;B137z2?rb& zra34RsvB@{wlRIVy9UETV~FpK9rry&Z-X%cUkxX@$?_bE{?Lh+3MdjsKshx&2J2fp zJYQTLbPHXYsX570-Rl<@XU1XQX(8@Cdy>+W$5Iir!MMHEf-k?K-Yc2_i`5HT+gq@+ zvqGEH()*Fdbpa}~frM5Ko}Z9HB@ zS@Y<7L@;!djcLdzAfaU>H(pW49B_S^d-lY&+^(&@R|VZG>~Ed}tpHL7l8cD2>t|lk zMu7mw*F~V9+;>mVqBd-FO;rbdTc!F?>PI0}wjFB5*DW!krM{{NJnlG$gl09Sx><)P zjJam0q;;5t2Z#V$h@y%UF%u?4Tr3lI*+$77NpJ@}X3Bu;Tk*zV!9|e7h4E9lkqzSo z7~s#sNPs!&#E4>sSa?&}wyEIC6&xhSg!x{fHmLIEI=mb-qs@rAFhbn|BgSj^9*slRi%P*u;)Cyh|Gi7g$6%x7L{&Ro%0774nI#fYHnYurNDC8$XJK zQGGB!(T8unZ;CkF+pF6!Gl8j-(U@jHVt;hWT%I3?SFdh3uD3w}bI^DZiTCc_K4nfE zbAX`J>5&m4rqM=YwnmNJWH4^RK#bxregf*kU%kFfs*W?ulceKDsRa_|F~?8o*KBpe1cBcjx&i%8i*(yeZf_fD=NKFDNjEZZX zMTvE2aVQr;(dcLFzOC%e!NcthdN-_VOdUAea?iOy8`$$#w&2+O7}N|0ZZS@8dFe(Q zY7_JBow4l(J~t=ra?ju6)Y7Ojc<90j0(1DTF^u!Vbkh;v({z9Gs-R?Fi8o@TfLP*r zp@x&xTMLwu`|*d*$%j$2VNugfzsV!)WX1Sx&8Gmi^bpp!^00h z+}POIxF>Z4d2)J=qzY(-j~)taMUSDN8Hr-BoNl{CQUS_jmU7;OE@g^LV9H4*kTbmZ z`~n4cLaD(C$DJafM*@Lqh#$IZNm3}YfgTy@xlMa}e?S{J=0fN>?*H@&cU?at6$9R@ z+hsRQ62UtFfW-5`yOvQpKyH^PdY_(h3&X96T2dnk0_Iadjv2!k&p1g0yU=|uL>Bd-aJ(}1c7 z^|Ziu?PxDtacYO<#W|;%NXaO)FR>LUzr^^smnM+G_!fsix2dMz7Pj5qPhAPmR0#)Ctg_lmoM4?)DsLasTZ(Y0Aqt zH{cy-m+5`B-Q2wGfTnwDO0t!?cd@?JCh$0e{j%d?WC^m_HvPB)-It?3`qvN+>kt`Hp_-Ulj)hu_=$cfF@)9_{dyh&Le{(Vr3>E0CnZJ)r~~3>2a`>NV=w zUw#zNrAgGiIND)MPht>~%e~5=fYv9JNhcH}53$NZghT}Z0RTgi8&CUfu|I_|)qB|D z@gO}3&m6ypLYr}k2G;jSpI?ZtR^$Xz5D1%~A0mMs3FQzZm+q(AOVA$4yY=FZn6SNq z+D!rN(G3C%YETV!0P5a-%c%nIzyF-?TkZh@2RcI_o|+`un;?J)!+BZanMO831I!-z z`T&EqM>%FIkE znxU4brl#(Nx2X6TaPs8IX~&ga*mFATS+~h9&#?PsO}IxM<%IO&*H?GQdxE3^m^o2I zTqvr}C4dtfM$R}Q#;$xIQ4BE-(Ctl5k9>2Z=XBg^*T5l4F*Wn$l^v%};1I^hNuqT; z8rt<7;oIB;0s*hx+NPvLXdRCdh3jj39P!Yh+RIJ{zw1;PsJ=iTfCUK;y625yBub;= zbL-tV-7|C@oBeO#4$ItK-~ON*sdK!bIYxM5*kzJ!5bCUMAk{;i{t}A5mV|S(sB2>1 z(4fVxHVmY`5EsnbJJXKH_DPJmjlr5QEwm%xx3?U(e($U!^y6MdfGUf{DFesMP1xV# z81lE)*tP(3F?{WXEAZs=mkDs;``GPr+zUS6rf0+Vh`>O9$BRnugNCx-xOmk|9CX1^ znrb)Jx15>@7mRvi=!QAtR21xnZs5wzHTokC2vuEuwwFM+SOg2J>s#=}XRgEjr>5YZ z({u2?2Ts9uXF!XTJuD2IJ-*<;Zt7Gwo3Q27Ag7N{IEhme9=Qi|pQt+}P{1)(O$z@EuB{jvG^Vdq|ho z4STj_-rGXKgNB+e8#(J5^3n)7hSholE6)Z;E@jYb53F2uFl7AII{LdnhW&dq;k*g1 zn6Z7s$opC-fZy_5D_R#&Y7E<+^&=?JD>%az+e7u)8VP#{|iQc;e%y7|T z&UZ|mT}zB7@Ra&m@?(7X-6x%@hsR`bZq|D?oOT5E!3(Fzcojk21CCptb#ov#bkzuv zb@7r=*I9Z_Sp^tj^1Tn8^Vew126Dx8_k8wp2in~}fdCH7#ckq!m*;2Lw!n?ivzJ#1 z%yCLt2CeJcdz426jTuib&roa*Op(3l?s?zwIoR0R=XXm1W-{{BKyL_CpI|Vles2$^ zCtA>~iFXXn28_!crQLy5ERm zMs&?@pcY=Ys>Es|`e|)}kZT=N>TgW+ZAG6v0WJ|(e@$smwE)$iK%WbWZ4$_^VbfNo zeF%`jaK9s=k-tYgM(SFL|0GaR0gBI`=i!G{6@tVyOG(9pVfm;baM1-k?p1QAFMjprx; zQ3ktSf;3ckObQUI@Lo*eTe(gQ4xqA)s%I=d!|-=g00QONZS61^QngaCn9^slEh}T| z7^j*0?z?XR-h$%i_VJ~Y3scimOK7)%9qzbI9iuu@q9XTu&vBax5dq#wr^Z-jn~Dw` zqb}l0xVgShxhE*!gCiKoaiSg>?H{l%ZuSU7F!CZs7(~Og&D|~$dyJJZ<;2$*QE<|= zkKFF~*qCsl;6&^R+`DXG#u12V2PAM|ArQc%L>PFf-r(2-nCk*9{OjVskK>O>xlVZp z_(h>reyi)Auhx*o(R;bGed89~VR4B4eV8ZK$6Sz^grdS=%%~Cv1To@DnXt5hB?u~o zsEZAR2~vq96~Is;5xxwPZJ(Kx*4!Bdnz8>3y~=6LP)E&mLOty8EC?Vrm=#qf>=VBCxE(;wIxC{=1K6u3(*O8IT|X2kt0 zZIVzy`)%>l#_cIrWMPm%vwUhiwYV1gId9knc9Sca>yD?-hfhSn8n1{Qb*)fZ=B`5pv@-!SL00T7!Ui z(UTZL(!J%J_JMU2a*peg%Qoa*YRM7?;Ln7E$F+Kimm(`@$i0yxD~C*YVgMK>sNr$> z&2+o@FqiG$+HeW)#|~Xpc~qK;=dy|JuPTbdS)+a6dSUECT|fT}z(K~3#-r_0pQ5&^ z`m02Lc+JJTge|pKERKmv?z!ilQ}7lP4;Y-@fBD$b{LGjTcGN?^@aih8Ztc^gy>NVn zhyg|Vb2RMS$r*BiL!yM@f81<$+g%0*wK{RU=%KLJW`cqC5C|kPajiq5(QbEXP|#MP zBlZtytFY6Oz)U%Fra0MAZLlyi1`pn~K!l&c2YVR+Niu3A+D8#{opPVoiQ{f}Gg4t_ zV5yy+6~NG!Ku!W6di|%@*8X4@?yxxN!ozNn`RSi{Fd&3q@<{jyjtPSXC`lhw0zuZ% zB4uDB$&>+zh|gMs33%}eN8&_6$|1cSz-#x&Ljnmp+K^z|NRU-kfQ4*RBDX0vbnL?k zr@B!~Y6N=<4k%r}uW zSOAk1?b%{m@i@hVI<9Xk+Ol!o(mjU8TdyzO*V^c}g=(pRE!G1V{%xZ6hWNnJ^Turq z7b652JQuGG+d^c;+yev7N~tb_Lur73_2vmg2m+MZ;A2^!hC(p0nVtOs8IS9IavMtx zP3c)QN~bLjbm660GJGsQcI?7fRqTlGGFpYRm8J!@cy+-zQw~=G+mQ>$j{IGGjDzb{ zsUTuNGYXd+fLAc*(;0XRN^He)+VyZe5m3;q8{){ec6+j^Gr_sB)+WP53Libj>VmoH zN%nbnl7lAn9P#al1-@}2pJB9u$F$n0*3jfl*0Sm&7fuzuDln8Z`r&horapm{X37&64Ikk_HHrc~qZ{kh@i91}GZ$r~Tc3ERc7e&*sT8C=F{g*}PWpj`h8m$u;Q%C^sZ ziP0ccq^AL+a^mRO@CuD6vEHQ{8>D)`HSxka+nwYh_Ef0?d1r_O1Mm6O7dI%frv?X8 z*GvR4Bn>bb{K&VScWS4GNV_b2_4(J}=@)Onwaz?DH5_Q&?>Zpc5>^373NbsSw!OIn z5L58zy-lYAG2&V|prB(U4FMze3@l}csgA_0ksti$kHrylZ3TyFOyx!u&WG#OcEUpO zetv(k#lOP642%Q@v7Wzb&yJfNRboj~Xx~Cn96Nju#iT>T7>XdGKy)km>K`*A%<&kN z33*wHkdKhilFX(=9J_eA5@ruTOsO_p}wh~b>&jq+@P^4Up6-@*dET~Auc<~3f zWD3L|H#tr&WupnoRT8O}lcxmGn&UV&Sf|&^VQX_s)~EF$+Vd#!lutTUmMUl}ZGA2| z*X`$UWEV<1^aad;l}({e8T|P+OzAokUuB|tCaN1il8yd_N;qQ*^Cr6Uh(46h2VgU5 z<=Ki~Sbuzc{OnKv^iL=7CKrFh`I(tBQ?!&d4x5@thCgA|@2932hN1b#=XFXy-sQj2SJ_O(@ZD%L01Rv@X+&B@i`Sh*^gK1lmUMB10=+ zdo$||UW7X=>W@a zx!RtD*1i)hwmalSu)ewP#&3>?T3DyzMoIA&XuN?o3gxPifFsfGi4+LO@aFm! z{N7hzgIx>`DTE!z73-lJ?UT>nfOc=-^GlF4qrtAX#~fis)e|m_hV7B+098z+ni%6` zYYHG$x^VvZ808(&FxUJ0d)|PS)gKL#o?xF*3W4@A=T04iNAEukwblyUeXI$`=f_C( zfl>#IPmz*+NRx)MNvat4oSkhLOFII}QUX5cO_F7OvF^`{ger$ht}zyK$DF4vZ6XCJ zgGw(-ZN*zg%-6-6;;6t%9yCOoW)*NaY8h3v=z|Z1?`2Vap?@Y2@hJ4IeV8llaNX9?}Rd@(2(8w3Plz5Hq?(PYw%H;ZDqBym= z@E}3Y#RIO-2bNUMSqY%A>up6bFa$2jU{UMEBktczEG3Grk3aq85Cep8p^Yb`AI;IQ zCET$zA^h=D*DKE-)_!qq`>QRvu3uPRU$4WPT)bPQ+mz2B*GZGpLS)d$k-JdLk1Bxq zsg#H5G_>pXgmV-ATXNe)2TMlIGJN3Zrja|r`1mgUuC0Ll9CJ=%) z2$W|c=ZWUXk|Puv;?#kaE?dxKg33|UfP@L#Uv=#v(4mA=q)V3xPJ`;M?QrlCBp!() z3Ya_#<3S*CRYSa1r}M)4%}dwd4$H*iyY6?KLoGxcQv=43K#IWvCz;aN=ep3b#I8$a z{Tu-ikLLlDLSSyuuf9@;kG|glG0ZmUR5uvXdBRCp9E0r$Z--o9Npj?#{|}#Eh5OIV z!To2aC3>dn?{kvKfGq6ki3#|@@3@aV80c=+dPxxnVM2FK4r{Kq4VnxJ7u%jYk5Zod zk2g7wg~W7M+lSaK6R*JxoEpdB>Wy2J6J)I3B`*$iox}iL>swul6p6?7_q+6*d)qs3 zYi$qym%Sd$P1Rv`8iO{WS_Silj1h1|W6)E_m*C8)%h2pBRT z^|hA6R1!n8QPW1;M0;Kf2-k2OLZ3vHDy;8!lVHi_gc-Ra?LpZvJ0gXW! zYPmh~%o9&M(fpa8`I#=fiG={ez4v15fOKh4O0P<~U359Dv9Tf&1=0h%Dz9AMgYmkB zlS=@nm!^rsw<5D+LV)R{*@mEA>l0@>PonvG|3n6y_#TDrh5I{nVobL401g#^=Oldix5sA15oFjtt?HH6tvLNDO>q}A< z4X*0e#8W#vU)X^=EcN>I1Bz5te5ld^R!1m^5S4@n78+|o07JCXDG(-$1wrpBFw+Hy zVCc&Flq0IQDCPlrNVtI@<3!f9+tz^ zoYh(F9>?AoP|ET;&0#eByywCR`1TJx>?9y(;n`~#lR>z;C|16tF$K-si$68~mM(N& zAH_uz!2Y+J3Bk(i2Gu*77S)M zAAzN8lS^~cw7DYyKwUO^;XJq6fM&Mg0K*g*BRc1lb=b&JBbPYs>Q0SoB*mOIb__*~ z>IRfN?6w9J;HT$)=i@bR0EaIEb59wcz>b{!TM`b3y#W*R@2+R zfdxl++w98e&M(eSQ$32Qk@CjSMHK|v9b`QM8fw_6fB@g=7^;r?IYr6D_AfgX)BN-V zEX>Ujh^b9ad7UUq6n5J^_{ujnpnJ{{>Rt<8yL1!2`i*OF>B<`1T3x42Yhz>I^(!Zj znFbkHG6|TPo^hb3M<8`vY(9v4BI2&T=n08fdl~XPPMS2<1ST^B{~lhwv;r?(ybd$V zk3((p{XzUHTopYd4>lRmvR}67zY~Bsc%4^CRBQk-cWk1~G+ZmN&yJ2LXGa_}rTdh> zm+Kt0ZS9Zt?=WM{Dq;`OCK~q#N`0%s8oDpX5(-nQqvMGsk2h>nvBv_8l!oMXc#Qlr zRrIlZeffA93{}+)$K77)cAIlc1|B?LgL{q}7@!r9qk#r) zbJPTZLyKeVPPjdF9Ny$YfZ@p}pWOaG{MG-{?~ad;e`srG-=vb@X`)%DybeS}@|*|} zZpaw-zuj7g2T!L?g21kiPS{M7O?B9uYMRvy4M~yBsj8W$@Ne`1Ef!$dO` z4%bOCBoWHy7Q6nn_B+h!xwo|6#(Wiy2!R!MPM|drYr zLb^4SnxH)e)vI-#^x-+U!!p-6J>k~k!yIz8CfO!cvQPsJzLID>1``s**aBGKm=nQ5 zIZ{#3Kf(IWY2S& zW5C9Ri8ppag8S0t9eC`aLn@&U)s0d^N1sF=MZ*B|*x~{-g%iP~SD*drO?dch z6K*>7!sowo5iY%U2`*l`1^;k$@?VQOX)rpJT`qx8ZwVR>lq69m;&JFVR5 zSoa;5)1;UiU2^i~F<^?%i_}N%#EuHEp#3S5QnCQZ>MCWoG#b3X?(z8pGRqB}~^ z8W=>>;aVxgg!5ImVjDR*;WvFHDuI#K-4b4@Oy~c@B{+ej=9nbAY`Bj z4hqD@R@bj87>;~-E`cBab18i9hnh5>kdMH938N#Tcfsmv6TbNLCj6^EuyA{aczh`2 zR2Slp>x2ywf!0D$TR`-wkvEaq60ay?R8VT$RDKQeAF?WRioM}f47Azk5-f!lTTB9zIdMz zU}oekh7pQA;`NSB&SSDj_w_owiN%XFHg0eH%jNmmzw8LXy_rVz%5%S|Ws=;e?uhxq zcmf}P*E~%u49~o?+aZ9lynvi)=JOPQ5F)mUXYU2HXF$U|B-&?>PeQ9Z1}jc4y|c3i z4_#P<>nm$;;@A?rbafN%IX&&bNS)j-_c|F&Hd6u-=sNf8wH`crlCAmCn2p8O90y>4 z#*fy}LPSgb+nZfjoT+m(KO@PrdOZqwG{}>ynN9C_&{I1(nbaEhf=wYLj?!??rg+TT zH;5DhL<|Y4W{AQ@>rQNq-?fy}2QWcgh7lh3CfqnV6#?wSrqk_z@LebA{%rB=*6!38 zU4xPr)S4pyjouX~8l9h=_Vx$Qy}IJmMQ+R;NJEtmMSJY@$OskRCAwp>g~qK4 zG2qRWRk(U>4ZiyHtMGeYcpjd65e-{g?%oYS>6U^oopSGLc6WS1&i=y^O9MB-uhN(@vzggMbLNv-uf>?0~ zCqo+{ON;=Ng_W=vh25vXeb|G~7YLY89t96E2j-8#(aKTkjoQw1{QYn3ZwU~TL6VJ* zm#P)OK!GuiE9kxCXA)f(_oIAm3=Ycm5)%=95ys(s(`@z-^*g>-0R-6MGXRwO0HI#R z5cw}LPmaHEojv|Q3V-WAX~MaaA^IS83mF_@bco5Bf{(a8_QI)c_~n1U2VZ%KCAPc= z1UoSkb)5EUT}?iA0!q#j(z%)hK{L9sGYG(t>XuDw5Xz(cf`Jt zS~#`1M8xXkiDep?GbbDem{>|S^~}X>*w|^o`yM<-@f;RsoAALg7FTa=b=)VrM?%JV zEjrzOcmF1-Q*LgsTVYOl37lSEKm^vW)f>=lwZ2jBuH13_1!(V_Hq#5oL4q=AfF?~x z>%AL5y&=?&fkqjE9fOIEfy@+UYnkJUc5H}tErX$#4Xo|X!G+0nSe#B^eQOtHr*b#m zlkoiYJ$U58JbARBvEYlB*Wp{=eICXfC;!^Y7DYvDiYx21Bkm}HKqFU-wpb$&aRuJ< z&^=Ihgc-do@Vbr7Z6`fgg{Pjq30JOMg{Pms>ZBN3u#cdnhxbjL)F>kkWx(Djw9~6! zxJ7V4MpZr943pZJxLI<_U1*}sg+yVrtdYeu=sZj{U{NcJW_jZp(B@}S_R3+;4p$Ei z8c8kTKt$s>n8|lKgy%qm__!yQK>_JwQH6q_(vvSV8sp79Ndkc@?><9ZYy=w2hoO=lfTFf7-_->+ejXhLFQteG+o;XI zKmdf=$3P)&JN}JQ&WA4;_%Hr)9ZnxJ1tOA0*G!BXiXoC^<8b%+1b*y$oFp|_hp$|m zbmt{GR6Y=wYM!sGt`O@DlDb)0FYL1E-7JBP9qph)YlXqIp@HsptW)iF;P~;}8zXD@ zd{Sei{D6EsA!&l~@88N?5}N=45E#4(*L+%_m?J1+X`CzTnd00$dI@iRcy8+3Q;CAq zi6%;s;E#qT0W^d3V6w@x?_ly%&x!9Ys$GZQ2x-rw1Wr(Riu?|N*SR1|HeX23WMoKGTwt?fNn+itKDw!QqVKcJVk`&!ZF}brX5piJcKdY;NyTu%AsQc{qJ+0q#0A4QG#W zhD0=IBf5jJ&oM55BTNHFaGHd`bBfJ?U;u#%TIiz-C>q$IM1w;9x@%C4!gl;bB%Z%^ zt#@bk3xIz6llQsl)?mRvh`kWvESLZs_(iOl6)g%b7%2=;<*S~sD_QG zkQ;vXwS)o_EzUOx1d!?_$8_Z^A;R?Em`~%}@h)vP_+CygPDp|=j*)O`ak3B^7D+y) zj?SFQ=jyfF@b%{}!3)njfbiT3+`4reR&TXY!~&BO35o8f$KCTy)|@0MC-02j0IjBb z6qjRQZ3q^XvwN7^?wL9O+m~=6tCT!naaZMe9?&ldo47QIV-ui|z|?WuY*^~R1sKpTj0;RnGr*P~gEo=B{YU=KC6 zTjK1vWZ7>SM6*j}PSOi6T>zeE{62Y5dVMyYOug7=^l-(;tGIPNW-G5Kk9UODOQSt} zRq?zBD+j()Q@YO^D`rRy$oG!$e2^fAT2j}5qtmD1O)do!*3W$AGp&E|FaE_B-}9dL z{D;ZO$-8Rx`uQx&&DwUyOp_~Rmk9hlrxqO{s8i?rm})w$g|yKqwn7dq<%NO75%t;V z6@i`)DAuN7b`uGu2CrP-r-?Z+UWb*{O=<^SIB{Gs#Pjw}hl1}+jyGhHunhthblbVI zx-Lah7AD9>HE6yQH4C{qkJOf=!6Y?e%Tfd|; z{?R9Y6MpsApM)1)z7FmEHovZ%$~FLSs|*JLhM#z-hd#_BlcO7TJj0 z`$6ddkXMNjS5Gze%A{T0<(a+X%}w1tbF#6)`O~Kq=NzXZPZl6=J`8#NIEUj-*a#uf zU|9eXLR`eeN$STxm3Klt5!W$ReZdg-P{4Xn<0WAV zeij$%Vy|}aa~DWrg$N-A6Gt!Al?(6F6p6Aiqpuro^L2G*L|;thEqENET3RQ4c6;}W zYin!2`P_5Qy>Zm?I!ju0@Hc<+H*b9HYhU}NhaP;#EA#WS?>)6VH;2Y%C?UW<+>lY< zg~pEK4mhMT;A%97iG(m>;EqGA)01OlE70q4!XX3?=pkV&8{1r6!?eR)$Cak8E+>;h z13i=^jMZ6mjUWTXbSRF-4ZhzOcRe?1vkpjLz5q;n&Db%A8p0!B_}Bi7P3PS zwGDpWT)7QTKl3X5&%gM)@ZbLb{|$Wd7rqRaudO(M)p747B`*f#=E>g0O3GbF+|fvu zT``%P@hqq3)p>5uIJFeMjR65p&T~QHR7Kdp9E{47>1s@!?)r;aOTnt!LIX66HcJbD zQhOr@l_yIuXguhE!Z|mD<5Vy8(w)=?Zn9(;nTnw*b4@92kclkbpfV!Wg@_yoM3lfm znTW>c3IsU%egqog-*rJ+0HGfyy4gY2=o$y3Vo&x&JSLt>zsutIGLg4K4S@$&dft{K z7IAgoKlwy{LjKgMJV+6oC6Xces2oLrU1V5LhkP|l+&T&nX7+^2QSs=%EIq8^_#3N*-* zmCTn=5W(XbKrbjfhqgV5w`EKn3)4cCr)DoS<}iJ_7Ra0vZ0!7dA+03z_f+>BEL zH%;Yyr&D<8-K{_n-ggKk*Cy;2->h&)NCu+0KZk8RilW;&{-*L5=0vko^IR%cYliC3d9}!59y1^}kyH3y3@txg0 zx~`744JhKyfus+0%JkODI-5@Ueurgz=B|0SknW1ZKPT!!=?PSrfV#kfbUL-P=Xs^wSpZC{A6!+GXgitO=mXu9M9O;q*!ZXFD2IoaBPpg{VszgHwNedgLW1e z5?j?aXmpyJMF#>F-DOh_%#M4o<)sLT2^%eiX-etdM#hx^6{|-QY!A>1BM@QnGz%M; zfua}@ioigN65ax_g>ZdhqK(3o>k1cX`45F~b;(VJT}2CfjH8lUD_*zKUw$o~cqRhK zV!|DB#qm}bpDS{J)!+3|WBUXwnT>!|R5!Q7?c&adOzlrPu<)Ia*M`RMpn`<#;~&E0 zb_x8$U&Y9xq3?+SsMNw7@GRwRUjf3fs#bF=A@p961@YhEeW7}^$L z7T)*VKMr4csSWLZ;*n`gaID5C7m&-jSW3O6svPG52!uP(@1&0?#n{;F&}6ZB2wIdF z&gu)|{mrUT)%AyHko4PVz$5M+J_82*8^M=ycPidXRT-``{Dz8PB3^}d#C<~>`r96* zGXQX~+i}(GIj9}!rG5ewk!kUQNOfVCPMkP_mh5j@@TNlpKm47)^LJi#AOFqY`+I-y zpFH~LqwjHq{0HlcOCNPh`%HZzZD67$9QPRv7$jkRQ5wMH^P!jwU?e#eu%RUwZy>e3+k^SpNk=Sq z9oT3kfngH#WYqVcQ)3N=lgofGI_0?+Cy*0Fi8+!j&_}tl$lX_lAhjgM~smPa1gQ9SK}; zy8M|*Ly`0_5bDmph0Q$#-cG0glAG9D5_|_>n3qzfR1a9?SD_$Xh%l<}t4bQ;QrPIC z=hA2M_6sF|02Slma1VU^u@t`JJt?FKyso0-_91ue_D%}_zki)k?iehwq+`@xu0*b? zdqa{02;&kTGo|xb?mZQxQb#Gg-s8E$QEL|*=s363hi9+$DNH`2MS41UF7^54AbPk5 zv1B257x6p}(uO>55qQ9UciVd$?2ObFHQI}Fu|dl-T>FOXq|~k3ca}h@EzBuhGo4O; z@UtG7Kvw~R*Iy^x*IxwywjeDlo*#96)#8uueY7(C`~tcPpr;DDl8gJY0}Ks#6U&h! zMg7!I{S*?`&$z$e`ul(X@1MTso_n6?Oiun_H%~u2F-AcoQb+yGq>~VII&In{*lU6b z8v4$6os{7AwuRMQ2L?JD@X)zw(xIaT@r7dxu1=4L#2AHBPGNU*mvrzbwHSA*5EZ4O zJw%s09XPWg5=$%y2$tq2VQmM|^O%HQcHHbt{!(CG|pzqj{~oGhgWcUbb? z;6iOOU1a->M7TzN*=olCU_57UKtV$4#b2r?_i6^_i#%(@s)^k1CdR0}ye>#_%XELc zoiSM7s=>_609H45;7k)EIHsf`!nV*I5`n??`WF1s?>eCH!VnjO`7;t0r*c$0-sm`kwcFI(JI?Kt+sK?8x;9Jp5NXo%V$&o^BOu1W zfjucS!eiix5o{UM=b~x?1Sp8K2m68&kR(sMtE?5Q7wZ(6gXh;0FLz%*t^)wJv=@{K zjn)}&82ASw3Gn=-4F1(04B)xTxd$!~ff$~EMKGWnzNui$ zszAb!T4Ge4sBLXF&hHQdrt`-P{OEVr$j$JL6l~A!MDQ^uY54j@3!nXZ=0kfIq(E`= z1wt449)Fgfi|PUlaf1q!jBm@rqvzAY{SPa=-arhsnLy+EpPU+VKoQ`{*PNtpOw$M` zSg=IIQN7FHTHG(Ka{LktMuL%XFEcuhYdlMOP6C`bm17I?xhXOlX2ws9CM_rSvkAL> z6Z3%_IlbSUjfRbm3V7zG-N_FnzC#Z`g3yPtGnLY!a8(RhP30}Zyd3VF^-WZRf#CWM zU`vbms1g$x>T6&G%g6mwSde{@r5jF-ytu!=kD;;dnCU%gc_X1E?ce>oe;0NB`1dOx z`-%VKJ%8cbKlEKEj?I6d-e|mcYI5>^w^5JhV!=x;I_^KrAc#=_`);Fu{(1|({k}1Z z#Fx2^`s^&HRn8ojxYuow6*P*TYnqq{IJ9#gUF_WFKz2X0rJ(o@7<*x0#YeaY$LE`{ zyN5}wFrm_bb#5Yx=C}~J0P`!Y_U&JQZ$fd;dADKR1E}$$f`y=Fh>ywQGDXX|;y2?b zbuf5Kq6woDN(iD=hfDPLPT}S237kIe#;`HThKvb^*hWzG{0mp%-~QIK@P}W!3S)Ik zZiAT35`jdQQdDC*NDccG#Sv9HYF&*Rg-RQYa2vRH?K#ztQ+zP@kG|e1d!@8`kv9l{ z-+UlvcSzEmqLcx%+vR<_FB)Buu_UH9m(dHRy^{6UBA6MFL~umuSPr6K7Y9 zX|X90ZYU99JdSJO|NI}-;9K5VJ2IE*ur)mPPzsOSlfb|I+yFlHxdDah^)p5#3aq)yWKIzunHD38!|>B&~=})Q*NFP90Qa$03_8ti$!FP zqeLzfse&L^9$yX@t?P~qvw_{s!GR2x?qsmFy${Dvpv0je?vUjM0jA1QIy ziETmDUwi;h_j+u6>D6tA6?2%q14I(Lu$qR4*1$*sfN!u^9qiE8stykIr@GGJPkU5g z5$_rCb&i%zZES2{u$@crCY3vmMg8gj`DZWUpZof=`1?nH^hb}s>s{}9_qhufK0G}& z^>{6<-TU(O^-~Koll6)5G4tqMV{q?DbZfIDO67n)oQ$aMCf9-k6{y;nKw?3$9QuTS z_d5fQ+K5QkNg`gp(Wblt@4kDIhzjb^bJ6$hI!+oT8Vyv1xMO52DMmXEvA1m2|NP$0 z%0IgL;&0u6Z$dF?^BqZoIj7XiAxcGU)S8%yCHP~hh@K4iz)gNfDS-$R^ko|0i2x87 z&>Z%(2>>UqK|`sV+q3Rgn=sbk`Y4fk;pMCF87EPA>VhMah(Lv~S609=%a0mH~o^jARNz zAT5bG`#tNuB?u%87#JeZq}(Ed1kmxdpG~_XO|lcI)tjWcLX}7&V8KWxO9oNQL9wnj z^%R*#oRaZ?a7qK>j*LW&WVbaHA*=Y6hZwCL&`uyt!iH!;Tm&ua-t#<&p9^3jqJc_0 zvRv+GBVrC=V?cY<|K_iVE8Y=^jh{187gKou$amG@u9K|O-|rNOx=t|VUOW!Up%Cu@ z)q<&kLPkmm)eAwC;%Cg|x+-8oL<9mKh`$#d2K@HNYOm{A;eh8qc`SvGyt50x{Zzv0 zy@
z1V@<-S&Ojkfs1mM@fx6le}Ie%EPfZop8pT({pVrh!L>3W@jBxLbcIdM9-# zI2lO}@~qe~BT`HRymTM7Ss)u7oSd`zw_Cr@B7FH9{zyazdp3ltDReDCbNy2ooThYlM`e0v4)eETiS3_i6%IZ;pR}v zH8=U{O*S6PoPHg>Gp5Fzdc)~#U%j>k=Z;S@V8~efin{mdiE)~&IL2u4?iStk{r#PP zyScvc$*tAh=WpHmObfmlW%=Q$ByC<$)D8vEU;-T0kk9@?-l@IF+=^5IAd)IX2)Ya= z{01-egqtyv*0*K~39#@gog5bK(&F z!x75k52x_A9LM+%f2HSuUO28BkcM9#>VL#M`WOiiwm~m}7y%0Oy7>B-2>ZaDmO;sV zXEF7;fjh(taw8t^Z+#fxcb-CG*Z>nRl>>mxsB>}K;Guq*JZR)nRu$G#%!`sx5F>Oi zL=Rw-1kOKh9|1doh5mq2&!es2xWGN#u1hw&Sy{|I_ z462B8{CuVY;1vmTxnAj|jVwd;51{u|Yu^-@x}zCSR@dPh$Nd=wD53KF{QTYUCYC?S zY*+T@fBxq?_~*W_{+nNb&p-CqV?T4^f%i|nb;V9Y=6ztVfX3LkTim#j^l78P1wA!hhfXhLn*#}~l;`Z8N`{!^bs_GM?r6J(8732o zjgR8{&238}O|{Bq&@q5^d%$)OHkVBjK}p?hkP7aRn`x3dgusD%oX|KE{{z7WgaLv| z1wfFy(HQ)R#A}Gaf?k8f-x}t=c%d?2V7+lo>aej_teVgiwW%pKsgVo|(gh>iKn|si zHL3%;$@BF2z7N*mk$dkPENFSp{VDvIlQ#V0U+afB1-7^^s)RYOAZAtHB8waFE;b>nYXG3x#J?@Gz>(&CUI( z0SrUs$tR!eyMH!+17H)_2BKWd2S511u@fimoj!Zd*<%Y+i!-&FO)`fpFc5=9ih`AD zV`+PLKS{HJBZ0lz_I4YZHM@T8#_hGO)z{W;-rU)K@x?#bhi@`Cs4A0FC(wRjDG`05 zhHT{pB;P?L4eB8QVEu$=VrxuUZ_5%185Sri!j(f&;$^*bLAhhx7+=2DfR|r+0akAB zlL3<(N5{$aB-|?i2a)2s6yKuPXOD;+`^15gTAhq-3z9J}&>&8~hAy>0*3ps z1%$I9UvTOsB7lK#*c>kb!2!C>4hHz0T>?>x02C)dV_seprPH(OF*OyO7MFU)zy2RTQ-Gj-f*$PtvkXy)=c1@<88S0vYWF!dBYMa zfu2GWw%A@QdI>azi83$0h?x07e4Xl~F?D1e>u>zO|(hE2{YIFc~n8gY6! zmNIZ08_YJl*XluMPDYIiKMh-#_>pB0?gt+Qh=(DzY_;qm|zGo50 zL}$bCQOKfrw@R=u9NNA5euqgR3i!nw+$Fqze0==;$3FJ4=BGdX={Lo-@=edDeMqri z_`(<3P9@|1;5PgTC`qkxcak*bK=gh^_elBlzY_=^62a28i2kGReg_9ocSCEU9abi0 z_{!=C+&IuY;R`R-AYb3-g-RFUIh2XA1c3u9u|6L|b?-ZHFwt~+%)K6SLlmn5tB&!; zBezA@*zUmc#^l=B>u@KDTL<2(C_S$Gj@kDb7z3kc$?y(0LQ)+W$$XbIpx#iBC2=Vd zj=hiwCzK>bgV97cx@f!@ScLIcA&=EMCM20+FcEF`hP_Tw%sHV!ux#KOO{7W+BGK@k z)aI;~1cIoXy2fjW0|*s3AK6=!c%hBw2OdaZY5vX)|H=i0b3gnYDSZ94jHEiASi&&j zG^Uzp#{j}_<55LxODYN*ZGzF}r%2vQGu$8Ml`a3s%5FPiz3tyJvpN>HN`1 zFl|N^iy)@lhSn(@r+H_~~M7_x4K=9hKhfyeE4C$^qg_Fyf>2gQaUGR?Ef2(fBb@K!kf-r<+B?c66{%F4Z8fiYUkHG_Fzo?hd-wsJ;$^ zhJ(RSvF*_+3nMo_qHP1FZs;y8Eg>EMGYJ^}#1$vDe!!%)y7AJ51i+Ma+yWv59Plz% z&xyDWf0k~*T!a!k8+3nx@UW4QwQ-w{E2u3w;s3ZJgkAEf<~Ro!0INyj&zXrjw7baJ zB(S~DB75}b$F}MT=Y61i;0A*#0=j9;e%Z&yBdxy_`jv zOc-*qN8!-5DjF?rI_#dxE?XuF%?t?#no(~TmgumwA(3l9qygNjM#uz+$U_)czrp3A za^V!d9DkoT$23t@g%NI;RUIs~FcK!(c!NNVIY8YtVUJ8oJ<#Aw-b!DF03|o*-S9_M z?mnBqJI)&T@+%xdIe`FZgnr4G1j4VNK>MoTrwkPQ^W`ukT`QIX3 zjHNWm8yEsL80>Wi6hN#e-1%5zKyf~Dw9i3MXl;>*n99a{o7_hOF~PA>E|peJsG6VEH$9M|p=81nSl?Wl((WrPpf$%KFLttBmzEj_ID68z=ZVRs8&Nz;Z zfNL^%6c4QM9IW=t2i+*0HHOF^$l9u7m=R#Ln?|3lk?N$MHehdW9~Kv9-MVVhzL6yy z6f4Ew85=rVD~8NAiq2}{@sa%sFgalcM`Pm41l<;hYdP;i@D_u@bviHoCx8cVg;#Y5 zC^)DDP-4B5rD&f~=u&Z-9HH&-EJg6(_C4#sGADyw2~%c4;o=-gEvHM^e9A8W-pP}2q^ft3k4aP|A&7{xMGM7$D2D()`YlZ>sJC^ zSBLuIw}R>2f~>ddDES^>CP*Z>i_;AU4!YzbiIEsniCJOEoeJ#*(A`Q2FvkYT(7iQJ zXbZI&yJM1$GiP$x?%}3a7a+llAB|S)3GFlAGKR1uW70wnPiaHPZ_8Q@lvNL8U5TTZqO;GZpz z-dZl62Hr~`CQ3qZGf_9AWI309Pmdc|nln+*r7CLn#tPlm{qxZI1ipGP>@hkV@o#zt zJAA%4=fX9n_-H{)Zo+ZkAumW<;x@K+oJ6XdQ2v4%m>hE!dN%tcN1#5r;@ zk}#+p1Lkl+4=)4^?RFd1H}>GQl`hn44GzeJb2_bwRDotGgV?+y&tMpNVO_~N!AYaQ zzbJ2*t&#&b$G3Mo3}DFxfiSs?@(DAlJoQqNx^^o9k#BXTuc@T*#*nPxr!5kJ% zarl~pN^BfzD*7M4xgt2^Yap68CIV+wu@fLt&lDM!;Sz6sMd?WZB!JGZ-R>*5Zr%D9 zZZ7=dv(G+z>2Lnczxk#`c+~PXV7QaA^sUn-ZJhTT`%tWfV91Dtk$g}|gxQz)O|}RY z6lBOmlyk9GLNDYE+Jr@BI8c#kEI77x9@6Y?x5+q;!-0k@--j*qerRyqgT!qn10s}$ zBQm-Tt&b9ddX1z6nPrZeY#)>>0#0HDlGuibvypVt*kk0{tr`XL_goP;o=*h%vW_P?O zlr%Z48BsSRVYAEqeVBvJ#4AD^Ln1=L?@Of*F&B^jOiv)^@3_lejoe-p|J`ZPO%Hwh z&n;&Zq^r}1Li@Qb0@2uaK9)knG*;(rZinv|g9I(nmRjT|-Iq(Zkh^y3tBxTrk-B^S zG%Z@jbK5az&fGZ@+KjkzmM-+kl^6+br<+6A{B02XM*S%CJ%|_lfY|6P{hKQ}942@+ zTFe*_?9~;FJlTe+Ns+WAIpxOTcb^3bL(bvP$p9Gh?l|Dn>7az6OCj+utqfqRGwpzf z*hf)B$LPMkj|l_==s+GBOedTIUJx#+NFx_;ti83_A)rl>9%&D#f$^XL1DIiyR(`?s zJ1I&_gQ}J5pn7N^29s5#Fb^QSV>l2j>#l2tp8lxC1A)r^*kM2@^Pu1np*wxcXywtaZP+q=XGi5rpgv}Qy#)0ZP z2bK*7`;yWcZ4WrYT*f`caL?!sky=SDjSvB&Dhi8?o1WwhF@|-e#6by1N8w~eX-*qG zhPpMpvGDWB#sv%%^wClQ2qu!!u%tn47$kX=f;5#P;9r`00&X>AF5wD}ZO_)~hPX`x z3uNgnseBWWI%EO@RGdjf(Mf3Pp3~qr76@%XZ^U0lpn*Q+5;7RfS@fRU0Q~Tpg$c4G zB`FUMS-p1H#(9Jy8zYJ9`{>J$F-6l+5-k4e^oCaqyGN53ioK(iIcU*rq zlk*0=T7t*7P7z_zwQ{T`(RB&P%A&jGVMKkn&nBKbP<)Od&j+XiM4?lqX=j~U)Hrq8wv#6O^2*A} zFZ{}{{K^-_+YtWRU;AtDR+hH`!yT0R-f=E;k{#e`-1_?p(Ai6yJLkpOeeF~`gMBI_m@*Ra?W)g0tbh#Rb7x2`nN zd=4p#I%!i%7?4;?9u9mAH>82*?vpeaUg8E~B{l+wnzsHqX-sr8Cr*gd`6g-~0s%Bw zqan9B1au-{xHZxy-CK-JDOIoI>N1MSG308hJRpQ8zB# z%+Wr832Bc4_oUDynnIuj!gRvtP{~59n(%jzh`nT)_o@|u;d*+&Kyg#{oL6++rIo}H z_YTZX_nai84t>=3BVnzj5WB>(2%n7v@x35W81yr5EQo{_#rbUq7`C?CaAkE8)LoV% zkt#?FK)Z?+R3KsF!Hft_w0go7+HT}wfCk8)d1?%P;QhBL7#G?yFjsEH4^O?&hLxRVXpB$sd>79l5T-(nlC4B?lyG98!OPSbI6xa&TiL+BD3#Fd z?)Q23k*t@hHlg*;Ztd(%WEDJtjH`uMq@F{Fu-WZlQn_!Qo3NIH)fNYr__~4`Ak=_S z_k6wWSDcMLWyf>vZf|dYbz@`WlVAGMmwxqc|Lwnh6aJW$w*kW)6q7VCA9repcFXv? z5dS2BV4M_(E7l#=L|l(5A!3gT+R7oG#9}rT-BV}1kp&z({%hbUB+wf5%}9&{{fiSV zWuB6-jvw{?k~Wv(FgS@rU82k-D3us+L^t7L3~}wOTc%L58u$f8m?^6+*u7I^E4ne8 z*yvF_PrO7yY(o&VfRS|6YG}&mQW33Y!gcYmGjO12do}Qh%2X8F=NokQ;L3y zpvOW9D~>9bRX|zB+$v>&?PV;cN5B4@)lJyHLS#x?R4=3vE%kp8o};Xah$Q5oQ(OU5 zOj?!x$CvLrn)e@`w(yFAJNHMK-AJ(_`+*tO>=0br2C9^UihXg<%oGOJdBGL1zMUimeaa0S< zKzT^t7@;54#)06$ypDFDxXVI?gdx6XeEcvlQ59#xf%sdUPUpA3{N*oy^2dMt$A1?_ zA(cM{sh?}2)gD5#mc}m6Ze*765MHcH6K!c6%D;uJ4I7aTJ zk_JwPE%5`0*om}3xDF!FRwWW_v6s|1cQVJ4XTq*ILfFh&LM+pr&|T99(Grn$BAX&} zBh;lNCyvkv5b>m2To*M&Gf4xC`XZ4rC+^6~yEP03L>iMs?~U~4v>QthDUlSBw7?XN1#5}MpbU}*FI6zZ@eI&K zl~cqMsbwxSZ#Y7d5NM!hPeaIYU#yCXt>gg6#o|7fbn#3~F<{ot9)2GOiR$Y?fQqku z-USF!1bv<%;T8-70sU=lQQ}}r^$T^}CvxKd;Ts7&a$x{V^FSPTudf~j(m`Vj&xpZ= zD0absy$Z;N0YQ-iM|*o+_{OCMc>$&Vde)yX9HS`8HHmPm&!idE+AmIxthZlURrSAd|vXV)OkHa z6lD?bWx{%-fNVLd0LdsaW1~lv%nkZo-)D5CqyTCf&%~{(-K~t0(@l(ziF841MKs}* zNa6CF^Y|FC!2;hGh@_)DE|i2SKR5^xH*q-8J?Pp4+lAL#hQUHryHqm*Px9rUy0o;q z-R_Imu3h_u{r&x4{^A$Ec;lyk`lr7+(TD#C%iDnA4hm``eVR7U0RvB#BoI5QtHcjt z;veboqhsN-e9V?R0De+yh{3R0g#b%Fd)Z(_`UM5!NUIAVktPvfu-5kvIn-Q?=h*5#6C=6- ziRsZN{?-~$c2bobsAR#5lI`#m^P*~z%uWONL)GEF!b3wg47WX7;SF%;pv;^C zL&=e{XCW-@a0vjRsD_BomoE$x)ri``mRLEHsKFD^W&oP7N;Wy)_XtGD>W?-dY)tGS zh@BRXfTYIUHn4O7Kgq~Zb}N)pHnN+yrqnsW0hScwfD=}o$E-q>>-o|6DH8yvE?ZJ7IPG&!SdA~7fu6UX)l zo2+^bsDwfF`@KH98zLBa`Q|iq2Pvs%M7ZPAZpYiE0veqoNujUd)5o5$#n==2;oE^C zP@owV)x(0g;CG&#gp>1ac;H-z<_)Ql=-%v+!v>GJS4&_Zq=CdNKycK?EVT|8J@sM( zc6*c19E;W!L=OZ|e3)3D~7Y(HIJd<~tfS`Q= z2MiL7RaFYQDi3%6dDc@z(eFSn5!b3{mX7c*A3C&@FR07>s`2SE%TiHJ=kiBZE* z!+ek!GaB!@NI_;WL!P>XK_Ycnih##z3Fn@`XJPkFHWEYiN}VDJvUGxj{RGvF8l&=e z%K@U0`vWG@DiSdAJr~zY{>c&v1Pflg59)pyIB|71rT3EurWEV#v{QKbs)4aaixMVf zc};Q&)V&=8N^W=@ln_CI+w-o51J>{NQLo=;u6uhggBLEdn;dy>0GXt4|4LI$y&!y4 z#$3T=v0iSA03!Z8sMyk{&Pogu{;AK;!(SL&hjYieG)Soia}rUw>ek6nF!GK-z|iSr zaQ(J{CoeBUW2_nCp3uCFl}qIXq#Pjw=9ksy=3|%q&Za z4k%VYdcii!>f;<;T`?Lf6i5~sf8|sTYw#z$ zybT!cpjg{$A*)j|Vkk}OfDD)rmW~&s3i`Wzy$zdyv{kqiz;iYMP z1sIlE7!7G%kR6MZ0hG=&C?Xz$nn*c^1Y{B}IUyIWG*!+60Z4;H%*Jw3DjvJkb4Loi z*tikI^MeU|nj{`D&;};g(&ALL1QB4CG$0T__dx|5c>N$ZY)1fM>?r=|DEcP@zr^@d z=_onC%%>=ANK6D+fHxUEM!-ghDL!kKaSH4t+BnsYA(wqSlM~*fLcZfQPhXmYv&)xQ z-0pw^y5XU_9R)P1xpAm*lBQtC!2M+aBZM@!#C0QJps0a8WDhVKoLR0gL z#}xON^sdoJqOqwiwnU^Y1WDgvWmLO%XrjXb^`x#DzGRd-B08QsfPZUuclXzS|M!3Y zXMg8+e&?w-GZNz;b$J^w+(8-S`z^OXGDXGUfkT;h5@PQG0Yt|ksvCZX3229|F%jC1 z8H9itJU?G3z78a^*X0p6z;2h|-7l$vS0$*Gv=2!L;x(Wfq_|Zksd!#Uj0X1X6#EB~ zKoF@0=7@x9f!G6>ShUHZq1l=|1(lMJw}#V6<8!BCi(rk~Cm8Y*Smk`2s!^ zv%a?^nliMvx|7$pCgGLq6L7yHloko6B(I5Hy1FltyK&NBu2=CzH$BF@L2TPYI? zNXs4#<$W$5I^)m#xj3SX@{}-#0M@vkKR06d$_2wUyXWt=TCEpu-n{uwZr{HB8&2hL z{j;C_>;V3hmA3)I9TcaLXgg$|%k~jgM}hF`7vj|l*Ly)hs02j`MHR>ZP|#4g)`EZ* z-Pnv@Bt9H6h~Z%*g#ZmHO^tA17QG{X(_k>NVWkWJ)bLSDBAW*Fg1B(@oyR)J5p#H6 zkqCJ7ZlkdEbb%9ovHC`7T@kH&Q1p>C;%Ik_tz5WEX zq2p49uh{KD9?jblNQe#vVjqD34`88K^qRaxBVvFNjPt^r!2Bmq-#8AJ*J^P8@^x66 z+H^zTq4&b>lmm(+xUrkStE-E!(mDy43$9ih7eE>inR2SW<0XK=0J=;_VsunB7&dMJ zJQW5It{gxG>?XQ|tFT>xQR@+91R(SGHeURnhcS!RUaX4Jcp)LuBR?}s9vIm3J2vcJ zY`d}*SWv@9HF|8d_enyPH&Ojl3olC~47C~?z;HYU8D9$pAj&{7NMJg>42BS8c-vqP zB+#b7$ycc-s=&lSl8h=4q3whxIIy_p?Y1kQzjp1~C$C?>{<}Z%6F;#Ie~Qc7fZ+~` zeK{q4t)I4kyk@rHOK0rZAFbtn{g(V}T4&h?8LkK}z6j;Q45-tRo{Iab= z%AmnoiEj_Ge_0{eY9*bMv1173%&jQVXw%5JrDFzjoQh~&a@^gB*mlY`nUsE(Js zGa~LEH@DOqT7i%3sIeC^6?=-81X)jzv_P%94ePB91BP0XFb5}+1F}&NOFbW28-gL9 z57-2<`Iw@F2hZmo9DrB97@@vwx51K;G$|6>z~RX|(y?%T+v$usX}}ryjt5_Y@$nS; z&4m2->$Sv3qNF5TkxIhmAwB`yJ)kd-U%7Z1u5B$r+Nc#krb0foaEXKMKyiju z&OG|dhqu-kJXAFnw`0H-8TmqMINlA|=q|$NFHggSS%;a%4ouYc93$O>)}RSn-AUMW z<7H|yPVZlLpH1-5RZDBFc=tfuJ*bG&AWJo#pF*IcYdo=yn#gStR2UNs;XG&;$zT8t zax<0q+4BC7foC?1f``BafUJA#&)aYvQ*BWd%6+||qrUY7*h9*)q`}SMy{;voS_D5~ zkI;;jNfb&w=gwvJ*4Ebk`Nu#0@vp(K3DBR)@-|?&gEHvtwM>$D8wO@x!!{U}y$OsC61t84j z0_AWp;9-Ro4u)!R&?Yt@RLO<|1%?SP1t-#O<0jz&i;CaH&{4*VK!K<>_!x7~8jhKQ zI~h)qjenU1QEh}c540`V$bb>YL2M(a$N0RtZm`0)**G?X_3xO8x8ZpGMkFutJrna- z1PXz`y^--SGH@S72^BCpSY>eI%*Gn$Q>!)W_VrQ{qO? z5d$+=-x-H5zjimQwH;Wf)xktkt}^l0SrgMye@-e}Td{@0@YXttu`9QGu+AAgXr%Z9 z?k?^87_>S|PP)(+B7@@e8q~)n%BR}2*r3`ftUM*;{=133M?Lg}pTRsst&6 zErK849KE3kY{ab9%ls$avlE4^XLf}RFm5#uq_b=ZeqJ2 zegeLSFbfdWAZR>^28z3T`!F*z1sqi~cuS}hjX{Gj9yUBMgA$$K44Uv+P{NRln{pMv ztbzjyKFWkV2EA6m_VBgE>jIdG?mIO7!U&I$K$v#B{nRi1;xB&er+(_E{zSP%elyD3 zfZ+~G(jK&pRrz6&gE0qQSv(tK2fu|WN*`KU<(5l`*tcb&Al1;;Y^fO=xGm*4e1u8Q zb9#RT-?p{!wM?+B1byP|Pyn~lkk!~94mHj0lmHwk%F2e9LrBb}meR~c+bAS7s00B* zG{{8VEVsQ>$ta<2PH}*eD27{C>cS^#_P*feA-yfK{=c%f2%me|z{AI1h2wL(Fg8~6slr3h zC{4noksUGUkA-U+i|~z`7n}fpfg<}^RN3$#gjWat6$tPM&l>{;zj=n~GE_`_O;sr) z3R5~!xY+t?I^gJLN^=2iVI%~(e~=Ofs&)d=0r21;ZWm%1fdb>W520`BjS$pa{#)U( zq?K@MC=wo)rU^ie#?soTpGJ+9P4pM{*7#p)D5?gRWkAD`7}e`Z>+^3PKH94u1%$aV zzH)sNP8?riCZHhQjM~MY9%X8r3R57?YRZg;x88lXM9hnzZMekseY*+@Z8c@DmM~Oe zi}KxI)Fj6H^?JQ$pMLu3Kk*(9Z=}2p81A5?^`2zpa7sC zIq=VB!*fKWQ@ae>OnF}}fD0(iep>`f5Vctb3&rmWYSsQEDV{Y642zQ?0^a48OZT#2 zh7PUQA;MD_Nr=@H#Y@8avC%cw21%aHgPSuyqVbXsS+Sso#WuQkZ117qRS_TG8YK(t ze(J9$<6{8_^iuH?w=X<~F&Hx70BwDmUtj*(4QTD}!-qcj2sAP4t=`^Pu~!0F3%VZ! zcVnLO{Kd%l^!hmr`kKV*^_2h?BH$1mA363>sZx0i*R!Q|hRe;HH^2I)$@Ag$l(zxH z9hCi!>CXdZebLP+Ch}FAU%8kPaxVe{tuu^>has|w7HD)JT42Vvr3=bxq7`AI7^Cq>|Q}0R#dA$&@Ejf<$Z> z)cuNV50WC0q{t516>*OwsY1@`7ArOlHoCpi0(B!*;>>pH5!V-ac;ph`DG?V<6Ok6L z<`7p-{vJ?q&Ee;W78@VRN=S$ZY zviMr>Tqm7X5xo*t@L(eJvoI+O{Qy&ZhC&}k0EA&eWTWfj_H9Hw#qdxNlz-w#BH)kP z@Lc`{=m?7nkH;pLdjJHffw(GT1@TG>AS@4`mO%pxMl%Kwx+rW(+TiBi)6ZXrt5;Ux z`#$zwc<{joC>Dr5M9k@1kvgi2XJCIs*G!8!+5S+1*C4&@B-CA@2X685&y_M--|cTCKirxc!m^F|Z9n zycx#sN#O<%Z7lMyF=5;F)H4xjgeKyS+{eiCL0vLgVqxSOHMr7{)M_`NYKbro%FQR> z)8s<=>;npun!Vf~*HQ6t~E58@$VBMF1JTk1Il4*Ue@y%2yrFHxQd z7^rP^XB+<6KmRTG$cNYAqaXb?HwDw^-YlIg?mZCgYdT7t(5>5bQpxGY5MeK?DB`i! zcTrSJm~*@Eb8~aA!k^LdHek4;((SuRMj<|VE}8=I4GVij zF$NCC1EvHh!tsj6Ko@a1q5wnkXA?THDg`cyqO2 zE9%R;j(|a2$wqC&Z5JhE#Tgssse$lRX2g;!BAT**zg(rfc8i2-nAi+~1+e~|7J8td z$cPcK=l&sjxae2`afP;nN`j>U+@I(=5OpI)#guT`p}nA{V4EWj=EU|HneQK~0Noik zDF^QRzx(Ykz@^KV;fH_dV{rP+8HlNAhzTKkRy1buOzZlb66N8md}&Ztp&nM>pOKdrW{HJmb0KOg{4}@2N1J@D4?azsag5+~BlBte4jKmZ1@TV@2dXG?-lRu%0Uya+8 zU_yxx<`n9C7fVPy*KCr+OC(esp#hw%9}5*xbKjHbc}$6}gz&*O+MffM(D29V^`L}J zIVzR9n277+$c<$p$PI;<3wnPbPZ7VVMROoAO4hjlDTND8!x$#1Wb4!*R`UE2AFNYA z0YTNisQruqhOcXjbs#$KB_=W+p+0}{T)2&ioWD5YwiP^t#)r7T_=oYbh{Ab0>fz)w zw#9*=zQy;3A#hU|yTWsp+OXxe;_tctQ(|w!&7l0m7UG~unQy;roXP)+B*EshSZ~yim`>c|GROM|6!<`jW z0QQs+7dPmzNWvy;gCruHk?+wl@7~wV5ooAuqgz>seX+eOl;2GF z+H#Pl04l@<5!moi6LebHl(1x8at>Xf%pok2D} z07c?fBwk@$!jGt)Et2%u`aLGlm2K+pLWclibw^>!YcU3fT_>T~2vn&`;zzKh%OWu3 z&qco@@ac^m^}52^l=TLYl%fD00?;TVj#J43{pJ2TI&0MfLhTX+cu-NpN4clb_9N+n z#P}fPB6`Ks1^>f;_t#-zVXm@;N;OAx9QTav z?d?DO*`NK{KKvOiZv%!qEN&{b!~SDSvG$`$teb~F9yobvNd7T}&8`4KjES#?;;xxM zai_Epv8tsNr@L4js{x6a7*&Dl+;hMADQ9j)tk1+I1NP&#KG+i|0TZZG@UF1M%~^p8 zMA8(iNpw8n3j{E9-@7>kB5#ZsEQ*1n#DY+33e9-QL1zMhQbNlT3jVEX22Y@MbR#ez zDJTSvD4|dgRRjx4%!&jnxL_K&HjCc51?&IylvGob1NeR!6M&V4Fr^H{vWK+9+IVM-%K~bLBY&g=L{z9-%X3V78k!rCxbjqHDz zF*Oc`rX_eVjGBnn*Yo4*hq`!8tJQ%UE35Fi&wUks@t3~@Ya3h8Xr?eTI|g5Q@_G2) zkA4eO*)LQ}IZWZcue|cgpIL)PEN=sbJ1h=Ww+n7IY1lL_q0g121TSI6<2x);DB@wv`Zd1W6OpTvF)C^(MQww*t%GdVd4Lb2%Hw9u* z^BNFsS1TIrCzyzfmV1NoE{z^U0Owus>I8`Zi6tbC3iPAQfa=&8;p8djgyBe$-XbG0 zUbSGQUC;SPL=CFci0Ahk#<3mStibh=v(`y;ZUly*U3Ot4WR>0?szNXBi;G z->ux!*DhB88=VietS6jt5WrY95d#=oQJ0I@_E;wFL7k!firTVbgCOSs`#=Q0HNlkj zi6jeT(*WSXLEMfFo`S~P^|0#8s{ttst!2<)>#(}D3;*Pw{&QHlwE`dizK=np(e!sC zkdXL=Odl9fERXW7=$Hh9?CMel6ykIEx4OCx|NgUIf?xZ0Uxnu`UV*Ha!`xic0R>j8 zAn^U&-~T#%?0dcyj%suXe(uve>Hhxj|Ne9EXSlo#81A^(qCQIrnN`EKXfslRP?91H zmjh3h&MFAd1*e;>DOE207D+*fz!**zGR_GH8*j9xU_>d#RX!ATUg~AkCmX9|awt5B zDJx%05F`+Kh*)ZklOWIS15KG=w*a8-l@P#%RZhu>L0Z&iRG0&Th!%15MH_=A2;rl3 zGfYGDUeDRDr+C={YVoiL(ISO$~6KOOG^t!x@S;F z7$>=C-T24<_#a>U-`V>Qa7&KkJQ%K?Ir*l|i^U=nkpMx0APGj1qG(a1vTVr;7Uj== zEL!|5%d#a)|7TmGgDBaOEm^i{B`VOgf+>>}DMm4X0f_(s5Mhyl#pZZ-_ofqP>hJ1U z-P7mZyTB4$U{3*e&zzo4GiRpj?YF9{;T)C;V0gAtf&&b(4}%{}&46X?_f8m}U`~l~ zU_qo3_<)IHGRC7RSCX7=2JpTL!v60EI}!E_`K6#|8ys^002QB&U=(pN6ZJQxoPA&Y zi~@XFqxfA!Mti@6fOyGh=1eI1FATs0{`w%r-v|$Lgdb8JvtegM3NxVtY?f*8lPc3E z)H_=`XEFow9B?o<+P7y}1@G5j?|}s(vG7|PLIL5<$K&JJ;nL0sO($b@jmT;jkfe93 zt)M3YSb9~NiYOh&)EgGT=S2t5a z>^#Xo!~SVtP`bPYAOk3T$z|XhVayYJ5~0{Qp`Dz;Y-vxR!8;<$XTH&ZN1r+jzw(~{ z0YCO5Z->{s?)5M|HKoS@`$B;B*P)$Lr>1JK9*hu(d3ctVmf^np9)iz*;ZC^Y?uX&P zfq59whFJ9+X6EW{QlR~ablYl6czH;h#NB%P{qWu2`7Lm|((QI1fmL3cb3rD6;aQ3V z+ADlcSrrQw>^Ed8K(Mq>;iamvMJl=0e1t_MvsKpCCT|CtSW!EL&z4QuO>$hmKw102 z()lE~7Gp)?14e0EJxep~S{tg3&EOTFaJBf>&B_nUGN5&A6y{nD;81zO1wsgRr%A!? zu~pB^sSp^c?;7U6C$hfUx|!kzJ7nwnZM|Ok`l>8wAhD(tdpd~Lt9}O$3|y}Sv2ypJX;zJ_&gZ33uD=bB$L4vF@KIM$O@W^`#Zib=`?qd5YPOBtK-Z^ zU2sEJ0R|NgECPdw)jmKcy<3XLwdx-X&FhE_e zcoL#eq1eIhBV$+x@3oUbcUZs&{_JD$KOwZfSD3qA4cPuo-mN%KtajU zJX7FeE;vxZk8OTCRj~E>_f9|e5WGv_>U(Qu_BV?Adz=4jn!YFS_i42+WQb>dW%-@_lfQ%LFhy zOOY>V&rZCZI7NpJJZKed7s{CjTOiA0XTj*B|A`0)1hZE2f>tuH!6zZ!p6_{$uzXp` z&-aQji2yFGgiyhodW5L-N(95O;G@2|C<6k5D1ON5(utZeFeVKl*!>1TW)cB2^Wn6< zb#`?5Z65@KFwuRtzpvekLYJ$8Cw*VR%+&2Y(f8Sm(*9D-{r8Q;Hv{}_!cbYozem~@ zi{Kd?4D?V^5Fw&0*j3s3Ak?iD#q19&@L`;ScJ+9rWxEps4epYOVJ%~?9})ls7;vfr zg`hte*T#=HScvilOhE`BLh!-O6WlS<9NQ?VfYmw!^MnTwJk9wt*zeBtP(~durXiYsYxd$U^NYmq*sMTQa;RSf_@BAq|^~7WF!$0~X&}=qUK8jL0 z<~hV%YIRBdA3lEk_=hA&_|RYd)nDEA8^7@z%XD(*&g)N2&&@n2+qsdtRhs>0l%+(C z8o9ZS%m94haTmBQLa#r72X{RQmtDFO)+6S3T6+BP$0rN}OypTfhS;U&DT7%1V)SXh zT3fXp&|?u^7pOrz3#$}~{Ogd|u}FB$$2;ehZ;;pSMCk`~fXKnnLIPy0g`2hbzIyNg zAqKJk@Dss3;&qBhsSBso`Rqqs5l3^5eeiH1Y8av(Rc?`0K-TmDOz~Tjh5p}w1!QI) zV9F}I6G5vbXALmCmtkLVjpH$+a&Q)X^??$sdwj!|L!8wAbeth1*1OeWJ;KMy@Uj(fc zApjp}%A*4mwjJ>0d{0fxat{`(GGIdjv; z8_mXzYCCv^n=|IGK&>;+Vd2sdbI|F}|_D?|%y1#m8Yi6~2P?2YE&25EJDwy0#b? z@IPPxz}6E31H&L;6)bpg;sJ&Kv+)&t|BwZA0%oqzB%v*1$~ypT`o@93e!2CtuY4Em zGYSTjdsakMWlSF`PpuAn4jqU8`rCg9Z~gW+!1sLb+tj(iU+E2 z3fPxUJ@E02f1HQ}a4>aRmCmuJqD12iFv)gq5b0{5ZuNunGLxL8!Eii5@u}hm~{oe3%xtQEfZE z547_$Z)H3wNIMZFw*O0NixJd~&>Kkgk-k zQMfj0qnb3RQ0^nWi)yK#uPh@IM`oG~?~?)(vR|2;)gL|Cg}?aA&%lBGhv2}TSHc&* za5sGIp8MeN@nsm5*+4>wFTxtqf!dSOO!KuYtG!kQZbKV7UNa3fVOMTs!7#w*G1Sj) zcK~0%?+Li>#V=ZOe58A|w6yey5CY*GmkD5a)>4qu<4C@+O|1YR)F}=ufC-Q42Miz5 zSDgf_t-SgYk&cKJ6;Dq+%U1C~Fw?B&VdpG5;IlS&PAk32`^9#344pkivPG}hG;JTA z3ZYbR3)F1E1}YK^4|qTj*AWf|?7B?*4)`!I62Snlvpw5q%e06XKL9uYq!o-{=fivI z^oU>sEQ$bZ3imHS`uB|`oMDIMLcqfnBc|#}R)dLq6Q1+=aeQsgw1Jhp01-$KiSq&G zNc6svhKW7~l>k<51VVUoLX!h>m!hn0_L3O1fHa9F9g8q0GiFpxiS?1d}eM3ZadQm$_o+Y&D^m!B~;Jp53|WsREK@> z{VEId=fF&&ZUEo`onjy%L{_o65ty?h`^~g7JB-aC}PGKMeW`!3-K9^?smJ z+$B?{VZL$in5C6Nzlg9+wmr;O(M+213(<^);|8 zLkJce2w-DNC1!%Fw{-bb-m>&S)LmbA+hQ|^miXQBY4+U}Hu|H9{OBS<%JhsYY@zpc z5fPXthe%OxNU)$;flTC*G$_6RaBLCQmU8~c)Bjt3u4HQSH4RqCAHx1o%ms?@O1=uU zT&Sp$I!dJm@`^c>70jPlfQKL5lVlhW{J1Fr!-6zPob&z*Bon~!ETtSRFG$L*tEo6P zm_j_b83|?|Jww_!l`A*c+uCl-<(Hk^_ShfbU8v%zJC$tnGt&_uS62c6V3`v8(`t1`hUJ)>Y_tILoqN z91!-q+p*TXk6C6mZpmoZ-%r9iWq$s^-r;cYF|ZrIo2IN2W#+2RG>DMjYq^~e==WHK zcP~n~_x>mJo*S{Ce|JZt(XspPyYDHO$OJGvTj}*rEFkSCfqdsU06xA#3OMMq!7-;r zaXn?CJZqPT&x!=PX5X#CX0r~ovwl&X!F#12riDxZV;@)@Zudx$F&3lB9=OX8ygmqk zL-N`zh={MzXNPd69OvVA*8>tnj8!Hp5-B~dP#{uDpa(IZ&XrFYDa-hG#6)_mpnR(U z38m+h4O3dY$4t=zzY4DS1BJ)m1Ey?KZ6H7u9Pz)f&nZCU&YSCqSpZk5Z8hpk$WYk2 zj53ER;BvEicg*^UUV*3T1B}1||9y}F#i(ztZiMi+;MV~>pDAY?V4b;(j~n^~d(?fp z^Bv;vF5AYQM7W?%nPwLZXi#9n0|MY|fHf0j26RY3v9DH?55sB5vBmDs42S*yC_8$R zd0ze%(I$x>Zt)vo)Vc2uW9d{goiA(^)vz1=&_;LA2 zokGB924GZEpqu;)ZHF#9pz5!DU<}LoftP<*EdV4+@S?He4NhZqW?so>fI}Ed$k_jd zU}S<|FPa`Wkhd+^r-&ji0)RIer24_?4f<(UY>X2>7^3?g1A(v&biWB!@*q=kp~G};_o%)=BgXxR>IL4Pb^JNy618uR9nguCw|~BDmslTzYxfF?m($g!>a3>T zOkYz9Jj6;E*-nq>f))YTNQv=G-rujXQl|~99EWPInix!s71xi@gy63;VnrOVDc$wy zK9mr$6R~N6?fU@U@sa=b7_}2@h3)UO{s?e2b1G)E?fAODr%%JNSXMe#Y%JJhRYXwL{|p2+nE zbe?tK3?#y6Fob)*{rGCKE6np$~)w3jvD}(5(DZOZY3qK*~F0@_xF4Xb&+~udGs^UIiGb-^nU9 zT;&g<&+1>GfDs}14=s@8eei!AK*UNvxK{QIy0aKRhc+u?8lrY$Mj^-_^vlN-sPr3y zI}DX~BDx2`HL-pmRNp0+AJb+9zmb#o9q?uY2!I728@OWU&kqLU2ZT!A)DibH@QHBG zShP2Uzq|S)7!UY!&GL?84y|b-^Q?k3YX}1fAQM44l~p@MX2yJxF_t2uooRErirWG9 zb>TGS@ZtNGckTMpM^7y-ymK_@{x_Mlto$F?8t zQN(2X$gjyg+fkhe5cfGyjKQ@hp4tk4U{7z5-z!Nlea7gTVug>U{=kV6LM0pY4?MVv zfdW>513>7E$N@#fRs>}%2nrn^!+!ZKthtYZD?84AgFSx;)!!w3U%|XSmhICCB4YkR z5RfrbL~a}#?Ft3PB!Kr>@M8;@*HG_(i6UE;XTJ>3LiRlNl_&4J`_n(Wur&WeQt>|` zO%SLSb_nh8SgYkaEOh3X2YbnszWeDTaPaVPcW?Op8I4B6et+}`Ohm~9Fq{D?N6QZ* z@c8b)29v5_@*9pxID{s)Pc(`h=r zV#08kLZostaNs%(9xUiQAVR1`YV1VDBV3v)17S7AYfBd&VNHHG5)y>;YFcI z2tY&09%GF9=kKL=1u|m*yT2d=gswk6o`GAo$ov@m3JLEV$?M4k2MzX5g9XJfpxD)4 zTz&}7fINEt7d~-de(&r0{gq#rw_=fiLuU4z&rI9ikHS7{`r2;2?f$^|(f(92?NnI~ zpN5GjnE-|}AVb>4tSGu}t61O^C_wn~dcpX+8MN%M=f~*i<;QewQL##!u3oE|S#_+{ zRYZiJZq-Dr%mq}Lh7eHjl??hejbkG`AW->Twzw}TKL!z`qbtY3!MH1C3JBT#ah2hi zBB2_tIK&0Y$Io1?BI7evPn(0w8z}OW!V9hkEC0pk0b|7rp;8SLF)PEt0zx7!{PRs( zg$I@TVJuKUb}iu3-E7)}m5O=9_JQ4Fex8FQ|9gkO1MuH4?jwZXIg`NCJcE&97Oe6w zgF5_p5Z$Nf7%RU$=lw8vR58wjkf;+ZLI7_T^f3-RP|(L@a!|eu0f@-m_~1-@?tJc6 zTYxCTjNC`xM0Bkf#0b%R}Kl}IP{OXDdV*yc` zV12js*;@$X0gPAcyzTg2NB1IP9V$wXm2;rMDg+4BXjDF>5P&IUHt0tnAnpq$l&Gu6 zor;g6{J<4338(G3(qsxKc@5S3gYh%wo_l{^__+#z7}H;VEp8P=-naOQA? zcV_hOIg6|qfr`kk;Xs64L7`ZNVsuNrSKI;L7%A?&^D}#&eB{eN)$5)7d3kT2$n$KJ z(Gtu|IJ1N`n-paM4?ny+de;C>ojUcvCqMbgK1@W(1TdTdIko5Gr$#vZy4uV|?E>c` zxMA{T=1F|OiNEtto0|_zDOqh`z-rTDvU!CT3quvW0>Q>OUcdFI3~GqY2N-=;%?<9h z2rx{69RfvIr1B+ddsSbJYI%RBv8oX5XH=iNI~4_-s5QFRh%6X1roaL)fPki}7(8bP z_SxgA0;9H#!f{`D9jw2;gX!nN^c|!6UBAcGRk>-r?-=Aw_D4ymbSwRj!3T!yL-?Db zx%aFe@%sodm_i7*jjo!PV2lGa2O(JL4@UQ)td!@Dkd^xb7K}lV#v@qL*8to@I6>f% zR(+I20PggtUq;I~0PrZV-f7am!$O6Vu4&%|X_6R~-T$(*^yM>Vj!4PAefzp!`NChm z|G?2FUq9$~-zzVrlvMiq1(m_m4^*+3Gy#wv$P#&ZJ<%bKIcKG6Du@+vg5u|dM!+zH z>_OFu(VoY2g~Hdv%b~3wd0k=AP^x=jh-AL7Cux($_2 zzBZc;PHqt5g9#hh*gzG|QULgD%Yzc|=OB9XnBqbXG~&UT7l@M3DdD&bSN_1#^e3{x zBNgX>507D4sMcEnEtt$3U(Gls!Cw>Wj~nQdnOzHdY~{0O|F8RfD&T`Z15CkT^)7^E z!m{z7H-})TG%-?=2;s96{pZ?2u6TUW;9dlIqCVR6;J%=LHn~zq|D5|L6n(#=>rADE z_psUr_s%8mo6qm>RnR`h`5|aG_{~{e!MhI8Jr80rg!ekGyYjp6Phfpc<_DH8*tNT# zLMuc%wjkVr0}+@bDhz=z!J6Xt)#gK<>KO*%z=QU8K*+io4t`T6j^H_!4;Ht6{)1mQ zdFteM4hH={kUi>>iNgC}A?%VYLt$bv42Q!bgTY`D;gQJ%Fq|Q&wX+lDu)B+PXG2It zdSio{#|A7d7}x;#U==L8i|PvAQb0CXLvYT`nj(ef6^BTc3k#({LR#6oNRUx&artP- zA2Zz-i8g)BN>4}SysodBC(@>cSbbG0it0zFCxTxOxk9Uyf_MNA^Yh<4tf!hMeg+~% z=bi*)Ek}77G6;JXTqtmWgb*u!`~1R~bPHtIUrN+G~*dt9ElL(JYCV=4#$)Ux^7m9LloAP1c z5#AWvP@VArRAvq~&AiM|8So(-ba_5KVnR5E_yGc0o=}@HdOsLsV!<`iFmYsw{Bd>0 z)gg?v5mag)XCg3X$E@toIWc)P{N}k|$|@`VyYc+NZx!B;VEj)O%e)?&;v*zh@?+v&MSukD`^N51 zyov$1GPmWO|3L`Ms=r0lFAVQ*YQ+Oo07tbR4o0dI3k-gbux+T;SGiBo53NMFamFim z4#I7=aAku#Z$Uv6TLB=7sbcE2OoNWcAi;v1)tBYqy>N8bH&qeecH0N%KmM`b{lmqD z`JWv0yMG}&*CX=d)WQ-x`s6;y^PB*~Lm&FkhYFa8lL=rrQ}XyH`-9&6m(TzP`a~G> z0<%HSy~pT>Q!arm6jN#rp_;_#OqtsIjN=P(Jh*!0b zf(O_s{!X+OSTELC4egJyi0Xu>b}hd7n-B09Z1g ziwZLl_c7I1fNCVjRQ|Xhz=n)6$hW2l`%n1JI=2Lt7QUmtertlT6oYVn5j2&D$Cgi> z_yn9Cf`9XgKfd=vfAL>`eBtELpOaV~_4pL7a{pIdbpKH{bjmn@!GcnE-|} zD985Q^ObTmSV1;8_}K@eC8NR7)19e70n*=jd5C5p$ufS>4(q74QcV@Be8(tFB;F!0RhiIlwAi>h`}QBkFMa7tx4}dL$zdXAPWpTLGv~eGyYhNxr^sjn_RKj7d6ey`zA(1B zGa&L0|1OKvi!Fi~fd~qMNB8Wzt)QZoBSp>C>KGNya;9>`CGB@)gA_*fn0}x?9N%8V zY>3$O!9GP;zH+4kqB6F9!JtRfUT|mz&s9ty!e!z8i?4sJyUovIqMmp^4X8E~C1R?L zpo$nEgn4nTbN-*8eoS=`qh9e@Je0$K$`1e)pq>EkUXio%XnThc zvmC4-OA}!qFzjQ%C;|sBi9GC@{HH#y#$fRER<2cGaF6_B%-V53Q{10~nbp-}$Fj_T z`ts|8(cnK7i;o%RN$8^T1KOaQ|fmg)0e zdsVGI^(rB1xekBoZGHMp%ply&(JOtv2fN=FK?J%KxGm8(%7K8(6MuwhECyV3MF>_xMVwHcDhU}7h+7er{9=vsabH~iRTO>ozJ+Du+&V#wsf(Xp{eGqJ zhfP~$Vwb83Rs{&Ffq=^SsE$Ng_iua-G2H{l0AVF9=ubs~BZRC{m?!LG3=Djl2M!<>t)UmHmRL+BA$x@G`pVwe`51>rmGyA&f zA+G<5`mDX3X?(0mn93C9shx zf(UH*e;}?9$Ij&AyvTHPRoMsqiauNxXGVLwVvK zF=2s73^2+>pAtcvFk~a*7!$5vB4RB>bREI{M(}YR*nJFGkI>OHtd~BuIRdeGV%S^w zH^uT_ot5kYm!n5_6?fhBmC61Q37PmXoMAa~;I`Y#(O^HCh>va%aCTJY@s1Eskp6{a z8O99G00?PIWuisB2rnSI?!JivNA!Q zC`(*nWlXI09mb^Kn3?$6_vdtk{U%&7BB#;gyzJiS0_+AbMPa{1O3u}W02L?0sE+tI zs62CV?}C2lzc6qU1bxSfa~n8@>5CJe)wmfpdB zXX{PZ)*4fhe4_n0IP#6s4xb;DDo5t!9%Ff;*KJ(4|y%IGV}F>*NCh*ChMy9Bbrsa z{Nu~G`=mb2w$aGzf-&{R?S()hZNlIyB5Yqudk|cQD^s~w{vP0J*A3O*Cjf3Db#0s- zDT8YjA_PVxo)lFpBd{n34=g?Xz&io%Jj0tzP2{-^KrZxY zqrOFP=CSIa6o?%Vg26F82!=C%jIywLqX8Zh1B%PX7!V9*G2v+|>)W>>Exy+RR-K8m zJLVEy-gM(tCIQE`ZoemnZT)HPg;g-&ZxfXWW}bea#i;YHPIlfOhvj+w;UBLLgFXVp zHw-KNOi||E1E1aXs80~I;X3cjur%I3!0~+v(#%ay)s?z0e&vLzKWIC=Kf!U7$6Z65 zFX&rzpAdXqu3o5Ko4qDX|9-&Q4Sv_LzkM|?PxC;Vy`SS=R10zgW)#^uzmb!t7?{r% z;>ohN{PO_2CIOIM08)dAoN*}@kKc_`Tc5ysah<>nlzD>axw6!3gBrv#^OD&C)x31> zSl4Cg=^?3Fyn1cRmIU!P2YBz!ic5?+FfpJA`Ua zh+yCwKhySgU@$n2o4bR*C#JoyYK%(Sd*KKC z|6yM+(I!F#;Mlx@&jE>L37GGJa}MOoB+YjNA~5i6uyxzKk)vbq$(LiBG!8r*$n)2a z{5l2iDgp%eGP-Velic8>p1rcPNSIf|m}6FeYiu#DFJoFZ4ORh;7k;zYm>uu8x z{9-%^a1BDF-!VwQxSUmNJiXFCjFK?u7bf!a0#&6RDej~)M}RQ^DOF#(O$IBHKp}41 zD_X4dgIyOlD~&5!^|yVAH$$e&i_hPTe2C7{^KjB z1kHZR;=cZ1{uhhI&z@~@7bfxyWnz-}hR6qtqQCN4Ec+w-;%PdP1#AfhGd@sZkQ@(q zRl(q6l^oS?Xh}^RQ@{;*m80tC`dSapAgA0Rn3blK0k3Pk!i@8Z$_#W|em6Nwf{VC6 zd@vrTq;O1mTP*rzF#N0bD^^UfNlu;T0jCnS_?o9laT!{lSdBl0Fi7_gV73FI4hSyq+Tfomj&<>8fkGx#v z8u>kfC_ldu=z0ms`-Wjsr61{c#DQRdVZT5BtNjC?dITo&f{}?y;u|Er!O7bi!;Oz+ zwaztUhESoR(QM+POccUU*VeQ^gG;7fVP~2kW_kM(v-V?O5MD1<2J2M&iR2)ugevR6`;G1~s8qyf%u2bC_dqd)w2flog832q}hA1si<_La?CGY<# zb;fNc0bA6E;2Ohw&3|+2YY{UV7C43M2aaL6ggKW%FC)=?aoz1EPp?-nb-32O4wWLuiOKR6 zHX)VH>PrBEP?4n4qT{&D1W<}+%at~;dL2Qn=*!0KVxq_pV+5W6tJk59?ecDRy~x5;yZRGM2nSB_y>!PCo)v?qy1f(cES7FP z3=?@l$pkQb;{;n9Z^&zn=^xDUdglEWY6`QF#j@yb08%dBB0n5!;$eZJQy>5cV(Zr%(6q zrsPulOF!Q|@R85JL|$+*F-d#_r5r5ZJsK=Nme*U?&}s?2w6f5q2-KXOrAxUIQslod zD=Fv&Cews+GSWkTiE3+l(DXM#byb8N4vm7g`5c9SwiQ{gFVAQAcEGDTCR!PnzM6ys zz6A{)JlH<){9&J}s~>!SVuh0i0I`BioJWWnirc}ktZ5g`4ww}+>xduIN0Gj778-hG z#_w@t7>F;!K1WdfRWQD`;9Q)5YtIVYOGt>Oz(`WD`u@xE&ndoZ+(``A3%3-1%|9fB}9|s(m0xS-M zg>?s?7%j)-fBZU3J7IYSMtmDo*~vk7{>O()ckY3SydY%)7`~x$ux!p=xKM9z|A!gv zUTJ`W^I@0PtNZbv6qk=&TPO7k~u=GO8wsurCn&vAR`CIUMy* z{)grLAO0XrM}8d>6J4? z=7;gZ2k9!{5S4sY#h>cCE{m%-4WXhkt7-f%r1OG6wN!g!oG`A?mq3 z!Z>l2GN#Ui@?W>v!70Nq4Y69b`kY`?XEndqQ%s3r+`jeqF!c+T2LZ-12^IR^zeoDB zjQZj8`58Y%W#jYwo}|iq(~Nz-LPc%!AV3_!(kXuKvrty9r{c_r)&B7I*@Y#J+#i<3 z@bhJNz_E;jS6EDSospTc03R+IPm3K$XI-uQT27U#Ul3Yk*#lFiwr`l|LzSYp?OL!JwC}!_ zK_8O-2CR*08LxQbzJ(@+SQ+c{`fYVkLA4{I>w*|$1og8xFAPwCtrx@cqGbJZaU~e& z0DhgUAN-Gli?UcKx<}r*bodhw!9-rrG64+Ff#lN{oockV{9u;VJ38tkfd;$fHQ$Vi z4m04c3Engju8!cQ353vW0Y01m%Gd|+MWG5bsv_cVj`4_Q&A|b}dQryOz#**5f{qjz zO68B+8V>?a2N`%!!kssH8@x@`D}+_RhXrT)*v+2V?e-E4<`qdOJ4lshDUottkD?#X zMvMarVLeIaAwf?uRT?N>e;$+lSq}pe)9|4(kJ?JcWhlp#tzJ{LEw7}}<%{=hux*wM zM3n{zm?e^B$v{U4Hp1VdRC(>n3B_`vzAjODCeU;(_Jt@Ns6k!6grfJ~Pt82^0XTZJ zfQh`IWdazU1L>c5@bJ{uSAT0(YhIqIP|NDWE#4v0r3=2RrT&f(F3*ge z8)Lx15CgcF4YrI&AYcayak>_<@5IK>Z81qxNL z;z48rEMVOK=(>YE5&MC0Ij_uZfHjQj<83--1UY-+BS;be&>(^LJ-Sa(yT1M?KPIl7 zl|^Ib2-0!zaJPHxXGTxmIu8?h!OH|NJU4=^%{R1awf1*_sO1_ofMFAuNkYUdf?%c! z4-h;~2pI)COmMIP0Z53?hiMfvMYU*HrlL@zveg1>B!si<(=`R8X8G$idyLEXY)lgC zXEMft0R|eN%8Vcf72|sm>!W!GjXAkmJgE6C>+8^#LIwH^04 z?(ZsjnPLpf^YjQDB=hj=7~`*C$_WR^^?7STund!TQ1psg`)8P$zWmei{Yz)w(A9_e z!I>=Lz)d<_y$G%sT$?(dfH8gz&RX!RY^(G^jEZ@wqYQSv_es!MC3jJRP3_qr}FV6rz&r zW&u_g@k}*8YcDo8the3JM6r5)XXO!)pv99VDrKwZFwqVV@tdv>#xMdHolDaH07*cR zz7+202LSPPBde1sHgpP<<;QXTz@VK}+hHFQN**v#PgFhyGE!i{um={X3kEI(hHVMp zV88+eTc2kcaE$MQJ%{YOuiNy~U3M8Wr81b9GL9YlDAH@41o$oivAV$`qG9V1Vt9FF;}%U8_=tJRB@e4!E->GmQ; zkBKyO6wL5V&KQAI6tvA3a32u}VG1kglUI_VF99a{1*`Nez7H`lK^W^;nFIu3AIJME z=)Sx3NYU%Oc6lXZ3G7lz>wh4^>$LzN1vH$`Lhvk<5CnKqU;={cat@Kmqd}@a#8Lm2 z;^>}#I2e3#2_|yR$OJGvhtk;a;*+&{=Q~AKpR<9S0{?`Jub>cP%@lz@gFh82&$vu1 ztPVABSM20oQLb-hzPgeZ6;v~(4zbF)wMwtcxo~`)Vf+YwZ-C9l{Xm`0Up@Bu!`4^p zXUH<7lpjQCSSdR`Zz3JjdD9F>RoSkONkEp&{;il1_<5{=h^mr~K}`$_xDtg`*o3%D zs(y^F86j=apg`?K5E;@!nxQo>Fskf3T{fxC^ zObg+4D)1odFTGQw1!(GAIeMt*o&2Xu$3HXKiSiti31D~*rFXow(Al>0nmljbAVdzC zi~MNWhFrb;)?Xp^U+|_0fWS1t*$MZL2#%903Rqf|ZHV$4JaBrfeyosJKVL~dZPsDD zxnTWK8?7KbHzF7)@E|QM)jS^1z#3)Qd0P#}b%)=_Q};YQoi9~)oG;B<7{%$5xuR!E z+~z^9n8R}^pjBlJ)+&?oml$)+;K59q{lK7J4_9E^#+UqU=-4^GBAj*Yd|HMx&?f=LZU?S(7OaQ}kD*Hx_&K2|Z z_RNn6$ZKH3MJt7z`C~Ak0gRGWU?Fq0SRmof^Nf&U8tgD7s=y$TIb;LIDIhWS+}fZ* zM0wW$4hpHjI{JZYoR7{#EWqi3p=TrUy{#(3n0-ho#IWqR@300i&}mhi750rQ$k6v8 zrGUeBg0dBmU=@@*8+;G@mjViyxDFiqoplJj@=x5)F$zDZuTl>{pmd?yjyD0&?^`CW zhou2%r4OEs0Q4Zkf&~D^NLU|+h^fd-egX;^ixioOD#&rZ6vhOhzylTpG>T#O*t?eZ zf8;|jk#kZefZ;io_NFUNiM(}vUTa?|Sq70QN`db)kva2#bAp6esataePYo%6kZk~t zEYgax0vs@X$_c_MTAY;~x4D%6zA9!&S6bDm$u6RA>Z}q2-x#wHnjTg!SNR(JT&Gt@ zb*9YEMT#~FR4@rhIKWAR0G+AICh$5E?IS-o9vD=BLi`yN+%eb<}T`F`o=&X^lOuec}?o%z9n82T#*r~c>C z-t0dC>|((pCUP#x1TZ|u(mSzh(44vEc;1@+(M;4b#Vll+VX%OKi-ioYu?vGU#F^+l8Bvg35 z6BuTRlANQ6Qk-1`g`Q$9SqJX1w2#Ge)r?`K{DB7T&Y8}88BY4`LvkksqC958z zvJhQI3P_;u)mYE~5n$nEx$@UaSMLNHDtnb=%NVCaVfx^gRF(la(AVt)HwCPM@IDNg zFl>Y{bqHpbuz3vtu*?FbmD19XXQ+cZ**-XDM{SZA4wpZ@xOnOxL-((GFp+asCV=6a zAl>=JeVr{YdV8L?b_lj9m0NCU!GZ8hf>X54ArkzjWh9a-A~-NGgPhQ;1^O&up=Y5y zS6>3j^YL?IYh%Y}!eoG})PDw~dYRR-7Er)jSomOA;7Vm3U|ZzzE2!WtQN5S{o>Y^@Z#LU!foza5BKa{ze)xco2ks zm4tPC6c{={LU4V$tO$OOIOc)|1Vvd^z=<#2vkAO|J z-t`Jb^+#usn|1)ID_g4d#oz*hc4Cmg^QF)6?*T(!QPk`cT@Dzp(0P7@bbqi?|9Hi| z_W7zgBnBW5mBC8is{INb6qRxpF{tr2 z!Ig}@hh+dD@Bv_T1HyRI!x%<^UkWhYFApvGpg#G<9X^zO0jY8)w%R>H*ByJckce@Aw zVgK-_KMNC?2uuLOH&ITG+M8ZhW_j~1l4WRkXU7~u2gS@B@qfLd_xa5EfK}k_c?HT> zS=bL`1=mub72v_!Sak*uYcL9yUKf#Hgjc%H1UjrPtJSzV&`9U!_vUoUr{NL9qaGFlzIRwFjY_ViI}V#D{Mmy5^zjELjdA>Rq1;KM#h2~1^}#*XV=O7 z7s9QuITN6B3mdq}?JgPRMVrs3I}=7w7mv9R?o^4W{Sq82VSOoNLgpUP61>ZlUTRqu zBgjPwqjDfE5hwmdcmF3o027&rkqKbw0e~%|G>(rBXqe@c7BFZK0V>U!DhS9lYly?)pzL-RerI{#hu;Ii_L&no z*JT12zKPQ7J=t$fU-5)!Our?|Y8z>prDqx}WXK{1H+zn&Qw|zz8ZzCBoU{O_unDo& z7H%#RC5K`F1tP2qSlA7&R>@ISd;%c7w*@B4`*acI5wN-fJza?uPV(GZ`E0tqf$Qln z!uoI$t}7`xfnLqB)~XDw7QU=;+!4w$JWma0DV03b*QU$3vv1x^a``f$+}poGDg1pCY~VAt&(9&1tq z73M4n^OU0(X_Q+w|I1%UY>Ok^TVGMLCWRgOLWNB55UCw?2tk&u7#T`trg zc`1W&R&w6e!ErPQrT(G;vs%Uq8;}{bs?T6GH|oI}!H>B%EfxVBOW+#knBqbRg343} z3&P(zqE0@y}Ul0SnGw3q&E`L%@o2*CuJ|GmOZy&L`=C?O3N>^sKlwqcT8J)u8>&xcj^p!AzP zL8@P@?l=!(3>Lz2!1j3nh|Ltt_W{D#3^cCjfKesXjLQ_6AnI8On`U$PiNE{G@;CnN zcgu{A7ch~DL^5F*zWIW4`AhFF8{&$r-g+?wWw53P0018FNL1Jat8&Bi@K`WbA&R}g zj{RY^E`1(tDK!MS+6B7R8g9+F=`w=<;T52}raJA{e%}xL zz|Lbw4t-|#?%gAp$V7}x0K-`zOG{rH=FJQ4sdYB~ury6f2_4BPP~Q3|^I*Y&1c(5E zfFwBJAF(l=D{xmoxMNO3q519>ZeXa|GG?NvEXrW{;&aNh!SYzvdYpg3`s8%sMck=p zTF)60OeL(*mq?MPO+aRDALNblS<^vKOIqnx{YfzRNP&U0b7QIvt8k;fIsjtQfOQ)pvaU z^PgIViA)40fZ?o>{=$PNTAQysR?DZ}Cg19sQ`^>!nL95Apg}lAA2JMj*sD4>dWB>2 zXF^m}q!1PL89FGkwpcC<(&BavpkR3rJU9^!X+&KWfj+1Lku~+XYlT%3l@a5!Cac0V z>KI$_bSq6{9RmY=hDaf@epPuKtkfOm;O8+&8_y;%uu=gAzKyUB9MeYi90uQczNkMz z|JDWyVS5pXh#3K=)IBJ%o|JCnC{h`9fz&hWZSIA`#y8q_!Reg()_12AAi82<#j%E2zfRNf`jnVJ0s;XS?VN$1_|Z4Og*&&wk@-ah6mprVR8El@6eVV@R{NUsc5b`r z>MLJ-+n2udh2t=hb6O^V;jEI!i~8JE57%m~w+LD_VYI%c|NRn5VZel7rUV|SDY?wj zeGkDw3@j`#!Jv#$*uoR7BDGwSDOUw9JkoH=i09`@IXDOWob8W4_U-yoJn#`e9|uI@ z*EC+%0tq3Y@b$7Bta$AI5TAE>g1f}qsJ36x>8s{XbP6m$6Ye;yt6DVn?eN{T${8zN z7;k!D6+=`<3^X9jGxjqHnlMo}H?g4W1789;4x|VO0YwNV0LGaXRG4LZH3$%FC{;0` zl+3Kb>na^XWS36*SJ#393tGbcD0EB)%|=jQrVDn=z~B3!*T8w(H>s9K1wc#MCz{Q= zW+KQeA?s~5>Y~+bT|9#PTV8zib)UH9mYYw(M9x{60EV+nddKfM+?ajwiM-x=yEG)$ zL}Y$&j02nZ+flj#2$%@Q1~_FMF%A4!Fjc|4|&9+DPeM3~G&)79_r}qP)k5b)xgft2iSD z-?`FE=fyaUE52l%P@P4`c?2Z-S7ib!zfo}BUN1nb{9T{fxn!Rz=Y4-dCGHh27^Pe_ zPeAn=BC~}JgcSFe0|ouRSNe_-0KWnS!TTss;G__=_!SU{9xSLjjA=m_e*_!Lf$FeJ zDK)?-)uvP$FqE`x5-z)F1N=Wf@;cbKaaO%=-Ckd^3z=dVsP0jzf2o@M-XD$#Jczm3 z>Fupn<2x_8`0`KPe*0JEVIt?GOaQ}KDBa^*A8BoxzCfBLUJk(bjL5uZXC?^|`!5)# z!DRz}Y{~^Ecpd``3{#N_&lYfn$!GdHbTxX`BhnvLAA?Xq+4H)4=xTSMf`JFFD8gBM zXs`D>1p;FjSbJwoj5#KNG^bY{k_s8BX%?APvLb^+m~Njdx8V1{MAk6?!u=j#bRe^x z2W4Z0ZS%xH0U+gAXi5P<3MlZpQf))nUeG6ACh|)d3lw-=SOq#5g988&0SxY=5MHZY z7Fn`978r!)3|C%&0vsZ!2f#*m)BwnvBWe^|L1++BNM_;ME4RUq{hgbj-e|z$%24?& z9AD^3hCx85Da{Z?sQ?6lee!c?wVJBEM!haJ%*}3^-8lQk8(;OhJ3jZBPacMeoO3b( z3}>Z0UbHt{|Cq?~w`5s;vsyL5oF9EKC$8zkO{;?0GQ-?w2rwim8*6E>v5t{y z374+UoJ6nOz~K>9zgbACW;Ps%ur4`;qD>e2i~}|)aSF<4HnEB}t`>xc!_f$u^_m0%Ika1KSm}>o zu{(mgOl#Jqd7>`O5hDo}x&voMq;t{?A{veQ=8YRSz4^*3uDtJyU%dG#n8-OJ6Toox z3ieKX{gLK|Yqn>3>qb)Fn)=Tm*s0Q)EcAEvuX)2oMeEx0K3Um9RcoAW5RvHt7^~n7 z16B$7rIIE95O@=T4F?^}{>?1a4yuBs2h0gm9xPR#LU6X)lsXnkWgPS(6yAvdTyPFP zt_!0|Fj1ksGc3ps@FlD0krM|uNx?Dg>vCgF3A}9LveJ21yFUNve?N4;iMvl2=Edmu ztbz*PPmIcjz{Q`d27L;Z|2XC#oo?$7>v4d_fsZhfVxuECRAWV}^i2f^-QMY6dLGzZy*|p$UUwDVc_v z90M}-*=gl4+o{WZvIc2pn`+inU#Nf6ou&f+{r*51JEo_nW*eQ^zkT&J*FSvTwoQ-i z+O=!kLpzZdkW2u>SuWjL`>UPimbYbDZJUk)DV*6rn+{kU23N{2x3pP3(_qu>j1yR1 z1Wrm=^8l^-%gpSQ>zT|sWa>&ROW@cB*IIfk!HR-q9^`DGv_kBIWj$z+>kzc8QnD06F%X&B98aiva(SMZ zhPj!k#(C#$`@So#yz2P1FTVPon{U2(vQgCwTqc0wY?q}yD|zRVr}9SUyW~sTG&F;A zg4AHa>~aZw?g}1Q074il+N(&v1QXqPXHsC@5cbb820oaxjk91W?@Ux@$(cnG3{(ga zDm`8VfrFNyDwSTcW3Q?a#w*R#aa1WLD@{zctDs{1x$s&&z|naag@B8G<4Lr>&Pn&5RYk-lm z^(+H~_YOE%5T|FH*l-Lz)wodqX&Ssw;s&4ttL+!`9grv>Q3}28CoB3V8D1 zqWl~|Ig+5GuKgEUl4T$theEd1mw6iXy6V?Zf{9boTrt&Z)HZM4_=e@B-tnRsJn;DA zlRYS2$T9&8&jab7Y(CxGuwh%4H(n_yw2IJv3)b}D#Lf7fdJmMoF2e>?chaus&ccCS zu-b&J!Oyp8sLPz`!w$~APW3NhV6q5hymn0OislE>P_Nka|9m!xtJEDkV4jXt9XnIX z_q1zWXCAI~S2^##^UoApV9&>S!m^w>&~P=l`79q}c>*SZEAg;?T^y5rSraHA2B0OW z)Y*4$Qs8Z&`5z&@jFPp*>J?0xBUAPsEZA~(Ux)%k(C^ai^WaPss_nAUuJie@_0q<4 z5-3PD4#Xc`73Xg zy#2fjYOj374Yx_AVY2_k3sfe6;dvnkOK8nLP^)ivgAldzg*8nW;Gn1PmSy0e!6*^o zbib3pS3m?RLpv)9&p?E9zg;y^n)XZQsOSe;Z>DfIZq*e#TZLeT&2^LqCN^EyQ=(xO z*lfqy1lWCEoo=X}8wY0eJ-5#TR?67? zUfG*}*o|mrkbz(P4T5;({*T~Aq# zl*!{o7tKi%#7@|@bw<6Xr0{K^w;5@QIB#PcrsQWPDGPFb?2iz-l3`H$OepYBSLTS> zskZu5>_$#88fub_sA&d5cBRv7DBz&)RR(jj)3rvUcGGgV+xpZeKk+%3$O}>?fZ=%} zqs83|+0f~TL3c4uX zXD_xd{Bw1%kOnmVK6AUY5MjA0DHZ!@uyexxZYH0AL{8QjjH!G89K#^^S$lkKnEoDs z4;CFUWhP+9I0iI?eis}lglQOT`~(aP*gVF>U>OIr09J2N@689cj4mIWN-%y$)VEUk zCxkTW%S^L|#xb0SwP08Jyg*uQ8X`vb_B!y;{QlSyj&fgku^2 zyr}{p6|jjMnov)=oa3S`=9f?UWtP?`;hYUsA~ynr1HR77&tgJVThDUOk#Ha|ZpN>B zrfaMMhxI7)F@OT2V|Reb^0t2|b|Q5Ryq}zH(cjrD-ktZ~qfWW+z7H6*<1rgc2Zk-( zwPL0E@HfGr7nWVHzrX24c%cb~p$fG4b#lN1W~R;3xspX@cChIj81S}O{{lvV0go7j zU{t4N5G>e0yXONPmpoFV{X-Rv5zIL-K*F*RKCgww_Iz|9Wp799|JK26oY|)0i7dIP?Nx+(P$`u zL5##`G*Wr#eJ2M=WzmqGTImnf6VQ|_L`P0prrUMpGeHc+RI3Ty{!kDbvVGfmuiDg^ zz4+pbcHVsZ?YDPfA}<)30EXw4>@Itqsjs$R_VpsKU!?64nVF5G{2D46&*ZW8R|r4= zA8cKSW>Z&YX7s#Pdn{NRvAMxd4w3amxbt3Lg$i&Sr~u$pF@PVuR}-cnVlX4(^Q)J~ zRl&o0&aEY>e#Yr3hQXgpmt)8NXJz-;Umy2ZspLV{4l?_?%fkDIkc!6;-oxRyt6lJM^1vxSj<3J!mZ9pYK#IPX8NA2LaxZG3cg(^c1m_nH++LBr5 z4GWm1C6{F07!sh!Ra>=2^Xd(Aa~ECzlIw22`R1D!VInUGnE;08n=I@b)n+bwB+Fah zEwb9Qw+~oWAp>pI2MC=Y?WZ40}B&5=-csLNoG94Tr9 zW2a>mV>kHI+PGbKD0 zDDZlH%mlQ`c_VyyazAU3tZqC1^Ma6M5dt1TZ`gWpL`@ef7>|c~)zEs~}Bp zgE~2rgs7SytY{?Ra0Wj?>#Q>~#Mz=QZoQ+l1)~x4(l9l=mKc~QobG4J)axEJ=;Ls9>R*1`sh> zyng`R;gH`KZhG*m7I@4B@9<~=K^RBD0tmF`3Gj}MzMSc^0Xi99QDIFRrCv#)>l!(~ z1u#yI#5io25^(ipE!e)bsn`GtvZNi5g?cA?{gGq2S{kkG(p_->HbHF)y?A8b9l;oFOB z-{@5ZNomqm*yxOH?qt~qR})v(uEQbXST~SZk3@AL?-xcnR&STDK;hjEl)pi1f&PvG z2k%2r-2djh4GRFh)1PlEZa-)hG1<2J)zJGqRLgO8DrKtBzol>hz@FoIOAizhKqXx_ z?=SOT;6TBqWB8pba|;T5J)wDmhkf<{BLoZJ_J%+ei-CL-JOJ@+39xKJq0A69dEeTN z0xsEEgG(-yK%uV9kElI=rQ)ZhjRmaq`w9>=jp8Tib_Z(9>RAO8N?2GLNE1a}IWjKw z25M)@{-99t7;*^&`uzg-A6ZaLLT^Cv95M$?C>{gVLGc)9DQ2so%^C-fpMtF$ItrYO zWQ9}hCLEeyQfvq9blIpicAmHM{I|X6>Z@**RKp>d$n#bvfZ=&7M+ar?yj}HX`)_5U zw$ZvP3Jw&U2nY-aXz67J1kC=_nb~#Hf(i~Ete~l~IuSPmqhUDsGQY4idOcyggb0FD z=B(|KDKS-=$_hI%4i$nuM(`B+>S`N&A9=<=iQ_8DG*Y0aEqMBQ5LZ7Rq_Yb{s5}*3 z;qt1}^?O@A7=>%kvEMVu&p+8&49f_v$m635#AJ{I=a$72BCV4-mJYBR)09cRWQ(a1{JFbA-8Gr)#vy{dt7)AC0; zBCtU3FF7+3E3hO@3mwTUkRqpi4apAF)XIoLrD-LPnmM3-Bjk4i3k?-tp{73RkJ=&U zN%K0JRY^jDf?PR9lKBGFyGg4gdi_EbnCM&F-&9_fiq$l~zXmH)MG88Z`HpB{ghAaQP~wW&D{WU9t0z|IE}b(0 zTp8Qv!&d_Kw~ZME#MS!3+ES7-X_O(;5SbUinicrW9~hIsz(QZZbu5l&wf0+t4bI?}S{hRTAJhSunV>q*>LWj}EAmr-8*mK^ z>OqD2D}6bpB9PD0%m9z3J-brF+kLGw7<#pV7|rl)zr*XPeq)Lfp-P3gv`FEMsVz*y zx%oSu8A#86|v&vj6(G~5XW4vsth4EA?RC(6~Mw- z1;O$Id#1QD=U~L=VeJJp+w+vzH9I72BX?to?(sajZwE`(>G7-=_PGDeQ z%m76}0hMZMsif5BFeS|p)UT!fNS3QBc7YT)t%A_L4Jes`p*kiLga!y1%rErd@dGQc zajK?vwZw2oVR5bPAv5m0CCSS==~^8E)EoPxiVBM zzGvoUr`~wO4X=3YGoSg?<1mrul}rG`^Ievo81_%?`f6?J(uKU%ezSZfa}^0vyAB3f ztr;Q=?F-;A1)`c|sJx*C!wtSx38=Z1one9Xci^)>4VfKvz%ZZ2iI z=B|hT8(s)T$1XwxYavxGRQ5V>j#ZW{A68o(r+`mSQb1shyj*DpJ}Kkwi_c$mn1ryL z1qmg)e)J&DihC9+K!7WFQx+q$4*F*TDqtjH9imVFL&PitR6znTM!+d`4j4ip!6PvO z^0*6}L9q3+FwkZhr3M8Y7+7%Nfr2wduun>>=R?@JaRuh4deCX>_y`0J$lO3H7bp$_ z`6?&}N3)NNQu4V_OB~6RK<8Kp-xn$WAH zh)qywLa0>`o8~$anCS0RKh;TQ3}P4ZnoggaX~L%2w(O^X?y!V|^Ie#hWgp#h3g)I8 zs;_f~>8Q)TN}@wZbH#?)*_pRp`{I{;{d1rDnAZF z1PnB_Fg^`Tsc_#03>Aew?adNukSw)L(&n*C;7SBHn3v#f&|Z<7nI{VcZ3k_i%PSs? zzBz|H5d(num`gmfHpe!rJ$5D?+K=Wr|YDW^|uV_i~Sb3b762k04JPL@QOcX z9qe}=&N=X3U~Gg13Sig-z=DAT``xw~`Y;qK2-6U@OfSJD+xk)|OLIdrS2KN@ol`KS zp;W}AtZ9iOnG0x(>OKWglFie~1_Bl2pO6WQ_5qrWhBHmB^aoPm*A>Vp%0kt>=cqP0 z?AY8=t0{=NpzA%cu&e;bhS`qF_w?Zv2@*2pI7#ghupj_){KT?kFj|WJAkabJfkIDj z-`G}u3mYYnpw$v&CZYb4se?=)bj^)MU9{Tm)|M@szwdRgef{Bs2M;`O@Zdq_ATyD( zRVIMp1tbzQ+}G$_`e;^by+w{vZP2dN08H`PZF==!2jDysQ4iMI%>ltLkGH!~f`K3@ zrRtdnwcOFs0{R~hCTt@5zQi+oUofbDyx=y&oew~S2L#+#!6|+EED^k~L+SIoyc{qfc(a24(|!mAXvV^RGABo1%mof$ zwy_A?XP2b19jeHUG{YxFP48STl{kS0RK5xgb%Psq9o?0pEsj*MCbNuM<}Z2gw2w5AXCY)6Dw-FZHf~?+iSNac%bV$x!4yqc~R}o z)N8J|@{0b_;=*0KckiBnhO=BIfZ+uuy;Ijbna>n^q-EmO1p3ij#HkzEsWa5 zs4REbG~AejVpW49{7Bv1`1r6^Oy_VUxAwy zDC(j!KaBi21r`{q5c@6AcUiV6mSA&d9%fpLl06Vg8B>2q$y)ye5~Y;Mak0##Il-)Q zP+^{Pbq6%S(7p{6bcxIgDp-^8R3;E%t&4-9eqU*2L?&|OFiG(gY#ZGnsPfx3bYQyO zQqGwtmxs!fpq5r*o2>=SjMK z4R$*1#?;hQ=hc^9eih74cka3GzWatSk+W7NfZ+uvyNdpa{NcPKZj+>|TrDkbBE3ssbb>Px;F-8Rkimi0@h>CLL%GtFBUZHd48kYy71^xN) z3X_8cCfun|q?I+W`{a6QOeS78j&C=8yqcu?;gu)E_h23Ma{qyN<-_W2gIC&+a&%6m#Itm#pq8h~U73?*rjX8JKWRG@t|m9|gu3xcmhJ4-5=gpa44h zqB2F4-VEWK8*Lyb#W)y%QH(~ST*x}&0Cs9eMWd#j5lOj^R4C?}_FJHR9tI=jFVG+- zM`8(bZA>K7LT22PGNqi2-+}Q#XsGj<@;}jvy>uH&sXZiAfG1R|CWx(2V5HFR2ALi< zOt)0fCGwYeWX}SeS`n~iuBOftQ$cmjFOZ2Nho$A7G-2onk_0daSacfX>yRs!VK^L0 zb3{$GOOuU(yvCl|_$t?a4YXpTMw?<~EflqzHgDQEbLk})eetVb{b~UdIV)uX7+#Qa zupFFvXfM=v-kdj@Z<9mKtQi!wQ4@Ut8K}^@UWw>PbHWThVA24%lbfA+It5|}Zo!pW ztl-$Jb&Y(X5q&N3L8VyII_eOsW(=JKdBy8!2UA}X*O@M`j`N`{!J9r(ZTjEL|6VLy zHdFHUnGmcTIOSXg3$`ag`~jDTgMc_4R%a9fHo~5Zm;_#*WewbS?vqgJP=xkacJu8^<#;gfykM=(P(d@@g{ z5Yl=XCNTikDN@I3$czJ}-kXMe6DWG)r8_zbh_u^H6U{Y)6BPVO_GQH^<(!>v!;^~@B)_M(!&SK=C)7Nver%VMc=6%8Ze^M%|OrSZb#LEADpc+;1W36 zMdv7)n-@YF+zjjjU|cSBX5C;RR=%gHkOQ5I;{k^4BH}vwlKKOP+vlLkISK~HunHRh zkOCor0%oGs4hGla+c0HqgVA+rAi@D6K-D}FDti7*S+I~^QVCvJn>-jHVj_m}aIjbomw#u_-~FTI z;@3ab?-hSjtBI?nIp7Mqql&Q;+9W|S6!J_MrA=`XjQKz+U0Nz#Gp346fd%c$K+cW? z3iNC}qj(1jNYLEi$wNJO(fKp#_fj)?DHOw@R1<&He$Y^C0NKsK9<$nXk8Z=bD>N4LJX}*|W?5Vvgwr!k}3`JWxKZ;EEkAMl8ET%gx<=;dh zrU}%LzXVBv`pKlamivX+xMA)!t!DG0m%QYp|BrMp_){y1=8 zwQPb@$o@V?4SB^45ay`>2nGm(`uHdBTa*v|GaUFR1pW>23JJ4HD3lR6 z(7p7)Y*@=yYWQU9rO)ca!`F8|VK;o$pWxciju?`TmzJGV-J@G?0&zgW)Rg@XPWl@TCv6-R*- zKA8;&9Bd*5-)TrwL8(_l$a~NmXdu;SXaGCX+31Bz}WVH0;spiZT_sT)?QaMPyC}sn~fz~S) z1WxOMo{`!?7~`3&Q3^(hh)9XJl^MYN5HJA@f-=C&9ihUDK?C5JJj}--Xa_Yt@h3KHSjNReGae9HJbte5Sk`zU6{-h!Z04xLpec8Q^MjiJZogq)H^ru;7G z_ju~*r{PPt+zof#eIFb@b_{ymE_4S3GNf4oW?_hPu+(kkOTG3L-F|+>i?7^%$6a^b zHQDL%ERoZ)5KrX!EwdNAbyHT~`bV|artcAX12fSO@>9(`vW%RMY9>%54K$ht1rt2d zhMcBz?y)h^o!q_@e0t6!O}aGDI$U@d;H?q@;(vi{za5C1K4Ue>iR}~n3K+DVu6VtI ziB=ueu}a>m2}U~u@_y-a)obEaV(Ox+Jre$mV^Gkl_NxCwvK`lBA4$@l(`irv1jrp= z&8Ls=z?8oG9pkieJW#+ee+V{82Y{3D7R+}I5-82CV&DM6r-hD>eAPp#!Z=HFMJd6< zv9jpjCWhVL8Flu41&$tF*RG76PUjWv_Vk}LnyqV^t+r^YEvltWztxh;xT#|oNFdRX zO1f67n+>Ie@;N9lQ!{>Ln|=IfpSW+4Yd8W@}sEhC6MHdNhXZt-Uv2QkS62zpuT3v1pT2*8x3J)emCrTUP}O_%oar<8I^omrt3e+N{o&k zdgyN+y6cla^uZ6lce2yvSs)XJ;T)3w!V}$|*!D>|z+5Rr{l$7Hwwp}QpB?1QKL{rH z5js%dXl0`i1ZOL-G|Hp!J`d5XF^omP2Y&rs)gQ!-VEKal%vSB9GP2K9xk_3rQGVfP z!}>cwsHkEoD{Uc!iZoay;2b`Mua~W7feP!hTHhf!=T~ur?E^5@#sP<@il{DoB~T*k zE`JW#^~Orw^u@Lp_A!iyfMY(Bz#z!OV;fl01OLueN|Y?nQo;`*>6=jMJs$Kj%Axg5 z5XG>297l`4IT|eeD_mTDZ#lg05m;Fnz&d3(93DUv56T(&Tjh*@n$WC`tVqCChhY|O z#!ojsvzt#DX2C9Xq|?MMsEwrv+$(28bk2@sX6Ow@>V6SJL3d-Stz#{a-^9Xlq3TlG zRp+@2-eyfAl#d)ghqQrJU4smA%A}PM#3Iz|X0jnPBSL+oRvv!fUbySduR?Qr3tWEH zb#URumq5GSRzO8hPIQ=Sa@#11fo65gdzR;|YW}I(?YZkW={V+CKMVeNTppbU5CeiLtKh+BV9H?6onJj+_Ip4dhB-Dn zgk}bBfubGtyPPtRrnhxAVEWiwz5M%>~ zPp(KX&`=vtskjTx2J{C*b=Rq#jdLxv4}}&1hvrw*N(dDJ5=|I)6(U;uRdgxRWT_bq z6<%8HU|C4BM-OE4((G_2eDO=SLf)K#8*h38Y}vX+_0?TNW>!K!V0z zs%IC1DGG?Z>D}x!0264+vBBCZGi2i_`k8jfAeeSBR`e-}0Rh1J6mL|gSSx?3ETi(B zVuPx+0U_!ej_FUjr8P|w&r*>8<&nnBV3tvW@=f4O!pH%G(3w$w2g5*+10(q+6a!hbdl*N(58`n7UpLBA z?;UjSzg42|9-IM@Ov6*6mOUaN!M91JIwh5-ARh!WJCHM@QpQE06un{;vdlzo1O*0E zEEE-hspfXH)T<)&JtFWi-L9*Z3Tl@bS>JG2C{}<>0li_N{26v1S(1!_P}@}>Ja!7U z$T@}eqgYz$D}t$Q{1%j{qL!-|4rB@;a|Q*J`s&vogWGQTvSciJaM|To!KO_c&1PA3 z7m-r03ekYavJ#p-$@CsFbkEgz;mw_m)}|Lt&1`v@>SH$^6Z;aru4 zg;D=x_SO2-tb9oo(;!C}bld_sb#CX0e84t&%&~|Dc5Nf(j*ro*3%e%(h7>+$FLXK9 zmhdb{Rr%sG_=6w@LJ8hld~9 z1zWdY0O#+#Q29})jk4&sP%AqcZt138zNtZjj}VIr%uobKAka`2?48W>W^2Pma~ro_ zd(~Ap-gC#TA3XvSdCp}57|v-qJnWw;ZmCbt)fR*=-QOPKiT_wG-ur*b z;i3KUYKJo{(mFACNHPW^X_R=KoV`m-AdeVWpcy_XXZqd9++fdiLufhD)vIH!T>Pa zV}mk7EG>88p1ZyV$4)N6rI%d^Q`0k!HK5;(OejS$l3;)wDVyrM+b7F9M+Qi&0zst}s`q&i;GHII9VMIjmQ{yH z4k(b<>HDYuxB^QkT<}XIW96ya<_?9i@=v8PXU9M=S$|qszEu@G2Xf^aZTYnHHHAvt znFCe<1WAy9srn$DpPLuZZ~Rm>J1~Z!EWJ}A1u`m`xuFbA57rc6fdVaw#9|gLXC~Hh@+^Sf+q{3n;e2%v3|N4-`A0uJV><6@!f;NkR&`-Oy1ViAA8W%~LJy zR7qwB0$5E6MjqU|1lu;Wl-Ynx610)jp(BUj?z`@V&D$=7?K>`jyjIuG5o0H*P4dB@ z)e^Kng>sm*PLG-Z(aSN-xZ;3=0u5aSHb_k5SSp`^2@$**KMCU1QU7A zWC9o_LV|;ZU3ZJ=3+|Uh=(kHGFF8sb6*9D>o6pkDCJQ%1;Gn>ISX(8p4@1h|&Rpzw zch0d9C4^1^ytn9AEHI_Rcha@iAYWElR)GRr!N9{a zTPH@nm{o&t295kLs=l8ekbz`E8hvlGz_5_4%9Ny%&o>Sg7zN+?u|2LAwh!zexN`@Dc{kB zT4Q#4!={VA|GJmH`mtNT_~FN3BF~9T0K-I_3{O4sXg0O;(JX6xmmGQ;j#jXakYJ^p9GC;?d+4Tus#y%s1~;DDY@2$x4P zzk|{P2hSdi+-e6GiLqp62l7uSdJ->m=gZN`r*N?J9}4XLTU@&5voJb&wgd>m;js6$ zJR`CXUMH1io>|j`5M~8LP5Boz3@{)jfP4n>%+Anr=Py9{>&l-XgHuZqTu9RcMR&BY zo}w@kxFP^S{tO)nKJ-@ErS>NPQ1w&YBMB^MQ>nY|eh`kGDB$ADUM$&%7ISFK+`23E z-Wm!)nCXWNJ3W$MO_?XM8iat+7pG4h%`#gk%l=7eqS!A%_`JLg^D_H#tv=msPi^_O zm%ie)2X6VohaQ57e8Xh|7$y?PaN*I%^7h3&X)S!Ce9P53D`DPV7of?lqA+5emfWlm z{A7XWgHTEE*#tPc+bRtXMtJ$swHeH?G2swdNsG>d_kYlrhO6dX-?_E^U>$Ws)Q&w| z&8!jo3PknBW&<359aZL{YBJE>i2@@*YHR9h-<%mD$m4g6w96tnli+uOWPjY$P>z_- z!U*&(k%ciiIR6BhrTbJdTKW)-7Jq3}JN`SkuDfLEacZ05Hoy4Tx}XUyl=dB^D~SA+PD6 zal!xu6K;b3FJ;+f33~R*J{_Pyl|q!#c2xEapp+IH7}Xlnjj5?EH@^IpukU^N^Z)mL zn8-I&CV*igvEXp=;jh&?7jG9?{YL$Qi;$_0fkR+|;7Wj*6cC|WhgJ3%=0R>6h!r4I z+Y0;@tV)f*LSVK)(>tv8BSGB@NGRZE^apF4gH`iZO$8XArCQ5dTKb zOu%+`L`>v!$6^1_)*FBchg!N(wcp(b($WdazU$8!Dk-#+!a zH~!t%ZJHzuXkoHl7_~Y(Hm|j3VIgc!GZ+^3R57uPsgb%J7%cR z!I$#YAXm@;u`&!+Km^PINfbybR)S+47+GF{t_}noQ(sykJH@w#A_l0o;lG#3yk6F$ z^dpDDas3aJHw5=Eov*9_hM=v~%9vC;0V9-J`A}Wapus`OOba%^(#U@*pM{}e5>)I2 zd8#iYn=q6h;jf0p(!VKB9r?HA;DJvUwAi)gf8m z9yw0TgF*eW4PisYD9|92B|AZ2fmjvtX(*+60;XqneBVo7{;kV3JoNS5yLUgIe&gqj zOaR04P~P;WADn*k+kfiEuX^!oes9C3ojYA4gwNH) z^hI(IxZVy1Y8HSd*osJ7=c6IoK56_hE&M4Ht8O%N~-U}hR> zLn>wA^x@;_nNg+_c~BO;MR|RDWnFt^xlw^;j-rD*DGvx*E<%oyI{M^LMTJz;6QWZj z{K;!goSxhHZQFNTH?!EB|LVattzUapHPET+A z@#(471+-RMzD1j+W;T4srtR16yzHWDA9&z_FQ1w4sVm0^!`Vx2u9bMbY~ym!;aB_& zf>Dn=Tr>BG*RQ&rty^CQmCNKnw`N-_OAjvmQB?slH3lh#7sh5t$v7p>+fFNf4_E-w zN*NOU0l;Kmt7*=xfHywAA-{kMkU`!D z=ih=143wTFu%-taX%R;iz~F#`ViGh+z>>@k-L3=*Ux8uw*Ry`_mxn7S{tAl6Ur0;t zx>DzUS>D2HYI%MoDgVuzH^ai>iZU0pT203|*yxZ13KZ)=;6W*Ekf5RAS1wR&2Nixg zS6fiiiV6}H^hv%r6xU%&vI~zsz897UQxYs(q|WP=5WqlzXCC!9kU3w3%p5Djh7g8$ z9J%Kt*V}rZD6xE^81+xp>rK_s%veGdW07Tgv{C)?ZReR}8YGjXwr-QCBO||typHXu ztv6k={l#0d*~V9&-1W8RGf+J1WC9qT2lB3W{ipN3^*i42j;%W``?cAbjemb;rgP!U zbhAD;+k)AdHcU^qV5(hjZ{9d}YszL^o^gyp4j% z$hUEuvNY;YZ*KMg0YJ#26%pLF$?Cj06ks9xa~y^>T3Umx|)x34r-;+Oo)R zlt^|!+V&gw$$NKIUdu0|y&!ZjM?It0%s{4;YNp~fXupF7&B_}!N{qFZ~?M%17r`ewSF^TP7BVWm8y-opB>MD>* zy{0#8$%&$)fGGNXk_=sWY-Q=>pYDF@$zT0f|LPwdhBF}bEpK{7YijEUvTW+gtk#l* zLrcD-@}*WD&IWf1zxB1KOzmxY_}vaBTA3IpD~>hH^j8FC4G~vc`Qlj%epBKFi3QVV6q!;7fj$ zL+P1?fOQD{8l2BUp}>OtXZW;K6rU93;6tOOm0JOJoij?I)oNWjw_(%AHf-GV;_0br zLEBHank`b5Qu#MviefI%dnx~DYwU&u5zSUhKjKQ^*HpX)a+(~;aYJpZo#!qt0xi!Z z<-fGtgGZlOfNeXjghsOoKAf{KQ6dFn8i-9Wpg#k1&V-RBCr4q-6$&U&{1z-TF?v!K zJuJ&(yruT=n9Ps3Mmt=&hXtc-BHe4=Y+zn6Co2pCNoh{;rIxlb zgm8ZmA9M-1Z$eo@#gBml2g5#;YJZ1;oc?z2m$vIaEf)`d0ATMyIMW#Jc_NL*^tIcz zZ~E}u+{VkB&8DE05mTKh+Kq|;g?blTQpwZUK!5dm!$gS`D$--KksI@cS!t2$&7=B* z5p3Bw3wsWqf<1fY;rt6piqZ)XIl1C2b_}7z>ExPtPtioR2?Xaq3BA`(C-b^ z`io+;lFR3+CIMxAI1;nRj~)ET!QBV`_50rUo0Cw|&r&9U;VhE>@E`uig|Bs>$gBs@#$Y<=_F^469QWVOyl#WZAc=3zjCR`yw>c=fTTDGh-cC~))1 zz9$u^@Uw5NY)eCRrUjjPgsA?Mh%u)FI|`f`NdSYO@kB?EA5{4ctEg843so=__90xh z3Cz3z0IM+!v1tepf|wkRMKIt1_*T=LRq!zu^zylZVHA4Da5VUEF+BNx=2KysZL9>l1;VbvMX^H zZCtf=^Nd=GIWn785v$OUY{O9A{)Znu05cn}f%epl4h5Z?Ku{`9LuvN0u>J(z6lnp2 zew;^!c@Qd{T%B?#k0TV1OW-jg$BNvT5mYF4nXjq#xX8ion^kYL<@mNiwX0W4P`!2> zAf~~BhLPxXmqoX`Ac%pG=kv0}T#lQKQGYajYU$K{`}gksr@!~#e*FoU$TOCS55qTE ze((o>F#n}r`ps*9=4XH5U9Wh>D}HNk!`yeumvfF@8d}~&D4jNep%xy00R!bK%^XFof<5y)PG)O#^)gV zL`Z`LTt}I4<*Mz4{lOrADwqy!+kn&aS`ybyr<*$)!KKadz&9B{g_SUdwCb@m!4bcChsF)5EWvluTr| z!C+)|qthUQC_>9B=q1_0%{GaYpxMJ{SlA(cX>sw?pD&y^_FupM```4S@H;G>p6s}> z(}u18xK`irE7CO46v{l&Q056WXE5>P!TL6UXB>nB6~3RL$$)A42xwRd_pgAkOp~W zcRC$0)#*T1t1GsESOvrkW@kDkK7$-Lnr-ykta^U&P#4m z{sbZ`{SK05L#WKGqENmGZqq1q`ZAOcY?eo3&aK3;_693u5WM z12bWOxfcKeRx>43PuY5y28?wro`xo^uo-7a#yExQ_8?+NEbp*NKd^Ez<}d|3OnQJb z5JK|<9hHt%0M0I;5A%!|g9Qc+pP#`sU5szI$eN?)|f~(;aez%xdKD*jD}rWQHIXGBc4N2}D>&M_Lgjy(C;<3IWP@BQ_AVIrp`6O+VqC_neF|J$Wk zUU}0$yXfLe|EomH-vgL!lFB@z&0h+o=rKU^^s>-|T$>s)^+|tZhR`oA56LRD(^7vZ zUz8E7tPB-Upb)UMvmMPO3OO+J2P64@*EUbh%zjs+m0fz-rB~j5%PpUMCZGXFON(F1 zJDXl4pmwdF7H2LVz4z!dDvB`!p&7!@u=)3j|A2}A0Bje$u-rI>x*mmJWg!qT!Wn!A zO#f$hgB-U#bn*}FUt|(Vm>z;UT^Cbzn(d$y>H0U3v*&-_-ST_Mr4_peYyZ z92xKZF!He!Y@j8zTvD1IXi22nDY6U2aA}V;J^XIbJ@zkg<(}Vx;ep2hPWE6Trz2&z zyZn#@28ZSB{M&0a+Tl_wQ*tEC&Bj&MSAoJUr~pn9Gz{dVVx>QT>2?zqmRI2T;uLJy ze2HhfG83>3G>f3aKp`vOX2K=!1!{&uJ+sshWkv~}BxtB_n4R7D-Pc|J@?*Ds<>m)rBI}a1$J2>CBYDkh zez5)ew|w`jnvK~XZ+B+DUq193Y14UH*+4IZeo<@M;i#sUE7jzC({43js?$)bA1EM` z_D%*JwN1YHmMtiiUqk7@#`1C>7MFU;iAk|@rG9~_y_^M9A(Mt)dReLvvdhau==KK7 z%PY&DKE8kVzklz0|KHQ6={h%@KQ*`WPqX^e8?%~xku!2oYADLl`!rbkF!R0&PU!>i zATJ^S1Bpk}4ph1zQ>x%i2zIHaK>!u&e*zGA>qGUNDV6vx#-u+J5y@kW+d|eRrs}}p zczJ15fPEAk$yScf!saC5aTJ4rvayi0G0fh~-Q_4qxR#nj_O-^Y1m|xxo z8@F7hKtirId(x2_ok=1WCYq%8JK!v*Um-CX$l3hkwM@(_HiF8TxkR2BCPA4q1api; z^<6$w8jlDK_H$7vh9o!Jb8A6mYH7D;Onb^qB1%i7WoQUa6y)zU9Q0@fMK&7t1+APQ z?e^6_7zNePu zTj)jWbQ*d;c}WWnXgPVQw~HlH1g&_WbXpooKL~wN^tDc>uJ@)l<_9|0>kpOEr{$I- zS(Z!=OG}!WC|vY5y}1myJJ1 zM>l`u-KS+kZk+elYxAt}yR};9wIb6o8fdv?mYEmb%v#X-E0}0)+;-1R6K3KNu8@cx z*Wjz2WeaxB(v=aIsK{Ch{&dT%6EIjh!+-!+%3c?D$y&pgV(k|7y-5M3WV{9R?1c~I z9It>VUBD%64lzCogohG)FwSXBnn8zg+n@&{(4aIhoiCdikUt zTQV1v3AFiuCOkP~3q(1}v+_~-ZC(L_EVoM{&FTv6@aO|Sp<5-9<@!4c#V>eJ0z(C0 zY82KQ4C6|ZtM;@UJExQ*qz-58M+9Y@qphs4pn;AiS)$)tmVez$*(FM(85lX4w`Cd} zKYsLA5AS*OPk-UfWe{b|eb0 zCjWGN_mNWEL&5YbajOan0j-QG}bCPS-&79~hOxzJbp#cOKa`Z#EFg`Vi?-R3ka zD5Ua77*PBLwbwmLa6yCnK+P&_bT%?o*j?>NG6iDPEHaY>$W`sBt-t=#E1NfM{RjW? zmp}N2zwpRNF7AYr$-HLE7{IsmQGJU)ksV_qkCumBO`fr)n9^?2q5*SLmC zTm@~Dh{_>wvxnQh8ku8b8G;569()`H9ky8qSSm;I!7KYm%3=2o2^RiOv3&G%08gEf zh@%r(TMiyPxUy-}rvEuAM;j$W`7as`Ic-qeSLaZSk$Zf+j{3XtF2>3;}c-t#)(E*6sgBU`Cr&{V$lT${8yYABJZy;vc>1w_b6@i{J7~ z8#nLxbvd(sl~m2sw3Lw)H$_y_yRsQ&DPM@;5Z%sjQ<6oPm8J(WL3G;kc%}s#=Q@%$ zY{PV?p}+)fFFQTmkm+sMxS=ESG-1PB3$|>Ug3TLs-nJ}Dwn$NDjlo}WybCKFx?e=xsHt+cU*S`8YkIzi+c=++hzq&5phO)osU{>G!5M+&S zm#s`&-v!|7tcBqjEWOCi+hvNzV^W`1BUTBXeSi=s1c?XIiN5ji4#?3`e^>vainXAlN?QLdBC86u0z{i$J!xs6gR)sFA|t18`gO&E z6)Lb@rtBB8AEjPy+W1?qy6N=;frEP{+heEYbVk65gi`xwzxe)d*|u%lKX12ZzirSj zTP0iCXlL|9l{H&6^@7qnpkphPO0jngNSK;xnEfH@20d%@1ijEI1azaJ!a&=ZC8eo| zj|D2926A>($0H30$mwx3(u@Srf@&qEEIS-%vxR->2|#rFg{njAh*HOxmwl=0Ju0x< zJ@tj7`yT$8_q^w)9$4>5G`kJMV5|7TWf&Ds#s6yr%4vjiBsqh3*wF?Ik9c*sKk z2FE;@7zGsv$<$+fI*hK zVwUr{z(?f72<7;M+V{#jIs)5YLhnbRR#K2-BE8eO1tUVYqi_U)(jKu3YV#@OG->=F zD15ZqX|pU1ORIO-oSEBXOrR=GyzsQKYsu3|JVN@LBnLVP9zh+aGLU6-}UY-FMat9-#WW#?wxtwc(qijQ{;eX z2klYeXTy3mgHS93`6(#JMhP5h^5G_>+?GH>1BG0DSD~GaZI38JdJ{6Utdi={L7dQl z4z1{w?}A=`tUw0s6>pbRQJ$hK4XdF0G$7xHLidY4DeN?vDTonKKqu3&=ILcq`RFCi z@MN!h@;^Vh|B*lX*Z=(Q?_c{a-B@pIZvNf8x#jn3DzH=AfCjpbn)Zqo%$GrlFy7aG zwxLzAVEzrI0vFzClCcg+;U`v8{`{CT18bELtJDSSIESuDEP4AF!4QO?djOCMwB$cR zP!O6T5+(=?O%a@7usc<{6%gc~K)wmZP_o3`qfqodg`=hS%O~goSi0{ROz7ORQD(Mn z+wtFK=jMKV^XARj^z@XD&p?~j$@g)2bP3FDeTfd5lxfGrT#ZHEsZlvPD%K%0n@nk+ z23ma}-_+w$=|7QWw`Dy4puvV#x;hA4XzO2}7-h<89?uM%?rW0*CFa;o z{xK@y6?=#c(tYw(c$0$z8;)^6Z;C*hr}S(Bc=LnW_d)IX;LQ*kDD;Oo>hHp$_X#=L zestJ9eh(ZLB4;@*TU57Ha%I%Q3HnD zI6RtYj_OmOnRuX4@G3ylF$o)T}qdmH!a!6T!=^65#8i7qnN(D8e73orWI_a) zY|8>gF&c*9FB&alk>+veq`^__x$#6zk3f%M9Bm&tRfVZ z@%V9^ZcktNmTl+n{Kr|=e2shvr!_NBGY_>;GktPc><{#^N9CL&GPi7q6gX`$+wRol zgWNQ$AM_U0v|Q3EXfkQk%!&vbY=utIUiP#MlOBA%%3`FL1?@#_K}F^^gaJUahS|)~ zjpLHG{Q+?0WS3w1$ct!zh7L%(Bj2Cu19_?=A~H@|6SZUayV=>j^?+Vpm{ z{dMll4nl25t$YR5Ouf`0p`O`nE-}y za><+ie(U>h`qsB@*?Rs@O-;|f1yBRr+$i&QQ!8eAAZZ{bW?`i}RFTt2tZ6~-^`K)e z=nbP9sx0{{D0<8+Cm_=Vy_7ApQ4BEy6ag|5danpFTTnVFa9UES{1r;Q<#kilT{ob? zZ<$O=-?9`o%>s$iC@alSpot`zgWn!ohnUv+h07!BLz$;MwQ%w)yY9dLpZvfNyy-!J zRrjQ*H@AJ;2PMw>E(scP@6Vt&iV;F@iEY3Ie*BF2^1XU0d#z+7NcayZ`fB^+=Zt!5i zIyc%-&3bPKz0r>a3F>??SS*X)=WulDPm0B_eFA9N%|xEJ(r&je*t~VyXJ=+MT)BDE z28rQ!LTBsS73-P_qkKHPP>{h+elV)5}boC$KguM)lEfG=1Xu(Jwx} z`@x_4WGBD2RbjJ_IAZe+0eEHL9oK z_pDc@0@+TZsbe*?+YQyGH9xq}(O%a&Geuc?vxJ?g+V#{tzb&J|f-vR4=8;ttD~Asr z__h1??fUJ%_x86hjK8aw%{8~a=+E-n)LXM!L$Zu59oQ+;OoKKlw~pm`qaP#AG-5o!Jw`#k*n?FSF$n=sC|$oj z&?r570E}rUV_=W~8^*j)swj+XNo2{wJC|nr>7xHgIqZFK)Ia(G=s&XV<+T$z`(Rzp~fU>128ND2y2?q`&3Bdrv?kTPE)U}-8wtZO{jTcKts&{4VoG0 zaUieRiH9DW^nz__rc|*SI#aV2Y(#Sk8{kP^(P$_^Ls#_r%c2~W!tOsI(x{rqZ#?gYzRflT`5(m&}mgfZj>IG8OWSKLJxkUuEVjajiCw~z)3H2BCmX3!J5}ghrSi*Pmb+ z9Gy`zwt$CiwujzfQGYf2NCbUlZ5W_HaS~`LWDw3-vkdb8VqTv6Qa0@WWiuOmW^wUr z$6Ls2he-`znmILeJ>uA1T%#BU1zrV8A01 zSUgUVoVDMS-qo_E*aEHb?ar08EY~|sW{4If2(#QW7c`D80kI325H7s4&Xbv?o4QY$ zR`BeBa;8*6m2z-UE6Hf=)tgt9jA;Ovp53IJJPpfeUU^YlA^>(}4?bvQ@gi*p_@eE7q+UDBTFd~dTk{iAaB z`?5j5Y%DJK)N^GAZ}Jzg0gh;PPo@H^&_~A6krcQczpcZg4Z}==Xjd}iPMMG|0*OZP z!Uam9y45@y(Zo!UAf%Zo@K6(a$>GR2JX#iGW~!+G0)Y*R;MP)c8MGOMOc^>PHCJL_ z`sG1^?z=GG!@VqcS-vx-{uDSI4h|eUdi2ls?cMX6_ul*2L#eG}8yjzYNmkqRK2dAG zQko~^wKdf;C@`SGZ;LIw$-%9JumB>^_D=f?#xj@$;Ey*4Mm({?I5-f2W{1W2|l0>l-QvFzwg6PZ!KN+pO6pnFBk0-ADx73 zJ~!mN^Uiza)ZBT$v+a@}zF}_jrGfy18lPZfOapqBPNoU9j8Z6Hm#!T7cI7qEmy;p| z7=!^CHEEJiD;Z2kX<>paDfX0be?}`O%=k!F%;KbMW2oLka&FY&r!x;2jEOQcY=dy1 zLXQ=>84{?$nqCHOf=FqxK41iBI-NN=KDLa>l~PMbM^Gqf{aO`jOYEZPL$|vkfx|E> zm5Bmsl4Yn%070PP=_C6O{_9`=)&K7nI0sh*os)r^e)o5OxBjy~`|mFNC;#+c{@Bi) zJAY&I<}E)eAF7K$#S#%p$xe5gFf-kRX{m@Qh624z#4=FS#&%Q3Sy$$RR>Rr;M}<~m zI<%^u;nS?2beh}SA-A#5wF$z8RjuhD>&o;&0D$_SmR>e>NNIXO`u##VR;s_g4(qJ; zkyshQ;_?8xy^)FuuXev2X$Q=KS$e7Upm5H5*1tlqrHR(mCn7~E*aY4>F8R=|`jmV* zXJ=-oUOO0cU$SrS;ZJ<-bD!wO_r2&HJR)k_?#pVmH_2KydS@izl{7|dfciI_g?n5O z270QXUzN`*RO@OXxX%Jp;2Nx?Xa4bV#T_DSGyqv1AjE(J-3Nl-84|eSFS&06dS-!{ zGD2p7hjsRT3Z>mOvKV<*L4k#V9AkT5F0lJkxOC^IVdeN_#ltxwOG`@!B`dI3J}KYQ zn%X&AtGE5@K{sBg!HOu`n{iF%NB%D$w)1n<;gx=UnKm1^vx_y5243{V6XAdA_ z&dg@AkZ~U-mEQ+}(q>A{w4xbng?{FRYhN2ARCpJaQ(l8)4%Dg&?F4D+z*5l}dby^y zA*$FAxsHlClJAQs)tI2xcM96Hs#f39Y`6Z_Raako_r({ScW~FPUFQ_jaLxgS_q^vl zo4@UEecwO2=%NdLbNlw~Kapj%?G!&$tui*g2egubRzY++O$87d&6@HbPyvnV+QG+e zGo2Y{9CCIfieGI@UDL}YXQmozDJ3xlEi?P4^m+R%aRTW8?> z?Q`;T8n$kkhD{sWuwkYp!AVU8UMk%3P0wl&u%?w7D_so|mLz~!Sm?vTQjdWMt&sH| z_I~DWS6Uc{qNLb&x0!&(|FLJ^C!bu z$KV46V-_G`hA_vGUqTET0Iu!V~rS)WcaWeyGvfkk{%R?TpN^JP5Tnh!vJp@T8CRgk8-+E9u@JZtu%rOp$3K;UFbrfuCk1KT7(*s^&F zHp{;a5F>@YcAgbUXm1e1Ris!y}@6&Mf$U%8eM#=-ZS!*5}jaL3<9QrQ%hn~vv zO;3xw@g`Yo$L=Sg6`f}rxc$VC2;LGQEEwQ}lwlG{f5h_;>OkDHm?*nfi6IWu( zdltcOs~xr(0TAS85je9DE`^K%K>X>i#eGLxj{ys%-sd3<<2;aHp?@4k%fC@9ef6IK zEKD}sId=t@77jdCZ(Q6)T)3&x-jdbpEwzL}hXB`J0jPYfN(m~(P$TS8J0{YyDfjLP zl{YgnRg}U8&Dv?5POEO_WqKEM%oY0%TF_wQuxLL9#r|0jYvC0;DietZ8;%_Wwh-I0 z5NN@Ne%FX;Ai{urHY|IoV19qb4oH5?RKtKU~HJ{sPeSZLhoY%ijC;@$aFC< z$3s|JlmKF7sLU8-N+EX6hqMM&zx7qHW{NDcW(f*O-G%-8_QIiq2NZG_b~id&wJj@? z0OylZ#;%+w`mf!UwdSd^*T@#@juixEHgG5boRUmU+J#lrV1R=&L9jfP1M^j+c5oEH z%>ot>2-6?S9PrQaV8I{z%RzRw^x%Mj1znF>5rN@qiNL=>ohUtcK*KDM>lYdaN9E*b z!GhXqdJha%{zW=~D*9gEhk#j*>we5G5S$2Vd;dS-)206iQDD#0D^R#K}xO}sZ zDicFy=H*6PD3ubkKi2yI$*kKE*P3J6VV632UtcC>^ z)_+NtvD;M&!9jzc+}KrDD&~k{lbb3cWS$w0#PmU!{W2-7RLOQ|Dj zv$GrD_L7&r{P5iLwU4cL&zXP!vbCyph-na*}N8 z6%5u1l42*cntBz3+WK1UCDBlyjYh_1_-3y)6Fb3%na*_pB^8^295NfqKcb;_zHFPA z3{-D!L#5I(%Umz>9P|qlT3Y);SW)$ZqxFc*5Wv+Z5^2$Q^vB4`1fgopGvxq)`r!LH*mUC*=>UXfAUx~fgDx5K zLpji_f&veNURm_sS1#`V87Q9I4=DsTk#kyly(jxcvwc@qh)blM{@O-s1I?r9ft8#* z?vw9zsT6pIZt|ig2w83%8&POAj^H+sBLF)*CLh&hLr;PR0QN0ck(>4VAdHuS1{0Rj z%fRSwJ@-emOgvx}Jvadyqe<94W@_1_Ghg`A9yHV%0CqXKuvQVvR%lj2jit23H-RTq zyHu!IfMiz4HdAkRI&Uh0K+4@cAdQ8hF88;K6F<;`NU&l z^XAR4ZQB+_$b%gtsbE*wB+q9JPPjDP^m<~K{QKr?z`7N(|Z@{i05nNs-H#L;fo zj8BASF4Q?v@`lC8?MjM9?xHl3*d>rQP!hRFC_fAHYxxk+x}OaN4uk|MO=uA~A>Rq* z=TI|YTJ-}@SD}Ca?Hw^f697d=c&t0>O`BS@4}@eRR(cXB9E1Z0c0;eb>{dspw)G7% z$I-l9zP++r|L2~2?pY%;;K@-jJ$*-B)UOp;?P|six@o(I5Xc4^01hqUpiH`NkprbI zXugYB>03&244V+t194Fc05D=ZJfon;A_Eqbf-fRkqIBo1X~DA#98_?YAp{GhSte-# zgYs7>2dB!S`|rxdJAVmaVY1cqM8?Wsaew#3(ZioE&^o&wuI`c!2`)b8%TEUjLGp^rok0|F%5LpLpys<do8%B_fQAaK3T9I&old~O{;9eR7|=Uc3b#y=jw!mS z1qwxBO(I@-S#yLw&dgFHy_==>{`H$r<;F2GOcQDkdNMue<(8U3pni``XlrtgJh{+= z6DOBp-@d)Df8S$r=5b1`5K&H`xu^I1JUvvrsKNJiYi9PNfBn%9ABMGL<>;X3KK;>J z>-;xKtKkLCzF{d!>pTS!W5C57M5WIj%F*aBjcODXTAhKS1pkVXp zG|M3HI;_(o+vCAp+iI+W1r#pE0=GXT1{dJbi4#o z@&o`}*k~|t)I3Of-*>H$DV;!r4#7%}mO?$`^l2YrKYO&YYIB0<2jITLk|ugIRppyKM8#a6m;ZCy?R8& zkO1={g6U>3Ll#a+%9pCWytMe(M?U<)+hBb%%8S`A%A0+YY~uoN%mg?4;2cH@IPk$B zIENvq5q#^56v^N&FeSLkcYF01ooB3rWe$)t65RiQu?EJ6!9{^*!Byg+9>6ZrjL=+Mz- z>j(M%8xT;AmIRJY02m(x@~g3COBIYxfEzE-dquGK-LM3ftq_7SfYu)&cT8|5tT`id z@3+c_$jVA`MEH>)vtxB;_M%Ynb8-O3cxW4laBXUfsb)NM!{pzlw*4mNMcQg)KcrP> z>iR3LzW(B?FTdulyY9O61;yx{lYrr|$9Ao}@|8F3Xtz6Wpzrhm@7up0PMnyBojcE0 z+rC=Ln_?!^%wLuZmu{7ln)AtVd^n(GmfG$=8Y1Wg6+$^Df-RHr{pG{IT3rWHA~51i zBL+OE`bT=-301$^+8RvorQGaWq5U6nW4F*Og$~|CfS@PKb|r&goi@pRft)ytQrFY% zN@n5YQ8;pV4|JF3q0`EhxkbgEFiZu3BR3gfFCJ5U4lHTrqcJ7Pk3YNj-h0>O2vihD zPC#?RE`jyyI zIugRXB{ozuaMObDY-Of>6@;A}1q`O(8_wx!GPbh9PLM1+pv&9Y0~iNLVV7zOVI#;o z9HHMqy~m1X8kETwAsFcNp)RQR2FhjbLa3D_W-O2(Mw%(asNHT|*XT@Ne&HooJ@ECf z-FgCEAaYIu1}XTmmtB8yc6P&$$cLb55%Zyghv3+;dARJdi`9~wg6Rvr+D$*0*SIEc3vDhXN8NuifZH`xk(F0c^|!yL+2` zA+)($w`W&0&?)LyVHgHFrqvML-T=D&LIHS1?NNrDop2tj5ZC~@mj(e4aO+gug=U+x`8tAQ83`Zu?k>-Lc}`Q z{44;IHa}>`MwvW%R5A;{P%Pj5-vLf^VImWG7SdbZe|X!4FS+}|i?4p;_VX^kc>eg& zu6%S0z4}1=Qn{5nDvbRS412qkNkW)`HFFADPnJ|9gDhiAr3tR2Xb19)FmI*M%z@S0 zVEr2G`(s%O$Cv`RrIU&|FaQFWf0Jf3gdY?1NTC6OVfF<0;7%qNu>2aM=v5-J9O@0* zRP<=gmuvQD)iJaA~VGYfqOgEsjW!I%Rf!Z!GK!EG#p*SX$_xfpxKnXFFgKJTSYS?$$nsbqp|_nLF~>&wB!`s#{iFWEPh^vLmW!J`1c`8a@Q4(vu)z8y~(2!8;%rXe(VD7EvjGBb$W>|^0P zD|EkvxeDv%kmV`|IZx^(B&zR7J`IgVU%%^lzemeC3mC|I|Hd0% z+3HM9zs<%W*PEZ@aO&hqc=WNyVbi7^QXzMYGa@+#>ir@H1Bypb!s6l(78d&|C{j<( z;K>Jof+Oker?s-BGUUKWqMUVdFgwD5+gr&x<0xuQXx|BX0I6MNju>JPx>=hdbf2u* zg1`ic_H)o-qiHn+8q-5*00pHTKR&NkJsdl_Pendc%%X~BsU0OXE8+bl^a>5(fQox@ zGVge)gi(C@J8gv9Z~gh7f8Yafno{=n?asyAqoUFL9{Fz8?SNxNFvae4eL`;w_fP51 z`$S;Or4~mlaK>QFXPm+Qn5=bSKLx8?-BZ1!%P$ixJez<8Gd~!xVP`+aXMru3)P4)Y z-k%k?=SN}XsRv;q6FI|jaNqq$vgYhpF1YaWAKJ40vaKiPk1fkFqR`_~M$8rrdb_15 z;-psknO<&*LI;`4%xVR6Mhaji3A;ChiRohTJ!dc@6Na5MD9mDBe!d#QPNMWiRr=i# z=vR1|DWF3X+H9f6G#yf%#=oK3e?srPsT>^*r(t8yXdtAPm1_SkS!|?hQ{!~4q#;n6 zxG&hWVe|J~c-b|dyZ5fIJg?0Y=PY0#nVp^4chNEYz5E*w3!2&W`qSXt@I`~&3&KxT}-1P@wS>#e9qBOPK@Z&59R zO`g<|0B6D|yt!SajkH-!F$pT&>tY8OsN8U8d-MWIl(VLl3LG6yhw+iM_YO%iT)FMw^i9g3q249+?4XKrs9D-lY;_=^Tt zda=XE+}y%DH7YPrI6nq&g0SGBG<2;25^9TT4HBRn%ExxtE&B`qzFa=~i!eBF3MMj< zZ;L@-r$%7lK%4&Pd~Q$#Iu_52k(nbOoN0pjQ-gJ!@N)<~ z!jxv9r4hVL{kuDquHFBnVNaat6u%4yS{w$ zr`BzvJ=^4*1`JO;@x;=rU;X;m)@$`EOSLpjD<5?%H75lt+P{CV1Pz;^-JXL}r+Nw~ zoLZEh()@5j9xpD_22TTduB(y&=8v>O2eQzBS@qOk^U@ULL*wQ;%-le%+zD&6~c1c8ytFTe$6<0Fy3+?ThinL_cv9dKRagA%$TQs1Lbd`7^V;LkH9Y6R@f%OV`i;e|vud zZ^>1ihvI8j)tT=+0C_-$zx3_f^Q5UYOCW&&BSr%bwh3UHIGz%l#4#8<`Fs33&p6I= zezB8aJS7f(IObstW(En+NTXRTHIlleR(GrCdA#$zXR50G_8RuCeNMNqB)VJOsFrm4 zOm*s#yJb*u(ekhl?;?(SQNt`s;2Cs@2N7 zP#l}cBn#S7`XDuuqmyj zdB3a_#03qu%IySPih~zo+A)z{NhDtj-5}1Cq=CQ)cmc-(P1zS?_LTOf zsc%mx0!!t}NokxWQ#`F$>lIKMLyLzCd&C&PUP@0IvEW@pKan15%o;lo*Ic zlNk&IEmr8rPK_;MMt1Mo4l~n}gm$sK5`lzGVOS7jr%FCAvlA>4&j;L=ohd^oqjP16 zTXJ(-?$FB#N(gXpYy)0}Fi;-o`P|pP^rabiNpYf;6nY;G(r|?e!)tLxBC8c|wEzya zD2HB7XA+cM;@FkS=}05{DzY05oq)q*u?bH6=-35* zEqg1V$g!!hbDmD>pGuOGzmhKO*bI5(asER;`($P7$ap%v`jTtj+ioquOu1S^c>30slKl27H3#g86yKvfG)B&M95 zwS6zgJZMrL*ffS)h|p)1f!UsuC~8D7P%IR9amD*`DOQLFO3#lTK6YyR#)mdM2NzMi;sL{+J$vdm-SnDuVZkZGV>LqNY0|J^jj(OW=6OJD{0(*8p3v+W*B!om`sz9+e0d1+N&qaZ& zAmWq}G=NNfQVy8)F8~1yp|tMi9vB62j$}=Pj|>33w7mljg{U|%d2I55uYK){+u)@} z^F%$V6z&alalHWympX~EM=z-Cn}A!60GyUO$T9{V>u}l){rngD!<^|kIBh!uo?iMv zyXuw1RYhdg50aJu5~2XNrS|$kB(2G`HT!?1^H2R2w5R7Fk33$s*s8a0{?_ATi!X01 zUvv4}TCG+%GkLh71t`!2j+43;6KJW2MC}5GEpD_RA`r2H7N;h@8_1;0dML{sxbqvw zRO<&J3xoi&F+qW&Jta+o_fyxW!e4NJV|zuNX$K8qG_tXg?RdkWAn9DzF)%v|%%$Mx zAbnS)0})ZC;#M1Cd+Z=g&0L^+`v>25*%j9wtQ6Dj*ccHmf_TLP28>s|>NRFEEWgwI z(p8%DJ%>a}EwoZv&7&UR#EGM@urLdwiDabuIU3vpu`H0SMiG#VBqfH!uymdNo(SV)GwPJB20B0BdRG|QfKn$eqYG1ho1D}=h`Ycmn zt)js#;IyB5P@HPDoijb=LdMxZ2n{alWVB-aC$q7b_G$K?#BJ(wiM=IM=B{hW5ZNDF z^oEr3kU?BYbx06s!_FdbfLg8Rt^EU|lS>v4KD%}6)(hTBItL6U1_pX3#>bc58WjpF z!A>(NqgebGX@6`Djk*B*-KVD}V19N27LTLBQ;}BOv#Lf{ znwwW4%TTQ%+aSFpJh0&AJ!|45DTIhp83FaP2&7>Ys_kcHrr^MV9WXU{+-#SMH$y-c z*Y@N=Dy<-sJRacW0ErAmnhFTv_tY6up?#3og8?*^a^&oxxv^H{`c*&%#CbSyWrALC zX=Ye1^?c*rd+&USX$g*Ydul$2YLBU)^hWb#hLJaP9EZ;&$cgUK3EHC~y=?&em5AK; z0E90x)u(}nEG5zTb!}QJ2HS4hH)C>uA+?WbFC~$I1vFwx4XV>|{o$lF^D}8{$Hp8i zJb)lxjkgG|r?Xm3%FZ70z&P+0h3Qxj>nNTAS?IsvD9u&g1DtBF`$V-qfA z_rZvS0ZwQ;ZpyMrak;wvG-&Q*@RBy;bXckOytz~vDD_-@>0>)zlFsY=MGhE%swL{z z-f+`ktx|mlqXn5Hgz~}(6$OrMNmbwhF*S9{T#g&iunhVK`iSPo3AI}0PLX)*;H2K$ zQ#632Dy^prbX}`qZwmHO2r?72(-y>*p`d;hA#cwW6NzCGrr_}5eQ@IVA*vS!LFT9{ z;BJ=6ooskbx(V@E!2S`b2p`=^nP!ut&Ho|DK!8%imf2U%HRaOA#wMn#8}-oj?JkB+G8lv2FH! zEEqVWHo?8(%uIuC#NZe-F*QyCIJ}F2I;L zw58cwi0)NUt#`#GH@_i{n`(agL|vbD&IR6_9WbAuWIE;CtgWn|^TL7vmDM>5@FznB zdMFIC2^nvNc0i@Kur85U*&uv}%1XlnoByRTQfCZ@doSR`iN{3PF%Iuys*y@6G3{%+ z(AqwoI{VgH`Fmisaay<%a~-CES(R04)td&OGbxa`LBQkgD5yml7xPE1OwC4lQ41e2#k!58(`4OdHxcz2Sw=ZEyF;6 z1qKaR=<6-fD!r#CG#6~;fb|$`Akn%dbg=r0BL+CqI3c@lJnOz7v83y|e>>^xw zUye_hFiPV1=;uE7_jkfsh_rcZj}C|Sn*F}Tq$eubasj&13gA{5eg%j3G=M)yKvt`5 z|L&@rLO5e&;K2t9;A(Bfl#53O)E0aea=Q<7^ z+!~kF`2A6(=aMy-zveZ~=3F{IbE3g1fk2xDhKRX<$5hfdSQ{sr+!X_ZtfVqaA@cbd zK*;F@$i&&MZ-j`UyT1GpTK?^{!t9@k^6k_V8mD4QH>uhIU`$<&BI@@F@bM$@GoJNCk!-FwYs-|m=YB^me*MVe)t4VX^h`ZTlhmeM#U>i~UaA~};Lf*D*-6=zJ<7h>&5_3wEjI)4 z)MvoK+gsg3#u9_1DclI)fQV&qUCE_=FXhRh1szCfrX6%?f2p|bVW-Q)jLBiK#Gr=9 z0R{cyxd8|Ta|tQ+ zfNVllfC8~@>hwtp4=V0gVSOCrU^WLkR%PtkwGR&LKLquKdRCPopN$WA?ki#GHe2F} z2PK0E{+Sc-u zx@|yoPi^q)U;Ek@kHT4sq;cpuRUAEORxIyeQ+GK#PoL$%&u1!+ckE^!`1zpgzH(`b zGl#GXSOO_ZYUk7_V1cS7tX{#TAX4#HFkm5WB|5I(m85e&4Xxd|#d97PW9TU9zk6)O z<<}06t+{gQGP7flUnX&iEloXYiotyb?O6d9iiY*#d=7 z1&a%yX?cAuj`*aIv~TJiHhylka$Jn1;QuWXTv^P@u?F5O%e#+`{%u47XC* z0UZRIl(_p39D<$C?KLdMg2yDVpfQq(oDV)W4++FLBoN~zTft#{BKY$sRPA96rRedP z{T#B?2Y)kC0HecWq@0+t5CvW-DgOxyJ=+6nl~<5dfA$Z z6T9mv)~YKd^Rn_8QWZD^;lR66Zmg8&=(NjHrWPvj&|-B4B88N+1EmNodm(HnP1IY+sd8+G2Dc!i{uAb$kHP|hl@#1~!~YOafeuXrR#7=p zgvTAMp3vn|d3|3`-wn&h*F5y(lMi1oDajl#_z~CZ1H;R%je_V}LXd%7QFCPhc6w~J zQu0HvHp)18Qzb>rz6gg7?nW~xShj2l@Z_K6#{-oiT}Q0}I4q6ZV%NyShmXSU-TPr? zX4a)c_#pH9Q3L`GGPVJ4hcfv|l@X^Igms7o7~rLe8U{2BOc_W>QHWIbSw?^XLQy(m zMxm|FV6i5@7j#GfJ0@fa$6J_>5`|Im@Zo)*KFf8-XvDAOjmw$o{nu?J~Z8IjUsF=UOUgms{@Ndp$zt$Na) z{)nz`|6_ng-jX_xi$3;j+i)s?@^==myy`up&%=0L^}sz}g0 zDbqS6@bm~WHYuByAZ#I36E;g=9q*KOx&|Ki*x<>fLu_X&9m*m>QW-%YX%8MFqCh5m zfhCGmlYG`w+5wm?usH`#GiYilfFK}>%7SxbY$h1vSP*5|Yr{zBgq&ar=xBjA#5NR* zr4>DW{kH^Fb?2^a+b#&EAqNazoH%hJ85&tURjc*?oSB#vQti}3!GHoPMWj_Z1BQh9 zPG^%S9vqg|-@A9WnVg%jdi6?r&USh1U=}1jVFUMf@ZeF{VHky}sTrDF0%cPU-~fUJ z4zvqN4Fq13C>7YQn*nr`4S`K}@aHE-%0M~Y0+}#DwjfXC{mC;T3go0vt^t*0Jm_;N zZ_TZEZcRNOi%dj_CuY=JE>*sE@4a`PfU_D4tt5)?4WW983d1W<7VW@TMmMlGqRo^(WHS zOrcXAdAyW3wEw%ODm|ljk1f6G-HXO9Svq-QZ{4i!Q@gMTtfu!qVQufk=xsu=L1(|Q2A%ddIm~?Xx+IO(hLB2 z(IBT%+#|yxYkWU0m1KmCX@x{I5E#r_@ct1Xl!|QpWQCNL(Eu@j%Gzu5waY0A zcraH*2-Q}TmUtY_cC_XelBjlfq)Jz-AX;Zpnu5;19e95$zQdEhlEEtoywk#&Dbcs{ zF7h;ZjiP9!5EC#+J!@z3Tkz5mR5qEk&E|8TN*9j&B&3J)lF2-NAY=c|hfmZ`Eqr73 zm9P82;K+(a69zOSNjtH|Otu^n6$7qIB`z$$+D7wAUdg~nNrn!gB&PyoJd+0i2QKq} zBp_ZE58U&?PS z>ZN9ye67h%h1}AVeGI4>lU=1_wFK}g8xL&G?~7I-=E-s5)G0WA{3u*<$)#qZElB!+ zEX9dYK%lT|*M2y0aze^%?3&oEja_JWAlVgf7wPX5zW^3!XnT@LO2sk^mQ}#p_?B4t z5J-(Mr7Ud4gc2vpkPql0i66OAg0Bm*O5l$~x`;RvT(ZxD&oJ;uYd4#PFMQ#%pN4Z0 zt&{aI9DUqyiZ>h3umt#=q;LQM?BB@V%zE6jt}1XKLHivgJHj#yJ{X9dkdf^*Em*KA z3Ir2vlY(eB($?G`r1jSC0vud`Jo5Mfj^oFlnXLA%+&I2`{TqiymoG)oV9u2!uyP}1 zMU6_@RI{0r*QQ#4zm`_mGW#k!zSuJY5!(q6TkZk<9>VmWkyd$)i zP76`7zf`Hc{kj`(-ulpecO8J2DRRJYh8XA@o$2i#d{3IFo-nkvvV1_J=n*W_U?yQ) zVHZo06CUD0K@%>q*~%~ob8zzHF<8E0DfIVMY4VwzoH40~J#gg62`Y2+83c}uA+TV8 z?{}?~CH$(!Y+sxbuoJa4YO-y-6JY=>BSBp-W1VRl1TZj@z?~`)Bt(VKRbJSN2@G_N zL_wu($Yr$?7C~G62Cf3ausCkmx=%j&o1zQP`$|Y%;^k(Y_#IwJgHnx4jK*~1ck;f|_ z_U_oQe`MLsdj|%WzHMl9+34iS0}FB7O7V0Hys}0j2V`ZGQdNQI8iG)ULp)zGof^rq zDX??1q%9~tpnR4VxZVLQ=%91lJuj z_a1l|A_oj-h(m{VA78op%D0z_rPauG;lzj&JJR;ZEMVJev`ofo1N>1*7+45w%Tp0S zrjoN)5VsfL@Zp0nG&F2L!g10nv|4TF#%ROMTVC*FpPt4kbb|)}F)d||cB00xrH@*= zP@>5b2{mPMNAO@pktpoU7zR#5RHU_aAk`QMBIqEOeIwXFFlDtJnGzTrm?Ff@9uvh< zrT>v{ee)~N!nump@skkrKBfxQ_ZmvEq&g@CuqjQjsR$|Trq8a)_BCT`vcJ4_#>_y3 z0_}iAnkF7t@WhSU3@b?syG(9O;;En4^__P@u2;z86&BBJzU{fe!K{~=m zxXeI6Q3`~KM^3dCqJjt`ZKql7^bNL9N)f;boq(D?r+mi5fQF)B8r#)V0(;>Ko#A112UWpjPX56S@xpI)HJw10mD1PPig*`5*Bs7Y%93o-gDs3e~M>tt@~7a-oZ3-ZY06&ky?-|^hg z*mZkWthx5bT8(+NFgMW->^LT(ZhLOo)(mXZKw~WH8-lQdA(U2CLLgeuVOwZ3QxMQ{ zUJ4`acp2CVGBK;Pi|uDL; z2q^SU>MyWPn`VFFHEi2bL^@@v2=sEtJ-F=!^txh z@nF4~5Ur0tB;o_E!v#uJ&;%ND(=mJwZ{zbt^ZWPo)Xb$1{91qBPCKW?tV1G$12$k{ z;b9nd+L&fc!sTfRYny{Nqu9l$k$$&L_v7}Kfq27Xeqebs!+skG?5QfD-(2%I!6VsCqJh0)L=PoTFkv8^k4$I?h6%^iR zPPoXXAhHt~T;7qo)X)b`vmBAu-tT1rUCM2z)7a@0y1{~OH=cv$%)dy#F-NJTynj^mMGToo<&?0C>=Ic-mK!vgRZ z(3uAW?+^DA^8pwZT>3;L5KauKoQ<@YLTT&$LT{tZ_ec$((7nC=KRhtHO!wDJ3p45U)Y~z+He!h+uAo6CV!lp^;-@B0iV>$Z2*K64 zq2FO@W&tYI5RRWX0gJ|#NQ%Sezah(mNzUI#+_+yB_hyeX^>0l|x>*S;z_a^K!52f~ z!HE4XkTr?ph&?9STncDE7cwK5LO@fqL?Nq!mw>bx<#y^^Cv8qY6@A1)J%)va7BL!_ z&fxFDp8^3xqhX$}BSP1zr`Y#qxb@br!G;azx;vY0#C6^n~XrAm((XiKFrT9iE zfeu=nC7QYevyLfng%vp1<1U+xI+V&4102efX6A1~9~pwo8?;9a3>^ zBuWl45hLmkC{-YaRG5~i%Yj;qQm%L)z>tuTC?={M2tdRMpG3t%o4Q!y8@F5B{W6wH z33SAy@0H4Dmk-r$Yi1u1Fqm1nt<~#(YV{4{pWU$G+?NSWJvr5cWxrn<2``6Y-;Yqc zdhmOT+rbH!F9kz8;$R>kfS%AyTY}g>Cv6cnCXRv7q$rJ5(ysr{1P=dam^+qRJfEky z?6M#2?O(O>hnB8c^oG9SYp*YrYpY9@-bK}FUvE$>sx)a6FNdXT4lK%X86(Ba_47K4 z3W>pc)Ajl3!)yNC8+X59`rkadXUjwP?s(+hr}pmMkf)T+YHVuXfBXOXI`r)){%I~5Hz6cK+$y0 z<_P9@MyEy{B&>@Nqb4_@W1tJ9lue>oF_b1*xXZ9mAjm>akg9FQxwcet@~9jHtaWR( znotU`lB__hx|GQfslZ@%!lYxUs-jpZ@fRerA{D>KfbR#X`Bb5XVwuJC2pY6ahS>Z} zxir3X^}o6PmUl(#)~)-)KmYSTKWCsJPf2`_IC1#7`E~2AF@sL!E%^K3B#IG<^C438 z^wYUi0{@Ie-Gg?I<3JTtMFRUC3=a0f;LxC?2hy(A(%{k$Rx5zfnOEoV$S}u1WX!h} zBxXiR0Pc(v%1*h|0;W<>vw*ISNWC>h5UD$ubk)ItSexiZqiw)co4^949`Jh+xJYSV zb`Q)jv`xAqF@MV#_cV~Uf(IVoc;|LFFEJA*3;J7Ox#t#>L|nlI(b8E`025J=DT^07 z1dzfoq<-o_`{k477fj-lNptQ$r1MYxXK2k|P)%cg07I?6^L>DHh5scV1e10P*` zFxQ=5xUCPsFN~>*mdX^0U}hfphpN*(FD!B}i>via=xFGzhjr zKuccCni=6?gIb8?%a>CwA1CHmM3PpUkckRy&96_L`=G&B>3TrI0~Z8fVrC6+#UCfB z(5G!9)0}LZ`}dsEv6f)IQpcaq;C$Q$~c|yDVDev-GIO$_c_|AZ6xj@!H>8wEVIkTeNiT z`k_V3%f0fUvyam3(5)oiMVt z{bC-ez>ejV&P_P|LC%!G&uc5eRN&IdJO+pKtOE_;x@KaF?4(kuU0w{sD^@ML^zJ8~ zc<`*J({jM@PsI56CG(X^_0~e6u+nyX#2*hQOEfZKAOIp*>`Wc-#GFm)HqC$s*!beb zW^z_sF~DL7VI-7e3;>jeaDDb50|ma&G21Q+ojUIWDSOmly&e3pV#-wX46PPV_^h)4 zmkKFLNoBB{T*5YQkcD9z{F*nF>Qn3f+|R2GamPk-F9I26SHuh zB5fQ#3cBx6RVd#S7AlKLa1=x`$OtenIZ+TXr)?soZ3oKKSw5Sm&0jLEe=TgxeXzZ- z`LP@dFjA=X^h2fA1Ep%!Fb5@? z@QYXqT$U=Ig5nDT-C}l|yUCi{MZR4q@NM=OVM}zptwd~WQh2IZDiw={wy5?EU(-Ld z_`R#vUHYTzZ~k$8$*bOU@8m6IX zSxq2oq(Bm@DT4!YSOi+3D`E6?HXza!3kj7=**Yd)7)4$TNp%^kpG80ka{h}5QM6)# zcs+!|B=9j(U=J2seW7HbA^S!FWT0%DZLKoEU@t(uhtSR%!G#iVBDUGgoyf|OOFlzN zpJ=PKqP?ICghr+zt=9T4tM&H3>e4H(d-Sn~?>(zbSPmHei8ydzd%R@X6+_i(`R!O9 zh*be7{6nxnz62qQYf>wM^k{p!rBuD7S{B5p;G zk0Ype*MU6`L2S|$rR}0Zs0xObC{_Cwo7BY*_x3M(*Ojk%cV+$Mui5d`W8cU-j=qGL znAn(

|L2$jbGD!%MH4nmp7@lXeoykHC|@_&N)N8pz|4*2nJlz~z^giS@@y%etmu zJz2=y$)*aFUEL=#@dq8OV<5w1$`e0L?v{Ox3I$>cxOKhC#%#$|lLMrrLIN&R_XKCQ ziXx`>55r7aDAf{*Sp@n1V7+ZnCznV!PgHZJWl!J8%}ZBZ`t2t+Za8a; zu^ce`(_!X_!rqb5CI8ldgd!dYZK)vOL4=zyf!0FdXW|#uRWT|hb0?JA+?GGVPMMI$*$(xZ zAN|nlRxi8$lPfO0@xzOju3OwcJQmb?2T6Yr70ZU2Du`Vg^;+3fZ6KW)lyV&=t%88X%QCJi zn^*`*&%mLjB-1hmlCq$eN+}~`jq^AgA@2*k$`8eW$?6`Ws6-5{O;H456HQ(%IChG! zg7oorDGL6$O2+FJjr*OT!Q-55S|#9zi1A2W?+Hm=pbq z?I}15kpqSo5Yy8~=2om)e{&cWE(?iXXUZN+aO@_H0ocF?)T0I5( zD5MTx;CEsl1p^os+A^{GVo4=7(JbV_6a(rPkYR|E%#G3`JoqZ$E|~25kO?n)YXuk? zs;d6}O8L-%Xa5e{i^KU1oiq+T7nDX1s-X0CbD|b(6CBW)@KH-AN^D7uITQmHHp9Zq z`{Ks4-+(;MdaSOIYmhmZQpMJ;+NK5we6T2j#%#Q4<^uu^fhV@a^aSdn zQm`ga3@}m>KnA3F;I^DXC3mb%*qjgDBx$io6&YC%p6v6;7jilZEIGyTDp1lBTn7RA zRI4~Dl*dLEt$fFo*WCE@gWtLRAbg*Z1BMqEOP8&SO!D$=<_9a0Q4!|k6A+M)(&Uam zB$*#U*6q<3C0gT-YL&%;cyQHfy-t9LOa*tn4CQtxWtM(qpn=jLGErNRrL}%;VVR>? zN?R6zQYWHiuwbCwN?eCZER95AB7%T=BX-TGKn{Eoh><0+za*|JpmRxcc>fwPf{`?;2jTyi)DyQ-x9m zO64lozZZo~@KO#zcsiTFle@Hv=8?o=i1UFM%KmD%JkW zdizIz?24P-*`8~bpFMTpiJXOc5pm$T$EIro%N`wHa_Kwz29_+DojOual9+r5Oxi(9 zdID(9Ra~rSjgRaK&FL1H7N9YU#pIcrtKCM+%AczjG-M620?{LgiIUFvT^u9G(?NF5 zvo#)@MX+|zRyRR#Mbl^JNnga3T;!n~yQPJpMpgp#ZH1_qeFxjs8#2VeZMGwFmb3zF zaF&?#8PjsPf3R<0(VN$;UAO6x2k$-%-$&$t;RQxdb>QH@!03AnI$fltHs5xAOcM(l z*oHLKnOh=&a)5v*^n}`A_w1K zAbtzTC^RqyF{k7D@3v*W+$y^R9;1Y{uu@6sKujxr zn&_E6#_cQs;najC_Q1XyR#M=&m)L3s$;;wrrYZ{=D+RbH_kJ2&1qKPw%|fL>HqxxL z;Id1tEMQWqpu$4YjJ4)EC{Y(rP=@)4DmHavK6xuHlzMv&WAo2$eD&*|+i>@ncf(7J z95B4VIC0`&{j$riy{uF$z6O7&l+Y!Y?AtVeHEN1u-_(g;Zv`}3SbB&fn}ev@UyrZ4;ynXH^0K&je`LPE+BN65Gpq~tO31dWkp7`&82*r`o;eoaE> z#P6Q#(l>z4p2raE?SMnagU|^uY*3Edtp3W?p0S>up?9pg=JorZ{LUAjfjnMt?0)uv z10zE#cZ@B+^2cgDBLkDC_RT59-upJS0HQtO371#kncl%A5nRuSj0wdbH{orGMo6Sm zGvopXNn6;4R3PJ^b$sRLSi@#@47RF*K|{feu`%23gPk7|>PHcZDAImCEQ-0XEDo#B{;3NxsiM3J4aMb;!~XT6W5a zIHVK@hV5!;+gV%o!G3OHWfeI0N_s8n9|96h4i1#frAj^ykyM35=8rluJpA3SfBoeh zOw-1}qwU$9pG+3^{$*M}bX(dy@&q)GPeLANJ+8a;=T|LXbL|(GuDRxi`i2&%a;+DN zlzym?VG@A_VHPZ75crw}sbC172PpUq0tBbC2HiTF?rRXX!9Lcpa#hgDRzPQ;7v+G2 z1?|W-l+6-v@LgBj_|DSf_ii%)^hoa6@d9J#w)=Jsja~Be$l^8c=;>WFI(cgUTsC2I zb%bLK*d3DA<2IXbCw9xyYFCaZu8{noxf_b6fNiBQ+^QZp1!2cTE4)NNV9P9(1p%_4 zV{KYQ@0Ah?U|JzIufY^XXeli@*c{+-fJU?pz|F4;g%W}VIifi{>j@>>VtZ`XUx^M2 z_ZCJqohGwsRHy}g{o_A!(=BgnCyis@eKDObbHMN-;?$`F?bWNU94S@GZ!?ph!pYWx z2I@+gxPD?C?0^bFX%TI;2mJdWwU5q;Lqq*AJTxddT8~+<0xJpSt!6qxWdoI#PLlS% zy@47F{JaPj(4N_`3zB|Fd{#k#hwNMiM1CKtz7T%~??4p;lPoXi9F~zihyv<6ftTK% zBJ}l@87xGB6Z>$=Bwbj!@`gJfdg!i~+d1!iS8KfD;>NdraK*AUSAKcv+G}s@HDH0O zAIb(OlpzwZKt2glF4-Y&3?(ghCUBrSfx`1W7sxT{0B_*y5_sRTV0&Ib;h%|?h0s;nw2lTx6fa;0Ecl-FEy%d3YTx#O{WO|mmT)?Q%jeCFPrLyIomvv~RXACC$n_} z7gh{P>`cIKhAD`Y1&JN6j{@uEU{S1FbgHg{NW}DpX((1vp<}^?H$GFPo^me1ASIoe z6Eln?%4T40X~TR3wZ26^w0PO2g~{VP9zAm8g@M-`FucfEvgFEU7=>>y6rw@9YPW_i zRw!r>DrN{mv#RFu8_soQ$AHxYRH{W-ym*`j8_OyB8r_=c$Ohf7v#UL~TJ*qzBN{r# zb9pQiL;9zv79j-w8=AqRSgPEx{>B^Qm8))eX6qAoEVAS&4P77h#6wubq62E#8wOJbN@T;|ft5>YOwi4G%oA&N~@}KM)oCAgz8SQp$ zYG82SO7o|?9yJG6IEeXq{EgbSlu-s6q*QTbjbcV*6v(QU^7(uK1R=X8jxHJ{VGBSe zVDyEI9IDHtNk%WPD#IRD*=k6EZ9v7XrCYRL)J))!7MTMQKOaumm{!5hPrWT<_n}vR zmgv(p6T~iroUULmicFM=-~xeEg;A>y` z;B6mq*|jwF=G}&wq|yf4k0+lMdgVbJ4z(G z00MYg1D+JT=RL8acZi<`GQ5A>%`S|PEdVUbU>^s6Tl)swAu)`Kp#e#+56Y!@pHAU)Nl+{K~hs+wEXsej>JB2{fhRm`A@wmK##`AMG+0G|f83 zL3`|y26m2wL|LH8$ChXlH0!qLxIteXtl%-nE|x?l)aBMX0B39!gVK;R4b~tJpTbHx z^jlC6oB@1>qOoKqA+RR`q?FPySzPmxLbiNTf{0vu49QQ(@y7iz$XABy+YR7A^9mhC z+qr0|SG6AbOLrJ%B-0EY@Vn?q7RNuGmCMb>!S= z8(^13)E;13g54=aJ5XTeLyLa{mPsbA85U%vkzHUR6n}jS9LQQX2wcnVM6MA?pl$&{ zNzE`RwOT=%{6zHLR*-2AaB~o!I(GEn9WN%YmdC{rt5&TlFTVVZ|7FRVE8jOby2PX* z46{(Fkk>vAnwaYiCEZ}%7sC#J1s;5TUxm}af=vl~;KDI=U82SyfKIjmpo29~9W%bl z&P(q*g#uh$XbT+V{M6b0tN@yUJ*Hadzyv(Did1kx8C=6G z%{_=hG$v6QKf%^bS{|UP4Q0O%MyeBb(WdW2-^H*Mg|xSKD{cD%I-y%yBnhJ4@oftXg0fZJf1r`0b|$IL)E0VbU?=*>V~BxCYo&~8uQ zDl}|*!KO!;EeKuO0s#Tu2jH?&1PhdMDWNlD1OxqL=<6*T&`_rDZS@)!kht+R)#Z`l zv2WgS$L)Dq;s-3Qc>T}*%JQ|>|A*m4%cF7?1&%dg77E5X^WTL$UTEm8-@WbORhPXH#tmp_HfD9bKGURfOR`SJ#(*(- zu;I_3rCF7iLQvNEk-Zss;g*eYkp+kjT(A&QOB#ax9H`>WRjctLkQQj|Hg%t*G=$>! za8+%TdM=RrBVKSHhF#4VjE)0Ps^nBb$pr7Vq4^+brx99CGbliB3jWlN=HkwteCT3I zELECIJrP?Wxq^~?0~(5PPw((`V?#q%uUWqK!N(rkaK=VzIbe7Rarp3c^|hBq*MvcI zUDlw4CvjXk<3y;F)b(P(fe5L1T2&)pD6+1h5Jj}Aio|YuY-|7-LFT^9Y*Tdmvny{5 z?THaN#US9oA!Wa(gx!vP7uqd!aBLHUfT0{^#oO9uQ)t@MPO_AW7Q|!0iXAXz>&9$nAR|j~5#0+^MHFjx4*eGPZR6Tjpj@wOh?OOi$n2Eprw1Xy-J_HSXERmjqe|g321j0a= z2JFF;#^yXIvVFM)3qns(3vDGlh!yT=+oK14ecEItXt1Y}cGy}RD;X47psU!JiiD69 zT0C@!l1Nn;CbjCoCA|Z~*Ise`Ywvw{!)*)QfAAbIyrkHwS1eoA=;`VGfEnn>lAOCh zrZn&sX+l)u^abMXg^WT$DkHFA4Nss57Oe3HKGr`lkd-F}SzgOlFOcs;%*~{%5t1#z z$#-8}JXTIL+lf?quzRKU-WzWs8-OCsGF{kU3ipAj7)&=H zDB?EODS?9iz7hu3jj3aanZ+$tAAv0 z?fO^$|j+e|dfTtg$-V#9e&{RFqZizV3tGG}CHUjQPktVS$ zECX`=RsI)bb^#nyW(63YqL%sz84!S9&Hxmbkrt4`%mQgVv|tW){Ke1QZndK6shRSj z14sK0>_66b^w@NHW-eaSZmKI6EnWS<_NO20U>b73@DgM1-t7mjx%S4NFsp-+Fk&hm zg^a2Ai?bn7;QSdJ1s}0~0NDlv2y89Nrbmb}$Ho>D>ktH;s$ASJQ~jw-{GvYyG6%^- zT1#_RNbVfj5_SO}$M?aBoK?UUU?kKJ0@(#jOVDH3s7m}CvQ-p%C!yEebvrNtLb?B4$1?5Qx?uykbgwWEtKxxP`Ki4D`xW&i+O@j$;L8cFlS ztYrO7Hb=Is!nyI6WRz5>8HI zM8fpeB^6@3YARsY#(;oG$MVV51%qRLso#wO2nBjCO2jZDqQNl@_=%Gw4kjihtIs^W zeaXg+&o1AwfJPZot)LlY{GjI5v!h!Md1NZo_1qzbsomen>XY87tRhS5cf41HgrH?XdW z)Jd|V39iQLDq^JZ(GwQ)GZ)TV2wke+ zh|GbB3&u1xj6_6w3of;k^s*VbA(OhZ025SV>n7<@o(FKjrm1r(TVQh~2yJF=?%lI* z_^!JiSo7o)&n`K1c%nB++66ol__K;p6s1aVLE5{f5SFKQZND2`ZfS2hVE8^_>5?TA zi^dkc*Zf8`VhO^mlVjjj5#Z#_6lqB52(IA;PD#il8VE`)nY=DmOW2Z1qKyn#s5e@& zdS?|ar7c8AgJ}=cA!JNvrh&ls;7X26EQ4SHJ4{kX%Qm;cb~eudSw7DVmOKcfc(Gb7 z5=($74}3kMXk<%3*ab{2pf&?VkytueD6po(($4np!YE%D2F=1t%a9<$FhAEge%GB} zxdZaJXk+ay?^(2J^>rU#vTFThrAkea*Mf)~13Fg>Sa!~)nUK*=>K*KY%2EhtGGhu| zYC%2kHhx8geGf4j1@VCb=$iCxIN)p-nt)Bz1cb#OwqO54b&u1PxCLY=J|{ECRR7Rsw_+y&eaAEcN76cA|DY zw`b&A-@50L#~$0fq_I#hAsh+00S18?-ggAN;Ee?xqt`=WE^Q{aPfYBja?2brd>^rF z+4#vtql<4X6boxjx-tv>bFt+^CU+f(mEl?vVY)_3MM;s{0$Se~(c7TDoF<`L)Cj6mTVNk^tO5Xd)ohMU=)UG@<8*ezg|^T0=Rr`AXkYMXWeW@# z^l7h8k!2V(4_1QN#+~Rp0C~LNIDY8q`HHIC-@ka(jYFfWFP}QGZ-L5DlxPS72B(>Z z0c7EYlX9r6Ho>Nq_;`H&XGL=JRvrUVC1JQ1+aq4V#6Yz1P4Lx+wI-h20bhEaHWY-)PG93o0k zmJ!Jita7DDAu%rEr*4Mz_KCSBeEImHN2$k84j8_VK(B!7u6xx;rBZscIq0HPgV!OF zjW%pr0b59yijm~|_;U~~C0z@W0Wx3|}<&a3pAdc948MU2oB3u4&PN^9$EL4+N! z?25~7Yke4yQ9!WJj=2^X4{#JR7K)ax194ttje-p#>@>K36ZoE?oPemW!2O2OWD30} z3ZBn`Wl;!}M49e_|56}~MM7YPKbHz?1LJo*_Q<_QA&-kL*5CU6#p5fl{G-wFRjZ2S zngNg%5`t0rBr*%=xq#HGSN#xrsffT$(kf#MfKvc=Ri8OMX4;1^=wu-Rj~VFJGuZvx zbC1Gf8vOgb7K3cS20j;f{g>Uhtbu)REssf-Pg0E8MfUD$@?|Sr{;lX;v|`t$4WG?Z z6E89j9(-zc>GB(%?(JW6L#=mY<^1eK%TWDD^YZ${{+M)(;IEW=3%yWr9fKAg0gq8^ zA6+uXCTh+~#3GQ-0z~{;Vid&U85fEO6mg}Vva4heIytRaR?EQufY7s+s)&Fuav-~MbeZ!R&GPYFZ`2E$9{cXH2k(DmC2A8;S|>g??z{3t6?!`m z7DE-y+qLKe#&K)QvFVMUn>ds+3}+>lEn9i2r?>a*rBY!LuPPWZG6l~}1FoE7ENN6+ z3BjNt%&5{(QU;<|z(KXQx1TB(@PffcPxfH3WHh)Ov zqA80y5pdYH+UB}sSdgb4xbOB&kjF(A*WCQx4-Sv5_yDpC=;RoYz|mTy7o<^-a)2T1 zuzlvycpxElDQ6Efc$o?HZjrTUAE=eFK^tgmO;!%J3Q`@X1H z8Jw9s+E9|!S8T|@EP}04@g`9@DwInngcWHjWyLVoQk+|u2SHW}Pt_8FNbUG$nK8@$ zW+{+>#!wY#L7igCMC^2tKm+M_gAB}4#e_|DfgHP1eu@8?1!u5l&+ftdzJ1@C=bqa= zn#4E}X~nI;14%o0i@-ow40!_vN|AsEvv^Bdw{N_2<2P-iv>Y&epRs-W)`{iI);w0L zR^M2ul!q8(1|m!h2|%D=F`>!@O_MYJUQ}Tasce-4;2aiP?KaeU23WHodI)Rk!Gn&~ zBZyc~x<-Nmx1zUT!igrSLuH#*@Tn(kM4M)6s~v?-k6&xsI+d6p=blRV zk3*44Cvku5ZfW%kTv8fB;ISn1`I3w!L0~gC--79xMssRv_KwFl-f<^%499s~=yBtl zKk%btOV|BkwRbQg?E;!K6|pTZE5(U@lQfmhDLc`lG@7DH8U-%Z;J|=uWvx2241Rf| zTYY(HiB9&`jkO&Uza4{p5u=R`cEQ_6s2CUo9)UhNwlo1Q$nAdXqQ*cTSmrf&_apu$2w z-Huz+DoN(e;;U&c4fC5cO?n@h`G5*ENh~PNN1RoV&X;tcrIcTzu~cbkuBl@HqtVp2^2{RLOI65EbUBNr3gUuv13R3zy8&)UAceXp#hz?Lt+V(RD}?$ zAVN!}gjG;rwF;PKq1)0UgHk361(@yUW@nH8rvtk;y0Xdu@;EE;mCt`>)5NKXe_;-@ z;|2t1T&WidypqD(=z@r=crG1GCGXFb39b(WQDi9rh7%`Gy2ov5fhSch)RxVU>JB{s zhHXua`$7+fxrHXo%r>Zmk{eHj^m%%Eid6Q9_bqUs)JFk@iv?*C#TTl2Aisr>fCWZ7 z#uCUB7V0rIw!-_i?9-M~hEaFBZk2sX&26`nSo|VdZEZkKTwhqYV{-cFPtQ*u{%y!x zODDMYSk=WG@Ex#PudDumMk@xkT2?Bwk;3;s6#K2zB))!iUOXQ>G2dM^bXl^^ywTbCc$T)3fHYP|z1 z-}xgy^9zu9P3G|;qpF^MXxDR(f2dR*oE=|zmTC9c-FP6tFH$>u4{<)fUKfs4^dCa-iW=yA?+pAANMiZC|_n^2v$mvcghJ z5Tho)4%yLV_z$uwsXN|OP;t69Zj5C_4p!kE(bJR;+E72$fX2LFZQ1&hd1`GnQWU;SrfCpS<*@e_G0h~kCQjW;=w^ngW z6LCz{2E;O8e+Xn3aNh_7&`*It0D>5?X%3w+KYKjb+IYSlO8z&H16lf0Afu2bWK(TD zAW)l8tun}99fNX%ye+Zh)f>A~#tCZ|@bgVi&F?vW^5lOzdT8$-ef)p??jxHwKa{Vk zF0#1d=J#H*;*uNwxOZqYB1gwUQS6^1cbt(Jm|9o|l7h%4d}?cLQxQR@xG@y;;JZ5p zXAn7BkYNW`eB5qzqx0O?T(Z8Ib-~-$;N}OU& z*>O~<4#cXJm%irlR5u!p;sf_Tvig}VPmi}-?TAS?EQu^I(*Y&fN9e$T)jDLRCVZP) zU?=>2sk=m4pP1P9;Ro*hVu$EF2MlL9Hf`Ft_4?~yb+S|}yrmEoDpnw9bG0DCNZWvk zY5%Mo(}DwPF%8(Wq2^ZSylRiY+MOoSVUo29%C%G_z=5VrW)falUkZx&Mn_{oLpNPu_Uy;tX}gP4D{I(WPtOWmd)}4N+nHXA(JP-V1@4 zm4)5qlq%CO1peceEfC;oCwis4$_Q`brb-@%2p|(O0yyEL(=4EFsxo|9__ zOyn_d-aqYjM@gl38^D5_RDgtAdg27M_|)X_yPkgFt9u}imlRmhmd=-+3yZHNgGrTVORz0NA}>FMdp{olEN-L74`hs`sr!PMO4467WJ0S!T6 zjrrJfl47vcd;DdUuc3fYj>Xu0t9pIzGxvS-&;NJ#-!TUa=O8w2eE8{WuDO0krB-=! zAq=aU0i~1`TDL=>d_G;m&YyuMShG#N-h^6hSUOyC!z!wj5R}^{)1}ziKxdsZwewv- zu&_{X5#xaGgDC|PI+h{`Jc?M(2B(D}Z2>w|hPJaM1D}{mAF~840tX8iFz<|@z@|Ca zn2J*pR_thVy=MQq*^1T4lamkaKd|>hzx!Js{F6r?y6b4(*81X&>u&w|fx(d_e>gC@ zba|!Lr%<~wJaM8&5w+pDg5hT%9Yq;};%k9Kj_EYmvQ08M@#r!>B#{Nd??U=EbNHoG6UPtz z+@bwXo$CG@<{E}`6o2`bAN$JW^whsL2g78s5Nd1VTp|qs3c3Qxy^2Rc77!X9q;MJO!6KsEdUt+u&E2X zQ>`;2C4V2X`0V_Qy7Rqk_!GbTUI1B2#J-)*G)OMBY#9u#9rbMgBQvyW^{qF&`yE$6 z9^ZG|`PGkpYX9!dA6>ls+P-piQJ)#NBD3%b%vcqe#ZRDB$p8k_G_(|zdCT}mW0`0b z5}ozp(byX^jiKE_-9w9gH)y7pUP4yWfFO{nbJ(aJ1_qw8N6X2f1ZiuA18K9> z_}h&SeCmwdf+NV|T!sGgkNvmXKk|{^_~6jcz^4j@pkfXRMNOcBj3uzSD^$T+PXZ9S zN~LJrG!4VU{Z7dX+IFCf9asS1A3alOC$2;h6u8{6bzHRDU>bu4Nvw1T1sMn6i7bI? zfm3-;_Hp1g+0@&jo!SpgTL9 z$Hf|zO8+h8N>2${1@@-*s9UQEkmJ20M*y8GZMqNH?Ie}WZJDHfoqz#r$8P&Vh@qZ) zPXIVAgFRhU3A!C$=~iz2f!m()HG zX^+!ts6rj+#B8f{WG4EQmODzdK()EKBlb!$%PaJSqN+t?B(Q5KmPpc$h^9h^+ZfPf zBeE%JWZ;0s@8my;3r*BmB&pi+)YHqJd1mX9*Z><4F^{d6VAdlNbp$?CHE&`*j{yo$ zSO-}3khZr12_7vo0^A7=%bZDr1_-{}-`UTC*0a$m&kaY&)n25Jg zKxqd$>);p!ncl&bxXI_@&Y1i@am**pykYLJpT)vLLR}@>QVL0jnYL*Zpj1a+Ez>X{ zqf#nHIx0jaXIOvW*onjc{>#s9{$C&e?cY8DdHle{*fnpjEnT|i!vjO(S5|xbp;#)j zZXpVtyCd0xM{Kcc0V=JBL`aZn8M2Z|e+7;aD4W{LK!jreAY0M9`(3ZMVbcN1dmX*M zJQpf|Pv8Mx#~SmhGldT2da66kno{qgwe>5J=Oy{p;RGpFQ{Z_rXiiU<6>!>)=BI8y zaA13GUkt3Vh!z-_PvRdg^BV%jcvam26$L&U}RBBQZB^?%e%*uZj7gf^zntm~{ zRp#m1nk?A^f{qv}p=<(IDMb7l0ka^fT`hV9*KHEGG~F)!eENz_9)n=(wz*}SHy>Eu zZqFE0kSGSU1=)wS7Y$?&<|9Wd%=`BOP;~_wPxbNV(WslnEDQ@Bl`X5pNSkvDvy=bv zzPmp0!1Mjxa=>uzLr+hidVJ-|wS{7_c#An$LajCHS^^e8hLH#vm68F%ie*r|*nqyi zVG@odGLa*V)t07FeBiK9CW5+ z0G_5HBP%e@L{~i@Xz-useMls|sB}?|1_TEhIb)Y0&Ko!Ly{f-t3P}PGLw@1#@ie^G~3CgD2 z)Rt$WRXcV?OH!zX)LAo~H|G*JjI!cb+lJen0SFeLN74-?0S*0fkWC4TEd7U~akIa2 z+E_UHKW9&D`1EsX`QasRD6m#5=OW zz|=sk)<@4PN#?R6A}4RHw!!9%Ic32eDY0h+UL*5BtA1&3jmHb66m&+*;(L>y0+~m} zuuyMvQz&XV%^)RpDANtTQ ze)Wz!?wEo+ejsDbb#K0^r+@U9d-_L8rAiOmKSu?2sSBiAX~4DNTrHs-P?d3(4VoYW z4FDNv2!c*90m@?r6m*ChePHl>z{20gHEe^^0f(K)ec_|M(q4+eK1ai_&cK1z&wugv z?`~UPmQ?cAcwYpNNg9_`oId&8?|eSDOFv6-aMu&{%|{;FaM|ixt{EO%bN$@xM5EQN zCxxOphKs%C&%U6wQ@Bd@N)85L$dt8trsv40Nf@ZK&8b*iziV)Rt|qYWvP$shRWx^h*dsWDPK_u)t$+8Uyb{#Ed9j#B_w}5pi%C+?j%)AspoQ zO*4WwDh`;hSbFmK(WgJM@uAOuPv$oV4Cg7Prl#VVnW+cH7B8NzR;q6XVm(=R5C*Is zw-h9<@-xtY6MG9rM#rF9EfLL6B12%|BRMgaBF8lF>v0vn`lH@RNWep$5g>z{dDCJ+86XltZ>?;Y5W_C?RnTK1Z8aMk zcI|xjXaC(V|NMX7u;HFlkjD>nth@TwcMmOE{^R9ZFBHkyu}mFT2{c%sK+Kjf3@Q`w zsxB5Ga3#K{Pxi`g1OOem2nkQi359YTXp$st}>wE zRr9lxt>(h1cDXXDvHBrN7Qhxo1mx#$#o?OL0@yy9heNS^P8k3YC`em$jTX!RoWJF^ z5f-4bXpJgjM7zM_s|udl+*r4JPkY=NpHb7R$bg2zAR9m>Go(|%Iu7od2>{lX+=tOEKq%+EK7RcIKHz?ubS72>wIl0oLjfNx(g=MJVG$ThN7Fkqpo!yw%; zGduD7lM~1P__u!RSGPhQuZXzxhPVGr-_Y1iXayY=3)H5U8HO!P#6ap1T7d4 z`Hlk#kTG!nCGd`O%O+UH%&q#p@Yx7(`^=7G`x#ugl!A7QgKyz%H?i-hI^I)tf(6K! z20uli{d9#_L~JeD2{5FIq%#tip1`ysHWxZIb>h<#--9;J!R8(Mun%^^L6W zA6|C#2EK7T8B(YG&%ho?rRqKmOVSFXZo(1BUY)d-v{*%}RgM@aW=lsaUwy44lP4 z6yH8~Pd1o(ler8$%p`D!$~e_or%B0;+I?97fhyie z8e%2Ft0v;-kcd%}rIB%vt&nwekx~%VN@23qn)~~c$B+HmM}GBRfAh{ezcB-Oydq-# z`t`*{>u&xxwVr`Xi{*-87)lUev#E%iO_5HAnnZ{fN03e0q=E661n`dM4y$MHxlj-V zvkh`Sc&P*X8hP4<4@5{jnI)tQ38UA0$A=F36@1G*~@@ z6-uU(e&BJyrXw;CVL(HgEY$DZx#hlG$8eTo*N*$9#zxmYUaAeOtkedsoS8V(h>Ddo zEL6<`ZeD(*z@;Ntq@KDog*3tBltc&pWu3X&(gr(Fd@N*il`O~bw^loL&8#_iaAw^6 z<^lDDz@8O~4=5VLwK=aylTaXbLF)wq4eaD7l@sLJm=HM7AZZIRXyWuq!AvZbRnYzX_j(TZlH_8 zteT=|(jYYh7OK@^JqnWh=NG1a`&0kt&p!UCPyNj?$m10i{r#5@Ex+o@y4a* zR3KwBfZYnm<(9AyEQ+wl2=fja_8&O5bl<*{%M2J$2ok7Od$B@Tg@xwKLa>+;FEvh= zkU5hD1`)FsCSAeCUlH8~u*VyndvV-uePMcL{7x ztGz3V#lqF>X|6tqCe_erlU@OV zh%KpPi)eIX3+tf-V-r+c^tyvfFak7a=Ad2*BI+K5myttxv`dc&q_EaXK*QNJ&8V zb7G_>03j8Sz(69Q#z#pUhs{QP&lBJI!X1#uIf-4{?mZqy)o-s@dF@XYOFe_FR;v*d z${O_pacfpeZv%Jy1>#&sT_mt_!R4uR22LfA3BdobnUu0hN@kb!#KhFlj_3BSGfYH) zfBlEc8@3$!@&%R((U%bgOkU0 zefW`k{^b#P5s?Fimoc_(-CAF>X60Q$Q0g`LshjXIoq7gHGHD|XmB=VGJTgjY1_T6T z_Z)HeNMa^Z7A7LdKwZM@Tmu@-L{b{a2t=-hHI+RU3*_Not1R$2ys(QTm)t}lFZD7D zX$3t!(OjI=|8nya4}9RyKK{`=wr+hauM2+#$J%Rdy|jO1@y`@X)n3vLc#M@NMz~1GhgI+Fa40DF}VUVvygZULt8D(PftIw;R}Bcd7QI2zW>Qd zs8qkYcx>&DRC`92wBxuD1d)!SvKbp^HG(%g9;TdHU}gc+0BI^@uQ-lr<#Y;43>VV0 zA>f%C`%O(v4?eSX&*j*>A`FX)S%}CS2iV>k2>=#Piq|yQgAv7)I;}(6+D4Blu<5$g zQDq1wfb6Tywz{R;&FLqOp4#+*$2a`OUU-p_1BRD5wrttb93LNjxLm3Y1!3XFRC1kG zH(*7P#5}ZP7#SWRnil~B9)t)O@bI9@3bBhuzl3_dO(_YYV{KEYYLWX$U_0wV#!6TZ z2fW~Y6h>OG5(;3TuGLz#)3kZp)a3DBD@UjP?l1o0zvnuIJYdbWw_efHH~f=^VW>t< zyAWkn5rG8^S!rdr_z>fC9RqmJWlV#^4a#Z@x+*Fl0~1cypy4#Vg0Fe-(+U3TZCL8q zKF**)pT-=dXJQrLpSVxg)-~0pC0N5iS_UbxM9W~jwCZ#5rf>h_UqT+|G>-0hV!Cf| z*~XE@Yu;^`hT&$T(FlVg7BHCkcZPmR6=YW4L=uyLfxK;$1q^{>zobWmummYpP^2(B z-|F4B_sH5L4NJX4mnf`&NMgen=sIwk0ksD$2RcaCKpqU{c02)T^C@8QBYhySU_v|7 zkCa}ob*z&(*gkRmzz4p2|9^cBUP9!6;R1+l+qN~rcihZWd(?VpnzIt6Y${o zwW}&T$Z-%f=gPA)Q~k%x)Pu6@gu)!xCMFo2;HB47w3 zR~bRzAhZpq&HxyolV#{gNqFgoZY=|jiJ`B3@CRm}P4HNTjx>Sy`MXmEsXoJ(^j_na zNS^7nS!%*oD}fV1+V@MH=0Q8KVEq>|(jB*s;9*`#Fw% zJ0Cu_bk$9}dU^+M>gip)xZap+n9(y$(x&-MS^`jQ>VZXzN?dfaY=n3(_=r(73p!EJ zkz>iF&3LG%+P6jxEnZK@Nqz31xo*-#&z&X1*f112IR;4^-Sr!w#JLf&+8yXT z@C`fFYV15UaqMRwf8gU!!b^<2`P2mxpZUyZCTC`jet4l?zmJ_1Q|$<4qWF!9|nH8o`pjRnA}hcuZqmqR86vE8s7d(8x17c?4(rolF%0$}@RKE{%` zy>(&X_%AizHknGX(+jQwpC72k;BMHIgfElCtF!0hZ4 z895b;MLHlctVprlW=rT$%GN03C04poAt0{@W*7>tWwiyow&Nw=PU-n(bNDkdpvfCSx+zbIq!x ztWkDuJ5>4$s6YUPbwTY|Uuc zJ)~HpqTE5Mf?*Lc;<151VamM2d`4AK`6TAy5TskO*QO)zJPYMKj14YNj?GU`|IYp2 z{`-62`;0s#ae>9wt(&J-uU>aou~2-qp>#k#e&9&(i4>myG4e9aw!S!JHZ_;F;KVGrMEz-t}r6!Qft~|g^rw72wY`^^&bJHIyDNO4naewmcdT--3+gr zq?MhlmD_>E4hp&imGYqSo>Shw0lL@$4Q_`v)!@o0LD&x9$5St5(akJmsR*A%=yZ0p zsfaYSsfd`WB)A=N3@484`^N*@AKM3coTu2TcRq8^){)^$4lP-E^^Y0`Auy?-W>oCc zNxWcKfVv#RE+GcMPDyk-MKmCJUOY6@I4vDGIJZ8I%VuWpG0%;Hik6!5Wi_b6rRL;V z4pa)MCxO`m9IF$Tq6QrT1Bo_Rb0%UXP%KLT9b0H8I%zi#OrARO6Aymp@9&1Q5IJDD z0AuUcr)I~;*WFXCR<1Im?HbIBQ3`=_!PJ5j#uqQ~EO$AbfaQ~RY(=m@RS)8|KwTqC z+(9z%y6E@{al>33otvBd>zzCH{@XwP%jg6Iu99^({; zW>5eY2zY6WZXnFRoR*$&EQEIX3!U0NEvaK+Y-NN64u9zU#snP*gbrg?hMHMm?kt@K!SUnu zOB#(v&Eyr#Q$-+qXYy6x>IiBW6^zn8bTR?f{7E^aYX!25dO6(}dLT4So9+6OM@}4g z-{TK_{=0CNA_oi?XzbXrd1lqB)ejbn)f>%#yEMQ;n~+%ptd>C6$3me@+mfpzc9u-p z=);y^GW%cwLqtukC^Zpsg+%JqU8+@Y%-=D2>cp>ou}@CWl4hBVWAq`vop1qumwnoSaB8IWO!;})Dc zdf>}@o_;X5UO&%)UNiB$-qJTRruqiPUW+9YagxSnq&2?_mOl#3(I*t-F;EdFbg2vCHc7LgPetq0M_tJE=jYo@W8~O zL%;CEqhELy&SK<%;R23lpWQM!ykx~A)k^hEQ522_*sh8WChm?l&_4veQIl=2ijQ$Q zB|}{-y+rK-n>Mk~(Ogr?)TAGtn>cmyqZ=Q6DbF<5^JcJNI{|X-x_ykVwZ{@TEdY4H0Xj7TzUflhJ&Ehn z(+wRWND-Xd6oUmAsXiSnd@rEjU(?4*#of>ALg_lxBAqO02?PonjhgTxo(yE%WEcia zNz9x)@RgmLA9xz_IPWnru``+L?cX?5uJ#U(t+}N!Hb(Mj7d z#owAcIrXcLKKl3D;VefE7%uSG{oFGrhX%$U>h14+YpGBg3I#NvNz?GiA{s1PEpACo z>_Vbt0MgEEi7lX6w_we3%nMtSQrS~VLDn46n{ za;1;h1@>V80SJM#q;8YLBUtq{>B%9j&(D2l&z_zC={JA#SHA;!mk?co4}1rx$FCoYUQe|LUj%U^BYwCSArbL0%eMG`xAZaKVa)#VRYD&;p|%}q*b zt^g{PUTQFf=1n#Y!D$L^ZEbxQ3WYFkx0+8MId<&B+qORTUq1TL-^&G4d7PD~m8!64 z)%y2VYJJPO=@e=hSihjcz=?A+M#1)w@Yw{FX%{3i*5UN`>`qHq79m^dvyqc!80dK` zUX=7f+LdNFjV16d7)fHm!V3fn9sBpdfvgBq4=C8)5zeS7Wla)k7Ysl^{Q{;V(6k9J z`*&~q%Oks=JP3Kb+@S|6D4IBP+sx=t>)|Fu~9U58w z-cu(ND3q7aHy8Gr4AQLm?E*uZ^^{A!Hw>pU&jOV^YCIoe$oh=L0$9y~nal_cHee2+ zDQ2SZ{2^hm0TR&_W}BA54ZBQsYVI48b2I<{$G-h{D1g;)jw5FnE}}p|;xo@Ybzs@j zb=!)i@;h6txDtjXvydw}duJS2P|pxWp^hT8&~7*HIyHIXHy^(L_D}xVpM5MJWb!x% zaUxl=`nsEHeS^1z)TL6ap+m07QC>T1TN3HB3$`+XgR2;1GpUsDgbZCF_@Sd5(mm(t z8ajlBy0h}3L;UEKOyZt>Fp!*qQRv=phwu@!+|EiSL5sjq3#gL&sjG~LskT0GR?rEh zB9L*wa>{nA4vmGm`o3-V|MArELoc{3aUSP34s3g9zB0J_@sZ)B*9{CVyLw@6W-csL z)3`OK4AWqKbIMDr30It@tMg84NK74uYW zroM3aPiKy8`<=%g{MOK%;C#rIL7@QBv#B z$&<%^`x{T+_a8s`-~R6%TeogK=jbz!S8go3%dn!R}9=LC^?($^ziD6o&J0 z&}`hq@7>uOBGW7QR@wZ9oRYw@$#%v(U|PbaB+PZQz5w<4*;5bQ_W%45*y8*=E{Hg= zeZy>RV8zCvk)>}6i?y+4v)(Ke`*qx!H56H89I)g8nFT-qh>fQLVww;*U_&XJs=`RA zVrCA|pb0^PR7PMG1onhDFg11Lm(=_}e&WFgzja=q*zdzN2#e+bN+Ad|IPz{bk9AXIt6*;aULRx^|P&3bGj6_`*G5vi{#`9EF^IT z4lMnyRF*0bGbd~F%#L=N8V2KGOfzU4=M^`2Va(&4%3L#Il;mjot_RD-l_j*SJhc_gDIs(QfRnp6jx zL4y{{exk1MiDTyRvN;~7%yYdnt#gg0Lc<@Uk9pfPEO9xm; zd{)7hQ!;o+v+Yxzq`6_!1s->Q@pqmu>4Sg0_L5sZ-rK)){nW`_ob3GZ11tpY}@vgmz^=r0mDTe4?q0*`oj;y7a@;4UdA|h z@L+xE^u%L*gNuH?SgMdip${4qf(G(mh$%e~dX)~g03wx{7y*GDGHtV+3^4F4u5{Lu z0z+G|q|++!zFgN4krDQS#5ncWZ zw)8R)!DG@UTj+MX)rcGOIbgU5;_ff~-Xr1r|LcdBt-9e;L0G=b&;(-0RF{LaOw~gv zHHxC66zDiIG=n)N_BR@HUznag^uHf};7hyV0*M?jIGvskm#2ugit&Q*e$&dYd-@vL*D-{oFh!lA&s0R5dDD+kU z5at1N9Jls2>W#mfoY?j0Cm+4-Ik>yeeqiMN|l2t%>}9r5ojYwS{H zgAX1Z&D(*skuR9AJCL6T1!KZDJd{1NM+@J~9(Lt87n_7_V@N7TR6AZSL|R^@y8zc$ z9nfE$LG7+CZ*EQ5#z$R-_uXW0-E!CFjdrR@Ngvn6=WxPpwWkEw6|oM0PQJpjFot<`pG=&n(U83P zsMlJwJw?eMtitM5g}%!dX+L}An#sWM#pHthur zLcbYHVjpnw-kE9tL&3wO-1Ak;+4zaXs>tf8qLr0FKU4`}?g|P<&AnHLDm9!S#*z?I z?-bvyb)k^{`%^&M7SHLnEy|;Q&O7Q%T&@dfz;Cw{-5+j#IqxV-)%vvVArY=ukrF`plnzoGQt3xIKHK`n*= zVNeAS(IdKYu>a7g+$KCU-&910YyYBYkv1ZmI@dEfbaRw7HJH`+Y_srw1~2W;KT=E) z@(&&6l8QvFO*4xw?8#UKP;IIzj0&`ZM08Or4Bk(?__ed2ONISt}f)9xW@#@zy4;1+SI|b(X2#sq3NA763x8C330q!Yr>f!Lh0xnsHD3 zCBIzHWsEgl?$ZKlM@s0vvPzo@0= zS?3)CRqf8}X7OJzPW;b*Uv;2E zDLjb?uh7ZtQNgF6>eN&u-DcC1?~v6NXTZ~r8#`;_h=xhbY%iD=_df7En}offZeXi$ z?w}Ugu27RZpjkN;GsgmCYQIBwQIh2uLYRz1cZuUC&2)F5{OQW_-iKXgRLCM z=%t`n0zQ1IVTu4tEm_#y7jZlvV^QhJTae<~150OT84PTqOT77D(TD*;7?TWKVbD`S zn9g~nd4|0vT~k>`g?W(;Oh^Z1oyV_B$OYJcIvW4Bm%rA1J;QFsd4yfV@$3pn#oECd z?|^uU9mq{rPNgNaH2afqsLzF7R!1Y%^fhGK7kZna98G;^RI;@iKm3JN#*^TKclz88 zl$1{!h`zMiY~g-Ql<5b7S9qS)rJBihCOzf??axJIP(`B4 zg*~#093isvAi2P__`}C30w`WFjMco0JiY%&-BiQYHTx9MTqd~Om-xAe#K|0HA44b) zvZ#NnMTYX$hOtfZ$bM;K&6-Q*i4s9_nmO^z`$Oj~!46>FMQ7vw)zHOZmXO_HCi7#R z_?{q$Gn^4d)j(2Pz@IP%XH_(Pe!3uCKiaU+1%tU{PYp$A^6m%f z!TOj{-hIshnp7c9pu^M{BWV{FbRbrDDtmHYq%iM1WJTR3pZ*>o z?J0DWe=Xy7lxt?5;(jbJL}6sqMVw^JJjSDrG~RGsmgu3;(In zTg$%gXQAFu^Fid4ifvMo6DQllW}}ZYCo&#wUzL`sm0B}%UW#--j zY{QqgBw<5cwc5poH?7wjq7CLQ?ZC*>v^gORqyw*<3JDH&ExgZa#zYce0Oe&cU%;G8 zS|xZ7cm;sa%tw)*nyXq#{7_BPHtZN z3*j?9M^@{Rs#^`3Cjnn6ap(4-#o%1eg(!!IoY_iB2TuD|nrKlz!bS!tPfd^yfll}o z0j6&=!IlIKm8|d+epOY!jzoh@UL*zM&QR3PXVX(LR?1@0y3}Ep^`UTrr`Y#2Gly)f zpu72s2pK-PMt2DoA7q`G)A*=+Cp3I0bDH#j4-g#s(nc!LL(H;o1K4~HOl0T{K7ZG7 zAH6ECerxUt({~%Tv-`m`NsPBEN2UC)80J1tw7F|ZoSmscSI>7Be)F?Qe7q2JoA1(Y zmths6^-MK8^_1l)Yt868;R(>?nx1AO}$dA0X)Qk_-E+(vE84j5+rZV8T}1wIy08l zFf0-J(K_N?d7!SWCw5c}m0{{%1F<<(CWS$Ev_bZ6d$em!p=moM--)1dV;VoC)N9~3 zhL0#cwS#rXmR@~g=`+V0Y;K>nsZoj_6}`t{7gNOk>Ae{x!Hx0lw9KezA%CJD77`@6 zreH>PfSHlnc~Ot~RYKq*B*cNCXVW|Z!dRB83l!a)*{8k7@!GPTijnf>E>UaxY_jl8 z9iZqzwSIS~cEc{v%YD5ushgJPgul2O3Vb1{&OC2%$0_(!eB~a$LdU6m^$GX>(-rET zSK>JH($ejfm)A9-zLxOTNTt+$?p&FS9uSWmFw; ztb!oul78}Jf-=&p_h~3|nQ6LC9bE&mdTW>{3-r|t9R7)#V*i(6g(NZv8cShJ?yBQ` zskg$wph*5|vM#Baa#g+7L2PqzxKKlXhp4zj`zd(XYx$pofoq+KIf2 zo3phj=%&7@bMA2nU8 z@A9*r2{dRhPNgAW_QEJ^?+6n$UDMg%-Ih0~9kiC)D0dsQV8A2SWXcNYm0BN?aCv)s z3uZYOE0IFW&Q)EdqB;O?1LCF`DV^9;ai29Q*7 z>Ar5=8E4ssCH!5|FRHIHeO0Q3heZK1V@g32LMjYU*63-XID-&5Ot1C)vbu_HlRD>} zw~){E9SSX5>j9^)Jq#4m2Q7=uu3iivuS+L8CL}OgX;;tTbymUI^753Qw~Egu{2B4h zSr6d=fI%qkUvQMl6x-eLyq&4!$3-)26GM&%NkZAFb<9}x^pY23D~uKIb% zF*Km0H*%>#uGl6<@3UzyHI@5-qdWu;rq!E{EbPB#7}$PbxiH}&|E=qAb*W@WVDER{ z;B+(Rv8*R2vP!vcJ!0AdA^&uK3%4lS`D=lnHb0-I8icC*OIPwi88}?NQ^nWOakw5nS74R>D*C+`s`k_ zUCxFoGZ1FjuKf!$P$|04(~qtGM5ImLDh~yRo4xu~8K*Bw&6KQX5=kQ<)WmAd;2A7L zGjI*yQyjF)roN74aU1;e+-0G@rKyD+W%opIojDj_fgxnt^i@BMMwTlT>rz4|>5-SC zyuINcvYT)Kqy|#+CxFx5)zd5SAc=S5H1AQK+u)ouPjNoqH62fMMBGau876VoE+F3; zlVdd{E?Vx`YDN;q&muo044bUGyrmR%%ONhJrPxIA^yll#(^Ki62uvx*Bgfq2i+O*V zhu1#%wHH3yyo)aY9FQyMAE9WNLxy~33iQ+2=;`s!=zX)x**7LG3+++Ff!KAD^Q&R0 z*$;46%s5`79IrSM?PPE6ZkE_xhdP7O(jFAgzn!B9x2Us^zh{d|sJPZ@XTP`Farmp- z-@DNp3Z%v?x+g-!97s)mio+S?uRZZ|bo(tc*N*3O zqpve`z%=~W{@qXHl^z$>F52Hz{URk@#nGo>Rk__-ZY@^90{yPf9n~Rs)hu{Lh8X~2 zobm)6Og0oPY4mlsP`5efhA2-hdW^-jdCaT3AE^5KBbd_II2ie%Y>iG#4$xB==mbf0 zPESyLhF{7Hzi-D=uwsX?K1ExLV&d^pH}DSVhz<){D-(uML>Xj6-K+%ic^7h(pjm9v zChAubl=2lmVyWiU4PA5klV50a^lQ4~7x>&x+mRyix;cQ5f^#Rx0cH#BOV0`)Gkw#t zN7icG+-HS&Ur2v)nHw~Gy~`LNZSn_~{jybLoF3dyp_3@=#YPp9yy_<7osIVIMMTeG zR$@~UP56WSH5SgtsNT79lK;_PqND#Y`xn!({L0pT+l({aDo5_U8<-VNtDmUJx}Ck$ z7ab~=m48v;ww3|KaR3pe=X=yZ1)-mO_$VOM|K4>8b-eH2y&p)xhN}Mer24U-@d}Bj z#wJ{oYqqJM;89~f!k^MC%7Qfw+qDYPI1D@TtE9cwNX?fOSn%i0P>;L^dgwG#= XKhP1a^BZr2j(>VO##bw~?qdH3CyNBc literal 0 HcmV?d00001 diff --git a/web_console_v2/client/src/assets/images/logo-colorful.svg b/web_console_v2/client/src/assets/images/logo-colorful.svg index a0c476217..f90316f4b 100644 --- a/web_console_v2/client/src/assets/images/logo-colorful.svg +++ b/web_console_v2/client/src/assets/images/logo-colorful.svg @@ -1,20 +1,20 @@ - - - - - + + + + + - - + + - - + + - - + + diff --git a/web_console_v2/client/src/components/BackButton/index.tsx b/web_console_v2/client/src/components/BackButton/index.tsx new file mode 100644 index 000000000..7a04963b0 --- /dev/null +++ b/web_console_v2/client/src/components/BackButton/index.tsx @@ -0,0 +1,29 @@ +import React, { FC } from 'react'; +import { Left } from 'components/IconPark'; +import GridRow from 'components/_base/GridRow'; +import styled from 'styled-components'; + +const Container = styled.div` + cursor: pointer; +`; + +type Props = { + onClick?: (evt: React.MouseEvent) => void; +}; + +const BackButton: FC = ({ onClick, children }) => { + return ( + + + + {children} + + + ); + + function onEleClick(evt: React.MouseEvent) { + onClick && onClick(evt); + } +}; + +export default BackButton; diff --git a/web_console_v2/client/src/components/BreadcrumbLink/index.tsx b/web_console_v2/client/src/components/BreadcrumbLink/index.tsx index 54e734aef..36dff226f 100644 --- a/web_console_v2/client/src/components/BreadcrumbLink/index.tsx +++ b/web_console_v2/client/src/components/BreadcrumbLink/index.tsx @@ -7,7 +7,6 @@ import { useTranslation } from 'react-i18next'; const Container = styled(Breadcrumb)` padding-left: 3px; - margin-bottom: 12px; `; type Props = { diff --git a/web_console_v2/client/src/components/Header/Account.tsx b/web_console_v2/client/src/components/Header/Account.tsx index c5401ab97..910c5fa1d 100644 --- a/web_console_v2/client/src/components/Header/Account.tsx +++ b/web_console_v2/client/src/components/Header/Account.tsx @@ -1,13 +1,12 @@ import React, { FC } from 'react'; import styled from 'styled-components'; import { userInfoQuery } from 'stores/user'; -import avatar from 'assets/images/avatar.svg'; +import avatar from 'assets/images/avatar.jpg'; import { useRecoilQuery } from 'hooks/recoil'; import { MixinCommonTransition, MixinSquare } from 'styles/mixins'; import { message, Popover, Button } from 'antd'; import GridRow from 'components/_base/GridRow'; import { Settings } from 'components/IconPark'; -// import LanguageSwitch from './LanguageSwitch'; import { Redirect, useHistory } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import store from 'store2'; @@ -15,16 +14,14 @@ import LOCAL_STORAGE_KEYS from 'shared/localStorageKeys'; import { useResetRecoilState } from 'recoil'; import { ErrorCodes } from 'typings/app'; import i18n from 'i18n'; -import { FedUserInfo, FedRoles } from 'typings/auth'; +import { FedUserInfo } from 'typings/auth'; import UserRoleBadge from 'components/UserRoleBadge'; -import Log from 'components/IconPark/icons/Log'; -import { logout } from 'services/user'; const Container = styled.div` ${MixinCommonTransition()} display: flex; align-items: center; - padding: 4px; + padding: 2px; cursor: pointer; border-radius: 50%; @@ -55,10 +52,6 @@ const UsernameRow = styled(GridRow)` margin-bottom: 0; } `; -// const LanguageRow = styled(Row)` -// height: 40px; -// margin-bottom: 10px; -// `; const ButtonRow = styled(GridRow)` height: 40px; margin-bottom: 10px; @@ -78,38 +71,17 @@ export const ACCOUNT_CHANNELS = { click_settings: 'click_settings', }; -const AccountPopover: FC<{ userInfo: FedUserInfo }> = ({ userInfo }) => { +const AccountPopover: FC = () => { const history = useHistory(); const { t } = useTranslation(); const resetUserInfo = useResetRecoilState(userInfoQuery); - let systemLogs = undefined; - - if (userInfo.role === FedRoles.Admin) { - systemLogs = ( - - - {t('settings.system_log')} - - ); - } return (

- {/* - - - - {t('app.switch_lng')} - - - - */} - - {t('settings.system_setting')} + {t('app.system_settings')} - {systemLogs} {t('app.logout')} @@ -118,7 +90,8 @@ const AccountPopover: FC<{ userInfo: FedUserInfo }> = ({ userInfo }) => { async function onLogoutClick() { try { - await logout(); + // logout api is now unavailable, only fe remove the user storage. + // await logout(); store.remove(LOCAL_STORAGE_KEYS.current_user); resetUserInfo(); history.push('/login'); @@ -130,10 +103,6 @@ const AccountPopover: FC<{ userInfo: FedUserInfo }> = ({ userInfo }) => { function onSettingClick() { history.push('/settings'); } - - function onSystemLogClick() { - window.open('/v2/logs/system', '_blank noopener'); - } }; const Username: FC<{ userInfo: FedUserInfo }> = ({ userInfo }) => { return ( @@ -162,7 +131,7 @@ function HeaderAccount() { return ( } + content={} title={} placement="bottomLeft" > diff --git a/web_console_v2/client/src/components/Header/ProjectSelect.tsx b/web_console_v2/client/src/components/Header/ProjectSelect.tsx new file mode 100644 index 000000000..e45ae4d0a --- /dev/null +++ b/web_console_v2/client/src/components/Header/ProjectSelect.tsx @@ -0,0 +1,98 @@ +import React, { FC, memo } from 'react'; +import styled from 'styled-components'; +import { useTranslation } from 'react-i18next'; +import { Dropdown } from 'antd'; +import PrettyMenu, { PrettyMenuItem } from 'components/PrettyMenu'; +import { useRecoilQuery } from 'hooks/recoil'; +import { projectListQuery, projectState } from 'stores/project'; +import GridRow from 'components/_base/GridRow'; +import { useRecoilState } from 'recoil'; +import { CaretDown } from 'components/IconPark'; +import { CloseCircleFilled } from '@ant-design/icons'; +import store from 'store2'; +import LOCAL_STORAGE_KEYS from 'shared/localStorageKeys'; +import { Project } from 'typings/project'; + +const Trigger = styled(GridRow)` + grid-area: project-select; + font-size: 14px; + cursor: pointer; + + &:hover { + > [data-name='clear'] { + display: block; + } + + > [data-name='arrow']:not([data-project-selected='false']) { + display: none; + } + } +`; +const Placeholder = styled.small` + opacity: 0.4; +`; +const ProjectItem = styled.div` + cursor: pointer; +`; +const ClearButton = styled(CloseCircleFilled)` + display: none; + font-size: 12px; +`; + +const ProjectSelect: FC = memo(() => { + const { t } = useTranslation(); + + const projectsQuery = useRecoilQuery(projectListQuery); + const [state, setProjectState] = useRecoilState(projectState); + + if (projectsQuery.isLoading || !projectsQuery.data) { + return ; + } + + const hasProjectSelected = Boolean(state.current); + + return ( + + {projectsQuery.data?.map((item, index) => ( + onProjectSelect(item)}> + +
{item.name}
+
+
+ ))} + {projectsQuery.data?.length === 0 && t('project.placeholder_no_project')} + + } + placement="bottomCenter" + > + + {state.current ? ( + {state.current.name} + ) : ( + {t('project.placeholder_global_project_filter')} + )} + {hasProjectSelected && } + + +
+ ); + + function onClearCick(evt: React.MouseEvent) { + evt.stopPropagation(); + setProjectState({ current: undefined }); + store.remove(LOCAL_STORAGE_KEYS.current_project); + } + function onProjectSelect(item: Project) { + setProjectState({ current: item }); + store.set(LOCAL_STORAGE_KEYS.current_project, item); + } +}); + +export default ProjectSelect; diff --git a/web_console_v2/client/src/components/Header/index.tsx b/web_console_v2/client/src/components/Header/index.tsx index 526cc14c1..d0028f89a 100644 --- a/web_console_v2/client/src/components/Header/index.tsx +++ b/web_console_v2/client/src/components/Header/index.tsx @@ -1,11 +1,12 @@ import React from 'react'; import styled from 'styled-components'; import HeaderAccount from './Account'; -import { Button, Tooltip } from 'antd'; +import { Tooltip } from 'antd'; import logo from 'assets/images/logo-colorful.svg'; import { StyledComponetProps } from 'typings/component'; import { QuestionCircle } from 'components/IconPark'; import { useTranslation } from 'react-i18next'; +import ProjectSelect from './ProjectSelect'; export const Z_INDEX_HEADER = 1001; export const Z_INDEX_GREATER_THAN_HEADER = 1002; @@ -16,8 +17,8 @@ const Container = styled.header` top: 0; display: grid; align-items: center; - grid-template-areas: 'logo . language account-info'; - grid-template-columns: auto 1fr auto auto; + grid-template-areas: 'logo project-select . help account-info'; + grid-template-columns: auto auto 1fr auto auto; gap: 12px; height: var(--headerHeight); padding: 0 30px; @@ -27,10 +28,14 @@ const Container = styled.header` `; const LogoLink = styled.a` grid-area: logo; - - > img { - height: 32px; - } +`; +const Logo = styled.img` + height: 32px; +`; +const HelpIcon = styled(QuestionCircle)` + font-size: 14px; + margin-right: 10px; + cursor: pointer; `; function Header({ className }: StyledComponetProps) { @@ -39,12 +44,16 @@ function Header({ className }: StyledComponetProps) { return ( - Federation Learner logo + - {/* This empty element is used to fill the space blank */} + + + + {/* This empty element is used to fill the blank sapce */}
+ - + + + - - - - - - + + + + + ); async function backToList() { history.push('/users'); @@ -92,6 +88,7 @@ const UserForm: FC<{ isEdit?: boolean; onSubmit?: any; initialValues?: any }> = async function onFinish(data: any) { try { toggleSubmitting(true); + data.password = btoa(data.password); await onSubmit(data); } catch { // ignore error diff --git a/web_console_v2/client/src/views/Users/UserList/index.tsx b/web_console_v2/client/src/views/Users/UserList/index.tsx index 29abd8684..1b7ca4723 100644 --- a/web_console_v2/client/src/views/Users/UserList/index.tsx +++ b/web_console_v2/client/src/views/Users/UserList/index.tsx @@ -1,5 +1,5 @@ import React, { FC, useState, useMemo } from 'react'; -import ListPageLayout from 'components/ListPageLayout'; +import SharedPageLayout from 'components/SharedPageLayout'; import { useTranslation } from 'react-i18next'; import { Row, Button, Col, Form, Input, Table, message, Tag, Popconfirm } from 'antd'; import { useHistory, Link } from 'react-router-dom'; @@ -108,7 +108,7 @@ const UsersList: FC = () => { const isEmpty = !query.isFetching && query.data?.data.length === 0; return ( - + ); + + async function onClick() { + try { + const blob = await request(getTemplateDownloadHref(id), { + responseType: 'blob', + }); + saveBlob(blob, `${name}.json`); + } catch (error) { + message.error(error.message); + } + } }; const DuplicateTemplate: FC<{ template: WorkflowTemplate }> = ({ template: { id } }) => { @@ -170,15 +176,7 @@ const TemplateList: FC = () => { <> - - - {t('menu.label_workflow_tpl')} - - } - tip="This feature is experimental" - > + { /> )} - + ); diff --git a/web_console_v2/client/src/views/Workflows/CreateWorkflow/StepOneBasic/index.tsx b/web_console_v2/client/src/views/Workflows/CreateWorkflow/StepOneBasic/index.tsx index 77182a324..b2a94311f 100644 --- a/web_console_v2/client/src/views/Workflows/CreateWorkflow/StepOneBasic/index.tsx +++ b/web_console_v2/client/src/views/Workflows/CreateWorkflow/StepOneBasic/index.tsx @@ -244,16 +244,20 @@ const WorkflowsCreateStepOne: FC = ({ setWorkflowConfigForm(parsedTpl.config as WorkflowConfig); } async function getWorkflowDetail() { - const { data } = await getWorkflowDetailById(params.id); + let { data } = await getWorkflowDetailById(params.id); + data = parseComplexDictField(data); + setWorkflow(data); formInstance.setFieldsValue((data as any) as CreateWorkflowBasicForm); } async function getPeerWorkflow() { const res = await getPeerWorkflowsConfig(params.id); - const anyPeerWorkflow = Object.values(res.data).find((item) => !!item.config)!; + const anyPeerWorkflow = parseComplexDictField( + Object.values(res.data).find((item) => Boolean(item.uuid))!, + )!; - setPeerConfig(anyPeerWorkflow.config!); + setPeerConfig(anyPeerWorkflow.config ?? (undefined as never)); setGroupAlias(anyPeerWorkflow.config?.group_alias || ''); return anyPeerWorkflow; diff --git a/web_console_v2/client/src/views/Workflows/CreateWorkflow/SteptTwoConfig/InspectPeerConfig.tsx b/web_console_v2/client/src/views/Workflows/CreateWorkflow/SteptTwoConfig/InspectPeerConfig.tsx deleted file mode 100644 index 93b85115e..000000000 --- a/web_console_v2/client/src/views/Workflows/CreateWorkflow/SteptTwoConfig/InspectPeerConfig.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React, { FC } from 'react'; -import styled from 'styled-components'; -import { Modal, Tabs, Button } from 'antd'; -import { ModalProps } from 'antd/lib/modal/Modal'; -import { Close } from 'components/IconPark'; -import { useTranslation } from 'react-i18next'; -import { WorkflowConfig } from 'typings/workflow'; -import PropertyList from 'components/PropertyList'; -import { Z_INDEX_GREATER_THAN_HEADER } from 'components/Header'; - -const InspectModal = styled(Modal)` - top: 20%; - - .ant-modal-header { - display: none; - } - .ant-modal-body { - padding: 0; - } -`; -const ModalHeader = styled.h2` - padding: 20px; - padding-bottom: 10px; - margin-bottom: 0; - font-size: 16px; - line-height: 24px; -`; -const JobTabs = styled(Tabs)` - &.ant-tabs-top > .ant-tabs-nav { - margin-bottom: 9px; - } -`; - -const InspectPeerConfig: FC< - ModalProps & { toggleVisible: Function; config: WorkflowConfig | null } -> = ({ config, toggleVisible, ...props }) => { - const { t } = useTranslation(); - - if (!config) return null; - - const jobs = config.job_definitions; - - return ( - } onClick={closeModal} />} - > - {t('workflow.peer_config')} - {config && ( - - {jobs.map((item) => { - return ( - - { - return { - label: vari.name, - value: vari.value, - }; - })} - /> - - ); - })} - - )} - - ); - - function closeModal() { - toggleVisible(false); - } -}; - -export default InspectPeerConfig; diff --git a/web_console_v2/client/src/views/Workflows/CreateWorkflow/SteptTwoConfig/index.tsx b/web_console_v2/client/src/views/Workflows/CreateWorkflow/SteptTwoConfig/index.tsx index 5145a635e..4e5a22fd1 100644 --- a/web_console_v2/client/src/views/Workflows/CreateWorkflow/SteptTwoConfig/index.tsx +++ b/web_console_v2/client/src/views/Workflows/CreateWorkflow/SteptTwoConfig/index.tsx @@ -33,7 +33,7 @@ import { to } from 'shared/helpers'; import { WorkflowCreateProps } from '..'; import { WorkflowAcceptPayload, WorkflowInitiatePayload } from 'typings/workflow'; import { Variable } from 'typings/variable'; -import InspectPeerConfigs from './InspectPeerConfig'; +import InspectPeerConfigs from '../../InspectPeerConfig'; import { ExclamationCircle } from 'components/IconPark'; import { Z_INDEX_GREATER_THAN_HEADER } from 'components/Header'; import { stringifyComplexDictField } from 'shared/formSchema'; @@ -135,7 +135,7 @@ const CanvasAndForm: FC = ({ isInitiate, isAccept }) => { ref={drawerRef as any} visible={drawerVisible} toggleVisible={toggleDrawerVisible} - showPeerConfigButton={isAccept} + showPeerConfigButton={isAccept && Boolean(peerConfig)} currentIdx={currNode?.data.index} nodesCount={jobNodes.length} jobDefinition={currNode?.data.raw} diff --git a/web_console_v2/client/src/views/Workflows/CreateWorkflow/index.tsx b/web_console_v2/client/src/views/Workflows/CreateWorkflow/index.tsx index bdf59e2ac..3c699bec4 100644 --- a/web_console_v2/client/src/views/Workflows/CreateWorkflow/index.tsx +++ b/web_console_v2/client/src/views/Workflows/CreateWorkflow/index.tsx @@ -1,13 +1,14 @@ import React, { FC, useState } from 'react'; import styled from 'styled-components'; import { Steps, Row, Card } from 'antd'; -import BreadcrumbLink from 'components/BreadcrumbLink'; import StepOneBasic from './StepOneBasic'; import SteptTwoConfig from './SteptTwoConfig'; -import { Route, useParams } from 'react-router-dom'; +import { Route, useHistory, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useUnmount } from 'react-use'; import { useResetCreateForms } from 'hooks/workflow'; +import SharedPageLayout from 'components/SharedPageLayout'; +import BackButton from 'components/BackButton'; const { Step } = Steps; @@ -39,6 +40,7 @@ export type WorkflowCreateProps = { */ const WorkflowsCreate: FC = (workflowCreateProps) => { const { t } = useTranslation(); + const history = useHistory(); const params = useParams<{ step: keyof typeof CreateSteps; id?: string }>(); const [currentStep, setStep] = useState(CreateSteps[params.step || 'basic']); const reset = useResetCreateForms(); @@ -48,14 +50,10 @@ const WorkflowsCreate: FC = (workflowCreateProps) => { }); return ( - <> - - + history.goBack()}>{t('menu.label_workflow')}} + contentWrapByCard={false} + > @@ -93,7 +91,7 @@ const WorkflowsCreate: FC = (workflowCreateProps) => { render={(props) => } /> - + ); function setToConfigStep() { diff --git a/web_console_v2/client/src/views/Workflows/EditWorkflow/StepOneBasic/index.tsx b/web_console_v2/client/src/views/Workflows/EditWorkflow/StepOneBasic/index.tsx index 1c3e22970..90c12a33d 100644 --- a/web_console_v2/client/src/views/Workflows/EditWorkflow/StepOneBasic/index.tsx +++ b/web_console_v2/client/src/views/Workflows/EditWorkflow/StepOneBasic/index.tsx @@ -93,6 +93,7 @@ const WorkflowsCreateStepOne: FC<{ onSuccess?: any }> = ({ onSuccess }) => { ); const peerErrorMsg = (peerWorkflowQuery.error as Error)?.message; + useEffect(() => { if (peerErrorMsg) { notification.error({ @@ -256,7 +257,9 @@ const WorkflowsCreateStepOne: FC<{ onSuccess?: any }> = ({ onSuccess }) => { setWorkflowConfigForm(parsedTpl.config as WorkflowConfig); } async function getWorkflowDetail() { - const { data } = await getWorkflowDetailById(params.id); + let { data } = await getWorkflowDetailById(params.id); + data = parseComplexDictField(data); + setWorkflow(data); setWorkflowConfigForm(data.config as WorkflowConfig); formInstance.setFieldsValue((data as any) as CreateWorkflowBasicForm); @@ -264,9 +267,11 @@ const WorkflowsCreateStepOne: FC<{ onSuccess?: any }> = ({ onSuccess }) => { async function getPeerWorkflow() { const res = await getPeerWorkflowsConfig(params.id); - const anyPeerWorkflow = Object.values(res.data).find((item) => !!item.config)!; + const anyPeerWorkflow = parseComplexDictField( + Object.values(res.data).find((item) => Boolean(item.uuid))!, + )!; - setPeerConfig(anyPeerWorkflow.config!); + setPeerConfig(anyPeerWorkflow.config ?? (undefined as never)); setGroupAlias(anyPeerWorkflow.config?.group_alias || ''); return anyPeerWorkflow; diff --git a/web_console_v2/client/src/views/Workflows/EditWorkflow/SteptTwoConfig/index.tsx b/web_console_v2/client/src/views/Workflows/EditWorkflow/SteptTwoConfig/index.tsx index 5d94f5dc1..21c1cc97e 100644 --- a/web_console_v2/client/src/views/Workflows/EditWorkflow/SteptTwoConfig/index.tsx +++ b/web_console_v2/client/src/views/Workflows/EditWorkflow/SteptTwoConfig/index.tsx @@ -33,7 +33,7 @@ import { patchWorkflow } from 'services/workflow'; import { to } from 'shared/helpers'; import { WorkflowAcceptPayload } from 'typings/workflow'; import { Variable } from 'typings/variable'; -import InspectPeerConfigs from './InspectPeerConfig'; +import InspectPeerConfigs from '../../InspectPeerConfig'; import { ExclamationCircle } from 'components/IconPark'; import { Z_INDEX_GREATER_THAN_HEADER } from 'components/Header'; import { stringifyComplexDictField } from 'shared/formSchema'; @@ -141,7 +141,7 @@ const CanvasAndForm: FC = () => { readonly={isCurrNodeReused} message={isCurrNodeReused ? t('workflow.msg_resued_job_cannot_edit') : ''} toggleVisible={toggleDrawerVisible} - showPeerConfigButton + showPeerConfigButton={Boolean(peerConfig)} currentIdx={currNode?.data.index} nodesCount={jobNodes.length} jobDefinition={currNode?.data.raw} diff --git a/web_console_v2/client/src/views/Workflows/EditWorkflow/index.tsx b/web_console_v2/client/src/views/Workflows/EditWorkflow/index.tsx index ee7f79f48..1272f00c1 100644 --- a/web_console_v2/client/src/views/Workflows/EditWorkflow/index.tsx +++ b/web_console_v2/client/src/views/Workflows/EditWorkflow/index.tsx @@ -1,13 +1,14 @@ import React, { FC, useState } from 'react'; import styled from 'styled-components'; import { Steps, Row, Card } from 'antd'; -import BreadcrumbLink from 'components/BreadcrumbLink'; import StepOneBasic from './StepOneBasic'; import SteptTwoConfig from './SteptTwoConfig'; -import { Route, useParams } from 'react-router-dom'; +import { Route, useHistory, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useUnmount } from 'react-use'; import { useResetCreateForms } from 'hooks/workflow'; +import SharedPageLayout from 'components/SharedPageLayout'; +import BackButton from 'components/BackButton'; const { Step } = Steps; @@ -27,6 +28,7 @@ enum CreateSteps { const WorkflowsEdit: FC = () => { const { t } = useTranslation(); + const history = useHistory(); const params = useParams<{ step: keyof typeof CreateSteps; id?: string }>(); const [currentStep, setStep] = useState(CreateSteps[params.step || 'basic']); const reset = useResetCreateForms(); @@ -36,14 +38,10 @@ const WorkflowsEdit: FC = () => { }); return ( - <> - - + history.goBack()}>{t('menu.label_workflow')}} + contentWrapByCard={false} + > @@ -63,7 +61,7 @@ const WorkflowsEdit: FC = () => { /> - + ); function setToConfigStep() { diff --git a/web_console_v2/client/src/views/Workflows/ForkWorkflow/index.tsx b/web_console_v2/client/src/views/Workflows/ForkWorkflow/index.tsx index bab3264c7..f79eb7e53 100644 --- a/web_console_v2/client/src/views/Workflows/ForkWorkflow/index.tsx +++ b/web_console_v2/client/src/views/Workflows/ForkWorkflow/index.tsx @@ -1,13 +1,14 @@ import React, { FC, useState } from 'react'; import styled from 'styled-components'; import { Steps, Row, Card } from 'antd'; -import BreadcrumbLink from 'components/BreadcrumbLink'; -import { Route, useParams } from 'react-router-dom'; +import { Route, useParams, useHistory } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useUnmount } from 'react-use'; import { useResetForkForms } from 'hooks/workflow'; import StepOneBasic from './StepOneBasic'; import StepTwoConfig from './StepTwoConfig'; +import SharedPageLayout from 'components/SharedPageLayout'; +import BackButton from 'components/BackButton'; const { Step } = Steps; @@ -26,6 +27,7 @@ enum ForkSteps { const ForkWorkflow: FC = () => { const { t } = useTranslation(); + const history = useHistory(); const params = useParams<{ step: keyof typeof ForkSteps; id?: string }>(); const [currentStep, setStep] = useState(ForkSteps[params.step || 'basic']); const reset = useResetForkForms(); @@ -36,14 +38,10 @@ const ForkWorkflow: FC = () => { }); return ( - <> - - + history.goBack()}>{t('menu.label_workflow')}} + contentWrapByCard={false} + > @@ -63,7 +61,7 @@ const ForkWorkflow: FC = () => { /> - + ); function setToConfigStep() { diff --git a/web_console_v2/client/src/views/Workflows/EditWorkflow/SteptTwoConfig/InspectPeerConfig.tsx b/web_console_v2/client/src/views/Workflows/InspectPeerConfig.tsx similarity index 100% rename from web_console_v2/client/src/views/Workflows/EditWorkflow/SteptTwoConfig/InspectPeerConfig.tsx rename to web_console_v2/client/src/views/Workflows/InspectPeerConfig.tsx diff --git a/web_console_v2/client/src/views/Workflows/WorkflowActions.tsx b/web_console_v2/client/src/views/Workflows/WorkflowActions.tsx index 61d0ce07d..f43999f8b 100644 --- a/web_console_v2/client/src/views/Workflows/WorkflowActions.tsx +++ b/web_console_v2/client/src/views/Workflows/WorkflowActions.tsx @@ -222,7 +222,7 @@ const WorkflowActions: FC = ({ workflow, type = 'default', without = [], return message.error(t('workflow.msg_get_peer_cfg_failed') + error.message); } - const anyPeerWorkflow = Object.values(res.data).find((item) => !!item.config)!; + const anyPeerWorkflow = Object.values(res.data).find((item) => !!item.uuid)!; if (!anyPeerWorkflow.forkable) { message.warning(t('workflow.msg_unforkable')); return; diff --git a/web_console_v2/client/src/views/Workflows/WorkflowDetail/GlobalConfigDrawer.tsx b/web_console_v2/client/src/views/Workflows/WorkflowDetail/GlobalConfigDrawer.tsx new file mode 100644 index 000000000..06db160d1 --- /dev/null +++ b/web_console_v2/client/src/views/Workflows/WorkflowDetail/GlobalConfigDrawer.tsx @@ -0,0 +1,120 @@ +import ErrorBoundary from 'antd/lib/alert/ErrorBoundary'; +import React, { ForwardRefRenderFunction } from 'react'; +import styled from 'styled-components'; +import { Drawer, Row, Button, Tag } from 'antd'; +import { DrawerProps } from 'antd/lib/drawer'; +import { NodeData } from 'components/WorkflowJobsCanvas/types'; +import { useTranslation } from 'react-i18next'; +import { Close } from 'components/IconPark'; +import GridRow from 'components/_base/GridRow'; +import PropertyList from 'components/PropertyList'; +import { WorkflowExecutionDetails } from 'typings/workflow'; +import { Link } from 'react-router-dom'; +import WhichProject from 'components/WhichProject'; +import { useStoreActions } from 'react-flow-renderer'; + +const Container = styled(Drawer)` + top: 60px; + + .ant-drawer-body { + padding-top: 0; + padding-bottom: 200px; + } +`; +const DrawerHeader = styled(Row)` + position: sticky; + z-index: 2; + top: 0; + margin: 0 -24px 0; + padding: 20px 16px 20px 24px; + background-color: white; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12); +`; +const DrawerTitle = styled.h3` + position: relative; + margin-bottom: 0; + margin-right: 10px; +`; + +interface Props extends DrawerProps { + isPeerSide?: boolean; + jobData?: NodeData; + workflow?: WorkflowExecutionDetails; + toggleVisible?: Function; +} + +export type JobExecutionDetailsExposedRef = {}; + +const GlobalConfigDrawer: ForwardRefRenderFunction = ({ + jobData, + workflow, + isPeerSide = false, + toggleVisible, + ...props +}) => { + const { t } = useTranslation(); + + const setSelectedElements = useStoreActions((actions) => actions.setSelectedElements); + + if (!jobData || !jobData.raw) { + return null; + } + + const job = jobData.raw; + + const workflowProps = [ + { + label: t('workflow.label_template_name'), + value: workflow?.config?.group_alias || ( + {t('workflow.job_node_pending')} + ), + }, + { + label: t('workflow.label_project'), + // TODO: peerWorkflow no project id + value: , + }, + // Display workflow global variables + ...(workflow?.config?.variables || []).map((item) => ({ + label: item.name, + value: item.value, + })), + ]; + + return ( + + + + + {job.name} + + {isPeerSide ? ( + {t('workflow.peer_config')} + ) : ( + {t('workflow.our_config')} + )} + + + + } + contentWrapByCard={false} + > + + + + + + {workflow?.name} + + {workflow && } + + {workflow && ( + + + )} - - - {jobsWithExeDetails.length === 0 ? ( - - - - ) : ( - - toggleDrawerVisible(false)} - /> - + + + {isForked && originWorkflowQuery.isSuccess && ( + + + {t('workflow.forked_from')} + + {originWorkflowQuery.data?.data.name} + + )} - - {/* Peer config */} - {peerJobsVisible && ( + + + + + {/* Our config */} - {t('workflow.peer_config')} + {t('workflow.our_config')} - + {!peerJobsVisible && ( + + )} - {peerJobsWithExeDetails.length === 0 ? ( + {jobsWithExeDetails.length === 0 ? ( - {peerWorkflowQuery.isFetching ? ( - - ) : ( - - )} + ) : ( toggleDrawerVisible(false)} /> )} - )} - - - - - - + {/* Peer config */} + {peerJobsVisible && ( + + + + {t('workflow.peer_config')} + + + + + + {peerJobsWithExeDetails.length === 0 ? ( + + {peerWorkflowQuery.isFetching ? ( + + ) : ( + + )} + + ) : ( + + toggleDrawerVisible(false)} + /> + + )} + + )} + + + + + + + + ); function viewJobDetail(jobNode: JobNode) { diff --git a/web_console_v2/client/src/views/Workflows/WorkflowList/index.tsx b/web_console_v2/client/src/views/Workflows/WorkflowList/index.tsx index 0883cece6..6f3af6088 100644 --- a/web_console_v2/client/src/views/Workflows/WorkflowList/index.tsx +++ b/web_console_v2/client/src/views/Workflows/WorkflowList/index.tsx @@ -1,21 +1,21 @@ import React, { FC, useEffect, useState } from 'react'; import styled from 'styled-components'; -import { Row, Col, Button, Form, Input, Select, Table, message, Spin } from 'antd'; +import { Row, Col, Button, Form, Input, Table, message, Spin } from 'antd'; import { Link, useHistory } from 'react-router-dom'; import { useQuery } from 'react-query'; import { fetchWorkflowList } from 'services/workflow'; import i18n from 'i18n'; import { formatTimestamp } from 'shared/date'; import { useTranslation } from 'react-i18next'; -import ListPageLayout from 'components/ListPageLayout'; +import SharedPageLayout from 'components/SharedPageLayout'; import { Workflow } from 'typings/workflow'; import WorkflowStage from './WorkflowStage'; import WorkflowActions from '../WorkflowActions'; import WhichProject from 'components/WhichProject'; import NoResult from 'components/NoResult'; -import { useRecoilQuery } from 'hooks/recoil'; -import { projectListQuery } from 'stores/project'; +import { projectState } from 'stores/project'; import { isInvalid } from 'shared/workflow'; +import { useRecoilValue } from 'recoil'; const FilterItem = styled(Form.Item)` > .ant-form-item-control { @@ -121,11 +121,11 @@ const WorkflowList: FC = () => { const [listData, setList] = useState([]); const [params, setParams] = useState({ keyword: '', uuid: '' }); - const projectsQuery = useRecoilQuery(projectListQuery); + const project = useRecoilValue(projectState); const { isLoading, isError, data: res, error, refetch } = useQuery( - ['fetchWorkflowList', params.project, params.keyword, params.uuid], - () => fetchWorkflowList(params), + ['fetchWorkflowList', params.keyword, params.uuid, project.current?.id], + () => fetchWorkflowList({ ...params, project: project.current?.id }), ); if (isError && error) { @@ -140,7 +140,7 @@ const WorkflowList: FC = () => { return ( - +