├── .github └── workflows │ └── tests.yml ├── .gitignore ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── appveyor.yml ├── appveyor ├── install.ps1 └── run_with_compiler.cmd ├── docs ├── sqlite_bro.GIF └── sqlite_bro_command_line.GIF ├── pyproject.toml └── sqlite_bro ├── __init__.py ├── sqlite_bro.py └── tests ├── __init__.py ├── test_general.py └── test_general_no_gui.py /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python package 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 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: [3.8, 3.12, 'pypy-3.10'] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v2 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install flake8 pytest nose 31 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 32 | - name: Lint with flake8 33 | run: | 34 | # stop the build if there are Python syntax errors or undefined names 35 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 36 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 37 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistic 38 | 39 | # start virtual display driver 40 | # (from https://github.com/miketrethewey/tkinter-test/blob/master/.github/workflows/ci.yml) 41 | 42 | - name: Test with pytest 43 | run: | 44 | pytest -v 45 | 46 | - name: Start virtual display driver & Test (!windows) 47 | env: 48 | DISPLAY: :99 49 | run: | 50 | disp=:99 51 | screen=0 52 | geom=640x480x24 53 | exec Xvfb $disp -screen $screen $geom 2>/tmp/Xvfb.log & 54 | export DISPLAY=:99 55 | pytest -v 56 | if: contains(matrix.os-name, 'windows') != true 57 | - name: Start virtual display driver & Test (windows) 58 | uses: GabrielBB/xvfb-action@v1 59 | with: 60 | run: pytest -v 61 | if: contains(matrix.os-name, 'windows') 62 | 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | build 3 | docs/build 4 | dist 5 | MANIFEST 6 | .coverage 7 | .tox/ 8 | .hypothesis/ 9 | .cache/ 10 | *.egg-info/ 11 | *.tmp 12 | *.tmp' 13 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 2024-05-11b : v0.13.1 'Setup me down !' 5 | --------------------------------------- 6 | 7 | * see https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html for details 8 | 9 | 10 | 2024-05-11a : v0.13.0 'PEP 667 me up !' 11 | --------------------------------------- 12 | 13 | * starting Python-3.13, PEP 667 forces us to use a specific dictionnary pydef_locals instead of locals() 14 | 15 | 16 | 2022-02-04a : v0.12.2 'Filling the blanks !' 17 | -------------------------------------------- 18 | 19 | * empty columns names are always replaced per the default 'c_nnn' convention 20 | 21 | 22 | 2021-08-15a : v0.12.1 'Pop-up results to excel !' 23 | ------------------------------------------------- 24 | 25 | * 'backup' and 'restore' functions are accessible via menu (for python >=3.7) 26 | 27 | * running script and displaying output in temporary files is available via icons 28 | 29 | * supports running in an environment with no DISPLAY 30 | 31 | 32 | 2021-08-09b : v0.11.1 'Script me more!' 33 | --------------------------------------- 34 | 35 | * supports '.output' and '.print' functions 36 | 37 | * supports '.dump' and '.read' functions 38 | 39 | * supports '.open' function 40 | 41 | * supports '.backup' and '.restore' functions 42 | 43 | * switch to github actions and pytest 44 | 45 | 46 | 2021-08-01a : v0.10.0 'Hello, better .scripting World!' 47 | ------------------------------------------------------- 48 | 49 | * supports '.headers on|off' function 50 | 51 | * supports '.separator COL' function 52 | 53 | * supports '.cd DIRECTORY' function 54 | 55 | * supports command-line scripting (see sqlite_bro -h) 56 | 57 | 58 | 2021-04-25a : v0.9.3 'Hello, World!' 59 | ------------------------------------ 60 | 61 | * support functions with no parameters or parameters on several lines 62 | 63 | 2021-04-20a : v0.9.2 'Give PyPy a chance!' 64 | ------------------------------------------ 65 | 66 | * compatiblity with PyPy 67 | 68 | * handle '~' convention for Home directory 69 | 70 | * indicate Python Executable and Home directory in Information bubble 71 | 72 | 73 | 2019-06-16a : v0.9.1 'Support un-named Tabs!!' 74 | ---------------------------------------------- 75 | 76 | * previously, un-named Tabs couldn't be renamed nor moved. 77 | 78 | 2019-05-02a : v0.9.0 'De-duplicate column names!' 79 | ------------------------------------------------- 80 | 81 | * header columns in a .csv file are de-duplicated to avoid error: 'a', 'a', 'a_1' becomes 'a', 'a_2', 'a_1' 82 | 83 | 2016-03-06a : v0.8.11 'Combine Functions!' 84 | ------------------------------------------ 85 | 86 | * add a combining of functions example: 'py_func1(1*py_func2)', not 'py_func1(py_func2)' 87 | 88 | * fixed a nasty tokenizer issue 89 | 90 | 91 | 2015-08-12a : v0.8.10 'F9 runs !' 92 | --------------------------------- 93 | 94 | * clicking on 'F9' key will run current selected instructions (patch by Yuxiang Wang) 95 | 96 | 97 | 2015-05-24a : v0.8.9 'Yield !' 98 | ------------------------------ 99 | 100 | * re-structure sql splitting as a generator instead of a list 101 | 102 | * remove too long history from pypi front page 103 | 104 | 105 | 2015-04-16a : v0.8.8 'Continuous Integration !' 106 | ------------------------------------------------- 107 | 108 | * re-structure as a package for Appveyor Continuous Integration tests 109 | 110 | * include a global test 111 | 112 | 113 | 2014-09-10b : v0.8.7.4 '.Import this !' 114 | --------------------------------------- 115 | 116 | * compatibility fix for python 2.7 117 | 118 | 119 | 2014-09-10a : v0.8.7.3 '.Import this !' 120 | --------------------------------------- 121 | 122 | * wheel packaging format on pypi.org (no user code change) 123 | 124 | 125 | 2014-09-03c : v0.8.7.2 '.Import this !' 126 | --------------------------------------- 127 | 128 | * '.once' default encoding is 'utf-8-sig' on windows 129 | 130 | 131 | 2014-09-03b : v0.8.7.1 '.Import this !' 132 | --------------------------------------- 133 | 134 | * '.import' and '.once' support 135 | 136 | 137 | 2014-08-09a : v0.8.6 'Committed to speed' 138 | ----------------------------------------- 139 | 140 | * use a transaction when importing a csv file 141 | 142 | 143 | 2014-07-02b : v0.8.5 'Rename your tabs !' 144 | ----------------------------------------- 145 | 146 | * tabs can be renamed via double-click 147 | 148 | * more OS agnostic 149 | 150 | 151 | 2014-06-30a : v0.8.4 'Move your tabs !' 152 | --------------------------------------- 153 | 154 | * tabs can be dragged with the mouse 155 | 156 | 157 | 2014-06-28a : v0.8.3 'Cross on tabs !' 158 | -------------------------------------- 159 | 160 | * each tab has its closing button 161 | 162 | * Ctrl-Z and Ctrl-Y works on Script Text aera 163 | 164 | 165 | 2014-06-26a : v0.8.2 'Getting to the point' 166 | ------------------------------------------- 167 | 168 | * switch to no-autocommit mode by default to allow savepoints 169 | 170 | * a 'legacy autocommit' Open Database option is added 171 | 172 | * add an example of COMMIT and ROLLBACK, and an example of SAVEPOINTS 173 | 174 | 175 | 2014-06-25a : v0.8.1 'Attach them all !' 176 | ---------------------------------------- 177 | 178 | * support attachement of several databases with the same name 179 | 180 | 181 | 2014-06-21a : v0.8.0 'Mark the date !' 182 | -------------------------------------- 183 | 184 | * recognize date formats in .csv importation 185 | 186 | 187 | 2014-06-19a : v0.7.2 'Remember me' 188 | ---------------------------------- 189 | 190 | * keep memory of last directory used 191 | 192 | 193 | 2014-06-17a : v0.7.1 194 | -------------------- 195 | 196 | * improved publishing on Pypi (was tricky, especially the front page) 197 | 198 | 199 | 2014-06-15b : v0.7.0 200 | -------------------- 201 | 202 | * create a github project 'sqlite_bro', from 'sqlite_py_manager' baresql example 203 | 204 | * discover how to publish on Pypi (hard) 205 | 206 | 207 | 2014-06-14c : "It's a long way to temporary !" 208 | ---------------------------------------------- 209 | 210 | * works with temporary tables 211 | 212 | 213 | 2014-06-10a : 'Sanitizer of Python (xkcd.com/327)' 214 | -------------------------------------------------- 215 | 216 | * imported python functions must be validated 217 | 218 | 219 | 2014-06-09a : 'The magic 8th PEP' 220 | --------------------------------- 221 | 222 | * PEP8 alignement 223 | 224 | 225 | 2014-06-07a : 'Yield me a token' 226 | -------------------------------- 227 | 228 | * the pythonic way to generate tokens is 'Yield' 229 | 230 | 231 | 2014-06-04a : 'Log me out !' 232 | ---------------------------- 233 | 234 | * export SQL + SQL top result in a file in 1 click 235 | 236 | 237 | 2014-06-01a 'Commit and Rollback' 238 | --------------------------------- 239 | 240 | * support COMMIT and ROLLBACK 241 | 242 | 243 | 2014-06-03a : 'See me now ?' 244 | ---------------------------- 245 | 246 | * character INCREASE icon, so the back of the class can see 247 | 248 | 249 | 2014-05-25a : 'sql everywhere' 250 | ------------------------------ 251 | 252 | * make it work as low as Python 2.7 + SQlite 3.6.21 253 | 254 | 255 | 2014-05-25a : 'Assassination of Class Room' 256 | ------------------------------------------- 257 | 258 | * the GUI is a Class now 259 | 260 | 261 | 2014-05-11 262 | ---------- 263 | 264 | * addition of Tooltips over icons 265 | 266 | 267 | 2014-05-06 268 | ---------- 269 | 270 | * addition of the Welcome Demo 271 | 272 | 273 | 2014-05-01 274 | ---------- 275 | 276 | * birth : need of a ZERO-requirements SQLite Browser for a Python Class 277 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 stonebig 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include HISTORY.rst 2 | include README.rst 3 | include LICENCE 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | sqlite_bro : a graphic SQLite browser in 1 Python file 2 | ====================================================== 3 | 4 | sqlite_bro is a tool to browse SQLite databases with 5 | any basic python installation. 6 | 7 | 8 | Features 9 | -------- 10 | 11 | * Tabular browsing of a SQLite database 12 | 13 | * Import/Export of .csv files with auto-detection 14 | 15 | * Import/Export of .sql script 16 | 17 | * Export of database creation .sql script 18 | 19 | * Support of sql-embedded Python functions 20 | 21 | * support supports command-line scripting if Python>=3.2 (see sqlite_bro -h), with or without Graphic User Interface 22 | 23 | * Easy to distribute : 1 Python source file, Python and PyPy3 compatible 24 | 25 | * Easy to start : just launch sqlite_bro 26 | 27 | * Easy to learn : Welcome example, minimal interface 28 | 29 | * Easy to teach : Character size, SQL + SQL result export on a click 30 | 31 | Installation 32 | ------------ 33 | 34 | You can install, upgrade, uninstall sqlite_bro.py with these commands:: 35 | 36 | $ apt-get install python3-tk # apt-get install python-tk if you are using python2 37 | $ pip install sqlite_bro 38 | $ pip install --upgrade sqlite_bro 39 | $ pip uninstall sqlite_bro 40 | 41 | or just launch latest version from IPython with %load https://raw.githubusercontent.com/stonebig/sqlite_bro/master/sqlite_bro/sqlite_bro.py 42 | or just copy the file 'sqlite_bro.py' to any pc and type 'python sqlite_bro.py' 43 | 44 | Example usage 45 | ------------- 46 | 47 | :: 48 | 49 | $ sqlite_bro 50 | 51 | :: 52 | 53 | $ sqlite_bro -h 54 | 55 | Screenshots 56 | ----------- 57 | 58 | .. image:: https://raw.githubusercontent.com/stonebig/sqlite_bro/master/docs/sqlite_bro.GIF 59 | 60 | .. image:: https://raw.githubusercontent.com/stonebig/sqlite_bro/master/docs/sqlite_bro_command_line.GIF 61 | 62 | 63 | Links 64 | ----- 65 | 66 | * `Fork me on GitHub `_ 67 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | 3 | global: 4 | # SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the 5 | # /E:ON and /V:ON options are not enabled in the batch script intepreter 6 | # See: http://stackoverflow.com/a/13751649/163740 7 | WITH_COMPILER: "cmd /E:ON /V:ON /C .\\appveyor\\run_with_compiler.cmd" 8 | 9 | matrix: 10 | - PYTHON: "C:\\Python27" 11 | PYTHON_VERSION: "2.7.9" 12 | PYTHON_ARCH: "32" 13 | - PYTHON: "C:\\Python34" 14 | PYTHON_VERSION: "3.4.3" 15 | PYTHON_ARCH: "32" 16 | 17 | init: 18 | - "ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH%" 19 | 20 | install: 21 | - "powershell .\\appveyor\\install.ps1" 22 | 23 | # Prepend newly installed Python to the PATH of this build (this cannot be 24 | # done from inside the powershell script as it would require to restart 25 | # the parent CMD process). 26 | - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" 27 | 28 | # Check that we have the expected version and architecture for Python 29 | - "python --version" 30 | - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" 31 | 32 | # Install the build dependencies of the project. If some dependencies contain 33 | # compiled extensions and are not provided as pre-built wheel packages, 34 | # pip will build them from source using the MSVC compiler matching the 35 | # target Python version and architecture 36 | - "pip install nose" 37 | - "%WITH_COMPILER% python setup.py bdist_wheel" 38 | - pip install --pre --no-index --find-links dist/ sqlite_bro 39 | 40 | build: false # Not a C# project, build stuff at the test step instead. 41 | 42 | test_script: 43 | # Build the compiled extension and run the project tests 44 | - "nosetests -v sqlite_bro" 45 | 46 | after_test: 47 | # If tests are successful, create a whl package for the project. 48 | # - "%CMD_IN_ENV% python setup.py bdist_wheel bdist_wininst" 49 | - ps: "ls dist" 50 | 51 | artifacts: 52 | # Archive the generated wheel package in the ci.appveyor.com build report. 53 | - path: dist\* 54 | 55 | #on_success: 56 | # - TODO: upload the content of dist/*.whl to a public wheelhouse 57 | # 58 | -------------------------------------------------------------------------------- /appveyor/install.ps1: -------------------------------------------------------------------------------- 1 | # Sample script to install Python and pip (and wheel) under Windows 2 | # Authors: Olivier Grisel, Jonathan Helmus and Kyle Kastner 3 | # License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/ 4 | 5 | $MINICONDA_URL = "http://repo.continuum.io/miniconda/" 6 | $BASE_URL = "https://www.python.org/ftp/python/" 7 | $GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" 8 | $GET_PIP_PATH = "C:\get-pip.py" 9 | 10 | 11 | function DownloadPython ($python_version, $platform_suffix) { 12 | $webclient = New-Object System.Net.WebClient 13 | $filename = "python-" + $python_version + $platform_suffix + ".msi" 14 | $url = $BASE_URL + $python_version + "/" + $filename 15 | 16 | $basedir = $pwd.Path + "\" 17 | $filepath = $basedir + $filename 18 | if (Test-Path $filename) { 19 | Write-Host "Reusing" $filepath 20 | return $filepath 21 | } 22 | 23 | # Download and retry up to 4 times in case of network transient errors. 24 | Write-Host "Downloading" $filename "from" $url 25 | $retry_attempts = 3 26 | for($i=0; $i -lt $retry_attempts; $i++){ 27 | try { 28 | $webclient.DownloadFile($url, $filepath) 29 | break 30 | } 31 | Catch [Exception]{ 32 | Start-Sleep 1 33 | } 34 | } 35 | if (Test-Path $filepath) { 36 | Write-Host "File saved at" $filepath 37 | } else { 38 | # Retry once to get the error message if any at the last try 39 | $webclient.DownloadFile($url, $filepath) 40 | } 41 | return $filepath 42 | } 43 | 44 | 45 | function InstallPython ($python_version, $architecture, $python_home) { 46 | Write-Host "Installing Python" $python_version "for" $architecture "bit architecture to" $python_home 47 | if (Test-Path $python_home) { 48 | Write-Host $python_home "already exists, skipping." 49 | return $false 50 | } 51 | if ($architecture -eq "32") { 52 | $platform_suffix = "" 53 | } else { 54 | $platform_suffix = ".amd64" 55 | } 56 | $msipath = DownloadPython $python_version $platform_suffix 57 | Write-Host "Installing" $msipath "to" $python_home 58 | $install_log = $python_home + ".log" 59 | $install_args = "/qn /log $install_log /i $msipath TARGETDIR=$python_home" 60 | $uninstall_args = "/qn /x $msipath" 61 | RunCommand "msiexec.exe" $install_args 62 | if (-not(Test-Path $python_home)) { 63 | Write-Host "Python seems to be installed else-where, reinstalling." 64 | RunCommand "msiexec.exe" $uninstall_args 65 | RunCommand "msiexec.exe" $install_args 66 | } 67 | if (Test-Path $python_home) { 68 | Write-Host "Python $python_version ($architecture) installation complete" 69 | } else { 70 | Write-Host "Failed to install Python in $python_home" 71 | Get-Content -Path $install_log 72 | Exit 1 73 | } 74 | } 75 | 76 | function RunCommand ($command, $command_args) { 77 | Write-Host $command $command_args 78 | Start-Process -FilePath $command -ArgumentList $command_args -Wait -Passthru 79 | } 80 | 81 | 82 | function InstallPip ($python_home) { 83 | $pip_path = $python_home + "\Scripts\pip.exe" 84 | $python_path = $python_home + "\python.exe" 85 | if (-not(Test-Path $pip_path)) { 86 | Write-Host "Installing pip..." 87 | $webclient = New-Object System.Net.WebClient 88 | $webclient.DownloadFile($GET_PIP_URL, $GET_PIP_PATH) 89 | Write-Host "Executing:" $python_path $GET_PIP_PATH 90 | Start-Process -FilePath "$python_path" -ArgumentList "$GET_PIP_PATH" -Wait -Passthru 91 | } else { 92 | Write-Host "pip already installed." 93 | } 94 | } 95 | 96 | 97 | function DownloadMiniconda ($python_version, $platform_suffix) { 98 | $webclient = New-Object System.Net.WebClient 99 | if ($python_version -eq "3.4") { 100 | $filename = "Miniconda3-3.5.5-Windows-" + $platform_suffix + ".exe" 101 | } else { 102 | $filename = "Miniconda-3.5.5-Windows-" + $platform_suffix + ".exe" 103 | } 104 | $url = $MINICONDA_URL + $filename 105 | 106 | $basedir = $pwd.Path + "\" 107 | $filepath = $basedir + $filename 108 | if (Test-Path $filename) { 109 | Write-Host "Reusing" $filepath 110 | return $filepath 111 | } 112 | 113 | # Download and retry up to 4 times in case of network transient errors. 114 | Write-Host "Downloading" $filename "from" $url 115 | $retry_attempts = 3 116 | for($i=0; $i -lt $retry_attempts; $i++){ 117 | try { 118 | $webclient.DownloadFile($url, $filepath) 119 | break 120 | } 121 | Catch [Exception]{ 122 | Start-Sleep 1 123 | } 124 | } 125 | if (Test-Path $filepath) { 126 | Write-Host "File saved at" $filepath 127 | } else { 128 | # Retry once to get the error message if any at the last try 129 | $webclient.DownloadFile($url, $filepath) 130 | } 131 | return $filepath 132 | } 133 | 134 | 135 | function InstallMiniconda ($python_version, $architecture, $python_home) { 136 | Write-Host "Installing Python" $python_version "for" $architecture "bit architecture to" $python_home 137 | if (Test-Path $python_home) { 138 | Write-Host $python_home "already exists, skipping." 139 | return $false 140 | } 141 | if ($architecture -eq "32") { 142 | $platform_suffix = "x86" 143 | } else { 144 | $platform_suffix = "x86_64" 145 | } 146 | $filepath = DownloadMiniconda $python_version $platform_suffix 147 | Write-Host "Installing" $filepath "to" $python_home 148 | $install_log = $python_home + ".log" 149 | $args = "/S /D=$python_home" 150 | Write-Host $filepath $args 151 | Start-Process -FilePath $filepath -ArgumentList $args -Wait -Passthru 152 | if (Test-Path $python_home) { 153 | Write-Host "Python $python_version ($architecture) installation complete" 154 | } else { 155 | Write-Host "Failed to install Python in $python_home" 156 | Get-Content -Path $install_log 157 | Exit 1 158 | } 159 | } 160 | 161 | 162 | function InstallMinicondaPip ($python_home) { 163 | $pip_path = $python_home + "\Scripts\pip.exe" 164 | $conda_path = $python_home + "\Scripts\conda.exe" 165 | if (-not(Test-Path $pip_path)) { 166 | Write-Host "Installing pip..." 167 | $args = "install --yes pip" 168 | Write-Host $conda_path $args 169 | Start-Process -FilePath "$conda_path" -ArgumentList $args -Wait -Passthru 170 | } else { 171 | Write-Host "pip already installed." 172 | } 173 | } 174 | 175 | function InstallPackage ($python_home, $pkg) { 176 | # new function added from pipa 177 | $pip_path = $python_home + "\Scripts\pip.exe" 178 | & $pip_path install $pkg 179 | } 180 | 181 | function main () { 182 | InstallPython $env:PYTHON_VERSION $env:PYTHON_ARCH $env:PYTHON 183 | InstallPip $env:PYTHON 184 | # added from pipa 185 | InstallPackage $env:PYTHON wheel 186 | } 187 | 188 | main 189 | -------------------------------------------------------------------------------- /appveyor/run_with_compiler.cmd: -------------------------------------------------------------------------------- 1 | :: To build extensions for 64 bit Python 3, we need to configure environment 2 | :: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of: 3 | :: MS Windows SDK for Windows 7 and .NET Framework 4 (SDK v7.1) 4 | :: 5 | :: To build extensions for 64 bit Python 2, we need to configure environment 6 | :: variables to use the MSVC 2008 C++ compilers from GRMSDKX_EN_DVD.iso of: 7 | :: MS Windows SDK for Windows 7 and .NET Framework 3.5 (SDK v7.0) 8 | :: 9 | :: 32 bit builds do not require specific environment configurations. 10 | :: 11 | :: Note: this script needs to be run with the /E:ON and /V:ON flags for the 12 | :: cmd interpreter, at least for (SDK v7.0) 13 | :: 14 | :: More details at: 15 | :: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows 16 | :: http://stackoverflow.com/a/13751649/163740 17 | :: 18 | :: Author: Olivier Grisel 19 | :: License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/ 20 | @ECHO OFF 21 | 22 | SET COMMAND_TO_RUN=%* 23 | SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows 24 | 25 | SET MAJOR_PYTHON_VERSION="%PYTHON_VERSION:~0,1%" 26 | IF %MAJOR_PYTHON_VERSION% == "2" ( 27 | SET WINDOWS_SDK_VERSION="v7.0" 28 | ) ELSE IF %MAJOR_PYTHON_VERSION% == "3" ( 29 | SET WINDOWS_SDK_VERSION="v7.1" 30 | ) ELSE ( 31 | ECHO Unsupported Python version: "%MAJOR_PYTHON_VERSION%" 32 | EXIT 1 33 | ) 34 | 35 | IF "%PYTHON_ARCH%"=="64" ( 36 | ECHO Configuring Windows SDK %WINDOWS_SDK_VERSION% for Python %MAJOR_PYTHON_VERSION% on a 64 bit architecture 37 | SET DISTUTILS_USE_SDK=1 38 | SET MSSdk=1 39 | "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION% 40 | "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release 41 | ECHO Executing: %COMMAND_TO_RUN% 42 | call %COMMAND_TO_RUN% || EXIT 1 43 | ) ELSE ( 44 | ECHO Using default MSVC build environment for 32 bit architecture 45 | ECHO Executing: %COMMAND_TO_RUN% 46 | call %COMMAND_TO_RUN% || EXIT 1 47 | ) 48 | -------------------------------------------------------------------------------- /docs/sqlite_bro.GIF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonebig/sqlite_bro/6f3e76125fe74d26648b9046fe7448b0587b7b88/docs/sqlite_bro.GIF -------------------------------------------------------------------------------- /docs/sqlite_bro_command_line.GIF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonebig/sqlite_bro/6f3e76125fe74d26648b9046fe7448b0587b7b88/docs/sqlite_bro_command_line.GIF -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] # flit_core seems the step after 'setuptools','wheel','build','twine' (see https://github.com/pypa/build/issues/394) 2 | requires = ["flit_core"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "sqlite_bro" 7 | authors = [ 8 | {name = "stonebig"}, 9 | ] 10 | dependencies = [] 11 | requires-python = ">=3.3" 12 | readme = "README.rst" 13 | license = {file = "LICENSE"} 14 | classifiers=[ 15 | 'Intended Audience :: Education', 16 | 'License :: OSI Approved :: MIT License', 17 | 'Operating System :: MacOS', 18 | 'Operating System :: Microsoft :: Windows', 19 | 'Operating System :: OS Independent', 20 | 'Operating System :: POSIX', 21 | 'Operating System :: Unix', 22 | 'Programming Language :: Python :: 3', 23 | 'Development Status :: 5 - Production/Stable', 24 | 'Topic :: Scientific/Engineering', 25 | 'Topic :: Software Development :: Widget Sets', 26 | ] 27 | dynamic = ["version",] 28 | description="a graphic SQLite Client in 1 Python file" 29 | keywords = ["sqlite", "gui", "ttk", "sql"] 30 | 31 | [project.urls] 32 | Documentation = "https://github.com/stonebig/sqlite_bro/README.rst" 33 | Source = "https://github.com/stonebig/sqlite_bro" 34 | 35 | [project.scripts] 36 | sqlite_bro = "sqlite_bro.sqlite_bro:_main" 37 | -------------------------------------------------------------------------------- /sqlite_bro/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.13.1' -------------------------------------------------------------------------------- /sqlite_bro/sqlite_bro.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import print_function, unicode_literals, division # Python2.7 4 | 5 | try: 6 | import argparse # Python>=3.2 7 | except ImportError: 8 | pass # Python<3.2 9 | 10 | import sqlite3 as sqlite 11 | import sys 12 | import os 13 | import locale 14 | import csv 15 | import datetime 16 | import io 17 | import shlex # Simple lexical analysis 18 | from os.path import expanduser 19 | import tempfile as tmpf 20 | import subprocess 21 | 22 | try: # We are Python 3.3+ 23 | from tkinter import * 24 | from tkinter import font, ttk, filedialog, messagebox 25 | from tkinter.ttk import * 26 | except ImportError: # or we are still Python2.7 27 | from Tkinter import * 28 | import Tkinter as tkinter 29 | import tkFont as font 30 | import tkFileDialog as filedialog 31 | import tkMessageBox as messagebox 32 | from ttk import * 33 | import ttk as ttk 34 | 35 | tipwindow = None 36 | 37 | # starting Python-3.13, PEP 667 forces us to us a specific dictionnary pydef_locals instead of locals() 38 | pydef_locals ={} 39 | 40 | class App: 41 | """the GUI graphic application""" 42 | 43 | def __init__(self, use_gui=True): 44 | """create a tkk graphic interface with a main window tk_win""" 45 | self.__version__ = "0.13.1" 46 | self._title = "of 2024-05-11b : 'Setup me down !'" 47 | self.conn = None # Baresql database object 48 | self.database_file = "" 49 | self.initialdir = "." 50 | 51 | # Do we use a GUI ? 52 | self.use_gui = use_gui # gui ok by default 53 | try: 54 | self.tk_win = Tk() 55 | except: 56 | self.use_gui = False 57 | 58 | if self.use_gui: 59 | self.tk_win.title( 60 | "A graphic SQLite Client in 1 Python file (" + self.__version__ + ")" 61 | ) 62 | self.tk_win.option_add("*tearOff", FALSE) # hint of tk documentation 63 | self.tk_win.minsize(600, 200) # minimal size 64 | 65 | self.font_size = 10 66 | self.font_wheight = 0 67 | # With a Menubar and Toolbar 68 | self.create_menu() 69 | self.create_toolbar() 70 | 71 | # Create style "ButtonNotebook" 72 | self.create_style() 73 | # Initiate Drag State 74 | self.state_drag = False 75 | self.state_drag_index = 0 76 | 77 | # With a Panedwindow of two frames: 'Database' and 'Queries' 78 | p = ttk.Panedwindow(self.tk_win, orient=HORIZONTAL) 79 | p.pack(fill=BOTH, expand=1) 80 | 81 | f_database = ttk.Labelframe(p, text="Databases", width=200, height=100) 82 | p.add(f_database) 83 | f_queries = ttk.Labelframe(p, text="Queries", width=200, height=100) 84 | p.add(f_queries) 85 | 86 | # build tree view 't' inside the left 'Database' Frame 87 | self.db_tree = ttk.Treeview( 88 | f_database, displaycolumns=[], columns=("detail", "action") 89 | ) 90 | self.db_tree.tag_configure("run") 91 | self.db_tree.pack(fill=BOTH, expand=1) 92 | 93 | # create a notebook 'n' inside the right 'Queries' Frame 94 | self.n = NotebookForQueries(self.tk_win, f_queries, [], self.use_gui) 95 | 96 | # Bind keyboard shortcuts 97 | self.tk_win.bind("", self.run_tab) 98 | else: 99 | # create a GUI-Less notebook 'n' 100 | self.n = NotebookForQueries(None, None, [], self.use_gui) 101 | 102 | # define default home directory 103 | self.home = expanduser("~") 104 | 105 | # defaults for export 106 | self.default_header = True 107 | self.default_separator = "," 108 | self.current_directory = os.getcwd() 109 | 110 | # Initiate Output State 111 | self.once_mode = False 112 | self.encode_in = "utf-8" 113 | self.output_file = None 114 | self.init_output = True 115 | self.output_mode = False 116 | self.x_mode = False 117 | 118 | def create_menu(self): 119 | """create the menu of the application""" 120 | menubar = Menu(self.tk_win) 121 | self.tk_win["menu"] = menubar 122 | 123 | # feeding the top level menu 124 | self.menu = Menu(menubar) 125 | menubar.add_cascade(menu=self.menu, label="Database") 126 | self.menu_help = Menu(menubar) 127 | menubar.add_cascade(menu=self.menu_help, label="?") 128 | 129 | # feeding database sub-menu 130 | self.menu.add_command(label="New Database", command=self.new_db) 131 | self.menu.add_command( 132 | label="New In-Memory Database", command=lambda: self.new_db(":memory:") 133 | ) 134 | self.menu.add_command(label="Open Database ...", command=self.open_db) 135 | self.menu.add_command( 136 | label="Open Database ...(legacy auto-commit)", 137 | command=lambda: self.open_db(""), 138 | ) 139 | self.menu.add_command(label="Close Database", command=self.close_db) 140 | self.menu.add_separator() 141 | self.menu.add_command(label="Attach Database", command=self.attach_db) 142 | if sys.version_info[:2] >= (3, 7): 143 | self.menu.add_separator() 144 | self.menu.add_command(label="Backup main Database", command=self.backup_db) 145 | self.menu.add_command( 146 | label="Restore into main Database", command=self.restore_db 147 | ) 148 | self.menu.add_separator() 149 | self.menu.add_command(label="Quit", command=self.quit_db) 150 | 151 | self.menu_help.add_command( 152 | label="about", 153 | command=lambda: messagebox.showinfo( 154 | message=""" 155 | \nSQLite_bro : a graphic SQLite Client in 1 Python file 156 | \nVersion """ 157 | + self.__version__ 158 | + " " 159 | + self._title 160 | + "\n(https://github.com/stonebig/sqlite_bro)" 161 | + "\n\nrun by: " 162 | + sys.executable 163 | + "\n\nhome: " 164 | + self.home 165 | + "\n\ncurrent directory: " 166 | + os.getcwd() 167 | ), 168 | ) 169 | 170 | def create_toolbar(self): 171 | """create the toolbar of the application""" 172 | self.toolbar = Frame(self.tk_win, relief=RAISED) 173 | self.toolbar.pack(side=TOP, fill=X) 174 | self.tk_icon = self.get_tk_icons() 175 | 176 | # list of (image, action, tooltip) : 177 | to_show = [ 178 | ("refresh_img", self.actualize_db, "Actualize databases"), 179 | ("run_img", self.run_tab, "Run script selection"), 180 | ( 181 | "newtab_img", 182 | lambda x=self: x.n.new_query_tab("___", ""), 183 | "Create a new script", 184 | ), 185 | ("csvin_img", self.import_csvtb, "Import a CSV file into a table"), 186 | ("csvex_img", self.export_csvtb, "Export selected table to a CSV file"), 187 | ("dbdef_img", self.savdb_script, "Save main database as a SQL script"), 188 | ("qryex_img", self.export_csvqr, "Export script selection to a CSV file"), 189 | ( 190 | "exe_img", 191 | self.exsav_script, 192 | "Run script+output to a file (First 200 rec. per Qry)", 193 | ), 194 | ("sqlin_img", self.load_script, "Load a SQL script file"), 195 | ("sqlsav_img", self.sav_script, "Save a SQL script in a file"), 196 | ("chgsz_img", self.chg_fontsize, "Modify font size"), 197 | ( 198 | "img_run_temp", 199 | self.run_temp, 200 | "Run script selection and Display output in temporary files", 201 | ), 202 | ("img_clean_temp", self.clean_temp, "Remove old temporary files"), 203 | ] 204 | 205 | for img, action, tip in to_show: 206 | b = Button(self.toolbar, image=self.tk_icon[img], command=action) 207 | b.pack(side=LEFT, padx=2, pady=2) 208 | self.createToolTip(b, tip) 209 | 210 | def set_initialdir(self, proposal): 211 | """change initial dir, if possible""" 212 | if os.path.isfile(proposal): 213 | self.initialdir = os.path.dirname(proposal) 214 | 215 | def new_db(self, filename=""): 216 | """create a new database""" 217 | if filename == "": 218 | filename = filedialog.asksaveasfilename( 219 | initialdir=self.initialdir, 220 | defaultextension=".db", 221 | title="Define a new database name and location", 222 | filetypes=[("default", "*.db"), ("other", "*.db*"), ("all", "*.*")], 223 | ) 224 | if filename != "": 225 | self.database_file = filename 226 | if os.path.isfile(filename): 227 | self.set_initialdir(filename) 228 | if messagebox.askyesno( 229 | message="Confirm Destruction of previous Datas ?", 230 | icon="question", 231 | title="Destroying", 232 | ): 233 | os.remove(filename) 234 | self.conn = Baresql(self.database_file) 235 | self.actualize_db() 236 | 237 | def open_db(self, filename="", isolation_level=None): 238 | """open an existing database""" 239 | if filename == "": 240 | filename = filedialog.askopenfilename( 241 | initialdir=self.initialdir, 242 | defaultextension=".db", 243 | filetypes=[("default", "*.db"), ("other", "*.db*"), ("all", "*.*")], 244 | ) 245 | if filename != "": 246 | self.set_initialdir(filename) 247 | self.database_file = filename 248 | self.conn = Baresql(self.database_file) 249 | self.actualize_db() 250 | 251 | def backup_db(self, filename="", isolation_level=None): 252 | """Backup the current database""" 253 | if filename == "": 254 | filename = filedialog.asksaveasfilename( 255 | initialdir=self.initialdir, 256 | defaultextension=".db", 257 | title="Define a new database name and location", 258 | filetypes=[("default", "*.db"), ("other", "*.db*"), ("all", "*.*")], 259 | ) 260 | if filename != "": 261 | if os.path.isfile(filename): 262 | self.set_initialdir(filename) 263 | if messagebox.askyesno( 264 | message="Confirm Destruction of previous Datas ?", 265 | icon="question", 266 | title="Destroying", 267 | ): 268 | os.remove(filename) 269 | db_to = sqlite.connect(filename) 270 | self.conn.conn.backup(db_to) 271 | db_to.close() 272 | self.actualize_db() 273 | 274 | def restore_db(self, filename="", isolation_level=None): 275 | """Restore an existing database into current one""" 276 | if filename == "": 277 | filename = filedialog.askopenfilename( 278 | initialdir=self.initialdir, 279 | defaultextension=".db", 280 | filetypes=[("default", "*.db"), ("other", "*.db*"), ("all", "*.*")], 281 | ) 282 | if filename != "": 283 | db_from = sqlite.connect(filename) 284 | db_from.backup(self.conn.conn) 285 | db_from.close 286 | self.actualize_db() 287 | 288 | def load_script(self): 289 | """load a script file, ask validation of detected Python code""" 290 | filename = filedialog.askopenfilename( 291 | initialdir=self.initialdir, 292 | defaultextension=".sql", 293 | filetypes=[("default", "*.sql"), ("other", "*.txt"), ("all", "*.*")], 294 | ) 295 | if filename != "": 296 | self.set_initialdir(filename) 297 | text = os.path.split(filename)[1].split(".")[0] 298 | with io.open(filename, encoding=guess_encoding(filename)[0]) as f: 299 | script = f.read() 300 | sqls = self.conn.get_sqlsplit(script, remove_comments=True) 301 | dg = [ 302 | s 303 | for s in sqls 304 | if s.strip(" \t\n\r")[:5].lower() in ("pydef", ".read", ".shel") 305 | or s.strip(" \t\n\r")[:1].lower() == "." 306 | ] 307 | if dg: 308 | fields = [ 309 | "", 310 | ["In Script File:", filename, "r", 100], 311 | "", 312 | ["non pure SQL code", "\n".join(dg), "r", 80, 20], 313 | ] 314 | 315 | create_dialog( 316 | ("Ok for this non pure SQL code ?"), 317 | fields, 318 | ("Confirm", self.load_script_ok), 319 | [text, script], 320 | ) 321 | else: 322 | new_tab_ref = self.n.new_query_tab(text, script) 323 | 324 | def load_script_ok(self, thetop, entries, actions): 325 | """continue loading of script after confirmation dialog""" 326 | new_tab_ref = self.n.new_query_tab(*actions) 327 | thetop.destroy() 328 | 329 | def savdb_script(self): 330 | """save database as a script file""" 331 | filename = filedialog.asksaveasfilename( 332 | initialdir=self.initialdir, 333 | defaultextension=".db", 334 | title="save database structure in a text file", 335 | filetypes=[("default", "*.sql"), ("other", "*.txt"), ("all", "*.*")], 336 | ) 337 | if filename != "": 338 | self.set_initialdir(filename) 339 | with io.open(filename, "w", encoding="utf-8") as f: 340 | for line in self.conn.iterdump(): 341 | f.write("%s\n" % line) 342 | 343 | def sav_script(self): 344 | """save a script in a file""" 345 | active_tab_id = self.n.notebook.select() 346 | if active_tab_id != "": 347 | # get current selection (or all) 348 | fw = self.n.fw_labels[active_tab_id] 349 | script = fw.get(1.0, END)[:-1] 350 | filename = filedialog.asksaveasfilename( 351 | initialdir=self.initialdir, 352 | defaultextension=".db", 353 | title="save script in a sql file", 354 | filetypes=[("default", "*.sql"), ("other", "*.txt"), ("all", "*.*")], 355 | ) 356 | if filename != "": 357 | self.set_initialdir(filename) 358 | with io.open(filename, "w", encoding="utf-8") as f: 359 | if "你好 мир Artisou à croute" not in script: 360 | f.write("/*utf-8 tag : 你好 мир Artisou à croute*/\n") 361 | f.write(script) 362 | 363 | def attach_db(self, filename="", attach_as=""): 364 | """attach an existing database""" 365 | if filename == "": 366 | filename = filedialog.askopenfilename( 367 | initialdir=self.initialdir, 368 | defaultextension=".db", 369 | title="Choose a database to attach ", 370 | filetypes=[("default", "*.db"), ("other", "*.db*"), ("all", "*.*")], 371 | ) 372 | if attach_as == "": 373 | attach = os.path.basename(filename).split(".")[0] 374 | else: 375 | attach = attach_as 376 | avoid = {i[1]: 0 for i in get_leaves(self.conn, "attached_databases")} 377 | att, indice = attach, 0 378 | while attach in avoid: 379 | attach, indice = att + "_" + str(indice), indice + 1 380 | if filename != "": 381 | self.set_initialdir(filename) 382 | attach_order = "ATTACH DATABASE '%s' as '%s' " % (filename, attach) 383 | self.conn.execute(attach_order) 384 | self.actualize_db() 385 | 386 | def close_db(self): 387 | """close the database""" 388 | self.conn.close 389 | self.new_db(":memory:") 390 | self.actualize_db() 391 | 392 | def actualize_db(self): 393 | """refresh the database view""" 394 | if not self.use_gui: 395 | return 396 | 397 | # bind double-click for easy user interaction 398 | self.db_tree.tag_bind("run", "", self.t_doubleClicked) 399 | self.db_tree.tag_bind("run_up", "", self.t_doubleClicked) 400 | 401 | # delete existing tree entries before re-creating them 402 | for node in self.db_tree.get_children(): 403 | self.db_tree.delete(node) 404 | # create top node 405 | dbtext = os.path.basename(self.database_file) 406 | id0 = self.db_tree.insert( 407 | "", 0, "Database", text="main (%s)" % dbtext, values=(dbtext, "") 408 | ) 409 | # add Database Objects, by Category 410 | for categ in ["master_table", "table", "view", "trigger", "index", "pydef"]: 411 | self.feed_dbtree(id0, categ, "main") 412 | # for attached databases 413 | for att_db in self.feed_dbtree(id0, "attached_databases"): 414 | # create another top node 415 | dbtext2, insert_position = att_db + " (Attached)", "end" 416 | if att_db == "temp": 417 | dbtext2, insert_position = "temp (%s)" % dbtext, 0 418 | id0 = self.db_tree.insert( 419 | "", insert_position, dbtext2, text=dbtext2, values=(att_db, "") 420 | ) 421 | # add attached Database Objects, by Category 422 | for categ in ["master_table", "table", "view", "trigger", "index"]: 423 | self.feed_dbtree(id0, categ, att_db) 424 | # update time of last refresh 425 | self.db_tree.heading( 426 | "#0", text=(datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")) 427 | ) 428 | 429 | def quit_db(self): 430 | """quit the application""" 431 | if messagebox.askyesno( 432 | message="Are you sure you want to quit ?", icon="question", title="Quiting" 433 | ): 434 | self.tk_win.destroy() 435 | 436 | def run_tab(self, event=None): 437 | """clear previous results and run current script of a tab""" 438 | if not self.use_gui: 439 | active_tab_id = len(self.n.nongui_tabs) - 1 440 | script = self.n.nongui_tabs[active_tab_id] 441 | # print ("silently running,active_tab_id , script) # testtmp 442 | self.create_and_add_results(script, active_tab_id) 443 | return 444 | active_tab_id = self.n.notebook.select() 445 | if active_tab_id != "": 446 | # remove previous results 447 | self.n.remove_treeviews(active_tab_id) 448 | # get current selection (or all) 449 | fw = self.n.fw_labels[active_tab_id] 450 | try: 451 | script = fw.get("sel.first", "sel.last") 452 | except: 453 | script = fw.get(1.0, END)[:-1] 454 | self.create_and_add_results(script, active_tab_id) 455 | fw.focus_set() # workaround bug http://bugs.python.org/issue17511 456 | 457 | def exsav_script(self): 458 | """write script commands + top results to a log file""" 459 | # idea from http://blog.mp933.fr/post/2014/05/15/Script-vs.-copy/paste 460 | active_tab_id = self.n.notebook.select() 461 | if active_tab_id != "": 462 | # get current selection (or all) 463 | fw = self.n.fw_labels[active_tab_id] 464 | script = fw.get(1.0, END)[:-1] 465 | filename = filedialog.asksaveasfilename( 466 | initialdir=self.initialdir, 467 | defaultextension=".db", 468 | title="execute Script + output in a log file", 469 | filetypes=[("default", "*.txt"), ("other", "*.log"), ("all", "*.*")], 470 | ) 471 | if filename == "": 472 | return 473 | self.set_initialdir(filename) 474 | with io.open(filename, "w", encoding="utf-8") as f: 475 | if "你好 мир Artisou à croute" not in script: 476 | f.write("/*utf-8 tag : 你好 мир Artisou à croute*/\n") 477 | self.create_and_add_results(script, active_tab_id, limit=99, log=f) 478 | fw.focus_set() # workaround bug http://bugs.python.org/issue17511 479 | 480 | def run_temp(self): 481 | """run selected script commands and display results via tmp files""" 482 | # backup existing defaults 483 | once_mode_bkp = self.once_mode 484 | encode_in_bkp = self.encode_in 485 | output_file_bkp = self.output_file 486 | init_output_bkp = self.init_output 487 | output_mode_bkp = self.output_mode 488 | x_mode_bkp = self.x_mode 489 | 490 | active_tab_id = self.n.notebook.select() 491 | if active_tab_id != "": 492 | self.n.remove_treeviews(active_tab_id) 493 | 494 | if active_tab_id != "": 495 | # get current selection (or all) 496 | fw = self.n.fw_labels[active_tab_id] 497 | fw = self.n.fw_labels[active_tab_id] 498 | try: 499 | query = fw.get("sel.first", "sel.last") 500 | except: 501 | query = fw.get(1.0, END)[:-1] 502 | query = "\n.output --bom -x \n" + query 503 | # print("run temp", query) 504 | self.create_and_add_results(query, active_tab_id) 505 | fw.focus_set() # workaround bug http://bugs.python.org/issue17511 506 | 507 | # restore previous defaults 508 | self.once_mode = once_mode_bkp 509 | self.encode_in = encode_in_bkp 510 | self.output_file = output_file_bkp 511 | self.init_output = init_output_bkp 512 | self.output_mode = output_mode_bkp 513 | self.x_mode = x_mode_bkp 514 | 515 | def chg_fontsize(self): 516 | """change the display font size""" 517 | sizes = [10, 13, 14] 518 | font_types = [ 519 | "TkDefaultFont", 520 | "TkTextFont", 521 | "TkFixedFont", 522 | "TkMenuFont", 523 | "TkHeadingFont", 524 | "TkCaptionFont", 525 | "TkSmallCaptionFont", 526 | "TkIconFont", 527 | "TkTooltipFont", 528 | ] 529 | ww = ["normal", "bold"] 530 | if self.font_size < max(sizes): 531 | self.font_size = min([i for i in sizes if i > self.font_size]) 532 | else: 533 | self.font_size = sizes[0] 534 | self.font_wheight = 0 535 | 536 | ff = "Helvetica" if self.font_size != min(sizes) else "Courier" 537 | self.font_wheight = 0 if self.font_size == min(sizes) else 1 538 | for typ in font_types: 539 | default_font = font.nametofont(typ) 540 | default_font.configure( 541 | size=self.font_size, weight=ww[self.font_wheight], family=ff 542 | ) 543 | 544 | def clean_temp(self): 545 | """clear temp directory""" 546 | ff = tmpf.NamedTemporaryFile(delete=True, suffix="_sqlite_bro.csv").name 547 | temp_directory = os.path.dirname(ff) 548 | report = [ 549 | ("", ""), 550 | ] 551 | for file in os.listdir(temp_directory): 552 | if file.endswith("_sqlite_bro.csv"): 553 | print("removing ", os.path.join(temp_directory, file)) 554 | try: 555 | os.remove(os.path.join(temp_directory, file)) 556 | report += [ 557 | ("removing ", os.path.join(temp_directory, file)), 558 | ] 559 | except PermissionError: 560 | report += [ 561 | ("PermissionError ", os.path.join(temp_directory, file)), 562 | ] 563 | 564 | active_tab_id = self.n.notebook.select() 565 | if active_tab_id != "": 566 | self.n.remove_treeviews(active_tab_id) 567 | self.n.add_treeview( 568 | active_tab_id, 569 | ("Cleanup", "file"), 570 | (report), 571 | "Cleanup", 572 | ".Cleaning tmp files", 573 | ) 574 | 575 | def t_doubleClicked(self, event): 576 | """launch action when dbl_click on the Database structure""" 577 | # determine item to consider 578 | selitem = self.db_tree.focus() # the item having the focus 579 | seltag = self.db_tree.item(selitem, "tag")[0] 580 | if seltag == "run_up": # 'run-up' tag ==> dbl-click 1 level up 581 | selitem = self.db_tree.parent(selitem) 582 | # get final information : text, selection and action 583 | definition, action = self.db_tree.item(selitem, "values") 584 | tab_text = self.db_tree.item(selitem, "text") 585 | script = action + " limit 999 " if action != "" else definition 586 | 587 | # create a new tab and run it if action suggest it 588 | new_tab_ref = self.n.new_query_tab(tab_text, script) 589 | if action != "": 590 | self.run_tab() # run the new_tab created 591 | 592 | def get_tk_icons(self): 593 | """return a dictionary of icon_in_tk_format, from B64 images""" 594 | # to create this base 64 from a toto.gif image of 24x24 size do : 595 | # import base64 596 | # b64 = base64.encodestring(open(r"toto.gif","rb").read()) 597 | # print("'gif_img': '''\\\n" + b64.decode("utf8") + "'''") 598 | icons = { 599 | "run_img": """\ 600 | R0lGODdhGAAYAJkAADOqM////wCqMwAAACwAAAAAGAAYAAACM4SPqcvt7wJ8oU5W8025b9OFW0hO 601 | 5EmdKKauSosKL9zJC21FsK27kG+qfUC5IciITConBQA7 602 | """, 603 | "exe_img": """\ 604 | R0lGODdhGAAYALsAAP///zOqM/8AAGSJtqHA4Jyen3ul0+jo6Y6z2cLCwaSmpACqM4ODgmKGs4yM 605 | jYOPniwAAAAAGAAYAAAEhBDISacqOBdWOy1HKB6F41mKwihH4r4kdypD0wx4rg8nUDSEoHAY5J0K 606 | AyFiyWQaPY+kYUqtGp4dx26b60kE4LC3FwaPyeJOYM1ur8sCzxrgZovN6sDEHdYD4nkVb2BzPYUV 607 | hIdyfouMi14BC5COgoqBHQttk5VumxJ1bJuZoJacpKE9EQA7 608 | """, 609 | "refresh_img": """\ 610 | R0lGODdhGAAYAJkAAP///zOqMwCqMwAAACwAAAAAGAAYAAACSoSPqcvt4aIJEFU5g7AUC9px1/JR 611 | 3yYy4LqAils2IZdFMzCP6nhLd2/j6VqHD+1RAQKLHVfC+VwtcT3pNKOTYjTC4SOK+YbH5EYBADs= 612 | """, 613 | "newtab_img": """\ 614 | R0lGODdhGAAYAJkAAP///56fnQAAAAAAACwAAAAAGAAYAAACSoSPqcsm36KDsj1R1d00840E4ige 615 | 3xWSo9YppRGwGKPGCUrXtfQCut3ouYC5IHEhTCSHRt4x5fytIlKSs0l9HpZKLcy7BXOhRUYBADs= 616 | """, 617 | "csvin_img": """\ 618 | R0lGODdhGAAYAMwAAPj4+AAAADOqM2FkZtjY2r7Awujo6V9gYeDg4b/Cwzc3N0pKSl9fX5GRkVVV 619 | VXl6fKSmpLCxsouNkFdXV97d4N7e4N7g4IyMjZyen6SopwAAAAAAAAAAAAAAAAAAAAAAACwAAAAA 620 | GAAYAAAFlSAgjkBgmuUZlONKii4br/MLv/Gt47ia/rYcT2bb0VowVFFF8+2K0KjUJqhOo1XBlaQV 621 | Zbdc7Rc8ylrJ5THaa5YqFozBgOFQAMznl6FhsO37UwMEBgiFFRYIhANXBxgJBQUJkpAZi1MEBxAR 622 | kI8REAMUVxIEcgcDpqYEElcODwSvsK8PllMLAxeQkA0DDmhvEwwLdmAhADs= 623 | """, 624 | "csvex_img": """\ 625 | R0lGODdhGAAYAMwAAPj4+AAAADOqM2FkZtjY2r7AwuDg4b/Cw+jo6V9gYTc3N0pKSlVVVV9fX5GR 626 | kaSmpLCxsnl6fIuNkN7g4N7d4KSop4yMjZyen1dXVwAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAA 627 | GAAYAAAFkiAgjkBgmuUZlONKii4br/MLv/Gt47ia/rYcT2bb0VowVFFF8+2K0Kh0JKhap1BrFZu9 628 | cl9awZd03Y4BXvQ5DVUsGoNBg6FAm6MOhA3h4A4ICAaCBhOCCANYAxcHBQUHj44ViFMECQ8QjY0Q 629 | DwMUWBIEcQkDo6MEElgMEQSsrawRk1MLAxaZBQ4DDGduGA0LdV8hADs= 630 | """, 631 | "qryex_img": """\ 632 | R0lGODdhGAAYAJkAAP///56fnQAAAP8AACwAAAAAGAAYAAACXIQPoporeR4yEtZ3J511e845zah1 633 | oKV9WEQxqYOJX0rX9oDndp3jO6/7aXqDVOCIPB50Pk0yaQgCijSlITBt/p4B6ZbL3VkBYKxt7DTX 634 | 0BN2uowUw+NndVq+tk8KADs= 635 | """, 636 | "sqlin_img": """\ 637 | R0lGODdhGAAYALsAAP///46z2Xul02SJtp6fnenp6f8AAMLCwaHA4IODgoCo01RymIOPnmKGswAA 638 | AAAAACwAAAAAGAAYAAAEkRDIOYm9N9G9SfngR2jclnhHqh7FWAKZF84uFxdr3pb3TPOWEy6n2tkG 639 | jcZgyWw6OwOEdEqtIgYbRjTA7Xq/WIoW8S17wxOteR1AS9Ts8sI08Aru+Px9TknU9YB5fBN+AYGH 640 | gxJ+dwoCjY+OCpKNiQAGBk6ZTgsGE5edLy+XlqOhop+gpiWoqqGoqa0Ur7CxABEAOw== 641 | """, 642 | "sqlsav_img": """\ 643 | R0lGODdhGAAYALsAAP///56fnZyen/8AAGSJtqHA4Hul0+jo6Y6z2cLCwaSmpIODgmKGs4yMjYOP 644 | ngAAACwAAAAAGAAYAAAEgxDISacSOItVOxVHKB5C41mKsihH4r4kdyoEwxB4rhMnIDCFoHAY5J0E 645 | BCFiyWQaPY6kYUqtGp6dxm6b60kG4LC3FwaPyeJzpzzwaDQTsbkTqNsx3zmgfapPAnt6Y3Z1Amlq 646 | AoR3cF5+EoqFY4k9jpSAfQKSkJCDm4SZXpN9l5aUoB4RADs= 647 | """, 648 | "dbdef_img": """\ 649 | R0lGODdhGAAYAMwAAPj4+DOqM2SJtmFkZqHA4NjY2sLCwejo6b7Awpyen3ul046z2aSop+Dg4V9g 650 | YZGRkaSmpLCxsouNkDc3N2dxekpKSlVVVYyMjWKGs4ODgnl6fJ+goYOPnl9fX0xMTAAAACwAAAAA 651 | GAAYAAAFuCAgjuTIJGiaZGVLJkcsH9DjmgxzMYfh/4fVDcAQCDDGpNI4hGAI0KgUKhgmBNGFdrut 652 | 3jhYhXhMVnhdj6U6OWy7h4G4/O2Sx+n1OZ5kD5QmHgOCAx0TJXN3Iw8HLQc2InoAfiIUBQcNmJkN 653 | BxSSiS0DCT8/CAYMA55DBQMQEQilCBGndBKrAw4Ot7kFEm+rG66vsRCob7WCube3vG8WGgXQ0dAa 654 | nW8VAxfCCA8DFnsAExUWAxWGeCEAOw== 655 | """, 656 | "chgsz_img": """\ 657 | R0lGODdhGAAYAJkAAP///wAAADOqMwCqMywAAAAAGAAYAAACZISPGRvpb1iDRjy5KBBWYc0NXjQ9 658 | A8cdDFkiZyiIwDpnCYqzCF2lr2rTHVKbDgsTJG52yE8R0nRSJA7qNOhpVbFPHhdhPF20w46S+f2h 659 | xlzceksqu6ET7JwtLRrhwNt+1HdDUQAAOw== 660 | """, 661 | "img_close": """\ 662 | R0lGODlhCAAIAMIBAAAAADs7O4+Pj9nZ2Ts7Ozs7Ozs7Ozs7OyH+EUNyZWF0ZWQgd2l0aCBHSU1Q 663 | ACH5BAEKAAQALAAAAAAIAAgAAAMVGDBEA0qNJyGw7AmxmuaZhWEU5kEJADs= 664 | """, 665 | "img_closeactive": """\ 666 | R0lGODlhCAAIAMIEAAAAAP/SAP/bNNnZ2cbGxsbGxsbGxsbGxiH5BAEKAAQALAAAAAAIAAgAAAMV 667 | GDBEA0qNJyGw7AmxmuaZhWEU5kEJADs= 668 | """, 669 | "img_closepressed": """\ 670 | R0lGODdhCAAIAIgAAPAAAP///ywAAAAACAAIAAACDkyAeJYM7FR8Ex7aVpIFADs= 671 | """, 672 | "img_run_temp": """\ 673 | R0lGODlhGAAYAHAAACH5BAEAAPwALAAAAAAYABgAhwAAAAAAMwAAZgAAmQAAzAAA/wArAAArMwAr 674 | ZgArmQArzAAr/wBVAABVMwBVZgBVmQBVzABV/wCAAACAMwCAZgCAmQCAzACA/wCqAACqMwCqZgCq 675 | mQCqzACq/wDVAADVMwDVZgDVmQDVzADV/wD/AAD/MwD/ZgD/mQD/zAD//zMAADMAMzMAZjMAmTMA 676 | zDMA/zMrADMrMzMrZjMrmTMrzDMr/zNVADNVMzNVZjNVmTNVzDNV/zOAADOAMzOAZjOAmTOAzDOA 677 | /zOqADOqMzOqZjOqmTOqzDOq/zPVADPVMzPVZjPVmTPVzDPV/zP/ADP/MzP/ZjP/mTP/zDP//2YA 678 | AGYAM2YAZmYAmWYAzGYA/2YrAGYrM2YrZmYrmWYrzGYr/2ZVAGZVM2ZVZmZVmWZVzGZV/2aAAGaA 679 | M2aAZmaAmWaAzGaA/2aqAGaqM2aqZmaqmWaqzGaq/2bVAGbVM2bVZmbVmWbVzGbV/2b/AGb/M2b/ 680 | Zmb/mWb/zGb//5kAAJkAM5kAZpkAmZkAzJkA/5krAJkrM5krZpkrmZkrzJkr/5lVAJlVM5lVZplV 681 | mZlVzJlV/5mAAJmAM5mAZpmAmZmAzJmA/5mqAJmqM5mqZpmqmZmqzJmq/5nVAJnVM5nVZpnVmZnV 682 | zJnV/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswAmcwAzMwA/8wrAMwrM8wrZswrmcwrzMwr 683 | /8xVAMxVM8xVZsxVmcxVzMxV/8yAAMyAM8yAZsyAmcyAzMyA/8yqAMyqM8yqZsyqmcyqzMyq/8zV 684 | AMzVM8zVZszVmczVzMzV/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A//8rAP8r 685 | M/8rZv8rmf8rzP8r//9VAP9VM/9VZv9Vmf9VzP9V//+AAP+AM/+AZv+Amf+AzP+A//+qAP+qM/+q 686 | Zv+qmf+qzP+q///VAP/VM//VZv/Vmf/VzP/V////AP//M///Zv//mf//zP///wAAAAAAAAAAAAAA 687 | AAiFAPcJHEiwoMGDBgEo3Lew4UKEAwEIlEiRoUWIEzNmVCgR48WOFkF6HEmypMmTAqWpXIky5Upp 688 | LV2qPDikps2bNV/CNFhzH06bLGfyHDIQ50qi+3YW7Ekwp0qmCKESfCp1KESbGKsuzZAVKU2tTb0S 689 | zHCz69KfZgX+xHqVKNm1YIvCxYkxIAA7 690 | """, 691 | "img_clean_temp": """\ 692 | R0lGODlhGAAYAHAAACwAAAAAGAAYAIcAAAAAADMAAGYAAJkAAMwAAP8AKwAAKzMAK2YAK5kAK8wA 693 | K/8AVQAAVTMAVWYAVZkAVcwAVf8AgAAAgDMAgGYAgJkAgMwAgP8AqgAAqjMAqmYAqpkAqswAqv8A 694 | 1QAA1TMA1WYA1ZkA1cwA1f8A/wAA/zMA/2YA/5kA/8wA//8zAAAzADMzAGYzAJkzAMwzAP8zKwAz 695 | KzMzK2YzK5kzK8wzK/8zVQAzVTMzVWYzVZkzVcwzVf8zgAAzgDMzgGYzgJkzgMwzgP8zqgAzqjMz 696 | qmYzqpkzqswzqv8z1QAz1TMz1WYz1Zkz1cwz1f8z/wAz/zMz/2Yz/5kz/8wz//9mAABmADNmAGZm 697 | AJlmAMxmAP9mKwBmKzNmK2ZmK5lmK8xmK/9mVQBmVTNmVWZmVZlmVcxmVf9mgABmgDNmgGZmgJlm 698 | gMxmgP9mqgBmqjNmqmZmqplmqsxmqv9m1QBm1TNm1WZm1Zlm1cxm1f9m/wBm/zNm/2Zm/5lm/8xm 699 | //+ZAACZADOZAGaZAJmZAMyZAP+ZKwCZKzOZK2aZK5mZK8yZK/+ZVQCZVTOZVWaZVZmZVcyZVf+Z 700 | gACZgDOZgGaZgJmZgMyZgP+ZqgCZqjOZqmaZqpmZqsyZqv+Z1QCZ1TOZ1WaZ1ZmZ1cyZ1f+Z/wCZ 701 | /zOZ/2aZ/5mZ/8yZ///MAADMADPMAGbMAJnMAMzMAP/MKwDMKzPMK2bMK5nMK8zMK//MVQDMVTPM 702 | VWbMVZnMVczMVf/MgADMgDPMgGbMgJnMgMzMgP/MqgDMqjPMqmbMqpnMqszMqv/M1QDM1TPM1WbM 703 | 1ZnM1czM1f/M/wDM/zPM/2bM/5nM/8zM////AAD/ADP/AGb/AJn/AMz/AP//KwD/KzP/K2b/K5n/ 704 | K8z/K///VQD/VTP/VWb/VZn/Vcz/Vf//gAD/gDP/gGb/gJn/gMz/gP//qgD/qjP/qmb/qpn/qsz/ 705 | qv//1QD/1TP/1Wb/1Zn/1cz/1f///wD//zP//2b//5n//8z///8AAAAAAAAAAAAAAAAIcwD3CRxI 706 | sKDBgwgTKlzIUKEfPw0T/qES8SAViBUL+vmTsSAVjh0H/sEYct/FkgJHojRJMqM9P1VaVrRHE2ZJ 707 | e/BqhsRJM2fHnD1puuQJ9GdQewIBKN23tOnSfTR5FgSQlKlVqlQXZs169anCrQOxrhyLMCAAOw== 708 | """, 709 | } 710 | return {k: PhotoImage(k, data=v) for k, v in icons.items()} 711 | 712 | def btn_chg_tab_ok(self, thetop, entries, actions): 713 | """chg a tab title""" 714 | widget, index = actions 715 | # build dico of result 716 | d = {f[0]: f[1]() for f in entries if not isinstance(f, (type("e"), type("e")))} 717 | 718 | title = d["new label"].strip() 719 | thetop.destroy() 720 | widget.tab(index, text=title) 721 | 722 | def btn_presstwice(self, event): 723 | """double-click on a tab definition to change label""" 724 | x, y, widget = event.x, event.y, event.widget 725 | elem = widget.identify(x, y) 726 | index = widget.index("@%d,%d" % (x, y)) 727 | titre = widget.tab(index, "text") 728 | # determine selected table 729 | actions = [widget, index] 730 | title = "Changing Tab label" 731 | fields = [ 732 | "", 733 | ["current label", (titre), "r", 30], 734 | "", 735 | ["new label", titre, "w", 30], 736 | ] 737 | create_dialog(title, fields, ("Ok", self.btn_chg_tab_ok), actions) 738 | 739 | def btn_press(self, event): 740 | """button press over a widget with a 'close' element""" 741 | x, y, widget = event.x, event.y, event.widget 742 | elem = widget.identify(x, y) # widget is the notebook 743 | if "close" in elem: # close button function 744 | index = widget.index("@%d,%d" % (x, y)) 745 | widget.state(["pressed"]) 746 | widget.pressed_index = index 747 | else: # move function 748 | index = widget.index("@%d,%d" % (x, y)) 749 | self.state_drag = True 750 | self.state_drag_widgetid = widget.tabs()[index] 751 | self.state_drag_index = index 752 | 753 | def btn_Movex(self, event): 754 | """make the tab follows if button is pressed and mouse moves""" 755 | x, y, widget = event.x, event.y, event.widget 756 | elem = widget.identify(x, y) 757 | index = widget.index("@%d,%d" % (x, y)) 758 | if self.state_drag: 759 | if self.state_drag_index != index: 760 | self.btn_Move(widget, self.state_drag_index, index) 761 | self.state_drag_index = index 762 | 763 | def btn_Move(self, notebook, old_index, new_index): 764 | """Move old_index tab to new_index position""" 765 | # stackoverflow.com/questions/11570786/tkinter-treeview-drag-and-drop 766 | if new_index != "": 767 | target_index = new_index 768 | if new_index >= len(notebook.tabs()) - 1: 769 | target_index = "end" 770 | titre = notebook.tab(old_index, "text") 771 | notebook.forget(old_index) 772 | notebook.insert(target_index, self.state_drag_widgetid, text=titre) 773 | notebook.select(new_index) 774 | 775 | def btn_release(self, event): 776 | """button release over a widget with a 'close' element""" 777 | x, y, widget = event.x, event.y, event.widget 778 | elem = widget.identify(x, y) 779 | index = self.state_drag_index 780 | if "close" in elem or "label" in elem: 781 | index = widget.index("@%d,%d" % (x, y)) 782 | if "close" in elem and widget.instate(["pressed"]): 783 | if widget.pressed_index == index: 784 | widget.forget(index) 785 | widget.event_generate("<>") 786 | if self.state_drag and elem.strip() != "": 787 | if self.state_drag_index != index: 788 | self.btn_Move(widget, self.state_drag_index, index) 789 | self.state_drag = False 790 | 791 | if not widget.instate(["pressed"]): 792 | return 793 | widget.state(["!pressed"]) 794 | widget.pressed_index = None 795 | 796 | def create_style(self): 797 | """create a Notebook style with close button""" 798 | # from https://github.com/python-git/python/blob/master/Demo/tkinter/ 799 | # ttk/notebook_closebtn.py 800 | # himself from http://paste.tclers.tk/896 801 | style = ttk.Style() 802 | 803 | style.element_create( 804 | "close", 805 | "image", 806 | "img_close", 807 | ("active", "pressed", "!disabled", "img_closepressed"), 808 | ("active", "!disabled", "img_closeactive"), 809 | border=6, 810 | sticky="", 811 | ) 812 | 813 | style.layout("ButtonNotebook", [("ButtonNotebook.client", {"sticky": "nswe"})]) 814 | style.layout( 815 | "ButtonNotebook.Tab", 816 | [ 817 | ( 818 | "ButtonNotebook.tab", 819 | { 820 | "sticky": "nswe", 821 | "children": [ 822 | ( 823 | "ButtonNotebook.padding", 824 | { 825 | "side": "top", 826 | "sticky": "nswe", 827 | "children": [ 828 | ( 829 | "ButtonNotebook.focus", 830 | { 831 | "side": "top", 832 | "sticky": "nswe", 833 | "children": [ 834 | ( 835 | "ButtonNotebook.label", 836 | {"side": "left", "sticky": ""}, 837 | ), 838 | ( 839 | "ButtonNotebook.close", 840 | {"side": "left", "sticky": ""}, 841 | ), 842 | ], 843 | }, 844 | ) 845 | ], 846 | }, 847 | ) 848 | ], 849 | }, 850 | ) 851 | ], 852 | ) 853 | 854 | self.tk_win.bind_class("TNotebook", "", self.btn_press, True) 855 | self.tk_win.bind_class("TNotebook", "", self.btn_release) 856 | self.tk_win.bind_class("TNotebook", "", self.btn_Movex) 857 | self.tk_win.bind_class("TNotebook", "", self.btn_presstwice) 858 | 859 | def createToolTip(self, widget, text): 860 | """create a tooptip box for a widget.""" 861 | # www.daniweb.com/software-development/python/code/234888/tooltip-box 862 | def enter(event): 863 | global tipwindow 864 | x = y = 0 865 | if tipwindow or not text: 866 | return 867 | x, y, cx, cy = widget.bbox("insert") 868 | x += widget.winfo_rootx() + 27 869 | y += widget.winfo_rooty() + 27 870 | # Creates a toplevel window 871 | tipwindow = tw = Toplevel(widget) 872 | # Leaves only the label and removes the app window 873 | tw.wm_overrideredirect(1) 874 | tw.wm_geometry("+%d+%d" % (x, y)) 875 | label = Label( 876 | tw, 877 | text=text, 878 | justify=LEFT, 879 | background="#ffffe0", 880 | relief=SOLID, 881 | borderwidth=1, 882 | ) 883 | label.pack(ipadx=1) 884 | 885 | def close(event): 886 | global tipwindow 887 | tw = tipwindow 888 | tipwindow = None 889 | if tw: 890 | tw.destroy() 891 | 892 | widget.bind("", enter) 893 | widget.bind("", close) 894 | 895 | def feed_dbtree(self, root_id, category, attached_db=""): 896 | """feed database treeview for category, return list of leaves names""" 897 | 898 | # prepare re-formatting functions for fields and database names 899 | def f(t): 900 | return ('"%s"' % t.replace('"', '""')) if t != "" else t 901 | 902 | def db(t): 903 | return ('"%s".' % t.replace('"', '""')) if t != "" else t 904 | 905 | attached = db(attached_db) 906 | 907 | # get Category list of [unique_name, name, definition, sub_category] 908 | tables = get_leaves(self.conn, category, attached_db) 909 | if len(tables) > 0: 910 | # level 1 : create the "category" node (as Category is not empty) 911 | root_txt = "%s(%s)" % (attached, category) 912 | idt = self.db_tree.insert( 913 | root_id, 914 | "end", 915 | root_txt, 916 | text="%s (%s)" % (category, len(tables)), 917 | values=("", ""), 918 | ) 919 | for t_id, t_name, definition, sub_cat in tables: 920 | # level 2 : print object creation, and '(Definition)' if fields 921 | sql3 = "" 922 | if sub_cat != "": 923 | # it's a table : prepare a Query with names of each column 924 | sub_c = get_leaves(self.conn, sub_cat, attached_db, t_name) 925 | colnames = [col[1] for col in sub_c] 926 | columns = [col[1] + " " + col[2] for col in sub_c] 927 | sql3 = ( 928 | 'select "' 929 | + '" , "'.join(colnames) 930 | + '" from ' 931 | + ("%s%s" % (attached, f(t_name))) 932 | ) 933 | idc = self.db_tree.insert( 934 | idt, 935 | "end", 936 | "%s%s" % (root_txt, t_id), 937 | text=t_name, 938 | tags=("run",), 939 | values=(definition, sql3), 940 | ) 941 | if sql3 != "": 942 | self.db_tree.insert( 943 | idc, 944 | "end", 945 | ("%s%s;d" % (root_txt, t_id)), 946 | text=["(Definition)"], 947 | tags=("run",), 948 | values=(definition, ""), 949 | ) 950 | # level 3 : Insert a line per column of the Table/View 951 | for c in range(len(sub_c)): 952 | self.db_tree.insert( 953 | idc, 954 | "end", 955 | "%s%s%s" % (root_txt, t_id, sub_c[c][0]), 956 | text=columns[c], 957 | tags=("run_up",), 958 | values=("", ""), 959 | ) 960 | return [i[1] for i in tables] 961 | 962 | def create_and_add_results(self, instructions, tab_tk_id, limit=-1, log=None): 963 | """execute instructions and add them to given tab results""" 964 | a_jouer = self.conn.get_sqlsplit(instructions, remove_comments=False) 965 | # must read :https://www.youtube.com/watch?v=09tM18_st4I#t=1751 966 | # stackoverflow.com/questions/15856976/transactions-with-python-sqlite3 967 | isolation = self.conn.conn.isolation_level 968 | counter = 0 969 | shell_list = ["", ""] 970 | if isolation == "": # Sqlite3 and dump.py default don't match 971 | self.conn.conn.isolation_level = None # right behavior 972 | cu = self.conn.conn.cursor() 973 | sql_error = False 974 | 975 | def beurk(r): 976 | """format data line log""" 977 | s = [ 978 | '"' + s.replace('"', '""') + '"' 979 | if isinstance(s, (type("e"), type("e"))) 980 | else str(s) 981 | for s in r 982 | ] 983 | return "(" + ",".join(s) + ")" 984 | 985 | def bip(c): 986 | """format instruction log header""" 987 | timing = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 988 | return "\n---------N°%s----------[" % counter + timing + "]\n\n" 989 | 990 | for instruction in a_jouer: 991 | if log is not None: # write to logFile 992 | counter += 1 993 | log.write(bip(counter)) 994 | log.write(instruction) 995 | log.write("\n") 996 | instru = next(self.conn.get_sqlsplit(instruction, remove_comments=True)) 997 | instru = instru.replace(";", ";").strip(" \t\n\r") 998 | first_line = (instru + "\n").splitlines()[0] 999 | if instru[:5] == "pydef": 1000 | pydef = self.conn.createpydef(instru) 1001 | titles = ("Creating embedded python function",) 1002 | rows = self.conn.conn_def[pydef]["pydef"].splitlines() 1003 | rows.append(self.conn.conn_def[pydef]["inst"]) 1004 | self.n.add_treeview(tab_tk_id, titles, rows, "Info", pydef) 1005 | if log is not None: # write to logFile 1006 | log.write("\n".join(['("%s")' % r for r in rows]) + "\n") 1007 | elif instru[:1] == ".": # a shell command ! 1008 | # handle a ".function" here ! 1009 | # import FILE TABLE 1010 | shell_list = shlex.split(instru, posix=False) # magic standard library 1011 | try: 1012 | if shell_list[0] == ".cd" and len(shell_list) >= 2: 1013 | db_file = shell_list[1] 1014 | db_file = db_file.strip("'") 1015 | db_file = db_file.strip('"') 1016 | if (db_file + "z")[0] == "~": 1017 | self.current_directory = os.path.join( 1018 | self.home, db_file[1:] 1019 | ) 1020 | elif (db_file + "z")[:2] == "..": 1021 | self.current_directory = os.path.join( 1022 | self.current_directory, db_file 1023 | ) 1024 | elif (db_file + "z")[:1] == ".": 1025 | self.current_directory = os.path.join( 1026 | self.current_directory, db_file[1:] 1027 | ) 1028 | else: 1029 | self.current_directory = db_file 1030 | os.chdir(self.current_directory) 1031 | self.current_directory = os.getcwd() 1032 | if shell_list[0] == ".headers" and len(shell_list) >= 2: 1033 | if shell_list[1].lower() == "off": 1034 | self.default_header = False 1035 | elif shell_list[1].lower() == "on": 1036 | self.default_header = True 1037 | if shell_list[0] == ".separator" and len(shell_list) >= 2: 1038 | self.default_separator = shell_list[1] 1039 | if shell_list[0] in (".once", ".output", ".excel"): 1040 | if shell_list[0] == ".excel": 1041 | shell_list = [".once", "--bom", "-x"] 1042 | if shell_list[0] == ".once": 1043 | self.once_mode, self.init_output = True, True 1044 | else: 1045 | self.output_mode, self.init_output = True, True 1046 | self.x_mode = False 1047 | self.encode_in = "utf-8" 1048 | if "--bom" in shell_list: # keep access to the option 1049 | self.encode_in = "utf-8-sig" 1050 | if shell_list[1] == "--bom": 1051 | self.output_file = shell_list[2] 1052 | else: 1053 | self.output_file = shell_list[1] 1054 | self.output_file = self.output_file.strip("'") 1055 | self.output_file = self.output_file.strip('"') 1056 | if (self.output_file + "") == "-x": 1057 | self.x_mode = True 1058 | with tmpf.TemporaryFile() as ff: 1059 | ff = tmpf.NamedTemporaryFile( 1060 | delete=False, suffix="_sqlite_bro.csv" 1061 | ) 1062 | self.output_file = ff.name 1063 | if (self.output_file + "z")[0] == "~": 1064 | self.output_file = os.path.join( 1065 | self.home, self.output_file[1:] 1066 | ) 1067 | if self.output_file is None or self.output_file == "": 1068 | self.output_mode, self.init_output = False, False 1069 | if shell_list[0] == ".print": 1070 | if self.output_mode or self.once_mode: 1071 | write_mode = ( 1072 | "w" if self.init_output else "a" 1073 | ) # Write or Append 1074 | with io.open( 1075 | self.output_file, write_mode, encoding=self.encode_in 1076 | ) as fout: 1077 | fout.writelines(instru[len(".print") + 1 :] + "\n") 1078 | self.init_output, self.once_mode = False, False 1079 | if shell_list[0] == ".import" and len(shell_list) >= 2: 1080 | csv_file = shell_list[1] 1081 | csv_file = csv_file.strip("'") 1082 | csv_file = csv_file.strip('"') 1083 | if (csv_file + "z")[0] == "~": 1084 | csv_file = os.path.join(self.home, csv_file[1:]) 1085 | guess = guess_csv(csv_file) 1086 | if len(shell_list) >= 3: 1087 | guess.table_name = shell_list[2] 1088 | # Create csv reader and give it to import 1089 | reading = read_this_csv( 1090 | csv_file, 1091 | guess.encodings[0], 1092 | guess.default_sep, 1093 | guess.default_quote, 1094 | guess.has_header, 1095 | guess.default_decims[0], 1096 | ) 1097 | guess_sql = guess_sql_creation( 1098 | guess.table_name, 1099 | guess.default_sep, 1100 | ".", 1101 | guess.has_header, 1102 | guess.dlines, 1103 | guess.default_quote, 1104 | )[0] 1105 | self.conn.insert_reader( 1106 | reading, 1107 | guess.table_name, 1108 | guess_sql, 1109 | create_table=False, 1110 | replace=False, 1111 | ) 1112 | self.n.add_treeview( 1113 | tab_tk_id, 1114 | ("table", "file"), 1115 | ((guess.table_name, csv_file),), 1116 | "Info", 1117 | first_line, 1118 | ) 1119 | if log is not None: # write to logFile 1120 | log.write( 1121 | '-- File %s imported in "%s"\n' 1122 | % (csv_file, guess.table_name) 1123 | ) 1124 | if shell_list[0] == ".dump": 1125 | if len(shell_list) >= 2: 1126 | csv_file = shell_list[1] 1127 | csv_file = csv_file.strip("'") 1128 | csv_file = csv_file.strip('"') 1129 | if (csv_file + "z")[0] == "~": 1130 | csv_file = os.path.join(self.home, csv_file[1:]) 1131 | with io.open(csv_file, "w", encoding="utf-8") as f: 1132 | for line in self.conn.iterdump(): 1133 | f.write("%s\n" % line) 1134 | else: 1135 | self.n.add_treeview( 1136 | tab_tk_id, 1137 | ("output"), 1138 | ([("%s" % line) for line in self.conn.iterdump()]), 1139 | "Dump", 1140 | ".dump", 1141 | ) 1142 | if shell_list[0] == ".read" and len(shell_list) >= 2: 1143 | filename = shell_list[1] 1144 | filename = filename.strip("'") 1145 | filename = filename.strip('"') 1146 | if (filename + "z")[0] == "~": 1147 | filename = os.path.join(self.home, filename[1:]) 1148 | with io.open( 1149 | filename, encoding=guess_encoding(filename)[0] 1150 | ) as f: 1151 | read_this = f.read() 1152 | self.n.new_query_tab(".Read", read_this) 1153 | self.run_tab() 1154 | self.actualize_db() 1155 | if shell_list[0] == ".open": 1156 | self.close_db 1157 | if len(shell_list) >= 2: 1158 | filename = shell_list[1] 1159 | if (filename + "z")[0] == "~": 1160 | filename = os.path.join(self.home, filename[1:]) 1161 | self.open_db(filename) 1162 | else: 1163 | self.new_db(":memory:") 1164 | self.actualize_db() 1165 | if shell_list[0] == ".restore" and len(shell_list) >= 2: 1166 | filename = shell_list[1] 1167 | if (filename + "z")[0] == "~": 1168 | filename = os.path.join(self.home, filename[1:]) 1169 | db_from = sqlite.connect(filename) 1170 | db_from.backup(self.conn.conn) 1171 | db_from.close 1172 | self.actualize_db() 1173 | if shell_list[0] == ".backup" and len(shell_list) >= 2: 1174 | filename = shell_list[1] 1175 | if (filename + "z")[0] == "~": 1176 | filename = os.path.join(self.home, filename[1:]) 1177 | db_to = sqlite.connect(filename) 1178 | self.conn.conn.backup(db_to) 1179 | db_to.close() 1180 | if shell_list[0] == ".shell" and len(shell_list) >= 2: 1181 | os.system(instru[len(".print") + 1 :] + "\n") 1182 | 1183 | except IOError as err: 1184 | msg = "I/O error: {0}".format(err) 1185 | self.n.add_treeview( 1186 | tab_tk_id, ("Error !",), [(msg,)], "Error !", instru 1187 | ) 1188 | if not self.use_gui: 1189 | print("Error !", [msg]) 1190 | if log is not None: # write to logFile 1191 | log.write("Error ! %s : %s" % (msg, instru)) 1192 | sql_error = True 1193 | break 1194 | elif len("".join(instruction.split())) > 1: # PyPy answer 42 to blanks sql 1195 | nb_columns = -1 1196 | try: 1197 | if self.output_mode or self.once_mode: 1198 | nb_columns = self.conn.export_writer( 1199 | instruction, 1200 | self.output_file, 1201 | header=self.default_header, 1202 | delimiter=self.default_separator, 1203 | encoding=self.encode_in, 1204 | initialize=self.init_output, 1205 | ) 1206 | if nb_columns > 0: 1207 | self.once_mode, self.init_output = False, False 1208 | if nb_columns > 0: 1209 | self.n.add_treeview( 1210 | tab_tk_id, 1211 | ("qry_to_csv", "file"), 1212 | ((instruction, self.output_file),), 1213 | "Qry", 1214 | # ".once %s" % self.output_file, 1215 | first_line, 1216 | ) 1217 | if self.x_mode and nb_columns > 0: 1218 | os.system( 1219 | "start excel.exe " + self.output_file.replace("\\", "/") 1220 | ) 1221 | with tmpf.TemporaryFile() as ff: 1222 | ff = tmpf.NamedTemporaryFile( 1223 | delete=False, suffix="_sqlite_bro.csv" 1224 | ) 1225 | self.output_file = ff.name 1226 | else: 1227 | cur = cu.execute(instruction) 1228 | rows = cur.fetchall() 1229 | # a query may have no result( like for an "update") 1230 | if ( 1231 | cur.description is not None and len(cur.description) > 0 1232 | ): # pypy needs this test 1233 | titles = [row_info[0] for row_info in cur.description] 1234 | self.n.add_treeview( 1235 | tab_tk_id, titles, rows, "Qry", first_line 1236 | ) 1237 | if log is not None: # write to logFile 1238 | log.write(beurk(titles) + "\n") 1239 | log.write( 1240 | "\n".join([beurk(l) for l in rows[:limit]]) + "\n" 1241 | ) 1242 | if len(rows) > limit: 1243 | log.write("%s more..." % len((rows) - limit)) 1244 | except sqlite.Error as msg: # OperationalError 1245 | self.n.add_treeview( 1246 | tab_tk_id, ("Error !",), [(msg,)], "Error !", first_line 1247 | ) 1248 | if log is not None: # write to logFile 1249 | log.write("Error ! %s" % msg) 1250 | sql_error = True 1251 | break 1252 | 1253 | if self.conn.conn.isolation_level != isolation: 1254 | # if we're in 'backward' compatible mode (automatic commit) 1255 | try: 1256 | if self.conn.conn.in_transaction: # python 3.2 1257 | if not sql_error: 1258 | cu.execute("COMMIT;") 1259 | if log is not None: # write to logFile 1260 | log.write("\n-------COMMIT;--------\n" % counter) 1261 | else: 1262 | cu.execute("ROLLBACK;") 1263 | except: 1264 | if not sql_error: 1265 | try: 1266 | cu.execute("COMMIT;") 1267 | if log is not None: # write to logFile 1268 | log.write("\n-------COMMIT;--------\n" % counter) 1269 | except: 1270 | pass 1271 | else: 1272 | try: 1273 | cu.execute("ROLLBACK;") 1274 | except: 1275 | pass 1276 | self.conn.conn.isolation_level = isolation # restore standard 1277 | 1278 | def import_csvtb(self): 1279 | """import csv dialog (with guessing of encoding and separator)""" 1280 | csv_file = filedialog.askopenfilename( 1281 | initialdir=self.initialdir, 1282 | defaultextension=".db", 1283 | title="Choose a csv fileto import ", 1284 | filetypes=[("default", "*.csv"), ("other", "*.txt"), ("all", "*.*")], 1285 | ) 1286 | if csv_file != "": 1287 | self.set_initialdir(csv_file) 1288 | # guess all via an object 1289 | guess = guess_csv(csv_file) 1290 | guess_sql = guess_sql_creation( 1291 | guess.table_name, 1292 | guess.default_sep, 1293 | ".", 1294 | guess.has_header, 1295 | guess.dlines, 1296 | guess.default_quote, 1297 | )[2] 1298 | # check it via dialog box 1299 | fields_in = [ 1300 | "", 1301 | ["csv Name", csv_file, "r", 100], 1302 | "", 1303 | ["table Name", guess.table_name], 1304 | ["column separator", guess.default_sep, "w", 20], 1305 | ["string delimiter", guess.default_quote, "w", 20], 1306 | "", 1307 | ["Decimal separator", guess.default_decims], 1308 | ["Encoding", guess.encodings], 1309 | "Fliflaps", 1310 | ["Header line", guess.has_header], 1311 | ["Create table", True], 1312 | ["Replace existing data", True], 1313 | "", 1314 | ["first 3 lines", guess.dlines, "r", 100, 10], 1315 | "", 1316 | ["use manual creation request", False], 1317 | "", 1318 | ["creation request", guess_sql, "w", 100, 10], 1319 | ] 1320 | actions = [self.conn, self.actualize_db] 1321 | create_dialog( 1322 | ("Importing %s" % csv_file), 1323 | fields_in, 1324 | ("Import", import_csvtb_ok), 1325 | actions, 1326 | ) 1327 | 1328 | def export_csvtb(self): 1329 | """get selected table definition and launch cvs export dialog""" 1330 | # determine selected table 1331 | actions = [self.conn, self.db_tree] 1332 | selitem = self.db_tree.focus() # get tree item having the focus 1333 | if selitem != "": 1334 | seltag = self.db_tree.item(selitem, "tag")[0] 1335 | if seltag == "run_up": # if 'run-up', do as dbl-click 1 level up 1336 | selitem = self.db_tree.parent(selitem) 1337 | # get final information 1338 | definition, query = self.db_tree.item(selitem, "values") 1339 | if query != "": # run the export_csv dialog 1340 | title = 'Export Table "%s" to ?' % self.db_tree.item(selitem, "text") 1341 | self.export_csv_dialog(query, title, actions) 1342 | 1343 | def export_csvqr(self): 1344 | """get tab selected definition and launch cvs export dialog""" 1345 | actions = [self.conn, self.n] 1346 | active_tab_id = self.n.notebook.select() 1347 | if active_tab_id != "": # get current selection (or all) 1348 | fw = self.n.fw_labels[active_tab_id] 1349 | try: 1350 | query = fw.get("sel.first", "sel.last") 1351 | except: 1352 | query = fw.get(1.0, END)[:-1] 1353 | if query != "": 1354 | self.export_csv_dialog(query, "Export Query", actions) 1355 | 1356 | def export_csv_dialog(self, query="--", text="undefined.csv", actions=[]): 1357 | """export csv dialog""" 1358 | # proposed encoding (we favorize utf-8 or utf-8-sig) 1359 | encodings = ["utf-8", locale.getdefaultlocale()[1], "utf-16", "utf-8-sig"] 1360 | if os.name == "nt": 1361 | encodings = ["utf-8-sig", locale.getdefaultlocale()[1], "utf-16", "utf-8"] 1362 | # proposed csv separator 1363 | default_sep = [",", "|", ";"] 1364 | csv_file = filedialog.asksaveasfilename( 1365 | initialdir=self.initialdir, 1366 | defaultextension=".db", 1367 | title=text, 1368 | filetypes=[("default", "*.csv"), ("other", "*.txt"), ("all", "*.*")], 1369 | ) 1370 | if csv_file != "": 1371 | # Idea from (http://www.python-course.eu/tkinter_entry_widgets.php) 1372 | fields = [ 1373 | "", 1374 | ["csv Name", csv_file, "r", 100], 1375 | "", 1376 | ["column separator", default_sep], 1377 | ["Header line", True], 1378 | ["Encoding", encodings], 1379 | "", 1380 | ["Data to export (MUST be 1 Request)", (query), "w", 100, 10], 1381 | ] 1382 | 1383 | create_dialog( 1384 | ("Export to %s" % csv_file), fields, ("Export", export_csv_ok), actions 1385 | ) 1386 | 1387 | 1388 | class NotebookForQueries: 1389 | """Create a Notebook with a list in the First frame 1390 | and query results in following treeview frames""" 1391 | 1392 | def __init__(self, tk_win, root, queries, use_gui): 1393 | self.use_gui = use_gui 1394 | self.nongui_tabs = [ 1395 | "", 1396 | ] 1397 | if self.use_gui: 1398 | self.tk_win = tk_win 1399 | self.root = root 1400 | self.notebook = Notebook(root, style="ButtonNotebook") # ttk. 1401 | 1402 | self.fw_labels = {} # tab_tk_id -> Scripting frame python object 1403 | self.fw_result_nbs = {} # tab_tk_id -> Notebook of Results 1404 | 1405 | # resize rules 1406 | root.columnconfigure(0, weight=1) 1407 | root.rowconfigure(0, weight=1) 1408 | # grid widgets 1409 | self.notebook.grid(row=0, column=0, sticky=(N, W, S, E)) 1410 | 1411 | def new_query_tab(self, title, query): 1412 | """add a Tab 'title' to the notebook, containing the Script 'query'""" 1413 | 1414 | if not self.use_gui: 1415 | self.nongui_tabs += [query] 1416 | return len(self.nongui_tabs) - 1 1417 | 1418 | fw_welcome = ttk.Panedwindow(self.tk_win, orient=VERTICAL) # tk_win 1419 | fw_welcome.pack(fill="both", expand=True) 1420 | self.notebook.add(fw_welcome, text=(title)) 1421 | 1422 | # new "editable" script 1423 | f1 = ttk.Labelframe(fw_welcome, text="Script", width=200, height=100) 1424 | fw_welcome.add(f1) 1425 | fw_label = Text(f1, bd=1, undo=True) 1426 | 1427 | scroll = ttk.Scrollbar(f1, command=fw_label.yview) 1428 | fw_label.configure(yscrollcommand=scroll.set) 1429 | fw_label.insert(END, (query)) 1430 | fw_label.pack(side=LEFT, expand=YES, fill=BOTH, padx=2, pady=2) 1431 | scroll.pack(side=RIGHT, expand=NO, fill=BOTH, padx=2, pady=2) 1432 | 1433 | # keep tab reference by tk id 1434 | working_tab_id = "." + fw_welcome._name 1435 | 1436 | # keep tab reference to script (by tk id) 1437 | self.fw_labels[working_tab_id] = fw_label 1438 | 1439 | # new "Results" Container 1440 | fr = ttk.Labelframe(fw_welcome, text="Results", width=200, height=100) 1441 | fw_welcome.add(fr) 1442 | 1443 | # containing a notebook 1444 | fw_result_nb = Notebook(fr, style="ButtonNotebook") 1445 | fw_result_nb.pack(fill="both", expand=True) 1446 | # resize rules 1447 | fw_welcome.columnconfigure(0, weight=1) 1448 | # keep reference to result_nb objects (by tk id) 1449 | self.fw_result_nbs[working_tab_id] = fw_result_nb 1450 | 1451 | # activate this tab print(self.notebook.tabs()) 1452 | self.notebook.select(working_tab_id) 1453 | # workaround to have a visible result pane on initial launch 1454 | self.add_treeview(working_tab_id, "_", "", "click on ('->') to run Script") 1455 | return working_tab_id # gives back tk_id reference of the new tab 1456 | 1457 | def del_tab(self): 1458 | """delete active notebook tab's results""" 1459 | given_tk_id = self.notebook.select() 1460 | if given_tk_id != "": 1461 | self.notebook.forget(given_tk_id) 1462 | 1463 | def remove_treeviews(self, given_tk_id): 1464 | """remove results from given tab tk_id""" 1465 | if given_tk_id != "": 1466 | myz = self.fw_result_nbs[given_tk_id] 1467 | for xx in list(myz.children.values()): 1468 | xx.grid_forget() 1469 | xx.destroy() 1470 | 1471 | def add_treeview(self, given_tk_id, columns, data, title="__", subt=""): 1472 | """add a dataset result to the given tab tk_id""" 1473 | if not self.use_gui: 1474 | return 1475 | # ensure we work on lists 1476 | if isinstance(columns, (type("e"), type("e"))): 1477 | tree_columns = [columns] 1478 | else: 1479 | tree_columns = columns 1480 | lines = [data] if isinstance(data, (type("e"), type("e"))) else data 1481 | 1482 | # get back reference to Notebooks of Results 1483 | # (see http://www.astro.washington.edu/users/rowen/TkinterSummary.html) 1484 | fw_result_nb = self.fw_result_nbs[given_tk_id] 1485 | 1486 | # create a Labelframe to contain new resultset and scrollbars 1487 | f2 = ttk.Labelframe( 1488 | fw_result_nb, 1489 | text=("(%s lines) %s" % (len(lines), subt)), 1490 | width=200, 1491 | height=100, 1492 | ) 1493 | f2.pack(fill="both", expand=True) 1494 | fw_result_nb.add(f2, text=title) 1495 | 1496 | # ttk.Style().configure('TLabelframe.label', font=("Arial",14, "bold")) 1497 | # lines=queries 1498 | fw_Box = Treeview( 1499 | f2, columns=tree_columns, show="headings", padding=(2, 2, 2, 2) 1500 | ) 1501 | fw_vsb = Scrollbar(f2, orient="vertical", command=fw_Box.yview) 1502 | fw_hsb = Scrollbar(f2, orient="horizontal", command=fw_Box.xview) 1503 | fw_Box.configure(yscrollcommand=fw_vsb.set, xscrollcommand=fw_hsb.set) 1504 | fw_Box.grid(column=0, row=0, sticky="nsew", in_=f2) 1505 | fw_vsb.grid(column=1, row=0, sticky="ns", in_=f2) 1506 | fw_hsb.grid(column=0, row=2, sticky="ew", in_=f2) 1507 | 1508 | # this new Treeview may occupy all variable space 1509 | f2.grid_columnconfigure(0, weight=1) 1510 | f2.grid_rowconfigure(0, weight=1) 1511 | 1512 | # feed Treeview Header 1513 | for col in tuple(tree_columns): 1514 | fw_Box.heading( 1515 | col, text=col.title(), command=lambda c=col: self.sortby(fw_Box, c, 0) 1516 | ) 1517 | fw_Box.column(col, width=font.Font().measure(col.title())) 1518 | 1519 | def flat(x): 1520 | """replace line_return by space, if given a string""" 1521 | if isinstance(x, (type("e"), type("e"))): 1522 | return x.replace("\n", " ") 1523 | return x 1524 | 1525 | # feed Treeview Lines 1526 | for items in lines: 1527 | # if line is a string, redo a tuple 1528 | item = (items,) if isinstance(items, (type("e"), type("e"))) else items 1529 | 1530 | # replace line_return by space (grid don't like line_returns) 1531 | line_cells = tuple(flat(item[c]) for c in range(len(tree_columns))) 1532 | # insert the line of data 1533 | fw_Box.insert("", "end", values=line_cells) 1534 | # adjust columns length if necessary and possible 1535 | for indx, val in enumerate(line_cells): 1536 | try: 1537 | ilen = font.Font().measure(val) 1538 | if ( 1539 | fw_Box.column(tree_columns[indx], width=None) < ilen 1540 | and ilen < 400 1541 | ): 1542 | fw_Box.column(tree_columns[indx], width=ilen) 1543 | except: 1544 | pass 1545 | 1546 | def sortby(self, tree, col, descending): 1547 | """Sort a ttk treeview contents when a column is clicked on.""" 1548 | # grab values to sort 1549 | data = [(tree.set(child, col), child) for child in tree.get_children()] 1550 | 1551 | # reorder data 1552 | data.sort(reverse=descending) 1553 | for indx, item in enumerate(data): 1554 | tree.move(item[1], "", indx) 1555 | 1556 | # switch the heading so that it will sort in the opposite direction 1557 | tree.heading( 1558 | col, command=lambda col=col: self.sortby(tree, col, int(not descending)) 1559 | ) 1560 | 1561 | 1562 | class guess_csv: 1563 | """guess everything about a csv file of data to import in SQL""" 1564 | 1565 | def __init__(self, csv_file): 1566 | self.has_header = True 1567 | self.default_sep = "," 1568 | self.default_quote = '"' 1569 | self.encodings = guess_encoding(csv_file) 1570 | self.table_name = os.path.basename(csv_file).split(".")[0] 1571 | with io.open(csv_file, encoding=self.encodings[0]) as f: 1572 | self.preview = f.read(9999) 1573 | try: 1574 | dialect = csv.Sniffer().sniff(self.preview) 1575 | self.has_header = csv.Sniffer().has_header(self.preview) 1576 | self.default_sep = dialect.delimiter 1577 | self.default_quote = Dialect.quotechar 1578 | except: 1579 | pass # sniffer can fail 1580 | self.default_decims = [".", ","] 1581 | if self.default_sep == ";": 1582 | self.default_decims = [",", "."] 1583 | self.dlines = "\n\n".join(self.preview.splitlines()[:3]) 1584 | 1585 | 1586 | def guess_sql_creation(table_name, separ, decim, header, data, quoter='"'): 1587 | """guess the sql creation request for the table who will receive data""" 1588 | try: 1589 | dlines = list( 1590 | csv.reader( 1591 | data.replace("\n\n", "\n").splitlines(), 1592 | delimiter=separ, 1593 | quotechar=quoter, 1594 | ) 1595 | ) 1596 | except: # minimal hack for python2.7 1597 | dlines = list( 1598 | csv.reader( 1599 | data.replace("\n\n", "\n").splitlines(), 1600 | delimiter=str(separ), 1601 | quotechar=str(quoter), 1602 | ) 1603 | ) 1604 | r, val = list(dlines[0]), list(dlines[1]) 1605 | typ = ["TEXT"] * len(r) # default value is TEXT 1606 | for i in range(len(r)): 1607 | try: 1608 | float(val[i].replace(decim, ".")) # unless it can be a real 1609 | typ[i] = "REAL" 1610 | except: 1611 | checker = sqlite.connect(":memory:") 1612 | # avoid the false positive 'now' 1613 | val_not_now = val[i].replace("w", "www").replace("W", "WWW") 1614 | test = "select datetime('{0}')".format(val_not_now) 1615 | try: 1616 | if checker.execute(test).fetchall()[0][0]: 1617 | typ[i] = "DATETIME" # and unless SQLite can see a DATETIME 1618 | except: 1619 | pass 1620 | checker.close 1621 | if header: 1622 | # de-duplicate column names, if needed by pastixing with '_'+number 1623 | for i in range(len(r)): 1624 | 1625 | if r[i] == "": # 2022-02-04a replace empty column title per usual default 1626 | r[i] = "c_" + ("000" + str(i))[-3:] 1627 | 1628 | if r[i] in r[:i]: 1629 | j = 1 1630 | while r[i] + "_" + str(j) in r[:i] + r[i + 1 :]: 1631 | j += 1 1632 | r[i] += "_" + str(j) 1633 | head = ",\n".join([('"%s" %s' % (r[i], typ[i])) for i in range(len(r))]) 1634 | sql_crea = 'CREATE TABLE "%s" (%s);' % (table_name, head) 1635 | else: 1636 | head = ",".join(["c_" + ("000" + str(i))[-3:] for i in range(len(r))]) 1637 | sql_crea = 'CREATE TABLE "%s" (%s);' % (table_name, head) 1638 | return sql_crea, typ, head 1639 | 1640 | 1641 | def guess_encoding(csv_file): 1642 | """guess the encoding of the given file""" 1643 | with io.open(csv_file, "rb") as f: 1644 | data = f.read(5) 1645 | if data.startswith(b"\xEF\xBB\xBF"): # UTF-8 with a "BOM" 1646 | return ["utf-8-sig"] 1647 | elif data.startswith(b"\xFF\xFE") or data.startswith(b"\xFE\xFF"): 1648 | return ["utf-16"] 1649 | else: # in Windows, guessing utf-8 doesn't work, so we have to try 1650 | try: 1651 | with io.open(csv_file, encoding="utf-8") as f: 1652 | preview = f.read(222222) 1653 | return ["utf-8"] 1654 | except: 1655 | return [locale.getdefaultlocale()[1], "utf-8"] 1656 | 1657 | 1658 | def create_dialog(title, fields_in, buttons, actions): 1659 | """create a formular with title, fields, button, data""" 1660 | # drawing the request form 1661 | top = Toplevel() 1662 | top.title(title) 1663 | top.columnconfigure(0, weight=1) 1664 | top.rowconfigure(0, weight=1) 1665 | # drawing global frame 1666 | content = ttk.Frame(top) 1667 | content.grid(column=0, row=0, sticky=(N, S, E, W)) 1668 | content.columnconfigure(0, weight=1) 1669 | # fields = Horizontal FrameLabel, or 1670 | # label, default_value, 'r' or 'w' default_width,default_height 1671 | fields = fields_in 1672 | mf_col = -1 1673 | for f in range(len(fields)): # same structure out 1674 | field = fields[f] 1675 | if isinstance(field, (type("e"), type("e"))) or mf_col == -1: 1676 | # a new horizontal frame 1677 | mf_col += 1 1678 | ta_col = -1 1679 | if isinstance(field, (type("e"), type("e"))) and field == "": 1680 | mf_frame = ttk.Frame(content, borderwidth=1) 1681 | else: 1682 | mf_frame = ttk.LabelFrame(content, borderwidth=1, text=field) 1683 | mf_frame.grid(column=0, row=mf_col, sticky="nsew") 1684 | Grid.rowconfigure(mf_frame, 0, weight=1) 1685 | content.rowconfigure(mf_col, weight=1) 1686 | if not isinstance(field, (type("e"), type("e"))): 1687 | # a new vertical frame 1688 | ta_col += 1 1689 | Grid.columnconfigure(mf_frame, ta_col, weight=1) 1690 | packing_frame = ttk.Frame(mf_frame, borderwidth=1) 1691 | packing_frame.grid(column=ta_col, row=0, sticky="nsew") 1692 | Grid.columnconfigure(packing_frame, 0, weight=1) 1693 | # prepare width and height and writable status 1694 | width = field[3] if len(field) > 3 else 30 1695 | height = field[4] if len(field) > 4 else 30 1696 | status = "normal" 1697 | if len(field) >= 3 and field[2] == "r": 1698 | status = "disabled" 1699 | # switch between object types 1700 | if len(field) > 4: 1701 | # datas 1702 | d_frame = ttk.LabelFrame( 1703 | packing_frame, 1704 | borderwidth=5, 1705 | width=width, 1706 | height=height, 1707 | text=field[0], 1708 | ) 1709 | d_frame.grid(column=0, row=0, sticky="nsew", pady=1, padx=1) 1710 | Grid.rowconfigure(packing_frame, 0, weight=1) 1711 | fw_label = Text(d_frame, bd=1, width=width, height=height, undo=True) 1712 | fw_label.pack(side=LEFT, expand=YES, fill=BOTH) 1713 | scroll = ttk.Scrollbar(d_frame, command=fw_label.yview) 1714 | scroll.pack(side=RIGHT, expand=NO, fill=Y) 1715 | fw_label.configure(yscrollcommand=scroll.set) 1716 | fw_label.insert(END, ("%s" % field[1])) 1717 | fw_label.configure(state=status) 1718 | Grid.rowconfigure(d_frame, 0, weight=1) 1719 | Grid.columnconfigure(d_frame, 0, weight=1) 1720 | # Data Text Extractor in the fields list () 1721 | # see stackoverflow.com/questions/17677649 (loop and lambda) 1722 | fields[f][1] = lambda x=fw_label: x.get("1.0", "end") 1723 | elif isinstance(field[1], type(True)): 1724 | # boolean Field 1725 | name_var = BooleanVar() 1726 | name = ttk.Checkbutton( 1727 | packing_frame, 1728 | text=field[0], 1729 | variable=name_var, 1730 | onvalue=True, 1731 | state=status, 1732 | ) 1733 | name_var.set(field[1]) 1734 | name.grid(column=0, row=0, sticky="nsew", pady=5, padx=5) 1735 | fields[f][1] = name_var.get 1736 | else: # Text or Combo 1737 | namelbl = ttk.Label(packing_frame, text=field[0]) 1738 | namelbl.grid(column=0, row=0, sticky="nsw", pady=5, padx=5) 1739 | name_var = StringVar() 1740 | if not isinstance(field[1], (list, tuple)): 1741 | name = ttk.Entry( 1742 | packing_frame, textvariable=name_var, width=width, state=status 1743 | ) 1744 | name_var.set(field[1]) 1745 | else: 1746 | name = ttk.Combobox( 1747 | packing_frame, textvariable=name_var, state=status 1748 | ) 1749 | name["values"] = list(field[1]) 1750 | name.current(0) 1751 | name.grid(column=1, row=0, sticky="nsw", pady=0, padx=10) 1752 | fields[f][1] = name_var.get 1753 | # adding button below the same way 1754 | mf_col += 1 1755 | packing_frame = ttk.LabelFrame(content, borderwidth=5) 1756 | packing_frame.grid(column=0, row=mf_col, sticky="nsew") 1757 | okbutton = ttk.Button( 1758 | packing_frame, 1759 | text=buttons[0], 1760 | command=lambda a=top, b=fields, c=actions: (buttons[1])(a, b, c), 1761 | ) 1762 | cancelbutton = ttk.Button(packing_frame, text="Cancel", command=top.destroy) 1763 | okbutton.grid(column=0, row=mf_col) 1764 | cancelbutton.grid(column=1, row=mf_col) 1765 | for x in range(3): 1766 | Grid.columnconfigure(packing_frame, x, weight=1) 1767 | top.grab_set() 1768 | 1769 | 1770 | def import_csvtb_ok(thetop, entries, actions): 1771 | """read input values from tk formular""" 1772 | conn, actualize_db = actions 1773 | # build dico of result 1774 | d = {f[0]: f[1]() for f in entries if not isinstance(f, (type("e"), type("e")))} 1775 | # affect to variables 1776 | csv_file = d["csv Name"].strip() 1777 | table_name = d["table Name"].strip() 1778 | separ = d["column separator"] 1779 | decim = d["Decimal separator"] 1780 | quotechar = d["string delimiter"] 1781 | # action 1782 | if csv_file != "(none)" and len(csv_file) * len(table_name) * len(separ) > 1: 1783 | thetop.destroy() 1784 | # do initialization job 1785 | sql, typ, head = guess_sql_creation( 1786 | table_name, separ, decim, d["Header line"], d["first 3 lines"], quotechar 1787 | ) 1788 | if d["use manual creation request"]: 1789 | sql = 'CREATE TABLE "%s" (%s);' % (table_name, d["creation request"]) 1790 | 1791 | # Create csv reader function and give it to insert 1792 | reading = read_this_csv( 1793 | csv_file, d["Encoding"], separ, quotechar, d["Header line"], decim 1794 | ) 1795 | 1796 | conn.insert_reader( 1797 | reading, 1798 | table_name, 1799 | sql, 1800 | create_table=d["Create table"], 1801 | replace=d["Replace existing data"], 1802 | ) 1803 | # refresh 1804 | actualize_db() 1805 | 1806 | 1807 | def read_this_csv(csv_file, encoding, delimiter, quotechar, header, decim): 1808 | """yield csv data records from a file""" 1809 | # handle Python 2/3 1810 | try: 1811 | reader = csv.reader( 1812 | open(csv_file, "r", encoding=encoding), 1813 | delimiter=delimiter, 1814 | quotechar=quotechar, 1815 | ) 1816 | except: # minimal hack for 2.7 1817 | reader = csv.reader( 1818 | open(csv_file, "r"), delimiter=str(delimiter), quotechar=str(quotechar) 1819 | ) 1820 | # handle header 1821 | if header: 1822 | next(reader) 1823 | # otherwise handle special decimal treatment 1824 | for row in reader: 1825 | if decim != "." and not isinstance(row, (type("e"), type("e"))): 1826 | for i in range(len(row)): 1827 | row[i] = row[i].replace(decim, ".") 1828 | yield (row) 1829 | 1830 | 1831 | def export_csv_ok(thetop, entries, actions): 1832 | "export a csv table (action)" 1833 | conn = actions[0] 1834 | # build dico of result 1835 | d = {f[0]: f[1]() for f in entries if not isinstance(f, (type("e"), type("e")))} 1836 | 1837 | csv_file = d["csv Name"].strip() 1838 | conn.export_writer( 1839 | d["Data to export (MUST be 1 Request)"], 1840 | csv_file, 1841 | header=d["Header line"], 1842 | delimiter=d["column separator"], 1843 | encoding=d["Encoding"], 1844 | quotechar='"', 1845 | ) 1846 | 1847 | 1848 | def get_leaves(conn, category, attached_db="", tbl=""): 1849 | """returns a list of 'category' objects in attached_db 1850 | [objectCode, objectLabel, Definition, 'sub-level'] 1851 | """ 1852 | # create formatting shortcuts 1853 | def f(t): 1854 | return ('"%s"' % t.replace('"', '""')) if t != "" else t 1855 | 1856 | def d(t): 1857 | return ("%s." % t) if t != "" else t 1858 | 1859 | # Initialize datas 1860 | Tables, db, tb = [], d(attached_db), f(tbl) 1861 | master = "sqlite_master" if db != "temp." else "sqlite_temp_master" 1862 | 1863 | if category == "pydef": # pydef request is not sql, answer is direct 1864 | Tables = [[k, k, v["pydef"], ""] for k, v in conn.conn_def.items()] 1865 | elif category == "attached_databases": 1866 | # get all attached database, but not the first one ('main') 1867 | resu = list((conn.execute("PRAGMA database_list").fetchall()))[1:] 1868 | for c in resu: 1869 | instruct = "ATTACH DATABASE %s as %s" % (f(c[2]), f(c[1])) 1870 | Tables.append([c[0], c[1], instruct, ""]) 1871 | elif category == "fields": 1872 | resu = conn.execute("PRAGMA %sTABLE_INFO(%s)" % (db, tb)).fetchall() 1873 | Tables = [[c[1], c[1], c[2], ""] for c in resu] 1874 | elif category in ("index", "trigger", "master_table", "table", "view"): 1875 | # others are 1 sql request that generates directly Tables 1876 | if category in ("index", "trigger"): 1877 | sql = """SELECT '{0}' || name, name, coalesce(sql,'--auto') , '' 1878 | FROM {0}{3} WHERE type='{1}' ORDER BY name""" 1879 | elif category == "master_table": 1880 | sql = """SELECT '{0}{3}', '{3}', '--auto', 'fields' 1881 | UNION SELECT '{0}'||name, name, sql, 'fields' 1882 | FROM {0}{3} 1883 | WHERE type='table' AND name LIKE 'sqlite_%' ORDER BY name""" 1884 | elif category in ("table", "view"): 1885 | sql = """SELECT '{0}' || name, name, sql , 'fields' 1886 | FROM {0}{3} WHERE type = '{1}' AND NOT 1887 | (type='table' AND name LIKE 'sqlite_%') ORDER BY name""" 1888 | Tables = list(conn.execute(sql.format(db, category, tbl, master)).fetchall()) 1889 | return Tables 1890 | 1891 | 1892 | class Baresql: 1893 | """a small wrapper around sqlite3 module""" 1894 | 1895 | def __init__( 1896 | self, connection="", keep_log=False, cte_inline=True, isolation_level=None 1897 | ): 1898 | self.dbname = connection.replace(":///", "://").replace("sqlite://", "") 1899 | self.conn = sqlite.connect(self.dbname, detect_types=sqlite.PARSE_DECLTYPES) 1900 | # pydef and logging infrastructure 1901 | self.conn_def = {} 1902 | self.do_log = keep_log 1903 | self.log = [] 1904 | self.conn.isolation_level = isolation_level # commit experience 1905 | 1906 | def close(self): 1907 | """close database and clear dictionnary of registered 'pydef'""" 1908 | self.conn.close 1909 | self.conn_def = {} 1910 | 1911 | def iterdump(self): 1912 | """dump the database (add tweaks over the default dump)""" 1913 | # force detection of utf-8 by placing an only utf-8 comment at top 1914 | yield ("/*utf-8 tag : 你好 мир Artisou à croute*/\n") 1915 | # add the Python functions pydef 1916 | for k in self.conn_def.values(): 1917 | yield (k["pydef"] + ";\n") 1918 | # disable Foreign Constraints at Load 1919 | yield ("PRAGMA foreign_keys = OFF; /*if SQlite */;") 1920 | yield ("\n/* SET foreign_key_checks = 0;/*if Mysql*/;") 1921 | # how to parametrize Mysql to SQL92 standard 1922 | yield ("/* SET sql_mode = 'PIPES_AS_CONCAT';/*if Mysql*/;") 1923 | yield ("/* SET SQL_MODE = ANSI_QUOTES; /*if Mysql*/;\n") 1924 | # now the standard dump (notice it uses BEGIN TRANSACTION) 1925 | for line in self.conn.iterdump(): 1926 | yield (line) 1927 | # re-instantiate Foreign_keys = True 1928 | for row in self.conn.execute("PRAGMA foreign_keys"): 1929 | flag = "ON" if row[0] == 1 else "OFF" 1930 | yield ("PRAGMA foreign_keys = %s;/*if SQlite*/;" % flag) 1931 | yield ("PRAGMA foreign_keys = %s;/*if SQlite bug*/;" % flag) 1932 | yield ("PRAGMA foreign_key_check;/*if SQLite, check*/;") 1933 | yield ("\n/*SET foreign_key_checks = %s;/*if Mysql*/;\n" % row[0]) 1934 | 1935 | def execute(self, sql, env=None): 1936 | """execute sql but intercept log""" 1937 | if self.do_log: 1938 | self.log.append(sql) 1939 | return self.conn.execute(sql) 1940 | 1941 | def createpydef(self, sql): 1942 | """generates and register a pydef instruction""" 1943 | import re 1944 | 1945 | instruction = sql.strip("; \t\n\r") 1946 | # create Python function in Python 1947 | exec(instruction[2:], globals(), pydef_locals) 1948 | # add Python function in SQLite 1949 | instr_header = re.findall(r"\w+", instruction[: (instruction + ")").find(")")]) 1950 | instr_name = instr_header[1] 1951 | instr_parms = len(instr_header) - 2 1952 | instr_pointer=eval(instr_name, globals(), pydef_locals) 1953 | self.conn.create_function(instr_name, instr_parms, instr_pointer) 1954 | instr_add = "self.conn.create_function('%s', %s, %s)" % ( 1955 | instr_name, 1956 | instr_parms, 1957 | instr_name, 1958 | ) 1959 | # housekeeping definition of pydef in a dictionnary 1960 | the_help = pydef_locals[instr_name].__doc__ 1961 | self.conn_def[instr_name] = { 1962 | "parameters": instr_parms, 1963 | "inst": instr_add, 1964 | "help": the_help, 1965 | "pydef": instruction, 1966 | } 1967 | return instr_name 1968 | 1969 | def get_tokens(self, sql, start=0, shell_tokens=False): 1970 | """ 1971 | from given sql start position, yield tokens (value + token type) 1972 | if shell_tokens is True, identify line shell_tokens as sqlite.exe does 1973 | """ 1974 | length = len(sql) 1975 | i = start 1976 | can_be_shell_command = True 1977 | dico = { 1978 | " ": "TK_SP", 1979 | "\t": "TK_SP", 1980 | "\n": "TK_SP", 1981 | "\f": "TK_SP", 1982 | "\r": "TK_SP", 1983 | "(": "TK_LP", 1984 | ")": "TK_RP", 1985 | ";": "TK_SEMI", 1986 | ",": "TK_COMMA", 1987 | "/": "TK_OTHER", 1988 | "'": "TK_STRING", 1989 | "-": "TK_OTHER", 1990 | '"': "TK_STRING", 1991 | "`": "TK_STRING", 1992 | } 1993 | while length > start: 1994 | token = "TK_OTHER" 1995 | if ( 1996 | shell_tokens 1997 | and can_be_shell_command 1998 | and i < length 1999 | and ( 2000 | (sql[i] == "." and i == start) 2001 | or (i > start and sql[i - 1 : i] == "\n.") 2002 | ) 2003 | ): 2004 | # a command line shell ! (supposed on one starting line) 2005 | token = "TK_SHELL" 2006 | i = sql.find("\n", start) 2007 | if i <= 0: 2008 | i = length 2009 | elif sql[i] == "-" and i < length and sql[i : i + 2] == "--": 2010 | # this Token is an end-of-line comment : --blabla 2011 | token = "TK_COM" 2012 | i = sql.find("\n", start) 2013 | if i <= 0: 2014 | i = length 2015 | elif sql[i] == "/" and i < length and sql[i : i + 2] == "/*": 2016 | # this Token is a comment block : /* and bla bla \n bla */ 2017 | token = "TK_COM" 2018 | i = sql.find("*/", start) + 2 2019 | if i <= 1: 2020 | i = length 2021 | elif sql[i] not in dico: 2022 | # this token is a distinct word (tagged as 'TK_OTHER') 2023 | while i < length and sql[i] not in dico: 2024 | i += 1 2025 | else: 2026 | # default token analyze case 2027 | token = dico[sql[i]] 2028 | if token == "TK_SP": 2029 | # find the end of the 'Spaces' Token just detected 2030 | while i < length and sql[i] in dico and dico[sql[i]] == "TK_SP": 2031 | i += 1 2032 | elif token == "TK_STRING": 2033 | # find the end of the 'String' Token just detected 2034 | delimiter = sql[i] 2035 | if delimiter != "'": 2036 | token = "TK_ID" # usefull nuance ? 2037 | while i < length: 2038 | i = sql.find(delimiter, i + 1) 2039 | if i <= 0: # String is never closed 2040 | i = length 2041 | token = "TK_ERROR" 2042 | elif i < length - 1 and sql[i + 1] == delimiter: 2043 | i += 1 # double '' case, so ignore and continue 2044 | else: 2045 | i += 1 2046 | break # normal End of a String 2047 | else: 2048 | if i < length: 2049 | i += 1 2050 | yield sql[start:i], token 2051 | if token == "TK_SEMI": # a new sql order can be a new shell token 2052 | can_be_shell_command = True 2053 | elif token not in ("TK_COM", "TK_SP", "TK_SHELL"): # can't be a shell token 2054 | can_be_shell_command = False 2055 | start = i 2056 | 2057 | def get_sqlsplit(self, sql, remove_comments=False): 2058 | """yield a list of separated sql orders from a sql file""" 2059 | trigger_mode = False 2060 | mysql = [""] 2061 | for tokv, token in self.get_tokens(sql, shell_tokens=True): 2062 | # clear comments option 2063 | if token != "TK_COM" or not remove_comments: 2064 | mysql.append(tokv) 2065 | # special case for Trigger : semicolumn don't count 2066 | if token == "TK_OTHER": 2067 | tok = tokv.upper() 2068 | if tok == "TRIGGER": 2069 | trigger_mode = True 2070 | translvl = 0 2071 | elif trigger_mode and tok in ("BEGIN", "CASE"): 2072 | translvl += 1 2073 | elif trigger_mode and tok == "END": 2074 | translvl -= 1 2075 | if translvl <= 0: 2076 | trigger_mode = False 2077 | elif token == "TK_SEMI" and not trigger_mode: 2078 | # end of a single sql 2079 | yield "".join(mysql) 2080 | mysql = [] 2081 | elif token == "TK_SHELL": 2082 | # end of a shell order 2083 | yield ("" + tokv) 2084 | mysql = [] 2085 | if mysql != []: 2086 | yield ("".join(mysql)) 2087 | 2088 | def insert_reader( 2089 | self, 2090 | reader, 2091 | table_name, 2092 | create_sql=None, 2093 | create_table=True, 2094 | replace=True, 2095 | header=False, 2096 | ): 2097 | """import a given csv reader into a given table""" 2098 | curs = self.conn.cursor() 2099 | # 1-do initialization job 2100 | # speed-up dead otherwise dead slow speed if not memory database 2101 | try: 2102 | curs.execute("begin transaction") 2103 | except: 2104 | pass 2105 | # check if table exists 2106 | here = curs.execute('PRAGMA table_info("%s")' % table_name).fetchall() 2107 | if create_sql and (create_table or len(here) == 0): 2108 | curs.execute('drop TABLE if exists "%s";' % table_name) 2109 | curs.execute(create_sql) 2110 | if replace: 2111 | curs.execute('delete from "%s";' % table_name) 2112 | # count rows of target table 2113 | nbcol = len(curs.execute('pragma table_info("%s")' % table_name).fetchall()) 2114 | sql = 'INSERT INTO "%s" VALUES(%s);' % (table_name, ", ".join(["?"] * nbcol)) 2115 | # read first_line if hasked to skip headers 2116 | if header: 2117 | next(reader) 2118 | # 2-push records 2119 | curs.executemany(sql, reader) 2120 | self.conn.commit() 2121 | 2122 | def export_writer( 2123 | self, 2124 | sql, 2125 | csv_file, 2126 | header=True, 2127 | delimiter=",", 2128 | encoding="utf-8", 2129 | quotechar='"', 2130 | initialize=True, 2131 | ): 2132 | """export a csv table and return number of columns""" 2133 | cursor = self.conn.cursor() 2134 | cursor.execute(sql) 2135 | # do nothing if nothing 2136 | if cursor.description is None or len(cursor.description) == 0: 2137 | return -1 2138 | else: 2139 | nb_columns = len(cursor.description) 2140 | # with PyPy, the "with io.open" for is more than necessary 2141 | if sys.version_info[0] != 2: # python3 2142 | write_mode = "w" if initialize else "a" # Write or Append 2143 | with io.open(csv_file, write_mode, newline="", encoding=encoding) as fout: 2144 | writer = csv.writer( 2145 | fout, delimiter=delimiter, quotechar='"', quoting=csv.QUOTE_MINIMAL 2146 | ) 2147 | if header: 2148 | writer.writerow( 2149 | [i if isinstance(i, str) else i[0] for i in cursor.description] 2150 | ) # PyPy as a strange list of list 2151 | writer.writerows(cursor.fetchall()) 2152 | fout.close # PyPy3-7.3.5 needs that close 2153 | else: # python2.7 (minimal) 2154 | write_mode = "wb" if initialize else "ab" # Write or Append 2155 | with io.open(csv_file, write_mode) as fout: 2156 | writer = csv.writer( 2157 | fout, 2158 | delimiter=str(delimiter), 2159 | quotechar=str('"'), 2160 | quoting=csv.QUOTE_MINIMAL, 2161 | ) 2162 | if header: 2163 | writer.writerow( 2164 | [i if isinstance(i, str) else i[0] for i in cursor.description] 2165 | ) # heading row with anti-PyPy bug 2166 | writer.writerows(cursor.fetchall()) 2167 | fout.close # PyPy3-7.3.5 needs that close 2168 | return nb_columns 2169 | 2170 | 2171 | def _main(): 2172 | welcome_text = """-- SQLite Memo (Demo = click on green "->" and "@" icons) 2173 | \n-- to CREATE a table 'items' and a table 'parts' : 2174 | DROP TABLE IF EXISTS item; DROP TABLE IF EXISTS part; 2175 | CREATE TABLE item (ItemNo, Description,Kg , PRIMARY KEY (ItemNo)); 2176 | CREATE TABLE part(ParentNo, ChildNo , Description TEXT , Qty_per REAL); 2177 | \n-- to CREATE an index : 2178 | DROP INDEX IF EXISTS parts_id1; 2179 | CREATE INDEX parts_id1 ON part(ParentNo Asc, ChildNo Desc); 2180 | \n-- to CREATE a view 'v1': 2181 | DROP VIEW IF EXISTS v1; 2182 | CREATE VIEW v1 as select * from item inner join part as p ON ItemNo=p.ParentNo; 2183 | \n-- to INSERT datas 2184 | INSERT INTO item values("T","Ford",1000); 2185 | INSERT INTO item select "A","Merced",1250 union all select "W","Wheel",9 ; 2186 | INSERT INTO part select ItemNo,"W","needed",Kg/250 from item where Kg>250; 2187 | \n-- to CREATE a Python embedded function, enclose them by "py" and ";" : 2188 | pydef py_hello(): 2189 | "hello world" 2190 | return ("Hello, World !"); 2191 | pydef py_fib(n): 2192 | "fibonacci : example with function call (may only be internal) " 2193 | fib = lambda n: n if n < 2 else fib(n-1) + fib(n-2) 2194 | return("%s" % fib(n*1)); 2195 | 2196 | -- to USE a python embedded function and nesting of embedded functions: 2197 | select py_hello(), py_fib(6) as fibonacci, sqlite_version(); 2198 | \n-- to EXPORT : 2199 | -- a TABLE, select TABLE, then click on icon 'SQL->CSV' 2200 | -- a QUERY RESULT, select the SCRIPT text, then click on icon '???->CSV', 2201 | -- example : select the end of this line: SELECT SQLITE_VERSION() 2202 | \n\n-- to use COMMIT and ROLLBACK : 2203 | BEGIN TRANSACTION; 2204 | UPDATE item SET Kg = Kg + 1; 2205 | COMMIT; 2206 | BEGIN TRANSACTION; 2207 | UPDATE item SET Kg = 0; 2208 | select Kg, Description from Item; 2209 | ROLLBACK; 2210 | select Kg, Description from Item; 2211 | \n\n-- to use SAVEPOINT : 2212 | SAVEPOINT remember_Neo; -- create a savepoint 2213 | UPDATE item SET Description = 'Smith'; -- do things 2214 | SELECT ItemNo, Description FROM Item; -- see things done 2215 | ROLLBACK TO SAVEPOINT remember_Neo; -- go back to savepoint state 2216 | SELECT ItemNo, Description FROM Item; -- see all is back to normal 2217 | RELEASE SAVEPOINT remember_Neo; -- free memory 2218 | 2219 | \n\n-- '.' commands understood: 2220 | -- .backup FILE Backup DB (default "main") to FILE (if Python>=3.7) 2221 | -- .cd DIRECTORY Change the working directory to DIRECTORY 2222 | -- .dump ?FILE? Render database content as SQL (to FILE if specified) 2223 | -- .excel Display the output of next command in spreadsheet 2224 | -- .headers on|off Turn display of headers on or off 2225 | -- .import FILE TABLE Import data from FILE into TABLE 2226 | -- (create TABLE only if it doesn't exist, keep existing records) 2227 | -- .once [--bom] FILE Output of next SQL command to FILE [with utf-8 bom] 2228 | -- .open ?FILE? Close existing database and reopen FILE 2229 | -- .output ?FILE? Send output to FILE or stdout if FILE is omitted 2230 | -- .print STRING... Print literal STRING 2231 | -- .read FILE Read input from FILE 2232 | -- .restore FILE Restore DB (default "main") from FILE (if Python>=3.7) 2233 | -- .separator COL Set column separator in next .once exports (default ,) 2234 | -- .shell CMD ARGS... Run CMD ARGS... in a system shell 2235 | 2236 | .headers on 2237 | .separator ; 2238 | .once --bom '~this_file_of result.txt' 2239 | select ItemNo, Description from item order by ItemNo desc; 2240 | .import '~this_file_of result.txt' in_this_table 2241 | .cd ~ 2242 | ATTACH 'test.db' as toto; 2243 | DROP TABLE IF EXISTS toto.new_item; 2244 | CREATE TABLE toto.new_item as select * from "main"."item"; 2245 | .dump 2246 | """ 2247 | 2248 | if "argparse" in globals(): # not before Python-3.2 2249 | parser = argparse.ArgumentParser( 2250 | description="sqlite_bro : a graphic SQLite browser in 1 Python file" 2251 | ) 2252 | parser.add_argument( 2253 | "-q", "--quiet", action="store_true", help="do not launch the gui" 2254 | ) 2255 | parser.add_argument( 2256 | "-w", 2257 | "--wait", 2258 | action="store_true", 2259 | help="wait the user to launch the scripts", 2260 | ) 2261 | parser.add_argument( 2262 | "-db", 2263 | "--database", 2264 | default=":memory:", 2265 | type=str, 2266 | help="specify initial Database if not ':memory:'", 2267 | ) 2268 | parser.add_argument( 2269 | "-sc", "--scripts", type=str, help="qive a list of initial scripts" 2270 | ) 2271 | args = parser.parse_args() 2272 | 2273 | if args.quiet: 2274 | app = App(use_gui=False) 2275 | else: 2276 | app = App(use_gui=True) 2277 | # start with a memory Database and a welcome 2278 | app.new_db(":memory:") 2279 | if args.database: 2280 | app.open_db(args.database) 2281 | if args.scripts: 2282 | if isinstance(args.scripts, str): 2283 | scripts = [args.scripts, "", ""] 2284 | else: 2285 | scripts = args.scripts 2286 | for script in scripts: 2287 | if os.path.isfile(script): 2288 | with io.open(script, encoding=guess_encoding(script)[0]) as f: 2289 | welcome_text = f.read() 2290 | app.n.new_query_tab("Welcome", welcome_text) 2291 | if not args.wait: 2292 | app.run_tab() 2293 | else: 2294 | app.n.new_query_tab("Welcome", welcome_text) 2295 | if args.quiet: 2296 | app.close_db 2297 | else: 2298 | app = App(use_gui=True) 2299 | # start with a memory Database and a welcome 2300 | app.new_db(":memory:") 2301 | app.n.new_query_tab("Welcome", welcome_text) 2302 | if app.use_gui: 2303 | app.tk_win.mainloop() 2304 | 2305 | 2306 | if __name__ == "__main__": 2307 | _main() # create a tkk graphic interface with a main window tk_win 2308 | -------------------------------------------------------------------------------- /sqlite_bro/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stonebig/sqlite_bro/6f3e76125fe74d26648b9046fe7448b0587b7b88/sqlite_bro/tests/__init__.py -------------------------------------------------------------------------------- /sqlite_bro/tests/test_general.py: -------------------------------------------------------------------------------- 1 | # from pyappveyordemo.extension import some_function 2 | import pytest 3 | import pathlib 4 | import tempfile 5 | 6 | import io 7 | from sqlite_bro import sqlite_bro 8 | app = sqlite_bro.App() 9 | def test_DeBase(): 10 | "learning the ropes" 11 | assert 1 == 1 12 | 13 | 14 | def test_Basics(): 15 | "create script, run script, output result, check result" 16 | app.new_db(":memory:") 17 | with tempfile.TemporaryDirectory(prefix='.tmp') as tmp_dir: 18 | print(tmp_dir) 19 | tmp_file = str(pathlib.PurePath(tmp_dir, 'sqlite_bro_test_Basics.tmp')) 20 | welcome_text = """ 21 | create table item (ItemNo, Description,Kg , PRIMARY KEY (ItemNo)); 22 | INSERT INTO item values("T","Ford",1000); 23 | INSERT INTO item select "A","Merced",1250 union all select "W","Wheel",9 ; 24 | .once %s 25 | select ItemNo, Description, 1000*Kg Gramm from item order by ItemNo desc; 26 | .import %s in_this_table""" % (tmp_file, tmp_file) 27 | app.n.new_query_tab("Welcome", welcome_text) 28 | app.run_tab() 29 | app.close_db 30 | 31 | file_encoding = sqlite_bro.guess_encoding(tmp_file)[0] 32 | with io.open(tmp_file, mode='rt', encoding=file_encoding) as f: 33 | result = f.readlines() 34 | assert len(result) == 4 35 | assert result[-1] == "A,Merced,1250000\n" 36 | 37 | def test_Outputs(): 38 | "testing .output, .print, .header, .separator" 39 | 40 | app.new_db(":memory:") 41 | with tempfile.TemporaryDirectory(prefix='.tmp') as tmp_dir: 42 | print(tmp_dir) 43 | tmp_file = str(pathlib.PurePath(tmp_dir, 'sqlite_bro_test_Output.tmp')) 44 | welcome_text = """ 45 | create table item (ItemNo, Description , PRIMARY KEY (ItemNo)); 46 | INSERT INTO item values("DS","Citroën"); 47 | .output %s 48 | .separator ; 49 | .headers off 50 | .print a;b 51 | select * from item; 52 | .headers on 53 | .separator ! 54 | select * from item; 55 | .import %s in_this_table""" % (tmp_file, tmp_file) 56 | app.n.new_query_tab("Welcome", welcome_text) 57 | app.run_tab() 58 | app.close_db 59 | 60 | file_encoding = sqlite_bro.guess_encoding(tmp_file)[0] 61 | with io.open(tmp_file, mode='rt', encoding=file_encoding) as f: 62 | result = f.readlines() 63 | print(result) 64 | assert len(result) == 4 65 | assert result[0] == "a;b\n" 66 | assert result[1] == "DS;Citroën\n" 67 | assert result[2] == "ItemNo!Description\n" 68 | assert result[3] == "DS!Citroën\n" 69 | -------------------------------------------------------------------------------- /sqlite_bro/tests/test_general_no_gui.py: -------------------------------------------------------------------------------- 1 | # from pyappveyordemo.extension import some_function 2 | import pytest 3 | import pathlib 4 | import tempfile 5 | 6 | import io 7 | from sqlite_bro import sqlite_bro 8 | app = sqlite_bro.App(use_gui=False) 9 | def test_DeBase(): 10 | "learning the ropes" 11 | assert 1 == 1 12 | 13 | 14 | def test_Basics(): 15 | "create script, run script, output result, check result" 16 | app.new_db(":memory:") 17 | with tempfile.TemporaryDirectory(prefix='.tmp') as tmp_dir: 18 | print(tmp_dir) 19 | tmp_file = str(pathlib.PurePath(tmp_dir, 'sqlite_bro_test_Basics.tmp')) 20 | welcome_text = """ 21 | create table item (ItemNo, Description,Kg , PRIMARY KEY (ItemNo)); 22 | INSERT INTO item values("T","Ford",1000); 23 | INSERT INTO item select "A","Merced",1250 union all select "W","Wheel",9 ; 24 | .once %s 25 | select ItemNo, Description, 1000*Kg Gramm from item order by ItemNo desc; 26 | .import %s in_this_table""" % (tmp_file, tmp_file) 27 | app.n.new_query_tab("Welcome", welcome_text) 28 | app.run_tab() 29 | app.close_db 30 | 31 | file_encoding = sqlite_bro.guess_encoding(tmp_file)[0] 32 | with io.open(tmp_file, mode='rt', encoding=file_encoding) as f: 33 | result = f.readlines() 34 | assert len(result) == 4 35 | assert result[-1] == "A,Merced,1250000\n" 36 | 37 | def test_Outputs(): 38 | "testing .output, .print, .header, .separator" 39 | 40 | app.new_db(":memory:") 41 | with tempfile.TemporaryDirectory(prefix='.tmp') as tmp_dir: 42 | print(tmp_dir) 43 | tmp_file = str(pathlib.PurePath(tmp_dir, 'sqlite_bro_test_Output.tmp')) 44 | welcome_text = """ 45 | create table item (ItemNo, Description , PRIMARY KEY (ItemNo)); 46 | INSERT INTO item values("DS","Citroën"); 47 | .output %s 48 | .separator ; 49 | .headers off 50 | .print a;b 51 | select * from item; 52 | .headers on 53 | .separator ! 54 | select * from item; 55 | .import %s in_this_table""" % (tmp_file, tmp_file) 56 | app.n.new_query_tab("Welcome", welcome_text) 57 | app.run_tab() 58 | app.close_db 59 | 60 | file_encoding = sqlite_bro.guess_encoding(tmp_file)[0] 61 | with io.open(tmp_file, mode='rt', encoding=file_encoding) as f: 62 | result = f.readlines() 63 | print(result) 64 | assert len(result) == 4 65 | assert result[0] == "a;b\n" 66 | assert result[1] == "DS;Citroën\n" 67 | assert result[2] == "ItemNo!Description\n" 68 | assert result[3] == "DS!Citroën\n" 69 | --------------------------------------------------------------------------------