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/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/__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/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/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 3370920..bdfff0f 100644
--- a/mysql_kernel/kernel.py
+++ b/mysql_kernel/kernel.py
@@ -1,10 +1,36 @@
import pandas as pd
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
+from .i18n import get_translator
+
+_ = get_translator()
__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__
@@ -18,11 +44,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)
@@ -36,51 +70,200 @@ def err(self, msg):
'execution_count':self.execution_count,
'payload':[],
'user_expressions':{}}
+
+ def generic_ddl(self, query, msg):
+ try:
+ with self.engine.begin() as con:
+ result = con.execute(sa.sql.text(query))
+ 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:
+ msgpart = _('Rows affected')
+ self.output((msg + f'\n{msgpart}: %d.') % (object_name, rows_affected))
+ else:
+ self.output(msg % (object_name))
+ return
+ except Exception as msg:
+ return self.handle_error(msg)
+
+ def create_db(self, query):
+ return self.generic_ddl(query, _('Database %s created successfully.'))
+
+
+ def drop_db(self, query):
+ return self.generic_ddl(query, _('Database %s dropped successfully.'))
+
+ def create_table(self, query):
+ return self.generic_ddl(query, _('Table %s created successfully.'))
+
+ def drop_table(self, query):
+ return self.generic_ddl(query, _('Table %s dropped successfully.'))
+
+ def delete(self, query):
+ return self.generic_ddl(query, _('Data deleted from %s successfully.'))
+
+ def alter_table(self, query):
+ return self.generic_ddl(query, _('Table %s altered successfully.'))
+
+ def insert_into(self, query):
+ 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)
+ 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')
+
+ 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
+ res = {}
output = ''
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()
+ 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 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:
- self.engine = sa.create_engine(f'mysql+py{v}')
+ if v.startswith('mysql://'):
+ v = v.replace('mysql://', 'mysql+pymysql://')
+ 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!'))
+ elif self.engine == False:
+ self.output(_('Please connect to a database first!'))
elif l.startswith('create database '):
- pd.io.sql.execute(v, con=self.engine)
+ res = self.create_db(v)
elif l.startswith('drop database '):
- pd.io.sql.execute(v, con=self.engine)
+ res = self.drop_db(v)
elif l.startswith('create table '):
- pd.io.sql.execute(v, con=self.engine)
+ res = self.create_table(v)
elif l.startswith('drop table '):
- pd.io.sql.execute(v, con=self.engine)
+ res = self.drop_table(v)
elif l.startswith('delete '):
- pd.io.sql.execute(v, con=self.engine)
+ res = self.delete(v)
elif l.startswith('alter table '):
- pd.io.sql.execute(v, con=self.engine)
+ res = self.alter_table(v)
+ elif l.startswith('use '):
+ res = self.use_db(v)
elif l.startswith('insert into '):
- pd.io.sql.execute(v, con=self.engine)
+ res = 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:
- output = pd.read_sql(f'{v} limit 1000', self.engine).to_html()
+ v = re.sub('(?{msg_part}
+ {results.to_html()}
+ '''
+ 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:
+ msg_part = _('Rows affected')
+ output = f'{msg_part}: {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)
+ 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
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):
+ search_res = re.search(r'\d+, *["\'](.*)(?=["\']\))', e.args[0])
+ msg = str(e)
+ 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(noclasses=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):
+ 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": {},
+ }
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 0000000..27a97af
Binary files /dev/null and b/mysql_kernel/locale/pt_br/LC_MESSAGES/messages.mo differ
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..d8570b3
--- /dev/null
+++ b/mysql_kernel/locale/pt_br/LC_MESSAGES/messages.po
@@ -0,0 +1,78 @@
+# A jupyter kernel for mysql.
+# Copyright (C) 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 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"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+msgid "Mysql kernel initialized"
+msgstr "Kernel do MySQL inicializado"
+
+msgid "Rows affected"
+msgstr "Linhas afetadas"
+
+#, python-format
+msgid "Database %s created successfully."
+msgstr "Banco de dados %s criado com sucesso."
+
+#, python-format
+msgid "Database %s dropped successfully."
+msgstr "Banco de dados %s removido com sucesso."
+
+#, python-format
+msgid "Table %s created successfully."
+msgstr "Tabela %s criada com sucesso."
+
+#, python-format
+msgid "Table %s dropped successfully."
+msgstr "Tabela %s removida com sucesso."
+
+#, python-format
+msgid "Data deleted from %s successfully."
+msgstr "Dados excluídos de %s com sucesso."
+
+#, python-format
+msgid "Table %s altered successfully."
+msgstr "Tabela %s alterada com sucesso."
+
+#, python-format
+msgid "Data inserted into %s successfully."
+msgstr "Dados inseridos em %s com sucesso."
+
+#, python-format
+msgid "Changed to database %s successfully."
+msgstr "Mudança para o banco de dados %s concluída com sucesso."
+
+msgid "Connection failed, The Mysql address cannot have two '@'."
+msgstr "Falha na conexão: o endereço do MySQL não pode conter dois '@'."
+
+msgid "Connected successfully!"
+msgstr "Conectado com sucesso!"
+
+msgid "Please connect to a database first!"
+msgstr "Por favor, conecte-se a um banco de dados primeiro!"
+
+msgid "Results truncated to 1000 (explicitly add LIMIT to display beyond that)"
+msgstr ""
+"Resultados truncados para 1000 (adicione LIMIT explicitamente para exibir "
+"mais)"
+
+msgid "No rows affected"
+msgstr "Nenhuma linha afetada"
+
+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..ec28f4a
--- /dev/null
+++ b/mysql_kernel/messages.pot
@@ -0,0 +1,90 @@
+# 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: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \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=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: kernel.py:48 kernel.py:49
+msgid "Mysql kernel initialized"
+msgstr ""
+
+#: kernel.py:85 kernel.py:192
+msgid "Rows affected"
+msgstr ""
+
+#: kernel.py:94
+#, python-format
+msgid "Database %s created successfully."
+msgstr ""
+
+#: kernel.py:98
+#, python-format
+msgid "Database %s dropped successfully."
+msgstr ""
+
+#: kernel.py:101
+#, python-format
+msgid "Table %s created successfully."
+msgstr ""
+
+#: kernel.py:104
+#, python-format
+msgid "Table %s dropped successfully."
+msgstr ""
+
+#: kernel.py:107
+#, python-format
+msgid "Data deleted from %s successfully."
+msgstr ""
+
+#: kernel.py:110
+#, python-format
+msgid "Table %s altered successfully."
+msgstr ""
+
+#: kernel.py:113
+#, python-format
+msgid "Data inserted into %s successfully."
+msgstr ""
+
+#: kernel.py:123
+#, python-format
+msgid "Changed to database %s successfully."
+msgstr ""
+
+#: kernel.py:142
+msgid "Connection failed, The Mysql address cannot have two '@'."
+msgstr ""
+
+#: kernel.py:151
+msgid "Connected successfully!"
+msgstr ""
+
+#: kernel.py:153
+msgid "Please connect to a database first!"
+msgstr ""
+
+#: kernel.py:178
+msgid "Results truncated to 1000 (explicitly add LIMIT to display beyond that)"
+msgstr ""
+
+#: kernel.py:195
+msgid "No rows affected"
+msgstr ""
+
+#: kernel.py:198
+msgid "Unable to connect to Mysql server. Check that the server is running."
+msgstr ""
diff --git a/mysql_kernel/pygment_error_lexer.py b/mysql_kernel/pygment_error_lexer.py
new file mode 100644
index 0000000..8968f0a
--- /dev/null
+++ b/mysql_kernel/pygment_error_lexer.py
@@ -0,0 +1,29 @@
+from pygments.lexer import RegexLexer, bygroups
+from pygments.token import *
+import re
+
+__all__ = ['SqlErrorLexer']
+
+class SqlErrorLexer(RegexLexer):
+ """
+ A lexer for highlighting errors from MySQL/MariaDB.
+ """
+
+ name = 'sqlerror'
+ aliases = ['sqlerror']
+ filenames = ['*.sqlerror']
+
+ flags = re.IGNORECASE
+ tokens = {
+ 'root': [
+ (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/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
diff --git a/setup.py b/setup.py
index c8c4d58..a931ea8 100644
--- a/setup.py
+++ b/setup.py
@@ -10,16 +10,16 @@ def readme():
return f.read()
setup(name='mysql_kernel',
- version='0.4.1',
- description='A mysql kernel for Jupyter.',
+ 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',
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'],
+ install_requires=['pymysql', 'sqlalchemy', 'pandas', 'jupyter','pygments>=2.12'],
classifiers = [
'Framework :: IPython',
'License :: OSI Approved :: Apache Software License',
@@ -34,5 +34,6 @@ def readme():
'Topic :: System :: Shells',
],
packages=['mysql_kernel'],
+ include_package_data=True,
zip_safe=False
)