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 )