From c7ef17ccaa3bb0c744c87fd6eda6279f55770dcc Mon Sep 17 00:00:00 2001 From: ShanmukhEstrella Date: Fri, 18 Jul 2025 16:02:11 +0530 Subject: [PATCH 1/7] ILegend Kernel --- ILegend-JupyterMultiKernel/Kernel/LICENSE | 19 + ILegend-JupyterMultiKernel/Kernel/Makefile | 10 + ILegend-JupyterMultiKernel/Kernel/README.md | 26 + .../Kernel/ilegend_kernel/__init__.py | 3 + .../Kernel/ilegend_kernel/__main__.py | 5 + .../ilegend_kernel/ilegend_kernel_config.py | 1 + .../Kernel/ilegend_kernel/install.py | 67 + .../Kernel/ilegend_kernel/kernel.py | 2040 +++++++++++++++++ .../ilegend_kernel/kernelspec/kernel.js | 89 + .../ilegend_kernel/kernelspec/logo-64x64.png | Bin 0 -> 157805 bytes .../Kernel/ilegend_kernel/magics.py | 64 + .../Kernel/pyproject.toml | 30 + ILegend-JupyterMultiKernel/Kernel/setup.cfg | 10 + .../KernelSwitch-UI/README.md | 15 + .../KernelSwitch-UI/package.json | 22 + .../KernelSwitch-UI/src/index.ts | 202 ++ .../KernelSwitch-UI/style/index.css | 5 + .../KernelSwitch-UI/tsconfig.json | 14 + ILegend-JupyterMultiKernel/README.md | 59 + .../SyntaxHighlighting-UI/.gitignore | 125 + .../SyntaxHighlighting-UI/.prettierignore | 6 + .../SyntaxHighlighting-UI/.yarnrc.yml | 1 + .../SyntaxHighlighting-UI/README.md | 33 + .../SyntaxHighlighting-UI/RELEASE.md | 1 + .../SyntaxHighlighting-UI/babel.config.js | 1 + .../SyntaxHighlighting-UI/install.json | 5 + .../SyntaxHighlighting-UI/jest.config.js | 28 + .../SyntaxHighlighting-UI/package.json | 198 ++ .../SyntaxHighlighting-UI/pyproject.toml | 81 + .../SyntaxHighlighting-UI/rollup.config.js | 33 + .../SyntaxHighlighting-UI/setup.py | 94 + .../SyntaxHighlighting-UI/src/ilegend.grammar | 28 + .../src/ilegend.grammar.d.ts | 3 + .../src/ilegend.grammar.js | 16 + .../src/ilegend.grammar.terms.js | 20 + .../SyntaxHighlighting-UI/src/index.ts | 94 + .../SyntaxHighlighting-UI/src/style/index.css | 37 + .../SyntaxHighlighting-UI/style/base.css | 5 + .../SyntaxHighlighting-UI/style/index.css | 1 + .../SyntaxHighlighting-UI/style/index.js | 1 + .../SyntaxHighlighting-UI/tsconfig.json | 24 + 41 files changed, 3516 insertions(+) create mode 100644 ILegend-JupyterMultiKernel/Kernel/LICENSE create mode 100644 ILegend-JupyterMultiKernel/Kernel/Makefile create mode 100644 ILegend-JupyterMultiKernel/Kernel/README.md create mode 100644 ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/__init__.py create mode 100644 ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/__main__.py create mode 100644 ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/ilegend_kernel_config.py create mode 100644 ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/install.py create mode 100644 ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/kernel.py create mode 100644 ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/kernelspec/kernel.js create mode 100644 ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/kernelspec/logo-64x64.png create mode 100644 ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/magics.py create mode 100644 ILegend-JupyterMultiKernel/Kernel/pyproject.toml create mode 100644 ILegend-JupyterMultiKernel/Kernel/setup.cfg create mode 100644 ILegend-JupyterMultiKernel/KernelSwitch-UI/README.md create mode 100644 ILegend-JupyterMultiKernel/KernelSwitch-UI/package.json create mode 100644 ILegend-JupyterMultiKernel/KernelSwitch-UI/src/index.ts create mode 100644 ILegend-JupyterMultiKernel/KernelSwitch-UI/style/index.css create mode 100644 ILegend-JupyterMultiKernel/KernelSwitch-UI/tsconfig.json create mode 100644 ILegend-JupyterMultiKernel/README.md create mode 100644 ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/.gitignore create mode 100644 ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/.prettierignore create mode 100644 ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/.yarnrc.yml create mode 100644 ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/README.md create mode 100644 ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/RELEASE.md create mode 100644 ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/babel.config.js create mode 100644 ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/install.json create mode 100644 ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/jest.config.js create mode 100644 ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/package.json create mode 100644 ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/pyproject.toml create mode 100644 ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/rollup.config.js create mode 100644 ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/setup.py create mode 100644 ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/src/ilegend.grammar create mode 100644 ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/src/ilegend.grammar.d.ts create mode 100644 ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/src/ilegend.grammar.js create mode 100644 ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/src/ilegend.grammar.terms.js create mode 100644 ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/src/index.ts create mode 100644 ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/src/style/index.css create mode 100644 ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/style/base.css create mode 100644 ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/style/index.css create mode 100644 ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/style/index.js create mode 100644 ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/tsconfig.json diff --git a/ILegend-JupyterMultiKernel/Kernel/LICENSE b/ILegend-JupyterMultiKernel/Kernel/LICENSE new file mode 100644 index 000000000..2a8087f74 --- /dev/null +++ b/ILegend-JupyterMultiKernel/Kernel/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2022 Anne Brecklinghaus, Michael Leuschel, dgelessus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ILegend-JupyterMultiKernel/Kernel/Makefile b/ILegend-JupyterMultiKernel/Kernel/Makefile new file mode 100644 index 000000000..fc4dd931f --- /dev/null +++ b/ILegend-JupyterMultiKernel/Kernel/Makefile @@ -0,0 +1,10 @@ + +install: + pip install -e . + python -m ilegend_kernel.install + + + +clean: + pip uninstall ilegend_kernel + jupyter kernelspec remove ilegend_kernel diff --git a/ILegend-JupyterMultiKernel/Kernel/README.md b/ILegend-JupyterMultiKernel/Kernel/README.md new file mode 100644 index 000000000..9b99330b7 --- /dev/null +++ b/ILegend-JupyterMultiKernel/Kernel/README.md @@ -0,0 +1,26 @@ + +# Jupyter-ILegend - A Jupyter Kernel for ILegend Services + + +## Installation + +### Requirements + +- At least **Python** 3.5 + - Tested with Python 3.12.2 +- **Jupyter** installation with JupyterLab and/or Juypter Notebook + - Tested with + - jupyter_core: 5.7.2 + - jupyterlab: 4.x.x + - notebook: 7.x.x + + +## Development + +### Development Install + +1. Download the repository. +2. Change to the root directory of the repository. +3. `pip install .` +4. Install the kernelspec: + - `python -m ilegend_kernel.install` diff --git a/ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/__init__.py b/ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/__init__.py new file mode 100644 index 000000000..cec4b2bde --- /dev/null +++ b/ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/__init__.py @@ -0,0 +1,3 @@ + +from .kernel import ILegendRouterKernel +__version__ = ILegendRouterKernel.implementation_version \ No newline at end of file diff --git a/ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/__main__.py b/ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/__main__.py new file mode 100644 index 000000000..5ca8b8055 --- /dev/null +++ b/ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/__main__.py @@ -0,0 +1,5 @@ + +from ipykernel.kernelapp import IPKernelApp +from .kernel import ILegendRouterKernel + +IPKernelApp.launch_instance(kernel_class=ILegendRouterKernel) diff --git a/ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/ilegend_kernel_config.py b/ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/ilegend_kernel_config.py new file mode 100644 index 000000000..80800c062 --- /dev/null +++ b/ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/ilegend_kernel_config.py @@ -0,0 +1 @@ +c = get_config() \ No newline at end of file diff --git a/ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/install.py b/ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/install.py new file mode 100644 index 000000000..019346339 --- /dev/null +++ b/ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/install.py @@ -0,0 +1,67 @@ +import argparse +import json +import logging +import os +import shutil +import sys +import tempfile + +from jupyter_client.kernelspec import KernelSpecManager + +logger = logging.getLogger(__name__) + +KERNELSPEC_FILES = [ + 'kernel.js', + 'logo-64x64.png' +] + +def get_kernelspec_dir_path(): + """ + Get the path of the kernelspec directory where the static files needed for the installation are. + This currently only includes the kernel.js file, + because the kernelspec (kernel.json) is generated dynamically. + """ + dirname = os.path.dirname(__file__) + kernelspec_dir_path = os.path.join(dirname, 'kernelspec') + return kernelspec_dir_path + +def create_kernelspec(dest_dir): + with open(os.path.join(dest_dir, 'kernel.json'), 'w', encoding='utf-8') as f: + kernel_json = { + 'argv': [sys.executable, '-m', 'ilegend_kernel', '-f', '{connection_file}'], + 'display_name': 'ILegend', + 'language': 'ilegend', + 'metadata': { + 'logo-64x64': 'logo-64x64.png' } # or 'logo-svg': 'logo.svg' + } + json.dump(kernel_json, f, ensure_ascii=False, indent=4) + + kernelspec_dir = get_kernelspec_dir_path() + for file in KERNELSPEC_FILES: + shutil.copyfile(os.path.join(kernelspec_dir, file), os.path.join(dest_dir, file)) + +def main(argv=None): + logging.basicConfig( + format='%(levelname)s: %(message)s', + level=logging.INFO, + ) + + ap = argparse.ArgumentParser() + ap.add_argument( + '--user', + action='store_true', + help="install to the per-user kernel registry instead of sys.prefix (use if you get permission errors during installation)") + ap.add_argument( + '--prefix', + help="install to the given prefix: PREFIX/share/jupyter/kernels/") + args = ap.parse_args(argv) + + if not args.user and not args.prefix: + args.prefix = sys.prefix + + with tempfile.TemporaryDirectory() as temp_dir: + create_kernelspec(temp_dir) + KernelSpecManager().install_kernel_spec(temp_dir, 'ilegend_kernel', user=args.user, prefix=args.prefix) + +if __name__ == '__main__': + main() diff --git a/ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/kernel.py b/ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/kernel.py new file mode 100644 index 000000000..692efdcd3 --- /dev/null +++ b/ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/kernel.py @@ -0,0 +1,2040 @@ +from ipykernel.kernelbase import Kernel +from jupyter_client import KernelManager +from queue import Empty +import traceback +import re +import textwrap +import ast +import requests +import datetime +import json +from ipykernel.kernelbase import Kernel +from .magics import CELL_MAGICS, LINE_MAGICS +from ipykernel.kernelbase import Kernel +import pandas as pd +import os +import re +from dash import Dash, html, dcc,Output,Input,State +from dash_ag_grid import AgGrid +import threading +import socket + + + + +class ILegendRouterKernel(Kernel): + implementation = "ilegend" + implementation_version = "1.2" + banner = "ILegend kernel: route to python3 when #kernel: python is used" + language_info = { + "name": "ilegend", + "file_extension": ".ilgd", + "mimetype": "text/x-ilegend", + "codemirror_mode": "ilegend", + } + tables = [] + details = {} + user_ns={} + check = False + + + + def _launch_python_kernel(self): + km = KernelManager(kernel_name="python3") + km.start_kernel() + kc = km.client() + kc.start_channels() + return {"manager": km, "client": kc} + + + + + + def _extract_kernel_choice(self, code: str) -> str: + """Return 'python' only when first line starts '#kernel: python'.""" + lines = code.lstrip().splitlines() + if lines and lines[0].lower().startswith("#kernel:"): + if lines[0].split(":", 1)[1].strip().lower().startswith("python"): + return "python" + return "" + + + + + def _strip_first_line(self,code): + lines = code.splitlines() + return "\n".join(lines[2:]) if (lines and lines[0].lower().startswith("#kernel:")) else code + + + + + def to_json(self,df): + return df.to_json(orient="records") + + + + + def get_columns(self): + if self.tables == []: + return + for x in self.tables: + response = requests.post("http://127.0.0.1:9095/api/server/execute",json={"line":"get_attributes " + "local::DuckDuckConnection."+x}) + output = response.json() + self.details[x] = [y for y in output["attributes"]] + + + + + def initiate(self): + from IPython.display import HTML + import threading, time + from IPython.display import clear_output + stop_event = threading.Event() + def show_running_time(): + start = time.time() + while not stop_event.is_set(): + elapsed = time.time() - start + s = HTML(f"
Kernel Warming up... {elapsed:.2f} seconds elapsed\n
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + self.send_response(self.iopub_socket, 'clear_output', {'wait': True}) + time.sleep(0.01) + timer_thread = threading.Thread(target=show_running_time) + timer_thread.start() + try: + response = requests.post("http://127.0.0.1:9095/api/server/execute", json={"line": "get_tables " + "local::DuckDuckConnection"}) + output = response.json() + self.tables = [x for x in output["tables"]] + self.get_columns() + finally: + stop_event.set() + timer_thread.join() + + + + + def parse_db_output(self, text: str): + lines = [line.strip() for line in text.strip().splitlines() if line.strip()] + result = { + 'database': None, + 'tables': [] + } + current_table = None + inside_table = False + for line in lines: + if line.startswith("Database"): + result['database'] = line.split("Database", 1)[1].strip() + elif line.startswith("Table"): + table_name = line.split("Table", 1)[1].strip() + current_table = {'name': table_name, 'columns': []} + inside_table = True + elif line == ')': + if inside_table and current_table: + result['tables'].append(current_table) + current_table = None + inside_table = False + elif inside_table: + if ',' in line: + line = line[:-1] # remove trailing comma + if ' ' in line: + col_name, col_type = line.split(None, 1) + current_table['columns'].append({'name': col_name, 'type': col_type}) + return result + + + + + + + + + + def render_database_ui(self,data): + import html + db_name = data.get("database", "Unknown") + tables = data.get("tables", []) + html_parts = [f"
Database: {html.escape(db_name)}
"] + for table in tables: + table_name = table.get("name", "Unnamed Table") + columns = table.get("columns", []) + html_parts.append(f"
Table: {html.escape(table_name)}
") + # Table of columns + html_parts.append("") + html_parts.append("") + for col in columns: + col_name = html.escape(col.get("name", "")) + col_type = html.escape(col.get("type", "")) + html_parts.append(f"") + html_parts.append("
Column NameType
{col_name}{col_type}
") + html_parts.append("
") + return "\n".join(html_parts) + + + + + + + def start_aggrid_dash(self, df, port=None): + if port is None: + try: + port = self.find_free_port() + except Exception: + return "No port Found" + column_defs = [ + {"field": c, "headerName": c.capitalize(), "headerClass": "custom-header"} + for c in df.columns + ] + app = Dash(__name__) + app.index_string = """ + + + {%metas%} + Data Viewer + {%favicon%} + {%css%} + + + + {%app_entry%} + + + """ + app.layout = html.Div( + [ + dcc.Store(id="theme-store", data="light"), + html.Button("🌙 Dark mode", id="theme-btn", n_clicks=0, + className="theme-toggle"), + AgGrid( + id="grid", + rowData=df.to_dict("records"), + columnDefs=column_defs, + defaultColDef={"sortable": True, "filter": True, "resizable": True}, + columnSize="sizeToFit", + className="ag-theme-balham", + ), + ], + id="page", + style={"margin": 0, "padding": 0, "height": "100vh", "width": "100vw"}, + ) + @app.callback( + Output("theme-store", "data"), + Output("theme-btn", "children"), + Input("theme-btn", "n_clicks"), + State("theme-store", "data"), + prevent_initial_call=True, + ) + def toggle_theme(n, current): + new_theme = "dark" if current == "light" else "light" + btn_label = "☀️ Light mode" if new_theme == "dark" else "🌙 Dark mode" + return new_theme, btn_label + @app.callback( + Output("page", "className"), + Input("theme-store", "data"), + ) + def apply_theme(theme): + return theme + threading.Thread( + target=lambda: app.run(port=port, debug=False, use_reloader=False), + daemon=True, + ).start() + return f"http://localhost:{port}" + + + + + + + + + + + + def find_free_port(self,start=8050): + port = start + while(True): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + try: + s.bind(('localhost', port)) + return port + except OSError: + port = port + 1 + + + + + def do_execute(self, code, silent, store_history=True, user_expressions=None, allow_stdin=False): + if code.strip().startswith("%put"): + return self._handle_put_command(code) + kernel_choice = self._extract_kernel_choice(code) + exec_code = self._strip_first_line(code) + if kernel_choice == "python": + try: + if not hasattr(self, "_python"): + self._python = self._launch_python_kernel() + kc = self._python["client"] + msg_id = kc.execute(exec_code) + while True: + try: + msg = kc.get_iopub_msg(timeout=10) + except Empty: + break + if msg["parent_header"].get("msg_id") != msg_id: + continue + if msg["msg_type"] in { + "stream", "display_data", "execute_result", + "error", "clear_output" + }: + if msg["msg_type"] == "execute_result": + msg_type = "display_data" + content = { + "data": msg["content"]["data"], + "metadata": msg["content"].get("metadata", {}), + } + else: + msg_type = msg["msg_type"] + content = msg["content"] + self.session.send( + self.iopub_socket, msg_type, content, + parent=self._parent_header + ) + elif msg["msg_type"] == "status" and \ + msg["content"].get("execution_state") == "idle": + break + return { + 'status': 'ok', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + except Exception as e: + tb = traceback.format_exc() + self.send_response(self.iopub_socket, "stream", + {"name": "stderr", + "text": f"[Router‑Error] {e}\n{tb}"}) + return {"status": "error", + "execution_count": self.execution_count, + "ename": type(e).__name__, + "evalue": str(e), + "traceback": tb.splitlines()} + + else: + if(self.check == False): + self.initiate() + self.check = True + magic_line, *cell_lines = code.splitlines() + cell_code = "\n".join(cell_lines) + + + if code.strip().startswith("start_legend"): + import threading, time + from IPython.display import clear_output + from IPython.display import HTML + stop_event = threading.Event() + def show_running_time(): + start = time.time() + while not stop_event.is_set(): + elapsed = time.time() - start + s = HTML(f"
Activating Legend Features... {elapsed:.2f} seconds elapsed\n
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + self.send_response(self.iopub_socket, 'clear_output', {'wait': True}) + time.sleep(0.01) + s = HTML(f"
Legend Features Activated in - {elapsed:.2f}s\n
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + timer_thread = threading.Thread(target=show_running_time) + timer_thread.start() + connection_name = "local::DuckDuckConnection" + try: + response = requests.post("http://127.0.0.1:9095/api/server/execute", json={"line": "get_tables " + connection_name}) + finally: + stop_event.set() + timer_thread.join() + output = response.json() + self.tables = [x for x in output["tables"]] + return { + 'status': 'ok', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + + + + + + elif code.strip().startswith("%%"): + magic_name = magic_line[2:] + if magic_name in CELL_MAGICS: + output = CELL_MAGICS[magic_name](cell_code) + stream_content = {'name': 'stdout', 'text': output} + self.send_response(self.iopub_socket, 'stream', stream_content) + return { + 'status': 'ok', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + + + + + + elif code.strip().startswith("%"): + magic_line = code.strip().split() + magic_name = magic_line[0][1:].strip() + if magic_name == 'date': + t = datetime.datetime.now() + t = t.strftime("%Y-%m-%d %H:%M:%S") + stream_content = {'name': 'stdout', 'text': t} + self.send_response(self.iopub_socket, 'stream', stream_content) + return { + 'status': 'ok', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + elif magic_name in LINE_MAGICS: + output = LINE_MAGICS[magic_name](magic_line[1]) + stream_content = {'name': 'stdout', 'text': output} + self.send_response(self.iopub_socket, 'stream', stream_content) + return { + 'status': 'ok', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + + + + + + + + elif code.strip().startswith("sql_to_json_line"): + from IPython.display import HTML + headers = {"Content-Type": "text/plain"} + response = requests.post("http://127.0.0.1:9095/api/sql/v1/grammar/grammarToJson",data=cell_code, headers=headers) + output = response.json() + if("code" in output and output["code"]==-1): + s = HTML(f"
Error: {output["message"]}
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + return { + 'status': 'error', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + stream_content = {'name': 'stdout', 'text': json.dumps(output, indent=2)} + self.send_response(self.iopub_socket, 'stream', stream_content) + return { + 'status': 'ok', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + + + + + + elif code.strip().startswith("sql_to_json_batch"): + from IPython.display import HTML + headers = {"Content-Type": "application/json"} + queries = [q.strip() for q in cell_code.split(";") if q.strip()] + payload ={ + f"query{i+1}": {"value": query + ";"} + for i, query in enumerate(queries) + } + response = requests.post( + "http://127.0.0.1:9095/api/sql/v1/grammar/grammarToJson/batch", + data=json.dumps(payload), + headers=headers + ) + output = response.json() + if("code" in output and output["code"]==-1): + s = HTML(f"
Error: {output["message"]}
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + return { + 'status': 'error', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + stream_content = {'name': 'stdout', 'text': json.dumps(output, indent=2)} + self.send_response(self.iopub_socket, 'stream', stream_content) + return { + 'status': 'ok', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + + + + + elif code.strip().startswith("show_func_activators"): + from IPython.display import HTML + headers = {"Content-Type": "text/plain"} + response = requests.get("http://127.0.0.1:9095/api/functionActivator/list") + output = response.json() + if("code" in output and output["code"]==-1): + s = HTML(f"
Error: {output["message"]}
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + return { + 'status': 'error', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + stream_content = {'name': 'stdout', 'text': json.dumps(output, indent=2)} + self.send_response(self.iopub_socket, 'stream', stream_content) + return { + 'status': 'ok', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + + + + + elif code.strip().startswith("create "): + from IPython.display import HTML + headers = {"Content-Type": "text/plain"} + response = requests.post("http://127.0.0.1:9095/api/data/createtable",data=magic_line, headers=headers) + output = response.json() + if("error" in output): + s = HTML(f"
Error: {output["error"]}
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + return { + 'status': 'error', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + s='-----------------Table Created-----------------'+'\n' + "-----Ware House: "+output["warehouse"]+"-----"+'\n' + "-----DataBase: "+output["database"]+"-----"+'\n' + "-----Schema: "+output["schema"]+"-----"+'\n' + s = s+ "-----Columns-----"+'\n' + p = output["columns"] + for column in p: + s = s + column["name"] + f"[{column["type"]}]" + if(column["primaryKey"]==True): + s = s + " is a Primary Key"+"\n" + else: + s = s+'\n' + stream_content = {'name': 'stdout', 'text': s} + self.send_response(self.iopub_socket, 'stream', stream_content) + return { + 'status': 'ok', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + + + + + + + + elif code.strip().startswith("insertrow"): + from IPython.display import HTML + headers = {"Content-Type": "application/json"} + if '->' not in cell_code: + s = HTML("
Error: Improper usage of command
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + return { + 'status': 'error', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + row_part, path_part = cell_code.split('->', 1) + row_part = row_part.strip() + path_part = path_part.strip() + if not (row_part.startswith('[') and row_part.endswith(']')): + s = HTML("
Error: Improper usage of command
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + return { + 'status': 'error', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + row_part = row_part[1:-1].strip() + row_dict = {} + for pair in row_part.split(','): + if ':' not in pair: + s = HTML("
Error: Improper usage of command
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + return { + 'status': 'error', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + key, val = map(str.strip, pair.split(':', 1)) + try: + parsed_val = ast.literal_eval(val) + except Exception: + parsed_val = val + row_dict[key] = parsed_val + payload = {"path": path_part,"row": row_dict} + response = requests.post("http://127.0.0.1:9095/api/data/insertrow",data=json.dumps(payload), headers=headers) + output = response.json() + if "error" in output: + s = HTML(f"
Error: {output["error"]}
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + return { + 'status': 'error', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + else: + s = 'Row Added.' + '\n' + '(' + p = output["row"] + for fields in p: + s = s + f"{fields}: {p[fields]}" + ', ' + s = s[0:len(s)-2] + ')' + stream_content = {'name': 'stdout', 'text': s} + self.send_response(self.iopub_socket, 'stream', stream_content) + return { + 'status': 'ok', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + + + + + + + + + + + elif code.strip().startswith("delete_row"): + from IPython.display import HTML + headers = {"Content-Type": "text/plain"} + response = requests.post("http://127.0.0.1:9095/api/data/deleterow", data=cell_code, headers=headers) + output = response.json() + if "error" in output: + s = HTML(f"
Error: {output["error"]}
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + return { + 'status': 'error', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + deleted = output["deletedRow"] + s = 'Row deleted successfully:\n(' + for key, val in deleted.items(): + s += f"{key}: {val}, " + s = s.rstrip(", ") + ')' + stream_content = {'name': 'stdout', 'text': s} + self.send_response(self.iopub_socket, 'stream', stream_content) + return { + 'status': 'ok', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + + + + + + + + + + + elif code.strip().startswith("show_table"): + from IPython.display import HTML + headers = {"Content-Type": "text/plain"} + response = requests.post("http://127.0.0.1:9095/api/data/fetchtable",data=cell_code,headers=headers) + output = response.json() + if "error" in output: + s = HTML(f"
Error: {output["error"]}
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + return { + 'status': 'error', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + try: + df = pd.DataFrame(output) + except Exception as e: + s = HTML(f"
Error: {output["error"]}
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + return { + 'status': 'error', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + display_content = { + 'data': { + 'text/plain': str(df), + 'text/html': df.to_html() + }, + 'metadata': {} + } + self.send_response(self.iopub_socket, 'display_data', display_content) + return { + 'status': 'ok', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + + + + + + + elif code.strip().startswith("show_all_tables"): + from IPython.display import HTML + response = requests.get("http://127.0.0.1:9095/api/data/showtables") + output = response.json() + if "error" in output: + s = HTML(f"
Error: {output["error"]}
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + return { + 'status': 'error', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + s = (f"Number of tables: {output['count']}") + "\n" + s = s + "Tables:" + "\n" + for table in output['tables']: + s = s + table + '\n' + stream_content = {'name': 'stdout', 'text': s} + self.send_response(self.iopub_socket, 'stream', stream_content) + return { + 'status': 'ok', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + + + + + + + + elif code.strip().startswith("load duckdb"): + from IPython.display import HTML + headers = {"Content-Type": "text/plain"} + response = requests.post("http://127.0.0.1:9095/api/data/duckdb/load",data=cell_code, headers=headers) + output = response.text + stream_content = {'name': 'stdout', 'text': output} + self.send_response(self.iopub_socket, 'stream', stream_content) + return { + 'status': 'ok', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + + + + + + + + + elif code.strip().startswith("query duckdb"): + from IPython.display import HTML + headers = {"Content-Type": "application/json"} + magic_line_new = magic_line.split() + payload = { + "dbPath": f"{magic_line_new[2]}", + "query": f"{cell_code}" + } + response = requests.post("http://127.0.0.1:9095/api/data/duckdb/query", data=json.dumps(payload), headers=headers) + output = response.json() + try: + df = pd.DataFrame(output) + except Exception as e: + s = HTML(f"
Error: {output["error"]}
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + return { + 'status': 'error', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + display_content = { + 'data': { + 'text/plain': str(df), + 'text/html': df.to_html() + }, + 'metadata': {} + } + self.send_response(self.iopub_socket, 'display_data', display_content) + return { + 'status': 'ok', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + + + + + + + + + elif code.strip().startswith("load "): + from IPython.display import HTML + import threading, time + from IPython.display import clear_output + stop_event = threading.Event() + def show_running_time(): + start = time.time() + while not stop_event.is_set(): + elapsed = time.time() - start + s = HTML(f"
Loading csv data into table in DuckDB connection... {elapsed:.2f} seconds elapsed\n
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + self.send_response(self.iopub_socket, 'clear_output', {'wait': True}) + time.sleep(0.01) + s = HTML(f"
Total Time Taken - {elapsed:.2f}s\n
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + timer_thread = threading.Thread(target=show_running_time) + timer_thread.start() + try: + response = requests.post("http://127.0.0.1:9095/api/server/execute", json={"line": magic_line}) + response2 = requests.post("http://127.0.0.1:9095/api/server/execute", json={"line": "get_tables " + "local::DuckDuckConnection"}) + output2 = response2.json() + self.tables = [x for x in output2["tables"]] + self.get_columns() + finally: + stop_event.set() + timer_thread.join() + if(response.headers.get('Content-Type') == 'application/json'): + output = response.json() + s = HTML(f"
{output["error"]}
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + return { + 'status': 'error', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + output = response.text + stream_content = {'name': 'stdout', 'text': output} + self.send_response(self.iopub_socket, 'stream', stream_content) + return { + 'status': 'ok', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + + + + + + + + + + + + elif code.strip().startswith("db"): + from IPython.display import HTML + import threading, time + from IPython.display import clear_output + stop_event = threading.Event() + def show_running_time(): + start = time.time() + while not stop_event.is_set(): + elapsed = time.time() - start + s = HTML(f"
Exploring Database... {elapsed:.2f} seconds elapsed\n
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + self.send_response(self.iopub_socket, 'clear_output', {'wait': True}) + time.sleep(0.01) + s = HTML(f"
Total Time Taken - {elapsed:.2f}s\n
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + timer_thread = threading.Thread(target=show_running_time) + timer_thread.start() + try: + response = requests.post("http://127.0.0.1:9095/api/server/execute", json={"line": magic_line}) + finally: + stop_event.set() + timer_thread.join() + if(response.headers.get('Content-Type') == 'application/json'): + output = response.json() + s = HTML(f"
Error: {output["error"]}
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + return { + 'status': 'error', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + output = response.text + try: + structured = self.parse_db_output(output) + html_str = self.render_database_ui(structured) + self.send_response(self.iopub_socket, 'display_data', { + 'data': {'text/html': html_str}, + 'metadata': {} + }) + return { + 'status': 'ok', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + except Exception as e: + from IPython.display import HTML + import html + s = HTML(f"
Parsing/Rendering failed: {html.escape(str(e))}
") + self.send_response(self.iopub_socket, 'display_data', { + 'data': {'text/html': str(s.data)}, + 'metadata': {} + }) + return { + 'status': 'error', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + + + + + + + + + + + + + elif code.strip().startswith("drop_all_tables"): + from IPython.display import HTML + import threading, time + from IPython.display import clear_output + stop_event = threading.Event() + def show_running_time(): + start = time.time() + while not stop_event.is_set(): + elapsed = time.time() - start + s = HTML(f"
Dropping tables from Connection... {elapsed:.2f} seconds elapsed\n
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + self.send_response(self.iopub_socket, 'clear_output', {'wait': True}) + time.sleep(0.01) + s = HTML(f"
Total Time Taken - {elapsed:.2f}s\n
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + timer_thread = threading.Thread(target=show_running_time) + timer_thread.start() + try: + response = requests.post("http://127.0.0.1:9095/api/server/execute", json={"line": magic_line}) + response2 = requests.post("http://127.0.0.1:9095/api/server/execute", json={"line": "get_tables " + "local::DuckDuckConnection"}) + output2 = response2.json() + self.tables = [x for x in output2["tables"]] + self.get_columns() + finally: + stop_event.set() + timer_thread.join() + if(response.headers.get('Content-Type') == 'application/json'): + output = response.json() + s = HTML(f"
{output["error"]}
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + return { + 'status': 'error', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + output = response.text + stream_content = {'name': 'stdout', 'text': output} + self.send_response(self.iopub_socket, 'stream', stream_content) + return { + 'status': 'ok', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + + + + + + + + + + + + + + + + elif code.strip().startswith("macro "): + from IPython.display import HTML + import threading, time + from IPython.display import clear_output + stop_event = threading.Event() + def show_running_time(): + start = time.time() + while not stop_event.is_set(): + elapsed = time.time() - start + s = HTML(f"
Establishing Macro... {elapsed:.2f} seconds elapsed\n
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + self.send_response(self.iopub_socket, 'clear_output', {'wait': True}) + time.sleep(0.01) + s = HTML(f"
Macro Established in - {elapsed:.2f}s\n
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + timer_thread = threading.Thread(target=show_running_time) + timer_thread.start() + try: + response = requests.post("http://127.0.0.1:9095/api/server/execute", json={"line": magic_line}) + finally: + stop_event.set() + timer_thread.join() + output = response.text + if(output.startswith("Invalid")): + s = HTML(f"
Error: {output}
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + return { + 'status': 'error', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + stream_content = {'name': 'stdout', 'text': output} + self.send_response(self.iopub_socket, 'stream', stream_content) + return { + 'status': 'ok', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + + + + + + + + + + + + elif code.strip().startswith("show_macros"): + from IPython.display import HTML + import threading, time + from IPython.display import clear_output + stop_event = threading.Event() + def show_running_time(): + start = time.time() + while not stop_event.is_set(): + elapsed = time.time() - start + s = HTML(f"
Fetching all Macros... {elapsed:.2f} seconds elapsed\n
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + self.send_response(self.iopub_socket, 'clear_output', {'wait': True}) + time.sleep(0.01) + s = HTML(f"
Macros fetched in- {elapsed:.2f}s\n
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + timer_thread = threading.Thread(target=show_running_time) + timer_thread.start() + try: + response = requests.post("http://127.0.0.1:9095/api/server/execute", json={"line": magic_line}) + finally: + stop_event.set() + timer_thread.join() + output = response.text + stream_content = {'name': 'stdout', 'text': output} + self.send_response(self.iopub_socket, 'stream', stream_content) + return { + 'status': 'ok', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + + + + + + + + + + + + + + + + elif code.strip().startswith("clear_macros"): + from IPython.display import HTML + import threading, time + from IPython.display import clear_output + stop_event = threading.Event() + def show_running_time(): + start = time.time() + while not stop_event.is_set(): + elapsed = time.time() - start + s = HTML(f"
Clearing all Macros... {elapsed:.2f} seconds elapsed\n
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + self.send_response(self.iopub_socket, 'clear_output', {'wait': True}) + time.sleep(0.01) + s = HTML(f"
Macros cleared in- {elapsed:.2f}s\n
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + timer_thread = threading.Thread(target=show_running_time) + timer_thread.start() + try: + response = requests.post("http://127.0.0.1:9095/api/server/execute", json={"line": magic_line}) + finally: + stop_event.set() + timer_thread.join() + output = response.text + stream_content = {'name': 'stdout', 'text': output} + self.send_response(self.iopub_socket, 'stream', stream_content) + return { + 'status': 'ok', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + + + + + + elif code.strip().startswith("get_tables "): + from IPython.display import HTML + import threading, time + from IPython.display import clear_output + stop_event = threading.Event() + def show_running_time(): + start = time.time() + while not stop_event.is_set(): + elapsed = time.time() - start + s = HTML(f"
Fetching tables... {elapsed:.2f} seconds elapsed\n
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + self.send_response(self.iopub_socket, 'clear_output', {'wait': True}) + time.sleep(0.01) + s = HTML(f"
Tables Fetched in - {elapsed:.2f}s\n
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + timer_thread = threading.Thread(target=show_running_time) + timer_thread.start() + try: + response = requests.post("http://127.0.0.1:9095/api/server/execute", json={"line": magic_line}) + finally: + stop_event.set() + timer_thread.join() + output = response.json() + s = "" + for x in output["tables"]: + s = s+x+"\n" + stream_content = {'name': 'stdout', 'text': s} + self.send_response(self.iopub_socket, 'stream',stream_content) + return { + 'status': 'ok', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + + + + + + elif code.strip().startswith("get_attributes "): + from IPython.display import HTML + import threading, time + from IPython.display import clear_output + stop_event = threading.Event() + def show_running_time(): + start = time.time() + while not stop_event.is_set(): + elapsed = time.time() - start + s = HTML(f"
Fetching attributes... {elapsed:.2f} seconds elapsed\n
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + self.send_response(self.iopub_socket, 'clear_output', {'wait': True}) + time.sleep(0.01) + s = HTML(f"
Attributes Fetched in - {elapsed:.2f}s\n
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + timer_thread = threading.Thread(target=show_running_time) + timer_thread.start() + try: + response = requests.post("http://127.0.0.1:9095/api/server/execute", json={"line": magic_line}) + finally: + stop_event.set() + timer_thread.join() + output = response.json() + s="" + for x in output["attributes"]: + s = s + x + "\n" + stream_content = {'name': 'stdout', 'text': s} + self.send_response(self.iopub_socket, 'stream',stream_content) + return { + 'status': 'ok', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + + + + + elif code.strip().startswith("get_all"): + from IPython.display import HTML + import threading, time + from IPython.display import clear_output + stop_event = threading.Event() + def show_running_time(): + start = time.time() + while not stop_event.is_set(): + elapsed = time.time() - start + s = HTML(f"
Fetching attributes... {elapsed:.2f} seconds elapsed\n
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + self.send_response(self.iopub_socket, 'clear_output', {'wait': True}) + time.sleep(0.01) + s = HTML(f"
Attributes Fetched in - {elapsed:.2f}s\n
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + timer_thread = threading.Thread(target=show_running_time) + timer_thread.start() + try: + response = requests.post("http://127.0.0.1:9095/api/server/execute", json={"line": magic_line}) + finally: + stop_event.set() + timer_thread.join() + output = response.json() + s="" + for x in output["connections"]: + s = s + x + "\n" + stream_content = {'name': 'stdout', 'text': s} + self.send_response(self.iopub_socket, 'stream',stream_content) + return { + 'status': 'ok', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + + + + + + + + elif code.strip().startswith("#>"): + code = code.replace('\n', '') + match1 = re.search(r'(.*?)--file\s+(\w+)\s*$', code.strip()) + match2 = re.search(r'(.*?)--var\s+(\w+)\s*$', code.strip()) + if match1: + query_part = match1.group(1).strip() + filename = match1.group(2).strip() + variable = None + elif match2: + query_part = match2.group(1).strip() + variable = match2.group(2).strip() + filename = None + else: + query_part = code.strip() + variable = None + filename = None + from IPython.display import HTML + import threading, time + from IPython.display import clear_output + stop_event = threading.Event() + def show_running_time(): + start = time.time() + while not stop_event.is_set(): + elapsed = time.time() - start + s = HTML(f"
Data Quereying... {elapsed:.2f} seconds elapsed\n
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + self.send_response(self.iopub_socket, 'clear_output', {'wait': True}) + time.sleep(0.01) + s = HTML(f"
Total Time Taken - {elapsed:.2f}s\n
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + timer_thread = threading.Thread(target=show_running_time) + timer_thread.start() + try: + response = requests.post("http://127.0.0.1:9095/api/server/execute", json={"line": query_part}) + finally: + stop_event.set() + timer_thread.join() + output = response.json() + try: + df = pd.DataFrame(output) + except Exception: + s = HTML(f"
Error: {output["error"]}
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + return { + 'status': 'error', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + if filename!=None: + df.to_csv(filename,index=False) + s = HTML(f"
DataFrame saved in the file - {filename}\n
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + return { + 'status': 'ok', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + elif variable!=None: + self._python = self._launch_python_kernel() + python_kc = self._python["client"] + json_string = df.to_json(orient='split') + "\n" + escaped_json = repr(json_string) + import textwrap + escaped_json = repr(json_string) + inject_code = textwrap.dedent(f""" + import pandas as pd + import io + {variable} = pd.read_json(io.StringIO({escaped_json}), orient='split') + """) + msg_id = python_kc.execute(inject_code) + # Wait until Python kernel finishes executing + while True: + msg = python_kc.get_iopub_msg(timeout=5) + if msg['parent_header'].get('msg_id') != msg_id: + continue + if msg['msg_type'] == 'status' and msg['content'].get('execution_state') == 'idle': + break + s = HTML(f"
DataFrame variable '{variable}' stored in the Python Environment
") + self.user_ns[variable] = df + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + return { + 'status': 'ok', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + else: + if(self.start_aggrid_dash(df)=="No port Found"): + s = HTML(f"
AgGrid haven't generated. Please try again
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + return { + 'status': 'error', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + url = self.start_aggrid_dash(df) + html_output = f''' + + ''' + self.send_response(self.iopub_socket, 'display_data', { + 'data': { + 'text/html': html_output, + 'text/plain': str(df) + }, + 'metadata': {} + }) + return { + 'status': 'ok', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + + + def do_complete(self, code, cursor_pos): + kernel_choice = self._extract_kernel_choice(code) + if kernel_choice == "python": + code_without_header = self._strip_first_line(code) + lines = code.splitlines() + adjustment = sum(len(line) + 1 for line in lines[:2]) if (lines and lines[0].lower().startswith("#kernel:")) else 0 + adjusted_cursor = max(cursor_pos - adjustment, 0) + try: + if not hasattr(self, "_python"): + self._python = self._launch_python_kernel() + kc = self._python["client"] + except Exception: + return { + 'status': 'ok', + 'matches': [], + 'cursor_start': cursor_pos, + 'cursor_end': cursor_pos, + 'metadata': {}, + } + msg_id = kc.complete(code_without_header, adjusted_cursor) + while True: + try: + msg = kc.get_shell_msg(timeout=5) + except Empty: + break + if msg['parent_header'].get('msg_id') != msg_id: + continue + if msg['msg_type'] == 'complete_reply': + content = msg['content'] + # Adjust back to original cursor positions + content['cursor_start'] += adjustment + content['cursor_end'] += adjustment + return content + return { + 'status': 'ok', + 'matches': [], + 'cursor_start': cursor_pos, + 'cursor_end': cursor_pos, + 'metadata': {}, + } + else: + suggestions = ["load ", "db ", "#>"] + if code is None or code.strip() == "": + start = len(prefix) + return { + 'matches': suggestions, + 'cursor_start': start, + 'cursor_end': start, + 'metadata': {}, + 'status': 'ok' + } + prefix = code[:cursor_pos] + tokens = prefix.strip().split() + if tokens and tokens[0] == "load" and prefix.rstrip() == "load": + match = "~/" + start = len(prefix) + return { + 'matches': [match], + 'cursor_start': start, + 'cursor_end': start, + 'metadata': {}, + 'status': 'ok' + } + if prefix.startswith("load ~/"): + path_prefix = prefix[len("load "):] + if path_prefix.endswith('/'): + dir_path = os.path.expanduser(path_prefix) + partial = '' + else: + dir_path, partial = os.path.split(os.path.expanduser(path_prefix)) + try: + entries = os.listdir(dir_path) + except Exception: + entries = [] + matches = [e for e in entries if e.startswith(partial)] + matches = [ + e + '/' if os.path.isdir(os.path.join(dir_path, e)) else e + for e in matches + ] + if not matches: + matches = [ + e + '/' if os.path.isdir(os.path.join(dir_path, e)) else e + for e in entries + ] + completed_file = ( + len(matches) == 1 and + not matches[0].endswith('/') and + os.path.isfile(os.path.join(dir_path, matches[0])) + ) + if completed_file: + matches = [' local::DuckDuckConnection'] + cursor_start = cursor_end = cursor_pos + else: + cursor_start = len("load ") + len(path_prefix) - len(partial) + return { + 'matches': matches, + 'cursor_start': cursor_start, + 'cursor_end': cursor_pos, + 'metadata': {}, + 'status': 'ok' + } + if prefix.endswith("#>"): + return { + 'matches': ["{local::DuckDuckDatabase."], + 'cursor_start': cursor_pos, + 'cursor_end': cursor_pos, + 'metadata': {}, + 'status': 'ok' + } + if prefix.endswith("db "): + return { + 'matches': ["local::DuckDuckConnection"], + 'cursor_start': cursor_pos, + 'cursor_end': cursor_pos, + 'metadata': {}, + 'status': 'ok' + } + if prefix.endswith("]"): + return { + 'matches': [")"], + 'cursor_start': cursor_pos, + 'cursor_end': cursor_pos, + 'metadata': {}, + 'status': 'ok' + } + if prefix.endswith("}#") or prefix.endswith(")"): + return { + 'matches': ["->"], + 'cursor_start': cursor_pos, + 'cursor_end': cursor_pos, + 'metadata': {}, + 'status': 'ok' + } + if prefix.strip().endswith(" ->") or prefix.strip().endswith("->"): + return { + 'matches': ["filter(", "groupBy(", "select(", "extend(","from(" , "pivot(", "asofjoin(", "join(", "distinct(","rename(", "concatenate(", + "sort(","size(","drop("], + 'cursor_start': cursor_pos, + 'cursor_end': cursor_pos, + 'metadata': {}, + 'status': 'ok' + } + if prefix.strip().endswith("x|$x."): + match = re.search(r"#>\{local::DuckDuckDatabase\.([A-Za-z0-9_]+)}#", prefix) + if match: + result = match.group(1) + p = self.details[result] + return { + 'matches': p, + 'cursor_start': cursor_pos, + 'cursor_end': cursor_pos, + 'metadata': {}, + 'status': 'ok' + } + if prefix.strip().endswith("|"): + if(cursor_pos>=2): + var = code[cursor_pos-2] + return { + 'matches': ["$"+var+"."], + 'cursor_start': cursor_pos, + 'cursor_end': cursor_pos, + 'metadata': {}, + 'status': 'ok' + } + else: + return { + 'matches': [], + 'cursor_start': cursor_pos, + 'cursor_end': cursor_pos, + 'metadata': {}, + 'status': 'ok' + } + if prefix.strip().endswith("~") or prefix.strip().endswith(",") or prefix.strip().endswith("[") or (prefix.strip().endswith(".") and cursor_pos>=3 and code[cursor_pos-3]=="$"): + match = re.search(r"#>\{local::DuckDuckDatabase\.([A-Za-z0-9_]+)}#", prefix) + if match: + result = match.group(1) + p = self.details[result] + return { + 'matches': p, + 'cursor_start': cursor_pos, + 'cursor_end': cursor_pos, + 'metadata': {}, + 'status': 'ok' + } + if prefix.strip().endswith("filter("): + return { + 'matches': ["x|$x."], + 'cursor_start': cursor_pos, + 'cursor_end': cursor_pos, + 'metadata': {}, + 'status': 'ok' + } + if prefix.strip().endswith("from("): + return { + 'matches': ["local::DuckDuckRuntime)"], + 'cursor_start': cursor_pos, + 'cursor_end': cursor_pos, + 'metadata': {}, + 'status': 'ok' + } + if prefix.strip().endswith("--"): + return { + 'matches': ["file", "var"], + 'cursor_start': cursor_pos, + 'cursor_end': cursor_pos, + 'metadata': {}, + 'status': 'ok' + } + if prefix.strip().endswith("select("): + return { + 'matches': ["~["], + 'cursor_start': cursor_pos, + 'cursor_end': cursor_pos, + 'metadata': {}, + 'status': 'ok' + } + if "#>{local::DuckDuckDatabase" in prefix: + match = re.search(r"#>\{local::DuckDuckDatabase\.([A-Za-z0-9_]*)$", prefix) + if match: + typed = match.group(1) + matches = [ + table + "}#" + for table in self.tables + if table.lower().startswith(typed.lower()) + ] + cursor_start = cursor_pos - len(typed) + cursor_end = cursor_pos + + return { + 'matches': matches, + 'cursor_start': cursor_start, + 'cursor_end': cursor_end, + 'metadata': {}, + 'status': 'ok' + } + matches = [s for s in suggestions if s.startswith(prefix)] + # if not matches and cursor_pos==0: + # matches = suggestions + return { + 'matches': matches, + 'cursor_start': 0, + 'cursor_end': cursor_pos, + 'metadata': {}, + 'status': 'ok' + } + return {"status": "ok", + "matches": [], + "cursor_start": cursor_pos, + "cursor_end": cursor_pos, + "metadata": {}} + + + + def do_shutdown(self, restart): + if hasattr(self, "_python"): + try: + self._python["client"].stop_channels() + self._python["manager"].shutdown_kernel(now=True) + except Exception: + pass + return super().do_shutdown(restart) diff --git a/ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/kernelspec/kernel.js b/ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/kernelspec/kernel.js new file mode 100644 index 000000000..a1a7106fa --- /dev/null +++ b/ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/kernelspec/kernel.js @@ -0,0 +1,89 @@ +define(["codemirror/lib/codemirror"], function(CodeMirror) { + "use strict"; + + return { + onload: function() { + console.info("Loading ILegend kernel.js"); + CodeMirror.defineMode("ilegend", function(config, parserConfig) { + return { + startState: function() { + return { + state: "initial", + }; + }, + + token: function(stream, state) { + switch (state.state) { + case "initial": + if (stream.match(/^\%/)) { + // Line comment -> consume the rest of the line + stream.match(/^.+/); + return "comment"; + } else if (stream.match(/^\/\*/)) { + // Block comment start -> switch to comment state + state.state = "comment"; + return "comment"; + } else if (stream.match(/^"(?:[^"])*"/)) { + return "string"; + } else if (stream.match(/^'(?:[^'])*'/)) { + return "atom"; + } else if (stream.match(/^(?:[0-9]+)/)) { + return "number"; + } else if (stream.match(/^(?:!)/)) { + return "builtin"; + } else if (stream.match(/^(?:=:=|:-|@<|@>|@=<|@>=|[-+\\/><=*#$?^])/)) { + return "operator"; + } else if (stream.match(/^[\s()\[\]{},\.|;@]+/)) { + return null; + } else if (stream.match(/^:+/)) { + // ":" needs to be checked on its own + // Otherwise, if ":-" occurs after one of the characters above (e.g. ")"), ":" would not be highlighted correctly + return null; + } else { + const atom_or_variable = stream.match(/^[A-Za-z_]+[A-Za-z_0-9]*/); + if (atom_or_variable && atom_or_variable.toString() !== "") { + var firstCharacter = atom_or_variable.toString().charAt(0); + if (firstCharacter !== "_" && firstCharacter === firstCharacter.toLowerCase()) { + // If the token starts with a lower case letter, it is an atom + if (stream.peek() === "(") { + // Atoms which are preceded by "(" are highlighted differently + return "builtin"; + } else { + return "atom"; + } + } else { + return "variable-2"; + } + } else { + // Consume the rest of the line and mark it as an error + stream.match(/^.+/); + return "error"; + } + } + + case "comment": + while (!stream.eol()) { + // Consume everything except for "*" + stream.match(/^[^\*]+/); + if (stream.match(/^\*\//)) { + // "*/" -> switch back to initial state + state.state = "initial"; + return "comment"; + } else { + // "*" without "/" -> consume and stay in comment state + stream.match(/^\*/); + } + } + return "comment"; + + default: + throw new Error("Unhandled state: " + state.state); + } + }, + }; + }); + + CodeMirror.defineMIME("text/x-ilegend", "ilegend"); + }, + }; +}); diff --git a/ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/kernelspec/logo-64x64.png b/ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/kernelspec/logo-64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..8609e0ca0b407602c647a7e8966b6bdef597138c GIT binary patch literal 157805 zcmeFa2RxTu`v89PHImRmWK>3Fk8DCES!I0*8I_UD%xonMq7bsT>`k(wLiP?>A$#u? z{qOHI())UP-~ao2{{PQ&e|qk7&ULPHo$FlZI@h_*b)T>2XWP#SfJyd_iG_*z9TU?N zoGfer?*$1N$OHg;4q`BYZwvqfmHwOr=tZ?nZs`Ep&afH?a1a8)!2`4C2OGeYxb(9F zcm%fE_F%$Fn7coa-5xkT1||d(bGKRu7WjY_umlV7hG)0DetrNB?Zfgq@EwA22*5mq z0Xc;6^Am;uj4=Qc0wa&)8h`&jJZv0XEC`H8SQUCSUi{U(b!C!ZI#2rdxA~wLIw?|PbTfFEUp&6FHf{? zk~rU!Z#qZ!nDl|yt&5gr;>XM$7EhN%Dww+{Mx-~~zC^DrcKR@EOa$22cwhoPusLHF zk)1wtQuN*fuk@0J-h*0l5tt{~OmA{H&m1;;=pFf?@6^lU@QlVI;Ot^z?!(50H-io9 z&`EYcR7CFPy$2w9*=etmnSZUxaf1^@}I)Lxep!{-&H#BpMjJ z7Z5%b(FxE^4fAENHvC=Z|BaEZ6)uz)AUsuD0-@%QADNGMpv%uS;!_d5XKy4KR%9-1Km2XY` z{qH&bgg3LZ>w<^ZqSZh-VsIF27+P3>^8Fh=aT6MrZYMgQdUQ zgAu-OMx&0WX&ug3nL`$*a7OdrjY*Inj0v*RkT~55DIZf9cT@srhHS5Dys~cl8}dp} zKNA-wUJvJt@U`77PvY;6jAVa9MhEBAR91>!q?V__nZs(HT1mHV`WtRh`(s-s68PUa z!`Z~8()n_R>P48F2cirI{!Ly4#A0s!Hnm6*;s{UNeC-e#{x@N6lMSH`-YhY!t^l)1 zB<#mK@%)B43n;zFCeq&{2eG{&NhEK9VrG;zgSWoB^xM6^otED0jeG={VCp#HAr(#Q^Rfl7ft%pH*(!sc;#4lj}-BL38REwOWk+ei3u%}-D&?9 zq52vWN_w^P&M1-xUaN{Sk4dA7yQXsjw+qjZGY!-^1?Mjbl)tY(hul^O&&j-zSXZ@R zZhAg#WzcjZwKPQpO@UgmHvd?Eop$G}Q#cxk&NxcvS&zEYsH;rde#cm#=)Y}mT*(Lb zRfziTsj73XwWMCm7}~gf0^Py^pSMkuzAIwR2(cLhguE3g!UC3KbB4~s-S^^o*?V{> zLT_)`NQnAuaaLF9Etk7_`scsi3lq4f>I z6YfOyRb9#yyHVvB&^z$P2s|0T{=7txelvm0F18=BtRei|>{7S-&U0*vhw7rC@>d_n z)m(aHul6{G;*k0|%YYE)@F^0WHxi9#cHo)&KXQ4Rs8yP7Cz554%tcXh*QG)^hRVi% zZ#a%(_L}7Rhoi}0v^=?S?{SfP%gpcFKqmRhJ}u8>NmdA)8Q6*jN$~tU!=*!YiGOA; z38nrxf#1HQ71_n1yV#OB6gJ;k$TN}Fa_(4ya_9)61`=P}7L9F9eY_ooiQ{^!)au93 z6=eB^9zQ*5Gw?oA+v_Z31@WC_HO<4wV$OF+{I5!+i412bXZD6~`~FpE{o^8MSDRpB!uq zn5Wd*#M;dHoQ@opW)(#-?UQ!J*BeMTZ;q^1yzOvs!;77wJVd}PE| z7e60aQ&{l*+a@(+4wCGcn~~WQE0J$G+9B{uH3Fccn&6P-q0Y8hP4rSCNultxFmA^t zYcYxQVq*C3m!|nhEg(k9gcERudi!LIK1%di=5v5Xk4s+28+thI_m23?5rb? zGF$tW5wnEz*(NL20r&b&L|B=&V)$Jql>72Pu2p0UQ!xSsW7*6LH*%ovdp@EJ0IjV! z^NX>lxE_+VsIUgzZVfY26w;Oy;?hNaljRGA>EiAB^j)>}e0;EKW}QqOp+oVOcC>cG zE16L&B3nRPvJQ#7Q8Bt5gz;gkTcC(6uG%_#rQae|q*WUnuou<#165lQrP{!^Czrjf z=GlHwqpT%?gQ>~2yZR!?zI1GB$*!x8@~x8?4Gf(|0=FmM^rX$3a^2}ei29#9Z?A%v zB0?Ok?XK}|tr-(eypnIbPl!f+w*~+}SvQh=b>#H8n*XnI#+dDzVNIO0&%E{$MiQbe z_*LP&OVU>IsQh8=`5WyCK{RHa0&;i#w2)xP$uDWrn03g~Z`QJKWKF?tYZ=zcg0$yo z6${On8XuCz*2Em2zhkO;-Yzs~D-?q}sM}tC!FekToo{<{J}WYEWGQ*K0OCyhRgR^g z!wztb$H<^EIvXbX1>JN@29Lv*#jr)j>zy%WT(%S&s)mjX)OBob5k#4M^;>snbE7k| zwsIkuBQ3kxe3hxAs`e@pxFcB!T@DfIJeD(7zFa1P?F{TnJ%Z^O=Ok^w{s1|jsVlF0hy8?S5q?L{y)t{#$jPOf55 zir#9s5&)=5L+;|K6lEmnZ3CkUM_v{Ud|Xm02FnC5=k%pl3DZzjTT=<226Np6Lys!c z39$qfXH*qOhYnVp(R4AQY%u0xYo~zX55oAxwDLo%T-|V{2`k^Wm56E%#x|O!biHro zySTO^Y{_N~JV&wN&OJNlko0UFOUQB?U&sRPwzW_AW_8}(VO(T4OaKtf zsq|OOR#C3sy_(8Bhn8~q-73G&i9fMa07f~UrMqU7%VTM0cn5zltpF|#zvTVI!!$ZCJb$Ebo?mSCD=vM+~vCRfyd0wCN*@2aS*u zE|EE?#DB1u0o{x;GLBCD4fdCRJaS;{Vgm1|W@%aQ_HiHvwS7yo6mE@Ho4N(5Fmk_R zvyx9Z(7A2sK#0uHc``c$tiy@rEac)2M<0!j5oWxstzenzSquPweLn^OdUmIp7skQP zFoF=x8^@cz2Fk+n;6tX7`B;f0&(ldP$i@bJbVk%|c@N_koh9uO8+*OWjALxG0g(8O z!c#qV2Ppz;S&|3xM79tC4T5)-hWT7Nd^ z^Q0{4cjxVY95aLy{S!S0Ao%__<}y&Rq^->DKGi+}r7ePQuWUsG01g#{8Wu)DXBc#V zQBBllWyPm1cRS8zi7KdwMrL?rHL&BvA1OITNXNct6LadBG7%WjKO6SU0F9G)O(@?k z-ox&u%*>oO?~NbwY=;^Qjuf*^OsO7xwdpAD56Q+*VMEpZl3Kj&pfF24H3OcA{+0a} zCvBXa_bK|g-Twlq9g!>=EIX|v0werKg9c2G$MUAF?kkdc?hGTc?IGN@J}9p(jF7Cg zIe#C#XL*fz$ErQxr#dI)R(AqykCFJAUBO9cD+0#cGp_#cGpmO3c1-DZ^xWT>4%=A; zZ0QTD%fx1e^9%5={4%}AWq9PB1}if=Ju?6x@{mf>a3B@dRDrM5zk0$#FoJV9#b)SP z_ZF^@*S)E$E^Y(k@ReRC*OeeM)bx6a~C)ssYR$w#wgzj^@Lp>EX=*bp4Ot1G&VG2G z#CEy@DN(*VUmij8$1fx}wKyaD!{{}CT3~Cek;Iw1;|XLsxDDmaa9L zFMNgxdoM>@3^#%6v8{UH2$~Z<%m%=zQ#!;X< z3CwpH#e|t|qE)zR{ki;$xAi+r&mNVA@bOyTOS&(i0wDc8oq*ZJ4UT>tpGUG|z7TW# zZcH*0WVgKu-$gtV33 ziKm-z@f1NKQCEAsh#eexV)$VDIeCvl!Qf+TV_RtjyZi%@V5ID0ml~yd5wDLn5QHq( zqInA`&WEL-7)0_30KeIyzTC4k`TZ6Jak#-3&c*Z%MP=-EEbnSh$tN5%1Tf>=KhjXu z`GOt=;$+r28p@pYXR8M?XxGPHho}fPwBC)T2m*yl7IG___1mob(-nA?&}c{Pm$pPH7sd7mjCoSQ*Kts3JPubt?h}Tr?I|7H4@s zqw!#wd$Tg;1DtdHCjfx_th;LeN^Lj{3_g&V0a_FBst^foL>c}TAh=a*am3su2-zzx zkuBHbp-7im1Ft_?KLFMUt4&%P`gX=UfK~!^)7YQxS?q+)p}<#OJ-K70N_V zAXK`$R8X~>DP}NNK%0Z&HzWjyfrW3dMr*Skbn+qUh0{hI%7S5U5n!TMM$dWMKQ{WU z{h1X3aKk5_R@z=QPT>Yk)n0;7$D*>`jI3#Aso)eNnZbcfo(NliyoEJHcK6wDIH0;XR&xT4z2doC)~Xx&4s}6;vT-EY z`$wxR5&?r>t&Ob-X6<)*c8EZSqa*OP)mJpGeVxpE5}5FKm_KHd#OrROfFUSQWJtI8 z9l$Bd<$05RE*x3QQK)wyiP>_yp#w@OlN1ax(UT1je4NqsIERX}0c2m(A7A!cb>7wk zSlHdqAiCG1_IAZFEN*iX0_gm~E1ypqz(omra7J?#FUJfc3ZRc}!}a7^%+T`WR=a?# z1d|=x#%2Y@)!{}n|3liT;h=Hd5pjlN5nb*%6g(f?1qB`7KUo79i_&BF!gv~%cyo}w zVw6iPJo*Vlx~yz|jzv}N9@Ge~Wr^P!)HrghaHv%8I-(JVvTaD#oEjUF!~u0RQ7$Pa zWNAmTLmM`01(oiRrApN_U+n;8^{ZhV>JS(H`56zP89AO6g0CuKzWx8Dx& z2GvR*F;f0U34qfPHgztO#eZZDfQIMSlYaui=r{$fI`}1{1ob5Y&FJnSOg_F~b%iPm zXEYjpsa>60EmCsSwjEOWHX0~7QtuDCB5Qj^kSZUIzAf|H`NzSSpeYuL)&hWEPpu#Y zDz73MWkh)BPc}q=TBMexcTjH?CkVw(z`)e`b9u$6A~O9=Q14&rKjAP<=_|pWD@YOt zx~_kulX4K*K^al}tr-Bcs2T+!UoV4as~w*s5Doe7PI&URdf85L>?O8=b7taJkb0;y zng@(q{4#4b=wm4kZVKN}uNJx*nc00G{3YBIL_Bq#5#{IIWereLC3HqoIL49t(9Z7| zvSd!pzcf{Tx=8#b5&%muK5T?=t~9*3-g@GmEzqkA^?PepV3ZF6#t1i z1W$+t++Qgbi;*m`r|TW9Nwz?i(8-^O!U*60Xi5ZXm0_?kPTD_4eHUqXbvM^$WFsV2 z1lhBm0FgSiO{-Y}(1ipR5XkkuOuCUjsytw62slnCn>?I>Ui*Eb|3>bD=`e`hEx)!V6W3U;j1NIN44o}RO=D_*z$%YGAZ&WR*DeJ(@< zK?dN5jD8Wdu18Wu0zr9i(=61-yN! znKcBn_Ag8kn8-Kka?D>|9%K<|FTz>c;@*liB1tV!&bS3#vBpDxXG2{!+BS||R>+}p zN^|=jEhHI2=hcOm>f%0lVR$`3LU9=CQ z=|s^-q{urYe|ZZ;n5nVeCGkl6w17ZNoMI4cMU#I~iQ{%)i!vi|TX@YczA z&c;P(>0#59P>E`;mb{kPSC`6|IoiRhGSTsUS1EMtT7=Y9pJ7a5S!crlIAMM=_#&{NjLo&J$E6R&MIb ztO`<*sp`)0RQzH!5VyVly4BEq{DF_V^D+-(g&T8&g)^de)O)W=5pH(N@2L6-WHXK@ z`gKhCA}AuLbXU(BRkpOo`1T81H3W~$jB=mb-01so)Z5zs(K(ZVxGc|)omL|-$Gy=1GocsON%_cFnBO=Jg*D41O z5L$mjme9%W3qYTrl1oUfVKohn2g?wiuSvl-obMY30EZwW6WoEk_eDqqj8grD$J9|^ zSnVYo^8boN0OODDM@6DhZXB}fJ!*q+PTRYmKuD0U{hBkfrT@ax`Aht$5)ugf#zcyC zsAa8qhDB|OI>1^$j`&T>RF;Few_FE8T+Kcs$zXC9`2@uTaKMXywq;yO36wqY@nHru;&3%{znsC4MiQWm8cPoR(R1xR zQvZ-W|K>Ky&Zr#F^Ao_a?w8?3Hp9Rpf>xAISfi+*(mftMVCb74-8)%H?zBpvNE6c3 zMGFg2Q2C}5!>KOXL=;`~+Hs)+IfK7-xEBZ(!Un3Zi;jadb~hdXMm`~v9Hvh+yc5)G zZ%YN^J9laJkNLt!b61cJA*n2Jk$eu}C_1S0kK{4Vctcs8g#7sfrE5a#M^Qyoy2n5b41M#19d7fS8>zBM_UN+$Pb48iz+&X+ zdr{no+gfnQUC9HAlP*zh3n=@7z}wyHM6W{x68ydWWCl6MeW)U*j}3riOB?74^E6HI zqhK!p{y=IB$UIXB7WMUlVh_Va3Xg7-Na7qq0GH4b$q2LHG1SFvLi^m@#n}H zoAAwk5xL!WWQv`kKNvDTyB!yfYeck9vg5o9;FsC-L<(Q#h*W557x!&1Ta?$58-ux~sGQ zFRzPTj^)dQy5SWAPMmZkX;&kguAb=E0w%BBP^k2~`gM5#L3;QnkaPB&hIpAFS|XGk z{i@w7lVL>{QRpj-gxq=4YbXjUXHZ)+tp7c&H-UyGVyVcWz+kld3+PUk zk)7@MWe@w!N7wH-U%Nh5qWwk0z#%l;1+xp81k&cZiARrsq0n9q17ZBv?3!)UuRlXN zGCf9HhpZPk*N`^~aWyFUC5sjbgL2vs?E##Wm6@yi&u%2f79%0(wXyx+w7FfP@W55H z{c{u;S@M*5d&n7-NC!)92KeVImY5L^IHJj34o(g}ygkH@9UgH(wVS_R#Gpxq-7EO@ z!_n@Mw*7(7iNwlxARk*E`^Pdme8YTm5%eDwDB2TBvA%))$5DMA%)Uo@Sza3uFl$w{ zX_D#GJn9T@1qQ_)+1+Pz>3I>*e*zAH1U#41$TZSzxe71BU;nv%0tm+M>AD<6e~bmm z?!h?W(wTdB&gk8PDws7AIHUO)amhK;-Yxzk9j)`|s$I_!;Lk;vG+R+SHUgLEQ0#`V zOmTBL_uX_+SQYgPCg-mu0&7CNnI6tmrNPD>PmHFTYEUi<1!I^r>(|m3=O-MgQ;K0U z1enV5^3_;16n8|AAiZzW)BL14%TNBS79t3A=;HB9vuUa=K7I4aF4z zNE~T28)A@g-h?1?yPwT>qM^(<2&b?Nk{Idu=;2xZyi5e#cvSBsv5LyfC^V#a{ppFaM1KzlbAuk6{=do?_ z0eK$KLo1YbPbclS#vKsQX(dse^I7u@+H8k%>$j^0B$lNu)4$^&h_@JGwMMHpYm+L} z+mU&ld#zc(d9yWy$B(T-vPX6kg3RsiF6>0Zi1##Xx%JQ#1_!@jw++6%M=B-o9b?ek zqv{^dl1#(A5z=rhGl<)WS5gV10nrJLvx~@IwnEPE^(0MD(-JPke7#JVV&U-jT zJaK)#<9AJHcM9ah;f5q+^KUSK&5KD9bJ^BTqxezUQ#Y{jMx6!T8dJ94-rZyN9O9PD38 z6!rlGJ=M(mYjE^qR-^59;30~r?RNXYVjC**=I)N$@aTgDh)dX+{iTx1{7pyJXLL)H zf!gjAqN`CEh`<-N&x`%nN}oHUKKoey993`x()>WlS%e;!Zd z7c>yCyAjxiiFEU}|I`>jhB4uoIIFp@aH5AdTkUOPArA}5FZoJD&$WRw%>NyJkWvt zVLICrEHf*Dk+9lt4^?eM+LmN z_qhcqPKQan?<(=L$$_j79mo7G&+X@wDxUT$)&I(H0c(QToJ(^d;HM4|WEUuVzwL;< zvwK_&TP~l{dCDtBFLzeK%6m0dFkIiI@PW!Fmx~FE=lUWYouXl|?azP3MgiEs^6`mS zv{6PG`fb0K-QF(%)Dl1E7=GLI+|YSv4XpfEMp@!peWY`$7?N9Pk;xh`_A9yFLkipQ zkb%zMd!67(aH74`?@Y`@qNfP5>0WkO(3?8alt1)IPs53RTaW0%zw^h?h@f>+m~@j- zmb)jKMh&fH$d{piv70IW?+h0Z@M$Fxm~)`o{PRhBFvMj2X>ND9XBIMpowxA4nf(i^ zwZAj3&TNoEXQ99J_6h4Hs;r)vuykYf;!R>04ayek))pfvcRiiq(0}haqkggad%qbZ zg!!;QrMrvv8=NNiBiCJTA|or|2_%^%Zs>^#iH0rp9NE^2&HP^7DeyNMRel#IPKLw; z+bbI4wjYa>v9dBzXjt32uL(cC?)!Eq#MSf6MfIGaks(Ak{~fCZ2vzZB6dopv&vlPf;Q;D?+>foH?OYy2M=e2g z3aa5ooh4H}Om)|z26OLLZHnPE{ww|mxXe34bJKf*h8LO5kgOr)`rCDi+g~N9K65Ly zQn21a(D5Cz)i23j_hpP3Jj<}cPsd^=?h}~dcFwu6eKAQ_jYoeVhY`k2n0kKo>qloh zO2^aBABd6vm~;#^h4=VhS^@xgr)|jABGVa8^xrN4OP+QT1-qyUB1c|a@I%XQ9!M}h z3-w^$Z)!Ke!T~;ZSMtb-^PthTV$?Fcd=`hXl!s+Vc3f@s|M+O+BthwKQruL04r$h&}+V!nE zi^4&s)NhQ8oQ4L+qx?j6)a)!m63^!j=9`l^X@koVH`mC6Yc@N*H0Q-K-O}(P3K^H= zbn8hH_FFk0ufFd97symuj`Lg>#e*kWFbqf&&!_tiq&(8c$Ir*)FdrI<9alsBhzUgj zo>B|;9Lk=*(RI}%ZS5n8+uYn-ewY>l12@*>>C!5TbAxc23w4sFQm5^Yo>iSO}^>nBl_9;#^@No z8{JsZzU402*6PhX_@}>8uqSVRT+$T+|JV%p5M>+1dh%H}^w59B`CfOkAj27sOh`O0 z9;{Jg*4q(lX-k#-9E1|qWta6Eyd4x&gJIxOM7NTVkX!iBi%^u8@mwnjt>D{@!+W0U z?5sCauYy6I+5@*OvOBDf6Mlmc03e?7`p<$0Y_)t&z{{|ghl$d!VX7$e$Zc#|sZU{0T`7cPjO)PwT4W%R}(INz#) zuq*6hXi0A!4^7>fzK$_3LtsOXe3hn;WfG7{RXM_MGu#xN^4u~p$Ege%+}gsz|3!8) z$_o%?^IHT#oB8VePoOy=;$qTR@p#|9Z2sa>>v=FLjIQ~=&1|v_{(Q6_N15%WQ@-Yv zR;~YAu6FNzlUK5r8IMgC8y0@F?T-0>3`H}FtKq;eGdRdpU;Tf~(Qmp(F^Vx9)Q`9L zWBJ0Aeb4V4sNazIUxCSA57sE1ISpV0Gaid=H6gvR?I!-^rV=Ci9_4LFr_-lkgUktJ3_yMt7bCi8js{HoIn%(=|=ALsLIs#)XJ zD%Xu$tAyjlQvEbayPV`-gsKT^d4~I%_4qv>Q*Xq z4a)3UIG8qi98Fnk*Dpr5@xnw9h24o$C?W&H5eazA^^F~$na0@@?ZuUX&M z_m|FAB#gg*;-y5;Cr>zgiEuV*zg+yw(6co0vWz8|a%adCJFOo@Thhc+UKtga=l5#% z{BIR5Xwqp_rpl+{{g$xAqhv-LD0lqozwY;8!AlxCTWi-ssq{bK&KuX}gz`g*c}#mB zicjMiI@3dmgU3}#YgkrI6BE#AJh*!V}H}xa2=&ahL5c| zZ6VzR%-YZ=Q$`sPy5ki}RYtu7ZZxJtOvD2ssVBmsOU^tegHGFh827pDADr1W?W=M4 z<=aYitS6%)QuFZQW{pMV)j-`{) z-s=*01zwNBIKsMFiI|U@5o?XnEK@>VyYWs+aW!1+4oaKQUhw(}V6%<6==8h#P0lu< z+Qul{m%}QOEb&$BZ<(!h&S(90jpV9JdD*O|_5IsUCDQ!g+VA)3bRI0a$*IA^sct>4 zF{)0aI8L_;l_|8T=Eue#BtAbo1OB~#;}440j%iLS5ax1g@T!Ce4g_%_*^5;#R7iUg zJ^Q+VB{CeW2w0DAITPYj9y{wJ>2Z=sq93xc|7I&rRXM&;yJ4$Gb*uUsXwC9^D!yQ<1?&y4WzmrELvDQTM2D?&@6eq!ic0PFF9l>9|jOI}~{i*G+o8%KXp z^0}byBU9+48(^^%FeUsgmeSO)z%)xhlW8dfH4dAWz65E{@RkCUyn$`>aeZioLbn#uG?<}nau@FzDJSk>Q>}o??#Hnp{e_Nl4pR% zpg4slV87bJKQhGAiIU)5i@Dd*%80S$e!?ekr}*S!3CgZdZgkGLpw3pqRJB!P_@)OwsIVCeB9_>{WYZh}PVj zuY%(He+Hn_S0I^QpU?!X=KSo|(LB5*V)CfwLGgSw)U4JIHaN8_iJ*Lu?y3;@XXgI%6nl8s!&cIHK_xNUIrSQ23KG_iZ#w zf(r&hr_?~T!qyRBrC_erFvssMFxJhRYAfu)wwUv?2!x3;iY0%&HF3Zf^9mQ?g_3+Z z!VBeeT8;YQH(1(j!=~jYDb?mRc-3A;iF$dvccGf&sa4H~4ZyLVPkw4U3lzgx(<f7fOieTg{w z@xzxMV-u$(X5YMWj!V5895cAU>-{~+F+QxgP9m4dpuyYs>RMgVA5uQuRJJ_g&~D+4 zl3m`o>zOY`SMndOahTxepYv+QQ>v;5#~L_R^r*nJ9uMk5L+Ae%sR{up)pt)_hqj%i z_xm7lDqqnJMJ2y-^zH?M-h8X8fh6Fcj2-XFQg4-8rAG0B{jmes=>s{b@WAu^YEOLU#@zg>p7(a!_%g@qydsSTd9&IMicb4Lnr=&RDe+Iz2 zpfFJYeUv32ekN#?4%gG8i80#zsGdgaj{~jh^K|`>ADQOsjHBl~Q=#0x@y>-LN;p69 z1{GQAUQp};%C_egW&F1(**#yEOW%%VZ#p)zI{@2??bVU@7dLPWQdSSoe7ta|9S@qZ zfHakZ2AS%;#98^!Eby$^H95GD9JtP|Rl1i12Wn^SNlx~$ zN^BHq6RKNv%ZIb1mu~+A82Dg@$mr?f1kBXb+-gd&_Yd}URJ=tVfJ@!U(T3*>i7sRK#>?4`%hq5d+R>ty%L7blZxnN_GC3VE%3h~y> zCtXoUT|UeBHuHXO+IRa~|Lr3;*R_j04;TI0pI zlg$T5_gP}`Kco~T41Xf~rvRx>ab(b8rxFSE!k}`3qrFz{I%wm-qahJGZh@AfN!<^! zU{m(wp);?dp!-LbSV8Ob&^zCb$CLbwXGM(C=S_XL{FW%i)#`v24(0duE>rd*Xtr#t zj*=QUeQ*hh<=Ry7Cv1we^CvLrXYj5$0WBhdR(y{Ak+-Z%FLqgvBbmhKe0AiboC>#8 z-URU`tAgKj%}dduD!1*`Wv;%u9A}rZ{sJjp-Fy>AckRzU8796);3{1s1t+m{?m<8~ zF`UgDAETZJo(mtULw;??n&voJlA%-wa~}dyYU+5&f@kCs!NumTPboTAq>>#Wn3djb z&B@O?y@OY-j{E1lE5x*i6Jfgxr~~$vbb*u55O|k`#BNU&FGLa_jB~xja8hpnzbc5l zFZG+&{8l|Q`S`TGqEM?NOffs!3rsWB-;O)F>xjXeNzfEn_QN&|514DA81?;&pFz&x zulVrsH1Hr~&}E~SPYl=lcroHg-F)h?pW0qFa25I(uXOxCm_pkEhPmP7^ngBMb=uw zpi;o*!bm>Jhkt)`18gGQp65+1veGT-<}+m6M626L?jz%%`<>FU&LES)kqJ{mn2e3S zkJ6hno`Ww8nAhW8qU5J%kfsY|%kPVr%wH7#7Jb-9)?&yXv`( z+LuV=ZzNs2KU)rNtYO_ANIp`2c_O58BF4qkHfVnY=zI%*)6MfDYu=-7T*xcTFURNV za7tdw$Gw1L@^(v`lu%pIJetcl{9l>E3?AMBP9 zvUqg_uBC_#(KT@5eVgNCyR1J#>AtKFJiL>6>jhj-by^)O$6b@Y+$4P|u9-a0ou;Ee zM(HE@?osE8RZZyF``gHk??a7M&-kHc>imHaT;x)eloOz1XvoZ!xeIq3h{-_s6k&5x z{lOo;NOsfnY^sFQbKcu%1+pL{I~BnSQ|gV1Xg^7b=wU{LI_L|h3*I*&h@1>`L$g)h zdEVc|qwPY)R6^S9q$CcMozJeyP<7>!-kq;Ih_&>RxIIKJ5EqUswo9*@3ZS|#Q3C`i zWocag3CMe2gOt-xJZqY~{u79GJrm;n#e^Vs`xqyTiMnQMA0UZYCWSnCAu|ST=L$i% zS6<4Oz_>{7;^8Qfi?f=^#&{x)`ZulSyB@`bau(db`tX$jH)w6P&TUR~2MyzC5#*ZT zm?DPof30`NizWqqEz*d&u)+hc0zYP%x!<0o*6-*Sxll0_FG2UjQ>Fv&Oyi;ZbR5)e zEVCVOivR(wIv#u_Em21{$dV#vvpkn!`ju+|kB8?W!x3VbH$Red`*lRM?BE&mqkL`@ zrG&ShuwVR-060;~5lck5?w@-`I!V60I_e{lcCt4g&(U{(;Q|>g!|c&01xuJw+*G&D zPEb*T91!|!_Z{j2o+nHiMU{Q_%O$_tHxTOupBlSKYD;{cn?%pRkc#9}ziSjhIka(& zM{L;qE7-*EkU_^dG7teC;`?&-pv43|X+FEk^YJtZS4O@0qdotV%>HSgNt+Sl6>uBU#cu!qd*NB!YQn-LCJH-W~8ZpPljdntT4?S2V?k2BF;Ak>_& zWPisq+`ZKCji&^7f}>=I>pM1>fDxh^tL_qY(KT?VhgsLD2XyqAl6dX%iCr_FV2pXs zHEK%|u?z;@+TMq&M9yU;)k)YuisgOe^2M3rbtaeR@=K!WF1?p3r=4cT86Q#GHk8HF z!&x%Q;Ihj9hNa~4R{MLr<>c}TjaeEJ3^zCFS`Y=svo_ni1O-DN!s4?Z0r^!}GiL1koW?q#Z6tjER)%<&RjAL8#zgyp0vps4h=?xNq#f|c&z>pTWv+CGS`57*O zjOPW>2D-e(nhXJn@|zkQFJukvhz4Z^5DZ<^U_-OSSQQ9kS{PVx> z6HO8@yF%PLtXk1_f4*CmC78VQ`_pHSw+}dQda?l#IlD^-OeZT&f0OE(X*HI)dFHgEZ&C>E3z-2-P!9;} zujX`Hvw;Eyqa)zjfcsaO@q!;X;{9MdxcsyHfJb?f7$Hs&jVX^9oU|uc&U%_L)^1o& zn-5thHraH3+@UuO_r)T$;lg&iaaNSa;w9_MGKX8nawE&PqVA|QSM|>yy5c+T5#J=$ zx+JlnA(@rdC1uc;e6+MJ=RHGxjTQZy+VSI*%{l&^WYEho^x?|4*_1&d)5{W+qN7aI zrCslM9_%}<(LgDBRX!~--Icb!@yXr$N32d^T?*5xtXCyyUmKB#YxGnxJh9NjG;)hk z^lX}%h5(Q5=U0IqX9j$(tqYpoxg{xc`IT9f(ExD+o>k04(mZoRx*P9e1VkV0o3MtmQ9pCACBDa7ovbe{qK0KYq~LdL$vu z`quAHx&Qtg{0A7=mUt30jXHNc$Ml=Mpfi!{ie|0V=ZaaxJ)tsNF zxHr20f|N96bBX#BX_1r>tN~g)yc3#QdYYd;kK%Zro*?0Zq|1NQ9_M&@0s-@W{ zt!wolPr@ai77XIPk^P=j5+Sj0#P`_g{cOrQnE9Ay53w$Jy&v&iA}W|@DA;FpwL?Pp zLc6;z^J^~hq9AJ1wPzA>rzRzcTNfzSo^*(HR+BETUpe2qsGi63fUu&c*=LekRo>v7 zdWEq3i(H4RlY`-xBvySLJ`P(AcVqFNzU|bU(WEZ`I?-aC?xXtFW-7kTw9l}A0#m*4 z&rlH5XQ;4_QIq?p5^ws3#ryC-luXXwh`o|1e8GM6MhUS-)K7pqUVD9A7y5AZRnK@_ z!s;st=mcrU=oPoj#aEj0^!pN)7%~@5c&B!8q>c(Z6{W^Cfka{ndgNS}xUq?~z__^F z<$VdaIN~-U<)vX{o+dOg6h$myG6cMJxOjT9HZpbeXbG_l4wSl?951>uP4h@f#PzsR z9}>~<=PGf>vtFlND8a|#(BzNc9~0L_j|SoZK?*E*zFA$Kf|0sL&1 z6VS}SNh0Q@t@59|h{^VkEhy?Txu5!+vF%mi@zGAla{u$+Q1@9drZ(Se$7~YpvT>y% zuD2TOdEH`bdJc3mDdEw|JoaYS(_M`AWr;k#7|^`$wFk4VC05L<%J-6zEl0FnA80Gu zF{XYSS<8t_#IpQxDqZe1t(WZOz!I;Q-E}_!i|)DG@&x8;eIc++smIPAx@k#+2ZM7R<HZgS^o$E9yA>DXf^14u$ zH8xUW_MdfXb4)+v}e|;iElGnRSRARnmCKk4*CrRM z5Pf{nQ6mHUh=u;(((n=T-S2uKLN-p3f%xBLgfqz{-#e2jPxvFonSO~U&4+*EKy>SL zsV&j?VW=GLA~z<(23F)Eax`C^bPwxkmNW;JP&(~b4j=Y^e!OhzK>&0dNg=B;2dw`< zydk_FNc$;sI8%EW7)l&A z4kmC76_$#UHS;|q{=yeeQu$E$ZBlN$C1btJAaw0}?R(cV3CkDaT0=)3G6W^RAc*< zMEy%zK&};*3_0UelOMT7+zl63lXq{vAzgPyK3qC!ExI-*#%{qdOPkrp*8SUPl!je? zU=1{*nYp=H!@Ez{=7KM#j%FU%g8~_`$g8h9*I8W{JZvUuizetTAwL0nk7CapS6iuP z0R#IL54(5LTV|j2i!e&p!~uoa!FiW6PVtMcQbT?{qQmNh#flIkNj=sXEX(XO$)qLF zVSc)yoUnIUXTYVz?e(QLRrDUGV1%R#M`{O*R+K4Ca^k%ohcsN)9)(zluGh799)=#_ z3sJ;`>W2uMwZX|o9ECnyTcZU>!gc6NR1u~gJ#eQF`LEGQ@Z3-wvMyn2RN==ENm zl`S1V!vFQvm#|D3sQn?u^z{eC(u0lq}AxA`9s zEB4-#$AP*q6h=lYt-_|nkR$2Pms%&YW%9Tq-Jc&@I*c^-MZdM9yL2XT0XCqoeh(2< z9@RZdS4oL!b;f-UJDEtp#*$yeoq)BJo@mY_-!YB$Ow-ApF7!-jZz+8BbPuZ@v<$}!=}ILS+ZmWPIwZgz zjHURCaA2K#Ikdm9qnZ|j?$=MP-{ioUdAthFcbPPJH>AuvE>6d!4N(m$4SxxY{nEk! zO$ZdzuC1x`QbBJ-YgTSNogHm$RGf*9avN;yT|WWC@@X!RRS!YwPi1qDpJ;%>@g4W! zf8gYP*7qh8xH|4l=?XtctT%cC;wVbhl2h7Axp^yRNRQ$!;qecbW}nSn%A_uC8Rkf$ z9W(6yEWPl>@EY$O%Lx_+35^QQ#G4K7ccBca4-%W8xBjICjDPhulWOsF@HzfPe`4BH z?SPi7_hs&C@k5^dZO!gQe zAS{f5iILvYme37s{IVAOf3#pKzuDs?z|3>F{-R9q#y8KLt-Oq|&O|CT$A0&{RBg9` zl`87de4(hjl1GjRG1M#T-g?T>8@;F159Y*hhxPQ|1?|llJvoOq4Ft!2KLKz=qx>g9 z-I2QBI515omn!7lT^$K3=Le+R4~*hiu3f54RdMnl1gVfUABJ+KS{H+6Zt)p3GpzmD zX%*S3ub^ST{#ScRs$oFOm~gH{>qX{x8P_G)0s#qBKsnEVEx6vlt0d)y9vp)QV(IFP6zP@THFuntZ zJwPKn#*w6`GuDX*O%G7Q%W`XyT!h=EfR>4=aBxxkfa_VZXlPnYE|uFqu93%ff?!;d zp@h=N4s`n%VFEZKucw?7mW`7Wb1sHk)qQFj3PVlPk5e9nQ6)DZ&YHr4augD zdHX0StFnRe+HoScEFx&2S=9@QTPbvIVr1Q2ZF+YYXeLe1wJXxYVw8a zA3zT;ZC`?Mw-XNZgvN}o)@RVk8l_YEq;oxuFr!UeGo^v5ik_(+CdDe52%QU+uDno`HE)-s8fX+A8zNc}-KTaw>U?T@(5?o4*8I?NM zGcNjFsJRXPC*my;p$yt2OX5^f&cvQ;?S)SJSsyW4v(voxs=hKvYkrvT8O0~hTjKwH zz@xb5ZH5npWkh_{1wn%$eWqv)FGOR-_vUFevyH>4vp)fs1BtjB=Og(aP+IhX28nG> zpELfEXHLtHlj1)C+`!9IatuMBg2z%B(Oa^;8UOm;C7>C9ov#j&}vk7aowc`~-+vwAZm0`6WQJ{n{ogpt&+EQ*s0I?oZ(9Q70$S^${)frA)M?`4Ucbb30N(%R~Ly*|eWs$=TOa?Zc8+mD)r7-3y#W6oz)PGV&+xE$!JkpZsX zqSj=AHs!Y)Bd*}r@Eo0UQ+V8BM*O_B5r#@ymG2+H1olI#MYdD&q;MPYx$8$A?_V1l{FA=U|x zPWC!!QIW^#!>12-b!eN<_cgfRYKN#COnEPt^i=hvp(^p~IOA0vR#5a#R!ySEGA>l$ z)8f&F#eP_l9wkY{f;xzk=H6ZPV!Z<*IK`h@GX3>>JNJ7RoNv}Je}lMFG;#_ZrIJ=JGY!Q?z#BQ_?zlN$zob3ehFj zc%kzHH^&>ql?f@jf>#&pd}(R}I7g-MKg>{=AMw`L{-~piy>`s+xM`W`NWUNBcv87Z zJmpC`-Z@9Z#W#yeEOQc=Q>4~Qpe2x-CdAi`f8;J(NilF;?4?kl+JKm8Bb|fzh%kfG z533_~UZF>Ht`Fh&V=>m4UZ!`rJkrGA9*{fui0-RH*Nqrac0#K4%Ubv8nFH>{^Y|rE zDQP(s+U2RU<43Plw#+Ks4r|tUkr94>Qm{nGyU;A~`Vq(69@G4(X6bju)eY?FPfln{ zrynqrB1}CT9qbZzKGGt^_wa*hOE;#A2jaXWVT#J477u zrkPu{>h+*$HOC8<`?RT|IO3BO-8V+@80uoI_|`8B&oQVQEd*NW1Q7+^A$GfY-xK%Q zx@gq_%xVQzat=-cDr%rrVFNh z-o2qu+tf0ikl(E+;9+|EYBopatFS_?9HexN1L~8h#c)u`l9HLH7l-0`*i< z@reLNnxTC!0wSo~(mB~~4n$r$p7`Ms8P(wvpQpWW#2-E75Tq6L>OFPf1>QmJ`&V3O z{;riyk%A`_UJpe@n>|@}JpK6Abrzc8ih$ERpKes_vrv8(;6nHp$0OKqis*PjSH!{$ z>7AC?&NfkjcV1rE1JW@8b6;+{sIrPZZ!*37?4ap?CYY z{SGJM$s@o06wARQuIdaAiZR~d_%sQf>Ode{bz_drZdEctu;RhC^x1iN&>?P9N zGZ~n{CH|bqQ#pcTjSI@EFS+&O*=|EPAq-I=$xN>nVn^J?IhcGp{lq28*-gHg@*3@^ z)7LO7&woMxwAzA95B@?pwl`DM2Ez+wjee&h80bBX!hk|BpAXl`)V`>K%j8Tu8*{#!o zrV95M{In_KMHyWz?vlHtO`KMhp`PRvE&M(NrAJ5^m~I`u*wF&6adG(Qj*)@Z@s7Qi zVK_!~IXuhgA0s&1t?3Ms`zhr{Ifb@buzaH&$|Z@6{8k`&Mu|0&ohs&-hSLoFTf&qT z<}TV?*;*uME)_(>HyzZf z1zQ``J)B^clOGT;D! zEGOeg--W}QPYLR>!DA#9WRa;eJhw^rKF7GODV<7c9An6kG?vzv2MxPxb%%e4a|;i!kvFvj#3^77Z7xDeo}pR!h54w9G7XXDKNQr>uT| zwI*S?>98R^+%CHP4cV&IAp8OR{rn-v_$DlfquK@}QN!kgv$VJ4$n?1R{Eh6Fy`9pI zrZM$h(lUF@^%BS*<3I-}(e`IZh3gnC_DM3?OIkA1O+@0FJ;W|_bNE>stB$Q~3BKlu zZvHEqW*O5aoCOl@THFuLo(<;JCwt#%LzMefaso*puu6b{jl*7QZ^`HA7%J7+6ymAt z{=*Pq%O}ZOmi1iOHdR`qC;OC>I@3Fm=An{~v)|$L4l9&4K=Gp)?^qKky&BEJI4l`D zKI`0ieQx(`kSF|CjwvW0LXO!Ca8m5)?fl&GBWOSZ;4!>AB8Bhl$L=e$%L}$hBky7` z()4n_4(Awp0Xyxi&F}6N>0giFrCRF^QkHVTS3AY{ec+$l>&FlFdl5$D?~sse+q2 z!$rIA?5(i;tsx9t@1XEis6;@&akc6KnkZ1A#Z9|R{nl-P>V&{|xCI=7;9n=cg%TOt z`GlKe4V)E5E8QxVwJcN%mRegsPpQ^sAAfTy4}oi0{s^Dra9^{LD_bWekG~^}nZe`n zUxqHE&m%Uj9!+D#7mrMlm^H@2@%haR7jMbT>-FgK)%WlmJNuZ~x+J}xHKLjZf%p|U zhq7JwbW%^KNU?%(K5k|VPQo*6TB}Ew{^@9|G#*O?4Py1DAG&zC8vnI zWHp?UZN7+Dq2n`gBp7tFBc3XGd6v=R&TrFSQ(};UXsBl9q3Zv1aZG2!o~RaC9#nEx z9Kj#L?>!jzk?!_(?2Q_oIG;)$+6~ZM_R=ZyEp5I){>218!KY5JHG`W;jfn2wjw`iLRyoZ!PAM8V zXi}yVm!{Dy8a_LQi?WioO!p&rX;6KWaEnzdbEHJ;u=F?UHkR`jP-a^q4u4itrbP{y zewAbIWQ(&;8PMGkeW9|VrayWX-4E>m>Y#XMeWI2;liu$TUZ2~CbAsR+zd_ha3g+B` z+4Uh+!0$Zkw=c`{xbOXHfz~57iZi|Ikal!9qk6lI#72|Gidqn|? z17RpAKPyo<BC*KUlMFy;KrPxUQh?vO5X3mcX+pr5WyNxN6`0b0pH=WOnP;UmQWjLPDB#u&l{ngz-1S+Y^fv4ij>HyYb@qu^ttd z;aHq`aDsogC@Fv9o1w2OGs-J`f(3jM zWIS%RY0$T?25|Ww-5n|>R{Mx=@LoN5>wy2^x4~Xt{oj+S6GckmLh?{h2XGEHi$f=TPdubXGqoO0?AYoqwA?-zf^ z#oaGw#T~s~1+`G=faEEVgR=zP5~8>N{uE`5TroO{=Fu^IYs%b7kd*G7!Upo!VC$z3 zfH3#ml%#-Pd)p7lH286xfw=+c?LMLCD2^CWx~ znu`5Q5#Fn9)(z3S-bHMZWhqn9mtC;+9xKT_p1*|y5*UUpp;39w0YArL3Uk4s18aI# zyL>^zx`ntUhgrLnSTFoO%x{1J-9Ets7>$qU+NBAmSvt;-6YRS^7&rAF$utQ<>i^a& zvzE1~F)ouF6T8c+-LHYC%lBelGYS!6b~{LB#5VI!4QY>=i85L@I1g054zXR1^A4oDCtP+s7Q)TVl{oL3|DY%mzIV?mcG_9lNyJwLZoBv~Ke9&bPm zM`=qveY7eGo;$(W0p=~5o%^l((#;7kPE<7YW+A$`Ti zHByHsDf$N$zqr=k$*FS)IP#rR`3SySWcgd=fC-&!-sW|B9%0#*XA3pz6d9NC67Yxw zAX(!J=}iuT;px6W1G6AUPnk>l1v>ZxIdO1`@Y+I-Zj(#EnWcz}NF19H!=88w9qT z9ES`J?p*26ibwtYVU1pVQX1DQKmRdhW6-&I+q=Zj_QSwWS@~Eql(ZX=| zsnDc?gBag7eQrpZlNFAh$n4zJL({Zlxw9BNKYmVfr0oL!O=g&0S-i_UiiT%)@8?}U z8U5njWc%JUti}fNBw6h<;an#b)lR-?FCt!o9Z0;R$#|@=0f}_46WS9-27Z`}&A8U$ zn7_)SAzq-7Tz>`5%_r#^6w?;zfAJItOI5QyK!fXJvEI7_#~L5zU={FyYrW2f%TqB^ z=6!BXvG*EKiF@7a%Fnp4(4Z>RAc@ZnFCLijpPfz(p8;79Q6CVc_UM7kgHcX<39xEQ zl7nAk^o8uqhrOIPBR{6j+;cvj5|rtzHdgR=JwORG0bUvcNNu?zs|L`$Y;^% zCjQ(vB8A~W;S^$JakIK(-P$7klcZy-$nE7anU2^lc5*G+dbfY|G|?$A`WqHz67fIvF^0+l8gkRRy2=zG?6vYQpb>420xrgWsYV@&n&?LTbJD__m8| zUm>5_U6_Nh1}f6Bl_Er;2~?yHo^QLbO2jJ;yqV^CGkP`Bd7$F3J*baOed{?kk}vWz zK}ykU^Vcb{ka6H#9|XWR-*`ZdO%3;N-{cAlvdcj38E?l5T3*+TQs4*2=kcwdboctN zaOv;?z$0X`WuCu?i}}-M&ClLxrfKIdQ~XbEx>Nkup6arF49?iJggJ5EqDa?53jFJ0QTL$;m$T7Sq%&JwL@ASfg23Z>cTG(FLb;xJB1eJ(Pc^0APczZqma1TqW7 z3_48GO!pOOG~m7ypt(X3ShJQ0%NB&08*}fGazMTg^AW{`+?{~=Bn-&T7<|&s`l1Z| zUFOn);a`Cvch|%y@6moNIZ1B2l5~zJ$s1b>kr} zs_$^QQeSw>BMNs~sdKmico@;3ZKILZ?Ycadm11}n3iHLgADLY}$C|WN&{}N;6$bwp z5gdw>x$B8z3JFQsq?8pSxzxc^{nQO`pS8C$Nam)r3ue}6q6&Z6!EL!S0T@nlw^C-Zb0SP>Eb1%*NqwY-N}6ExZ!{J7i)^1U=&;us!>{(?ohz;7Gsh~{;b zJW>}txE1>kB~39(1*K0z7F3~tpHzMf^|%8hn(16@T8-slx|bN(`j607&IHv~r-LTt zKseTyV<449jqXw}GEA5xcJ;#QWyNA1ty-bUQpdmxWZeHznuKi~oXrAmv(mGa>E|O*-P27Pu^*C-#jY zb|KU4a7)yyuz#N79q$`xt{BEb9hMcmEyuC-hP&l|`ji?mW;g29hQ=65$a1~9&9AJj zbd5R>+b#bi^333K1uiHGBtBk8nOV~{o4AZPQ#ZcDIq+Iqo|cA-_HxX2I9r?U-r|%> zHX5FkC{{5@2HD7T+wQU|Mp@ErmlkrWfB9%vo#Dsp+nln%K21=1@c15o>&8YY*jlAzw|W5YCdzlXa*;1UL9Y)0)R~pN z2aZ6Is;|J*WI+k4z$x8p+D%-=ZaV!&?6T>BnA_wOXa$ePVSC$0{&a+A*lVn< zM`D5A_HtY2ONsMmU3@L4kx%>&X+L3YNWG_+j`9?$p+EYsxt)|4rB6f z9gD1&oqUagR91AHxDsFar!%IUSqokHq;_lV9o${t)Y5-Q?pp<@5k-vaaEybzUjMO0 zz-5t_ix>E&O}3UJq;sts*P^e8-oU;^x^wHz zZJOqa!tnpaQ)>D^+uB_paY;UGO6~{yElDHa;iyF!6m;G@_mHMd#aAJIs($L^)$)%R z?VTZ+*Yu5^N%S`*44bL<+(BqrY7XG&D?9)@1k3*9#HoEO+$j&VFfE3IG%$wosii?S zQh>06p^8IYZZJ#lq;Ip6FiMp{lhA?LQ<&6#L>{87u8rI4u%b82#LtMFX}p`ZQ7{uu zL&lmcC@w3)7q)E`fee&q#BdGeiyD&HyX6D!!9dxztj%zOie9!_1B=PbgR!f zb$zLDJ8`}p9Q9Qj)bBtjGc$}`3zEBm*pw}g3{5HxTPo~s%3j(Rh}G`%StJrSX#T8o zsZAl1z~jO{Rmt9g%XK}!y|5HwOS%$gCiknSD8OiWa@0>lSN6$1<}>oZ8y1D{a0a1! zj+cexuP?l7azlYa%oN7!5WNs~L;T`iS*B|DEH>{&!2&(H_tup4LndQ#`gHW&@%o~l zdReN`?`k2RK)*o8c(Hg(OWZ0$efkQm$gPd^cP&Yf5;evJC)=BTzllqHPpR3K8ve?2 z{;d35go5+-2ri$af-{uglNfUXM&>?&m+6*K@BDr8vS^qH^*;5!^6U$!=7O0whW7$U-;31E>S;Ph{?42Ff-Tu zEfaY_)puVafHSTcLRO!2;Ypwxkx~wD&{Ol8)6o~(O&esuTm%3{jCjj+az4P;WMCm2 zfDrQFCuA`iqp@6UK%(&#;Wsu`JkY>Pg@Wj0RBMcx^k06x^|9T!p+V_qz2mB81`bc2 za|f%*Q5p};bE)T8ImFVP*w2D{HTlS*zUpl7zi#IAWT0pvGIf^)MNhQ=ON_6$eA)Uj zlnLlGmL^(RkghkCy+;>jS#GSmT(VLAZVAbS}LWV%*eo_!G-==>V{CFXBMIU zH{s`QtLZXJi9&L39sD>tXqtM%hW`O|zJ^EHdTp6D79}-rUQ}RSuDx8isy;?sf}7bb z_|I`q(M%d>D%N7igV1{rsIF2b16~@ZX6h`Q!n1?VP&Hn&<5{FqocR!;;0a|*#}|Fa z{G}{>I8DXpA@CF(_C-7lll6Bs^@)A1_i1q5= z-3Rs!Ju3JI!nTnGdy~ZQkH$G*q9od)!v7jJj5`%~1=~pbnA{)C^P}~$qqT4d3KXc+ z_=-A05`-_a$unG(T*te?ug?AYkAaN^E!s#*ZvC}=j*=4@E3V`teO>4%36>!v=4WJd zb^sPoH&y15PT{(((sTn_jHchQbB>TuVqP}5KQph{0N_3Ec%s)8@7qy)d+npLxH(>g zC-Pk4%%8PNL~E_P?1$X`Ntj`~4}J}?EQ;M2ZIXSVGP5B9!MNB#2*Ugrag!^PMoO%p zwaPaOt0-5MuMEPrdeg1tS&UxZYaH&NmELP^C$~fy)x$hK zC@Vlg`f=>FT*fRFg^-9H$2&b=VKD=S{C-w3FXHBDyf}N1$+ySi&;qoCapq6I+4O0r z@N5xz_l*HDo)$79qXQHEhne+xEe5eW?=`!|MQpO8?x>(~;D=ng_`#F=F3&gGkfkt; zpsLQ_z-Tm5q>^FOc+r>Ya|HuybvhEQBkIPNc`ACqJa1-^$E9JJ-7~DN`2zp`R zA)|v3hR6zg(dJXlJ1Fh^e%0Pt^-$Bc7FHach-QrYCcNl3sMv-sCC9)M~MDCj2>Ziz!&5cA;~|?WWr`DxsPl9Qrlo*KDppF86BJ z*^nK`#V3TwmA}dVpM3ZvW0h&%Y1)&0&PM3-d6UuAup>C7WZ=E)3fnMM@PLSkcQZ6=b7(o;;h$_-pMuTd4Yw;cOE zuPM_`hxT3O3EuBAMdx->_%2N=ajvF;cX|C~O!j-ja`X@4`O+7budbjcO)<|IaU^T( zg-1-`+b;3{(8(=fgX+++Cq(R3w^QgG624NG-FS#2jnf)%JMkXw1RqX{8n9TSuuhN= zoOSooiG$+DW^c}rFMs_j5PBfAmjao;6#B4%iWY*F)8&4SEgfxB41o-iOklC$NpRaZ z6I#Z5l|(W+YEf3KX`$MNDTnvETVJMAEtca!@m-pW46UB!a$hcU)@;~<(#4(_7~cC7 zS(DywBF|&;%^R^#0}p()OplTqvOK+PL#X?NFL-$YwgCH{i+ktVb!bFyUl+b}OyXg> zHZ&j9eE^PrBRp3zIJ0_@Cc^T9b|9FqU%}-Pa~yuboKTp=FBXnT+huQx$GU2f(6G6L zmoi7gI_3#9s0NTs!Wrd`pB;Y`S~@;OpO&_ex05&HohoQ=fF>fyJhHQCO$cuIRs(P_CxkdF<@H@*R#Wfz&w;#wwu@{qg&T&Cmvk z^@ez~`z_PkXyy0E(Kxr=wYZGa))bJSYB5*k7g=Cw&FjjfJZ@eKU({45i_A-J$}U6Y zhG_$AOHyh;DFv;tRkGmlZc=s&MK&G4rB@wHF1}pEG1;3k@pjfFO&j_4Y#Yz+)=q$+;t- zShNbA5W8DfyhE()$=yRaav%X46^{1F22{v~m8 z(CJSxWjA$`o@&{Bn^pMK@J=WjW>pb#p8HLKs?8ZzW&T3_O76&1a_8`!bWIfo5yfx|gf%2H?eI;pMH{W(mT z`}~TISMHQy|LmlA68!a}K$K+O!XrL%nhz z$UC!0&Od$ArEYrT>!xm<{f1MeUSpYLFR>3FuwdF4tWm$1{<)03$>f-I{ zv%;l(Kj=qkB4}Q@Cm9LIySP=G+TO5YyD(aG;H$xQ>oRk=z^psKU}t^R!ueg5TyBR1 zhLk|j;aHB|ojLUKhh1jfdpDfpJ4INJm2I79g1JB*+(O^sj?~Ehmeu9Ivs&(w)ygix zZL=#bVJE5G(}YaVMh%S@iFJXwoqd^82F7n+pq1`ajAQO|EMeb#aSOg?>=7+p|+^>=^?h`M5Q&YE=_p*+50=(R^gq$br5Dlm^P!{ zwPlwPD*W?Zah)~EIb7x4&s6GC}h+7$ZTJhS0 zt3DA>wnp?exc$r)cBYq{L)a3ebXE9mw)I$;@n3Z)LpWu<5@K_ z@(RcbW>eM|@Wb8#keYi7=WRbOzd=ZY&=S9gU0_0C*WP^`87<_{plp`@8$IHizBQf) z>nF-I2ZW$kBn!KA{2zGLwjggtbb(6WSrQV*;;}lxi^o;Hy&y(`0?~rj%;ltyF}vB= zgZM_CR4?MSUB<#0=cPh-rO2QRIQu=aF1@%@#Pe2)T{wx<9>>9)`kE5gQ@XOec_KQ% z68)Ml$X1B;AhN;Swq>z>HRY_vvXysK*u4@0R?7%mt-6pGNNZ)eg&^_ZuK4ns2+_7o zn+acf$HlYb`G~1+AOzq(&!weG&}Q}L1hb$q&Z*3B573thSPuo+mgGiYGJ1Z*rGkpE z%;zS>8)vOpo11(=a!RQz3FXhl(|6BZ>lqt}9D zqUvK7a3(4tw+j2B6^Old*yIp|}O%EjnhedGFcnOwcX%7{`UPM(PT_m2&+* zpWe4*XF2v-|A~`J#zg$|CeH_4NN{W1%m6tjAu<+`DZg;PEzU}|-E^`A*hbrkiQcA@Q43jR~HCr$lT#Atv>Yb$JqdCtR>L zKL4+l@-MdmpaT1!A`{!oce0m>#@?1FHE*6Q&SQ>-SlSU$pm!CD&KOzrPDaYVwUD=r z*Vs=F{FS)oGu zc<=u|-yV8@6tnsql#AzZwM{roEkH)t6)c|Zx2C0=d2&GOJ&C{=AN#4;><-m{HgbhEE!p^RzZSQSiFhtd_~19_KRo&PP%4b9S3;1AFVev3~q6!*xU< z;CC4&b%>QcH?A1YGIfy;7 z&XJ$?8Y-PoaM(&n!}J=81lfF90GcSt+xRnk)Mz}-G-5&Jnsp1VFi9Drz^GVF^`NNm zbXLjn-kLymI0)N;G?7)Dii-)b!6%oZJ#X@O7jK%g1@3-iPTgZGKm=c2h4SwBD zJ&ts|(wR3~2%1ACvtqiam^BK!>FymfA=e%y@1`g&K?X#Do*f3cQUa6qWLMHn@8Jo2 z{$IEl{8C_5&&HvfH0nP!Jl|3#|J~}Hm;Wl!(L@>*G=yXUU~I6q<)TSa%?}c?{GiI8QdC&zUen>wdNLzAQlesNY&^L zsgE{1mtv|VWu41luMn&H@|1P+eVU8P4wb-^u1)+(D!<|Ki#2oa+9XW_R&e;C*B-sN}&xvn8s zXXIOeyz|Gz;$DYcyC1CNL*@8(i?n3EngUCPLD11VLuvBvUk!^mGVrPk91=K591M=}^&zG2MM&=B!q z@%dwU{P0rPn2EXW)boz)wyh+NRLj+zH_3HA{*>4hRG`}O2a$m|CIhhcSWnb@PLXan zW3&cTaP^uykq!LSnn=ly|MrjO!CR?0EU79Yv83d)#c>B@-}3n}T0gog?M!d6hs0PQ_sL;FQQ` zGy4{wsnMPDmiN0X{X~Mw2&-!)*(8KH`OWX>o3mUE=Utw_=8hAKY3lbhXxF73-{}1h z<**TI0SinXCyYx&m1HOESD=KgwYta5h(?JqWLMZ%@!*vt}?On8gOPr(|ML2v?aAhR7VfP5Zlxe z;H<#Od%bbNO6XP_f1Uin<D1du0-X7mK1rz?YXxkvVs`1q__}sy|`sq{6rhG|=DqnAk$36z&q|_QT>F5=!lQSigKr z2mvV(8XJf+c*12(#IMwtKg=YH;6c7r*KV_y~L)iW~qnWn$$7v4PJL%}o#{%c%e0?!b!+)7f2W&D< zKj-2oYCV_EmS!{u=&wI`jXdCJ2Cc44B%LxHb5%XM-=XRk*6xUEc;sK$t4Pvla5l|` z5}_2S?GRt%hh=}vUNO+OVRqSfpuL`dwz44yU?jXOAcs+ppbGe-L{J9cq9bjhJcf5`h! zy+ujzU{{43{W{(D4DyYIpThcb^roMvRt!gzW)^1a8!{;jyT7#Fm2R2-5r9acg*p*; zsC{WY`T(ldyGI3Yt29^sJ8$XJO^uBjkt0n}9-lo-;~Je@q;_jpp&_rz)DQ<#*aUO0>dT72CnQCkA+K znY*U3=p!s7-nSFRb>V^Ted7u&ZfL(n zTMu!Boqs~Ie2z-q$ePoge|nIJRwo2tn%#SSZtuLY5S22C-~QBz4a7n_hX`cpLm;xw zZH?JrO1HHQX!4hX~+yD8T=+kcMeHie-bWmekM48^dpw-zShC>4+ zdlNkh!H2dxSDD;ZeHETUq4#5?*{)K}8&IO#jiy9l7 zgE$pz3Qha>HCtD>KW*3riF^R?A`BZtHUOZirW4SDHUdMC0(}~FFI@$?0HY6uxgyCO z!K|hMLLA3x{x+)5004#|RhSyA6tJ~QOsBicUAk{aZN%!DK0c3Gn10m|e);xtcSCc5DJw9gITUM0>UW6G7@Od=86I*K(*RKwJ@s$lud-+=<&yJmot z+79(63qu}?Yq>aG4BG*7S8i7!&C15kI`SR#=dkf{al}b`I_RXLnj$c~4vPnXX#R^- z;x7eLwvVbS=DVlqg3D0~g(WR8&l%m7Bsg>9STFO5+fPKe`E4Sa70B8#+4&{Y**Sk0 z;1vJ?B)mYK&dvlN!w)MwhU*bgcbNQsdAPNHNm=G$col1j#m>U=kt--bdLj1Fy$rhl z%!{-n?@|M=A|k;0nxh=j_Bh3zD&)#YyUWQX$62BI5+IuU5Er_mk>nFabXto;L%Hj{HZhL^pcJw- z2`iPv%Gx-oKUlE_u!8uqx7Cn>WX|o#PWy{+eF2%QIZ`W^o1~0S4gi{@7h=LX*ZmQQ z9f6s^)HxN<9G@PThHRWOb#P`9ZC_0>pnE6&PWn6z47U7$-R8b$tvYH0g>*`R5|(FL z{JPrOO0pS_?G~htN^wem(6u9eQ&1epJYXI&MGOD^imE4j;+<4HSezIJ+?elKuYnBR zOvA2WK((z>FaxGHKOY^j(;tLwQgbj?`>>H}s~cm?JU< z`-!o{F`gb9>#d|ntK0p-y%T_YK=ioa-V5a${5*F4DbkYucSpeLjw%%`-A5O=odO=^ zbQQmhT+#PwZqNd$GLxuhn&CVa<4A-SyB$k(m%A04GV*HV%_4kZKHnd~Bnj%;s@>G& zt}yehSn9MPW>v^#=N-ewxq9Z=r|!Mfx?|m_L+{%|UYpDR6HS;kIs~JWgJ3T8Db8~x zLbnS=j+W5YAov#uQxW)_36$k5A4W#U`El`VLSTO6x$g!hm6Y*W9<5+S?MobcNlCf4 zS|gvH$jdy)K6STOL{+wSyVQ%LsM!oWpq>Iartffsh{vbN7b)|iD88_b*AqxuHx$7ueHVSzu|(BZxQ>xjcujGM8IjZq2hPATR)6+N6c*=D8TF+&qcfp zTLere!!9Ff=FP}^ztHgrpb#VX?sAUa7(ev<7?yYacp{)NH@E^8aHlnTkoe_K68J3W zU!FUyw>}+VY)w18jGHTf8}OS1q!8u5F!xEw;l~q_(GT~zZI*P@n(+@ZU+pLnE?wf0 z6d+X~fkkI|CL)b>m>=iEq)aD293QMJV?&C0yu042YN+I}bs5PAfC2(WB@mby@D)Qv zb-)+p2vLH~nhci(&`Y9^<&8k!4PJ|VKMj^z|xd`t911z>)u^>)H zD22+8@aP503$0iwf{$!M{Q;4O5N>;LYA z`w1P!`mvu#>bu=P7!tQo`J?6Fz9hRp0p5YxtgvT?aVkx}tRB}NB-Cd+0z#9+T6!qM zwESkIAv<(2q?13_A57Wv>Ce{hNd$uef7Hk9AP8$zo8F-$E!fK4Zo)XuCS~XvvRmz% z?l_Xuq`A_tgk@1l=#- z8o%At`iBrCqWg_tF!Hd?Zkk4XQ-Y6xuKd8{3ChS@*{#mZCuHgMe~#zq$?Had7Y$RS zrzVl7_>sf03%B^}DYK$?fQr2ybh=tkqy<}VtqIvq$r-_wKlDE;;_6W~um45FKDN!0 zVZ{}RjFc7xzBj0*kV&hAa#x9ffnOvj`fjG)7Y$)L+T_=+Bxn897H_qA6T*lZXh*|H zERoQ)myU&3R733X9+l0%Rj+-J6(G{hBe0wR6nq)&Y?gKE$hB2to031FQbk76bnZ5Z z8as9pp72^^MpXyo2EU+8f1A=aQ2$VDyru_R>G4{iI2|E>I_honbx*@u*N+~9l=ROk zQB}aDOaqLMGq0a6PJa&Sa56;IRAAMgJ!iK!R(+6%Q3i$>SMe_qGs#6}Yq2FSNla(* zHs+-%ClE0Gleg#qjm)ZY=2uhc|GeG*#NFtijBvD(gabon5t|@0)$Etj|JgTXD#Ivs ztMo2>KkOYj(j67yENDii$kazd1ZAn=OLiG0GUdhi)y6G4x* z87f7Gt5aK+eWNiR@4CJ$%5x6-|Jp-i+wX9Bf;qm_N=US2!>p@;nGUehhXo!+MBc-% z9OEjik^3yde5 zP`ty`HJK4%1#=*R7lM_wzS0^9*#QQm$7t}RBF*=#x;HMj*0?>@Tm|J6qy^etL{|FB zW>%~9ww3Utj7aM%4^dRNnJ^Dixkj{z-q|<3AHPrWV>xank>c!b_<4fFR~d?mJ>4Ny z@8S5f9O{EN!QbIRG=L-fj<5F!&+g_-;@;cvyO)iI2mB!<`^{9Qbl_}+P<0KU>yl#? zlYc_GQCEsSES9jIFvANXVC5@f?Th)uwkl4#bC+&f(r7*oi3W{0%@t)RP@O+ zFS=>uZ`d(iN=?TciS~%6ty>-MLHuj{LiBid(mnirIBjkeM;F}+!|L*#dL^hHWoL15 zYtciZU~m*D3mHn8~pGBES6yyeWdHMus?4)uWv=Zf_2@nKKjRJ zOVlu*T-;j?4FyM$%%P#Dah9yu^&Et(PqQ^z-WN&BmPTbHNT5!`eKU z4mg}vc|J3FNskczoX64OoeckHG%efm2-{{=Mqq%&b)G;3^EHZqORG!bK)!1#m&bhuP35^*g1pm19`k*sw zOH}3gCXFx1smCZ?LiTAw7^a;(lDp45`}2h)v_6terKWzS#!bboZvYqKPYFm#7m_gd zVcffD=LhXT@gxX6$(>ulpL1cxa77B_tfj6z3^`!t7ii-rTf-_;a~-(qLsz05eno3N-d4p--Pj=ObbrOkn*Vk5}`WsWpD_xz1xMwSJ-TNav>vr zw;Bw1!rTwK&r#mOdKJ2kM|SF;`04|v!{j12;cM&M`Px^O2@L#R2>h2f+(QvQlUVS( zUZ;32kJM~<*=i1X%ji-5s72-DH);icb5M{#F7GOAO8ojAytM?zn2&DBH&t7Jn)^7n z&_3{9Oeh?cJmaA|;$#>(TsXqg%#UM|Qp9&O9e9`5LkC`x%rQ0A<5d43;=bL6EsXP? zKTd5%ZtaF+0c@QF2n5(m@J|RT`uuB4vNdvwAlrLO;j&{EX$gK7#JS+#KURPANVZ0j zOhL8{Xt}ZrZjIs7_Q^|qSS5UJn1tm`zZ_b5sR95UF)rnvB?(c(+@}jlLP`{KzBc4U}COsM+ zLg2r3L7CrpI_+x28oQ&13{KI-lmMk42ElpheJWxJ*}*(=H!TuaM!7Zf&B|5(?aM{W z{;Glhtmij8_Bw+hJ&B>2P$U4bE(kDPi6cO@B_DCh209%jo=X5{Mh}bS~Yghx$5Rm;VK12l+ z%hBDR9TR7G0@{StnFC(~-!4TCTDZD{bfAY!`&GuMjjoOg`tvFlA>DfCu1H_&)u33t z@3FVON^eU|_$LNj4I-!!vFST|i0w;K70u%1*8FiuoznN#iOf9dyY&?EHlsmyLn7Ci zsI`vOI04t$O_MJFSPz~-k$bONnjxfRR(^TOJ zhieG_@n4y*UF=^OmWdAP7Ocs$bV|%u)vXVe#4{Z4SV!HE>UtFprk}yE@0ukHo5P69 zyDb}@H`UDjU?WvhG?!!`F41@4y2;u11EAz%l9WqLrJfdJ4gA7;@KFo*fLolcGAL$%0CK0 z@KPp3+4gC=9$yr#axBXo7H%=Wd$=Ll6-Dn03MwqizD^%$%!m?}?$3dr1y?U>gC?wk zr6}rvns;NZu;&$BGYxf_!Sm;sgtN0uIEYKtw#g4P?Vmd>kZDRXy~Qx0%3h2Ohzs$2 zphSp~lx?j3(2&I4F;)`F6m;iCI+aD(GXYX|$xf_oHXT`3*R5O^W(sAI)F}bOG}%#) z4ICv_Gkm;9oD|D!yO~brWN$Ke`o6=#y(ST0)OS-dVCvmjgjmDcr{TX>pKu z^l2-K79ti#%nun0Kfk#1Ah0(&Ur>_$$nYJLv}k99cXWBkN#P>GJC8)pEctkwEkEYL zkui=(IMpAu-6|y$(>@6C%Ed=ZGpN2VD(iM{quhL?jt}lzLVcj5h9$KblBxA9{{4AZ zmt>v@_O-1b)5G#4#5X<6GXWi{dsfY_b^>f(joaLj$KegJo}sj72|TNn#y<02W+EX0 z4XP~1Am!!nlJ&JbS4GX~qqfr}WMZo|jGv~|w~}Cv@_dC5X*EHcSY;ET`0xfQ0u{1i zvody8HLCJ6ZMa)-_YC5s9y4>~}+R5)zxFPpkRL6dEv|#$k|%GPLCAll%u%=ec@OY22m+L?DsYz zNI_IEHu$S!wO_VQ=lg@lcm!Er)h-}o_gs7MCC|H1C?FO*DbDsNvb0QT6B(7d&Z*Nl zR!p_`x=MCF7&kuS^#f7F;XO-L9j1p03;CciIZyF1v7)s1W=`VYY>Wt6Cv^D35ajHb z9@_iwL^=)GybW!WQ`}Vb@8Rtae&@!E0Ovr9z-R0$Ev+v(E}HH9Ivyy0lHRkCC#NeD zfjgu7i91u_!Vo>V(lO@{CDBt(aQB7BhH*q)3TsSm1`V zJ!y7z8>cKGGUh8Xif-Z1?#B<}jn^QQQqz$yTcZ$};POu%KBYU%ww83_n7y9%EDv`d zpAv&EHd+QJJS(@;IE`BIK8|9Wg49RtWu|KqY>zl&b!4duHCFCTZEDM8l2MSo@b3$x z!7Y)>leqCznvUR+#vwwRyd4PAhylfMOf0=mW=Q#DncQ==Amb%^TR-F8Scp`UXnu!^ zMnF03jci5~f@sB;u0;{oWS-+c$ftkp{?#;wC-JExh6%dss2Zbg#-qlEYENDbU27(~ zE60bT_YN-`P9Hbm*_fv0NSSSq2oZWrz`FWU4qb4<4RwJrs>SYJE*Tzw{$v@rH-VeIF& zaEQ;~;HKgBm+puuMN}6EmgutjpeIaw?ABxsD=n65+_T)A1_|Pa?+-a|lPFuZ${UN-LhE#|eaj%hQKooyBsi*lF`eG?a&F-B%Rl~{;2b0wo+V`4TjWdN9f1@)1pIi<%l zVKMv;68I#Wn)-Z7)&bXf>K8#!cndg|9gtK;EG}n%wBcEBGciwuCQXnF1|ZVS~MIro?6Wne8#+C^YVa{>k732q!3c#a>H`qt=6TGTsjlJGJn zUx{x;%dtu{Q^BLQ-L1-f{eX=J9q!ggv|IZ(Rj2kJagOo__8O4A7|3)YCTALQ1AdU_ z-Toa_H}&xRKr~s_{-LpoDZZedh zG`|LB`$|#fJDoQf*D^^1LpV!50qm`JTAKZ8<8p5^W=| zi)jf+F#2oloRHvLBN0;7n;MXGR9 z9&SIgVT5p31w=P1=P!#Ywd$B0WPi30=0tWkjPesk#pQXwAUBnb^6G|*eA+Wk(nYs0 zJI-EeBjj~I9R|kS^c;D${z%{6vGo8r@p280iYQK$aXOZu$Ccs#AA9c|)a2W>izcC$ zASLu7gqkS5Hw_SaKoWuq0@8bLf>`K;&>?hCO6WzZbfkBXjx?o95m79tSa#m?{@(A* zeBb`<_xy48nLYEJ=Z_>a_avElo^{{rTGzVPT2XWZEMyhF<_C9w7S2Db8l5Z34e6cg zS}(Fp={Oj5kG|bj&(w7&RtecMp6KFriz{p(pB(i}zqwcHG3Wquo94W&??4j;fnn#g zXO!_iMiY((W+|rBM@s^ig1#<3!7LJo1P%4waz;~Hg?`}+>7*aUoOXA*`SBeOnkwZ8 zt`|E2ex(ULybcyu`K19cN>B~VuTgw&nJc6`{0FdK&nC$4j_cpRjW0Q9J_TIb&$m|F zzTQ?S@@vUx2#s|#VvHj&!>8uozI+V<0DL1n>=&HuaEWhL!&a%Zm#JGZ^@nsKCmI0R zvVaY>bRUU*sL0+zbwN#Oh zupwjkeUU&-^{S>l6F$L=_5DxzD~8yd(NZ+yX)X(mihSy$!f)CzNTlhtU^8 z7zw497XwPgD0)e44%+}^KH~eKR~}y)JQwbI>FB-n?a6Kf!Ib~(_7P9NfBxrY4jk@~ ziHS`Shg%tE_@TJ#IDNWYvzig5Vavol*tEiS zk4G+t;NMRs8ZS8`%(3%hLq+Ri6tK%kEUN(I22I%7!eXX*MO>H^E=SOXsbj~|MyWY- zT8v)`7j9R^!9&;6v$-|%k_zD+@TTP6!?NMuY%jVB>(nR`0bZ63d7%rZ*@uiYAD?ETHzid(v#Eqta(V zhEc^U0@^4wVQF5%6aU%SbwTOI`^wt8sa_!X=sZHy>exq&@|Q~sy)!O)>s0eYG+9NJ z?FHYef^g)RGCSVQLMmDB_-f^dPbD#%4P+z~0);Vht;$h2IA$0*G)6X>q_V;}Q))d~|iT%XCD3^Zei--1r)LBnh zZPFeCeM#;Ufyl+)HXlkVy28LYX|j50dQ;|2W@>7{t8ty#MF}fa%tx~g3W?n;+(zKk zUg*0qHANTlS^iLd1=$4R^fzUcvjTPgP_k7_P0!qSjsbo51og)`!hy@t=j#lX#ts`= zCdlUkOL7~)h(Ivr-uCUW;hcMkb7GVNtTP7Nr%?caN42xd)x~kD#hQ&iWUN4BK$$=o zJ^GSOkcSnw#=PGbM^m|;Vi^W7OI=4v;69Hqc-xggJ}%~t%K;esLka~Qgk{s^_+<_q zab}v?rK<@${dYUk%Qg4n$yp`;0P3A9-+P~X(VI`?6uUR$6!4e+0KkY1Z?&9)&%t$X zTc4n&f2iH^UwbLy{E+WT`^npM*`8$9^{=1Ca^C3T--m|mPM($w-Ba&6bGkRX-TCBo zh^*?tN2{0r{}23cxCbi;&jQYr^l<(Nc-@?|CS=1_nl@L{=2x%ZAApkiyRVpq5A846 z#>DT}yOz8TndCR+)zHYD=4rfF4f-k+%)rV7Lwoo$a9@kfNq)#8&T*o{jP`Tor2;SO z^=qT8SPjfb(+0x`gHk}efKT-AGMVHeSbOgCikv%WZYoej5)WL_+7={27A-dpa%A|I zfkCMaoE4lk9LK9!ZbP~ymR}_6FD-W5>o>30Kx~{xs_{p!ec=A^{r@XFSwZM3f4Yi~ zBWG4gw3{@MUnL(R*AoHo&J5o9m9o(oZ}N~oPbC-xNbs}*IWRMJ>g60Oi&9El)PSiF z?+@OnW<1uQ2sM%+tLWCxg!7rph~yyOnmgIxWh$V^UQ{5%0y95gTgl26NV?h(K@MiZ zI=zP2FLFC6&MXv^q?=S%7?wCjI=}{^3Pq&eLr5>-2%V|YPkB6I`Ot6ZagRHQamN=wsfX*B0mXT`P^Y!bVL5s>kv! zI{geF;!h~1l!1f1OksFa2ztC|qv*mE>AbOJOSvKZMyU1& zLpM~L?&7?(lKJwlV;=tdn)7~o5gh3l>nDDL{dEOfxP+Q8NM7DgP1VjRuE(9Sh9a*+ zH?{^A^E`XLs=w!5IzRsdc>n5fq$JZ!pSDQv7Ne;XDu#@OF&|_^@XOZo+J09)U9Phg{6R=C}PuIaj6JCUvOrjA8R`aCC;R{OqwZSBsaW7hk#YxI~0) ziT<+J3Kqw8e}7}P)7Dd!Mv`{3tk*#}@9L{ffnaLe+fm%Th{p@p8=Ajx8Vo;^#F)u~ zH*I#fDStUjWWeebZoYbL=%=pGW z5I_u^l(3l;XIo#FGjG{v+3EZWlVjA^&N4^nLoh(;1bgrh%z!;EL9h^@=rOGUr}x{> zxfivCaSBOhL!?{KTs^y%MZU9(O^7cUjAy3?{$Mfl2B^*!I%u;M<#Rf# zq=+^*>n)!#Gx+$Ne@-m(BC@%vW;HM0c^LZB&o55_K>)f=4m$*sZp7i zDhgtALgo#5_17V?DymANHInT7V;aQD@Dzg* zpKnPAYnGD5={@NqR8@YRY@Anypx&F*6wQK~3BJT${jaV7vRc*Ebjw9rp zW*d-4@TwS)NUnqUzVfs$K#Ve#sqO06!k~voOrRM`Z@;Tb0y-`~OrfNy`>g!tP+^!m zh4lB;J=P=$&iFKCX`!>)n{Y_?{4iDI3DGPdU0F2^+meqZZmd&dgC>3$lXi0x8)cADw zGfxo^xmp|%t6{!pBEQEDuhB})tnc}(N!JL-xjA0md@TE5rE|&46p}9=q*;+0%{!ML z3xwgX^+x9lVs<6a`Z=Sv=&j0cg9S0)=v7)Po*S)dLQT5#ijZ1}1135SF309tj(7b~ z8OVc$V&^%<1C38v?;@YI#cXaYiTB?pQ@wIzCTp#d+j#Ao-|>$jw(@Wu{%g|$9h)q7 ziDrR1i{I_7iCUIA!hUDcq)C#&A|DGI0AIa>$l84v^70dPQ!+lqoUpJiKQ>Uj`pJ%n z*LDOi;a`f1?w2qp%aVbs~ZK&NyosJqkWtG@MBwJch^RA>Es z8;Z=j@CFaUB}e8_$iuW1-~>-gWKy2K8tszI&}r0MWGv0p8`1mA)S%8X{kG!hi#UKK zH}Y`F*0Bo>cfNDFlY94~(cw~i4SL5$>>6`S*p+@|lp-Q)o&QIuz|qZ+T2PYkgu&!d zN%=fKAdgl@D#{~;Ku1KJV5Q;=^6V3zn6lQ&tLtNZhe16Nm0S{ItG5m%^YqnlN>VJ3_Qni`*AXjC9d;UYGAHt<#Eg?Rrs( zeV=9S%0jgw00gJzI{Etf;7L9x2*LyaA66vlm}Q%?hlZIfo~3x+Hc6m+bg zXk>RMOe}f(OK%4wd(G@k;l2|bh+=Yb;SD(xaxZ#qnD?N~3wy^*ccC=Lypk(Tm@5?+ zN@2l$i>vJ|_?U0K8@qNEjY(n4RRU2+-!HHZt0H_}M_NcenQ88ExNZ`Qmj?GBXj3M9 zxkXja^h6^8e1&{2FUrA+KVCg)_J_$zXZQT=td#^MZUlR_+HfI|(MYUYu-~ z^pSFpyOmAG2@J3cOMFuwP;L4Rjn#T-N( z#&u!(IZ%=(N?X}*pfxNq!SfaWVwJWwWS~yTl;4#Xs)9u7I!hnwBS&oUl{_UkoH`Yv z+A8#H;plk~MHe7>)S7fM%Ebhna5EQgT4~g7G#PIMpjfnIv zra!QtaHbVWVK{u(Uv{W$cCXLsIQeWm>h>T~5nPp)(^d^DKC4KFmit5>_9&TinO;2@ zzmF@Q<$3tNe2ocUa&A@U;}_F1z76a#v**n#Dq*TmB?1M8| zk=(pbS?XglG*Z8JT3cC`)USrX>T%dn`Yzpy0J7i441)wyObo43I$@Xb2t2iq(C)Hqq|=5?`aa{T8Z(`09jS5Mjv^wMFYtkPrI$rsG9G|B7R0X4{0cKD ztrm=~b9d_3!^#^-r1y8Z17L}q3`(uLXrT<&4G1EJ>Ix@Ic$rgSMX+=@T4>Et_z3jzq zF(*RV&Qx^2@*RDN+~ftYUT;Z2@VdzK~Gj+(*Py+iQ^2FP>^9^Xd z(%l$9v8;T{V0xW{>_OuQ+BDQYX_4!?EDReHLYdFT+#S)oc|q%!7S1TCm=1Bdn>k5j@FpoS$}AyP3s(u@bd$}@!O7Q- zJl}#YdAZ+!9R2}N3NF5U!BW)I@yY($Av%*u_$txV?hhcpKEd!;X_XCju7U+6cbv-z zRD9(<7$&ld=!k{G8jj!F2&)yB23qz6-k0|$&5!u5&HMc2<*^yDnT$KtFMrx* z7VDKmLlb+fidfX{G;#EU{A(5_z{{Z0dFOd^BZ12WgboUs&2hEx%hyr?fPTtEb2G+! zF+YVj>#*jQijfYdAoF*u`rs=!rgbF5iMC~HRg3e1WP$Q|b(-+hfmhOqpbxTawTV>9 zw&`_ilim8WvF8Up+s0qi_&Ecz6N(X+ZPkEc`&l-MV`^6a*6k@1k)-R5@v@-smrv+0WWUs zNefNMe-FaMOj_7kq*MzK$ae*tOcyIm6w<`BC=Tm3FsEteg1o|d`~lN;IEMbeeWFSZ zUpg*vb|Frzngd8Utg-?Pn>UF8!bbgY;dp+2enoK7ex6&yS*%osg;T`67fRAfT<1RS z6`m?fP#*|6vcFo3Xset7-!w}6FGoQ2(;tfCVkd2w)%Ne=na>6L4&CX+y)CkE!dz}?H0h2Ya_Zw%}i zBrg>r$xP%%Zo6eAQLdQg;w3TYArCaU{RR*(<8u*)Av4RMxI>U5GfP8}59@1ydlHJy z_Or5T;O&u{_F(#yio&oUw+c_1!VUg6VvS``eRfj)nMu!V%c-7J>||HJ2vv>PS(ep} z!KVR03>?5>Jhm)Dq^!*S0ENdZ=u%#aUjbbq_|iX! z)Bli2kb2Kyf)0qZP$iQMA^2~-Qg5$SGcEtgk$mggPve*$$5Y9sdq$RAQKcBF=ASFn zR5vBeKu^O)i$Max#r`x{ewx4vUI+3a3Zy4?uk;?5w3(RzJh;-Sb(9CAAr)+5= zptMsl#{|7yC9ijG7uhs7#44?lf0WXkVR)?*E&l*!d(gU`%fX-Q;t~tY$tgJS8KB6J z;EPl4S7Rc8EoOTjOvb4zO}H=oHBa$qxx$<1Q7nC^M^rzyJt} zo7ziB-}asGjx4ti;abEhUC5$7f>9bp=0oUJAqCx9p05MSYj4{=mW#*x!_3=4SC&`~EMONpaj?uhC{ zhfydempGrCo0H6gDPyA%PQV2bzf?6OI~~6nwFs^A)k2`?^qlBNVOv;MH}(_Uu-|MsZOOk92Rcw=riIa!Q8DC=`dioyxwaNqHXZ*@uJv` zw3MUIZf}1lp5ax!FW>ke*SI6UAR;8omg{w+fQRZ2;2cHkz?=aDbIr<-Hs?wa=|7pA|2~3 z;D6`(ivC;(ra@nnOqaT#rbRPl-GOkc!9s!8r%l7?I7{EvyUGYzuqu*4EE0r`d(q_B zZ*6X+3I^|g3Z=br6^Z!{M4sS$h@oNEn! z82UGRHqCONSkO4r#3>&?y_1H>5(LY5xOd`S1DF)XGtcR@LQgGrY0;Ds{vHN@w;$it zUbZxCU^SCecoQ++H_cCy;)#N3`dTTA1%6r)%EP&^Bc zYtN16rxeUpdqX2NS>W)Vy)p~@^U9-2)PG`go`ki*L;bb%g{NpO>LF|dc!|C_j$6dN4uqUz9 zuj8H{Hi;Nz1%KJzb(<~0aB7>GX#rV`yf0{|4NNKqu4N9>YeX0^;cs^f~7X;BL< zOz(%3S;aSc=jzF`xLnC}566n$UZ!>Kmd!7@>1Y-3GUDaj9Q4&KeumOD`tBm6^L4}U(4KOKLyJ0!mlPRMSvPJr>a@|Lk28=2 z{_uH|2!{oJ!eREH6!i&?WO`w*L2S5U|LxS!oR7ZL#J-F``rV4@NQd`gWV;2{tLA>9 zh1IlE*lUc-1&;KN9<>?6ufv=cy&)!hV0F z#78QenjMGBKH@maf2**r)2E@+lbxIWfZ%{a+3A`OWYO`kXp=Lc=!>hWio!}^2qZ}Y zs=Yt#Bb!{&RJkLgoQS(!+2(9$_r})4^M{v+)E0#m`~9$O{mQ7e<-1eBecg(Dsocd0i7`2y_LO3V@gVkjtEXoch+=JU&AjXjZw8Y<8 z*u8fEsY(`M<-1xfG66vjy8POFGIRbJO=+*QUnI!HvhX$}-H94jfkTU;b_BA_izr>% zo%XhT{lrHshK*8_?23OYPxaD0zeW8%9ILnKaLY&-7ckMv%hmn|Q1YNS=~XvWxgq6T z_*Yn5nQL|q%~8aC4Y3Lr7h+OVmc&gZCLy$5n5asu5=qTw-ea>*7ySL*wx|Kt(_26p_x_5%Aonb;-olyx92NEXI3|f>djC=B%cG;9$ zz!?F@rg(S>tEn(Nj=R(@50LlGPQyQpwY^ohTM~PH3bL+!BNrCCKYVxTlVXnUHQvaz z-1#Au$9=dj{-=Wn-gR9okKbz3aAEzP$?YwuMY6#*b(ljCG7~MJtY95K8#A8gpmhvK z9U~}JZg!-;`foy=3($q+Cz>$eI8~tGBK@fGWrlBtzE({p8@6pg2V|godDrz8Y0H_w z3n{DWFx6FqIyz};|=rwktu7V8T zZs((M3GwuHn6jvAeNvK5&XQH&l9EW87%}^wN1~o)SoSa@0LautzdHHt^J{0~PuCXu ztlogbm?UapRfTOWp4fDsxcW{XvMyM3m7?e~^z`egIWw%G@~T%!f(4f!AEIs6Z>5hy z9;4*7ZkKdm$**F-&rfDA>u_tX9et~&>1Rh}zIOr*!TToc<2WX(haNBll<&PoL&MGw z(MG0ysrk>0#KlJy?&D_)R(CR5=J|MyZZhd4-X|ZZMVv<@K2eYk>(xc&r^FZA0zp+d zNJL*S_!i+yZZOf6Gz9dlGG$OxKce$1M!bRgVuI+!-N1n{VTc>KoztrTemF^9PQccU z{K7-Ynllj2raYy(w8V=2jPUN(bUiI7R0udBiI$~4hs0_^ufT~b~^Kk zD3Zf@M|TF!&UbbzH!~C{M)y!%4Su&Vf|CXVY&aX=?rEH=c%_rhBaa%@&w9fDb}1GOCIWJUDcb^^!>#q9jt(_(u5 zW+CU6wfvIyR8ehGIXyj8fp;6}pibE70Y7qvNk>z4r(nf^{tpIYBes0R1eURxm{FeRiO3ep^*js;X z{WtaA74UAMoqY?t$68AjLS}qg*V}r2W^AL)s>Aq2PTI0*JFNV!=F$n9>^-p?sJvuq zS~tXs=SYYo=o(xyuM(D|b~sf3VE~}m<4&MZ;wWAFkC4p6I+f?uXlUAxm>N!^<3_ii zPNIp(;etu8Cgb&x1b()%v!!g883#s}yrbzty`tKTEVRsr5qxDBo>$#**UQvZzX4;o z`damd?&l=pj!6RT{_O=XJLpUyaw+plpq1|E){6_R;lzBAO$4BfZA{F45v_IEM(Plj zH?z7b!t}oBN@WP-JniKSW-H52D|%dKqzX+=6FAKaF}5Yoi{ts-oAX6(s#tjf@%*Mv zREkuFz@sdueeifm3HB5CBx;*N<29MR5v%1+Am6X*7|B=ds){vF7{{vcl^lA-2Bbt* zS+O%Iy=3zbaRuEk=f8o{xypY?#STn7ZIfeY;ahxzB@xgfEPYk^zzOG+W#Fhlh~5E@ z=&6IM9YgQmK(%}CM8V~2QnvY@4Yk>eoLT#yf(;GueciWxVkrXLhsY`v<$Y9onaiqE zFc?Zd^>x(3)reV{kACDbhR^*rw1hh7L~O`Jsth0oc1bCWjQ^7dlu!x_qc?T(pr*cb z_hPYbVLt!MU@-i#4Qt5PQ!a-S`aRLbpB=Z;x16}AU$Yj6`+SIPd-!dad#!b_bkAfI zlvYaKFFLwGdQN=1w#>l_2Mw z@+7gTjsAogUONS|1zAGHf`wbmGFW}-i)W^TvGk+tv>0E*xP)PCUJ?2$M0TD+d$(Kg z3JzbQXZYWcOQ)}H{esC1^Q&G^PQKXxc{N0?%Ps{-qoN6sWT!U;2u^g0l)`snBdhFB z%Kvf;&n5MeJRH==9_nEH3&^%^=9wD%FEedc{vpX_Wa=Uv`;TbL5ZtVek_&QP2v*)} z0XFZrn!HTmg%Fxx5XAoHv?#G);D4Jo+zg?x>{&3%_8AH8`Au(umL$ z7K{ny7+z4x8)6$0om`KSV8QyMu|tFLnMr9f5{xF@e(==6lAm95i`c2WT~;j37%Z_8 zpx>O#RWaoAXaqp4$Hs^4z2-J9KdhdyQX=E`v%fLed}8+%K5f}ZaYk#mI28@QMxzhg z56SIAy*Mzy)$MmysWRI}@>xtO1}TT|)FF9{2|4MetP4vqq>xxf(pZK6IY0BLzixJw zy2I^|MUmwH5r-8yy`<`l|BDn%>?bSKLcBT)sAfqQlNEN{udo-z4JO^Drwpa^PfFQz zGNE5%#1$0H16AsbqqqRKPZSV=u#-m3fZ(LVk1$w}Ckv+q-^ zTuG`gG&0SySx&lMVh*|kf#{D9_iud+2t9y?$H$ckKI$KvN11p%v7fW6HOiE6a~u0~ zM&u{@Rr9^0-%A5%8-3K%*657f-rPiovWv_P(1^P}r8X!2IMtZ3;94riYES&J3NZzEl}qL>h5m)%M@e4^2@FA#}HG5q<<@-N^L z{5fgjTelTcHfFV^H`&2x*W<^w=+-iOy}O({N&im~n)Kb{O^FlQM&RC_I7 z&sLxgY-a$lM{RYZWg&ZAWx%y-|KijB>uT?0^e_JQmXZpMA!FsKIu+Hs0 zo-~QPmH>nd^CHlvZYniwm0=?sS+`C~t*dde5XS04pGRH7n#t`cXxy-*dir1`+h!=1 z^*!s2(0J{Otb>NNj+YN|Jv#2$YYd^OwW2pc?1YzLnjg6T0CM}u=1lqjp_||uF1Q$e z{kYNeBbNQ%D^~jgWzL)t+O3HsADBG$jU_5W@qQmGp+`>WR}x4 zuKeL0-@0n~VVr_jQ%`H6s)Yhi1TCL*N821GWJQrhzh#)R^!nFW1b`e3$=^f=E}$JV zP0Y+4ktlnPfe@0iX4{@a=Ofd`zvB3&qlC`d64R_ww{L;APT5ZpIl@{R^Vg0Hi+$j6 zV%)on0ub?GQuB)Oqf+@yjoj`t;@e3LD5n0H=O@h%>!nXX`rIKgoY0X$Tqe_M_9`f+on#Vjgx3wzVwr5{(wKoD%rjqEMghZBoMn(_FHK^ zO^C67z83&62ZgGJu?5Vz%WqdzE|u=?TN0esX~Mi@bgT+K)@DjtChG1OAn(CQa^cQh4&Ckm$Q_ZwR9zpOdO+Ta zLvOe3R`9QMaFn~p_T*7Tt1Y>xv%@;kgprXsmDMswXk0NVf7L&rr4c*R|bUVwjY7aYEF5lbb*3s$2C1Ia9C9xUBZ2CoDA?A^&!Vyd%(9DQmv#Vy>Qz=b>GU=;-A@`E-E+eNPKVb(CUXfX9!s86WfXKj@Utd!{)FZp$ARkz0j==|qd?0Tow~44c}R_WPryH-J=n90QN)E5kkdn|EbbDAeq%}j4-2BQ2#f9U4j^)vW z{I`g2W~63x{{+*^jz3HmXU`252CXy37uErxlWuOberb+kZ+u_38%7j+9*(C;s0tN8 zg6vo8NTr7rKfILMo)|3{V_eoFeQ+$d8}O9ab8i;$w@RmaXkNBZirLpmrc$M8(y79p z<{3d!rKF%0-3j94G$)^A|0z&1C`U~*pq+6;)?ek30pr_215L3lUiv!?K*gQIP zz)3*Wo|$_T0?Wws3Vg?L-wjLMD+e|g`w_MUyHwppZmJ)7;%6jnMLcPt4W*)!i8#EZ zQ=8RTErvC+n2omjDHa#yjh-OOF~9wJoSmvW7on>k3otLR%F1w{a7Mkjl!uy6!UQB; z6_!p@PLpmiW+97dV3n%MKE(1ZIA~pG_LOh0@avBXmYdTeRtwi(WuM zcQ>3|l_&L%eoJQ`;3qkTlll-nA1R*73vxP2{(67PP#>KqJN+OtcvSNHqE=?I-ln3Q zZCv^S4T35lbkmcpur+f>(N)jsME$bL4h@~*l9SxkunOO{isF16fBNEMe{G(}*WSO3 zdx`f=dR{etIS0EM@wx=>yIPbmu&j9hDGwU}&{k1*q6HpWllZsDtX<1dh_ZnN+Zg8B zt{G<8FWyFbb|W!9Bs3^4-xL-lU+p#vK8tR-u8a9Nikv5OsRnZ>4R1t%Er?h&N@XWn zhB9?dkcdPEp_BX}Q3A-f%EOxgFcd1-26R=qBE*t3)itcett;!G{32$12`G((_@xle zj?4J@KF{>VO`2?W_Rm^QlCmf0&1?f*XVth9GwXG;-q&P@*M@+_AY#&H@ z+hA%J#+6Ik!a2`V<1-v;JP4uvAY9#sAE$7tnJj>CSd|l1CU(Hc1Z*+Sq7saD4V-2b zxohx@)E41Zj8aS2j!%J;Gc=K%JK{@T{t!JSe{#jY9OGZx+VwATcatXOAge&xhImVYDTHkkIBDP0?t66%JA@F+ zrmkyH(-h)u__r`LC|9IxlE!<~?`D_`n+!2r3?>a_w z-26>1#68u=pvHt|S*L~2)^S>s)@>D2{&k4&q4hLu*KIA1o2z$DBIbT%zQbF9lB2?Fyt<$5Xn-yF&QGimVZ z^FM>w)qll@aSk?XiZ>L*m?c%SFjQ^DoBarOpQ`+R42U^eFHDOA!R&{GkUFVV@GgXY zt>buMxBKpOW3ZM;6`W`bjf3a(GUUePlbFpga?&OF@6F2km!r0)Anl4{U&wu{SMk}d zbdlhf_E>1TiF=MS0## zE$~PftIcXwBg=f_7T?TUV?v1_s!#{1c#RRVO7v$C&HzDf`GhXs?jsuHn49SL zmP3~w2rv7z?Ik}dnl&&{e}#n_LJryOIsTz=B7q}f9EDgN?C#I5qZ^@MMe}jWJEp^I*MtY zXZIVCYu@(K@2e9dTc?svDmgi={WpHne$a90UR%bbS~-8|<;zM9(IIW*n%gXx$`Kxd zpYUv729udkFYO9KfOh@Emr^>Dp+&ix3q&Z3B0~inPvo8Hl1oKSgQ^0cOe zA#i$jh?!#575IK;Af@gV2nw8^pPi!)3L~I`B=yoGqJN;W?*_Xx?@6o7>aYatc4l+N zzz<|3a<2xxXO?=bFK;TX`~CUAe@bEXl)zB7AVPZ{2r9Vr`-}U3kV0L9hum^=zv%@t z17M3C@0{S0A_b6=9x_ObTZ9{BeA1!n$OB;G<>ibL;>JXW(M_y8S47yYbd0d@u2*Ix ztr3O=GMyiXOq^rnPHLFdHcyzCY>(8R8q32>EAHLs}yrQ1lu%wjgXQ2sQF z*rTh(cy7l&C!IUfZRJO~4~I!wrslFn`wvsRC9jlScJmf73XxRxHb4L&p6&B*yZMuO zP+yY(F=bz(#&VdvLpM`E0#XMYs0Cv#hl003279^E~PAV3l^fl(4W(P z00$qC_z!Y4QSZhze+qQ#IDHa(J5V3W_fN*=!tm)MQLTr8`?CvJbz8`5udJ$K_Q-sE z;3L+&HvY0AZ@L7nFz~K2k|C$JTmWwu7WMlMDn525hVSQ03%s&EM#D{spZZnDmD77us#*zTt0%1e}<$)|dy)o~I1N&vVW2_| zHkd(PM1CpCGnE1>kT=!{-CI+@Lvp~sQRm04wXlK>YCYDhc=58 zPwb`FrT}nd*LWFkGY!S6$Z%8noRqs=={%}SI=6C1CF+KG$XfQQnTJEh*JaB2*#e$J zWu!0|iQ;RTG~*=cshA@+bW&%xeJ-7dTeSNW?*SP_sr6#c=Xi);GJP9P7J`0j%z2p0 zBPCVvRX#>?0HjgSR7T?~FF%k2$gqK{N=Y z*9!-1PEod>Ew}M}!jP54J-XN=qhJM_F#*P9wcy%8RBO2AN0k^T60JGDukN*Jf?TdI zb&VNO&PD1nzCN36K&cq3=N=ufW=b!#mG*0TtUi`)+$Ob^xF6VC5tqcV){fkY9#AAH zc{+9VajBZtWI^9zpwR;3;~k~b`44_Rs*~w1$xz}PM5UDD1{q&4CsIk$DR$~Us8!HU9-=;kU{LN1b4m)xtNwKALj z|4H+eYfXPc=$|B~KX|M3!89=~zu9(u+Hv86m^#VDIC9?V0@&K}1oV?@q8rGyS>=Gg zDelZGe9fOUpmc5iC2FRVW-Ovze_P=+R9UKt;4 z^KQK|?s2m;CCI&@wpEX`D|&nLUs%M4gWl7J6@7l6&FF)_MH;&#p!o)zsix<3(mLRn z%*@^C`C-L=Fy{T*1BKfk1v;PqH(P9KBXgpE$jQsU9C2Nqf@dYNBt_?AAHR(+|H4jt zT;pi&X2(U;D}q%Z#N3gMrJ(YDqb~ktEm}5K!jgkJ*ENNFM0wg4loBOo4OIci zB_`jP^Io9Oo@hOmDnT1^>ce-*F}`XI^c3>%wSs5L0g&8vCB3SNuy@}QMv9kZWA>z^ zs$qc(%3PB=*?o0RI}8!|stQNzwi-UrY!pdzGS@TyeNL}Y0U}alWoxVhn+c1{8Eqls z7=^Pluvj}}in%kQk%Qm{e=-G8GY@JEi??gP*;9wsT)!LHR~^(Zs{LDhYCoRo&xnz>F0{g zOy^Rg8@0%iJ&opeiI5_6HvpizSfP~kuAi^r-|aI(2GDA}O8K(&SgP_PW_ykl4BS>< znV@A%x!*>7fQk9e5AO?^0Tsxnn&68of!GW^#6P-pkpzq`%s4NG(K^3 zaJNsTu_uA5J>`2^cw!WD1b@Cy95_yt1%zHAEMEcw$sl?kV!%c2clONgUeUvXf1Q}! z&ZdtfJnOpC?NY&8z0|J~=+3y;vjYHz(Su8x&f&Y@n|Y~(3+EfcSz5{0Vs>NsjGyf3vc3S@bR41f9nROXTLa@ znvXk`;jNj2cn_$FTh&oR!f6Ujut+9w;Si&=&JG08OXm8sBbEKqVe9ob3lzx8fa^HR z{RNKUe$0pGWw5+$vQuNm`vPU31RK``=R}#^@+|?U7+e~ zj?V>?!kZWXbzF8FfCy^B?lj8EF?y5)*YwJIyo{5H?!5EHj(Swgx&bdMrcU<<;1hNT zT>br@Px_OloAfE@iREd%sS<~XPh$TlsMU70Zni5W#QxqMAmvE(uBxSZu})NZ8-dAdZ+m>d7r23 zE3sYf@|(Vfr!O`RLE}i_himgmoPw@QgTO*E>NhPL|G{eiB@+As zcOnb9?a?u@c+-q1tc@XdjE}?PhU8<^bCpmfSTk6C+C?xF_e1 zUB716QRkiFZ{H_vJ9n>mUk|v)qW*p&W*R{NHxDU;i?@qJWfuv_R;fln@{w3W!n_klqBO7b$|$6ja25<;JyiomuX^&faI{ z-Z^v6y?;6Wk#C0Y`<3^7-lsI&xOvUO&tR(1?%n5tAi0{gm$xSW_y2fMEi7ilPF%xM zmu!awIie-QFm%)J{^Wzs2ZvM)_2c$n6I(_vslBPA=%=>sf<`Ygs8IS3OX$un@@P)h zIBBJ3DMniU`{D2XQ)cf9pLd$8xmk?2l%`X5pRGD>?e@t|KriSO2kXZMW0mg!|5Vdh z`*2`s{|8#t_^)ZXe*Jk_*K`IAtm+65z?R>8HS^(99UQ7hnI4C7i|qe;mVL%*h2|X2 zeTpBut-{d$exh6Vg!?OXl ztv%*OW9;q<=dlxhET99O$f^t(TgR^}cRnAg`sRfyDC(}Qzke) zxE3pEOhAW&g5@k?UXK*Eb56*W-1d?Rm1$Xso6$bE!&`-yqZGd>!;lVO)B~17D0Ha& zf1+&*T%y4)a#dbSB?bE!yP^B?EIx>H58c4owYt%F*q7C;#XJ?x2gZLAoE@aA550<1 z$8gGZwW-kfs75knQ3OC8A$*Mn!%vx#k88AEdG1%@L3kz(|!e2&Ns ziruzU-aoLTOLuI=JPF@~;0$y+{JlU_({*G$pBZfUhrE!UjIgr_k)bO4e**AE23~tx z?l-jGU^!7jZ^f5mJ3MVD@Zj^kv*P>`3aN$a`blvtL-=l?bnLFzadGFH+v^RdE`E$D z*A-)xd-qsKmBR#`a4%Fk1OvjpZqTrwVQKq`!Ta1a98mTa69P{eDzWcnf-7s22*4== zS3DIvCQCGyjCqppC$b)jW_lbWm{J1#EQd0WXr z^le=b?he*<+HaMHIb%~MFN)a!jV!eQhLCI2&-woe#Gij8xgnf1F=vMQ(96P=Yy9Pn`O!$-I|!%U;Ny*! zfekyb-f6mLfyjD^LzNNf{cE{8biw|?KgsN;QX6d?Prl2Kse-JZKh2;& zsgDMy(P+%wa}CY`#L9X0whn39UBr3|MGN&h$-TJycDMjcdq}V}zngL-qCBI^%KEG# zCANx8L`rsZntNcc_e^9@NEP##pwCtiT*q?zpXLni>mO7}AFZEt4<)fvzpp-w_dZEx_a`lEQK?phrso&S6-IGVw(^qvi1- zxr?qv3IPSgZJ>UDalHZCH*X8eE1EsV`J`JH&<2$!!}bb@TNK{`aIg5jA;$E|74d0g z1$QW;ZPd0xE?zU%EbQY-!For;3KHaJ|8FWSK9kXqV{tYRP^k*fZfnjVf?kIQtBLwTsnj|bF_Yp+WM-^?KeTr4sT*?=dArssd6iZ%Ti`>; z_*TR}k|TaC%sGWu7Z%kFbFsm?#`u{v{kA7|O0H4ZDB43=HKOcH6k8AZ+p(mKcNL3; zbYkqM*hcMv^mK|CagKh;;47j@x+W-4m;~pofHM`|f2r5&B4EKTqOVQ2l`;!AMHuhR zHJ=>uddPjTj@JYy>*TT9R9HEE>Rq@cd|r(EqmI!vX_{vazK1Ua^#lY!5TM;NtG5kV zS2A@d#$?$Y2Qp&R?JMG2007~;%VCFKu>^C}#*~pw&5|DB(+>4uCM{7d}OS}d?dY)Ge^;z#j{}>bOh3v zDE(6Xf9@lSj7xN3RyEc@^On48l1po|fr$XL^UG-CCQc7QNO8%2${Moig`3Y46Z6>1r&!K|4+3+gH1egc9QT;QFHyylC(1W?Wa)n z%R(V=B8xzU8g9qOr>;s)p(%b;#aRasr7r%B+uo8{Y6;{mHo^3OS(51>I;{0y2Q8YT z(fieA1(}P})vFgSbE*+Tz33GN%?}xiO$P<#oHdb*bPOop0IKAxfQfm|bg#l9`8q1kmeMc&CYuLvJAP<6YB<#N_jIY&(+W1I^N ztCQD)rWX@r#6Z^c8 ztQYT#yPDWe+zn%Qm>2B$3ld)5gw7q%e&Cr>ya~O^rkK3gPttg0>6Q}7uNZ163h|f| zr(z=%+cAQ+>W0ybw}sU;d2|t?DH8PFF-I-UKS}Ag4EEkU_)@l``KMIQ*KSG{-K&~c zLDDE+l>&?KaLCzc8TU8o>b>tL8F8e&oA-*f6KXGW3&dy!BAb{#y244koMr|9KiR0wFF+*~x6G9M3A)kyby1hja`qN9H&^r=|n zyt@33PuR{1W@&c>Yc*5RWM@WLa3DrE-O^~i{gC53K;f1DKM~c{rJ8SBb&VcMnP@f$ zIfjY|zzs@s>AiZ?lP0ArC7Cs1H5*~Hw8F}TF*#*AbEN2y=Qn-tiiD!#c27y1QM{=O;d4YQ1I^Sg5? zxk<}36a@x&xdL(^18&;jIy|B{>Dgq$WHU$MNWU%o>C^GorS2<)9Bl552o!i^Ne$Uw z8$%G1KuH)&D1^q!{wEx>=%k>8;t*9CeykfXCYo$`6r~fU z=G>xhMT)D0ZmH+?u1@9qW-4@+UGWXB_ZV+)i&*%NRdM13!jGvJj;9d$mZkq4uv7P0 zN8@;A3-ylP-RVLEm~!fdTItOrv;HKlyE_0NpmpKCpfNolY5+t@^&K!GHwlClgAtQ) ziqsL0t8h$@X9Z&QtN*F;=rdBAGvaU}NL-}tEx{_#sw?^nXSE)rE5;TV+}7>x7ybgO zGD+hIPf~gBYXVo#Cwf)JN;z)uA2O*0t|NjfRy_)KH*zTc|( zU5{yL^{YD%!UT=Jm>tWKo4op`BJ3xHpkcH9o7#5(KJvmZ0}re4=M<}y)`f-M4`jq$ ziX=Z>e{n&{8(eQh!(k~w4+@z?Y>*B0$;K{}{63?G)$THL2KWTnk#X-Rw#Yk2a^lhBQ|By>{$<}G zuiqamqLV#)Jbp*2xLcE&SsxhDWkIv*rLDYTud4iEeBf?PDH~g#fLe?z12>%GjV9bO zfuGVl?rfidBY=j=jkGaXrS7*RCfBl)%c#dzg6-2x3HT$QCj?+qdb9kzm=p}9XzVs6 z3Ypch2wzI7vBIk%Sf&Cq+oDT5u3dg&a`_Mri^Z>`Hd+1UVJN?KN zC*{fg%?aLnah+FtM``XlikHEIbU}|}<-YM@b-F|*+F{J#(-Up#dS4URzXLe+H%uEv z{uK28YKA|{UrTwkA@ZvobD4Uk(Gjjw1i!2V4!4-)ENE-o(1@;OfP{`Za9}B7nYxf1 zSz)~bcWd($S!?>BRi7s^@V=^uDMfm7-9Z(B{)P(-F^RY4AdBj}Wr0}lK}RY;{M6sY zgMVF3n%5^JS2il@*Mgi}z&!YJOIDF1Kvd5#6&`SVlvfHw{(?p;r^eiwg5LJ6B_5d*DW83Y(=6}xT9}YhX&SMOfW64!tM)f8oOrJ4yTzGr z=YG2#9Bi6A^}pdKPdvSzoCsDbCayo79O!J%3hlI)Dh@SAiS`>YH~=(&RNVU%E&H?0 zjSMvk)bZ#Klc=8JMOJ4`8ND+;600+SBRf6e1(%Ldan#h*CnFy5mRudGSk=hm!a>i3 zaiynd`7PCDl1?^fyW-6|Pe688>SS zC+4LU0|)^|^f?iEumy{MdP%R-RdHkLba_?9RoSDk7V0-CuJ$r|`CrEvTw^-}{4$OX z?Ca`Nv)e`Xl4n)Ec3JG4ZgpCI@T`#TEc#l+UX(t20w8+@x;Ovq_sj6r9iuHYZ~1Ct z6Pno9B&sn!35j3XYW|kC9G1Ssk|KX1#!WlQPJx0$tspBpapmEKt`P=H9t;v*m5s$j`wie+@{*7qcfO;K$` zfC9bmncs}u{=zb=Obp<#>0zz=UQLz?kJ#uK=tE#%y+@x3DaSn<>)BbZ`>cgd%njFf zZon7@<5(k}jJnQwjZH2+3! znH#Q41OqL*VWd1Wpi3z6=rMsxu248LYLie!8w8BoKXo4=D zvWeq$jA1MN$Oh4^3(A1*f?mBbI=A4G*mx}WPHgc;W;!Iv3qZSy4Z@)zd6D)Ewv9+s zsNr;AsD;~&@I*}7mFxAqfKo~gM9ODsTJjc;wd68pB#`uI$6Oda|L**+T*YynFapcY1Q?tms8Lz1Hq z$T?SzZ5H*vQgLoydv#vH4F;B6JT8Ifke_Nwak$05=eTz&rUK-o9ADn{NNYwM5Gq8Z znNRz>A=WQq9eGSHu0Q*c#$iZpC zsqHZ#tV~8Zph`093}VJfkFOIBaC=^EeKG=C88C5`aa*AjXQUgtgSC@nQ?GHgbTl|? z?|I@bR?o!pNc74`sgJA5nnJEdu$!Mk$_u7gKlWCa7rS7Y!3l0+pioC4*&2~gzZbd@*&O3%1pD9yZE!<1UEFYfzg%J`>?}D@jZc*WSxk0=*=M6*QYymP zw_GYP-1c02k_;mtG#t|xg-w710bGQ7lRR`3upHbI5Um4`;$1lNPxr!N9%05W6C1CJ zIxZcX3P51%u0zYu8aPrGgzt~Q7sz2mJro>KYb$-*+` zZk8=7myPVmp2pr6IajWUF5Dh@JIU`q;fv|HnK|P1iRKf_6hyU=+CqJ z*L*-L|7>A+!Ds1c=bLuuW9e+bCGk$n$?$`CNN$qkw#nmY1D%hC9Z}y_-Y5SYwfyYB zwu_ayH3Ux+7s+gjnS*V^v2UWmIS%L?cGIr;-130gE)byJ4Vr)nW%Q8l%FOcCL`;qw zn{^_T*vUTM0lCsi<=r$2&Z7759kdWKv6I}E-uhkJ_N&2aesD`kmKkYbku_PC8D$5e z#N+$3zkYCKW~61|=aR!wQ0`voAEp^ew0YK+UCnaTy7Kj+Ba*ca&bUZ1P4-Nz{ilJ# zUvF7+D9enE6cm7sFl3ar}(GxTfg!Lo5b%RgQioNq|J0TU6q$r>}7~ml4xtO^* z1A66=7@Njv^P-O8?EK;LcKcX9v+DEcCWrhzAT*FDB^Jb+a6I}iEb`aqBImjNx96At zKb&7`&OI%A!tDWu$@6NEA_Gfg2ka>#u8mBr1JRI=GIg5zr_ylD<(Fr4vU(7!GN-=E z9lX3OdByF~IlVT!ErJvKhYWHg3B>2hyP7F$!|h(T!s>H98B26~1;VG)Yk2^VLa_9& zqZ~k#qGA6+0{a)YfP7^#-Srw<$r+)iCP~-qJ3qQejc~iKf@~S~rSs0th=Wmb%_?uA zviI34gIdbXp1!DLhsUJhyJEYo)5@5p!-uu%)M9FJc)#MP7_VF%9W@p6h^ONfaQw)6 zlY}ve=0o;-0hK6{a~A;Cy4ewHS28DEFONA4<39W1x|OGPD0)KH4C(b~$y8m6CYx;5 zqN)U`K|Ts8w(#TqwUU39WxL^8w`8gr=D{U_1&ZlA!xlq~OnIcb2I5F? z(D4N5%-Jp*=<^>Oq(tN1ekIL6I7pDB0SM|34idRce?|!q_1KQv-jvEy!HSu^xMJg| zXZMA(9V(t7$tI@h+J9Us5RR5+70BM#XL}|8PemucyUb%*UoPByesfn+)QU~Oi__r+ zcz=rCrI6Im`)vRlwhE>!7MzHIdT{mSyiZYG5|5DrvAbdvCKu?wZkX-3JD&dd*!%M@ zSK{C2I`jF#H=ELE&U~J8Da^waS1}7 zp4pyaBE+S#rSc=6!ic0BwbOZ#yR1iGe$hoiKTf&xx!AA4;5U0~;c_VgE?<68h8pvrDqSK4myd$DXaa=%?L1rv%?S<%! z$&eKU!b=W2j75;+h&KcVgLvVh1Uk=0f4Y1B1TFh2GDj6nynf&X2bg#S5Y%zj?0G%W+cS5sYQRdB!80aUG=|Ty_#l@hrHU zOXtSNxt+2nbzj^nHKQ!Vzg-EnKwd(c$lS6iHW-qzhdh0Up`uG;hM}2EhB9}Wt zfA(3i^OEf9_WWbzFt0u7)b(Q$+3y+qrobqLJOj}sdI-Vvy0ZcnSm6^)pXa_U!Z673 z$rF|ND24xeQhaP)N9?5EM*@4m7;B{W6?X{NAgyI{Y=^xgG*M~S@T+v?x%RE56RZKs z?x@`dx63Xdc(4F^uQ^S&CVAR~F0fnL-MIT_-39S(r>b#b+o@U7{;QM78Si5qLRqi^ zQY~9=btJr(5>I0qB?H?Cx&yh@DtBGe-8#NUE$S&rrQk-gSoo&uykD!~E#*1eH}y+1 z{{IjC>y<5cK^6jck1Ac!(<`l99T9hHav{rchfy<4C?V(1jah1-u?)IJ#T)gf|Eo1? zO~zNQbY``vS%#J?T{YLWO}gITY`sp;@S)h~dg72y+arw`0`Fu{qQ2+P2mZoDekm4m zz5_xmcRf-x;U~^&OY^o$zTd6CNZ59G?x7rh8ww(OPen*6@FpoWEr@9Z%hE}=CRbKp zfK}w!tSIy-v85$;Q-lNcR7pHh^ou8h{r;IN@=GSQy)laHlWRClR>7o*uOlIXuf)2*8eKESqRd@gcUyZJa2}=%EE}#-vRa+NFA8&dXPkW z42gfQP#FMNE;?I_Q3I($3sf#|Um7GTbR#&IpNnV8KCVqmhabK+jqTxNqvkeZV-Lwd zzoo0XQ4JZ4amcB1vb)|?)vr3~vq2Sqqo1T_VdvP!ER>i8wgBK^vV1xr4boF&^~c|=|0@J6Xfebx<}j}h)Iii8W>DJ`bRwB*WA z%(PU-`;!e(DfxB61VlC`L;R6(j5|k|n_FEab5zv>=bq#3<3ou&-VeZ~en^ zF$pS(ncF!bdfozA=)`ElH&wtCG)F2` zjNV-UpUX1Y3g*9Co2mH;SNaV7^FIhoj3lPg64(y{6S>PFo1oC!oGOiI_`!yn zvdAj}rn8aRP~B*p0eI;L8;Z-~S+sGLow}T?q3HbSe_!gkDHB|xhkZGzNV&sF_u`%@ zq)pq=69Oqj3Fi|L1(*XY7Se=A`Q6RFRa!+)yc;qi&yg%6ZlTbGeV`Mi$1|rGOZkr1 zpB!KtOrHx2GxR@e+`RJ6e5-5%*o2?O>?J?>5FE`Mp{b^+u8J)XIV{-Kq*VBgw zQwTW8<)P@Hmamj;h<*5ria0s-)S%i+&L2kV0MsvJOy9!@I7CS?R!$-`fCApoXcl^_ z6T_~AvdCq8JK9 z2}wYEROJzBz)_oN=)M&lMFxQsVoVpv4#{IayN9M|<-fcN-h76%rV=^*hGojwIl^+_3*dfub{lEx;V;iznMDlq?){ zqaA`7VxEIKX2-p>{6lLn3VCE-TioAAZNDo^(*7JEMND!#)~_D(yf9<&Dd7y^^nx=F zGUk3wsdG-evZC%|r?!*t6%f5skV<~m+vVl_BTqwR(46V$-B<%DSv#U7@?1_7|8t_F zKrEoMiG1*}l>ObB^Di`Rl21%Y&8_fFlZPrv|+Sb2OeEphL{xr|}in&x*@Bz_|EYHQv zSzG!RFe)@Bm^=C^JT=@Wo~OWms=Ib{(ZCw~%n8AX42+G_kug#vgNMnaO9`f+;_H~e zRnUmyhaTrNg8V6od&}M`bPbrcaQn2Ta>uw~4p-PdN+gJRn22T^7I&utEB{7)EiDv+ES12c^s# zPa%}m^}_g2N^*0is@ef#yl1}tnai9jF;*wsw*Cq@u8qG`>#6^6j90fs$gYcq2QTlW z(`u<$KSS|cb;w`_Jvc9fT_qC&EZL7eAW60q4nEiirN_92MN$?&?Qdy_jINL-z){BnCn zke&!K+D{M!a65Z)0F#|gc;S8%=FAKT+IOXMg; zv9qJxxUJ37S9fOUmEA|ETEbq959WK4b3FS{`nfvf#0E-91uOvIpf7@h*wop7qJsW)y_bz7FSodD5C2O#i1ArI$$Rc+ z+&NY*Fm+4f9>o)~*%j|tlW4SX(uFX9rMtd3faiu-r3=&rt=V8OI1%LD>+wu<7ba^D~3lxUL^q*L*6EL8P# zJ1hFw@M8+r(K?bc!&GNu+Fs{g1yn1rf+j7^n}852x17QFqaIf<(*G!QLRrBsEJw%x zHb2>o__Nb{CHIr2C|vudm#T~UeN*$M(d#v0i8v}7i+2n(3mYwfSf<-^F$Izf3}Xwn zP69KzlN~G%wNW32yI7jLhlpLiLBdI2#Jz=ja42RWO6#>0(m%iGaLKD1xp)5Gw=jKr z)}@RnYsB~qqQ8>FIJ+4_lB057s42-kGn^7d<;3O}mGG!WKgqLlK4!2b9LFBVapyQ> zUDujVUB^!HlK372^J-_y3uwCwd-5cuHJb2o4?=v^X=8v!UY?R(O~$FW zF4&!?_7i)A zEFEZ)!5EUdS~VnBaK3R|%Udcukrj0>#dEx5XYg$ysOV|o{TxN@!?B|D*x;fyr2#hi zW@WD;00rEfyChb39np4qw#xV=?FCT*aokuy3_(!J#MKgtG123dQ$zqj+9lgA(pv8B z=$n+lZLb!WOWe9$lHe)`YEcnJJANs0qDJ)sB`(=L5GGpuIGp10w;sT= zO@X)tJ^SL8>5{!#UjfupW%ev;>LI;HiTUyN>aupFkrI|PeZ|Rv=o_IDt;Kt6H7X;_}>Umf%pprfh6W7sFHIb}cVI%#>()Ze^xGs*ZDhTsz)5 zZkca8t{u&wo9{)@)5UhrfZ(1@sYR7nf*p8|kx;!1}{b4&A#|1P~Wcy(|O2-oa7F!sDEv%Bez29i2Dmm?b zvvMuH;Cha;k8!id7Pw|9WMl?hCCDuDhh3^$gb+SXGX`tOWwB3n|qyC?yn zXQHSpAHa_$t#zgUOv7Qn=N3GX_0%RFC3DQ$O@(94bya3>T%GU({(U ziXuc)NbVJA2RIr}z=`PV#1|UUrL);OM8p~_EN=>x%jI_3@ab;9%N65dGDVSN-Y2rliC{OSG9YINukIQB5RR z2=lEo+W{8~jc>SK0$!9+is{D{}&_&sPM{miLP=1X9xJ z^pbu4IB?hPH*gqXraVlkG+#Zen+9U4+HSDMC1}7j#i^#4Ol>eoqyW5^ILWEFOp;hA zCZE{boRtLI>b_;5j(nfsLC<19To;&ThCO7cx*m!S+Ne;b;O=T0wZ?c!nyB2Rdq^7H zNtEnwZCO{rH-+mN^LChBmoUeSz4@_E^cAw>@^t^$C$uIZ+&@yI4w)&rvuj&qPMWpE zE8c7#_#AyXQ@AOE!z|ce;QOmV=mz4yf?Nn1x?EFEp0ZoDTy;kDTsE}^;>Q+&Der7{YX}zk3YyVZZWiOnR z7Yz-zN|Kk&tUqH`qME>&u&)v8M_H?}bdRESIUIg2+0B+Hp!-jwB87^#^--kbRXQB| zv0H~E>CB+q4kYS%b#%43A}_!)pwu%8>)`ls*o!s8vL3T=!rthA@3njCKRZNN9i++2B)n3 z5uRvFu0(#8|8~-0e%X8AOGJC)!vOaOU~ctJ{ZdxB99VY+qeEoo03a!Tj56!gqZC5BXV*MQhON_#iL*0-a(v z)hvZVY8o<4P5J6pPOaSk+$?z>-O$e^7F619CD2o)59~BECb>AXNt;kSp(O{pcg-ev z7niRjt|@gi^5h@5_6TX^GTu%E>2SXur*c}A5tfwl`gAu}Zo0UnpOylI6(m9;;yDA9 zG88gV=KbH~93?afIoUCmh|Wvq78Fq-QS z_HT}h4{DJtl&?xuTbOS9R3(>1-5_~F`$^da1zyLTr5tC>$V^FktcyWNvg;^sl#ZVe zEd^u|Bzo8MjQbJ`yCrBbLB=k>7}9@NDVnhBN*)~EZ_}affxh=*Fug50$YZ_!(nzbZ zJ(63R4_0Y2V)*9m0O!19Y(mqidzLK>%vzZoq>cdsh`z(B7!5nlHHi@RgU= z1hBgF)D*0Hpdf_E_Wpot=)|2$amQ(a_`BI4BYp0jx+Td2lvgwP9RSmns_j6a+&TQ7 zaZ(SWE8Do|+<_Ud+iVxUhrM0>*eJ%$l~cif-z|-8f|8C!%?*wWY2y+x>*Hb4fQQzb z38ec0IJd{4I)gV~eU5K?DfDJotm4Aod6mCA82%*a-E8GKN&oc^X^PS~M>6Xl(iFye zG7#hULz=?q%&QU|MJ>yRsjdE@bMdMg9*yIig28IuU)XWYiQ-dR`)WDYgb6-@3Ew27I-Yk>egz-5 z6S}Mxn%CIKl#SXJdC6?eXFZ_u_)dgulZxNX`#djpo>7!>UU={7`YqrIm>M~H{35vU zk|pGsq@ncb>tNTECaAV{*LT3K_n8UcF4u9&ZiV_-;=H=^IN2oJq``H-eX}0Bzr{}gp?*LYmp>ajnOOqS;d^5Q? zq#J=1SUs)_lEhEAG3{q55%%g3l^UwsOqHLwQ_r1tUU?;4AE7T!*19`EpIRzMo9D+W z(DM=jY(iAq;eO}M_qVNgqRti%4Mt%cME9I&Ezoq(ytXc6^H89fJ&klssTINf&A~z1 zeySULb9UwsV+xvTWh#?@uuld6E4N`&WL1f(W{cyjT>`K@a=c?^6>|HewZ&_*lyIB8 z{9^?ScXOj6O=BQB$a2l73IG-PT-F;E88fxC)>gRvLOSFq8h4{tn|R_1F3XPcYZvHM zi&GO#Lj~VO3#4_0V1BQdTDRSPy;r6=XVJZQ$a<`PNc;ZD$E9yr-WT1h>n|Kmb?=49^kUsuKt`oZ?kb8aNkY+RmV@lizP*pUWRW|S{o$O; zQ?`0}EP9QSVKnU3q$oX|0wA}GhC(3LXKZJqRhm_GKFa*_3+e(jM{?|Kmg|yHMmIF# zJ3xh9#EVv7ckWrxOV43I`{>q}dY90xMIOeWw+~h|@co{Ma)!Lv-xB}&g?N>i>|CS@d~r3iv3d9S)J3GvX!J?nVI)Z(6-Njq0nV0;HL z@rC!JRhpv&<=`-_Kyn_=z)atTBFQnN#ha5&9jW%@x8A(f@sCO)yCdLRc>@0#MG6+X zt8NnnGxZO6YEHAydi;ro9qJC2*KN@8Kta*)l)`?@-Q}LV&^y$#?rt&$TxrW#O4qB| zS!;##SRTezl4c(x)182{std;>lhz?2%ppMR; zWdtT5uRmxzx6l2HGx*|&1;ml{o06aIi6l3sJ0Xj0@)6)t)=4Br#@^OolAX&;S!GtJ zXH>VgAbyDLT5+kf=p`fFB<(d`>K`mpAH5Rm@q6Vq(ezLp|4q%Zx6y`2gES#Y#Q=>2 zqdgJ#$IA|>pUiQ|0`Ea<{5^sTUmbJ%Zip%8uK7NtUt?=4)qv>@LWug^z`>Lk)Dk4% z4YK&5YABt&+XpEeMc%5YuzdEidHjwBTc^Y?h{#=o$R0;lo#nSemPnX?4 z+oY&y$2pz|kN*w`dpj7=mG-(#{o(YE}b=W?j8?nlN>0qSu@##j&Md z6X>7(eLwNHd&-XE_~bDC<2;abwmWOjoHz*IR0pswY9U!iEyg8^;5s1eC1NTJeU{fM zo%jsUH&kp#-``h@v`&tpz%wvEI|ydF3+pf{nwqH(N8>*#|e^m3D#>O)%I8Yq zSoeTo5$=Q$bC#O)A+I=RZ@eN?qD8kBn;8_t2#{=wGNKlRZ9hOnW5YiD4PAXTYIeca zJyG&2eyFgv;0mRkSLqhbM(Kn?nDg0}o*?I?Vwzy*3Flw2d$aE^rYZ=tN!{teC@8W1 z&;zFDB;Z}0{?G$fgIbJOe&_*dA@5^lxvYc?Z&z<$v@(C;3g+QKiNp5jJStkwqD|u4 zg~lK|$Zi)<;N0@x5UPv23J3N>b4c_pq6qtIUt=ZJjZkT9OiD@AhCphvQ>|scCo5mH zm=h|mBkb29yxhPFX`3!{mi{Q=ukHLuV$_c`ZGh3GT*i^dM*_VDpP z1@UBzM6ht=Es#*?%#Ya`(cxHj@u;eiQY2tm9qe~;Hk@xzU*`D#-1dK)|4~}V#*ITc z`ZwehIwg21$(@Qwg0uaR!^8aJRMw%UJu(;*g2cVIHp`@v;aSgFjF1WYS@cui&SBR@aJM}T(~KNAW!+No89CO6Bp$#4K^Ia} z`TaJDUi@j9UH#Js;paF;jrydIzSc$9h+{c@Ze7=&z7@*?%Px_Ei%+A>232#@sFPwec$dC z2MT;X<*>`3QncrVcIWJ{D>*)8DIcV~X8m_qUxKe@^j`JC{Q6B3-e;Q8yh za}RJzyL|AT{`=aR^mF1^a*iRAn#^I7#t)G42FYQ30 zt8ZG>1oh7K@j&ju2ZfI^PSxEM1IDhHa37s64OYs?EAt9}e))@CS2(PvO{ZENQ|cWqEUt9{6palMETx4GI<}FMU$&SG$08-Qw;U$K zKefsXdGLQalpym&!1}x;O0%UDiME|vI_ANAt9K>Y8}E}H9^OYDm%FK^j3mWW@#;4q zR*N@l(>GBPj0P5J@AZJ4VT(T{b3fIU&41F{z5{+MmVJ0h@wB>=h(%xq$Nt;oWO3_O z;vg4!eXkNo;#Ghi;k$9Ar011WjgjWtTZ_Z7dTZO_*rJtJzz^_26NQY{f@i1gE)fZKlW#=6J zgm)tV`0TEw4umiK!vKR|115lnyJp1_P;3bg^7lP%?V6FH`6$P%`!zn^Oew`oFz&PJ zZFVSw0<43aybhyDxDu>n2If7~@?smP%!^GB?OG*&d`c<;HQgl5nXFK~@qs4I%b!g9 zodek=^}YjIuXLNE1$9)ooEiN%D1^}6ZT_j{gd8K;;7iVwMkN(iEQpw53hY=~G6V4g z_okW}og=_I1Es|H`2Mdj9-&A<=jA_3*tbzi+!Xs-hZn;p4d#syj&57R-ZlY%$`mMBbBzmWbh1Gun}KP$h}Pdyx^H@Gf-)fQ>qLZ$s`9L=aal!h0}D*#)hFtT!bt z=cx>;dnfC%`6Sj+VUoSu-&@9pE&MF4mK)xx0`4-mj2O{yp@*gIIRGs-uY^81MR=It zBeoqf+i3tn)D)u0sg6%CTsCP6f^@x*&|=h#zZi~{w9x3Fb>3VNnOn?~FkA(8!XER! zHzpZr!3hE6h;eqZ!dE>yNtFCC0(|*&4{k8{43wa|M&d%EcDx`HVG^s(RfC+qCNo^j zR&f-XjiY{MOhz+&o|ccNF*P^i>-Ps;Ac6p*78B7?dc=-E%i_$$uA0f-v<`Xo;O|0$ zpDfwmu5Zc5WCZ;>1gRDA=}J-NmB6;n@{41}_m8&DuPNOR?N`+%n;>}Z16DOzUv8%m> z8|O0{b}Kl0j4jiZgg%aS6pMWDlUa1}71gFzjI>%183fH6=th9Z-71H=YsRZ-=!B(m z=%6$9Se+;TOzF<_wcWP;PWKuk5C7`=A zx*;Ugg4=O5y`f)-CW^SrB?;;xF~;Mk5QzM^Wwu_(@-~B&n*rtpAK4@!FFG{h4ce3Y zJKz$|*@exb-2gr-s&?Tw4^MHBg7a5t zZ#ahV?n`toNKyc$edDYj;}w<)}(y{ zrh_`7T$5Qs(}*2mPe~~dLp9BI&z|O1^zpn;A<@RYrz)Y*@OTOBu3ayZ=rXY??x<(f zN}RQG0GlU}EPGK0qc~+LfoxpGaR$0`idXq_P^ni~id>NY zlR@_@75-zu8&>A-J^}%~$9sizgn>zVAj;2w97zaA8iD81 zen0`(q{5j6c;^#CQDqiL|Ll0s)P#Y3%a))DpDIq%9JLb_wR`LDoIU^PT4gm40m_b2 z;D*X$iuH{}A`QE*@NA#tUDM>2DTIUpKoLn%JO39L1tq$>9<>L_Rz=HtZ7-b9I z$d~4Ge+StA@oGhFR^=+iQB~Z@8JJV=6By>Vilo58^e;aO8>4+C2$iuBkf{H;AAa#~ zA4VFJtFzp>2`Hl{I>oX*S+`rA^h5(TLlC_Ra?Rb(0#NbZH`9~%JkdG)IqQ`f+N6M9 zdL^KE3cwDWSM;&Pd5jC{M~in*zEK=W)EDessQs&koN)ECA+|7K;P3$}c>$hPfqF3X zu21k0b$H8-=#x0F$f20;095*}=(uJtYm>jtfQ&0)%@&y@i*M0D3!@| zpZ~JFmifSsM=-%#B|0TMk&c`CV@sM*1!&UYt~JF>|p>=iFde^ap^B=>94oc zhOhR9$AGhmPPRAkDi? zd<+-6cM)uxxqvq;BLv-nZxP;u&@5cP#&pP*n_Y8IRZhTaFvDocDA=WtT!tqH^ZJt$ z`m~JvwSEaJjtt-97fZR^J@|@`vt;d?0@@WS(RmWCSx}5Qv>3l~#ryJF?8FjRtpMr7 z(_s_JQ>y}35*xx#31op zo3KN+LrZaMxo8GU^cjamxDIn>B){ey@oK@KGD*f<#H^{-%$#65aF|Yx z$M3#nUlcAnrg0IpiE!frgCK4{bPOeQboEGEGPJJ1y?#`7CGSVkDD;U)Q3T_nb9=?T1)$z5& zkRffNY7VIcE-w<{B;+uvz~t|xSqkNTz<1n!+lbRodjugZfVbngQ;57hy;B!2k{T>x z8Y`{ta&$t&1?={8-_^K93K$&Yzz)6JBk&`N3bIyBj}(2KLp;HW969VNe8@WCS-ptA zQcQQjFOA9C?K>7M}uPg+5LYS*1hh7@KJ7&?|pS1J*7{AsTCr6iRPm-79&t8 zazWw1>AnF*2dx~nMGNLw_i60+go)VE$Vm#r-&9zKLx}dr^pY~vofs4q%FH-{8aziO zm`!oWzt=3%kAJ&k@6EdOInLKB>{iTuP>`cLg{cpP8-F#I!Bd~>B2IebW{>vJb*Tn@ zgw|OWS&SQ~z`>HKB6wp$pFHyA)}AF#n%CI=xr3nLG&O#KCjCSHyfPb`ttlEfy7O=c z^9zG`Y;A_Cr$X_ubu{4pO+jHJeO+AStowm7yDT9pwME(WAuZLOeFH7^3^0mREVrEU zoZnkDro<_p#r&!%;5lA))o!Z43{YaycRhok-x?c@G>*QLkzBQ!X|4>r-Fb=hnd@4Y zTs%Av+3b}#8i33dMfW4SLd&_KCi0b%DzV@q$thf22PP<-WHpNc*rbEl-TzHBR>~>i~iS0VB0yt8M?u*s5!w5!V&04C>2P#mKwqGhFhk45_~3FWXq+@@^Q0b($Ua0y2L03=a9*0g;X@JU>?A^yeVq4Dk- zdboIu0GVfd!ie?@JnJIg>55d>5BA=g3ij4&14UyKlYHf|0u&Oj{S3@)81(goE^-kp zvR>c9|I+bi*W)W7qR>-6+RM1Y4mdc4J!gcf!f!l|G{re|(wuwFLzW;k4liy70x=-oX=UeVv+y>^!-e_CYf!la+-M zFn^bnBtBZCcSt-IMNe9D$;8Q0MWxoUsd(V@din> znK;&%$(*$z=!%X|1N&i<85|7tNAD1<-t!sGq^EN!mbxy5x#+`aPl6MS66}^#BNOJy zL&Xs-lbkTgbXpyLukDs`CxGbg(B)~SgwvLj*=iXRu4iUi!h4@DBV+e!UabCW0T&iv zjk#e8@B+THuvUb*{)ktF`eXVDH({h~qSmXu0DSM$f?Z}adN(R*>~HlIq8r}8D^gcY zT$b%t*aWf^!27Lc#yI_-y$a;~|M~c0M`DkEH@`_cc!s{q(~`vrE=NXxC>iymMNuF! z(LXO1N<*P{N1W$F2^;^t98?u(LX{EyRIY+B@J-+yo${4}IKup6&g?60R#rcl(+8y#i z-9X!pUedEAGVY|Y8;(4f=QVFb4F(6OEGo~gxu|O_gY;?|ok$3aMUZEd2wRX9Hi-t~ z)&$n445B&&*RfnqZFWKP)GR*07fclEJY1E_3(XFj0J8*g=?_OmUPdKTWgMs_P3hv4 zP;y-s)e1Q^whREbx=!>k^86p5gf6^|lmbITgyO#3PzO~;t4|7v<6H~-S(fL%4?KZ8 zr_m-CAX}h1_ng{@-S9s7sN=543A(0HHiB+W)RaOo%i_pFZ`3CLp2j3Hl!-M<#r!!)+mj(`1MFSRJw}%vmq^CCgML zVryFd)U%xF!H0+$8*eP0bW8c6e0_Vzng)Yyd?wrNT|@K?t;jxZSa7q7vM5*(Me9>t zJ_SgF(eGBlcZ-LBMMub=1*Quu>4$SUqMbzFKc-i{`WAEaQJ8D1fz{dvk(rt9>q&y1 zrVtcSTg`+_Dv%AaNN-euHvf8OAyl@$A%@iMkTZb=Lbf`_gQZx0)rK7!~n*uCqoF^%Uq9E zE`ju}M-|SWNp<=r3)P>Ag#n~H4S8^nolt-Y;@%z1pz$^)DmD0ZfzWaOb_N?yDru~l zgsztCt%M7Py!cd7NE?N3Cdn!0Oo*^J8-kGq)P)qQ>U;(MM{IJvC-vQM7AFp^P2nhj z)UHF@cSt+^Nxd!_3u!W;ecP$NX!PMDwiPu?y5#6m3<|tJw4rIs$NBAbUDP9!d4c%~ zd=m5`^bfxEpvR)5Xw5a68F;eK+9jW~zU9x+)8J>lLtq9j15h#afEb0+gi>u$^HbLV z?qR!(Bj+CmCO_j(q&}*1ozc{|7{=3N^-L1$!+dhcNr2WH;v2edBU<};dJtRZ!U2m`NnG*5hEkhldx^wz z)!KJT2-Y=RWrXyCpL4D3mm>LAEWV=lSO_@u(~S%K=>YH3!9bPtot!xAn0h&ZUXRtq zeL?|jGMebzbCe$8v!n@jjACgcL4qN3nmo+Y*f)_4kH%waxogDJvIwNPkVON%K}`b4 zmcGcksS3|CWTg%o);r_gh)I#;X%p=hJ`s*=jgV+B6mGUPVlc$<7$4QzJptFo-AfFW zt8uV|ON9H|d(#7F?*`&I&z2bLJqzwf7c7!#_NI*+TE58L*C;Kkp++>Y8>r}wqbjhA zaS)OCQbV-4!&WLwxN+HpNzp-w1gg1DQ(-OCDc{dp_O^?VZPf{pCx&%{JXUqp@^*b*x_h*429R7}U;QaYBvHu>4k z=k+<=TR?sqYDHUGsyQE5+w246fVZYlyNYvRNkL6$ldj2#HO9m=1B>MbP?N3*q$~cG zJ7UWEbS`?P+pP(nH?EI`o{O$3y~7Ti%_-8&{<0LfdHlB1cY}$adKoB^C$3eG0ab1< zLv8cQ655QWu?KWBr7Nn|UWqAf&YeB0<~qqhtq=ESA+SHrNq*C&bG~VsUCR@H@d7%s zCVM;WI$X(R9|JaYP1- z6HZ9LQRrYE>4wnWsuU`1N{4gFTwTQYI~HH`&hR*y`rGVN8;_p`oDBsq z)gWbRLVgTXeN?l@S&koaNk3NbD59SVr=Uv}l2Q3#ym0vR)g7QC)^U+IH%YtF{c2B< z&LCoD$d2T;J6A+J&a(t7RDo=Y%-&dnL_yuf>#IxAGyLF;RuNz=1?71`nKp7@U+K^f z%jpk5_1dNeHIecDFx$rBYty0Uv!0GvXcB40(qY&q+SaEKi*eC}2u$H&diyn@yqxSz zXM|o;!Y2G(hyN;q_Jd&Bke2uO7&!`fNYS&3Ij)9GtGP*|&q2X~#?+iYCu3p@OP%;x zbuV3(0AHlfu~-FGbQvJjS|Lxpy8zRos%C$8%W0MRJ-fZVogNSWx;VJZ4{2`2A;&T! zd&DBox|mWL@&iBTIDO^L5xxjhFKJR!WH3C<*g_nrjVCo_pQ*7{yIV<=Cv6(_rL?!T zINEEPj^tHR2b{cxTN0xHu}e7b^+`voyvC0Q+_lc&ff3YsTDwVNc0G0M79!V(B6jXl z7GBA?5)j%>EDsVUSNJyBuh`(gHSRFX&$ZpexaWBDWI7^-|I1KLyyI0()D9InqMEFc z6ZLIza(fgsFWC6oow_2I^g(I39@2nZ~ zyhKhbSHGJbuVM7rImnwS-WNLyXicvnVQ|LHl1rn1--{F(qSj6?3l*>7F>OEksdCM&UjhifrWcTy0=e zeruV2F!$vG1P85#q_Xdst4}K|U3uKFa^FlYkG{EZVcYFIKJk@Rw{}vWy zc{0^wyg_tl4g-1Uomud*?o z8oOp{W4;}b+%YxL<#*6Ez=T@Y>1eeMi^Rp_HR3yqqotj1qNoE5`DZe=0O-O~*=$pr zGBYKoXMF`E-1Pa>4F|iy{hYiJ)#H~=y{i&5jRM;QlfVOaa+2*N3d_$`=@W<(l?s&w^24nJu-MRG=t%6e5crhyJ9pppjf2%qXnoEzT7rEz zK@~8xf1jt~Tl4iyQ5Cn`Z};CjCGcOFOgN()1Aj*+bRaLi`fRIu=m)2C1*B3UIc2#w zX?dP5$EitWDRo`v)5Ezj$)w2yGd)%UD|A2iJ6t|AL7a9dvwmPwrq{N_DkM+WoEjq% zSXy{ctLxL^A-d$qjvSVqe-5If%XT5XuCd8)Th~ILkDuCG3hu)3q};32Htwz5(upB9 z_FE7%mR%RS5A?p<5>BjwHwptMaz9i@!FpH_uovC0SaYuAgM%xZA7OqKX!5B5BQ8eh z<1%qor<^-#DoXG>wMbR-K=W=Usekj>Mal2j@K#nKd)xr_O_-4OzdTD?{4)iz@eZ zuQP}l7XAw z3;qE36-G;Eu2u5nd3uUW^ON?Ez94Kh(qkA$HBzCZi-s6dex;7Z=~NXh`tQWmLJ~^D^V5?TE>UN5J!2KdvvD{47#gHgEB11wI63J1glGm@?093R1u&OyJ{e;wuH}rU${zSJOFn zVTdyoF}f*?PJBxSs6&x)gPs`PrASdVyIF~uk+7w_p)xqP-R=dB!W9?xF&={wp&hvv z!l&A+7g_!m7k5lT@-35PtlBqAb*oGr&VuaW@K+&-Ym(U=Rr>RoBv&f&l=#|1(~{iB zO&Nk?d5>o)Z?aw@`{L?Ty%zg_|NTE0{GX%)_2Q@RGC?l6D3e{W*fy@eECyfK8eaFz6&WHfRRVLSwhD=@M_hzU zlxk^wAR$>U>KB&!`>L+l-U4?Cnrj_>3;m76r?UeKBCrd5NKn6_(xUT4i0-?oF5-5R zMe3EZ<#hw|n}lIY5qMmj{W@{^=lv)o8}B(Jr*x19+6oWp!T_RiaVc9Eu`jw>vpt`2 z;1PvZm94*pxMGOEkm9%*#b=U^kP>%`Fs-<2DBwyx)OMDyi%P3WMa?4NPZkCjhXo8g z?-k$gJDQlOLl%_0sZtwIrQCkw;Wb%Bn%6@-uZt5fys&0vloy)b-maQ!hAmOgwu(r# z=_6yC=03-`yJhnRGo2KB1Fxg}_^fqu`S30qqloQ++7(*8wt>=o{r&Q%{e$1=vVD_% ziYaa)v0l8|wqe9K3l8szx|&n69or{t!d0}PKOmzYj2jV4Ct;W)IgR@R!11ep&%^n= z0)3Xk`Sa*NDM~7|ye^W304%$t?h3HN1@YEc2iIFAR|TDz8EAgtw|#906A_Hx<+a`VzrnD?DhkUFLr|A^D!n&yX$KMkWuj>=1EwKts)#HcC?byuacj=;Gxw>q2wQz-74?ae?zR->AvMe4fv2zQ#-yEoL!ddEy(V` zX~uKB93#b9qc`-)?rI-D9_CfSY9%-z#dYx5hn_x6{|^ z!+2Gi^cb%b9K=Z?d!4CzIs)bS9{_V-3cz-`x4C{X|x_5j|^20%ISmw z7)Ngmb#Umj8TJZIsxRxUD!4>U>NnyBT|V3t59WtOw=ZhQsixo-^} z`=i9>*Q_y@j1^gGdQhc>wy)CKA^hT^-ER3Esy(%Q-6JecX|kEvPIFGQf@KTj4s7aJ zj)!7AVh3BQ1)fkM+9vY1`1hTATGHv2X4fVZuxNNjp4Y&gPm0ame4{q54?Vm70F=oI z4|%fXdgwxObs^74fUpvjuW%CIB|B3|*BV3jZ8RLhyfG$g`dF^;1n*oy6apqGg*o4+ zYEqh2u%bA7Wwt33rerf(Ih?2p19i#47?T`ofRcVebu~{Y(;Dx{3{KXXQ!c3RHY}^T zwh@DXUYoeHmPXHPyjK@+OnxgB7^&PAA)XUsudiu)yj*u`$(sxt zx+mf5w_zrMA_u`m)+ir;z4yl}05%FL8p=K5KLD@HVQAb60R$Fg3EmvAmYLG%?e7Kp zCVVai>oh%OO7xm}4?ojJ#Lf-R@`$6CH?S0lXn)U3!}MKRAs^IoTVCyHY)12_6Vj$( zPz%RD&5wTReWT{^O&DT)Qy;*qXs4udj#C}xB45uxKQo*#Ldx|Apt2$>EGNK!WO0!9 zWg0O(fT;VUErVMmB4K?3d9k#LBarevhrTZJmqjJN{Hd2>U1r)*)r)?Gw|PJ_J z=M}BrIZU(Zw(Xy875b-k-qducKDo)|oU;B<2FY+q_F%!WxhyA**9LmwC>bblu{eS$ zGP$g6AJ6OQ=fBF_tD!L$y^~bBNV`@q`N~Cu&k#F(;>j^Km64<{Gsejj`Kx))k$1Ht zC9C9OkJLpnq+RcTpVRySsNfkH_OCs%d1QT$2WR4-xy*}tm{>iIwYAGZhx4;kV()0z zQXh8gyGaYbH&eSlPAI(JXbB1|FbqBQ7p=>Tq0=Gj-3^vscWSt@J>}fj)CsgR!Ei65 zCpUIz);(OQT<}(TT?WNgUHh5Zq3BC?LbKqqeZGQ1Ie7duU(&U{( zxQCpBmdJw}i5!FWCi@<|&iXxKF`w?b_NB5Uy&*{uar32`^|r^2*bZKO=RuJt36$RC zIK=ahp9>3rBi;0mW1*!=f#?lY9b_cBB44hE4kq0KwdfTGPA*nub29 z)%>=DeGj=W$qVLtRh1FLPIVN_;|cMI%x1`gm}goX73?=R)=M^aVodTp4O(>FMm3TAv^ zg+czrGlTzpRTT8_0!}u+oeW(QkWX26v}Xb>JIN#jCx@jj&BT!9nU^xBe+n+|P{4op ziRl2KGSN*2BcQyP?YTh6AlZkAl3^UXLXtMuP0S@#g!jYOQ~$$TefHVQSgj*xeTAr5 zT=Bv>yN}?6?xp390p_~a9s-idMT6{-fz3|5g8wL=tktx7dOLnGmy5n~xn-Z1e1WHD zDx}DCFYca(3ZSu^Qp1;u1A9sv1Qo`JSgPSD%W{UQt@x!ir+0FBicSuo2?x2`?k6Q{ z^N3eZ^`s~AgUb7#Q!Ex|B*dzv)30N?H1)KSS`R{d9@y}@JfJM(*P{}-@Tfho{u5fK zEV5={wLPefQ_u*x*_HP7R$do#OjaG*leXiG5ae7vf90mCac=yN*FA8^NyhvevCcErj-j7q_aMgH3ZQ zYzWi?lap0?nZgtgbwgY1vTjTm9&jP24zltflS=ZP-E2E6qqsj|deOV%V{Q1WYNluOxq0?$B=LUwMii{1K`g1&UW`_v96`XH~H zAu+4BvOCUK^_0CA1Pn`!>@}HFDtBlMeHe87+r2(d;4ehOQp+8?S-70vN#@6BUQfm& zGsO%JcR8S>82_ zk3w@inuw1r*c0@|{Hi@_t@PlL!<-U{;s(q0;Mq{5(HEYyNj~vvF=LzbQSJuH%ebh44-0#0a>_oTi2JL*_KnWJ;n)e3D? zI3mJv%Yt2aw+xY&BCYAk5hx|YQt6P7quYz29&U1V-&-(v)D8|9tMkl8fU zNPai0p1!a=p{bf9cU*cIo@t!?XDuc>GQ!|p_f3YBZ7KFY05qCPpO?j@9LGd1818$S zh$e;rkkS{SZrqStJwwS~Ca&>7e0maOa0Zm_Q{dTXezaeEg_>I-CAX9cQJ*0CCf zA!f!lTIX6jG_a2JG%>&W`M9Y%kc(xcL1gvKYn##i*@wOG%SfG@o_b9d(O z>tqWNC$q_GyGq+etxF@l-mN1I;sfD%O)u9y2{&A)`+QqCiG6h~A~)^G7-~u}Bq_D@ zHSbzIFqyjh6GIOfXEaRJHHSLSW!YU(gU?a?tDPj(snN!9c?Jj^ULP8eqAFn>uU)(B z^0p@9AlbvGyz1YjVB-T%%PQ2b8DEkr)~qq7eSN5+ey`Z~YRp(6tSM%sxr?-(bOw+OdHjy z4cr>Wh5=Xc;5?-IYs(dN8nQ}g2{W%6FcbgU=m}8qaPlec5Lv`~$FufrE-MP6_$CzY z;-PXE`9_<27^>rG$v%SADP+$^B0QO&7dO5PQ0N2hw|)x$jY>7Jq0ySi9iUlFIG~19 zWyY?k+z|X1X+J_SnjFitn`-B#1Us{XmxC(vZ!@^LvdH-O6525|k69%XoblN|1|1d$ zUDPYV;R#wK=Kd?s-pagtDf7%SYec*yBXJQ&)GKD%~bf0w)3+8V#AuYVmkb9jn6{KP>Q{Kv` zy_U*#Hu~?oz*Vt^|BgulPRez}R4r`OLo&P8Ff+{i870Y(%{8ys6`|K3rA7)3uC-0S zP{X_ZDg2td79g5a98Rh_ALu|pvs@*`l3zUkq_lp;OmjSQQ9Hdhz4}yo@5Eua>nCDb z99;Y?&i9Xt<<)2Aah?iIHtJqm7qH68` zV?C&@DaA4eXAsk+3y&k^UucV1Z*&W~5)q4HCrYorP)~74_yt(I*CIIM(YflAR8VlT z`+!rFb4z`l-($skJqi40H^83D-HNaixBS!L{W)bG%a(D#dOb*Zpnznj@EttS zA=Pm8!SFVQd}2$CK~^hobPD(5xhgEJv&BQV1zakG`n?ySNO`Y@yKi>Cy7X13EArk; zSxM`&^cvDEum{Dzs&9x1#LPla_@|LBZwokDKw4V<#dp~fUPCUi^p$){9e|VyiTMm!@kppESP3{EAgtWvbEi%4^&rcPo?$Y7ebkxDi(kU_XkVa47zlpsP(wl zoA4G!GHSL-Z?bGT&Oov4TmUFrp#%B=mrM02&izfrEzma!aA>MgeV89Pt_=IE7I3AyY-E)@$0(;V0D;?+N8z z#QXe|K30z@Vo=)HL3^|(r5cd^C>)-NbKf4=o^bK)Cua#`a{tEGy1145LGE(R1iY+}BU?iKNH0A_YrOu3H*iJHEH!KlXr ztvNVF!$z@FD5P)lyF}XR#_^UdZ|oC}KxU_jc6&oZuu1c6!%xWsWt?VmMsZyU&X|&6 zHe`&T&on-?G3RxOH(6~x3h!AN{PdSZKT+-xy4+iG*_vvt$wZJWQDX$uIvZ*7vff6O zyu`AEij#A?Qmj`5_nTrqo9bYPqts#cbT5kpnK)G`p|FD6{s8!hZI|&hF1sz}6=$#< zRS$KJ_YR=)^SM81_=}QMC<)?)4xJik`3&fhC}VsQ^P7_9Q)~tmu{g~&na7#AH4hrqPXxe`d*y! zYraP)r$1?-9*76DlwM#tKh53sz%OY~KXJX9Y4YeVb*j>;LHVXYX4!wqK~0^~h(DW1 zF7Os<)BO;ZI|wa@xSdI8bYwVY%7K%(ABJI=0DZ&&O7+sg`9owcl@vBHq>Kg@wYjRr zMx8h|8zB_$ynYh;%PcHks|@d4RE64PKLmlgVhxR2>z!r5zVQXFV;;L1D2-< z5cRQR@#bZB1fR)b(hVKuE#t`EEXSaON<5C1d=P%-B<&qR3Z&u*9C z!UE8wIOoGmOIkj8jRIBQU|(O9f{=j%s3C%m7=T&AwD&-|PxA4Q&J|7N@>Tkdb-ElS zdz^D#y*`!S*4H2Ckswt7N@b#R6Y2`3b4rS_+dn$}WfiEjLQtuyqN6 znQf~ZYvmb~s28-`EUB@-l@cjOHVGDO6*SU~mH$qRtj2Yjec;I-fOwWaJq@MP2U-Fq zVj~KVe&|AG@EAG78CoR_xlFjz@mnw~D3t^C^HQt+iT(=CYhmMIaneQLp3Pkwp!(rGqR#KgvTVL9{dj0$_2ZwAQT-D# z06lnoHMpRT{phvI**}!eEO?H?zSV%HXkL0m)L+`HI#u%h-yWE4q9au~MXEj8f0W-s z_8<4uiBA&)Nzd43Xp)KNeem}A{L2bQ>Jd-M2y`+x=1{^EyHjOMefHVE-)^9T@V`*MH@t1<7RRS03)$DF)#`o3U|$4p^FtVs z9juX(5YI4&ti5K@HNXGvUv^rmeIaef+<>Sbr=`G{&ERiidPpsSc736zd=|SmIeyWE z8NzO|9%7Ylx5`11Vfb<&C!!~&;l0?2tWPGnJkO7SlT;oMDR4zU$RfkJ$uq^2|G}*P z#{XMON`Oz~mK0_0=1V0%eerhyBU~tMKF!SzrpR6!-M_6_Zo{_Tat-<7XZ@gg}P zjNPMsiFX&tAXUw8e^Bq-43~?uEvICBO)*6yEx&P)6asc426d$V2fb}Mc+P$ADc|1s z%cvPryC$$8XZYtktvq}hEp6Qc!Cc$f5Y@?0VBt_RqhJ?OYpQcK(3p$Y{t@sJ4Uy49 ziE{8T{ELGvVOOLNBMnk~4wz`~q`AoOefiE&Jo;iAY4NC-U-#ka*o`(y2)U|eN<(?( zI9eQ$!MY?j&YmBk8pjWT)o3fe5C7UFNxWAkX;7BQQbVz31(c5?hMZwsxl&~o<&|*5 zECQ~u;LKPcnvv~qU#YozhE!#^;WFZDL-dlY5isR0z^pqxPAfV)COh>Xe1oh?M+ znbD_qqykUw3w`_xYv(P*P&u+jl9-%66Qk|HSVDRU&wu)u(m|_7--s#{HBHWW>Y(B% zmA0Zf`-W*b(D>t5sBG%vL1YvtrDy+fZBes19L@F0oss_MdJ$YNllOgn?$ZB~xx=>W zM4UY;I=WVvyRwYk{}Dzn7ArZ-f_Eb^hvG%r<{L;|IAfmt0FKi@nopj4>l>9EE3EoH z`k#hzul|Ro^VnV~8O>vs#WMZXj%AVm6c+vLRZ>ftnjw_DW>Weu@qa#AWWt!Np-P2w zdY|I<%ufM%{&IS@lJwMQ99wUZrOWDNpV*_=|LfjN?sVGpNO9@W9|i`e%go6vT+;G! ziPRvJlemo3P9!p;95<}Cdlaq-9HO867MPBweTHB3k`vB}gOS*w{s{sFA%P~Z{%||x zsc1i9#~V~ZR--S8gTI0nO$pG7+@)wczbDwflW6{=$(Kc=+ig2WvD=gUOAN)hV3A&mZuCCkY=>zui34i-g?D%XQbXh@!(!PP+ zHG#cxnk>2Lj47&%(0E<@$X{#4l`-+G;#gpEXj{qInk@F@5m4uk{ZO}}es6fcohT3c z$H@jhBf|ZYVV%OMX`1UA*`lk-6VUP9;{6*_Z6H8UmL%gZ3p6zPt2&u||D396N(eO5 zNjRVuIojy0P3}gA+bYp}eX2!{u^h zl9v}c1+}}-;ZD;qFvWW@%dma<=|hz_etK|8twXRq#nm%( zVN>T^jN+LQ`g3#23TG0XH1~df}^yqBc9ac>6$jd3@sobA0NT|*L*NnS{$JG4G z7rXIDwe>^zA)7U)H4{E}P{Gvr^*qfAtBu1%Pcg45Dlk;FMQ|P)Sp4NCAdv*5#w?*& zuU_LLgKd+lU#~jjC-tQ|5h;t$dNhPlqcOp1M9uQBoi5ZKbXwRyA3JsxO&ps~D_C{h8PJEJ3OF_zNFDX35&wg9zq09A z*W^0qE=y}#OWsQUi7glteKwv}$4&vJcSf?dZ`?DUOxOOmmb*Lje_?Hq*{l2RK%RCx zNK$H4xIFIN!~Ex?!C7NgtW!70AXR7JDvB1m(g44T9)1o5M=jFxt}NQinq9UomltOL z<|B5>HngY%qPzc*)4c%v3m9;szr&0qnt6tU)asCMxMG9^ofW4>hWbygQp2_l>zM^L zs!*b+Em%j`<2)(qZm5_(Ej1aU%+F$V-u?$^4Kg2f-#sBQx9fBccp%B0bSgD>bjERl zwZWQ9#N>` zi1>@F*ogY8>FalQTYdZ)Hb5%T7rWZ|u@{+^d5xk|OC zbfX6DbVJCSDl&#|P^umMzsXbp`_VFFsALyyjC z+7cs>idoNpz1HLRN8R`EG89jwM^X;SJMRK_CInuYr@=`DKO_ZZ z&h?!ca{1*;x4Sz9!-OWVVc@9uv&I?ar1c|bAF{UKm^s0r8fvgYc|{fY0|Q6TV58U8 zIC2?%p&&}&Lh9eq&O{4a^uvkuvwBXaYPHHt4prNV{S&TS3H%W!$i%l1yoX|7LDwX~ zldPZ-K}b?tyJS*Vd0P6OSree@pK&Y&zsV{1mv&|dkOcE>9Hyd7}{ z1g*tboi~?grR?bI!wq1ONwEg0a9HJvae2jCAUjbfwLJ$Z$_T9{B+Hu8%S;@lV(%n~ z4>1xk7MDd5bu%Q7Q7yE51o2d=u8T?mk&uYPW@l|NJ3_H3T(Uo84VoD0gQa0x2Jd5J z0DqHB*EN%Iua=Mem6-2h|A~OosHhT9HC=qXKgBT9OHYDKO6V}6rk$rdN-~1ad^ilHj zy#FF4kUA!;OQxea6rA9tx?HD-f`esLGpef=LtP(Q9`JE**&*SKvVHw|vY<952`c+Y ze90zk%2@_<-j$ggcv?5c-Q9D=6%Dmvd%c(WX&~DJ)V7+<;6>-DRFK}olKEW4fCARI zY2Fct^Ecw%|7YUG{1bMc5E2-E6bMj6OGXe-+&jUH)uCp zS}1FkRUXA0C%zK+sn~~*dzfnLyio}pyMX9+CU!Q61I-r^TIny`9<3@e3ukD;?jW+PH^6AMQv}>A4_TIjLkA08|W@SvJ7Vz1AOKPvZ7~^$`3s$ zUqoF9q!v!+0Jjb?x`Ir`g2P4ntw*j|(jOLZ$Pj<|EJaF6jz%{mlY5JK2Qaur{Sb{P z%<2;VE=p{_(Er>|k)qE0C!vvD{SjKpnNukvbh>9h?fMr7Xgc{Rr9v0=;^j{GtmrK9 zHQZasEO5SrrkYXQF&HbXpf8dm`=SOK~xHo2%i)Ym%z&8=0|1)6=^Geqfy@t$1|=o@N+rBwBSS<*7`?MAh_Qb3jqr9pk&No*u^<-)qGNrlqzmR%}JH0{6u~%mQr?PG+ssd7T z_TN7&x~KfqRo5o-;iU&77xJ6YBQqBP>5){1+XU)$kVwXQA}?Gh{R3`UZuO|P-R8xZ zl`6@*N!=%@Pkx5se;ATS0B2q8M3m%aBM)UOMce`GGD&tB08-iB7)h=N)NGBXk$CvL08j0GolIBjS%CZv4{WH;qQuQ@?8tmyPWTSL$2 zYWMA*VaOleSSW_F1!hx2Fhjb!U>MROeH2e?tA0WxL{n`IKu1A&W3m}ceq|A~@V?Dk zD2w!fFgEOj*o!xaEHMKprg7K4a%$iUrm%iAG%-wjQ$wpRE+b{rWZEV}`)crSYoPOr zdpZ_m#OEQ^U9EM8f8m&Ak<5>sh@kKuhMBukcrjIU4{2DOj+AphphUvUJg5k1L22gY zDW-&raJsD7`67!IzlABX%~Wg*5^>-VC!w`d_kHxv&NaGf{o6v=uE!t-Ih$y!=g3r| zZ`2PStNO92C~S5kG;uT%{T_=!2ZKiWoed<+E`iagRs@Ph8}SAi$l?RPY`T};>O(JA zg>@;c#q~XLrPWQM%@nCUdY8qZl`^(}(;AApR7|fxm#r<_N()F2|DL=bdLt8tIhvr2 zX;H$1VPx)dl|dccB9trTXFc{u$|tk12Z98HHNHTJQLqx}e&!)>ssr!??XQ{+-dEf$|@&*Jq}+w+yPn zh^E(B(fCpdB;ZBZ0@5g#2{q!<(U0GpeOeyLGvX5Z)_P>wa3tvP{u-mf5oH(5#0Yo9%P^gdZ6 z-5`>HmuSKLUc(mjPthsbkG=IgYxDmAaDN~&x0WY;yO?adhVDQWOvaQO+~8lkFTBO{ zYGvA4cAaiZZH$8>ZSU;oHnvm#VqR|D&r{}Sy2YSuNd6l> zVa9AH(3?)$(xvck=$Pbrm$zu;aKngX_w8$!|3ckc2E?_j3!+Wq1PJc#?jGDBxVyUq zm*DR1?(VL^Ex1FF1PdM#Ja}G{WS_my-S@sZcjnB@dw+V-tD3LA`dn(&q8qNZBS}aM zzi<_k;Mz)rd+fMZWyAfPKW)Mqe2xu=h;!Z0-VJ=Gp=*KX&V=rWr&>|aL2;n5;mQE# z{q`vv1LQ$|_Bv-lRk9*TwJRoTP7ZHtDC~-`ek)m`=jnkKMm-{LimzgrxeYVOt76+Y zSu##zRNcOm9(aM)AJtzgqFuJs#Edqesxco>vK|lCPwn%*sOd#@EH|dji?BnvW?u@A zL`OsOUVJdMUg=XNm*pnzqBHmgt~4;CKEX}vX+2-_|3HdlR(CWT@E*#LWl4}$ozXN2 zUx?8YEpBpktd)zHDk_`McNE&>bA+5Xyi>n4Gw2}h63}*S%1CvTqxs6}hXq!amTgnj z+}T#lzxmufV|)$j`O-$?s}dBuA)WLe;$+5rj^8|dze)3frcJE^L6r8xj?udmXr4;n!eJ9fwcKhrXI6uZmC;M0sG4r? zX7;wR!c8a*Fyg=!(pNc^@gOWJMA@N4xXFFNzV1mP*LW2XFKdd3^D($FpsiZYrFNj9 zHCX^fb41BXW-^-Dk-S!0M=K+S4z7w36|5utLuwW^Tr&xOOrmolM_-qPg9FlNLyXaV z(3&4npqb~Ax~`Tfp&z&*Nh&J=46{^IVyJqo=qP)PoF5Fj1~@MgUa^Xp9)}=QTZW9c z0y|k*#$eDk=Qu3n@QJw->ni3@HW;lxsYYk@n|q8rDFF=}b{vY~m>!*a9GgWG&^~jv zSs`F0z&sFKCEcWxLxcTOB95|3IBqQE+r%$zPrY^o*Oq1>0l%igDXIvo9DWa2MwNl$Hc5mBo4vNr8$+xK4I!7bt5HUX@niPXc{1F&N4-fqn`cXaa28s*Er! zl4&N{_0fZ7;VzAc5JuRtQ32VrFI5H?N*HFi-p@=Vg>vMSAi8@MT$WLE;)xB(B-_5HF zX-CEKPM#>oy&ath@n4}G} zXYjA~ga3x^f7w0eoNkjA%5s0!#q26;4xhS{`M<$56+{`6r>4#I6S%u>;hjD zX|lSu{=)40!m1MFIEE%nLM-DQcrU})DCV;eex{LOteb)VkK8ic1b+?HC}h@%i|+AZ zb?JQ+sQx1UW-)ntP9sn>lUb|ptFbbzilTLk$UKY!u8>h5#bMwm-YK78cVR*-9V&6b zX|QX8Q5(Xx%YBtyXMKb&(vXBgf138Nyt3cYi4(M{s=6Dza9zh2{veOGC1f&muF{1q zRmR&?P&MJ&0)DY;=^An?Qp+WED}Hj_SQZEH0yxZ3tB$K)>E{{47#u0x;DX}XK z?Bu-|Qn$y+)HkJ9l%WmqkJAGuX`z0K0To)v9Yxe^PU4%Fi8e#9oZQ-1UEq6ti$Lk? zkQ$YynoCW;&Nj1t`4Y_=a)c{O5*gp34u;rX0J;sjj%b}$B&1D|JepQTT2`~h`;V26 zl(Wv>o3CGFtVPwj!jyCLF$quEDyzA&;{-m^f`(eySyk{{zK}cOsYM(EGb4s?wO|7( z^e>x4yCjg>2?SIGRqIf?)2gUl=!a0}wOp9F6(Rrtd)`Ukd2lR)q6_(a$aQjfl2%-} zb77Z6oaSCE+*a)kBRCoPrzggr&m4jO@yrp+Fnf-tZ^>*P=1bzXbNLwV%pYkQ3GuubFeZGY_%2c6$6&0l-}cFrD7O%Xwp zYv1eR+aG{J#Dfj>`r(d5p_13hUv*BHc7tJ;2Ws!AO?306t}YsGNT9SZwmtTbJ*}&I zW|dX5TkyVl&q}eeGTlnYLbWM-kQ2Uo13HjfR3*8frKN_CQjFOJ*iopDh^ATp6sq$f zT=&UMTf9SivY1DP7w-pPrr(K;?9-$-38PC!#w%C`t@>A7XUk~4lfE+v^=@OQe75X^ zFl^C-EweEqxUg9pmt>i{3_|UbQnIx6ECnu&2d8cKBN$F zjK3s4GEDev75kRQFq7qLk#ml_UwTO~57{Om`Ed}rgcfB9*yw?=(Is=e2bOlziC++O zF;hv8%E2Z)>X2S#WQlgwA(tcSocDxuo`X_O3>Gu}WlZBZa@8o#AfEyO7Iyg)ZY8#P zr#b;!c7|@=0!P!m^w-Ru(lULBYG|Ghq~6}p*g@3Eip%s&fN_Sa1EFz%0boH{RFg{X z-5GF9&5r*>Q=DXB*%F%Do0-~y`>g093J{;YMqLuT)_i$^-h!a*Q6)YI<=RzToBADP zdHg=ydiQa#`TS{H>(g4x*oy&CrSl(| z$&zumj_hT_-1Dv^d!t}E!>fEU>f}?AubbU&OVUxVaC=fNGoMoGyl7~HDOS&sYz*_(_CgvlV>^|`bt<}j159y``cC*I%XzQ@vi$5d+#{H6deElCm;hNG5Cyp@LsE`HHmqlKY!k$QaXH z+cow9`?+)x>eOI4!@@Z6_DuHWxyCdvVu`jVwg#>*+r#Q(ioou zMrkr>)LmM^wq5h~XV4K@A{8MRs{@>`%z4xEOAYg~F;ebL_$sHZlq{#woXHNW)DborONjlS#IB3Z-k$*xD2 z4zW7{<(0eCGZUSXV?En9oNFPQiJEIg3}9v|uKe7}Mi_Soj^$d4J$}0^-CxzZ4~u&8 zQO#>JAC@HhY&+KO3F6!x7poONqAkul*FZVb{$+{;d(=*i_C#Cf6V+^jN5b+ikFRff z)smNUI**F0_Csy?@Y=Ym#PfPJ&$3@;sP(m=|Q`?_LFo40bVKI|=TzV!66C6@4wTq#7GB0CTJ@5NN?cqPO< zPqSx0U$kWEnUj*;=bw?o$f8qiz3ScNg-FhMyI~`kqm`|ak>cgrz_64=Ip-#`I?n~o z-Rg96#)P!V(6ZcW*VvzwJ8eFykA20Lt9`@2DpdbU+NRpYIkT@&zYuh<+!qf!A!59h z=6e@>d8A_D-Y2&l8ni$jsiZmUiW?v#(7*0f+AaJtx_z2*1cDX8RoTq2CB{q+I54DS zugW=D+qP$mF2J5k?q$Ak?QOStZ^=$dS8ki#^2S;F(x4nT z_)>#31h{gbcQvO8p#O#7_N3rWs(cUMXKH^VVQe0mI%{)zeBGDO5z9Oj_y)}Jt=nL; z^S5xT!wrA%n~X`oeOC4MB}@R>MQX$^f@*GYT3{r0 z6l;d@jHgsIS^|Xb@K#2x>+`3BU23-)Ki5Wq#Gv|t(3TCJi(+zo_i*3z^2KjNsn^XL z-<b`}Un?hf#alZD$TK}7embhY_);s=oBRb1@ zt=48~W>2}%ehQ+6hP$NW5VbE>MUIp?Byr2$9#a4$?{YQ=LvRNyY|Mx*brd+(8mYse ziIOGjl(yrj;HXL|BTLS$60lHkUaH&u&aaK1Yhp_a#sQ$?HrS0c+KqN2QyBhRm%;5a zNRE&DLYteUUqEhe69Gjg9a%Sd*J}TTfA09NRb6@uxk+^V9sKtk?NewB&24-U^a80x zvKtgxW|rtNxvmxlwIm$VZLv#jKAy+ucNE{ojb6u#_}Jc<=A@ys4SwNsH~oaFMq;P? zWRw9oDnrshNZIAWnyf~+mA<3(XJK8ROA~76;TR9bw*9Vot}gL$vlXN$y(UUi<8-yP zn$;rbzwC)O!A@-JJ}k4;JH_1q@h?^dPLw%BD%lH{n3VzZGL_PU^OQm7^V@RJYjip2 zYM;pdWJpR2s|L@#r#=`ATXW%3l$NFCIMW)L5v$elMW?9fsD!51qn()yIvx8iu8R{j zjkc83V6ZxObmr+;A%r)`g3o?;rw94!QzWKM$2ZQSJ4-S>{qUP3ujj-QBkW% z50O?lCax@&?t@HKCRSS}2o8&FLxeHAHNkd`@xj*zEgmlSxbCtW345;{I(Zu{aS@X+ zjkKvcP&&T7D&d@+gRRvnsP8|kRqo2dZ&zf+?{;02WN!EV)UNM*d~b70Tln=<`T|7pL`@npUe&kpPwwvF&t|yC*bcVy zY#nXVU}J3A6%hzC=YBns^=<>5rK_9WbI!(6W>*5qN+jJ^HYcX8B_1V~c`w&Am)zO= z(+js#?{|oDBc`Bjc1J$1e(74_y2JJCypXce!pGHFi!ec{Vrh(pBS&{GY>G8@cmJO= zn&zOo?Q}#ZZ7-{pl~=RO8P3&6qz7s*24YUCeXB2H;cBQjOqsm&kd6F0-Lqmos!Qk= z@bQ7r4jc>-Hpy|%`XcSygZjavy!ny7MBC0u5xP|N_3numWG;@1q^aYqc1}^}BQNhy zLsTb>cLr>o_RVw``H0Kf%!&Hy`xAN47Qim&;l^r?WSVXrWwG0#bg&%Osil7ODnvv~ zss!bQJLU18vqE$;I2Wbs@fpLR)`sd2{8k6g82(ksA^9w@#0lk;1QK?vhI(Lp3zx^u zuO(X3QXMnydu1|FXO6d!*OuD6)|dN`(w$vT@BPY1>&|ts`SK*(`fv?pdH(HCT%ao%TZU{el^KMVJnM=Y2owf98X z*UQ=UD^~_qxqGVQQwr6x6nXab(P$+%_U;lm>m020w{F8bp!LLo}tV1HX1`qMjXkD=^_`5*W25zq{NS@wst%X#wQ{8J{N{z)dfe?@Hv z-|@Vg>WZm8pIq^=4#LinB%D^6nOdLt3S*WV9G@9nIl&c09P8jq^(%+?o3~@yXb}RK^uq7@oOe{&#!< zBy{%RgVYr|otv{d1Mfy%>+$=a+FHWpc6GTpGY1AtgvIl%eaTp`a=83Ofw~fjRK{X* z^1`G=?MfHp-2xZOxAyvM*4a%t5(!LCb-lNyJ1HPnkG;`hbC=AN*=@Ife*+51-FbxZ zP!z{`RF>sgxxJVFGLL8s#d%59`7{!x1P$a?^bL0T*3d6+)U8nGu9hkwS64XMIuKz_=+eYzQ1M@F`tch;Q(c=fe z<2bkaFM2wWwEoVrFkixdd2Jx_m(R3p{f_S}8oWSIYZgOeP840#y%&ZOs}3MK)$Whd z?<(q@hI>T_vH@O>AosXs$u>xi*M3r(L-_hnKQ*6bONoU#?h`}l^uhDyuWStCJ~&M< zn|DSm2_L3evRY1#&Os#NIUFoUUt}FnUjlKITOZ{5hfzdRaxy9J;l+W46Uvx>3SX%p zp1iLup42}Po^-D=zxeixgb+LZRVzhhYp7%d*& zh=Q&&#iHd<>}`Phr?GyOY4P}*n=592fFwYE^{EE*KuazR$f#@o77K~do|WDp>#cc= z$*JT}+L5Y*`k#s1bKupIMd>kn{2R48Aoskwj7XJ~C@s8v7l&mkV;XOF2Wd@Aua&Vk z^oGbef_#t*-?Lf%$&$j)8($vDRNCXwSM+v`>EM_9pR>%L`XC1@8{1%U#P=I;^Qyga z31_almmPo2%_nbA;(OP*^Y+SZ_~PhW^#0-o?sR4Mzh^kIb}?3JR+VC;CPoz(%CRZ~ z_dPp5GN|eU3ir;&o@*J2zKvIoQ7K6c$ycB<(iW)1@hb)?g&7VcOd_8(CK zyHO1m)T!JuAD-Hj5Ke-=({FvIfY-y?7zcwn4I#D0OI^8o;Gks^UR8?e(&83le?{*h zULJ#k8E5513R)H@?A>-t-2&L3fNrq`^Rw>mhrg>jiv4t{iI{F6b0my%PY4mLaNIU6Crx&B?U z+=+y?p=)ed1G_fxxw!OG0Wy_3%DqF+t2u#6)g z>$o#|$mDC+jrV-ar)q{Rea<-Rax;DC(9mRlR(+f$^e=TjxQt^BM4Pz&_v_7nP|C1*u7*XfhW(pwza_!_r~QzfO6T>M4g}eNU$jdj z$Emn?IygiZdOgk)GvWUB&vZmf4kgso^ZNl7_N;=^Ekh6IN*L<6+L&w;vdZW6@89Z3 zX8aNgO&T{p{uH_2`GGAV#Seh_DT~TDjmn}vE4G0lWxziel~N}MMDO(4F@O0ru9O^R zjb7jGPx5)E4CsS-7(ssiM5+HS;}M(w#%@iqS23rS-~FJ#DG^G6y%yIlLe-fD8!eZ^ za+ngTb)f~zDRxIWhk4iMg5EhPW7IviO;#y+~+Bgqx%t!i8&48W}dzEysfR>f8@~BWq#orlsPx# z(P_+(@9t{e_)1zIjNjUnd&t2@0?UVKwNdAo!FOI#=Q5hwMs!ZYcgE5E}Msx^XRmuP1Qx@ zSkIECm0&qNpEr{et5_@_k9MI*{AFT)XVkMGFXoF%F4Isft9rql0)^sE_*?`3sR&F& zsy3cq;6Gss?Xb>SAc(GU1o8Bn8z29bNb7G$@BM2$PAB9Qn0a?%>XPph>`AWHDyJrs zra|R3i`R-iawFY(g`LI7ZAV`HD)IPq$q2{sk8gF~3;GM@+01$T^r(O3(~fdfPS2-~ z<=%hzC`omi_P2|BmqQn-owao)4yLMe7))C{#}=+~Go@jeKTDLL3R8Eh!#?Gyu7_%* z7$Y9c3Ijt(Ov$asr~Qv`i7};9UGv0PJUBe9R?CkRY}9dGtr@wtdWEo2quewvbz%jWWdH%L)#_7~*kHx4 zto4?rZcRsiml^QT7jC3djpPparmIJ?a{Hn1tik%FPvfpWv>~^y0SP`b(N@^BiM!Oo ztw!sB#m6^j3-O&#K^B}C46Hd$enl+|SrfZ=`{?B-R18w9Khvw9zGswLnZc8GKLpBp zlkX1;Tu}8kCr5@{7D4mOyri-b3nsrfp+*_I= zlWB*kJ*-sjKummiE6wxpa3sT$&-Hn^=FvH~134Li;_#5%^=mrJuwzWkCvy!8B15t_ z9Q{jjLli4A@|e1gy;Gs=0X4OHAJn=7mbUmq4(I>Q&0Ubzfw;Nf3mWdOIA{jQQYUU`EP0#0K6Tt|9e8E@j(pCW#^0c|)3Z{Y`9 z``36hYOCHZ_F}s`|~m0`Rtoi)TJPm{tt{BON^x0Pv6<-<@tjc7iHwV z7_e^e9&0&7RvQ{_m{&|@s?!QeEZ_CgzyVMNA=#dOA8S@RZLn|RpG|f*F-XQQutOW( zNM|uO$@v6dwDhy~s;Ik;dDp`yEteOmPn<+)vAwl-I&KtxQ5nJDRH#(7ujeohl$I;A z+R(u}t!GUc=Ck!)<80iJ^1Z>33#^K-Rk-~=-+BEht$s^ zMV_cxtH*B~JK=B}Bfvbe$MrBmn6`6)=kq^z3puftQMk+gwZsH7{jxU@cBONznFD!nV<^6e45t`)z@GUei;-`MyUcg34lc*Ze5K{ z>Li_xXDrX@otN;=l$;;B=7rOE>x+gez0^QeU9A%VD~8(hHvGBd>JdP#mRTEiOBiS1 zyDWvU=!L`Jp4bu?FWFWBPcQc z8G>a?vsfn~P!HelH6sW-2ZT>i^*XAGFT4%ovrC=Fb3vtW9r6nuFYs=Dunj`Mw^%)KqvOfkaTzCW+NCPI$eX^Fnuk{YEA zk|={ryD8Fa^AHt<@8?nvCNi|22%E9YIk>U32MnDgeP=$(| z5Ofp{y?CnQOlg^bDktJia_L`T?kkzA;E;pL0*z1qEN_ z*Q5?1Tn=P8twCAR19H)wE7jZ5HzU8Ym|-{XH@}19d={eiq|ZfX#e8I#0MW7qS4+_1 zK;X&S2UOl6=x^}s4@ExpIOJPf(TPdRm(5fe6{GOHdN(y)PZ0yz2 z)_#oFq&ohXnu9(p`;wyL+?AJtSN-o0`$H$xk@MTdQvyF^AlxIkA?N{<@>@nuxe;z0X)#Ihm!DQnxn>JK>X8H|5_6 z9rYh2ME+Sq&d$E{R=-&>cJsQ{I7|2eXkf!;=MaUpZK|F=lx zTr4fFMDGS5DbTs!`hLJZ#yQ`rEph3C`{?8~?;v0AopJYe8hmA8;?-d#txl~!IU92M z*!?caEURFU(cVr?(BDb#Tt7(lM!$#f^St=8-G5@BVK`!ZXmJR(d9#yivEdgx^ZMx> z4O~-6`EupW;C8de3%u}8M|s{wS1If4&pR;e_FBE}++p<(R2RQ_VsE7~tYUmF$W7Gi$gR`w_DIyAJ8Ej{_EkMOlF??L{pzU9*SYr>>`6%=P>(y=Yiz zie_UC1!pqe-F=cRn)pB{8q7YT7!KZ>FfS2@Qi4K)~4S{Zn!Bb^UNGdRHiv4t8xf24V6mP zRF=%{>mB;wQjo1vi@5geN+3&RYb@}rmHPp#K*zj?@%0ch&>1qKt)eo!1-k1_jq)fl zEV6J#bYuL`-iHxb-{b`)NR3mu~Hj_zph)lZ5RV z%U&m$Q{>skf3xAz|K%mxOScqkO+9SF4Kno;sfNsV`+Iep?wEiBoDTk;eI`iZnYf$ZO@kZdbr~4#YK23rRzyKLCyd248iYhr37G+!H2*dfNmL=6RJTqA~FGX;{XZn|4ieU80ZX)LHjmL5eB+=NH4Ivck#U~#pAp)m()+?)v zH!fX3nfWM#I`#XiS37DAXoG56-8QC+Mbmend_jJjuT|0a)fw$cB?HeLXvepd4k$~8 zQ`w@+E~8k1A3@b+?S;>^(ho9OBF#XM?@Pv@>^u>of-(rDw}yF zuk<&ewd^kXM1jM%q8oZ7F0p0U8ZWBh$srNmo)Yg@tZb7$G9~)!$Q@qlOiLpxgaKfcar5H+PbiGa9{5!SM;YP2tQc!~-f< z2;l7;8NMX|x+&M4Ydu#CY?-SQ5z~|Mz}j;R1&io#}UQU|^BpA!$PJ z80lcr5D+h$LQqWD#ZG%JThh$VmyyT3?aPSo%S5YXQqv4wvZyAqOo{VC+rS|RFAMbI zOYRAKF|ht=0Lar=sP(E^kSr0n6~1e^O}B%bKO=TXrBkgr%i?_%%&YKA`1^E(B{WylCbrE86TugULOFoy`De zj5bK!faNyHl0tt9mP7LqM5NeE#%|~jeI!y!O3sG_d#{D|5RW$|RkWyLP-zV3Qmy(5 z1|H@IK)h{r3s(qAbO@`Au??&j0Pv$i6JQ=>6MgS;xJRUZ#|&LuSVKI41+l41KtyDf z{fGj4I|Y9Goh$&{p8_QxF-y{jDD34OYth64hD*dbt1M*MGrh}aH2NpT^mVzpN&%F2 zR2U6@o@v>h+CVbthd{Mnz=52qf_^z0%(8Rn9j&I)G`*B+87Y~zh)tz~rkGCknG~jz zfRGRo6Or#Mf-Ig}f6+L9DEJ(R%q)(SRHG1FNv#mT-f0uwBM_e=m;ykCq~Jq z0S6<)5Z@+GMvFU$rfp#|m6pz9%ffs20F9u~P!%**WXH4)7f)4Zlh4GEkcVPfA(YS` zxkaT8#>)0#$%#3;U5IZb z{~Qk@y@aQ-ZWW@^>yI@;=S7VXu46fL;1}ahC5E^+eudG@=kC}jk7F$Hf?9s}Q#k3x zUtkCJ+Kj2ph$=|3zNocgXxvm1&vzhSy0S+FoJUAg5Fv$&iWl%0vsfdd9ASrEmAq~I z@`YRxNfl32m})h_QVJCkHzwep@f0B^Lk^D|ltFG%_Kk5EG!aVDUyXvARuDwV_oHbm z)NFQP+^2*(3rN4}C6OF|J0JGvLjHNZf1(BnzQ_Vi4ka`uwPYyBj0~G^QJf~CVH;t+ zTB%15L0mEGH*p!TYglK9@C9@z1Yg61Tt&FYJ4)5Hm)l9`!4^u4aAV}RN>V5lFw7+_ zuqZ_FWFnN8E1(w!#8^c2ZW625Xj`GIOsLRPfz_y6~ZS~aZJq| zR;3r62yW}U_OO9a?CkQI-NXwOERNc?R2=1RTDIc$J?E@paMBszaOLmM=+ zii{vJN?5e0Pca>GHH9Cn5Vmj|w9X!)B$Iu2P`HrfNMvN#u)%jJ^*g4FYU-DVdKvSz z?*-W<{mP8EegM1@kkPnih1zkyP$3(a3E`nt1KxY(L0-Rj8f2x>qJlx_fk7o<72L6Z zJ4S^!p$EqyqS8Z`Zs5r^0WV|>gxY4(RiJ``K^XI21ULwdu8ArPii%(WHR?$8iFE>q zko=0-_YdaMqQGJA_?WiC$edV#MOgIgVR{%8s<)6_m=vcO%+Ioy`nQ)FE>k#{a%(~jRy7zzaFZ=p-Ayzq+%c}qeJ^%1&oz~SvAa%CE38V zYDoVOhL87(fEO<>l;8K|(Nqj`!X_(GK$$vhM3N#>8B%NIKuKIyOIR&>I#dDs;OdFu zRSOPW0{sV7B5yjfyf6i2M2mUi4|?f~2Al_$hK<{gUvM2H$D!e9Z<))2t@R?l=Mc(1s8{2^?+*-A>BB)9ltk-o_+=PwdOAsjWt?%Vv$-!?%>C zC=2Tk3^F?XN{gL-7n5s2(TG+Yrv!zLODc-dz-%gEK*Fq#YKvmVlIKSk8ZKT|3GzTN zP{;*}pgs6dF5!$U(v}iX-TtLeB&ImhG+bfCW=Zf$Tqasb-TXCn@roq8;SJeb@wDf% z+Ja!AG?}qO?Ay!{Ey|btQt8Vy{RdR;aDMp+L6v}4n`TP=QQ#q)f^cD^^tHj9wSr1{ zN~V1n7z5&X$?iOJxnDZG1ENP~nNtgo{5>48WU~r&D034O58>Ruh3Q%aPPTw9p z4gwrzmTK$J*|a>QN}d(cEtN@BJ{}hW%GY@Q3yNkLBw)qo!7fO*B-KDijD&FSDFOp7~ukn-*0L>RsFSw8Z7H{_KG1 zO_d7bo$qqgHv@rs`dbkV5DAB*QPF_S&FdFm0rmg@5gg=8BrRxp2+}*Qn>N91NHSBk zHr1X6&J6DWn3Jjx=}r^dh0MDmpG2tvf~ePJi>T)O*gAbhokdWHnG%8qJ2UxwZ+OE= zP@qYGBxE;Tl%4px;GlD+w0py5?dH3{K2Q-E_pVGW&LwCiqa-oLV8U;+$awfsPq<#Q z0G+I3!=t1-Ij=6k(;UnLnL5y@sIcI^t)QIn_p!3eXpkhgdHa0eU>6;0OtT6jc@kh# z*koCb1`9DCa$MR_eh5HOLIy$rhQV&%g(V2-gKOk^&?gIliEhi|T8tq84WVWs*b`f?rD?Uc*RcJHF_cO!Bp{-cgZ_Gec0`G-e}f zL_%t19}dKVaQKIn;-|FtA9#9d6skW}t$c&4AXpaT2!EC+i|GgNCe>SJ(7|3Jghooz zPVroS-*FaCZoM7}d_q6YmJ5!DvnZ{cJ;sMu7R@6u4u} zD-dmr{2Xc6Qo*o;96p?1Oy<8Jt2@9^joT2ml2GCl*j`Sdg$_GEj4?sd!pzbYKH(AJ zQG_EQ4aqOcs4fP{h+BohRpog(?efM=8ol01A|Z{;>hoj>60?NM{qQx{pg0uQf+mOx z*Y5g@Rd_oJ6^kM5Kru|(R>#&$OqzAV(_ykiQ76?^|MX&>NBx- zsR1I$fc(HH>2Sk)6eAF-i@v^kG9?}t2ThmFNak4s4 zjOIm>c!eR0Vw#QhVezGBOezX;w~#-Pp&$u#K&f0dkMrUDe|d47EJ==xUDONnM=V~+ zqE`e^iJ}T8W4x_bjL-GIgC>9!PU(R(B)Y)xf1i=1K+7Ot>uTGm@3N8q)| z;Sqoo3_+^+*ugHk8?-V7M>gufJoRSEp6V6koiEaZEE>h**m{{zLQw1A+@5FX4%lhY zbU$?BcldgHk5il%)8w?{ePoNP!o9}$TW8wx2F zM(D}+;j%1p^Q$38DWP{%Fg-)?Rk;Pdl!t?}_;I{~5u~E8b6DCf3P-6`eZlyLw!`gt zcs1gw!0zw``deSdp9EB1(>UKlaHIwE0}@b7n57X#qy(qx(qZqNt-`0r;bbpzK4G+gsx5T%9XW)x zjB(?oZ5s9)eSyz|Z=>*GH-_@qApJxi!KG;tG3Av=G6&nRFd7GkAvR>XL1>xH+a#udj(x|Gm?l@1x&)@k zqS29R=Flt}P^_vCglH1$0Eh3JR>&jCd5NhbtvQyV-zr{~{up7Z4sWGc-LpwHi>6bG zqpce`KI0(+R!(4B#j6N=Ap^3kj(z}gEl3^WZ7Bg}?hG}_-b69Hw>TE2G=Q;}8LU@v zS~Dt)mBD>8h5ljbN#QJf7D=~q3epwT`pxrV``f1gQBf(T(@$C7Ys(bEx$EW*;?Y%6 z$(Rg+c<@=vW{{?6adIfRaY0@)0*OY!t^EgJUFaO`n@pTSDvKgZKoh({L#abS1FWe8 zlx2o2Gx5GwB8LXJJmh#PgHF5S?%ywMm~4T-7+{OY2v%rll6?=0M}?430Kf2^*$PV73ul*=)BIo-0hB+W4x`x|B^0`1!!{%j zYoBF zA{dM;CxjHT)uv$B+U<1N@CX`qhDl|!Zo(sAnp5i--bCXgX=ZstUv!AcQ)whestM7J z*1yCSWkxWWC*~@a(g!AR0>u+#l=M1Q^=AiqbL%a2V5pBEjyyt<6ZA_8*hSyt!#M0E z)Gr5!!5P`&=Dn91qreGbN;Ad#X}J_^57#IGqB!6WpKq`r2H-VF?`r~VaZu|4S!K{6b6f(+c( z>h0MVWfE6VB)l4ns6-3t5o4VOMoC1Puy^ zt*bR0j8V;ky@_9>aig2SBVrOThQ^fMl-z0=UYL8~((*!~w2~|i4@^KYT9F01W`u5J zO!{RYw887ceso>k5+?|4Kqz)IyFl6FB}gX&p&`m}kweij2vHA-m{l88#n5#r9QFBv z6vp;%UTes0U+#{9^C)0T5D^D!Lb8aqASBWwn+L}&#d1C?4#3Y4ha8WWgA1vMgo8CM z_nMy8UrTn;Ur0L+o%t>U#6GGX<}hSA18p(Im7dx>ytypRO?>(bZ&p8|Ei|wfE?x$L z0WcLl39_gw;Jv_#CP3$QWCorBGYJ*OAPSBw5HpdQ%dxfO4gkALSTIlDBA%CaUnc}4 zt4##Yfa~`X1h6d~Y(qy`%<=@l^y;Gv7|pdI%Rc^&?KlzG0CS{U+ zl^PuPM&hZ3X82&BnFV5yJi5aiM+8?{mAg>u%R(moJwbAB$YwH!mI^uyp2bA*)qG-v zJg)hoFj;Nr&i97w|Gb z0dn959W8D~^92D|xi(M#HW^6@Ds1FbomJ52m~i?mHPr>(R}@L<-osibK~*F$WdM2} zVS}X(oKKDw{&hqG?p;ZJ1%;ql)sl0arv2NqWSL)7G-Cxk?fG_BP4xANc0UdL(#%sFuE~e{;dvap;OrXi3STOP)u_?KHNR` zSJawLW-CQAk^gKILxWK-0Y|WUiCMxTQaYvEVZIGdIO@Q_H*@I7dHy@ma(RNz<&uK; zS8^a%0O18pCaI=uDG8RO6cm{NGui7O`JQ0WEFZ6AjBl17hL~)Q1fXOB`Ui@GuRpMa z!=j*!#a}xlU&EMCgHA?1)PV~fW|HHbh&&ZjCt(~@p6wNVlmm%4X;-H3hw2j=lipqh z*Ce(TCX9n~q+%3mtPga(xrZzk0R`*P<=q3t>4N`UX!zq>Fd>=ih76!g6xlZdTT?*H zPZk3=AE8m1l(&#atgl#VuWj*GKv|X{o^8Y+nFXMUTD1P3`i0`*{h67GD58FTGB`Cq zM`Tzi27wAlBuHi}gl1UA7$u($~~YIG?giWrJp|Lj4S!)=m5+0($b&^}npd$ZKSbOlPj zd+9kj$(Fv?80}0yp%r{NoW;z# zRe2p5kwpV&dp?;OlK+~e?`JK0)LYBmC>E;s`9vWz$UvL}E5e>~V-=aeRq^DM#2^)6 zBXFpwPG%|9Xb--EuGcf*3l$JAKY`+u97ll5>wf8ukS1rbvBEh=C5*_Rz8`@2!(Pp( zq%@8)98!A{i|+_$N|8WC@T&4;tzy*HxZQpWzr5pNcx<3cVP_ zC+sf6ht)#SgXM;s71F2-RGS0~684j~@TZHIO^1|9mj_1UuvyF&ivQ;qY)U``9hzv_ zzz4~|ks%8B=yUcV zTG&tW)Q1)%UUFHGni5Qf#6c>CBusIuKq3)c1d;Doc%TW1kAu-O1Zy#F3rDf+tu?j< zqZLt11>9i^sCgC8E1+i}{p5A=w)02eAW|%aU)wwLkg<+Wpo~tumxl_AwE^oAVW+`( ztds*%-EplR(d~k>Rd0rumz69^9D$TZ81`0*?LAl+247#k1)H}4k}LoT2U!m2V+v*M(L>rc^NY}%xHDJb13TPH#t zGaPA(yafTHd{V$f=K7is3Js%W)+ZNa^pTTnfX>T`!6qDpXCo${37%B1EPKUv4)=;T z+{=kN81kFG&ZCG_V-sW91JeQjPhZa&)#SG9mqJKF3xOa_NvI(p9R>725<&?C2%#uQ zHzJ~FK=CLlgb;c$l+Z*80W`!y5iF<(7!(i{P*l`&1Vlwe1%x9~^u>GLedpf$-g#^7 zwdTiO-+a5w?3vj!dq&faScK%IyymN5_qW7UHYx0Bqp)S@PVT2x>CxRFE}SbVBw&63 zS&h=EnZ02wz3c0-16Y@#q80{QK%8pdHSM52{42Z1f7LeMujUAuIBv4Hm>uNts~hHH#tEAdvg>=v z&DvrYTkT{c&(b+%o7R@QrvKT_&S>CVqI*s&+mP)S;kO5PO*|Cuq~`g#1pH$n9ks)0 z*7jf5>1Oi>Un{tQFq)=IlPC;T+SRHhP360+jX|bdC&%RN4S`Pb+>CTdwapZxaXeLF z7rW@@Ecxf4KkYETP*vr}mX%|9VY=TRW0}UJVs<|Aw&A;&O^&dcw%g?dAPXx=I_aMtYozyzW6XnS(X{SVpD!|>W zoPzhPy7O82nx%~;IM-NJ7NRJ;wwSVWG!wmX_elbLbU#?XZ>a~7eaQ6hnea~rRl}zh zIqD)bs(>$=?6h}tq!rlY=Pvm=c>v4P3z3a`7N72q73$)rMxGz~e1yd+qZw8H(SIBX zhf;R`05IW!RnSn0?2TFTHOe|oyWeN?ER4E03@(YQ4}AhdK9r^r&AK z`~O<*P*DZMMIRrX1K(uhKtX={Oy^@dSn_PhSB}(L*&DbE)CCrkMrv;t8TtpwN;o9n z=sUHs)lg-I&Lt_s++J-^e@#M^ST;R>_z5?-NO_aGCLZ+psArGF?L+&Lkg2fi$s|^@ zj_7Z3kF-fYza%nKSZR?v%e+$YK=T~&=3%K-KIXVAN^zyQaF<3yt(gHvZf~GN^%Pr{ zh$@a974+G;6Nhi7MOx_ya$27Rn;vCEL$9|s>?jTTkLI%@>}eTL+lx@*g3SUFbXV$s z*$mb8IMfwo1}VD!@`+|PZ||d~8fef%%(~-c#+}m$(1^j@>aQJ+U_yKlBN0y#s0|`eXV2y=Vze^js+po?ig#Ajf--noBMW^v_ZPF zZf5lA;7_~a&G4daWi2jG+t`aX3O~JJSNXAdwugo%7AcOnoK)?lU8<`5n<}2S?|@JQ za3JLMhgL#Fq{vgj->utWq&jsS8C7<1_lw^b@HGVA;4_{GEXlb+cd|HZ&fvWa36Bb!P#h7n zG+zfYb3eh$7T)Q@q3?MZNz-9AZ9N(j|H(AZYb2H&t7=2=UZz5ICUGDO(%T4N_;K^| zAHWjVFfUl8lC~0ZfRS{=9cyb#Fm%w?<21E)%;S>$lB^fc(hL5S0JyCSgwbU*+K zs2g}GER6-ZeST*Tgm_Yt1li}akmxg?r7|+4>MvKhvKH)HU`@@@qXp zcBf20WJOfqMB|+1Ipgo#_^a!zww>VC8mL=8xcCZz8KLLb@s=?MT=bGk>aQRg0HpXC z&b)v1(|MEBHBnOCYJuTG%&^ihwunR7xQ;LJDZQS^fy)<7e$T+(e^WUGEseNX4@@;H znHDu@-g)@bGPC7&I_7{|$$aN--!$h=_cEGgmU*xDlsw+0^+j{E8GPA?MpqU@U(($c zTI#?dsuA6nBWaMC-44!HEPX9ZsQHV>#ue(v_BhkI6&k+^^-KLB8C zM9RoBJqpkF+%I%q+J8fOt}GFh8u?uhxz`Q`G$@gY_fX8Wo*cPHV>IHhr0kwt0r4Yu zAyjHEG~zJ<0cG>uF~=>!SJtrs0IUQHyOFf;;Ys7)f`z8zy+Np=CdL8iC!qROFpyNI zOxt?l?Z@C|rhhj_(LKFm79!L2cQKAyKL8AH)vT^Vvav;9$SYn;Ui-50j+`; zklkvNu&9b#X}Geb$a+K>?0erYV9V(tl+xSiAi$d+LICNv?|CbsF)-(T;a`ev+vxxo?qGIl zVvNyHKEpLBp=~ONSQK~fpw0|ArdXuxm(yj%5y+TD5OVa^pyBBgsYp4{3oChc_8;fd z22Mb`oE0C&OB!&S|4^N@tt zvuvrij17PQI{wf|u(R)^QwzUAjo=74C9f`j{rvLg{{cpfznl8Fg0|!PJH8Ze?ocU` zpV6Ip2G(~Dm3RKCF}2b5Gt3K&g5X|ZR05lF(IGPO&m>MN|3o%ZZ`OPujM?!*rB7M9S8{Z@Ob~4pl{P)I*-?IG~9|qb%3XH!U}{}&mdA3?QN*H zAQ`hZ1 z)~vzgQaG`hxPb*(a+XX>lZoFy-8r-4|D$wiU-)9#KRMyZQ%~&eh)J&ls-SDkj~$k@ zV(2fz$0J6EZUXFI>gB_raj<7F$8IqeAnNkqc{0Z9wG;T471W%P-Vdv043W-tfFpsU zSmmTJcx7cy9A!e2VhLsl8%&)pR`@y`*|OaVtq|k07_C29eHES7t598_onfgFozij* z0;;|jCbpnQz~R*q0rG8qb?h-n&W1$y+_XWi3hQ8D2+6A6X--#f?^%yAmob}8#7-_m z|15OF$!Ag2#9C;Y(R7>24sN@(okbQo5*a_RsWSBjBd2l2uA;buH)FC$!4RHYF+*?8 ze74ng%Skzb@iw%~_*qpgrYAJOr+NBH5@L`=^vIK}HpcUigvc&w8wbhxY949HI6l9Ls#u7@&@C0ztJ^_ZBdXYveM) zagoYbaP_^jrjYMWKV{hP4CT=yzgQ#GFfI$t$510zv--2k?`(y^wQ4QLwZbl7)#<2* zHzz3Frh&(5eHhLgEFRBP=>*VHbN?rhXEhf-S4+ZQ)^eRp)3)uLp@*eRuKlTjT?v1= zhl8sHSw?()g+#;`3+{gJ^4)ajTNC0=glam@xJiXzkyZ!#0jLFSAWgHm^0SumG#q`B z9nD}^#VqTsqZArLxXLJ3mOW5}F9Z;uBI^BvWo8f-y3!=L$A2EduOY!^yT4g_=B^&~ z_x2#9lLZFU*A7pd#IZNQuzM!oEfD3W@~$f{rSCpR&;?>74jL9!vO|y(24(bZrrZaL zWE^k2U(*Ft|aRq zWSL!K_!EX}^=~zpavvo>s@IwiC9^#-#0M(}|M7Q=&9dkAn<`54))doIqmnOF^k)y9 z2A66i7vPU+g&iL&wj`s8e>z~)=)$u<03E{vwytD3-tka*t+hN^Po&TEBKSp)p%|%< zsg{5|BH!i9wEph&jW5+9&0#JBoElpfD$A3}EQ=%1T+~wnMCHXro~G|+zpd#s-z zl^2|H=Y!-vCRN_6U}HqO&kdPz2TMZ4al>#2`-ECv+4P0risPPDW`xeC&0QWlZggKd z3iZz_Hq65j{Y0=sJh^(GvKY(dWkT4`2{EkT$fM9(CZ0YEgzRA_`?Z$tC1a${=)BDQ zwd!9id@WJ@U~!u?5%X~sAARfKqHhsMK~%gIwGI2JkW7hOd0qg#S(!002r0O`bmOufXegvwgg#;M1=eOcQ$n9m&^ z=?Zq=)nr#tJwFW!p~op&NU2z#^S#*D!CTuSJ~3*;^_oud$*85BRBu4tZ)1_z2 z2X%=+o2vRS^9ls-S{js!;t~Kv7$i*i(Dj@*QXSMTXvBK1bIq%8IsmgOC!sdpV#3P{ z1IK4HugJh*-6BhM}E%b z^2Y1VBh)Hi2PUXAxlb$!M%uF(K^1b10H%a zqXMu!>q&RzM$zOU+pvb)mcggR(GM2ML-D#z*J;qgU4|E~VhQ(ALBkMQ5wKRWL zKtJ`HCoEMs8SMG(_m=bRhd=@60lJyH0h4x~L2_(fx5pv94rW|YYyh+TJyJ3FgJ zhjq!5SmT^nweY$ig9-MRg=_CD!?v3UF4rFoNV~Z6{h7@V2}x6pL4m(*2E>oqcukbO zsiI7YgI^@N7q44;hAIo2gqZRYBD*bXj>LT+JU_$61P{u9z5BaL6Q1OtQzDL(6xtBV z$Y4j*Yo74Vrc{Vkoq^1ndrCs@Ecs%Ty_?Zl(eSutDcV8jnZC#r+n;vJ+qNp&HG?YP z!8#5+CFi;L^7r06-5vo571 z#@SVt;!c-T!;3EEW7=EwIK`=IM$u|@8u~U+%9zmU0DUsLIQ$j+(e11473_!IXxzHI z!(IBEESYbfe|dqfAq*;v(m1Ez-eB8oF&lUZOW9M9o7&>T)z#5v{A zU>N?%_IqXuiLm4MqW)!k-PCC6*jU5W&k?v2PwgKMcr`u)p+EO-P2d#n#BA6&CVMot z_dE4W3;-E}x*O=*4FAiw{~j|i?&@8myC#Btq9#NsMmVIl#}|~T&-K`LFe*Trq~{q? zLYyjPXWiyO=xrg3Nc0Uie3s{c(j8M*oq6kx`=OiLgG=|67)4xP*BY#?LK`+_YyTDf z@Mypd0vWpFA6;{l?zXPYr|E_!A!Lyvu^4MAQNf4lKI83_s zk#(R-B~(2qZ*(muSlvGAZe5r9qahv|e`;6jX)0*)t(Gq4JcAn8-$ba@P|?mrcemt= z({nPuV5dM~yLM(L_n8DbYjWmN8N<`1Ss`j_r3lB)jxkw001glYbWTxtab9RO8Kj=_ zTS-goIMIK~<@T3bvI~8>uNv$mtcHHP^xm2#q3u#?`O<1b+;Of>-Mvgw-o-E_7w)lB z=n#|`I1@%v?q@$|39)%bn@qbUE|+b+WiqlZdu={zktoU*!ws^x!(?^}*wP|Vf@5Zu zQ?Oo2iGEQ&EwI}O$0U1nN#hbWpV{qx>5Cen|24a+FNS_pUh}Nc*uV!q?f`HdAVOBH zs)7Zw@3swL55*76J+46zHKXa$Y6xeFI+(qj(+uZR$e_DXwvrNZ=?~zvUqiUI#)OQh z;9UOo5q5f8In~02E11@=@xG!e_wIXz_Y%PCC;0EYeMP%A9|a5{n~5o>{Ynj$`5Y(^ zv!hu(B&2~Il2X6;7J1FPQd4|vyP%`RLG3}!LKev#L)!Gs9BeVnxkax15}?9f%^k+(km;%w_VNMWpeq%0B?r@YNL`^B<%C1?SE{ A{r~^~ literal 0 HcmV?d00001 diff --git a/ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/magics.py b/ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/magics.py new file mode 100644 index 000000000..ce06048a1 --- /dev/null +++ b/ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/magics.py @@ -0,0 +1,64 @@ +#Cell Magics +def magic_uppercase(cell_code, **kwargs): + return cell_code.upper() + +def magic_lowercase(cell_code, **kwargs): + return cell_code.lower() + +def evaluate_python(cell_code, **kwargs): + import ast + import io + import contextlib + try: + tree = ast.parse(cell_code) + buf = io.StringIO() + with contextlib.redirect_stdout(buf): + exec(compile(tree, filename="", mode="exec")) + output = buf.getvalue() + return f"Python code executed successfully.\n------Output-----\n {output}" + except SyntaxError as e: + return f"Syntax Error: {str(e)}" + except Exception as e: + return f"Error executing Python code: {str(e)}" + + +#Line Magics +def magic_start(line, **kwargs): + return f"Hello {line}! Welcome to the Pure Kernel." + +def magic_date(line, **kwargs): #If want to add as a line magic + import datetime + t = datetime.datetime.now() + return f"Current date and time: {t.strftime("%Y-%m-%d %H:%M:%S")}" + +def load_csv(line, **kwargs): + import pandas as pd + try: + df = pd.read_csv(line) + return f"Loaded CSV file {line}" + except Exception as e: + return f"Error loading CSV: {str(e)}" + +def show_stats(line, **kwargs): + import pandas as pd + try: + df = pd.read_csv(line) + stats = df.describe().to_string() + return f"Statistics for {line}:\n{stats}" + except Exception as e: + return f"Error showing stats: {str(e)}" + + +# Register magics here +CELL_MAGICS = { + 'uppercase': magic_uppercase, + 'lowercase': magic_lowercase, + 'python': evaluate_python, +} + +LINE_MAGICS = { + 'start': magic_start, + 'date': magic_date, + 'load_csv':load_csv, + 'show_stats':show_stats, +} \ No newline at end of file diff --git a/ILegend-JupyterMultiKernel/Kernel/pyproject.toml b/ILegend-JupyterMultiKernel/Kernel/pyproject.toml new file mode 100644 index 000000000..50e5ff6f5 --- /dev/null +++ b/ILegend-JupyterMultiKernel/Kernel/pyproject.toml @@ -0,0 +1,30 @@ +[build-system] +build-backend = "setuptools.build_meta" +requires = [ + "setuptools>=61.0" +] + +[project] +name = "ilegend_kernel" +version = "0.0.9" +authors = [ { name="Shanmukh raj MSS"} ] +description = "Extensible Jupyter kernel for ILegend" +readme = "README.md" +license = { file="LICENSE" } +requires-python = ">=3.7" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Intended Audience :: Developers", +] +dependencies = [ + "jupyter_client", + "IPython", + "ipykernel", + "graphviz", + "beautifulsoup4", +] + +[project.urls] +"Homepage" = "https://github.com/ShanmukhEstrella/Legend-Jupyter" diff --git a/ILegend-JupyterMultiKernel/Kernel/setup.cfg b/ILegend-JupyterMultiKernel/Kernel/setup.cfg new file mode 100644 index 000000000..3d2836c03 --- /dev/null +++ b/ILegend-JupyterMultiKernel/Kernel/setup.cfg @@ -0,0 +1,10 @@ +[options] +packages = find_namespace: +include_package_data = True + +[options.packages.find] +exclude = + notebooks* + +[options.package_data] +* = *.js, *.json, *.lgd diff --git a/ILegend-JupyterMultiKernel/KernelSwitch-UI/README.md b/ILegend-JupyterMultiKernel/KernelSwitch-UI/README.md new file mode 100644 index 000000000..cd3c93d8b --- /dev/null +++ b/ILegend-JupyterMultiKernel/KernelSwitch-UI/README.md @@ -0,0 +1,15 @@ +### Requirements +- Jupyter Lab >= 4.x.x + + +### Installation +- Clone the repository. +- Run the commands: + ``` + npm install + npm run build + jupyter labextension install . + jupyter lab build + ``` + + diff --git a/ILegend-JupyterMultiKernel/KernelSwitch-UI/package.json b/ILegend-JupyterMultiKernel/KernelSwitch-UI/package.json new file mode 100644 index 000000000..cabe6871e --- /dev/null +++ b/ILegend-JupyterMultiKernel/KernelSwitch-UI/package.json @@ -0,0 +1,22 @@ +{ + "name": "cell-kernel-selector", + "version": "0.1.0", + "description": "Adds a kernel dropdown to every code cell", + "main": "lib/index.js", + "scripts": { + "build": "tsc", + "watch": "tsc -w" + }, + "keywords": ["jupyterlab-extension"], + "jupyterlab": { + "extension": true + }, + "dependencies": { + "@jupyterlab/application": "^4.0.0", + "@jupyterlab/notebook": "^4.0.0", + "@lumino/widgets": "^2.0.0" + }, + "devDependencies": { + "typescript": "~5.0.0" + } +} diff --git a/ILegend-JupyterMultiKernel/KernelSwitch-UI/src/index.ts b/ILegend-JupyterMultiKernel/KernelSwitch-UI/src/index.ts new file mode 100644 index 000000000..a315ffdc9 --- /dev/null +++ b/ILegend-JupyterMultiKernel/KernelSwitch-UI/src/index.ts @@ -0,0 +1,202 @@ +/** + * Inline kernel selector for ILegend notebooks + * ------------------------------------------- + * Adds a dropdown to every code cell so users can switch between the + * ILegend kernel and a Python helper kernel. + */ + +import { + JupyterFrontEnd, + JupyterFrontEndPlugin +} from '@jupyterlab/application'; +import { + INotebookTracker, + NotebookPanel +} from '@jupyterlab/notebook'; +import { CodeCell } from '@jupyterlab/cells'; + +/* ------------------------------------------------------------------ */ +/* helpers */ +/* ------------------------------------------------------------------ */ + +const LEGEND_MIME = 'text/x-ilegend'; +const ILegendDisplayName = 'ILegend'; // ← adjust if needed + +const hasDropdown = (cell: CodeCell) => + !!cell.node.querySelector('.cell-kernel-selector-wrapper'); + +/** Is the notebook’s *current* kernel ILegend? */ +const isILegendKernel = (panel: NotebookPanel): boolean => + (panel.sessionContext.kernelDisplayName ?? '').toLowerCase() === + ILegendDisplayName.toLowerCase(); + +/* ------------------------------------------------------------------ */ +/* extension */ +/* ------------------------------------------------------------------ */ + +const extension: JupyterFrontEndPlugin = { + id: 'cell-kernel-selector', + autoStart: true, + requires: [INotebookTracker], + activate: (_app, tracker) => { + console.log('Inline Kernel Selector Activated'); + + /* ---------- one‑time font & CSS ----------------------------- */ + (() => { + if (document.getElementById('cell‑kernel‑selector‑css')) return; + + const font = Object.assign(document.createElement('link'), { + rel: 'stylesheet', + href: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap' + }); + const style = Object.assign(document.createElement('style'), { + id: 'cell‑kernel‑selector‑css', + textContent: /* css */ ` + select.cell-kernel-selector-dropdown option[value="Python"]{ + color:rgb(37,235,37);font-weight:600; + } + select.cell-kernel-selector-dropdown option[value="Legend"]{ + color:rgb(21,17,224);font-weight:600; + } + .jp-InputArea-editor.has-kernel-dropdown{ + position:relative!important;padding-top:32px!important; + } + .cell-kernel-selector-wrapper{ + position:absolute;top:6px;left:8px;z-index:20; + background:var(--jp-layout-color2); + border:1px solid var(--jp-border-color2); + padding:2px 6px;border-radius:4px; + display:flex;gap:4px;font:500 11px 'Inter',sans-serif; + } + select.cell-kernel-selector-dropdown{ + padding:2px 6px;font:11px 'Inter',sans-serif; + border:1px solid var(--jp-border-color2); + background:var(--jp-layout-color0);max-width:120px;cursor:pointer; + } + ` + }); + document.head.append(font, style); + })(); + + /* ---------- dropdown logic (your original code) ------------- */ + const choices = ['-- Select Kernel --', 'Python', 'Legend'] as const; + const pythonHdr = + "#Code in Python below. Don't Remove this Header!!"; + + const addDropdown = (cell: CodeCell): void => { + if (hasDropdown(cell)) return; + + const host = cell.node.querySelector('.jp-InputArea-editor'); + if (!host) return; + + const sel = document.createElement('select'); + sel.className = 'cell-kernel-selector-dropdown'; + choices.forEach(n => { + const opt = document.createElement('option'); + opt.text = n; + opt.value = n === '-- Select Kernel --' ? '' : n; + sel.appendChild(opt); + }); + + let prog = false; + const apply = (k: 'Python' | 'Legend') => { + const srcLines = cell.model.sharedModel.source + .split('\n') + .filter(l => !l.trim().startsWith('#Kernel:') && l !== pythonHdr); + const newLines = + k === 'Python' + ? ['#Kernel: Python', pythonHdr, ...srcLines] + : [...srcLines]; + + const newSrc = newLines.join('\n'); + if (newSrc === cell.model.sharedModel.source) return; + + prog = true; + cell.model.sharedModel.source = newSrc; + cell.model.mimeType = + k === 'Python' ? 'text/x-python' : LEGEND_MIME; + prog = false; + cell.editor?.setCursorPosition({ + line: k === 'Python' ? 2 : 0, + column: 0 + }); + }; + + const sync = () => { + if (prog) return; + const first = cell.model.sharedModel.source.split('\n')[0]?.toLowerCase(); + sel.value = first === '#kernel: python' ? 'Python' : 'Legend'; + }; + + sel.onchange = () => apply((sel.value || 'Legend') as any); + cell.model.contentChanged.connect(sync); + + const wrap = document.createElement('div'); + wrap.className = 'cell-kernel-selector-wrapper'; + wrap.innerHTML = ''; + wrap.appendChild(sel); + + host.appendChild(wrap); + host.classList.add('has-kernel-dropdown'); + sync(); + }; + + const inject = (panel: NotebookPanel) => + panel.content.widgets.forEach(w => { + if (w.model.type === 'code') addDropdown(w as CodeCell); + }); + + const removeAll = (panel: NotebookPanel) => + panel.content.widgets.forEach(w => { + if (w.model.type !== 'code') return; + w.node + .querySelector('.cell-kernel-selector-wrapper') + ?.remove(); + w.node + .querySelector('.jp-InputArea-editor') + ?.classList.remove('has-kernel-dropdown'); + }); + + /* ---------- tracker hook ----------------------------------- */ + tracker.widgetAdded.connect((_t: INotebookTracker, panel: NotebookPanel) => { + const refresh = () => { + if (isILegendKernel(panel)) { + inject(panel); + } else { + removeAll(panel); + } + }; + + /* run once notebook is ready */ + panel.context.ready.then(refresh); + + /* run on kernel switches */ + panel.sessionContext.kernelChanged.connect(refresh); + + /* add dropdown to new cells only if ILegend kernel active */ + panel.content.model?.cells.changed.connect( + (_list: any, ch: import('@jupyterlab/observables').IObservableList.IChangedArgs) => { + if (!isILegendKernel(panel)) return; + ch.newValues?.forEach(m => { + if (m.type !== 'code') return; + const v = panel.content.widgets.find( + (w: import('@jupyterlab/cells').Cell) => w.model === m + ) as CodeCell | undefined; + v?.ready.then(() => + requestAnimationFrame(() => addDropdown(v)) + ); + }); + } + ); + + /* ensure pasted / active cell also handled */ + panel.content.activeCellChanged.connect(() => { + if (!isILegendKernel(panel)) return; + const c = panel.content.activeCell; + if (c?.model.type === 'code') addDropdown(c as CodeCell); + }); + }); + } +}; + +export default extension; diff --git a/ILegend-JupyterMultiKernel/KernelSwitch-UI/style/index.css b/ILegend-JupyterMultiKernel/KernelSwitch-UI/style/index.css new file mode 100644 index 000000000..234900b73 --- /dev/null +++ b/ILegend-JupyterMultiKernel/KernelSwitch-UI/style/index.css @@ -0,0 +1,5 @@ +.cell-kernel-selector { + margin-left: 8px; + font-size: 12px; + padding: 2px; +} diff --git a/ILegend-JupyterMultiKernel/KernelSwitch-UI/tsconfig.json b/ILegend-JupyterMultiKernel/KernelSwitch-UI/tsconfig.json new file mode 100644 index 000000000..42841dd97 --- /dev/null +++ b/ILegend-JupyterMultiKernel/KernelSwitch-UI/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "es6", + "lib": ["es2020", "dom"], + "moduleResolution": "node", + "outDir": "lib", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["src/**/*"] +} diff --git a/ILegend-JupyterMultiKernel/README.md b/ILegend-JupyterMultiKernel/README.md new file mode 100644 index 000000000..5ac849a4f --- /dev/null +++ b/ILegend-JupyterMultiKernel/README.md @@ -0,0 +1,59 @@ +### DESCRIPTION +- ILegend - Jupyter Kernel for Legend + + +### FEATURES +- Users can query data using ILegend Kernel by using legend language in jupyter notebooks. +- It is Python implementation of Legend REPL. +- Users can load local csv files and operate on them in a similar fashion as remote data sources. +- Users can get full benefit of legend database pushdown for queries being executed. +- Integrated IPython Kernel features into ILegend Kernel, giving capability to users to switch between languages in the same notebook session. +- Query results from legend notebook cells will be avaialbe as pandas dataframes in subsequent python cells to continue further data analysis. +- Syntax highlighting and auto-completion are availabe for both python and legend code within notebooks. Both light and dark theme are supported. + + +### REQUIREMENTS +- JupyterLab >= 4.0.0 + + +### USER INSTALLATION +For ILegend-Kernel to install in your virtual environment, follow the steps: +- Activate your virtual environment +- ``` + pip install ilegend-kernel + python -m ilegend_kernel.install + ``` +For KernelSwitch-UI lab extenstion to install, follow the steps: +- ``` + pip install ilegend-cell-ui + jupyter lab build + ``` +For Legend-SyntaxHighlighting lab extenstion to install, follow the steps: +- ``` + pip install ilegend-syntax-coloring + jupyter lab build + ``` + + +To uninstall ILegend Kernel: +- ``` + pip uninstall ilegend-kernel + jupyter kernelspec remove ilegend-kernel + ``` + + + + +Now you can see the features below!! +You can start using the ILegend Kernel now!! + + +### HOME INTERFACE +image + + +### NOTEBOOK INTERFACE +image + + + diff --git a/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/.gitignore b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/.gitignore new file mode 100644 index 000000000..7ea783c31 --- /dev/null +++ b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/.gitignore @@ -0,0 +1,125 @@ +*.bundle.* +lib/ +node_modules/ +*.log +.eslintcache +.stylelintcache +*.egg-info/ +.ipynb_checkpoints +*.tsbuildinfo +ilegend-coloring/labextension +# Version file is handled by hatchling +ilegend-coloring/_version.py + +# Integration tests +ui-tests/test-results/ +ui-tests/playwright-report/ + +# Created by https://www.gitignore.io/api/python +# Edit at https://www.gitignore.io/?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage/ +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# End of https://www.gitignore.io/api/python + +# OSX files +.DS_Store + +# Yarn cache +.yarn/ diff --git a/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/.prettierignore b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/.prettierignore new file mode 100644 index 000000000..b7fead8e2 --- /dev/null +++ b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/.prettierignore @@ -0,0 +1,6 @@ +node_modules +**/node_modules +**/lib +**/package.json +!/package.json +ilegend-coloring diff --git a/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/.yarnrc.yml b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/.yarnrc.yml new file mode 100644 index 000000000..8b757b29a --- /dev/null +++ b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules \ No newline at end of file diff --git a/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/README.md b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/README.md new file mode 100644 index 000000000..54f85dda5 --- /dev/null +++ b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/README.md @@ -0,0 +1,33 @@ +# ILegend-coloring using CodeMirrorv6 + +A JupyterLab extension. + +## Requirements + +- JupyterLab >= 4.0.0 + +## Install + +To install the extension, execute: + +```bash +yarn install +yarn add --dev rollup rollup-plugin-typescript2 @rollup/plugin-node-resolve @rollup/plugin-commonjs typescript -W +yarn add --dev @rollup/plugin-url -W +yarn build +jupyter labextension install . +jupyter lab build +``` + +To check the extension is loaded or not: + +```bash +jupyter labextension list +``` + +Start working upon: +```bash +jupyter lab +``` + +Note: Jupyter notebook don't have support for the custom syntax highlighting. diff --git a/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/RELEASE.md b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/RELEASE.md new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/RELEASE.md @@ -0,0 +1 @@ + diff --git a/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/babel.config.js b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/babel.config.js new file mode 100644 index 000000000..8b5c76420 --- /dev/null +++ b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/babel.config.js @@ -0,0 +1 @@ +module.exports = require('@jupyterlab/testutils/lib/babel.config'); diff --git a/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/install.json b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/install.json new file mode 100644 index 000000000..1874ffe76 --- /dev/null +++ b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/install.json @@ -0,0 +1,5 @@ +{ + "packageManager": "python", + "packageName": "ilegend-coloring", + "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package ilegend-coloring" +} diff --git a/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/jest.config.js b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/jest.config.js new file mode 100644 index 000000000..b0471e662 --- /dev/null +++ b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/jest.config.js @@ -0,0 +1,28 @@ +const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config'); + +const esModules = [ + '@codemirror', + '@jupyter/ydoc', + '@jupyterlab/', + 'lib0', + 'nanoid', + 'vscode-ws-jsonrpc', + 'y-protocols', + 'y-websocket', + 'yjs' +].join('|'); + +const baseConfig = jestJupyterLab(__dirname); + +module.exports = { + ...baseConfig, + automock: false, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/.ipynb_checkpoints/*' + ], + coverageReporters: ['lcov', 'text'], + testRegex: 'src/.*/.*.spec.ts[x]?$', + transformIgnorePatterns: [`/node_modules/(?!${esModules}).+`] +}; diff --git a/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/package.json b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/package.json new file mode 100644 index 000000000..76f23d6a5 --- /dev/null +++ b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/package.json @@ -0,0 +1,198 @@ +{ + "name": "jupyter-ilegend-coloring", + "version": "4.9.2", + "private": true, + "description": "A JupyterLab extension.", + "keywords": [ + "jupyter", + "jupyterlab", + "jupyterlab-extension" + ], + "homepage": "https://github.com/github_username/jupyter-ilegend-coloring", + "bugs": { + "url": "https://github.com/github_username/jupyter-ilegend-coloring/issues" + }, + "license": "BSD-3-Clause", + "author": { + "name": "Shanmukh raj MSS", + "email": "me@test.com" + }, + "files": [ + "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", + "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}" + ], + "main": "lib/index.js", + "types": "lib/index.d.ts", + "style": "style/index.css", + "repository": { + "type": "git", + "url": "https://github.com/github_username/jupyter-ilegend-coloring.git" + }, + "workspaces": [ + "ui-tests" + ], + "scripts": { + "build:grammar": "lezer-generator src/ilegend.grammar -o src/ilegend.grammar.js", + "build": "yarn build:grammar && rollup -c", + "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", + "build:labextension": "jupyter labextension build .", + "build:labextension:dev": "jupyter labextension build --development True .", + "build:lib": "tsc --sourceMap", + "build:lib:prod": "tsc", + "clean": "jlpm clean:lib", + "clean:lib": "rimraf lib tsconfig.tsbuildinfo", + "clean:lintcache": "rimraf .eslintcache .stylelintcache", + "clean:labextension": "rimraf ilegend-coloring/labextension ilegend-coloring/_version.py", + "clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache", + "eslint": "jlpm eslint:check --fix", + "eslint:check": "eslint . --cache --ext .ts,.tsx", + "install:extension": "jlpm build", + "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", + "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", + "prettier": "jlpm prettier:base --write --list-different", + "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", + "prettier:check": "jlpm prettier:base --check", + "stylelint": "jlpm stylelint:check --fix", + "stylelint:check": "stylelint --cache \"style/**/*.css\"", + "test": "jest --coverage", + "watch": "run-p watch:src watch:labextension", + "watch:src": "tsc -w --sourceMap", + "watch:labextension": "jupyter labextension watch ." + }, + "dependencies": { + "@codemirror/highlight": "^0.19.8", + "@codemirror/language": "6.4.0", + "@codemirror/legacy-modes": "^6.5.1", + "@codemirror/stream-parser": "^0.19.9", + "@codemirror/view": "^6.37.2", + "@jupyterlab/application": "^4.4.3", + "@jupyterlab/apputils": "^4.5.4", + "@jupyterlab/codemirror": "^4.4.3", + "codemirror": "5" + }, + "devDependencies": { + "@jupyterlab/builder": "^4.0.0", + "@jupyterlab/testutils": "^4.0.0", + "@rollup/plugin-commonjs": "^28.0.6", + "@rollup/plugin-node-resolve": "^16.0.1", + "@rollup/plugin-url": "^8.0.2", + "@types/codemirror": "^5.60.16", + "@types/jest": "^29.2.0", + "@types/json-schema": "^7.0.11", + "@types/react": "^18.0.26", + "@typescript-eslint/eslint-plugin": "^5.55.0", + "@typescript-eslint/parser": "^5.55.0", + "css-loader": "^6.7.1", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.7.0", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.2.0", + "npm-run-all": "^4.1.5", + "prettier": "^2.8.7", + "rimraf": "^4.4.1", + "rollup": "^4.44.2", + "rollup-plugin-typescript2": "^0.36.0", + "source-map-loader": "^1.0.2", + "style-loader": "^3.3.1", + "stylelint": "^14.9.1", + "stylelint-config-prettier": "^9.0.4", + "stylelint-config-recommended": "^8.0.0", + "stylelint-config-standard": "^26.0.0", + "stylelint-prettier": "^2.0.0", + "typescript": "^5.8.3", + "yjs": "^13.5.0" + }, + "sideEffects": [ + "style/*.css", + "style/index.js" + ], + "styleModule": "style/index.js", + "publishConfig": { + "access": "public" + }, + "jupyterlab": { + "extension": true, + "outputDir": "ilegend-coloring/labextension" + }, + "eslintIgnore": [ + "node_modules", + "dist", + "coverage", + "**/*.d.ts", + "tests", + "**/__tests__", + "ui-tests" + ], + "eslintConfig": { + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "tsconfig.json", + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint" + ], + "rules": { + "@typescript-eslint/naming-convention": [ + "error", + { + "selector": "interface", + "format": [ + "PascalCase" + ], + "custom": { + "regex": "^I[A-Z]", + "match": true + } + } + ], + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "args": "none" + } + ], + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-namespace": "off", + "@typescript-eslint/no-use-before-define": "off", + "@typescript-eslint/quotes": [ + "error", + "single", + { + "avoidEscape": true, + "allowTemplateLiterals": false + } + ], + "curly": [ + "error", + "all" + ], + "eqeqeq": "error", + "prefer-arrow-callback": "error" + } + }, + "prettier": { + "singleQuote": true, + "trailingComma": "none", + "arrowParens": "avoid", + "endOfLine": "auto" + }, + "stylelint": { + "extends": [ + "stylelint-config-recommended", + "stylelint-config-standard", + "stylelint-prettier/recommended" + ], + "rules": { + "property-no-vendor-prefix": null, + "selector-no-vendor-prefix": null, + "value-no-vendor-prefix": null + } + } +} diff --git a/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/pyproject.toml b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/pyproject.toml new file mode 100644 index 000000000..0897c6b53 --- /dev/null +++ b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/pyproject.toml @@ -0,0 +1,81 @@ +[build-system] +requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0,<5", "hatch-nodejs-version"] +build-backend = "hatchling.build" + +[project] +name = "jupyter-ilegend-coloring" +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.8" +classifiers = [ + "Framework :: Jupyter", + "Framework :: Jupyter :: JupyterLab", + "Framework :: Jupyter :: JupyterLab :: 4", + "Framework :: Jupyter :: JupyterLab :: Extensions", + "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dependencies = [ +] +dynamic = ["version", "description", "authors", "urls", "keywords"] + +[tool.hatch.version] +source = "nodejs" + +[tool.hatch.metadata.hooks.nodejs] +fields = ["description", "authors", "urls"] + +[tool.hatch.build.targets.sdist] +artifacts = ["ilegend-coloring/labextension"] +exclude = [".github", "binder"] + +[tool.hatch.build.targets.wheel] +include = [ + "ilegend_coloring/labextension/**", +] + +[tool.hatch.build.targets.wheel.shared-data] +"ilegend-coloring/labextension" = "share/jupyter/labextensions/jupyter-ilegend-coloring" +"install.json" = "share/jupyter/labextensions/jupyter-ilegend-coloring/install.json" + +[tool.hatch.build.hooks.version] +path = "ilegend-coloring/_version.py" + +[tool.hatch.build.hooks.jupyter-builder] +dependencies = ["hatch-jupyter-builder>=0.5"] +build-function = "hatch_jupyter_builder.npm_builder" +ensured-targets = [ + "ilegend-coloring/labextension/static/style.js", + "ilegend-coloring/labextension/package.json", +] +skip-if-exists = ["ilegend-coloring/labextension/static/style.js"] + +[tool.hatch.build.hooks.jupyter-builder.build-kwargs] +build_cmd = "build:prod" +npm = ["jlpm"] + +[tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] +build_cmd = "install:extension" +npm = ["jlpm"] +source_dir = "src" +build_dir = "ilegend-coloring/labextension" + +[tool.jupyter-releaser.options] +version_cmd = "hatch version" + +[tool.jupyter-releaser.hooks] +before-build-npm = [ + "python -m pip install 'jupyterlab>=4.0.0,<5'", + "jlpm", + "jlpm build:prod" +] +before-build-python = ["jlpm clean:all"] + +[tool.check-wheel-contents] +ignore = ["W002"] diff --git a/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/rollup.config.js b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/rollup.config.js new file mode 100644 index 000000000..e358cdae9 --- /dev/null +++ b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/rollup.config.js @@ -0,0 +1,33 @@ +import typescript from 'rollup-plugin-typescript2'; +import resolve from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; +import url from '@rollup/plugin-url'; // <== add this + +export default { + input: 'src/index.ts', + output: { + file: 'lib/index.js', + format: 'esm', + sourcemap: true + }, + external: [ + '@jupyterlab/application', + '@jupyterlab/codemirror', + '@jupyterlab/apputils', + '@jupyterlab/ui-components', // add this too + '@codemirror/language', + '@lezer/highlight' + ], + plugins: [ + resolve({ + preferBuiltins: true // <== suppresses 'path' warning + }), + commonjs(), + url({ + include: ['**/*.svg', '**/*.png', '**/*.jpg', '**/*.gif'], + limit: 8192, // inline assets under 8 KB + emitFiles: true + }), + typescript({ tsconfig: './tsconfig.json' }) + ] +}; diff --git a/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/setup.py b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/setup.py new file mode 100644 index 000000000..0ae5f59b0 --- /dev/null +++ b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/setup.py @@ -0,0 +1,94 @@ +""" +jupyterlab_ilegend_codemirror_extension setup +""" +import json +import sys +from pathlib import Path + +import setuptools + +HERE = Path(__file__).parent.resolve() + +# Get the package info from package.json +pkg_json = json.loads((HERE / "package.json").read_bytes()) + +# The name of the project +name = "jupyterlab_ilegend_codemirror_extension" + +lab_path = (HERE / pkg_json["jupyterlab"]["outputDir"]) + +# Representative files that should exist after a successful build +ensured_targets = [ + str(lab_path / "package.json"), + str(lab_path / "static/style.js") +] + +labext_name = pkg_json["name"] + +data_files_spec = [ + ("share/jupyter/labextensions/%s" % labext_name, str(lab_path.relative_to(HERE)), "**"), + ("share/jupyter/labextensions/%s" % labext_name, str("."), "install.json"), +] + +long_description = (HERE / "README.md").read_text(encoding="utf8") + +version = ( + pkg_json["version"] + .replace("-alpha.", "a") + .replace("-beta.", "b") + .replace("-rc.", "rc") +) + +setup_args = dict( + name=name, + version=version, + url=pkg_json["homepage"], + author=pkg_json["author"]["name"], + author_email=pkg_json["author"]["email"], + description=pkg_json["description"], + license=pkg_json["license"], + license_file="LICENSE", + long_description=long_description, + long_description_content_type="text/markdown", + packages=setuptools.find_namespace_packages(exclude=['labextension*'],), + zip_safe=False, + include_package_data=True, + python_requires=">=3.7", + platforms="Linux, Mac OS X, Windows", + keywords=["Jupyter", "JupyterLab", "JupyterLab3"], + classifiers=[ + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Framework :: Jupyter", + "Framework :: Jupyter :: JupyterLab", + "Framework :: Jupyter :: JupyterLab :: 3", + "Framework :: Jupyter :: JupyterLab :: Extensions", + "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", + ], +) + +try: + from jupyter_packaging import ( + wrap_installers, + npm_builder, + get_data_files + ) + post_develop = npm_builder( + build_cmd="install:extension", source_dir="src", build_dir=lab_path + ) + setup_args["cmdclass"] = wrap_installers(post_develop=post_develop, ensured_targets=ensured_targets) + setup_args["data_files"] = get_data_files(data_files_spec) +except ImportError as e: + import logging + logging.basicConfig(format="%(levelname)s: %(message)s") + logging.warning("Build tool `jupyter-packaging` is missing. Install it with pip or conda.") + if not ("--name" in sys.argv or "--version" in sys.argv): + raise e + +if __name__ == "__main__": + setuptools.setup(**setup_args) diff --git a/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/src/ilegend.grammar b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/src/ilegend.grammar new file mode 100644 index 000000000..2fbeb4bd0 --- /dev/null +++ b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/src/ilegend.grammar @@ -0,0 +1,28 @@ +@top Program { Statement* } + +Statement { + KeywordStmt | NewOneStmt1 | NewOneStmt2 | NewOneStmt3 | NumberStmt | OperatorStmt | IdentifierStmt | ParensStmt +} + +KeywordStmt { Keyword } +NewOneStmt1 { NewOne1} +NewOneStmt2 { NewOne2} +NewOneStmt3 { NewOne3} +NumberStmt { Number } +OperatorStmt { Operator } +IdentifierStmt { Identifier } +ParensStmt { Parens } + +@skip { whitespace } + +@tokens { + whitespace { "[ \t\r\n]+" } + Keyword { "def" | "print"| "filter" | "groupBy" | "select" | "project" | "from" | "rename" | "pivot" | "join" | "extend" | "distinct" | "concatenate"| "asofjoin" | "drop" | "sort" | "size"} + NewOne1 { "#" | "load" | "db" | "drop_all_tables" | "get_attributes" | "get_tables" | "show_macros" | "clear_macros" | "macro"} + NewOne2 {"local" | "DuckDuckDatabase" | "DuckDuckConnection" | "DuckDuckRuntime"} + NewOne3 {"%put" | "in"} + Number { "[0-9]+" } + Operator { "+" | "-" | "*" | "/" | "=" | "<" | ">" | "!" | "::" | "->" } + Identifier { "[a-zA-Z][a-zA-Z0-9]*" } + Parens { "(" | ")" } +} diff --git a/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/src/ilegend.grammar.d.ts b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/src/ilegend.grammar.d.ts new file mode 100644 index 000000000..2bdd3582f --- /dev/null +++ b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/src/ilegend.grammar.d.ts @@ -0,0 +1,3 @@ +// src/ilegend.grammar.d.ts + import { LRParser } from '@lezer/lr'; + export const parser: LRParser; diff --git a/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/src/ilegend.grammar.js b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/src/ilegend.grammar.js new file mode 100644 index 000000000..9333fd866 --- /dev/null +++ b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/src/ilegend.grammar.js @@ -0,0 +1,16 @@ +// This file was generated by lezer-generator. You probably shouldn't edit it. +import {LRParser} from "@lezer/lr" +export const parser = LRParser.deserialize({ + version: 14, + states: "!pQVQPOOOOQO'#C_'#C_OOQO'#Ca'#CaOOQO'#Cc'#CcOOQO'#Ce'#CeOOQO'#Cg'#CgOOQO'#Ci'#CiOOQO'#Ck'#CkOOQO'#Cm'#CmOOQO'#C^'#C^OOQO'#Co'#CoQVQPOOOOQO-E6m-E6m", + stateData: "q~OfOS~OSPOUQOWROYSO[TO^UO`VObWO~O", + goto: "sdPPeiPiPiPiPiPiPiPiPmTYOZTXOZQZOR[Z", + nodeNames: "⚠ Program Statement KeywordStmt Keyword NewOneStmt1 NewOne1 NewOneStmt2 NewOne2 NewOneStmt3 NewOne3 NumberStmt Number OperatorStmt Operator IdentifierStmt Identifier ParensStmt Parens", + maxTerm: 22, + skippedNodes: [0], + repeatNodeCount: 1, + tokenData: ":e~Rlqr!yst#Ouv#Txy#lyz#lz{!y{|!y}!O#q!P!Q!y![!]#y!^!_!y!_!`!y!`!a!y!f!g$P!}#O(Q#T#U,V#V#W-W#W#X/w#X#Y2p#Y#Z3`#Z#[4_#]#^6i#^#_,o#`#a6o#a#b7b#d#e7z#f#g8|#g#h9f~#OO^~~#TOU~~#WP#d#e#Z~#^P#i#j#a~#dP#h#i#g~#lOY~~#qOb~~#vP^~!`!a!y~#|P![!]!y~$SP#i#j$V~$YP#V#W$]~$`P#_#`$c~$fP!f!g$i~$lP#i#j$o~$rP#V#W$u~$xP#_#`${~%OR!e!f%X!f!g&f!t!u'b~%[P#c#d%_~%bP#b#c%e~%hP#b#c%k~%nP#X#Y%q~%tP#V#W%w~%zP#h#i%}~&QP#]#^&T~&WP#c#d&Z~&^P#b#c&a~&fOW~~&iP#T#U&l~&oP#h#i&r~&uP#T#U&x~&{P#U#V'O~'RP#T#U'U~'XP#g#h'[~'_P#X#Y&a~'eP#i#j'h~'kP#b#c'n~'qP#h#i't~'wP#]#^'z~'}P#a#b'[~(TRpq(^!Q!R)R#T#U)p~(aPXY(d~(gP]^(j~(mPYZ(p~(sP#P#Q(v~(yP{|(|~)ROf~~)UP}!O)X~)[P!Z![)_~)bP#P#Q)e~)hP{|)k~)pO[~~)sP}!O)v~)yP#n#o)|~*PP!c!d*S~*VP}!O*Y~*]P!|!}*`~*cP#P#Q*f~*iP!}#O*l~*oP#T#U*r~*uP}!O*x~*{P#n#o+O~+RP!c!d+U~+XP}!O+[~+_P!|!}+b~+eP!Q!R+h~+kP}!O+n~+qP!Z![+t~+wP#P#Q+z~+}Pz{,Q~,VO`~~,YP#g#h,]~,`P#c#d,c~,fP#Y#Z,i~,lP#^#_,o~,rP#c#d,u~,xP#]#^,{~-OP#b#c-R~-WOS~~-ZQ#`#a-a#c#d.o~-dP#X#Y-g~-jP#T#U-m~-pP#f#g-s~-vP#R#S-y~-|P#a#b.P~.SP#T#U.V~.YP#V#W.]~.`P#f#g.c~.fP#c#d.i~.lP#g#h#O~.rP#b#c.u~.xP#V#W.{~/OP#T#U/R~/UP#h#i/X~/[P#X#Y/_~/bP#b#c/e~/hP#T#U/k~/nP#h#i/q~/tP#X#Y-R~/zS#U#V#O#X#Y0W#]#^0^#f#g1S~0ZP#Y#Z-R~0aP#g#h0d~0gP#h#i0j~0mP#]#^0p~0sP#b#c0v~0yP#V#W0|~1PP#h#i-R~1VP#c#d1Y~1]P#d#e1`~1ePS~#R#S1h~1kP#T#U1n~1qP#`#a1t~1wP#`#a1z~1}P#R#S2Q~2TP#h#i2W~2ZP#T#U2^~2aP#U#V2d~2gP#`#a2j~2mP#X#Y.i~2sP#l#m2v~2yP#h#i2|~3PP#X#Y3S~3VP#b#c3Y~3]P#W#X-R~3cQ#]#^3i#f#g4R~3lP#`#a3o~3rP#h#i3u~3xP#X#Y3{~4OP#f#g-R~4UP#c#d4X~4[P#a#b-R~4bQ#X#Y4h#f#g5y~4kP#h#i4n~4qP#R#S4t~4wQ#T#U4}#h#i2W~5QP#h#i5T~5WP#h#i5Z~5^P#f#g5a~5dP#]#^5g~5jP#U#V5m~5pP#i#j5s~5vP#h#i2j~5|P#c#d6P~6SP#i#j6V~6YP#d#e6]~6`P!d!e6c~6fP#m#n-R~6lP#b#c#g~6rP#c#d6u~6xQ#T#U7O#V#W7U~7RP#W#X#O~7XP#T#U7[~7_P#`#a&a~7eP#T#U7h~7kP#V#W7n~7qP#f#g7t~7wP#c#d#O~7}Q#]#^8T#f#g8a~8WP#j#k8Z~8^P#c#d0|~8dQ#]#^8j#c#d8p~8mP#b#c0|~8sP#^#_8v~8yP#X#Y0v~9PP#X#Y9S~9VP#b#c9Y~9]P#T#U9`~9cP#a#b/q~9iS#X#Y9u#[#]9{#]#^:X#c#d:_~9xP#`#a8v~:OP#c#d:R~:UP#k#l-s~:[P#n#o/q~:bP#f#g0|", + tokenizers: [0], + topRules: {"Program":[0,1]}, + tokenPrec: 0 +}) diff --git a/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/src/ilegend.grammar.terms.js b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/src/ilegend.grammar.terms.js new file mode 100644 index 000000000..605651796 --- /dev/null +++ b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/src/ilegend.grammar.terms.js @@ -0,0 +1,20 @@ +// This file was generated by lezer-generator. You probably shouldn't edit it. +export const + Program = 1, + Statement = 2, + KeywordStmt = 3, + Keyword = 4, + NewOneStmt1 = 5, + NewOne1 = 6, + NewOneStmt2 = 7, + NewOne2 = 8, + NewOneStmt3 = 9, + NewOne3 = 10, + NumberStmt = 11, + Number = 12, + OperatorStmt = 13, + Operator = 14, + IdentifierStmt = 15, + Identifier = 16, + ParensStmt = 17, + Parens = 18 diff --git a/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/src/index.ts b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/src/index.ts new file mode 100644 index 000000000..747daa871 --- /dev/null +++ b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/src/index.ts @@ -0,0 +1,94 @@ +import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; +import { IEditorLanguageRegistry } from '@jupyterlab/codemirror'; +import { IThemeManager } from '@jupyterlab/apputils'; +import { + LRLanguage, + LanguageSupport, + HighlightStyle, + syntaxHighlighting +} from '@codemirror/language'; +import { Tag, styleTags, tags as t } from '@lezer/highlight'; +import { parser } from './ilegend.grammar.js'; + +// --------------------------- +// 1. Custom highlight tags +// --------------------------- +const myNewTag1: Tag = Tag.define(); +const myNewTag2: Tag = Tag.define(); +const myNewTag3: Tag = Tag.define(); +// --------------------------- +// 2. Define ILegend language +// --------------------------- +const ILegendLanguage = LRLanguage.define({ + parser: parser.configure({ + props: [ + styleTags({ + Keyword: t.keyword, + Number: t.number, + NewOne1: myNewTag1, + NewOne2: myNewTag2, + NewOne3: myNewTag3, + Operator: t.operator, + Identifier: t.variableName, + Parens: t.paren + }) + ] + }) +}); + +// --------------------------- + +// 3. Highlight styles +// --------------------------- +const ilegendLightStyle = HighlightStyle.define([ + { tag: myNewTag1, color: '#0c4a87', fontWeight: 'bold' }, + { tag: myNewTag2, color: '#8B4513', fontWeight: 'bold' }, + { tag: myNewTag3, color: '#C11C84', fontWeight: 'bold' }, +]); + +const ilegendDarkStyle = HighlightStyle.define([ + { tag: myNewTag1, color: '#61dafb', fontWeight: 'bold' }, + { tag: myNewTag2, color: '#ffb86c', fontWeight: 'bold' }, + { tag: myNewTag3, color: '#fd3db5', fontWeight: 'bold' }, +]); + +// --------------------------- +// 4. Get LanguageSupport dynamically based on theme +// --------------------------- +function ilegendSupport(isDark: boolean): LanguageSupport { + const style = isDark ? ilegendDarkStyle : ilegendLightStyle; + return new LanguageSupport(ILegendLanguage, [syntaxHighlighting(style)]); +} + +// --------------------------- +// 5. Plugin definition +// --------------------------- +const plugin: JupyterFrontEndPlugin = { + id: 'ilegend-language-plugin', + autoStart: true, + requires: [IEditorLanguageRegistry, IThemeManager], + activate: ( + app: JupyterFrontEnd, + languages: IEditorLanguageRegistry, + themeManager: IThemeManager + ) => { + // Dynamic loader always reflects the current theme + const dynamicLoader = async () => { + const isDark = themeManager.theme?.toLowerCase().includes('dark') ?? false; + console.log(`Loading ILegend language for ${isDark ? 'dark' : 'light'} theme`); + return ilegendSupport(isDark); + }; + + // Register language only once — loader is dynamic + languages.addLanguage({ + name: 'ilegend', + mime: 'text/x-ilegend', + extensions: ['.ilgd'], + load: dynamicLoader + }); + + console.log('ILegend language plugin activated with theme-aware dynamic loader'); + } +}; + +export default plugin; diff --git a/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/src/style/index.css b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/src/style/index.css new file mode 100644 index 000000000..5b7e38377 --- /dev/null +++ b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/src/style/index.css @@ -0,0 +1,37 @@ +/* src/style/index.css */ + +/* Standard CodeMirror token styles */ +.cm-atom { + color: #d19a66; +} + +.cm-builtin { + color: #61afef; + font-weight: bold; +} + +.cm-comment { + color: #7f848e; + font-style: italic; +} + +.cm-string { + color: #98c379; +} + +.cm-number { + color: #dcdcaa; +} + +.cm-operator { + color: #c678dd; +} + +.cm-variable-2 { + color: #e5c07b; +} + +.cm-error { + color: #e06c75; + text-decoration: underline wavy red; +} diff --git a/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/style/base.css b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/style/base.css new file mode 100644 index 000000000..e11f45777 --- /dev/null +++ b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/style/base.css @@ -0,0 +1,5 @@ +/* + See the JupyterLab Developer Guide for useful CSS Patterns: + + https://jupyterlab.readthedocs.io/en/stable/developer/css.html +*/ diff --git a/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/style/index.css b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/style/index.css new file mode 100644 index 000000000..e98119b5b --- /dev/null +++ b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/style/index.css @@ -0,0 +1 @@ +@import 'base.css'; diff --git a/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/style/index.js b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/style/index.js new file mode 100644 index 000000000..a028a7640 --- /dev/null +++ b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/style/index.js @@ -0,0 +1 @@ +import './base.css'; diff --git a/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/tsconfig.json b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/tsconfig.json new file mode 100644 index 000000000..0b790fdfc --- /dev/null +++ b/ILegend-JupyterMultiKernel/SyntaxHighlighting-UI/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "composite": true, + "declaration": true, + "esModuleInterop": true, + "incremental": true, + "jsx": "react", + "module": "esnext", + "moduleResolution": "node", + "noEmitOnError": true, + "noImplicitAny": true, + "noUnusedLocals": true, + "preserveWatchOutput": true, + "resolveJsonModule": true, + "outDir": "lib", + "rootDir": "src", + "strict": true, + "strictNullChecks": true, + "target": "es2020", + "types": ["jest"] + }, + "include": ["src/**/*"] +} From 80178ee4f41e5609095f53d1b50554dfc6081d17 Mon Sep 17 00:00:00 2001 From: Shanmukh raj MSS <133531695+ShanmukhEstrella@users.noreply.github.com> Date: Fri, 18 Jul 2025 16:30:30 +0530 Subject: [PATCH 2/7] Update LICENSE --- ILegend-JupyterMultiKernel/Kernel/LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ILegend-JupyterMultiKernel/Kernel/LICENSE b/ILegend-JupyterMultiKernel/Kernel/LICENSE index 2a8087f74..a6eb512fa 100644 --- a/ILegend-JupyterMultiKernel/Kernel/LICENSE +++ b/ILegend-JupyterMultiKernel/Kernel/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2022 Anne Brecklinghaus, Michael Leuschel, dgelessus +Copyright (c) 2025 Shanmukh raj MSS Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From e3a1b6b7512fa1c003f88e303c065cdd827811e3 Mon Sep 17 00:00:00 2001 From: Shanmukh raj MSS <133531695+ShanmukhEstrella@users.noreply.github.com> Date: Fri, 18 Jul 2025 18:22:01 +0530 Subject: [PATCH 3/7] Update kernel.py --- .../Kernel/ilegend_kernel/kernel.py | 82 +++++++++++++++++-- 1 file changed, 75 insertions(+), 7 deletions(-) diff --git a/ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/kernel.py b/ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/kernel.py index 692efdcd3..188c06a7e 100644 --- a/ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/kernel.py +++ b/ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/kernel.py @@ -501,6 +501,75 @@ def show_running_time(): 'payload': [], 'user_expressions': {} } + + + + + elif code.strip().startswith("complete"): + from IPython.display import HTML + import threading, time + from IPython.display import clear_output + stop_event = threading.Event() + def show_running_time(): + start = time.time() + while not stop_event.is_set(): + elapsed = time.time() - start + s = HTML(f"
Getting suggestions... {elapsed:.2f} seconds elapsed\n
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + self.send_response(self.iopub_socket, 'clear_output', {'wait': True}) + time.sleep(0.01) + s = HTML(f"
Suggestions fetched in - {elapsed:.2f}s\n
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + timer_thread = threading.Thread(target=show_running_time) + timer_thread.start() + try: + response = requests.post("http://127.0.0.1:9095/api/server/execute", json={"line": magic_line}) + finally: + stop_event.set() + timer_thread.join() + output = response.text + if(output.startswith("Invalid")): + s = HTML(f"
Error: {output}
") + self.send_response(self.iopub_socket, + 'display_data', + { + 'data': { + 'text/html': str(s.data) + }, + 'metadata': {} + } + ) + return { + 'status': 'error', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + stream_content = {'name': 'stdout', 'text': output} + self.send_response(self.iopub_socket, 'stream', stream_content) + return { + 'status': 'ok', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {} + } + @@ -1820,14 +1889,13 @@ def do_complete(self, code, cursor_pos): else: suggestions = ["load ", "db ", "#>"] if code is None or code.strip() == "": - start = len(prefix) return { - 'matches': suggestions, - 'cursor_start': start, - 'cursor_end': start, - 'metadata': {}, - 'status': 'ok' - } + 'matches': suggestions, + 'cursor_start': 0, + 'cursor_end': 0, + 'metadata': {}, + 'status': 'ok' + } prefix = code[:cursor_pos] tokens = prefix.strip().split() if tokens and tokens[0] == "load" and prefix.rstrip() == "load": From 10d21edb0a72cf6a0320adf5ffaf92db48faed39 Mon Sep 17 00:00:00 2001 From: Shanmukh raj MSS <133531695+ShanmukhEstrella@users.noreply.github.com> Date: Sun, 24 Aug 2025 01:33:38 +0530 Subject: [PATCH 4/7] Add files via upload --- ILegend-JupyterMultiKernel/Demo.ipynb | 317 ++++++++++++++++++++++++++ 1 file changed, 317 insertions(+) create mode 100644 ILegend-JupyterMultiKernel/Demo.ipynb diff --git a/ILegend-JupyterMultiKernel/Demo.ipynb b/ILegend-JupyterMultiKernel/Demo.ipynb new file mode 100644 index 000000000..7b2f6589e --- /dev/null +++ b/ILegend-JupyterMultiKernel/Demo.ipynb @@ -0,0 +1,317 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "5469a760-15dd-4de3-9142-6ed6a8e2ba7a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Total Time Taken - 5.84s\n", + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loaded into table: 'test3'. RowCount: 100" + ] + } + ], + "source": [ + "load ~/Documents/CSV/startups.csv local::DuckDuckConnection" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "f0495a36-3743-456f-a108-b8258deb69e0", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Total Time Taken - 3.59s\n", + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
Database: local::DB
\n", + "
Table: sample__sport
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
Column NameType
AthleteVARCHAR(0)
AgeBIGINT
CountryVARCHAR(0)
YearBIGINT
DateDATE
SportVARCHAR(0)
GoldBIGINT
SilverBIGINT
BronzeBIGINT
\n", + "
Table: sample__tree
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
Column NameType
cityVARCHAR(0)
countryVARCHAR(0)
yearBIGINT
treeBIGINT
\n", + "
Table: test3
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
Column NameType
CompanyVARCHAR(0)
SectorVARCHAR(0)
StageVARCHAR(0)
HeadQuarterVARCHAR(0)
DescriptionVARCHAR(0)
"TurnOver($)"DOUBLE
YearBIGINT
\n", + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "db local::DuckDuckConnection" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "388a9dcb-02c2-4088-96f2-5706d1704722", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Total Time Taken - 2.52s\n", + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "\n", + " \n", + " " + ], + "text/plain": [ + " Company Sector ... TurnOver($) Year\n", + "0 Eunimart E-Commerce ... 3.380110e+07 2018\n", + "1 Hyperdata.IO Marketing ... 4.555556e+06 2018\n", + "2 Recordent FinTech ... 6.972143e+08 2020\n", + "3 Super Surfaces Design ... 5.000000e+05 2020\n", + "4 Freyr Energy Renewable Energy ... 2.000000e+06 2021\n", + "5 Eunimart E-Commerce ... 7.000000e+05 2021\n", + "6 Sreyas Holistic Remedies Med Tech ... 1.000000e+06 2020\n", + "7 Codingal EdTech ... 5.600000e+05 2020\n", + "8 Vistaar Finance Financial Services ... 3.000000e+07 2020\n", + "9 Quintype Content Marketing ... 3.500000e+06 2020\n", + "10 Cityflo Travel/Transport ... 7.000000e+06 2020\n", + "\n", + "[11 rows x 7 columns]" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "#>{local::DuckDuckDatabase.test3}#->\n", + "filter(x|$x.HeadQuarter=='Hyderabad')->from(local::DuckDuckRuntime)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "1cc1070b-de83-4259-b048-e40ab31672d1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Total Time Taken - 2.74s\n", + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
DataFrame variable 'my_df' stored in the Python Environment
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "#>{local::DuckDuckDatabase.test3}#->filter(x|$x.HeadQuarter=='Hyderabad')->from(local::DuckDuckRuntime) --var my_df" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "499fbbc7-64d8-4fde-ab43-6641e7120488", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
CompanySectorStageHeadQuarterDescriptionTurnOver($)Year
0EunimartE-CommerceSeedHyderabadEunimart is a one stop solution for merchants ...3.380110e+072018
1Hyperdata.IOMarketingAngelHyderabadHyperdata combines advanced machine learning w...4.555556e+062018
2RecordentFinTechSeedHyderabadA technology-enabled platform that helps MSMEs...6.972143e+082020
3Super SurfacesDesignSeries AHyderabadA design solution based company5.000000e+052020
4Freyr EnergyRenewable EnergyUndisclosedHyderabadFreyr Energy is a company that provides full s...2.000000e+062021
\n", + "
" + ], + "text/plain": [ + " Company Sector Stage HeadQuarter \\\n", + "0 Eunimart E-Commerce Seed Hyderabad \n", + "1 Hyperdata.IO Marketing Angel Hyderabad \n", + "2 Recordent FinTech Seed Hyderabad \n", + "3 Super Surfaces Design Series A Hyderabad \n", + "4 Freyr Energy Renewable Energy Undisclosed Hyderabad \n", + "\n", + " Description TurnOver($) Year \n", + "0 Eunimart is a one stop solution for merchants ... 3.380110e+07 2018 \n", + "1 Hyperdata combines advanced machine learning w... 4.555556e+06 2018 \n", + "2 A technology-enabled platform that helps MSMEs... 6.972143e+08 2020 \n", + "3 A design solution based company 5.000000e+05 2020 \n", + "4 Freyr Energy is a company that provides full s... 2.000000e+06 2021 " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "#Kernel: Python\n", + "#Code in Python below. Don't Remove this Header!!\n", + "my_df.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d5d9af73-ae1e-447c-86d8-cf8448ab0065", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "ILegend", + "language": "ilegend", + "name": "ilegend_kernel" + }, + "language_info": { + "codemirror_mode": "ilegend", + "file_extension": ".ilgd", + "mimetype": "text/x-ilegend", + "name": "ilegend" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 26611138f3dd91a6f1402506137da922c8ac3469 Mon Sep 17 00:00:00 2001 From: Shanmukh raj MSS <133531695+ShanmukhEstrella@users.noreply.github.com> Date: Sun, 24 Aug 2025 01:35:45 +0530 Subject: [PATCH 5/7] Create README.md --- ILegend-JupyterMultiKernel/Server/README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 ILegend-JupyterMultiKernel/Server/README.md diff --git a/ILegend-JupyterMultiKernel/Server/README.md b/ILegend-JupyterMultiKernel/Server/README.md new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/ILegend-JupyterMultiKernel/Server/README.md @@ -0,0 +1 @@ + From 0f036273662321e713eee7f97233328c6a685623 Mon Sep 17 00:00:00 2001 From: Shanmukh raj MSS <133531695+ShanmukhEstrella@users.noreply.github.com> Date: Sun, 24 Aug 2025 01:37:41 +0530 Subject: [PATCH 6/7] Add files via upload --- .../Server/DataServer.java | 314 +++++++++++++ .../Server/LoadController.java | 434 ++++++++++++++++++ .../Server/PyLegendSqlServer.java | 150 ++++++ .../Server/ServerState.java | 43 ++ 4 files changed, 941 insertions(+) create mode 100644 ILegend-JupyterMultiKernel/Server/DataServer.java create mode 100644 ILegend-JupyterMultiKernel/Server/LoadController.java create mode 100644 ILegend-JupyterMultiKernel/Server/PyLegendSqlServer.java create mode 100644 ILegend-JupyterMultiKernel/Server/ServerState.java diff --git a/ILegend-JupyterMultiKernel/Server/DataServer.java b/ILegend-JupyterMultiKernel/Server/DataServer.java new file mode 100644 index 000000000..399623928 --- /dev/null +++ b/ILegend-JupyterMultiKernel/Server/DataServer.java @@ -0,0 +1,314 @@ +package org.finos.legend.pylegend; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sun.net.httpserver.Authenticator; +import org.eclipse.collections.api.RichIterable; +import org.finos.legend.engine.language.pure.compiler.toPureGraph.PureModel; +import org.finos.legend.engine.language.pure.grammar.from.RelationalGrammarParserExtension; +import org.finos.legend.engine.language.pure.grammar.to.PureGrammarComposer; +import org.finos.legend.engine.language.pure.grammar.to.PureGrammarComposerContext; +import org.finos.legend.engine.plan.execution.api.result.ResultManager; +import org.finos.legend.engine.plan.execution.result.ConstantResult; +import org.finos.legend.engine.plan.execution.result.Result; +import org.finos.legend.engine.plan.execution.result.serialization.SerializationFormat; +import org.finos.legend.engine.plan.execution.stores.relational.result.RelationalResult; +import org.finos.legend.engine.plan.generation.PlanGenerator; +import org.finos.legend.engine.plan.generation.transformers.LegendPlanTransformers; +import org.finos.legend.engine.protocol.pure.v1.model.context.PureModelContextData; +import org.finos.legend.engine.protocol.pure.v1.model.executionPlan.SingleExecutionPlan; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.store.relational.model.Database; +import org.finos.legend.engine.pure.code.core.PureCoreExtensionLoader; +import org.finos.legend.engine.shared.core.identity.Identity; +import org.finos.legend.engine.shared.core.kerberos.SubjectTools; +import org.finos.legend.engine.shared.core.operational.logs.LoggingEventType; +import org.finos.legend.pure.generated.Root_meta_pure_executionPlan_ExecutionPlan; +import org.finos.legend.pure.generated.Root_meta_pure_extension_Extension; +import org.jline.reader.Candidate; +import org.jline.reader.LineReader; +import org.eclipse.collections.api.factory.Lists; +import org.eclipse.collections.api.factory.Maps; +import org.finos.legend.engine.plan.execution.PlanExecutor; +import org.finos.legend.engine.plan.execution.stores.relational.config.RelationalExecutionConfiguration; +import org.finos.legend.engine.plan.execution.stores.relational.config.TemporaryTestDbConfiguration; +import org.finos.legend.engine.plan.execution.stores.relational.connection.authentication.strategy.OAuthProfile; +import org.finos.legend.engine.plan.execution.stores.relational.connection.manager.ConnectionManagerSelector; +import org.finos.legend.engine.plan.execution.stores.relational.plugin.Relational; +import org.finos.legend.engine.plan.execution.stores.relational.plugin.RelationalStoreExecutor; +import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.store.relational.connection.RelationalDatabaseConnection; +import java.util.Collections; +import org.finos.legend.engine.repl.client.jline3.JLine3Completer; +import org.finos.legend.engine.repl.client.jline3.JLine3Parser; +import org.finos.legend.engine.repl.core.legend.LegendInterface; +import org.finos.legend.engine.repl.core.legend.LocalLegendInterface; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.*; +import org.finos.legend.engine.repl.client.Client; +import java.sql.*; +import org.finos.legend.engine.repl.dataCube.DataCubeReplExtension; +import org.finos.legend.engine.repl.relational.RelationalReplExtension; +import org.finos.legend.engine.repl.relational.autocomplete.RelationalCompleterExtension; +import org.finos.legend.engine.repl.relational.shared.ConnectionHelper; +import org.finos.legend.engine.shared.core.ObjectMapperFactory; +import org.jline.reader.LineReaderBuilder; +import java.sql.Statement; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + + + + +@Path("/server") +public class DataServer +{ + private static final ObjectMapper OBJECT_MAPPER = ObjectMapperFactory.getNewStandardObjectMapperWithPureProtocolExtensionSupports(); + private static final LegendInterface REPL_INTERFACE = new LocalLegendInterface(); + private static PlanExecutor PLAN_EXECUTOR; + private static ConnectionManagerSelector CONNECTION_MANAGER; + private static Client CLIENT; + private static JLine3Parser PARSER ; + private static JLine3Completer COMPLETER ; + private static LineReader LINE_READER; + private static final Map MACROS = new HashMap<>(); + + private static PlanExecutor buildPlanExecutor() + { + RelationalExecutionConfiguration relationalExecutionConfiguration = RelationalExecutionConfiguration.newInstance() + .withTempPath(System.getProperty("TMP","/tmp/")) + .withTemporaryTestDbConfiguration(new TemporaryTestDbConfiguration(9095)) + .build(); + RelationalStoreExecutor relationalStoreExecutor = (RelationalStoreExecutor) Relational.build(relationalExecutionConfiguration); + CONNECTION_MANAGER = relationalStoreExecutor.getStoreState().getRelationalExecutor().getConnectionManager(); + return PlanExecutor.newPlanExecutorBuilder().withStoreExecutors(relationalStoreExecutor).build(); + + } + private static Result executeLine(String txt) + { + String expanded = expandMacros(txt); + String code = "###Pure\n" + + "function repl::__internal__::run():Any[*]\n{\n" + expanded + ";\n}"; + PureModelContextData d = CLIENT.getModelState().parseWithTransient(code); + PureModel pureModel = REPL_INTERFACE.compile(d); + RichIterable extensions = PureCoreExtensionLoader.extensions().flatCollect(e->e.extraPureCoreExtensions(pureModel.getExecutionSupport())); + + Root_meta_pure_executionPlan_ExecutionPlan plan = REPL_INTERFACE.generatePlan(pureModel,false); + String planStr = PlanGenerator.serializeToJSON(plan,"vX_X_X",pureModel,extensions, LegendPlanTransformers.transformers); + + Result res = PLAN_EXECUTOR.execute((SingleExecutionPlan) PlanExecutor.readExecutionPlan(planStr),Maps.mutable.empty(),"user", Identity.getAnonymousIdentity()); + if(res instanceof RelationalResult || res instanceof ConstantResult) + { + return res; + } + throw new RuntimeException(res.getClass() + "result type not supported"); + } + private static final Pattern MACRO_PATTERN = Pattern.compile("macro\\{(.*?)\\}"); + private static String expandMacros(String code) + { + Matcher matcher = MACRO_PATTERN.matcher(code); + StringBuffer result = new StringBuffer(); + while (matcher.find()) + { + String macroKey = matcher.group(1); + String replacement = MACROS.getOrDefault(macroKey, "macro{" + macroKey + "}"); + matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); + } + matcher.appendTail(result); + return result.toString(); + } + + @POST + @Path("/execute") + public Response execute(@Context HttpServletRequest request, @Context HttpServletResponse response) throws Exception + { + RelationalReplExtension relationalReplExtension = new RelationalReplExtension(); + PLAN_EXECUTOR = buildPlanExecutor(); + CLIENT = new Client(Lists.mutable.with(relationalReplExtension,new DataCubeReplExtension()), Lists.mutable.with(new RelationalCompleterExtension()),PLAN_EXECUTOR); +// CLIENT.commands.add(0,new LoadProjectCommand(CLIENT)); + PARSER = new JLine3Parser(); + COMPLETER = new JLine3Completer(CLIENT.commands); + LINE_READER = (LineReader) LineReaderBuilder.builder().parser(PARSER).completer(COMPLETER).build(); + try + { + Map payload = OBJECT_MAPPER.readValue(request.getInputStream(), new TypeReference>(){}); + String line = (String) payload.get("line"); + if (line.trim().startsWith("macro ")) + { + Pattern macroPattern = Pattern.compile("macro\\s+(\\w+)\\s*=\\s*(.+);?"); + Matcher matcher = macroPattern.matcher(line); + if (matcher.matches()) + { + String name = matcher.group(1); + String value = matcher.group(2).trim(); + MACROS.put(name, value); + return Response.ok("Macro '" + name + "' defined.").build(); + } + else + { + return Response.status(400).entity("Invalid macro format. Use: macro name = value;").build(); + } + } + else if (line.trim().startsWith("show_macros")) + { + StringBuilder sb = new StringBuilder("Defined Macros:\n\n"); + for (Map.Entry entry : MACROS.entrySet()) + { + sb.append("macro{").append(entry.getKey()).append("} = ").append(entry.getValue()).append("\n\n"); + } + return Response.ok(sb.toString()).build(); + } + else if (line.trim().startsWith("clear_macros")) + { + MACROS.clear(); + return Response.ok("All macros cleared.").build(); + } + if(line.startsWith("load ")) + { + String[] tokens = line.split(" "); + if(tokens.length < 3 || tokens.length > 4) + { + //throw new RuntimeException(String.format("Error, load should be used as '%s",CLIENT.commands.selectInstancesOf(Load.class).get(0).documentation)); + throw new RuntimeException(String.format("Error in using the load command")); + } + RelationalDatabaseConnection databaseConnection = (RelationalDatabaseConnection) org.finos.legend.engine.repl.relational.shared.ConnectionHelper.getDatabaseConnection(CLIENT.getModelState().parse(),tokens[2]); + try(Connection connection = org.finos.legend.engine.repl.relational.shared.ConnectionHelper.getConnection(databaseConnection,PLAN_EXECUTOR)) + { + String tableName = tokens.length == 4 ? tokens[3] : ("test"+(ConnectionHelper.getTables(databaseConnection,PLAN_EXECUTOR).count()+1)); + int rowCount = 0; + try(Statement statement = connection.createStatement()) + { + statement.executeUpdate(org.finos.legend.engine.plan.execution.stores.relational.connection.driver.DatabaseManager.fromString(databaseConnection.type.name()).relationalDatabaseSupport().load(tableName, tokens[1])); + try (ResultSet rs = statement.executeQuery("select count(*) as cnt from " + tableName)) { + rs.next(); + rowCount = rs.getInt(1); + } + } + return Response.ok("Loaded into table: '" + tableName + "'. RowCount: "+rowCount).build(); + } + } + else if(line.startsWith("db")) + { + String[] tokens = line.split(" "); + if(tokens.length!=2) throw new RuntimeException("db should be used as 'db '"); + RelationalDatabaseConnection databaseConnection = ConnectionHelper.getDatabaseConnection(CLIENT.getModelState().parse(),tokens[1]); + Database database = ConnectionHelper.getDatabase(databaseConnection, "local","DB",PLAN_EXECUTOR); + return Response.ok(PureGrammarComposer.newInstance(PureGrammarComposerContext.Builder.newInstance().build()).render(database, RelationalGrammarParserExtension.NAME)).build(); + } + else if (line.startsWith("drop_all_tables ")) + { + String[] tokens = line.split(" "); + if (tokens.length != 2) + { + throw new RuntimeException("drop_all_tables should be used as 'drop_all_tables '"); + } + String connectionName = tokens[1]; + RelationalDatabaseConnection databaseConnection = ConnectionHelper.getDatabaseConnection(CLIENT.getModelState().parse(), connectionName); + try (Connection connection = ConnectionHelper.getConnection(databaseConnection, PLAN_EXECUTOR)) + { + List tableNames = ConnectionHelper.getTables(databaseConnection, PLAN_EXECUTOR) + .map(t -> t.name) + .collect(Collectors.toList()); + try (Statement stmt = connection.createStatement()) + { + for (String table : tableNames) + { + stmt.executeUpdate("DROP TABLE IF EXISTS " + table); + } + } + return Response.ok("Dropped tables: " + String.join(", ", tableNames)).build(); + } + } + else if (line.startsWith("get_tables")) + { + String[] tokens = line.split(" "); + if (tokens.length != 2) + { + throw new RuntimeException("Usage: get_tables "); + } + String connectionName = tokens[1]; + RelationalDatabaseConnection databaseConnection = ConnectionHelper.getDatabaseConnection(CLIENT.getModelState().parse(), connectionName); + List tableNames = ConnectionHelper.getTables(databaseConnection, PLAN_EXECUTOR) + .map(t -> t.name) + .collect(Collectors.toList()); + return Response.ok(Collections.singletonMap("tables", tableNames)).type(MediaType.APPLICATION_JSON).build(); + } + else if (line.startsWith("get_attributes")) + { + String fullRef = line.substring("get_attributes".length()).trim(); + int dotIndex = fullRef.lastIndexOf('.'); + if (dotIndex == -1) + { + throw new RuntimeException("Usage: get_attributes ."); + } + String connectionName = fullRef.substring(0, dotIndex); + String tableName = fullRef.substring(dotIndex + 1); + RelationalDatabaseConnection databaseConnection = + ConnectionHelper.getDatabaseConnection(CLIENT.getModelState().parse(), connectionName); + List tables = ConnectionHelper.getTables(databaseConnection, PLAN_EXECUTOR) + .collect(Collectors.toList()); + Object matchingTable = tables.stream() + .filter(t -> { + try { + java.lang.reflect.Field nameField = t.getClass().getField("name"); + String name = (String) nameField.get(t); + return name.equalsIgnoreCase(tableName); + } catch (Exception e) { + return false; + } + }) + .findFirst() + .orElseThrow(() -> new RuntimeException("Table '" + tableName + "' not found in connection '" + connectionName + "'")); + List attributeNames = new ArrayList<>(); + try { + java.lang.reflect.Field columnsField = matchingTable.getClass().getField("columns"); + List columns = (List) columnsField.get(matchingTable); + + for (Object col : columns) + { + java.lang.reflect.Field colNameField = col.getClass().getField("name"); + String colName = (String) colNameField.get(col); + attributeNames.add(colName); + } + } + catch (Exception e) + { + throw new RuntimeException("Error extracting column names: " + e.getMessage(), e); + } + return Response.ok(Collections.singletonMap("attributes", attributeNames)) + .type(MediaType.APPLICATION_JSON) + .build(); + } + + else if (line.startsWith("get_all")) + { + PureModelContextData modelContextData = CLIENT.getModelState().parse(); + List connectionNames = modelContextData.getElements().stream() + .map(e -> e.name) + .collect(Collectors.toList()); + return Response.ok(Collections.singletonMap("connections", connectionNames)).type(MediaType.APPLICATION_JSON).build(); + } + else if(line.startsWith("complete")) + { + List candidates = Lists.mutable.empty(); + String[] words = line.trim().split("\\s+"); + String line_new = words.length >= 2 ? words[1] : ""; + COMPLETER.complete(LINE_READER,PARSER.parse(line_new,line_new.length(),null),candidates); + return Response.ok(Collections.singletonMap("completions",candidates.stream().map(Candidate::displ).distinct().collect(Collectors.toList()))).type(MediaType.APPLICATION_JSON).build(); + } + else + { + CLIENT.addCommandToHistory(line); + Result res = executeLine(line); + return ResultManager.manageResult("unknown",res, SerializationFormat.RAW, LoggingEventType.EXECUTE_INTERACTIVE_STOP); + } + } + catch (Exception e) + { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(Collections.singletonMap("error", e.getMessage())).type(MediaType.APPLICATION_JSON).build(); + } + } +} diff --git a/ILegend-JupyterMultiKernel/Server/LoadController.java b/ILegend-JupyterMultiKernel/Server/LoadController.java new file mode 100644 index 000000000..788fadd12 --- /dev/null +++ b/ILegend-JupyterMultiKernel/Server/LoadController.java @@ -0,0 +1,434 @@ +package org.finos.legend.pylegend; +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.io.File; +import java.util.*; +import java.util.stream.Collectors; +import java.sql.*; +import java.sql.Statement; +@Path("/data") +public class LoadController +{ + @POST + @Path("/createtable") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.APPLICATION_JSON) + public Response loadTable(String payload) + { + if (!payload.startsWith("create ")) + { + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "Payload must start with 'create '")).build(); + } + String rest = payload.substring(6).trim(); + int firstParen = rest.indexOf('('); + int lastParen = rest.lastIndexOf(')'); + if (firstParen == -1 || lastParen == -1 || lastParen <= firstParen) + { + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "Missing or malformed schema declaration")).build(); + } + String pathPart = rest.substring(0, firstParen).trim(); + String schemaPart = rest.substring(firstParen + 1, lastParen).trim(); + String[] parts = pathPart.split("::"); + if (parts.length != 4) + { + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "Path must be warehouse::db::schema::table")).build(); + } + String warehouse = parts[0], db = parts[1], schema = parts[2], table = parts[3]; + // Parse schema + // Parse schema + List columns = new ArrayList<>(); + String[] columnDefs = schemaPart.split(","); + for (String colDef : columnDefs) + { + colDef = colDef.trim(); + String[] tokens = colDef.split("\\s+"); + if (tokens.length < 2) + { + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "Invalid column definition: " + colDef)).build(); + } + String name = tokens[0]; + String type = null; + boolean isPrimaryKey = false; + // Find all brackets: [type], [primarykey] + for (String token : tokens) + { + if (token.matches("\\[.*\\]")) + { + String content = token.substring(1, token.length() - 1).toLowerCase(); + if (content.equals("primarykey") || content.equals("primary_key")) + { + isPrimaryKey = true; + } + else + { + type = content; + } + } + } + + if (type == null) + { + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "Missing or invalid type for column: " + name)).build(); + } + + columns.add(new ServerState.Column(name, type, isPrimaryKey)); + } + + // Store it + ServerState.data + .computeIfAbsent(warehouse, k -> new HashMap<>()) + .computeIfAbsent(db, k -> new HashMap<>()) + .computeIfAbsent(schema, k -> new HashMap<>()) + .put(table, new ServerState.TableMetadata(columns)); + + return Response.ok(Map.of( + "message", "Table initialized with schema", + "warehouse", warehouse, + "database", db, + "schema", schema, + "table", table, + "columns", columns.stream().map(c -> Map.of( + "name", c.name, + "type", c.type, + "primaryKey", c.isPrimaryKey + )).collect(Collectors.toList()) + )).build(); + } + @POST + @Path("/deleterow") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.APPLICATION_JSON) + public Response deleteRowByIndex(String input) + { + try + { + // Expect input: "warehouse::db::schema::table::index" + String[] parts = input.trim().split("::"); + if (parts.length != 5) + { + throw new IllegalArgumentException("Invalid format. Expected: warehouse::db::schema::table::index"); + } + + String warehouse = parts[0]; + String db = parts[1]; + String schema = parts[2]; + String table = parts[3]; + int index = Integer.parseInt(parts[4]); + + Map>> dbs = ServerState.data.get(warehouse); + if (dbs == null || !dbs.containsKey(db) || !dbs.get(db).containsKey(schema) || !dbs.get(db).get(schema).containsKey(table)) + { + throw new RuntimeException("Table not found: " + String.join("::", warehouse, db, schema, table)); + } + + ServerState.TableMetadata metadata = dbs.get(db).get(schema).get(table); + List> rows = metadata.rows; + + if (index < 0 || index >= rows.size()) + { + throw new IndexOutOfBoundsException("Row index out of range: " + index); + } + + Map removedRow = rows.remove(index); + + return Response.ok(Map.of( + "message", "Row deleted successfully", + "deletedRow", removedRow + )).build(); + } + catch (Exception e) + { + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", e.getMessage())) + .build(); + } + } + + @POST + @Path("/insertrow") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Response insert(Map payload) + { + try + { + // Step 1: Extract and validate path + String fullPath = (String) payload.get("path"); + if (fullPath == null || !fullPath.contains("::")) + { + throw new RuntimeException("Missing or malformed 'path' field. Expected format: warehouse::database::schema::table"); + } + + String[] parts = fullPath.split("::"); + if (parts.length != 4) + { + throw new RuntimeException("Invalid path format. Expected: warehouse::database::schema::table"); + } + + String warehouse = parts[0], db = parts[1], schema = parts[2], table = parts[3]; + + // Step 2: Get row + Object rowObj = payload.get("row"); + if (!(rowObj instanceof Map)) + { + throw new RuntimeException("Missing or invalid 'row' field"); + } + + @SuppressWarnings("unchecked") + Map row = (Map) rowObj; + + // Step 3: Locate the table + Map>> dbs = ServerState.data.get(warehouse); + if (dbs == null || !dbs.containsKey(db) || !dbs.get(db).containsKey(schema) || !dbs.get(db).get(schema).containsKey(table)) + { + throw new RuntimeException("Table not found: " + fullPath); + } + + ServerState.TableMetadata tableMeta = dbs.get(db).get(schema).get(table); + List columns = tableMeta.schema; + + // Step 4: Validate all required fields and types + for (ServerState.Column col : columns) + { + if (!row.containsKey(col.name)) + { + throw new RuntimeException("Missing required column: " + col.name); + } + + Object value = row.get(col.name); + if (!isTypeMatch(value, col.type)) + { + throw new RuntimeException("Type mismatch for column: " + col.name + ". Expected: " + col.type + ", Got: " + value.getClass().getSimpleName()); + } + } + + // Step 5: Check for duplicate primary key + for (ServerState.Column col : columns) + { + if (col.isPrimaryKey) + { + Object pkValue = row.get(col.name); + for (Map existingRow : tableMeta.rows) + { + if (existingRow.get(col.name).equals(pkValue)) + { + throw new RuntimeException("Duplicate primary key value for column: " + col.name + " = " + pkValue); + } + } + } + } + + // Step 6: Insert the row + tableMeta.rows.add(row); + return Response.ok(Map.of("message", "Row added", "row", row)).build(); + } + catch (Exception e) + { + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", e.getMessage())).build(); + } + } + + @POST + @Path("/fetchtable") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.APPLICATION_JSON) + public Response getTableDataPost(String fullPath) + { + try + { + // Parse fullPath -> warehouse::db::schema::table + String[] parts = fullPath.trim().split("::"); + if (parts.length != 4) + { + throw new IllegalArgumentException("Invalid path format. Expected format: warehouse::db::schema::table"); + } + + String warehouse = parts[0]; + String db = parts[1]; + String schema = parts[2]; + String table = parts[3]; + + // Check if table exists + Map>> dbs = ServerState.data.get(warehouse); + if (dbs == null || !dbs.containsKey(db) || !dbs.get(db).containsKey(schema) || !dbs.get(db).get(schema).containsKey(table)) + { + throw new RuntimeException("Table not found: " + fullPath); + } + ServerState.TableMetadata tableMeta = dbs.get(db).get(schema).get(table); + List> tableData = tableMeta.rows; + return Response.ok(tableData).build(); + } + catch (Exception e) + { + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", e.getMessage())).build(); + } + } + + @GET + @Path("/showtables") + @Produces(MediaType.APPLICATION_JSON) + public Response showAllTables() + { + List tablePaths = new ArrayList<>(); + + for (Map.Entry>>> warehouseEntry : ServerState.data.entrySet()) + { + String warehouse = warehouseEntry.getKey(); + for (Map.Entry>> dbEntry : warehouseEntry.getValue().entrySet()) + { + String db = dbEntry.getKey(); + for (Map.Entry> schemaEntry : dbEntry.getValue().entrySet()) + { + String schema = schemaEntry.getKey(); + for (String table : schemaEntry.getValue().keySet()) + { + String fullPath = String.join("::", warehouse, db, schema, table); + tablePaths.add(fullPath); + } + } + } + } + Map response = new HashMap<>(); + response.put("tables", tablePaths); + response.put("count", tablePaths.size()); + return Response.ok(response).build(); + } + + @POST + @Path("/duckdb/load") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.APPLICATION_JSON) + public Response loadCsvFromPath(String fullPath) + { + try + { + fullPath = fullPath.trim(); + File csvFile = new File(fullPath); + if (!csvFile.exists() || !csvFile.getName().endsWith(".csv")) + { + return Response.status(Response.Status.BAD_REQUEST) + .entity("Invalid CSV file path: " + fullPath).build(); + } + + // Extract table name and create .duckdb path + java.nio.file.Path csvPath = csvFile.toPath(); + String baseName = csvFile.getName().replaceAll("\\.csv$", ""); // 'employees' + String tableName = baseName; + java.nio.file.Path parentDir = csvPath.getParent(); + java.nio.file.Path duckDbPath = parentDir.resolve(baseName + ".duckdb"); // 'employees.duckdb' + + // Load into DuckDB + Class.forName("org.duckdb.DuckDBDriver"); + try (Connection conn = DriverManager.getConnection("jdbc:duckdb:" + duckDbPath.toAbsolutePath()); + Statement stmt = conn.createStatement()) + { + String query = String.format( + "CREATE OR REPLACE TABLE \"%s\" AS SELECT * FROM read_csv_auto('%s');", + tableName, + csvPath.toAbsolutePath().toString().replace("\\", "\\\\") + ); + stmt.execute(query); + } + + return Response.ok("CSV loaded as table '" + tableName + "' in file: " + duckDbPath).build(); + } + catch (Exception e) + { + e.printStackTrace(); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity("Error: " + e.getMessage()).build(); + } + } + @POST + @Path("/duckdb/query") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Response queryDuckDBTable(Map request) + { + try + { + String dbPath = request.get("dbPath").trim(); // full path to .duckdb file + String sql = request.get("query").trim(); // SQL query + + if (!dbPath.endsWith(".duckdb") || sql.isEmpty()) + { + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "Invalid input. Expecting .duckdb file and non-empty query.")).build(); + } + + File dbFile = new File(dbPath); + if (!dbFile.exists()) + { + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "DuckDB file not found at path: " + dbPath)).build(); + } + + List> result = new ArrayList<>(); + try (Connection conn = DriverManager.getConnection("jdbc:duckdb:" + dbFile.getAbsolutePath()); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(sql)) + { + ResultSetMetaData meta = rs.getMetaData(); + int columnCount = meta.getColumnCount(); + + while (rs.next()) + { + Map row = new LinkedHashMap<>(); + for (int i = 1; i <= columnCount; i++) + { + row.put(meta.getColumnLabel(i), rs.getObject(i)); + } + result.add(row); + } + } + + return Response.ok(result).build(); + } + catch (Exception e) + { + e.printStackTrace(); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", e.getMessage())).build(); + } + } + + @SuppressWarnings("unchecked") + private boolean isTypeMatch(Object value, String type) + { + if (value == null) + { + return false; + } + + String lowerType = type.toLowerCase(); + + if (lowerType.equals("int") || lowerType.equals("integer")) + { + return value instanceof Integer || (value instanceof Number && ((Number) value).intValue() == ((Number) value).doubleValue()); + } + else if (lowerType.equals("float") || lowerType.equals("double")) + { + return value instanceof Float || value instanceof Double || value instanceof Number; + } + else if (lowerType.equals("string")) + { + return value instanceof String; + } + else if (lowerType.equals("boolean")) + { + return value instanceof Boolean; + } + else + { + return false; + } + } +} \ No newline at end of file diff --git a/ILegend-JupyterMultiKernel/Server/PyLegendSqlServer.java b/ILegend-JupyterMultiKernel/Server/PyLegendSqlServer.java new file mode 100644 index 000000000..8252a267e --- /dev/null +++ b/ILegend-JupyterMultiKernel/Server/PyLegendSqlServer.java @@ -0,0 +1,150 @@ +// Copyright 2023 Goldman Sachs +// +// 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. + +package org.finos.legend.pylegend; + +import com.fasterxml.jackson.databind.jsontype.NamedType; +import com.hazelcast.shaded.org.snakeyaml.engine.v2.api.Load; +import io.dropwizard.configuration.EnvironmentVariableSubstitutor; +import io.dropwizard.configuration.SubstitutingSourceProvider; +import io.dropwizard.setup.Bootstrap; +import io.dropwizard.setup.Environment; +import io.federecio.dropwizard.swagger.SwaggerBundle; +import io.federecio.dropwizard.swagger.SwaggerBundleConfiguration; +import org.eclipse.collections.api.RichIterable; +import org.eclipse.collections.api.block.function.Function; +import org.eclipse.collections.api.factory.Lists; +import org.eclipse.collections.api.list.MutableList; +import org.eclipse.collections.impl.list.mutable.FastList; +import org.finos.legend.authentication.credentialprovider.CredentialProviderProvider; +import org.finos.legend.engine.authentication.LegendDefaultDatabaseAuthenticationFlowProviderConfiguration; +import org.finos.legend.engine.functionActivator.api.FunctionActivatorAPI; +import org.finos.legend.engine.functionActivator.service.FunctionActivatorService; +import org.finos.legend.engine.language.hostedService.api.HostedServiceService; +import org.finos.legend.engine.language.pure.compiler.Compiler; +import org.finos.legend.engine.language.pure.compiler.api.Compile; +import org.finos.legend.engine.language.pure.compiler.toPureGraph.PureModel; +import org.finos.legend.engine.language.pure.grammar.api.grammarToJson.GrammarToJson; +import org.finos.legend.engine.language.pure.grammar.api.jsonToGrammar.JsonToGrammar; +import org.finos.legend.engine.language.pure.modelManager.ModelManager; +import org.finos.legend.engine.language.pure.modelManager.sdlc.SDLCLoader; +import org.finos.legend.engine.language.snowflakeApp.api.SnowflakeAppService; +import org.finos.legend.engine.plan.execution.PlanExecutor; +import org.finos.legend.engine.plan.execution.api.concurrent.ConcurrentExecutionNodeExecutorPoolInfo; +import org.finos.legend.engine.plan.execution.stores.relational.api.RelationalExecutorInformation; +import org.finos.legend.engine.plan.execution.stores.relational.config.RelationalExecutionConfiguration; +import org.finos.legend.engine.plan.execution.stores.relational.config.TemporaryTestDbConfiguration; +import org.finos.legend.engine.plan.execution.stores.relational.plugin.Relational; +import org.finos.legend.engine.plan.execution.stores.relational.plugin.RelationalStoreExecutor; +import org.finos.legend.engine.plan.generation.extension.PlanGeneratorExtension; +import org.finos.legend.engine.protocol.pure.v1.PureProtocolObjectMapperFactory; +import org.finos.legend.engine.protocol.pure.v1.model.context.PureModelContextData; +import org.finos.legend.engine.pure.code.core.PureCoreExtensionLoader; +import org.finos.legend.engine.query.sql.api.SQLExecutor; +import org.finos.legend.engine.query.sql.api.execute.SqlExecute; +import org.finos.legend.engine.query.sql.api.grammar.SqlGrammar; +import org.finos.legend.engine.query.sql.providers.LegendServiceSQLSourceProvider; +import org.finos.legend.engine.query.sql.providers.RelationalStoreSQLSourceProvider; +import org.finos.legend.engine.query.sql.providers.shared.FunctionSQLSourceProvider; +import org.finos.legend.engine.query.sql.providers.shared.project.ProjectCoordinateLoader; +import org.finos.legend.engine.server.Server; +import org.finos.legend.engine.server.ServerConfiguration; +import org.finos.legend.engine.server.core.api.CurrentUser; +import org.finos.legend.engine.server.core.api.Info; +import org.finos.legend.engine.server.core.api.Memory; +import org.finos.legend.engine.server.core.exceptionMappers.CatchAllExceptionMapper; +import org.finos.legend.engine.shared.core.ObjectMapperFactory; +import org.finos.legend.engine.shared.core.deployment.DeploymentMode; +import org.finos.legend.engine.shared.core.deployment.DeploymentStateAndVersions; +import org.finos.legend.pure.generated.Root_meta_pure_extension_Extension; +import org.finos.legend.server.pac4j.LegendPac4jBundle; + + + +import java.util.Collections; +import java.util.ServiceLoader; + +public class PyLegendSqlServer extends Server +{ + public static void main(String[] args) throws Exception { + new PyLegendSqlServer<>().run(args); + } + + @Override + public void initialize(Bootstrap bootstrap) + { + bootstrap.addBundle(new LegendPac4jBundle<>(serverConfiguration -> serverConfiguration.pac4j)); + bootstrap.setConfigurationSourceProvider(new SubstitutingSourceProvider(bootstrap.getConfigurationSourceProvider(), new EnvironmentVariableSubstitutor(true))); +// bootstrap.addBundle(new SwaggerBundle() { +// protected SwaggerBundleConfiguration getSwaggerBundleConfiguration(T configuration) { +// return configuration.swagger; +// } +// }); + PureProtocolObjectMapperFactory.withPureProtocolExtensions(bootstrap.getObjectMapper()); + ObjectMapperFactory.withStandardConfigurations(bootstrap.getObjectMapper()); + bootstrap.getObjectMapper().registerSubtypes(new NamedType(LegendDefaultDatabaseAuthenticationFlowProviderConfiguration.class, "legendDefault")); + } + + @Override + public void run(T serverConfiguration, Environment environment) + { + DeploymentStateAndVersions.DEPLOYMENT_MODE = DeploymentMode.TEST_IGNORE_FUNCTION_MATCH; + + SDLCLoader sdlcLoader = new SDLCLoader(serverConfiguration.metadataserver, null); + ModelManager modelManager = new ModelManager(DeploymentMode.TEST_IGNORE_FUNCTION_MATCH, sdlcLoader); + + RelationalExecutionConfiguration relationalExecution = new RelationalExecutionConfiguration(); + TemporaryTestDbConfiguration temporaryTestDbConfiguration = new TemporaryTestDbConfiguration(); + temporaryTestDbConfiguration.port = 9092; + relationalExecution.tempPath = "/tmp"; + relationalExecution.temporarytestdb = temporaryTestDbConfiguration; + relationalExecution.setCredentialProviderProvider(CredentialProviderProvider.defaultProviderProvider()); + relationalStoreExecutor = (RelationalStoreExecutor) Relational.build(relationalExecution); + + PlanExecutor planExecutor = PlanExecutor.newPlanExecutor(relationalStoreExecutor); + + // API + environment.jersey().setUrlPattern("/api/*"); + + // Server + environment.jersey().register(new Info(serverConfiguration.deployment, serverConfiguration.opentracing)); + environment.jersey().register(new CurrentUser()); + environment.jersey().register(new Memory()); + environment.jersey().register(new RelationalExecutorInformation()); + environment.jersey().register(new ConcurrentExecutionNodeExecutorPoolInfo(Collections.emptyList())); + + MutableList generatorExtensions = Lists.mutable.withAll(ServiceLoader.load(PlanGeneratorExtension.class)); + Function> routerExtensions = (pureModel) -> { + return PureCoreExtensionLoader.extensions().flatCollect((e) -> { + return e.extraPureCoreExtensions(pureModel.getExecutionSupport()); + }); + }; + ProjectCoordinateLoader projectCoordinateLoader = new ProjectCoordinateLoader(modelManager, serverConfiguration.metadataserver.getSdlc()); + environment.jersey().register(new SqlExecute(new SQLExecutor(modelManager, planExecutor, routerExtensions, FastList.newListWith( + new RelationalStoreSQLSourceProvider(projectCoordinateLoader), + new FunctionSQLSourceProvider(projectCoordinateLoader), + new LegendServiceSQLSourceProvider(projectCoordinateLoader)), + generatorExtensions.flatCollect(PlanGeneratorExtension::getExtraPlanTransformers)))); + environment.jersey().register(new SqlGrammar()); + environment.jersey().register(new CatchAllExceptionMapper()); + environment.jersey().register(new Compile(modelManager)); + + environment.jersey().register(new GrammarToJson()); + environment.jersey().register(new JsonToGrammar()); + environment.jersey().register(new LoadController()); + environment.jersey().register(new DataServer()); + environment.jersey().register(new FunctionActivatorAPI(modelManager, Lists.mutable.empty(), Lists.mutable.with(new FunctionActivatorService[]{new SnowflakeAppService(planExecutor), new HostedServiceService()}), routerExtensions)); + //Compiler.compile(PureModelContextData.newBuilder().build(), DeploymentMode.TEST_IGNORE_FUNCTION_MATCH, Lists.mutable.empty()); + } +} diff --git a/ILegend-JupyterMultiKernel/Server/ServerState.java b/ILegend-JupyterMultiKernel/Server/ServerState.java new file mode 100644 index 000000000..6ece23531 --- /dev/null +++ b/ILegend-JupyterMultiKernel/Server/ServerState.java @@ -0,0 +1,43 @@ +package org.finos.legend.pylegend; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import org.finos.legend.engine.protocol.pure.v1.PureProtocolObjectMapperFactory; + +import java.util.*; + +public class ServerState +{ + public static class TableMetadata + { + public final List> rows = new ArrayList<>(); + public final List schema; + + public TableMetadata(List schema) + { + this.schema = schema; + } + } + + public static class Column + { + public final String name; + public final String type; + public final boolean isPrimaryKey; + + public Column(String name, String type, boolean isPrimaryKey) + { + this.name = name; + this.type = type; + this.isPrimaryKey = isPrimaryKey; + } + } + public static final Map>>> data = new HashMap<>(); +} + + + + From 8740a2df6b7b1cc0899e94223d6ba6d542b00472 Mon Sep 17 00:00:00 2001 From: Shanmukh raj MSS <133531695+ShanmukhEstrella@users.noreply.github.com> Date: Sun, 24 Aug 2025 01:37:58 +0530 Subject: [PATCH 7/7] Delete ILegend-JupyterMultiKernel/Server/README.md --- ILegend-JupyterMultiKernel/Server/README.md | 1 - 1 file changed, 1 deletion(-) delete mode 100644 ILegend-JupyterMultiKernel/Server/README.md diff --git a/ILegend-JupyterMultiKernel/Server/README.md b/ILegend-JupyterMultiKernel/Server/README.md deleted file mode 100644 index 8b1378917..000000000 --- a/ILegend-JupyterMultiKernel/Server/README.md +++ /dev/null @@ -1 +0,0 @@ -