From b350ae1cf9e311fdbf0cdec8c491b2b4450f2241 Mon Sep 17 00:00:00 2001 From: caiohamamura Date: Mon, 24 Mar 2025 17:39:21 -0300 Subject: [PATCH 01/22] Removed pd.io.sql.execute calls (deprecated). --- mysql_kernel/kernel.py | 64 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 56 insertions(+), 8 deletions(-) diff --git a/mysql_kernel/kernel.py b/mysql_kernel/kernel.py index 3370920..874d591 100644 --- a/mysql_kernel/kernel.py +++ b/mysql_kernel/kernel.py @@ -1,6 +1,7 @@ import pandas as pd import sqlalchemy as sa from ipykernel.kernelbase import Kernel +import re __version__ = '0.4.1' @@ -36,6 +37,51 @@ def err(self, msg): 'execution_count':self.execution_count, 'payload':[], 'user_expressions':{}} + + def generic_ddl(self, query, msg): + try: + with self.engine.connect() as con: + result = con.execute(sa.sql.text(query)) + con.commit() + split_query = query.split() + if len(split_query) > 2: + object_name = re.match("([^ ]+ ){2}(if (not )?exists )?([^ ]+)", query, re.IGNORECASE).group(4) + else: + object_name = query.split()[1] + rows_affected = result.rowcount + if result.rowcount > 0: + self.output((msg + '\nRows affected: %d.') % (object_name, rows_affected)) + else: + self.output(msg % (object_name)) + return + except Exception as msg: + self.output(str(msg)) + return + + def create_db(self, query): + self.generic_ddl(query, 'Database %s created successfully.') + + + def drop_db(self, query): + self.generic_ddl(query, 'Database %s dropped successfully.') + + def create_table(self, query): + self.generic_ddl(query, 'Table %s created successfully.') + + def drop_table(self, query): + self.generic_ddl(query, 'Table %s dropped successfully.') + + def delete(self, query): + self.generic_ddl(query, 'Data deleted from %s successfully.') + + def alter_table(self, query): + self.generic_ddl(query, 'Table %s altered successfully.') + + def insert_into(self, query): + self.generic_ddl(query, 'Data inserted into %s successfully.') + + def use_db(self, query): + self.generic_ddl(query, 'Changed to database %s successfully.') def do_execute(self, code, silent, store_history=True, user_expressions=None, allow_stdin=False): self.silent = silent @@ -54,26 +100,28 @@ def do_execute(self, code, silent, store_history=True, user_expressions=None, al else: self.engine = sa.create_engine(f'mysql+py{v}') elif l.startswith('create database '): - pd.io.sql.execute(v, con=self.engine) + self.create_db(v) elif l.startswith('drop database '): - pd.io.sql.execute(v, con=self.engine) + self.drop_db(v) elif l.startswith('create table '): - pd.io.sql.execute(v, con=self.engine) + self.create_table(v) elif l.startswith('drop table '): - pd.io.sql.execute(v, con=self.engine) + self.drop_table(v) elif l.startswith('delete '): - pd.io.sql.execute(v, con=self.engine) + self.delete(v) elif l.startswith('alter table '): - pd.io.sql.execute(v, con=self.engine) + self.alter_table(v) + elif l.startswith('use '): + self.use_db(v) elif l.startswith('insert into '): - pd.io.sql.execute(v, con=self.engine) + self.insert_into(v) else: if self.engine: if ' like ' in l: if l[l.find(' like ')+6:].count('%')<4: self.output("sql code ' like %xx%' should be replace ' like %%xx%%'.") return self.ok() - if l.startswith('select ') and ' limit ' not in l: + if l.startswith('select ') and 'limit ' not in l: output = pd.read_sql(f'{v} limit 1000', self.engine).to_html() else: output = pd.read_sql(v, self.engine).to_html() From 35210218918ce0739c599ae053f7c8a481b374f8 Mon Sep 17 00:00:00 2001 From: Caio Hamamura Date: Tue, 25 Mar 2025 14:14:24 -0300 Subject: [PATCH 02/22] Update kernel.py Fix % sign in cell and maximum cell height with scroll --- mysql_kernel/kernel.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/mysql_kernel/kernel.py b/mysql_kernel/kernel.py index 874d591..f8edc27 100644 --- a/mysql_kernel/kernel.py +++ b/mysql_kernel/kernel.py @@ -98,6 +98,8 @@ def do_execute(self, code, silent, store_history=True, user_expressions=None, al if l.count('@')>1: self.output("Connection failed, The Mysql address cannot have two '@'.") else: + print(l) + self.output(l) self.engine = sa.create_engine(f'mysql+py{v}') elif l.startswith('create database '): self.create_db(v) @@ -117,14 +119,20 @@ def do_execute(self, code, silent, store_history=True, user_expressions=None, al self.insert_into(v) else: if self.engine: - if ' like ' in l: - if l[l.find(' like ')+6:].count('%')<4: - self.output("sql code ' like %xx%' should be replace ' like %%xx%%'.") - return self.ok() + v = re.sub('(?Results limitted to 1000 (explicitly add LIMIT to display beyond that)

+ {results.to_html()} + ''' + else: + output = results.to_html() else: output = pd.read_sql(v, self.engine).to_html() + output = f'''
{output}
''' else: output = 'Unable to connect to Mysql server. Check that the server is running.' self.output(output) From 85cb65e63eafcc4327c2e9b19230adb6faf4af52 Mon Sep 17 00:00:00 2001 From: Caio Hamamura Date: Wed, 26 Mar 2025 11:46:53 -0300 Subject: [PATCH 03/22] Ignore comments, spaces, \r\n\t --- mysql_kernel/kernel.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/mysql_kernel/kernel.py b/mysql_kernel/kernel.py index f8edc27..73f12d7 100644 --- a/mysql_kernel/kernel.py +++ b/mysql_kernel/kernel.py @@ -42,7 +42,8 @@ def generic_ddl(self, query, msg): try: with self.engine.connect() as con: result = con.execute(sa.sql.text(query)) - con.commit() + if callable(getattr(con, 'commit', None)): + con.commit() split_query = query.split() if len(split_query) > 2: object_name = re.match("([^ ]+ ){2}(if (not )?exists )?([^ ]+)", query, re.IGNORECASE).group(4) @@ -92,15 +93,16 @@ def do_execute(self, code, silent, store_history=True, user_expressions=None, al try: for v in sql.split(";"): v = v.rstrip() + v = re.sub('^[ \r\n\t]+', '', v) + v = re.sub('\n *--.*\n', '', v) # remove comments l = v.lower() if len(l)>0: if l.startswith('mysql://'): if l.count('@')>1: self.output("Connection failed, The Mysql address cannot have two '@'.") else: - print(l) - self.output(l) self.engine = sa.create_engine(f'mysql+py{v}') + self.output('Connected successfully!') elif l.startswith('create database '): self.create_db(v) elif l.startswith('drop database '): @@ -135,7 +137,7 @@ def do_execute(self, code, silent, store_history=True, user_expressions=None, al output = f'''
{output}
''' else: output = 'Unable to connect to Mysql server. Check that the server is running.' - self.output(output) + self.output(output) return self.ok() except Exception as msg: self.output(str(msg)) From 42cdca619b522814d8da9f915d911e4379abdbdf Mon Sep 17 00:00:00 2001 From: Caio Hamamura Date: Wed, 26 Mar 2025 11:49:41 -0300 Subject: [PATCH 04/22] Ignore comment in the beggining --- mysql_kernel/kernel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mysql_kernel/kernel.py b/mysql_kernel/kernel.py index 73f12d7..b1d1012 100644 --- a/mysql_kernel/kernel.py +++ b/mysql_kernel/kernel.py @@ -94,7 +94,7 @@ def do_execute(self, code, silent, store_history=True, user_expressions=None, al for v in sql.split(";"): v = v.rstrip() v = re.sub('^[ \r\n\t]+', '', v) - v = re.sub('\n *--.*\n', '', v) # remove comments + v = re.sub('\n* *--.*\n', '', v) # remove comments l = v.lower() if len(l)>0: if l.startswith('mysql://'): From b8abc10325ce96a90d8e22e6dc7e306c6a995a05 Mon Sep 17 00:00:00 2001 From: Caio Hamamura Date: Wed, 26 Mar 2025 12:15:56 -0300 Subject: [PATCH 05/22] Use engine.begin instead of connect --- mysql_kernel/kernel.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mysql_kernel/kernel.py b/mysql_kernel/kernel.py index b1d1012..cbde18d 100644 --- a/mysql_kernel/kernel.py +++ b/mysql_kernel/kernel.py @@ -40,10 +40,8 @@ def err(self, msg): def generic_ddl(self, query, msg): try: - with self.engine.connect() as con: + with self.engine.begin() as con: result = con.execute(sa.sql.text(query)) - if callable(getattr(con, 'commit', None)): - con.commit() split_query = query.split() if len(split_query) > 2: object_name = re.match("([^ ]+ ){2}(if (not )?exists )?([^ ]+)", query, re.IGNORECASE).group(4) From 2411f464013d9df59e77252659fbe5f439dd44a3 Mon Sep 17 00:00:00 2001 From: caiohamamura Date: Tue, 1 Apr 2025 23:37:47 -0300 Subject: [PATCH 06/22] Fixed kernel --- .gitignore | 5 + mysql_kernel/autocomplete.py | 207 ++++++++++++++++++++++++++++ mysql_kernel/kernel.py | 159 +++++++++++++++++---- mysql_kernel/pygment_error_lexer.py | 23 ++++ mysql_kernel/style.py | 82 +++++++++++ 5 files changed, 449 insertions(+), 27 deletions(-) create mode 100644 .gitignore create mode 100644 mysql_kernel/autocomplete.py create mode 100644 mysql_kernel/pygment_error_lexer.py create mode 100644 mysql_kernel/style.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1e14f7c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +build +dist +.ipynb_checkpoints +*.egg-info/ +**/__pycache__ \ No newline at end of file diff --git a/mysql_kernel/autocomplete.py b/mysql_kernel/autocomplete.py new file mode 100644 index 0000000..bbc2985 --- /dev/null +++ b/mysql_kernel/autocomplete.py @@ -0,0 +1,207 @@ +import re +from sqlalchemy import inspect + +class SQLAutocompleter: + def __init__(self, engine, log): + """ + Initializes the autocompleter with an SQLAlchemy engine. + + Parameters: + - engine: SQLAlchemy engine connected to a database. + """ + self.engine = engine + self.inspector = inspect(engine) + self.default_schema = self.inspector.default_schema_name + self.log = log + self.log.info(f"Autocompleter initialized with engine: {engine}") + + def get_real_previous_keyword(self, tokens): + """ + Identifies the real previous keyword in SQL syntax, i.e., section delimiters like `SELECT`, `FROM`, `WHERE`. + + Parameters: + - tokens (list): List of tokens (words) before the cursor position. + + Returns: + - str: The most recent real SQL keyword (e.g., `SELECT`, `FROM`). + """ + sql_keywords = { + "SELECT", "FROM", "WHERE", "GROUP", "ORDER", "HAVING", "INSERT", "UPDATE", "DELETE", + "JOIN", "ON", "LIMIT", "DISTINCT", "SET" + } + for token in reversed(tokens): + if token.upper() in sql_keywords: + return token.upper() + return "" + + + def get_completions(self, code, cursor_pos): + """ + Returns autocompletions based on the SQL context. + + Parameters: + - code (str): Full SQL query being typed. + - cursor_pos (int): Cursor position in the query. + + Returns: + - list: Suggested completions. + """ + preceding_text = code[:cursor_pos] + tokens = re.findall(r"[^ ;\(\)\r\n\t,]+", preceding_text, re.IGNORECASE) + previous_keyword = self.get_real_previous_keyword(tokens) + previous_word = tokens[-1].upper() if tokens else "" + is_preceding_comma = preceding_text.rstrip().endswith(",") + is_preceding_space = preceding_text.endswith(" ") + is_completing_word = preceding_text[-1].isalpha() + current_completing = '' + if is_completing_word: + previous_word = tokens[-2].upper() if len(tokens) > 1 else "" + current_completing = tokens[-1].upper() if tokens else "" + + if previous_keyword == "SELECT": + if is_preceding_comma == False and is_preceding_space == True and previous_word != "SELECT": + completions = ["FROM"] + else: + completions = self.get_columns(code) + self.get_functions() + elif previous_keyword in {"FROM", "JOIN"}: + completions = self.get_tables() + elif previous_keyword == "WHERE": + completions = self.get_columns(code) + self.get_functions() + elif previous_word == "GROUP": + completions = ["BY"] + elif previous_word == "ORDER": + completions = ["BY"] + elif previous_word == "INSERT": + completions = ["INTO"] + elif previous_word == "UPDATE": + completions = self.get_tables() + elif previous_keyword == 'UPDATE': + completions = ["SET"] + completions += self.get_tables() + elif previous_word == "DELETE": + completions = ["FROM"] + elif previous_word == "DISTINCT": + completions = self.get_columns(code) + elif previous_keyword == "DISTINCT": + completions = self.get_columns(code) + self.get_functions() + elif previous_keyword in {"GROUP", "ORDER"}: + completions = self.get_columns(code) + elif previous_keyword == "HAVING": + completions = self.get_columns(code) + self.get_functions() + elif previous_keyword == "SET": + completions = self.get_columns(code) + elif previous_word == "VALUES": + completions = '(' + elif previous_keyword == "VALUES": + completions = self.get_columns(code) + elif previous_word in {"INNER", "LEFT", "RIGHT", "FULL"}: + completions = ["JOIN"] + elif previous_keyword == "DISTINCT" or previous_keyword == "LIMIT" or previous_keyword == "OFFSET": + completions = [] + else: + completions = self.get_sql_keywords() + + if is_completing_word: + filter_func = lambda x: x.lower().startswith(current_completing.lower()) + if is_preceding_comma == False and is_preceding_space == False: + filtered_suggestions = [suggestion for suggestion in completions if filter_func(suggestion)] + return sorted(filtered_suggestions) + + return completions + + + def get_tables(self): + """b + Returns a list of available tables, excluding the default schema. + + Returns: + - list: Tables without default schema. + """ + + schemas = self.inspector.get_schema_names() + tables = self.inspector.get_table_names(schema=self.default_schema) # Get tables in default schema + + if self.default_schema: + for schema in schemas: + schema_tables = self.inspector.get_table_names(schema=schema) + + if schema != self.default_schema: + tables.extend([f"{schema}.{table}" for table in schema_tables]) # Keep schema.table + + return tables + + def get_columns(self, code): + """ + Extracts tables from the query and returns relevant columns. + + Parameters: + - code (str): SQL query. + + Returns: + - list: Column names from the tables used in the query. + """ + table_names = self.extract_table_names(code) + columns = [] + for table in table_names: + schema, table_name = self.split_schema_table(table) + try: + table_columns = [col["name"] for col in self.inspector.get_columns(table_name, schema=schema)] + columns.extend(table_columns) + except Exception: + pass # Ignore missing tables + return columns + + def get_functions(self): + """Returns common SQL functions.""" + return [ + "COUNT()", "AVG()", "SUM()", "MIN()", "MAX()", + "LOWER()", "UPPER()", "NOW()", "DATE()", "ROUND()" + ] + + def get_sql_keywords(self): + """Returns a list of common SQL keywords.""" + return [ + "SELECT", "FROM", "WHERE", "GROUP BY", "ORDER BY", "HAVING", + "INSERT INTO", "VALUES", "UPDATE", "SET", "DELETE FROM", + "JOIN", "INNER JOIN", "LEFT JOIN", "RIGHT JOIN", "FULL JOIN", + "ON", "DISTINCT", "LIMIT", "OFFSET" + ] + + def extract_table_names(self, code): + """ + Extracts table names (including schema-qualified) from an SQL query. + + Parameters: + - code (str): SQL query. + + Returns: + - list: Table names found in the query. + """ + matches = re.findall(r"FROM\s+([\w.]+)|JOIN\s+([\w.]+)|UPDATE\s+([\w.]+)", code, re.IGNORECASE) + return [table for tup in matches for table in tup if table] + + def split_schema_table(self, table): + """ + Splits a schema-qualified table into schema and table parts. + + Parameters: + - table (str): Table name (could be schema-qualified like 'schema.table'). + + Returns: + - tuple: (schema, table_name) or (None, table_name) if no schema. + """ + parts = table.split(".") + if len(parts) == 2: + schema, table_name = parts + if schema == self.default_schema: # Remove default schema + return None, table_name + return schema, table_name + return self.default_schema, table # No schema + + +# Example usage +# from sqlalchemy import create_engine +# engine = create_engine("postgresql://postgres@localhost/tume") +# completer = SQLAutocompleter(engine) +# completions = completer.get_completions("SELECT municipio, FROM tume.cadastro", 17) +# print(completions) \ No newline at end of file diff --git a/mysql_kernel/kernel.py b/mysql_kernel/kernel.py index cbde18d..33f3ff3 100644 --- a/mysql_kernel/kernel.py +++ b/mysql_kernel/kernel.py @@ -2,10 +2,31 @@ import sqlalchemy as sa from ipykernel.kernelbase import Kernel import re - +from .autocomplete import SQLAutocompleter +import logging +import traceback +from pygments import highlight +from pygments.lexers.python import PythonLexer +from pygments.formatters import HtmlFormatter, TerminalFormatter +from .pygment_error_lexer import SqlErrorLexer +from .style import ThisStyle __version__ = '0.4.1' +class FixedWidthHtmlFormatter(HtmlFormatter): + + def wrap(self, source): + return self._wrap_code(source) + + def _wrap_code(self, source): + yield 0, '

' + for i, t in source: + if i == 1: + # it's a line of formatted code + t += '
' + yield i, t + yield 0, '

' + class MysqlKernel(Kernel): implementation = 'mysql_kernel' implementation_version = __version__ @@ -19,11 +40,19 @@ class MysqlKernel(Kernel): def __init__(self, **kwargs): Kernel.__init__(self, **kwargs) self.engine = False + self.log.setLevel(logging.DEBUG) + print('Mysql kernel initialized') + self.log.info('Mysql kernel initialized') - def output(self, output): + def output(self, output, plain_text = None): + if plain_text == None: + plain_text = output if not self.silent: display_content = {'source': 'kernel', - 'data': {'text/html': output}, + 'data': { + 'text/html': output, + 'text/plain': plain_text, + }, 'metadata': {}} self.send_response(self.iopub_socket, 'display_data', display_content) @@ -54,33 +83,35 @@ def generic_ddl(self, query, msg): self.output(msg % (object_name)) return except Exception as msg: - self.output(str(msg)) - return + return self.handle_error(msg) def create_db(self, query): - self.generic_ddl(query, 'Database %s created successfully.') + return self.generic_ddl(query, 'Database %s created successfully.') def drop_db(self, query): - self.generic_ddl(query, 'Database %s dropped successfully.') + return self.generic_ddl(query, 'Database %s dropped successfully.') def create_table(self, query): - self.generic_ddl(query, 'Table %s created successfully.') + return self.generic_ddl(query, 'Table %s created successfully.') def drop_table(self, query): - self.generic_ddl(query, 'Table %s dropped successfully.') + return self.generic_ddl(query, 'Table %s dropped successfully.') def delete(self, query): - self.generic_ddl(query, 'Data deleted from %s successfully.') + return self.generic_ddl(query, 'Data deleted from %s successfully.') def alter_table(self, query): - self.generic_ddl(query, 'Table %s altered successfully.') + return self.generic_ddl(query, 'Table %s altered successfully.') def insert_into(self, query): - self.generic_ddl(query, 'Data inserted into %s successfully.') + return self.generic_ddl(query, 'Data inserted into %s successfully.') def use_db(self, query): - self.generic_ddl(query, 'Changed to database %s successfully.') + new_database = re.match("use ([^ ]+)", query, re.IGNORECASE).group(1) + self.engine = sa.create_engine(self.engine.url.set(database=new_database)) + self.autocompleter = SQLAutocompleter(engine=self.engine, log=self.log) + return self.generic_ddl(query, 'Changed to database %s successfully.') def do_execute(self, code, silent, store_history=True, user_expressions=None, allow_stdin=False): self.silent = silent @@ -88,6 +119,7 @@ def do_execute(self, code, silent, store_history=True, user_expressions=None, al if not code.strip(): return self.ok() sql = code.rstrip()+('' if code.rstrip().endswith(";") else ';') + results_raw = None try: for v in sql.split(";"): v = v.rstrip() @@ -99,30 +131,36 @@ def do_execute(self, code, silent, store_history=True, user_expressions=None, al if l.count('@')>1: self.output("Connection failed, The Mysql address cannot have two '@'.") else: - self.engine = sa.create_engine(f'mysql+py{v}') + if v.startswith('mysql://'): + v = v.replace('mysql://', 'mysql+pymysql://') + self.engine = sa.create_engine(v) + self.autocompleter = SQLAutocompleter(engine=self.engine, log=self.log) self.output('Connected successfully!') + + elif l.startswith('create database '): - self.create_db(v) + return self.create_db(v) elif l.startswith('drop database '): - self.drop_db(v) + return self.drop_db(v) elif l.startswith('create table '): - self.create_table(v) + return self.create_table(v) elif l.startswith('drop table '): - self.drop_table(v) + return self.drop_table(v) elif l.startswith('delete '): - self.delete(v) + return self.delete(v) elif l.startswith('alter table '): - self.alter_table(v) + return self.alter_table(v) elif l.startswith('use '): - self.use_db(v) + return self.use_db(v) elif l.startswith('insert into '): - self.insert_into(v) + return self.insert_into(v) else: if self.engine: v = re.sub('(?Results limitted to 1000 (explicitly add LIMIT to display beyond that)

@@ -131,12 +169,79 @@ def do_execute(self, code, silent, store_history=True, user_expressions=None, al else: output = results.to_html() else: - output = pd.read_sql(v, self.engine).to_html() + with self.engine.begin() as con: + execution = con.execute(sa.sql.text(v)) + if execution.returns_rows: + results = pd.DataFrame(execution.fetchall(), columns=execution.keys()) + output = results.to_html() + elif execution.rowcount > 0: + output = f'Rows affected: {execution.rowcount}' + else: + output = 'No rows affected' output = f'''
{output}
''' else: output = 'Unable to connect to Mysql server. Check that the server is running.' - self.output(output) + self.output(output, plain_text = results_raw if results_raw else output) return self.ok() - except Exception as msg: - self.output(str(msg)) - return self.err('Error executing code ' + sql) + except Exception as e: + return self.handle_error(e) + + def handle_error(self, e): + msg = re.search(r'\d+,[^"]*"([^"]+)', e.args[0]).group(1) + + # Convert to HTML with Pygments + formatter = FixedWidthHtmlFormatter(full=True, style=ThisStyle, traceback=False) + tb_html = highlight("ERROR: " + msg, SqlErrorLexer(), formatter) + tb_terminal = highlight(msg, SqlErrorLexer(), TerminalFormatter()) + + # Send formatted traceback as an HTML response + self.send_response( + self.iopub_socket, + "display_data", + { + "data": { + "text/html": tb_html, + "text/plain": tb_terminal + }, + "metadata": {} + }, + ) + return {"status": "error", "execution_count": self.execution_count} + + def do_complete(self, code, cursor_pos): + self.log.info('Try to autocomplete') + if not self.autocompleter: + return {"status": "ok", "matches": []} + completion_list = self.autocompleter.get_completions(code, cursor_pos) + # match_text_list = [completion.text for completion in completion_list] + # offset = 0 + # if len(completion_list) > 0: + # offset = completion_list[ + # 0 + # ].start_position # if match part is 'sel', then start_position would be -3 + # type_dict_list = [] + # for completion in completion_list: + # if completion.display_meta is not None: + # type_dict_list.append( + # { + # "start": completion.start_position, + # "end": len(completion.text) + completion.start_position, + # "text": completion.text, + # # display_meta is FormattedText object + # "type": completion.display_meta_text, + # } + # ) + + cursor_offset = 0 + + word_completing = re.search('[^ ]+$',code[:cursor_pos]) + if word_completing: + cursor_offset += len(word_completing.group(0)) + + return { + "status": "ok", + "matches": completion_list, + "cursor_start": cursor_pos - cursor_offset, + "cursor_end": cursor_pos, + "metadata": {}, + } \ No newline at end of file diff --git a/mysql_kernel/pygment_error_lexer.py b/mysql_kernel/pygment_error_lexer.py new file mode 100644 index 0000000..bc5cd1e --- /dev/null +++ b/mysql_kernel/pygment_error_lexer.py @@ -0,0 +1,23 @@ +from pygments.lexer import RegexLexer, bygroups +from pygments.token import * + +__all__ = ['SqlErrorLexer'] + +class SqlErrorLexer(RegexLexer): + """ + A lexer for highlighting errors from MySQL/MariaDB. + """ + + name = 'sqlerror' + aliases = ['sqlerror'] + filenames = ['*.sqlerror'] + + tokens = { + 'root': [ + (r"(ERROR)(.*Table ')([^']+)", bygroups(Token.Generic.Deleted, Token.Generic, Token.Name.Class)), + (r"(ERROR)(.*column ')([^']+)(.*in ')([^']+)", bygroups(Token.Generic.Deleted, Token.Generic, Token.Name.Class, Token.Generic, Token.Name.Builtin)), + (r"(ERROR)(.*near ')([^']+)(.*line [0-9]+)", bygroups(Token.Generic.Deleted, Token.Generic, Token.Name.Builtin, Token.Number)), + (r"(ERROR)(.*database ')([^']+)", bygroups(Token.Generic.Deleted, Token.Generic, Token.Name.Class)), + ] + } + diff --git a/mysql_kernel/style.py b/mysql_kernel/style.py new file mode 100644 index 0000000..5b63f42 --- /dev/null +++ b/mysql_kernel/style.py @@ -0,0 +1,82 @@ +""" + Kernel version of `Dracula` derived from Pygments + :license: BSD, see LICENSE for details. +""" + +from pygments.style import Style +from pygments.token import Keyword, Name, Comment, String, Error, Literal, \ + Number, Operator, Other, Punctuation, Text, Generic, Whitespace + + +__all__ = ['ThisStyle'] + +background = "#282a36" +foreground = "#f8f8f2" +selection = "#44475a" +comment = "#6272a4" +cyan = "#8be9fd" +green = "#50fa7b" +orange = "#ffb86c" +pink = "#ff79c6" +purple = "#bd93f9" +red = "#ff5555" +yellow = "#f1fa8c" + +deletion = "#8b080b" + +class ThisStyle(Style): + name = 'this' + + background_color = background + highlight_color = selection + line_number_color = yellow + line_number_background_color = selection + line_number_special_color = green + line_number_special_background_color = comment + + styles = { + Whitespace: foreground, + + Comment: comment, + Comment.Preproc: pink, + + Generic: foreground, + Generic.Deleted: red, + Generic.Emph: "underline", + Generic.Heading: "bold", + Generic.Inserted: "bold", + Generic.Output: selection, + Generic.EmphStrong: "underline", + Generic.Subheading: "bold", + + Error: foreground, + + Keyword: pink, + Keyword.Constant: pink, + Keyword.Declaration: cyan + " italic", + Keyword.Type: cyan, + + Literal: foreground, + + Name: foreground, + Name.Attribute: green, + Name.Builtin: cyan + " italic", + Name.Builtin.Pseudo: foreground, + Name.Class: green, + Name.Function: green, + Name.Label: cyan + " italic", + Name.Tag: pink, + Name.Variable: cyan + " italic", + + Number: orange + ' bold', + + Operator: pink, + + Other: foreground, + + Punctuation: foreground, + + String: purple, + + Text: foreground, + } \ No newline at end of file From e54a2e81d94b3f8e602ed645f5042173fa006108 Mon Sep 17 00:00:00 2001 From: caiohamamura Date: Tue, 1 Apr 2025 23:43:16 -0300 Subject: [PATCH 07/22] Only return if there is an error --- mysql_kernel/kernel.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/mysql_kernel/kernel.py b/mysql_kernel/kernel.py index 33f3ff3..85f2455 100644 --- a/mysql_kernel/kernel.py +++ b/mysql_kernel/kernel.py @@ -115,6 +115,7 @@ def use_db(self, query): def do_execute(self, code, silent, store_history=True, user_expressions=None, allow_stdin=False): self.silent = silent + res = {} output = '' if not code.strip(): return self.ok() @@ -139,21 +140,21 @@ def do_execute(self, code, silent, store_history=True, user_expressions=None, al elif l.startswith('create database '): - return self.create_db(v) + res = self.create_db(v) elif l.startswith('drop database '): - return self.drop_db(v) + res = self.drop_db(v) elif l.startswith('create table '): - return self.create_table(v) + res = self.create_table(v) elif l.startswith('drop table '): - return self.drop_table(v) + res = self.drop_table(v) elif l.startswith('delete '): - return self.delete(v) + res = self.delete(v) elif l.startswith('alter table '): - return self.alter_table(v) + res = self.alter_table(v) elif l.startswith('use '): - return self.use_db(v) + res = self.use_db(v) elif l.startswith('insert into '): - return self.insert_into(v) + res = self.insert_into(v) else: if self.engine: v = re.sub('(? Date: Wed, 2 Apr 2025 11:28:28 -0300 Subject: [PATCH 08/22] Update kernel.py --- mysql_kernel/kernel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mysql_kernel/kernel.py b/mysql_kernel/kernel.py index 85f2455..7c2fc2f 100644 --- a/mysql_kernel/kernel.py +++ b/mysql_kernel/kernel.py @@ -15,7 +15,7 @@ class FixedWidthHtmlFormatter(HtmlFormatter): - def wrap(self, source): + def wrap(self, source, outfile=None): return self._wrap_code(source) def _wrap_code(self, source): @@ -247,4 +247,4 @@ def do_complete(self, code, cursor_pos): "cursor_start": cursor_pos - cursor_offset, "cursor_end": cursor_pos, "metadata": {}, - } \ No newline at end of file + } From 4ac3b509a094bf60d6a8b93ff0cb55a53e1b69f4 Mon Sep 17 00:00:00 2001 From: Caio Hamamura Date: Wed, 2 Apr 2025 11:31:35 -0300 Subject: [PATCH 09/22] Update kernel.py --- mysql_kernel/kernel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mysql_kernel/kernel.py b/mysql_kernel/kernel.py index 7c2fc2f..6a24a14 100644 --- a/mysql_kernel/kernel.py +++ b/mysql_kernel/kernel.py @@ -15,7 +15,7 @@ class FixedWidthHtmlFormatter(HtmlFormatter): - def wrap(self, source, outfile=None): + def wrap(self, source, *, include_div): return self._wrap_code(source) def _wrap_code(self, source): From 85b17c4d05435a0046d426c4d8d909865031a62a Mon Sep 17 00:00:00 2001 From: Caio Hamamura Date: Wed, 2 Apr 2025 11:35:22 -0300 Subject: [PATCH 10/22] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c8c4d58..9021eda 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ def readme(): author_email='hourout@163.com', keywords=['jupyter_kernel', 'mysql_kernel'], license='Apache License Version 2.0', - install_requires=['pymysql', 'sqlalchemy', 'pandas', 'jupyter'], + install_requires=['pymysql', 'sqlalchemy', 'pandas', 'jupyter','pygments>=2.12'], classifiers = [ 'Framework :: IPython', 'License :: OSI Approved :: Apache Software License', From eb4fe07c86c71a4ff234f6365de4f223340abe36 Mon Sep 17 00:00:00 2001 From: Caio Hamamura Date: Wed, 2 Apr 2025 11:41:04 -0300 Subject: [PATCH 11/22] Update kernel.py --- mysql_kernel/kernel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mysql_kernel/kernel.py b/mysql_kernel/kernel.py index 6a24a14..6cd86fa 100644 --- a/mysql_kernel/kernel.py +++ b/mysql_kernel/kernel.py @@ -190,7 +190,7 @@ def do_execute(self, code, silent, store_history=True, user_expressions=None, al return self.handle_error(e) def handle_error(self, e): - msg = re.search(r'\d+,[^"]*"([^"]+)', e.args[0]).group(1) + msg = re.search(r'\d+,[^"']*["']([^"']+)', e.args[0]).group(1) # Convert to HTML with Pygments formatter = FixedWidthHtmlFormatter(full=True, style=ThisStyle, traceback=False) From 002cf5adb4a08f684ebba86dc1b1417e40e26427 Mon Sep 17 00:00:00 2001 From: Caio Hamamura Date: Wed, 2 Apr 2025 11:44:10 -0300 Subject: [PATCH 12/22] Update kernel.py --- mysql_kernel/kernel.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mysql_kernel/kernel.py b/mysql_kernel/kernel.py index 6cd86fa..2d46fca 100644 --- a/mysql_kernel/kernel.py +++ b/mysql_kernel/kernel.py @@ -190,7 +190,10 @@ def do_execute(self, code, silent, store_history=True, user_expressions=None, al return self.handle_error(e) def handle_error(self, e): - msg = re.search(r'\d+,[^"']*["']([^"']+)', e.args[0]).group(1) + search_res = re.search(r'\d+,[^"']*["']([^"']+)', e.args[0]).group(1) + msg = str(e) + if search_res and search_res.last_index >= 1: + msg = search_res.group(1) # Convert to HTML with Pygments formatter = FixedWidthHtmlFormatter(full=True, style=ThisStyle, traceback=False) From 9fe654399abdbd493fc0dfb2e011dc743cb0bd2c Mon Sep 17 00:00:00 2001 From: Caio Hamamura Date: Wed, 2 Apr 2025 11:50:35 -0300 Subject: [PATCH 13/22] Update kernel.py --- mysql_kernel/kernel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mysql_kernel/kernel.py b/mysql_kernel/kernel.py index 2d46fca..67e1f9f 100644 --- a/mysql_kernel/kernel.py +++ b/mysql_kernel/kernel.py @@ -190,7 +190,7 @@ def do_execute(self, code, silent, store_history=True, user_expressions=None, al return self.handle_error(e) def handle_error(self, e): - search_res = re.search(r'\d+,[^"']*["']([^"']+)', e.args[0]).group(1) + search_res = re.search(r'\d+,[^"\']*["\']([^"\']+)', e.args[0]).group(1) msg = str(e) if search_res and search_res.last_index >= 1: msg = search_res.group(1) From 25d946ca0c133cd5b6c5f34854a9303583e3cdaa Mon Sep 17 00:00:00 2001 From: caiohamamura Date: Wed, 2 Apr 2025 12:16:51 -0300 Subject: [PATCH 14/22] Fix for robustness against errors --- mysql_kernel/kernel.py | 6 +++--- mysql_kernel/pygment_error_lexer.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mysql_kernel/kernel.py b/mysql_kernel/kernel.py index 67e1f9f..30de3f3 100644 --- a/mysql_kernel/kernel.py +++ b/mysql_kernel/kernel.py @@ -15,7 +15,7 @@ class FixedWidthHtmlFormatter(HtmlFormatter): - def wrap(self, source, *, include_div): + def wrap(self, source): return self._wrap_code(source) def _wrap_code(self, source): @@ -190,9 +190,9 @@ def do_execute(self, code, silent, store_history=True, user_expressions=None, al return self.handle_error(e) def handle_error(self, e): - search_res = re.search(r'\d+,[^"\']*["\']([^"\']+)', e.args[0]).group(1) + search_res = re.search(r'\d+,[^"\']*["\']([^"\']+)', e.args[0]) msg = str(e) - if search_res and search_res.last_index >= 1: + if search_res and search_res.lastindex >= 1: msg = search_res.group(1) # Convert to HTML with Pygments diff --git a/mysql_kernel/pygment_error_lexer.py b/mysql_kernel/pygment_error_lexer.py index bc5cd1e..23a43cc 100644 --- a/mysql_kernel/pygment_error_lexer.py +++ b/mysql_kernel/pygment_error_lexer.py @@ -16,7 +16,7 @@ class SqlErrorLexer(RegexLexer): 'root': [ (r"(ERROR)(.*Table ')([^']+)", bygroups(Token.Generic.Deleted, Token.Generic, Token.Name.Class)), (r"(ERROR)(.*column ')([^']+)(.*in ')([^']+)", bygroups(Token.Generic.Deleted, Token.Generic, Token.Name.Class, Token.Generic, Token.Name.Builtin)), - (r"(ERROR)(.*near ')([^']+)(.*line [0-9]+)", bygroups(Token.Generic.Deleted, Token.Generic, Token.Name.Builtin, Token.Number)), + (r"(ERROR)(.*near ['\"])([^'\"]+)(.*line [0-9]+)", bygroups(Token.Generic.Deleted, Token.Generic, Token.Name.Builtin, Token.Number)), (r"(ERROR)(.*database ')([^']+)", bygroups(Token.Generic.Deleted, Token.Generic, Token.Name.Class)), ] } From e58731e2766279076f5177c267d9f143f3f8e4f0 Mon Sep 17 00:00:00 2001 From: caiohamamura Date: Wed, 2 Apr 2025 13:24:20 -0300 Subject: [PATCH 15/22] Make it usable with postgresql --- mysql_kernel/kernel.py | 15 +++++++++------ mysql_kernel/pygment_error_lexer.py | 14 ++++++++++---- setup.py | 2 +- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/mysql_kernel/kernel.py b/mysql_kernel/kernel.py index 30de3f3..2888f06 100644 --- a/mysql_kernel/kernel.py +++ b/mysql_kernel/kernel.py @@ -109,7 +109,7 @@ def insert_into(self, query): def use_db(self, query): new_database = re.match("use ([^ ]+)", query, re.IGNORECASE).group(1) - self.engine = sa.create_engine(self.engine.url.set(database=new_database)) + self.engine = sa.create_engine(self.engine.url.set(database=new_database), isolation_level='AUTOCOMMIT') self.autocompleter = SQLAutocompleter(engine=self.engine, log=self.log) return self.generic_ddl(query, 'Changed to database %s successfully.') @@ -128,13 +128,13 @@ def do_execute(self, code, silent, store_history=True, user_expressions=None, al v = re.sub('\n* *--.*\n', '', v) # remove comments l = v.lower() if len(l)>0: - if l.startswith('mysql://'): + if re.search('\w+://', l): if l.count('@')>1: self.output("Connection failed, The Mysql address cannot have two '@'.") else: if v.startswith('mysql://'): v = v.replace('mysql://', 'mysql+pymysql://') - self.engine = sa.create_engine(v) + self.engine = sa.create_engine(v, isolation_level='AUTOCOMMIT') self.autocompleter = SQLAutocompleter(engine=self.engine, log=self.log) self.output('Connected successfully!') @@ -190,13 +190,16 @@ def do_execute(self, code, silent, store_history=True, user_expressions=None, al return self.handle_error(e) def handle_error(self, e): - search_res = re.search(r'\d+,[^"\']*["\']([^"\']+)', e.args[0]) + search_res = re.search(r'\d+, *["\'](.*)(?=["\']\))', e.args[0]) msg = str(e) - if search_res and search_res.lastindex >= 1: + if search_res and len(search_res.groups()) > 0: msg = search_res.group(1) + msg = re.sub(r'\[SQL:.*', '', msg) + msg = re.sub(r'\(Background on this error at.*', '', msg) + # Convert to HTML with Pygments - formatter = FixedWidthHtmlFormatter(full=True, style=ThisStyle, traceback=False) + formatter = FixedWidthHtmlFormatter(noclasses=True, style=ThisStyle, traceback=False) tb_html = highlight("ERROR: " + msg, SqlErrorLexer(), formatter) tb_terminal = highlight(msg, SqlErrorLexer(), TerminalFormatter()) diff --git a/mysql_kernel/pygment_error_lexer.py b/mysql_kernel/pygment_error_lexer.py index 23a43cc..8968f0a 100644 --- a/mysql_kernel/pygment_error_lexer.py +++ b/mysql_kernel/pygment_error_lexer.py @@ -1,5 +1,6 @@ from pygments.lexer import RegexLexer, bygroups from pygments.token import * +import re __all__ = ['SqlErrorLexer'] @@ -12,12 +13,17 @@ class SqlErrorLexer(RegexLexer): aliases = ['sqlerror'] filenames = ['*.sqlerror'] + flags = re.IGNORECASE tokens = { 'root': [ - (r"(ERROR)(.*Table ')([^']+)", bygroups(Token.Generic.Deleted, Token.Generic, Token.Name.Class)), - (r"(ERROR)(.*column ')([^']+)(.*in ')([^']+)", bygroups(Token.Generic.Deleted, Token.Generic, Token.Name.Class, Token.Generic, Token.Name.Builtin)), - (r"(ERROR)(.*near ['\"])([^'\"]+)(.*line [0-9]+)", bygroups(Token.Generic.Deleted, Token.Generic, Token.Name.Builtin, Token.Number)), - (r"(ERROR)(.*database ')([^']+)", bygroups(Token.Generic.Deleted, Token.Generic, Token.Name.Class)), + (r"^ERROR", Token.Generic.Deleted), + (r"(?<=table [\"'])[^\"']+", Token.Name.Class), + (r"(?<=relation [\"'])[^\"']+", Token.Name.Class), + (r"(?<=column [\"'])[^\"']+", Token.Name.Class), + (r"(?<=column )[^ \"]+", Token.Name.Class), + (r"(?<=database [\"'])[^\"']+", Token.Name.Class), + (r"(?<=near [\"'])[^\"']+", Token.Name.Builtin), + (r"line [0-9]+", Token.Number), ] } diff --git a/setup.py b/setup.py index 9021eda..d056ac7 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ def readme(): return f.read() setup(name='mysql_kernel', - version='0.4.1', + version='0.5.0', description='A mysql kernel for Jupyter.', long_description=readme(), long_description_content_type='text/markdown', From 4719b512fad97767cdb111d0884243f0032fe5ed Mon Sep 17 00:00:00 2001 From: caiohamamura Date: Tue, 15 Apr 2025 18:41:41 -0300 Subject: [PATCH 16/22] Make duckdb usable --- mysql_kernel/kernel.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/mysql_kernel/kernel.py b/mysql_kernel/kernel.py index 2888f06..5845132 100644 --- a/mysql_kernel/kernel.py +++ b/mysql_kernel/kernel.py @@ -109,7 +109,11 @@ def insert_into(self, query): def use_db(self, query): new_database = re.match("use ([^ ]+)", query, re.IGNORECASE).group(1) - self.engine = sa.create_engine(self.engine.url.set(database=new_database), isolation_level='AUTOCOMMIT') + if self.engine.url.engine == 'duckdb': + self.engine = sa.create_engine(self.engine.url.set(database=new_database)) + else: + self.engine = sa.create_engine(self.engine.url.set(database=new_database), isolation_level='AUTOCOMMIT') + self.autocompleter = SQLAutocompleter(engine=self.engine, log=self.log) return self.generic_ddl(query, 'Changed to database %s successfully.') @@ -134,7 +138,10 @@ def do_execute(self, code, silent, store_history=True, user_expressions=None, al else: if v.startswith('mysql://'): v = v.replace('mysql://', 'mysql+pymysql://') - self.engine = sa.create_engine(v, isolation_level='AUTOCOMMIT') + if (v.startswith('duckdb')): + self.engine = sa.create_engine(v) + else: + self.engine = sa.create_engine(v, isolation_level='AUTOCOMMIT') self.autocompleter = SQLAutocompleter(engine=self.engine, log=self.log) self.output('Connected successfully!') From ce8915722e9223040c14b6277c2749afd7bf706c Mon Sep 17 00:00:00 2001 From: caiohamamura Date: Tue, 15 Apr 2025 18:42:43 -0300 Subject: [PATCH 17/22] Update details and bump version 0.5.1 --- mysql_kernel/__init__.py | 2 +- setup.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mysql_kernel/__init__.py b/mysql_kernel/__init__.py index 40c89a9..beb694e 100644 --- a/mysql_kernel/__init__.py +++ b/mysql_kernel/__init__.py @@ -1,4 +1,4 @@ """A mysql kernel for Jupyter""" from .kernel import __version__ -__author__ = 'JinQing Lee' +__author__ = 'Caio Hamamura' diff --git a/setup.py b/setup.py index d056ac7..56b9bd5 100644 --- a/setup.py +++ b/setup.py @@ -10,13 +10,13 @@ def readme(): return f.read() setup(name='mysql_kernel', - version='0.5.0', - description='A mysql kernel for Jupyter.', + version='0.5.1', + description='A generic kernel for Jupyter forked from JinQing Lees mysql_kernel', long_description=readme(), long_description_content_type='text/markdown', url='https://github.com/Hourout/mysql_kernel', - author='JinQing Lee', - author_email='hourout@163.com', + author='Caio Hamamura', + author_email='caiohamamura@gmail.com', keywords=['jupyter_kernel', 'mysql_kernel'], license='Apache License Version 2.0', install_requires=['pymysql', 'sqlalchemy', 'pandas', 'jupyter','pygments>=2.12'], From 1a38028c6795f1909e0200f0d38b939a2f1cc43b Mon Sep 17 00:00:00 2001 From: caiohamamura Date: Tue, 15 Apr 2025 18:44:19 -0300 Subject: [PATCH 18/22] Use raw strings for regex --- mysql_kernel/kernel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mysql_kernel/kernel.py b/mysql_kernel/kernel.py index 5845132..039db7a 100644 --- a/mysql_kernel/kernel.py +++ b/mysql_kernel/kernel.py @@ -132,7 +132,7 @@ def do_execute(self, code, silent, store_history=True, user_expressions=None, al v = re.sub('\n* *--.*\n', '', v) # remove comments l = v.lower() if len(l)>0: - if re.search('\w+://', l): + if re.search(r'\w+://', l): if l.count('@')>1: self.output("Connection failed, The Mysql address cannot have two '@'.") else: From c79d5be0a5aa80968cd52951062d7d74ebaae769 Mon Sep 17 00:00:00 2001 From: caiohamamura Date: Tue, 15 Apr 2025 20:34:03 -0300 Subject: [PATCH 19/22] Fix url drivername --- mysql_kernel/kernel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mysql_kernel/kernel.py b/mysql_kernel/kernel.py index 039db7a..3f7808e 100644 --- a/mysql_kernel/kernel.py +++ b/mysql_kernel/kernel.py @@ -109,7 +109,7 @@ def insert_into(self, query): def use_db(self, query): new_database = re.match("use ([^ ]+)", query, re.IGNORECASE).group(1) - if self.engine.url.engine == 'duckdb': + if self.engine.url.drivername == 'duckdb': self.engine = sa.create_engine(self.engine.url.set(database=new_database)) else: self.engine = sa.create_engine(self.engine.url.set(database=new_database), isolation_level='AUTOCOMMIT') From af29f03df7b556afe7968774758d7c969fde4662 Mon Sep 17 00:00:00 2001 From: caiohamamura Date: Mon, 21 Apr 2025 19:29:51 -0300 Subject: [PATCH 20/22] Localize messages --- MANIFEST.in | 3 + mysql_kernel/i18n.py | 27 ++++++ mysql_kernel/kernel.py | 46 +++++---- .../locale/pt_br/LC_MESSAGES/messages.mo | Bin 0 -> 1971 bytes .../locale/pt_br/LC_MESSAGES/messages.po | 89 ++++++++++++++++++ mysql_kernel/messages.pot | 89 ++++++++++++++++++ setup.py | 1 + 7 files changed, 235 insertions(+), 20 deletions(-) create mode 100644 MANIFEST.in create mode 100644 mysql_kernel/i18n.py create mode 100644 mysql_kernel/locale/pt_br/LC_MESSAGES/messages.mo create mode 100644 mysql_kernel/locale/pt_br/LC_MESSAGES/messages.po create mode 100644 mysql_kernel/messages.pot diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..ea6a5d1 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +recursive-include mysql_kernel/locale *.mo +recursive-include mysql_kernel/locale *.po + diff --git a/mysql_kernel/i18n.py b/mysql_kernel/i18n.py new file mode 100644 index 0000000..b85df78 --- /dev/null +++ b/mysql_kernel/i18n.py @@ -0,0 +1,27 @@ +# i18n.py +import gettext +import locale +import os + +def get_translator(lang=None): + base_dir = os.path.dirname(os.path.abspath(__file__)) + locale_dir = os.path.join(base_dir, 'locale') + if lang is None: + lang = locale.getdefaultlocale()[0] + if gettext.find('messages', localedir=locale_dir, languages=[lang]) is None: + lang = lang[0:2] + + return gettext.translation( + 'messages', + localedir=locale_dir, + languages=[lang], + fallback=True + ).gettext + +def has_translation(lang=None): + lang = locale.getdefaultlocale()[0] + try: + get_translator(lang) + return f"Tem tradução {lang}" + except: + return "False" \ No newline at end of file diff --git a/mysql_kernel/kernel.py b/mysql_kernel/kernel.py index 3f7808e..bdfff0f 100644 --- a/mysql_kernel/kernel.py +++ b/mysql_kernel/kernel.py @@ -10,6 +10,10 @@ from pygments.formatters import HtmlFormatter, TerminalFormatter from .pygment_error_lexer import SqlErrorLexer from .style import ThisStyle +from .i18n import get_translator + +_ = get_translator() + __version__ = '0.4.1' @@ -41,8 +45,8 @@ def __init__(self, **kwargs): Kernel.__init__(self, **kwargs) self.engine = False self.log.setLevel(logging.DEBUG) - print('Mysql kernel initialized') - self.log.info('Mysql kernel initialized') + print(_('Mysql kernel initialized')) + self.log.info(_('Mysql kernel initialized')) def output(self, output, plain_text = None): if plain_text == None: @@ -78,7 +82,8 @@ def generic_ddl(self, query, msg): object_name = query.split()[1] rows_affected = result.rowcount if result.rowcount > 0: - self.output((msg + '\nRows affected: %d.') % (object_name, rows_affected)) + msgpart = _('Rows affected') + self.output((msg + f'\n{msgpart}: %d.') % (object_name, rows_affected)) else: self.output(msg % (object_name)) return @@ -86,26 +91,26 @@ def generic_ddl(self, query, msg): return self.handle_error(msg) def create_db(self, query): - return self.generic_ddl(query, 'Database %s created successfully.') + return self.generic_ddl(query, _('Database %s created successfully.')) def drop_db(self, query): - return self.generic_ddl(query, 'Database %s dropped successfully.') + return self.generic_ddl(query, _('Database %s dropped successfully.')) def create_table(self, query): - return self.generic_ddl(query, 'Table %s created successfully.') + return self.generic_ddl(query, _('Table %s created successfully.')) def drop_table(self, query): - return self.generic_ddl(query, 'Table %s dropped successfully.') + return self.generic_ddl(query, _('Table %s dropped successfully.')) def delete(self, query): - return self.generic_ddl(query, 'Data deleted from %s successfully.') + return self.generic_ddl(query, _('Data deleted from %s successfully.')) def alter_table(self, query): - return self.generic_ddl(query, 'Table %s altered successfully.') + return self.generic_ddl(query, _('Table %s altered successfully.')) def insert_into(self, query): - return self.generic_ddl(query, 'Data inserted into %s successfully.') + return self.generic_ddl(query, _('Data inserted into %s successfully.')) def use_db(self, query): new_database = re.match("use ([^ ]+)", query, re.IGNORECASE).group(1) @@ -115,7 +120,7 @@ def use_db(self, query): self.engine = sa.create_engine(self.engine.url.set(database=new_database), isolation_level='AUTOCOMMIT') self.autocompleter = SQLAutocompleter(engine=self.engine, log=self.log) - return self.generic_ddl(query, 'Changed to database %s successfully.') + return self.generic_ddl(query, _('Changed to database %s successfully.')) def do_execute(self, code, silent, store_history=True, user_expressions=None, allow_stdin=False): self.silent = silent @@ -134,7 +139,7 @@ def do_execute(self, code, silent, store_history=True, user_expressions=None, al if len(l)>0: if re.search(r'\w+://', l): if l.count('@')>1: - self.output("Connection failed, The Mysql address cannot have two '@'.") + self.output(_("Connection failed, The Mysql address cannot have two '@'.")) else: if v.startswith('mysql://'): v = v.replace('mysql://', 'mysql+pymysql://') @@ -143,9 +148,9 @@ def do_execute(self, code, silent, store_history=True, user_expressions=None, al else: self.engine = sa.create_engine(v, isolation_level='AUTOCOMMIT') self.autocompleter = SQLAutocompleter(engine=self.engine, log=self.log) - self.output('Connected successfully!') - - + self.output(_('Connected successfully!')) + elif self.engine == False: + self.output(_('Please connect to a database first!')) elif l.startswith('create database '): res = self.create_db(v) elif l.startswith('drop database '): @@ -170,8 +175,9 @@ def do_execute(self, code, silent, store_history=True, user_expressions=None, al results = pd.read_sql(v, self.engine) results_raw = results.to_string() if results.shape[0] == 1000: + msg_part = _('Results truncated to 1000 (explicitly add LIMIT to display beyond that)') output = f''' -

Results limitted to 1000 (explicitly add LIMIT to display beyond that)

+

{msg_part}

{results.to_html()} ''' else: @@ -183,12 +189,13 @@ def do_execute(self, code, silent, store_history=True, user_expressions=None, al results = pd.DataFrame(execution.fetchall(), columns=execution.keys()) output = results.to_html() elif execution.rowcount > 0: - output = f'Rows affected: {execution.rowcount}' + msg_part = _('Rows affected') + output = f'{msg_part}: {execution.rowcount}' else: - output = 'No rows affected' + output = _('No rows affected') output = f'''
{output}
''' else: - output = 'Unable to connect to Mysql server. Check that the server is running.' + output = _('Unable to connect to Mysql server. Check that the server is running.') self.output(output, plain_text = results_raw if results_raw else output) if res and 'status' in res.keys() and res['status'] == 'error': return res @@ -225,7 +232,6 @@ def handle_error(self, e): return {"status": "error", "execution_count": self.execution_count} def do_complete(self, code, cursor_pos): - self.log.info('Try to autocomplete') if not self.autocompleter: return {"status": "ok", "matches": []} completion_list = self.autocompleter.get_completions(code, cursor_pos) diff --git a/mysql_kernel/locale/pt_br/LC_MESSAGES/messages.mo b/mysql_kernel/locale/pt_br/LC_MESSAGES/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..018198f460ac83a10b88e9bd7c2391404258a771 GIT binary patch literal 1971 zcmZ{k&u<$=6vqcBKWcs>@k`>zOG6bZWw%aB)M5&XoU|pvPC{@&f&&`wKHEcgXWW@x zH|8$@!Ic9C4v0#Tkjjle09oSBkqaj-{0ZFn&a9m{Nm*(9*_nBs_kC~P+rOSW^PRx+ zEXFGscQIbXSbhLMc)kMP0KW$P`Y+(y;P2o$P&_EaBCrT9fH$U8;7gc)13m-Z1-;#` z;0^Fk@JVptAt7D^FHX5VWe#p){af%deEk7>zh@s7;stO6Y=WPHi{KC7Iq)~|Joq=b z1lAs@;#vit$9xC81Twe-eh)qk{sX=a{tLbezWiu4zYKnc`6nP`h^H_h{qT7VZ;&R= zVw}P7XFZPLV}1ey#|cO|e0AWLXCJ8&&E3<4I ziw&bSCk4#r*B55%%4q6Im2tXEodHvO>~3cy)70V!N~G4Npn)7Q6?Y6RtuMu56TT?r zjQycKYx2`hgNf3Pt+!Dc3Qr01HZ#eIWqDF!8n zs>sGZH`Ll{Z*@Z6l^bSqOkExujhh(A;)2*a=D8!gS-2OO6>Lw1aJYT{92{=nKLIy& zI2}6YBCE23I;KIj-JNKUN6P;x!i{o+R_m*4QT?rGb%j>e8f*2aeyLus zwWKSej+NSFvM{zm8&Vm1U*<9|t)#07ri1Bhy`Q5Z|Bqo9NBr*J+{#`otP9EYtWUhDr%x#rczH1Bn>4(Fw~VWKxd$^QsaEZ z<5H5WoZL$#UUT1p=qeMM5($UZ{PKdhF0+9o?Tt0>Pkusz4Y5v9Hj@LKY|x$`eb^$6 zEgBklf+30uJGjqjb9&djU%ACAjDnCk@0&qhXipWaLkAxd*gPGIRVVbu;CJO zd|a{kplZ-V{pi#rbSh31+pGs=E-6zQ!OI>aFH+#5@m(4jyBv~%4_Jiul~m>@(>$~) z_f;_K-w<5Y!alfgH&qkM6mF5(Y5UH4TIQ$*t_*o!b(JO5pSvJhUXlNX1W8Vy7dewY zouQQ>^LUs?`-xB1|L#tcbYg$oUuw@NTGdn0_&d>NojEJ+jk7Kyc03B-g>RXugP b^c{^V@ogQp*%gx?iF4EdC*|Y-r^ezxunb0J literal 0 HcmV?d00001 diff --git a/mysql_kernel/locale/pt_br/LC_MESSAGES/messages.po b/mysql_kernel/locale/pt_br/LC_MESSAGES/messages.po new file mode 100644 index 0000000..425fb76 --- /dev/null +++ b/mysql_kernel/locale/pt_br/LC_MESSAGES/messages.po @@ -0,0 +1,89 @@ +# Tradução em Português do Brasil para kernel jupyter para SQL +# Copyright (C) 2025 +# Este arquivo é distribuído sob a mesma licença do pacote. +# Tradutor: Caio Hamamura , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: kernel-mysql 1.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-04-21 15:46-0300\n" +"PO-Revision-Date: 2025-04-21 15:50-0300\n" +"Last-Translator: Caio Hamamura \n" +"Language-Team: Português Brasileiro \n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: kernel.py:51 kernel.py:52 +msgid "Mysql kernel initialized" +msgstr "Kernel do MySQL inicializado" + +#: kernel.py:88 kernel.py:195 +msgid "Rows affected" +msgstr "Linhas afetadas" + +#: kernel.py:97 +#, python-format +msgid "Database %s created successfully." +msgstr "Banco de dados %s criado com sucesso." + +#: kernel.py:101 +#, python-format +msgid "Database %s dropped successfully." +msgstr "Banco de dados %s removido com sucesso." + +#: kernel.py:104 +#, python-format +msgid "Table %s created successfully." +msgstr "Tabela %s criada com sucesso." + +#: kernel.py:107 +#, python-format +msgid "Table %s dropped successfully." +msgstr "Tabela %s removida com sucesso." + +#: kernel.py:110 +#, python-format +msgid "Data deleted from %s successfully." +msgstr "Dados excluídos de %s com sucesso." + +#: kernel.py:113 +#, python-format +msgid "Table %s altered successfully." +msgstr "Tabela %s alterada com sucesso." + +#: kernel.py:116 +#, python-format +msgid "Data inserted into %s successfully." +msgstr "Dados inseridos em %s com sucesso." + +#: kernel.py:126 +#, python-format +msgid "Changed to database %s successfully." +msgstr "Mudança para o banco de dados %s concluída com sucesso." + +#: kernel.py:145 +msgid "Connection failed, The Mysql address cannot have two '@'." +msgstr "Falha na conexão: o endereço do MySQL não pode conter dois '@'." + +#: kernel.py:154 +msgid "Connected successfully!" +msgstr "Conectado com sucesso!" + +#: kernel.py:156 +msgid "Please connect to a database first!" +msgstr "Por favor, conecte-se a um banco de dados primeiro!" + +#: kernel.py:181 +msgid "Results truncated to 1000 (explicitly add LIMIT to display beyond that)" +msgstr "Resultados truncados para 1000 (adicione LIMIT explicitamente para exibir mais)" + +#: kernel.py:198 +msgid "No rows affected" +msgstr "Nenhuma linha afetada" + +#: kernel.py:201 +msgid "Unable to connect to Mysql server. Check that the server is running." +msgstr "Não foi possível conectar ao servidor MySQL. Verifique se o servidor está em execução." diff --git a/mysql_kernel/messages.pot b/mysql_kernel/messages.pot new file mode 100644 index 0000000..c993b9f --- /dev/null +++ b/mysql_kernel/messages.pot @@ -0,0 +1,89 @@ +# Translation in ... for SQL jupyter kernel +# Copyright (C) 2025 +# This file is distributed under the same license of the code +# Translator: Caio Hamamura , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: kernel-mysql 1.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-04-21 15:46-0300\n" +"PO-Revision-Date: 2025-04-21 15:50-0300\n" +"Last-Translator: ... <...@...>\n" +"Language-Team: \n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: kernel.py:51 kernel.py:52 +msgid "Mysql kernel initialized" +msgstr "" + +#: kernel.py:88 kernel.py:195 +msgid "Rows affected" +msgstr "" + +#: kernel.py:97 +#, python-format +msgid "Database %s created successfully." +msgstr "" + +#: kernel.py:101 +#, python-format +msgid "Database %s dropped successfully." +msgstr "" + +#: kernel.py:104 +#, python-format +msgid "Table %s created successfully." +msgstr "" + +#: kernel.py:107 +#, python-format +msgid "Table %s dropped successfully." +msgstr "" + +#: kernel.py:110 +#, python-format +msgid "Data deleted from %s successfully." +msgstr "" + +#: kernel.py:113 +#, python-format +msgid "Table %s altered successfully." +msgstr "" + +#: kernel.py:116 +#, python-format +msgid "Data inserted into %s successfully." +msgstr "" + +#: kernel.py:126 +#, python-format +msgid "Changed to database %s successfully." +msgstr "" + +#: kernel.py:145 +msgid "Connection failed, The Mysql address cannot have two '@'." +msgstr "" + +#: kernel.py:154 +msgid "Connected successfully!" +msgstr "" + +#: kernel.py:156 +msgid "Please connect to a database first!" +msgstr "" + +#: kernel.py:181 +msgid "Results truncated to 1000 (explicitly add LIMIT to display beyond that)" +msgstr "" + +#: kernel.py:198 +msgid "No rows affected" +msgstr "" + +#: kernel.py:201 +msgid "Unable to connect to Mysql server. Check that the server is running." +msgstr "" diff --git a/setup.py b/setup.py index 56b9bd5..7200dc9 100644 --- a/setup.py +++ b/setup.py @@ -34,5 +34,6 @@ def readme(): 'Topic :: System :: Shells', ], packages=['mysql_kernel'], + include_package_data=True, zip_safe=False ) From d11ee4f547a621073d8918281be2ab5d83f3e4e7 Mon Sep 17 00:00:00 2001 From: caiohamamura Date: Mon, 21 Apr 2025 19:30:27 -0300 Subject: [PATCH 21/22] Bump version 0.6.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7200dc9..a931ea8 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ def readme(): return f.read() setup(name='mysql_kernel', - version='0.5.1', + version='0.6.0', description='A generic kernel for Jupyter forked from JinQing Lees mysql_kernel', long_description=readme(), long_description_content_type='text/markdown', From c5bff5310a7a944503df986564b51dd692811a1d Mon Sep 17 00:00:00 2001 From: caiohamamura Date: Tue, 17 Jun 2025 11:48:57 -0300 Subject: [PATCH 22/22] Update messages --- .../locale/pt_br/LC_MESSAGES/messages.mo | Bin 1971 -> 1971 bytes .../locale/pt_br/LC_MESSAGES/messages.po | 35 ++++------- mysql_kernel/messages.pot | 55 +++++++++--------- 3 files changed, 40 insertions(+), 50 deletions(-) diff --git a/mysql_kernel/locale/pt_br/LC_MESSAGES/messages.mo b/mysql_kernel/locale/pt_br/LC_MESSAGES/messages.mo index 018198f460ac83a10b88e9bd7c2391404258a771..27a97afc162189a606f5e385170dcf4d48083632 100644 GIT binary patch delta 17 YcmdnYznOo7DhrF5uA%v6O_nZZ04`_*ng9R* delta 17 YcmdnYznOo7DhrE=u94wpO_nZZ04_NMlK=n! diff --git a/mysql_kernel/locale/pt_br/LC_MESSAGES/messages.po b/mysql_kernel/locale/pt_br/LC_MESSAGES/messages.po index 425fb76..d8570b3 100644 --- a/mysql_kernel/locale/pt_br/LC_MESSAGES/messages.po +++ b/mysql_kernel/locale/pt_br/LC_MESSAGES/messages.po @@ -1,14 +1,15 @@ -# Tradução em Português do Brasil para kernel jupyter para SQL +# A jupyter kernel for mysql. # Copyright (C) 2025 -# Este arquivo é distribuído sob a mesma licença do pacote. -# Tradutor: Caio Hamamura , 2025. +# This file is distributed under the same license as the mysql_kernel package. +# Caio Hamamura , 2025. # +#, fuzzy msgid "" msgstr "" "Project-Id-Version: kernel-mysql 1.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-04-21 15:46-0300\n" -"PO-Revision-Date: 2025-04-21 15:50-0300\n" +"POT-Creation-Date: 2025-04-21 19:29-0300\n" +"PO-Revision-Date: 2025-06-17 15:50-0300\n" "Last-Translator: Caio Hamamura \n" "Language-Team: Português Brasileiro \n" "Language: pt_BR\n" @@ -16,74 +17,62 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: kernel.py:51 kernel.py:52 msgid "Mysql kernel initialized" msgstr "Kernel do MySQL inicializado" -#: kernel.py:88 kernel.py:195 msgid "Rows affected" msgstr "Linhas afetadas" -#: kernel.py:97 #, python-format msgid "Database %s created successfully." msgstr "Banco de dados %s criado com sucesso." -#: kernel.py:101 #, python-format msgid "Database %s dropped successfully." msgstr "Banco de dados %s removido com sucesso." -#: kernel.py:104 #, python-format msgid "Table %s created successfully." msgstr "Tabela %s criada com sucesso." -#: kernel.py:107 #, python-format msgid "Table %s dropped successfully." msgstr "Tabela %s removida com sucesso." -#: kernel.py:110 #, python-format msgid "Data deleted from %s successfully." msgstr "Dados excluídos de %s com sucesso." -#: kernel.py:113 #, python-format msgid "Table %s altered successfully." msgstr "Tabela %s alterada com sucesso." -#: kernel.py:116 #, python-format msgid "Data inserted into %s successfully." msgstr "Dados inseridos em %s com sucesso." -#: kernel.py:126 #, python-format msgid "Changed to database %s successfully." msgstr "Mudança para o banco de dados %s concluída com sucesso." -#: kernel.py:145 msgid "Connection failed, The Mysql address cannot have two '@'." msgstr "Falha na conexão: o endereço do MySQL não pode conter dois '@'." -#: kernel.py:154 msgid "Connected successfully!" msgstr "Conectado com sucesso!" -#: kernel.py:156 msgid "Please connect to a database first!" msgstr "Por favor, conecte-se a um banco de dados primeiro!" -#: kernel.py:181 msgid "Results truncated to 1000 (explicitly add LIMIT to display beyond that)" -msgstr "Resultados truncados para 1000 (adicione LIMIT explicitamente para exibir mais)" +msgstr "" +"Resultados truncados para 1000 (adicione LIMIT explicitamente para exibir " +"mais)" -#: kernel.py:198 msgid "No rows affected" msgstr "Nenhuma linha afetada" -#: kernel.py:201 msgid "Unable to connect to Mysql server. Check that the server is running." -msgstr "Não foi possível conectar ao servidor MySQL. Verifique se o servidor está em execução." +msgstr "" +"Não foi possível conectar ao servidor MySQL. Verifique se o servidor está em " +"execução." diff --git a/mysql_kernel/messages.pot b/mysql_kernel/messages.pot index c993b9f..ec28f4a 100644 --- a/mysql_kernel/messages.pot +++ b/mysql_kernel/messages.pot @@ -1,89 +1,90 @@ -# Translation in ... for SQL jupyter kernel -# Copyright (C) 2025 -# This file is distributed under the same license of the code -# Translator: Caio Hamamura , 2025. +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. # +#, fuzzy msgid "" msgstr "" -"Project-Id-Version: kernel-mysql 1.0\n" +"Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-04-21 15:46-0300\n" -"PO-Revision-Date: 2025-04-21 15:50-0300\n" -"Last-Translator: ... <...@...>\n" -"Language-Team: \n" -"Language: pt_BR\n" +"POT-Creation-Date: 2025-04-21 19:29-0300\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" "MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" +"Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" -#: kernel.py:51 kernel.py:52 +#: kernel.py:48 kernel.py:49 msgid "Mysql kernel initialized" msgstr "" -#: kernel.py:88 kernel.py:195 +#: kernel.py:85 kernel.py:192 msgid "Rows affected" msgstr "" -#: kernel.py:97 +#: kernel.py:94 #, python-format msgid "Database %s created successfully." msgstr "" -#: kernel.py:101 +#: kernel.py:98 #, python-format msgid "Database %s dropped successfully." msgstr "" -#: kernel.py:104 +#: kernel.py:101 #, python-format msgid "Table %s created successfully." msgstr "" -#: kernel.py:107 +#: kernel.py:104 #, python-format msgid "Table %s dropped successfully." msgstr "" -#: kernel.py:110 +#: kernel.py:107 #, python-format msgid "Data deleted from %s successfully." msgstr "" -#: kernel.py:113 +#: kernel.py:110 #, python-format msgid "Table %s altered successfully." msgstr "" -#: kernel.py:116 +#: kernel.py:113 #, python-format msgid "Data inserted into %s successfully." msgstr "" -#: kernel.py:126 +#: kernel.py:123 #, python-format msgid "Changed to database %s successfully." msgstr "" -#: kernel.py:145 +#: kernel.py:142 msgid "Connection failed, The Mysql address cannot have two '@'." msgstr "" -#: kernel.py:154 +#: kernel.py:151 msgid "Connected successfully!" msgstr "" -#: kernel.py:156 +#: kernel.py:153 msgid "Please connect to a database first!" msgstr "" -#: kernel.py:181 +#: kernel.py:178 msgid "Results truncated to 1000 (explicitly add LIMIT to display beyond that)" msgstr "" -#: kernel.py:198 +#: kernel.py:195 msgid "No rows affected" msgstr "" -#: kernel.py:201 +#: kernel.py:198 msgid "Unable to connect to Mysql server. Check that the server is running." msgstr ""