├── .python-version ├── pyqt_sql_demo ├── __init__.py ├── widgets │ ├── __init__.py │ ├── text.py │ ├── main.py │ └── connection.py ├── connection │ ├── __init__.py │ ├── exceptions.py │ ├── error_handler.py │ └── model.py ├── app.py └── syntax_highlighter │ └── sql.py ├── demo.db ├── docs └── pyqt-sql-demo-rec.gif ├── .gitignore ├── .travis.yml ├── pyproject.toml ├── .github └── workflows │ └── build.yml ├── README.md └── poetry.lock /.python-version: -------------------------------------------------------------------------------- 1 | 3.8.5 2 | -------------------------------------------------------------------------------- /pyqt_sql_demo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pyqt_sql_demo/widgets/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pyqt_sql_demo/connection/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vduseev/pyqt-sql-demo/HEAD/demo.db -------------------------------------------------------------------------------- /docs/pyqt-sql-demo-rec.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vduseev/pyqt-sql-demo/HEAD/docs/pyqt-sql-demo-rec.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | .build.info 3 | __pycache__ 4 | dist/ 5 | *.egg-info 6 | 7 | # VS Code 8 | .vscode 9 | 10 | # MacOS 11 | .DS_Store 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.8" 4 | 5 | install: 6 | - pip install poetry 7 | - poetry install 8 | 9 | script: 10 | - poetry run flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 11 | - poetry run flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 12 | -------------------------------------------------------------------------------- /pyqt_sql_demo/app.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from PyQt5.QtWidgets import QApplication 3 | from pyqt_sql_demo.widgets.main import MainWindow 4 | 5 | 6 | app = None 7 | APP_NAME = "PyQt SQL Demo" 8 | 9 | 10 | def launch(): 11 | global app 12 | app = QApplication(sys.argv) 13 | app.setApplicationName(APP_NAME) 14 | 15 | w = MainWindow() 16 | w.setWindowTitle(APP_NAME) 17 | w.show() 18 | 19 | app.exec() 20 | 21 | 22 | if __name__ == "__main__": 23 | launch() 24 | -------------------------------------------------------------------------------- /pyqt_sql_demo/widgets/text.py: -------------------------------------------------------------------------------- 1 | CONNECTION_TAB_DEFAULT_TITLE = "Untitled" 2 | CONNECTION_STRING_SUPPORTED_DB_NAMES = ["SQLite"] 3 | CONNECTION_STRING_PLACEHOLDER = "Enter..." 4 | CONNECTION_STRING_DEFAULT = "demo.db" 5 | QUERY_EDITOR_DEFAULT_TEXT = "SELECT name FROM sqlite_master WHERE type='table'" 6 | QUERY_CONTROL_CONNECT_BUTTON_TEXT = "Connect" 7 | QUERY_CONTROL_EXECUTE_BUTTON_TEXT = "Execute" 8 | QUERY_CONTROL_FETCH_BUTTON_TEXT = "Fetch" 9 | QUERY_CONTROL_COMMIT_BUTTON_TEXT = "Commit" 10 | QUERY_CONTROL_ROLLBACK_BUTTON_TEXT = "Rollback" 11 | QUERY_RESULTS_DATA_TAB_TEXT = "Data" 12 | QUERY_RESULTS_EVENTS_TAB_TEXT = "Events" 13 | -------------------------------------------------------------------------------- /pyqt_sql_demo/connection/exceptions.py: -------------------------------------------------------------------------------- 1 | class Error(Exception): 2 | """Base class for exceptions in this module.""" 3 | 4 | def __init__(self, message): 5 | self.message = message 6 | 7 | 8 | class FileError(Error): 9 | """Base class for exceptions related to files.""" 10 | 11 | def __init__(self, filename, message): 12 | self.filename = filename 13 | super().__init__(message) 14 | 15 | 16 | class FileNotFoundError(FileError): 17 | def __init__(self, filename): 18 | super().__init__( 19 | filename, "File {} is not found or is unaccessible".format(filename) 20 | ) 21 | -------------------------------------------------------------------------------- /pyqt_sql_demo/connection/error_handler.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | import logging 3 | 4 | 5 | class ErrorHandler(object): 6 | def __enter__(self): 7 | return self 8 | 9 | def __exit__(self, exc_type, exc_value, tb): 10 | if exc_value: 11 | # If any exception happened at all 12 | self.handle(exc_type, exc_value, tb) 13 | return True 14 | 15 | def handle(self, exc_type, exc_value, tb): 16 | if hasattr(exc_value, "message"): 17 | logging.error(f"{exc_type} {exc_value.message}") 18 | traceback.print_tb(tb) 19 | else: 20 | print(exc_type, exc_value) 21 | logging.error(f"{exc_type} {exc_value}") 22 | traceback.print_tb(tb) 23 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "pyqt-sql-demo" 3 | version = "1.0.1" 4 | description = "PyQT SQL executor demo example using DB API (no QtSQL)" 5 | authors = ["vduseev "] 6 | license = "MIT" 7 | readme = "README.md" 8 | homepage = "https://github.com/vduseev/pyqt-sql-demo" 9 | repository = "https://github.com/vduseev/pyqt-sql-demo" 10 | documentation = "https://github.com/vduseev/pyqt-sql-demo" 11 | keywords = ["pyqt", "sql", "sqlite", "qtableview"] 12 | 13 | [tool.poetry.scripts] 14 | pyqt-sql-demo = "pyqt_sql_demo.app:launch" 15 | 16 | [tool.poetry.dependencies] 17 | python = "^3.8" 18 | pyqt5 = "^5.15.0" 19 | pygments = "^2.6.1" 20 | 21 | [tool.poetry.dev-dependencies] 22 | black = "^20.8b1" 23 | ipython = "^7.18.1" 24 | flake8 = "^3.8.3" 25 | 26 | [build-system] 27 | requires = ["poetry>=0.12"] 28 | build-backend = "poetry.masonry.api" 29 | -------------------------------------------------------------------------------- /pyqt_sql_demo/syntax_highlighter/sql.py: -------------------------------------------------------------------------------- 1 | from pygments import highlight as _highlight 2 | from pygments.lexers import SqlLexer 3 | from pygments.formatters import HtmlFormatter 4 | 5 | 6 | def style(): 7 | style = HtmlFormatter().get_style_defs() 8 | return style 9 | 10 | 11 | def highlight(text): 12 | # Generated HTML contains unnecessary newline at the end 13 | # before closing tag. 14 | # We need to remove that newline because it's screwing up 15 | # QTextEdit formatting and is being displayed 16 | # as a non-editable whitespace. 17 | highlighted_text = _highlight(text, SqlLexer(), HtmlFormatter()).strip() 18 | 19 | # Split generated HTML by last newline in it 20 | # argument 1 indicates that we only want to split the string 21 | # by one specified delimiter from the right. 22 | parts = highlighted_text.rsplit("\n", 1) 23 | 24 | # Glue back 2 split parts to get the HTML without last 25 | # unnecessary newline 26 | highlighted_text_no_last_newline = "".join(parts) 27 | return highlighted_text_no_last_newline 28 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: build 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up Python 3.8 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: 3.8 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install poetry 27 | poetry install 28 | - name: Lint with flake8 29 | run: | 30 | # stop the build if there are Python syntax errors or undefined names 31 | poetry run flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 32 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 33 | poetry run flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 34 | -------------------------------------------------------------------------------- /pyqt_sql_demo/widgets/main.py: -------------------------------------------------------------------------------- 1 | from PyQt5.Qt import QMainWindow, QTabWidget, QAction 2 | 3 | from pyqt_sql_demo.widgets.connection import ConnectionWidget 4 | 5 | 6 | class MainWindow(QMainWindow): 7 | def __init__(self): 8 | super().__init__() 9 | self.setMinimumWidth(640) 10 | self.setMinimumHeight(480) 11 | 12 | # Set up QTabWidget as a central widget 13 | self.tab_widget = QTabWidget(self) 14 | self.tab_widget.setTabsClosable(True) 15 | self.tab_widget.tabCloseRequested.connect(self.on_tab_close_clicked) 16 | self.setCentralWidget(self.tab_widget) 17 | 18 | # Create "Connection" menu 19 | menu_bar = self.menuBar() 20 | connection_menu = menu_bar.addMenu("Connection") 21 | 22 | # Add "Create" connection button 23 | create_connection_action = QAction("Create", self) 24 | create_connection_action.triggered.connect(self.add_new_tab) 25 | connection_menu.addAction(create_connection_action) 26 | 27 | # Add "Close" connection button 28 | close_connection_action = QAction("Close", self) 29 | close_connection_action.triggered.connect(self.close_current_tab) 30 | connection_menu.addAction(close_connection_action) 31 | 32 | # self.tool_bar = self.addToolBar('test bar') 33 | # self.connect_action = self.tool_bar.addAction('connect') 34 | 35 | self.add_new_tab() 36 | 37 | def add_new_tab(self): 38 | connection_widget = ConnectionWidget(self.tab_widget) 39 | connection_widget.title_changed.connect(self.on_tab_name_changed) 40 | self.tab_widget.addTab(connection_widget, "Untitled") 41 | 42 | def close_current_tab(self): 43 | idx = self.tab_widget.currentIndex() 44 | self.tab_widget.removeTab(idx) 45 | 46 | def on_tab_close_clicked(self, idx): 47 | self.tab_widget.removeTab(idx) 48 | 49 | def on_tab_name_changed(self, widget, name): 50 | idx = self.tab_widget.indexOf(widget) 51 | if idx != -1: 52 | self.tab_widget.setTabText(idx, name) 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyQT based SQL query executor demo example 2 | 3 | [![Build Status](https://travis-ci.org/vduseev/pyqt-sql-demo.svg?style=plasticbranch=master&style=plastic)](https://travis-ci.org/vduseev/pyqt-sql-demo) 4 | ![PyPI](https://img.shields.io/pypi/v/pyqt-sql-demo?style=flat) 5 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pyqt-sql-demo?style=flat) 6 | ![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg?style=flat) 7 | ![PyPI - License](https://img.shields.io/pypi/l/pyqt-sql-demo?style=flat) 8 | 9 | 10 | This is a demo example of using `PyQT` and `SQLite` via standard Python `DB-API` interface. 11 | It's created to be used as a tutorial or reference. 12 | It uses `QtWidgets` for interface, `QTableView` for displaying SQL query results, `pygments` for syntax highlighting, and `sqlite3` for db connection. 13 | 14 | ![PyQt SQL Demo](docs/pyqt-sql-demo-rec.gif) 15 | 16 | ## Features 17 | 18 | * Executes user's DDL/DML SQL queries 19 | * Python3, PyQt5, DB-API 2.0 (does not use QtSql!) 20 | * Connection string field available to user (default: local `demo.db` file) 21 | * Query field available to user 22 | * SQL syntax highlighting using `pygments` library 23 | * Buttons to execute or fetch query and to commit or rollback the results 24 | * QTableView is used to display results 25 | 26 | ## Installation & Usage 27 | 28 | ### Using pip (end user installation) 29 | 30 | Install it from PyPI into your Python. This is a no-development way to install the package. An executable `pyqtsqldemo` will become available to you in your `PATH` environmental variable. 31 | 32 | ```shell 33 | $ pip3 install pyqt-sql-demo 34 | ``` 35 | 36 | Launch via terminal. 37 | 38 | ```shell 39 | $ pyqt-sql-demo 40 | ``` 41 | 42 | ### Using poetry (development) 43 | 44 | Install the package into a virtual environment using Poetry. 45 | 46 | ```shell 47 | $ poetry install 48 | ``` 49 | 50 | Run using poetry. 51 | 52 | ```shell 53 | $ poetry run pyqt-sql-demo 54 | ``` 55 | 56 | ## Development 57 | 58 | ### Use pyenv 59 | 60 | I recommend installing and using `pyenv` to manage isolated Python versions on your machine that do not interfere with your system wide Python installation and with other Python versions. 61 | For example, you might have Python 2.7.15 as your system default interpreter, but at the same time you need both Python 3.7.2 for some other projects and Python 3.8.5 for this project installed on the same machine. Best way to achieve this is to use `pyenv`. 62 | 63 | After installing `pyenv` install suitable Python version. This project was developed using Python 3.6 but was later upgraded to support latest 3.8.5. 64 | 65 | ```shell 66 | $ pyenv install 3.8.5 67 | ``` 68 | 69 | You'll notice `.pyenv-version` file in the root directory of the project, so whenever you `cd` into project directory `pyenv` will automatically start using Python version specified in that file in this directory. 70 | 71 | ### Use poetry 72 | 73 | Poetry is the latest and most admirable python package manager in existence (as of 2020). This project is packed and distributed using Poetry. 74 | 75 | The command below executed in the project's root directory will set up a virtual environment using current python version (system wide or the one specified using `.pyenv-version` file) and install all required dependencies into that virtual environment. 76 | 77 | ```shell 78 | $ poetry install 79 | 80 | Installing dependencies from lock file 81 | ``` 82 | 83 | Make modifications and run project: 84 | 85 | ```shell 86 | $ poetry run pyqt-sql-demo 87 | ``` 88 | 89 | Make sure you run [Black](https://github.com/psf/black) to format everything properly: 90 | 91 | ```shell 92 | $ poetry run black . 93 | 94 | All done! ✨ 🍰 ✨ 95 | 11 files left unchanged 96 | ``` 97 | 98 | Build project using poetry 99 | 100 | ```shell 101 | $ poetry build 102 | 103 | Building pyqt-sql-demo (1.0.0) 104 | - Building sdist 105 | - Built pyqt-sql-demo-1.0.0.tar.gz 106 | 107 | - Building wheel 108 | - Built pyqt_sql_demo-1.0.0-py3-none-any.whl 109 | ``` 110 | 111 | ## Bug reporting 112 | 113 | Please create an issue in GitHub Issues for this project if you have a question or would like to report a bug. When reporting an issue be sure to provide as many details as possible and use proper formatting. 114 | 115 | Nice details to provide when opening an issue: 116 | 117 | * Reliable way to reproduce a bug/issue 118 | * What was expected? 119 | * What you got instead? 120 | * What is your suggestion or clarification question? 121 | * Screenshots 122 | * Logs 123 | 124 | ## License 125 | 126 | Copyright © 2020 Vagiz Duseev 127 | 128 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 129 | 130 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 131 | 132 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 133 | -------------------------------------------------------------------------------- /pyqt_sql_demo/connection/model.py: -------------------------------------------------------------------------------- 1 | from PyQt5.Qt import QAbstractTableModel, Qt 2 | from PyQt5.QtCore import pyqtSignal 3 | 4 | import pyqt_sql_demo.connection.exceptions as exceptions 5 | 6 | import os.path 7 | import sqlite3 8 | import logging 9 | 10 | 11 | class ConnectionModel(QAbstractTableModel): 12 | executed = pyqtSignal(str, name="executed") 13 | connected = pyqtSignal(str, name="connected") 14 | disconnected = pyqtSignal() 15 | fetch_changed = pyqtSignal(bool, name="fetch_changed") 16 | 17 | def __init__(self, parent): 18 | super().__init__(parent) 19 | 20 | # Stores last successful connection url 21 | self.url = None 22 | # Stores last attempted connection url 23 | self.attempted_url = None 24 | # Stores connection object 25 | self.con = None 26 | self.cur = None 27 | 28 | self._headers = [] 29 | self._data = [] 30 | self._row_count = 0 31 | self._column_count = 0 32 | 33 | def connect(self, connection_string): 34 | # Disconnect from old connection, if any 35 | self.disconnect() 36 | # Strip connection string of missed whitespaces 37 | self.attempted_url = connection_string.strip() 38 | # Throw error if url is invalid 39 | self.verify_attempted_url() 40 | # Attempt connection 41 | self.con = sqlite3.connect(self.attempted_url) 42 | # Use highly-optimized RowFactory to enable 43 | # name based access to columns in rows 44 | self.con.row_factory = sqlite3.Row 45 | # Remember current connected URL 46 | self.url = self.attempted_url 47 | # Let the listeners know that connection is established 48 | self.connected.emit(self.url) 49 | # Log the success message 50 | self.executed.emit("Connected: " + connection_string) 51 | 52 | def disconnect(self): 53 | # Attempt to disconnect 54 | if self.cur: 55 | self.cur.close() 56 | self.cur = None 57 | if self.con: 58 | self.con.close() 59 | self.con = None 60 | # Notify listeners that connection is closed 61 | self.disconnected.emit() 62 | 63 | def verify_attempted_url(self): 64 | url = self.attempted_url 65 | # Two types of SQLite connection URLs are allowed: 66 | # - :memory: 67 | # - path to existing file 68 | if url == ":memory:": 69 | return 70 | if os.path.isfile(url): 71 | return 72 | # Raise an exception with predefined message 73 | raise exceptions.FileNotFoundError(url) 74 | 75 | def execute(self, query): 76 | self.cur = self.con.cursor() 77 | self.cur.execute(query) 78 | logging.debug(f"row count: {self.cur.rowcount}") 79 | 80 | # Fetch first row 81 | first_row = self.cur.fetchone() 82 | if first_row: 83 | # Fetch first row 84 | self.beginResetModel() 85 | logging.debug("fetcher first row") 86 | self._column_count = len(first_row) 87 | logging.debug(f"column count: {self._column_count}") 88 | self._headers = first_row.keys() 89 | logging.debug(f"headers: {self._headers}") 90 | self._data = [first_row] 91 | self._row_count = 1 92 | self.endResetModel() 93 | # Fetch additional rows 94 | self.fetch_more() 95 | else: 96 | # Try to read from Cursor.description if zero rows 97 | # returned 98 | self.beginResetModel() 99 | if self.cur.description: 100 | self._headers = [h[0] for h in self.cur.description] 101 | logging.debug(f"headers: {self._headers}") 102 | self._column_count = len(self._headers) 103 | else: 104 | self._headers = [] 105 | self._column_count = 0 106 | self._row_count = 0 107 | self._data = [] 108 | self.endResetModel() 109 | # Disable further fetching 110 | self.fetch_changed.emit(False) 111 | 112 | # print('data:', [tuple(r) for r in self._data]) 113 | self.executed.emit("Executed: " + query) 114 | 115 | def fetch_more(self): 116 | limit = 500 117 | # Try to fetch more 118 | more = self.cur.fetchmany(limit) 119 | logging.debug(f"fetched {len(more)} rows in fetch_more call") 120 | if len(more) > 0: 121 | self.beginResetModel() 122 | count = self._row_count + len(more) 123 | logging.debug(f"fetched {count} rows in total") 124 | self._data.extend(more) 125 | self._row_count = count 126 | self.endResetModel() 127 | # Disable further fetching if less rows than 128 | # that fetching window is returned 129 | # And enable otherwise 130 | self.fetch_changed.emit(len(more) >= limit) 131 | 132 | def commit(self): 133 | self.con.commit() 134 | self.executed.emit("Committed") 135 | logging.debug("Commit") 136 | 137 | def rollback(self): 138 | self.con.rollback() 139 | self.executed.emit("Rollback") 140 | logging.debug("Rollback") 141 | 142 | def rowCount(self, parent): 143 | return self._row_count 144 | 145 | def columnCount(self, parent): 146 | return self._column_count 147 | 148 | def headerData(self, section, orientation, role): 149 | if orientation == Qt.Horizontal and role == Qt.DisplayRole: 150 | if len(self._headers) > 0: 151 | return self._headers[section] 152 | 153 | return None 154 | 155 | def data(self, index, role): 156 | if index.isValid() and role == Qt.DisplayRole: 157 | return self._data[index.row()][index.column()] 158 | 159 | return None 160 | -------------------------------------------------------------------------------- /pyqt_sql_demo/widgets/connection.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from PyQt5.Qt import ( 4 | QVBoxLayout, 5 | QHBoxLayout, 6 | QComboBox, 7 | QLineEdit, 8 | QPushButton, 9 | QWidget, 10 | QTabWidget, 11 | QSplitter, 12 | QTextEdit, 13 | QTextDocument, 14 | QTableView, 15 | QSizePolicy, 16 | Qt, 17 | ) 18 | from PyQt5.QtCore import pyqtSignal 19 | 20 | from pyqt_sql_demo.connection.model import ConnectionModel 21 | from pyqt_sql_demo.connection.error_handler import ErrorHandler 22 | from pyqt_sql_demo.syntax_highlighter import sql as SQLHighlighter 23 | 24 | import pyqt_sql_demo.widgets.text as UI 25 | 26 | 27 | class ConnectionWidget(QWidget): 28 | title_changed = pyqtSignal(QWidget, str, name="title_changed") 29 | 30 | def __init__(self, parent): 31 | super().__init__(parent) 32 | 33 | # Initialize anti-recursion flag during highlighting 34 | self.is_processing_highlighting = False 35 | 36 | # Initial widget title 37 | self.title = UI.CONNECTION_TAB_DEFAULT_TITLE 38 | 39 | # Initialize data model 40 | self.model = ConnectionModel(self) 41 | self.model.connected.connect(self.on_connection_changed) 42 | 43 | # Initialize UI 44 | self.init_ui() 45 | 46 | def init_ui(self): 47 | # Declare main vertical layout 48 | layout = QVBoxLayout(self) 49 | layout.setContentsMargins(0, 0, 0, 0) 50 | 51 | # Initialize control toolbar 52 | control_bar = self.build_control_bar() 53 | layout.addWidget(control_bar) 54 | 55 | # Initialize workspace 56 | workspace = self.build_workspace() 57 | layout.addWidget(workspace) 58 | 59 | # Apply configured UI layout to the widget 60 | self.setLayout(layout) 61 | 62 | def build_control_bar(self): 63 | # Add control bar 64 | control_row_layout = QHBoxLayout(self) 65 | control_row_layout.setContentsMargins(0, 0, 0, 0) 66 | 67 | # DB type combo box 68 | db_combo_box = QComboBox(self) 69 | for dbname in UI.CONNECTION_STRING_SUPPORTED_DB_NAMES: 70 | db_combo_box.addItem(dbname) 71 | control_row_layout.addWidget(db_combo_box) 72 | 73 | # Connection string 74 | self.connection_line = QLineEdit(self) 75 | self.connection_line.setPlaceholderText(UI.CONNECTION_STRING_PLACEHOLDER) 76 | self.connection_line.setText(UI.CONNECTION_STRING_DEFAULT) 77 | control_row_layout.addWidget(self.connection_line) 78 | 79 | # Connection button 80 | connection_button = QPushButton(self) 81 | connection_button.setText(UI.QUERY_CONTROL_CONNECT_BUTTON_TEXT) 82 | connection_button.clicked.connect(self.on_connect_click) 83 | control_row_layout.addWidget(connection_button) 84 | 85 | # Add contol row as a first widget in a column 86 | control_row = QWidget(self) 87 | control_row.setLayout(control_row_layout) 88 | return control_row 89 | 90 | def build_workspace(self): 91 | # Create a splitter consisting of query edit and table view 92 | splitter = QSplitter(self) 93 | splitter.setOrientation(Qt.Vertical) 94 | splitter.sizePolicy().setVerticalPolicy(QSizePolicy.Maximum) 95 | 96 | # Initialize query edit 97 | query_edit = self.build_query_text_edit() 98 | 99 | # Disable query control buttons by default 100 | self.on_disconnected() 101 | splitter.addWidget(query_edit) 102 | 103 | # Initialize result desiplaying widgets 104 | results_widget = self.build_results_widget() 105 | splitter.addWidget(results_widget) 106 | splitter.setSizes([100, 900]) 107 | return splitter 108 | 109 | def build_query_text_edit(self): 110 | # Add layouts 111 | query_edit_layout = QVBoxLayout(self) 112 | query_edit_layout.setContentsMargins(0, 0, 0, 0) 113 | query_control_layout = QHBoxLayout(self) 114 | query_control_layout.setContentsMargins(0, 0, 0, 0) 115 | 116 | # Execute query button 117 | self.query_execute_button = QPushButton(UI.QUERY_CONTROL_EXECUTE_BUTTON_TEXT, self) 118 | self.query_execute_button.clicked.connect(self.on_execute_click) 119 | query_control_layout.addWidget(self.query_execute_button) 120 | 121 | # Fetch data button 122 | self.query_fetch_button = QPushButton(UI.QUERY_CONTROL_FETCH_BUTTON_TEXT, self) 123 | self.query_fetch_button.clicked.connect(self.on_fetch_click) 124 | self.model.fetch_changed.connect(self.on_fetch_changed) 125 | query_control_layout.addWidget(self.query_fetch_button) 126 | 127 | # Commit button 128 | self.query_commit_button = QPushButton(UI.QUERY_CONTROL_COMMIT_BUTTON_TEXT, self) 129 | self.query_commit_button.clicked.connect(self.on_connect_click) 130 | query_control_layout.addWidget(self.query_commit_button) 131 | 132 | # Rollback button 133 | self.query_rollback_button = QPushButton( 134 | UI.QUERY_CONTROL_ROLLBACK_BUTTON_TEXT, self 135 | ) 136 | self.query_rollback_button.clicked.connect(self.on_rollback_click) 137 | query_control_layout.addWidget(self.query_rollback_button) 138 | 139 | # Build control strip widget 140 | query_control = QWidget(self) 141 | query_control.setLayout(query_control_layout) 142 | query_edit_layout.addWidget(query_control) 143 | 144 | # Initialize query edit document for text editor 145 | # and use SQL Highlighter CSS styles for it. 146 | self.query_text_edit_document = QTextDocument(self) 147 | self.query_text_edit_document.setDefaultStyleSheet(SQLHighlighter.style()) 148 | 149 | # Initialize query text editor using previously built 150 | # text edutir document. 151 | self.query_text_edit = QTextEdit(self) 152 | self.query_text_edit.setDocument(self.query_text_edit_document) 153 | self.query_text_edit.textChanged.connect(self.on_query_changed) 154 | self.query_text_edit.setText(UI.QUERY_EDITOR_DEFAULT_TEXT) 155 | query_edit_layout.addWidget(self.query_text_edit) 156 | 157 | # Connect model's connected/disconnected signals 158 | self.model.connected.connect(self.on_connected) 159 | self.model.disconnected.connect(self.on_disconnected) 160 | 161 | query_edit = QWidget(self) 162 | query_edit.setLayout(query_edit_layout) 163 | query_edit.sizePolicy().setVerticalPolicy(QSizePolicy.Minimum) 164 | return query_edit 165 | 166 | def build_results_widget(self): 167 | # Initialize QTabWidget to display table view and log 168 | # in differnt unclosable tabs 169 | results_widget = QTabWidget(self) 170 | results_widget.setTabsClosable(False) 171 | 172 | # Add table view 173 | table_view = QTableView(self) 174 | table_view.setModel(self.model) 175 | table_view.sizePolicy().setVerticalPolicy(QSizePolicy.MinimumExpanding) 176 | results_widget.addTab(table_view, UI.QUERY_RESULTS_DATA_TAB_TEXT) 177 | 178 | # Att log view 179 | log = QTextEdit(self) 180 | log.setReadOnly(True) 181 | self.model.executed.connect(log.append) 182 | results_widget.addTab(log, UI.QUERY_RESULTS_EVENTS_TAB_TEXT) 183 | return results_widget 184 | 185 | def on_query_changed(self): 186 | """Process query edits by user""" 187 | if self.is_processing_highlighting: 188 | # If we caused the invokation of this slot by set highlighted 189 | # HTML text into query editor, then ignore this call and 190 | # mark highlighting processing as finished. 191 | self.is_processing_highlighting = False 192 | else: 193 | # If changes to text were made by user, mark beginning of 194 | # highlighting process 195 | self.is_processing_highlighting = True 196 | # Get plain text query and highlight it 197 | query_text = self.query_text_edit.toPlainText() 198 | highlighted_query_text = SQLHighlighter.highlight(query_text) 199 | 200 | # After we set highlighted HTML back to QTextEdit form 201 | # the cursor will jump to the end of the text. 202 | # To avoid that we remember the current position of the cursor. 203 | current_cursor = self.query_text_edit.textCursor() 204 | current_cursor_position = current_cursor.position() 205 | # Set highlighted text back to editor which will cause the 206 | # cursor to jump to the end of the text. 207 | self.query_text_edit_document.setHtml(highlighted_query_text) 208 | # Return cursor back to the old position 209 | current_cursor.setPosition(current_cursor_position) 210 | self.query_text_edit.setTextCursor(current_cursor) 211 | 212 | def on_connect_click(self): 213 | with ErrorHandler(): 214 | connection_string = self.connection_line.text() 215 | self.model.connect(connection_string) 216 | logging.info(f"Connected: {connection_string}") 217 | 218 | def on_execute_click(self): 219 | with ErrorHandler(): 220 | query = self.query_text_edit.toPlainText() 221 | self.model.execute(query) 222 | logging.info(f"Executed: {query}") 223 | 224 | def on_fetch_click(self): 225 | with ErrorHandler(): 226 | self.model.fetch_more() 227 | logging.info("Fetch more") 228 | 229 | def on_rollback_click(self): 230 | with ErrorHandler(): 231 | self.model.rollback() 232 | logging.info("Rollback") 233 | 234 | def on_connected(self): 235 | self.query_commit_button.setEnabled(True) 236 | self.query_execute_button.setEnabled(True) 237 | self.query_fetch_button.setEnabled(False) 238 | self.query_rollback_button.setEnabled(True) 239 | self.query_text_edit.setEnabled(True) 240 | 241 | def on_disconnected(self): 242 | self.query_commit_button.setEnabled(False) 243 | self.query_execute_button.setEnabled(False) 244 | self.query_fetch_button.setEnabled(False) 245 | self.query_rollback_button.setEnabled(False) 246 | self.query_text_edit.setEnabled(False) 247 | 248 | def on_fetch_changed(self, state): 249 | self.query_fetch_button.setEnabled(state) 250 | 251 | def on_connection_changed(self, name): 252 | self.title_changed.emit(self, name) 253 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | category = "dev" 3 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 4 | name = "appdirs" 5 | optional = false 6 | python-versions = "*" 7 | version = "1.4.4" 8 | 9 | [[package]] 10 | category = "dev" 11 | description = "Disable App Nap on OS X 10.9" 12 | marker = "sys_platform == \"darwin\"" 13 | name = "appnope" 14 | optional = false 15 | python-versions = "*" 16 | version = "0.1.0" 17 | 18 | [[package]] 19 | category = "dev" 20 | description = "Specifications for callback functions passed in to an API" 21 | name = "backcall" 22 | optional = false 23 | python-versions = "*" 24 | version = "0.2.0" 25 | 26 | [[package]] 27 | category = "dev" 28 | description = "The uncompromising code formatter." 29 | name = "black" 30 | optional = false 31 | python-versions = ">=3.6" 32 | version = "20.8b1" 33 | 34 | [package.dependencies] 35 | appdirs = "*" 36 | click = ">=7.1.2" 37 | mypy-extensions = ">=0.4.3" 38 | pathspec = ">=0.6,<1" 39 | regex = ">=2020.1.8" 40 | toml = ">=0.10.1" 41 | typed-ast = ">=1.4.0" 42 | typing-extensions = ">=3.7.4" 43 | 44 | [package.extras] 45 | colorama = ["colorama (>=0.4.3)"] 46 | d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] 47 | 48 | [[package]] 49 | category = "dev" 50 | description = "Composable command line interface toolkit" 51 | name = "click" 52 | optional = false 53 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 54 | version = "7.1.2" 55 | 56 | [[package]] 57 | category = "dev" 58 | description = "Cross-platform colored terminal text." 59 | marker = "sys_platform == \"win32\"" 60 | name = "colorama" 61 | optional = false 62 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 63 | version = "0.4.3" 64 | 65 | [[package]] 66 | category = "dev" 67 | description = "Decorators for Humans" 68 | name = "decorator" 69 | optional = false 70 | python-versions = ">=2.6, !=3.0.*, !=3.1.*" 71 | version = "4.4.2" 72 | 73 | [[package]] 74 | category = "dev" 75 | description = "the modular source code checker: pep8 pyflakes and co" 76 | name = "flake8" 77 | optional = false 78 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" 79 | version = "3.8.3" 80 | 81 | [package.dependencies] 82 | mccabe = ">=0.6.0,<0.7.0" 83 | pycodestyle = ">=2.6.0a1,<2.7.0" 84 | pyflakes = ">=2.2.0,<2.3.0" 85 | 86 | [[package]] 87 | category = "dev" 88 | description = "IPython: Productive Interactive Computing" 89 | name = "ipython" 90 | optional = false 91 | python-versions = ">=3.7" 92 | version = "7.18.1" 93 | 94 | [package.dependencies] 95 | appnope = "*" 96 | backcall = "*" 97 | colorama = "*" 98 | decorator = "*" 99 | jedi = ">=0.10" 100 | pexpect = ">4.3" 101 | pickleshare = "*" 102 | prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0" 103 | pygments = "*" 104 | setuptools = ">=18.5" 105 | traitlets = ">=4.2" 106 | 107 | [package.extras] 108 | all = ["Sphinx (>=1.3)", "ipykernel", "ipyparallel", "ipywidgets", "nbconvert", "nbformat", "nose (>=0.10.1)", "notebook", "numpy (>=1.14)", "pygments", "qtconsole", "requests", "testpath"] 109 | doc = ["Sphinx (>=1.3)"] 110 | kernel = ["ipykernel"] 111 | nbconvert = ["nbconvert"] 112 | nbformat = ["nbformat"] 113 | notebook = ["notebook", "ipywidgets"] 114 | parallel = ["ipyparallel"] 115 | qtconsole = ["qtconsole"] 116 | test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipykernel", "numpy (>=1.14)"] 117 | 118 | [[package]] 119 | category = "dev" 120 | description = "Vestigial utilities from IPython" 121 | name = "ipython-genutils" 122 | optional = false 123 | python-versions = "*" 124 | version = "0.2.0" 125 | 126 | [[package]] 127 | category = "dev" 128 | description = "An autocompletion tool for Python that can be used for text editors." 129 | name = "jedi" 130 | optional = false 131 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 132 | version = "0.17.2" 133 | 134 | [package.dependencies] 135 | parso = ">=0.7.0,<0.8.0" 136 | 137 | [package.extras] 138 | qa = ["flake8 (3.7.9)"] 139 | testing = ["Django (<3.1)", "colorama", "docopt", "pytest (>=3.9.0,<5.0.0)"] 140 | 141 | [[package]] 142 | category = "dev" 143 | description = "McCabe checker, plugin for flake8" 144 | name = "mccabe" 145 | optional = false 146 | python-versions = "*" 147 | version = "0.6.1" 148 | 149 | [[package]] 150 | category = "dev" 151 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 152 | name = "mypy-extensions" 153 | optional = false 154 | python-versions = "*" 155 | version = "0.4.3" 156 | 157 | [[package]] 158 | category = "dev" 159 | description = "A Python Parser" 160 | name = "parso" 161 | optional = false 162 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 163 | version = "0.7.1" 164 | 165 | [package.extras] 166 | testing = ["docopt", "pytest (>=3.0.7)"] 167 | 168 | [[package]] 169 | category = "dev" 170 | description = "Utility library for gitignore style pattern matching of file paths." 171 | name = "pathspec" 172 | optional = false 173 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 174 | version = "0.8.0" 175 | 176 | [[package]] 177 | category = "dev" 178 | description = "Pexpect allows easy control of interactive console applications." 179 | marker = "sys_platform != \"win32\"" 180 | name = "pexpect" 181 | optional = false 182 | python-versions = "*" 183 | version = "4.8.0" 184 | 185 | [package.dependencies] 186 | ptyprocess = ">=0.5" 187 | 188 | [[package]] 189 | category = "dev" 190 | description = "Tiny 'shelve'-like database with concurrency support" 191 | name = "pickleshare" 192 | optional = false 193 | python-versions = "*" 194 | version = "0.7.5" 195 | 196 | [[package]] 197 | category = "dev" 198 | description = "Library for building powerful interactive command lines in Python" 199 | name = "prompt-toolkit" 200 | optional = false 201 | python-versions = ">=3.6.1" 202 | version = "3.0.7" 203 | 204 | [package.dependencies] 205 | wcwidth = "*" 206 | 207 | [[package]] 208 | category = "dev" 209 | description = "Run a subprocess in a pseudo terminal" 210 | marker = "sys_platform != \"win32\"" 211 | name = "ptyprocess" 212 | optional = false 213 | python-versions = "*" 214 | version = "0.6.0" 215 | 216 | [[package]] 217 | category = "dev" 218 | description = "Python style guide checker" 219 | name = "pycodestyle" 220 | optional = false 221 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 222 | version = "2.6.0" 223 | 224 | [[package]] 225 | category = "dev" 226 | description = "passive checker of Python programs" 227 | name = "pyflakes" 228 | optional = false 229 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 230 | version = "2.2.0" 231 | 232 | [[package]] 233 | category = "main" 234 | description = "Pygments is a syntax highlighting package written in Python." 235 | name = "pygments" 236 | optional = false 237 | python-versions = ">=3.5" 238 | version = "2.6.1" 239 | 240 | [[package]] 241 | category = "main" 242 | description = "Python bindings for the Qt cross platform application toolkit" 243 | name = "pyqt5" 244 | optional = false 245 | python-versions = ">=3.5" 246 | version = "5.15.0" 247 | 248 | [package.dependencies] 249 | PyQt5-sip = ">=12.8,<13" 250 | 251 | [[package]] 252 | category = "main" 253 | description = "The sip module support for PyQt5" 254 | name = "pyqt5-sip" 255 | optional = false 256 | python-versions = ">=3.5" 257 | version = "12.8.1" 258 | 259 | [[package]] 260 | category = "dev" 261 | description = "Alternative regular expression module, to replace re." 262 | name = "regex" 263 | optional = false 264 | python-versions = "*" 265 | version = "2020.7.14" 266 | 267 | [[package]] 268 | category = "dev" 269 | description = "Python Library for Tom's Obvious, Minimal Language" 270 | name = "toml" 271 | optional = false 272 | python-versions = "*" 273 | version = "0.10.1" 274 | 275 | [[package]] 276 | category = "dev" 277 | description = "Traitlets Python configuration system" 278 | name = "traitlets" 279 | optional = false 280 | python-versions = ">=3.7" 281 | version = "5.0.4" 282 | 283 | [package.dependencies] 284 | ipython-genutils = "*" 285 | 286 | [package.extras] 287 | test = ["pytest"] 288 | 289 | [[package]] 290 | category = "dev" 291 | description = "a fork of Python 2 and 3 ast modules with type comment support" 292 | name = "typed-ast" 293 | optional = false 294 | python-versions = "*" 295 | version = "1.4.1" 296 | 297 | [[package]] 298 | category = "dev" 299 | description = "Backported and Experimental Type Hints for Python 3.5+" 300 | name = "typing-extensions" 301 | optional = false 302 | python-versions = "*" 303 | version = "3.7.4.3" 304 | 305 | [[package]] 306 | category = "dev" 307 | description = "Measures the displayed width of unicode strings in a terminal" 308 | name = "wcwidth" 309 | optional = false 310 | python-versions = "*" 311 | version = "0.2.5" 312 | 313 | [metadata] 314 | content-hash = "681ae04603c1ea00e6b3a4fbbfe916e8f35a479d4a68c3faab060ddf263152a7" 315 | lock-version = "1.0" 316 | python-versions = "^3.8" 317 | 318 | [metadata.files] 319 | appdirs = [ 320 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, 321 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, 322 | ] 323 | appnope = [ 324 | {file = "appnope-0.1.0-py2.py3-none-any.whl", hash = "sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0"}, 325 | {file = "appnope-0.1.0.tar.gz", hash = "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"}, 326 | ] 327 | backcall = [ 328 | {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, 329 | {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, 330 | ] 331 | black = [ 332 | {file = "black-20.8b1-py3-none-any.whl", hash = "sha256:70b62ef1527c950db59062cda342ea224d772abdf6adc58b86a45421bab20a6b"}, 333 | {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, 334 | ] 335 | click = [ 336 | {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, 337 | {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, 338 | ] 339 | colorama = [ 340 | {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, 341 | {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, 342 | ] 343 | decorator = [ 344 | {file = "decorator-4.4.2-py2.py3-none-any.whl", hash = "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760"}, 345 | {file = "decorator-4.4.2.tar.gz", hash = "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"}, 346 | ] 347 | flake8 = [ 348 | {file = "flake8-3.8.3-py2.py3-none-any.whl", hash = "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c"}, 349 | {file = "flake8-3.8.3.tar.gz", hash = "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208"}, 350 | ] 351 | ipython = [ 352 | {file = "ipython-7.18.1-py3-none-any.whl", hash = "sha256:2e22c1f74477b5106a6fb301c342ab8c64bb75d702e350f05a649e8cb40a0fb8"}, 353 | {file = "ipython-7.18.1.tar.gz", hash = "sha256:a331e78086001931de9424940699691ad49dfb457cea31f5471eae7b78222d5e"}, 354 | ] 355 | ipython-genutils = [ 356 | {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"}, 357 | {file = "ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"}, 358 | ] 359 | jedi = [ 360 | {file = "jedi-0.17.2-py2.py3-none-any.whl", hash = "sha256:98cc583fa0f2f8304968199b01b6b4b94f469a1f4a74c1560506ca2a211378b5"}, 361 | {file = "jedi-0.17.2.tar.gz", hash = "sha256:86ed7d9b750603e4ba582ea8edc678657fb4007894a12bcf6f4bb97892f31d20"}, 362 | ] 363 | mccabe = [ 364 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 365 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 366 | ] 367 | mypy-extensions = [ 368 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 369 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 370 | ] 371 | parso = [ 372 | {file = "parso-0.7.1-py2.py3-none-any.whl", hash = "sha256:97218d9159b2520ff45eb78028ba8b50d2bc61dcc062a9682666f2dc4bd331ea"}, 373 | {file = "parso-0.7.1.tar.gz", hash = "sha256:caba44724b994a8a5e086460bb212abc5a8bc46951bf4a9a1210745953622eb9"}, 374 | ] 375 | pathspec = [ 376 | {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"}, 377 | {file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"}, 378 | ] 379 | pexpect = [ 380 | {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, 381 | {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, 382 | ] 383 | pickleshare = [ 384 | {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, 385 | {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, 386 | ] 387 | prompt-toolkit = [ 388 | {file = "prompt_toolkit-3.0.7-py3-none-any.whl", hash = "sha256:83074ee28ad4ba6af190593d4d4c607ff525272a504eb159199b6dd9f950c950"}, 389 | {file = "prompt_toolkit-3.0.7.tar.gz", hash = "sha256:822f4605f28f7d2ba6b0b09a31e25e140871e96364d1d377667b547bb3bf4489"}, 390 | ] 391 | ptyprocess = [ 392 | {file = "ptyprocess-0.6.0-py2.py3-none-any.whl", hash = "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"}, 393 | {file = "ptyprocess-0.6.0.tar.gz", hash = "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0"}, 394 | ] 395 | pycodestyle = [ 396 | {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"}, 397 | {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"}, 398 | ] 399 | pyflakes = [ 400 | {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"}, 401 | {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, 402 | ] 403 | pygments = [ 404 | {file = "Pygments-2.6.1-py3-none-any.whl", hash = "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"}, 405 | {file = "Pygments-2.6.1.tar.gz", hash = "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44"}, 406 | ] 407 | pyqt5 = [ 408 | {file = "PyQt5-5.15.0-5.15.0-cp35.cp36.cp37.cp38-abi3-macosx_10_6_intel.whl", hash = "sha256:14be35c0c1bcc804791a096d2ef9950f12c6fd34dd11dbe61b8c769fefcdf98c"}, 409 | {file = "PyQt5-5.15.0-5.15.0-cp35.cp36.cp37.cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:3605d34ba6291b9194c46035e228d6d01f39d120cf5ecc70301c11e7900fed21"}, 410 | {file = "PyQt5-5.15.0-5.15.0-cp35.cp36.cp37.cp38-none-win32.whl", hash = "sha256:e05c86b8c4f02d62a5b355d426fd8d063781dd44c6a3f916640a5beb40efe60a"}, 411 | {file = "PyQt5-5.15.0-5.15.0-cp35.cp36.cp37.cp38-none-win_amd64.whl", hash = "sha256:5bac0fab1e9891d73400c2470a9cb810e6bdbc7027a84ae4d3ec83436f1109ec"}, 412 | {file = "PyQt5-5.15.0.tar.gz", hash = "sha256:c6f75488ffd5365a65893bc64ea82a6957db126fbfe33654bcd43ae1c30c52f9"}, 413 | ] 414 | pyqt5-sip = [ 415 | {file = "PyQt5_sip-12.8.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:bb5a87b66fc1445915104ee97f7a20a69decb42f52803e3b0795fa17ff88226c"}, 416 | {file = "PyQt5_sip-12.8.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:a29e2ac399429d3b7738f73e9081e50783e61ac5d29344e0802d0dcd6056c5a2"}, 417 | {file = "PyQt5_sip-12.8.1-cp35-cp35m-win32.whl", hash = "sha256:0304ca9114b9817a270f67f421355075b78ff9fc25ac58ffd72c2601109d2194"}, 418 | {file = "PyQt5_sip-12.8.1-cp35-cp35m-win_amd64.whl", hash = "sha256:84ba7746762bd223bed22428e8561aa267a229c28344c2d28c5d5d3f8970cffb"}, 419 | {file = "PyQt5_sip-12.8.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:7b81382ce188d63890a0e35abe0f9bb946cabc873a31873b73583b0fc84ac115"}, 420 | {file = "PyQt5_sip-12.8.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:b6d42250baec52a5f77de64e2951d001c5501c3a2df2179f625b241cbaec3369"}, 421 | {file = "PyQt5_sip-12.8.1-cp36-cp36m-win32.whl", hash = "sha256:6c1ebee60f1d2b3c70aff866b7933d8d8d7646011f7c32f9321ee88c290aa4f9"}, 422 | {file = "PyQt5_sip-12.8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:34dcd29be47553d5f016ff86e89e24cbc5eebae92eb2f96fb32d2d7ba028c43c"}, 423 | {file = "PyQt5_sip-12.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ed897c58acf4a3cdca61469daa31fe6e44c33c6c06a37c3f21fab31780b3b86a"}, 424 | {file = "PyQt5_sip-12.8.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a1b8ef013086e224b8e86c93f880f776d01b59195bdfa2a8e0b23f0480678fec"}, 425 | {file = "PyQt5_sip-12.8.1-cp37-cp37m-win32.whl", hash = "sha256:0cd969be528c27bbd4755bd323dff4a79a8fdda28215364e6ce3e069cb56c2a9"}, 426 | {file = "PyQt5_sip-12.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:c9800729badcb247765e4ffe2241549d02da1fa435b9db224845bc37c3e99cb0"}, 427 | {file = "PyQt5_sip-12.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9312ec47cac4e33c11503bc1cbeeb0bdae619620472f38e2078c5a51020a930f"}, 428 | {file = "PyQt5_sip-12.8.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2f35e82fd7ec1e1f6716e9154721c7594956a4f5bd4f826d8c6a6453833cc2f0"}, 429 | {file = "PyQt5_sip-12.8.1-cp38-cp38-win32.whl", hash = "sha256:da9c9f1e65b9d09e73bd75befc82961b6b61b5a3b9d0a7c832168e1415f163c6"}, 430 | {file = "PyQt5_sip-12.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:832fd60a264de4134c2824d393320838f3ab648180c9c357ec58a74524d24507"}, 431 | {file = "PyQt5_sip-12.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c317ab1263e6417c498b81f5c970a9b1af7acefab1f80b4cc0f2f8e661f29fc5"}, 432 | {file = "PyQt5_sip-12.8.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:c9d6d448c29dc6606bb7974696608f81f4316c8234f7c7216396ed110075e777"}, 433 | {file = "PyQt5_sip-12.8.1-cp39-cp39-win32.whl", hash = "sha256:5a011aeff89660622a6d5c3388d55a9d76932f3b82c95e82fc31abd8b1d2990d"}, 434 | {file = "PyQt5_sip-12.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:f168f0a7f32b81bfeffdf003c36f25d81c97dee5eb67072a5183e761fe250f13"}, 435 | {file = "PyQt5_sip-12.8.1.tar.gz", hash = "sha256:30e944db9abee9cc757aea16906d4198129558533eb7fadbe48c5da2bd18e0bd"}, 436 | ] 437 | regex = [ 438 | {file = "regex-2020.7.14-cp27-cp27m-win32.whl", hash = "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"}, 439 | {file = "regex-2020.7.14-cp27-cp27m-win_amd64.whl", hash = "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644"}, 440 | {file = "regex-2020.7.14-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc"}, 441 | {file = "regex-2020.7.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067"}, 442 | {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd"}, 443 | {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88"}, 444 | {file = "regex-2020.7.14-cp36-cp36m-win32.whl", hash = "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4"}, 445 | {file = "regex-2020.7.14-cp36-cp36m-win_amd64.whl", hash = "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f"}, 446 | {file = "regex-2020.7.14-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162"}, 447 | {file = "regex-2020.7.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf"}, 448 | {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7"}, 449 | {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89"}, 450 | {file = "regex-2020.7.14-cp37-cp37m-win32.whl", hash = "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6"}, 451 | {file = "regex-2020.7.14-cp37-cp37m-win_amd64.whl", hash = "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204"}, 452 | {file = "regex-2020.7.14-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99"}, 453 | {file = "regex-2020.7.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e"}, 454 | {file = "regex-2020.7.14-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e"}, 455 | {file = "regex-2020.7.14-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a"}, 456 | {file = "regex-2020.7.14-cp38-cp38-win32.whl", hash = "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341"}, 457 | {file = "regex-2020.7.14-cp38-cp38-win_amd64.whl", hash = "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840"}, 458 | {file = "regex-2020.7.14.tar.gz", hash = "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb"}, 459 | ] 460 | toml = [ 461 | {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, 462 | {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, 463 | ] 464 | traitlets = [ 465 | {file = "traitlets-5.0.4-py3-none-any.whl", hash = "sha256:9664ec0c526e48e7b47b7d14cd6b252efa03e0129011de0a9c1d70315d4309c3"}, 466 | {file = "traitlets-5.0.4.tar.gz", hash = "sha256:86c9351f94f95de9db8a04ad8e892da299a088a64fd283f9f6f18770ae5eae1b"}, 467 | ] 468 | typed-ast = [ 469 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, 470 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, 471 | {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, 472 | {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, 473 | {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, 474 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, 475 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, 476 | {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, 477 | {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, 478 | {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, 479 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, 480 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, 481 | {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, 482 | {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, 483 | {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, 484 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, 485 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, 486 | {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, 487 | {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, 488 | {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, 489 | {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, 490 | ] 491 | typing-extensions = [ 492 | {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, 493 | {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, 494 | {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, 495 | ] 496 | wcwidth = [ 497 | {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, 498 | {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, 499 | ] 500 | --------------------------------------------------------------------------------