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 +} diff --git a/ILegend-JupyterMultiKernel/Kernel/LICENSE b/ILegend-JupyterMultiKernel/Kernel/LICENSE new file mode 100644 index 000000000..a6eb512fa --- /dev/null +++ b/ILegend-JupyterMultiKernel/Kernel/LICENSE @@ -0,0 +1,19 @@ +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 +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..188c06a7e --- /dev/null +++ b/ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/kernel.py @@ -0,0 +1,2108 @@ +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("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': {} + } + + + + + + + + + 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() == "": + return { + '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": + 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 000000000..8609e0ca0 Binary files /dev/null and b/ILegend-JupyterMultiKernel/Kernel/ilegend_kernel/kernelspec/logo-64x64.png differ 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/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<>(); +} + + + + 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/**/*"] +}