Showing preview only (749K chars total). Download the full file or copy to clipboard to get everything.
Repository: explorerhq/django-sql-explorer
Branch: master
Commit: 7627b426e0b8
Files: 214
Total size: 692.0 KB
Directory structure:
gitextract_ainzhnq4/
├── .dockerignore
├── .editorconfig
├── .eslintignore
├── .github/
│ ├── dependabot.yml
│ └── workflows/
│ ├── codeql-analysis.yml
│ ├── docs.yml
│ ├── lint.yml
│ ├── publish-pypi.yml
│ ├── publish-test.yml
│ └── test.yml
├── .gitignore
├── .nvmrc
├── .pre-commit-config.yaml
├── .readthedocs.yaml
├── AUTHORS
├── Dockerfile
├── HISTORY.rst
├── LICENSE
├── MANIFEST.in
├── README.rst
├── docker-compose.yml
├── docs/
│ ├── Makefile
│ ├── _static/
│ │ └── .directory
│ ├── _templates/
│ │ └── .directory
│ ├── conf.py
│ ├── dependencies.rst
│ ├── development.rst
│ ├── features.rst
│ ├── history.rst
│ ├── index.rst
│ ├── install.rst
│ ├── make.bat
│ ├── requirements.txt
│ └── settings.rst
├── entrypoint.sh
├── explorer/
│ ├── __init__.py
│ ├── actions.py
│ ├── admin.py
│ ├── app_settings.py
│ ├── apps.py
│ ├── assistant/
│ │ ├── __init__.py
│ │ ├── forms.py
│ │ ├── models.py
│ │ ├── urls.py
│ │ ├── utils.py
│ │ └── views.py
│ ├── charts.py
│ ├── ee/
│ │ ├── LICENSE
│ │ ├── __init__.py
│ │ ├── db_connections/
│ │ │ ├── __init__.py
│ │ │ ├── admin.py
│ │ │ ├── create_sqlite.py
│ │ │ ├── forms.py
│ │ │ ├── mime.py
│ │ │ ├── models.py
│ │ │ ├── type_infer.py
│ │ │ ├── utils.py
│ │ │ └── views.py
│ │ └── urls.py
│ ├── exporters.py
│ ├── forms.py
│ ├── locale/
│ │ ├── ru/
│ │ │ └── LC_MESSAGES/
│ │ │ ├── django.mo
│ │ │ └── django.po
│ │ └── zh_Hans/
│ │ └── LC_MESSAGES/
│ │ ├── django.mo
│ │ └── django.po
│ ├── migrations/
│ │ ├── 0001_initial.py
│ │ ├── 0002_auto_20150501_1515.py
│ │ ├── 0003_query_snapshot.py
│ │ ├── 0004_querylog_duration.py
│ │ ├── 0005_auto_20160105_2052.py
│ │ ├── 0006_query_connection.py
│ │ ├── 0007_querylog_connection.py
│ │ ├── 0008_auto_20190308_1642.py
│ │ ├── 0009_auto_20201009_0547.py
│ │ ├── 0010_sql_required.py
│ │ ├── 0011_query_favorites.py
│ │ ├── 0012_alter_queryfavorite_query_alter_queryfavorite_user.py
│ │ ├── 0013_querylog_error_querylog_success.py
│ │ ├── 0014_promptlog.py
│ │ ├── 0015_explorervalue.py
│ │ ├── 0016_alter_explorervalue_key.py
│ │ ├── 0017_databaseconnection.py
│ │ ├── 0018_alter_databaseconnection_host_and_more.py
│ │ ├── 0019_alter_databaseconnection_engine.py
│ │ ├── 0020_databaseconnection_extras_and_more.py
│ │ ├── 0021_alter_databaseconnection_password_and_more.py
│ │ ├── 0022_databaseconnection_upload_fingerprint.py
│ │ ├── 0023_query_database_connection_and_more.py
│ │ ├── 0024_auto_20240803_1135.py
│ │ ├── 0025_alter_query_database_connection_alter_querylog_database_connection.py
│ │ ├── 0026_tabledescription.py
│ │ ├── 0027_query_few_shot.py
│ │ ├── 0028_promptlog_database_connection_promptlog_user_request.py
│ │ └── __init__.py
│ ├── models.py
│ ├── permissions.py
│ ├── schema.py
│ ├── src/
│ │ ├── js/
│ │ │ ├── assistant.js
│ │ │ ├── codemirror-config.js
│ │ │ ├── csrf.js
│ │ │ ├── explorer.js
│ │ │ ├── favorites.js
│ │ │ ├── main.js
│ │ │ ├── pivot-setup.js
│ │ │ ├── pivot.js
│ │ │ ├── query-list.js
│ │ │ ├── schema.js
│ │ │ ├── schemaService.js
│ │ │ ├── table-to-csv.js
│ │ │ ├── tableDescription.js
│ │ │ └── uploads.js
│ │ └── scss/
│ │ ├── assistant.scss
│ │ ├── choices.scss
│ │ ├── explorer.scss
│ │ ├── pivot.css
│ │ ├── styles.scss
│ │ └── variables.scss
│ ├── tasks.py
│ ├── telemetry.py
│ ├── templates/
│ │ ├── assistant/
│ │ │ ├── table_description_confirm_delete.html
│ │ │ ├── table_description_form.html
│ │ │ └── table_description_list.html
│ │ ├── connections/
│ │ │ ├── connection_upload.html
│ │ │ ├── connections.html
│ │ │ ├── database_connection_confirm_delete.html
│ │ │ ├── database_connection_detail.html
│ │ │ └── database_connection_form.html
│ │ └── explorer/
│ │ ├── assistant.html
│ │ ├── base.html
│ │ ├── export_buttons.html
│ │ ├── fullscreen.html
│ │ ├── params.html
│ │ ├── pdf_template.html
│ │ ├── play.html
│ │ ├── preview_pane.html
│ │ ├── query.html
│ │ ├── query_confirm_delete.html
│ │ ├── query_favorite_button.html
│ │ ├── query_favorites.html
│ │ ├── query_list.html
│ │ ├── querylog_list.html
│ │ ├── schema.html
│ │ └── schema_error.html
│ ├── templatetags/
│ │ ├── __init__.py
│ │ ├── explorer_tags.py
│ │ └── vite.py
│ ├── tests/
│ │ ├── __init__.py
│ │ ├── csvs/
│ │ │ ├── all_types.csv
│ │ │ ├── dates.csv
│ │ │ ├── floats.csv
│ │ │ ├── integers.csv
│ │ │ ├── mixed.csv
│ │ │ ├── rc_sample.csv
│ │ │ └── test_case1.csv
│ │ ├── factories.py
│ │ ├── json/
│ │ │ ├── github.json
│ │ │ ├── kings.json
│ │ │ └── list.json
│ │ ├── settings.py
│ │ ├── settings_base.py
│ │ ├── test_actions.py
│ │ ├── test_apps.py
│ │ ├── test_assistant.py
│ │ ├── test_create_sqlite.py
│ │ ├── test_csrf_cookie_name.py
│ │ ├── test_db_connection_utils.py
│ │ ├── test_exporters.py
│ │ ├── test_forms.py
│ │ ├── test_mime.py
│ │ ├── test_models.py
│ │ ├── test_schema.py
│ │ ├── test_tasks.py
│ │ ├── test_telemetry.py
│ │ ├── test_type_infer.py
│ │ ├── test_utils.py
│ │ └── test_views.py
│ ├── urls.py
│ ├── utils.py
│ └── views/
│ ├── __init__.py
│ ├── auth.py
│ ├── create.py
│ ├── delete.py
│ ├── download.py
│ ├── email.py
│ ├── export.py
│ ├── format_sql.py
│ ├── list.py
│ ├── mixins.py
│ ├── query.py
│ ├── query_favorite.py
│ ├── schema.py
│ ├── stream.py
│ └── utils.py
├── manage.py
├── package.json
├── public_key.pem
├── pypi-release-checklist.md
├── requirements/
│ ├── base.txt
│ ├── dev.txt
│ ├── extra/
│ │ ├── assistant.txt
│ │ ├── charts.txt
│ │ ├── snapshots.txt
│ │ ├── uploads.txt
│ │ └── xls.txt
│ └── tests.txt
├── ruff.toml
├── setup.cfg
├── setup.py
├── test_project/
│ ├── __init__.py
│ ├── celery_config.py
│ ├── settings.py
│ └── urls.py
├── tox.ini
└── vite.config.mjs
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
node_modules
================================================
FILE: .editorconfig
================================================
# http://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4
[*.py]
max_line_length = 120
[*.toml]
indent_size = 2
[*.yaml]
indent_size = 2
[*.yml]
indent_size = 2
================================================
FILE: .eslintignore
================================================
/explorer/static/js/src/pivot.js
================================================
FILE: .github/dependabot.yml
================================================
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
================================================
FILE: .github/workflows/codeql-analysis.yml
================================================
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
name: "CodeQL"
permissions:
actions: read
contents: read
security-events: write
on:
push:
branches:
- master
- support/3.x
pull_request:
# The branches below must be a subset of the branches above
branches:
- master
- support/3.x
schedule:
- cron: '0 2 * * 5'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Override automatic language detection by changing the below list
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
language: ['python', 'javascript']
# Learn more...
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
================================================
FILE: .github/workflows/docs.yml
================================================
name: Docs
on: [push, pull_request]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
docs:
runs-on: ubuntu-latest
name: docs
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.9
- run: python -m pip install -r docs/requirements.txt
- name: Build docs
run: |
cd docs
sphinx-build -b html -n -d _build/doctrees . _build/html
================================================
FILE: .github/workflows/lint.yml
================================================
name: Ruff
on:
push:
pull_request:
jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run ruff
run: pipx run ruff check --output-format=github explorer
================================================
FILE: .github/workflows/publish-pypi.yml
================================================
name: Publish Python 🐍 distributions 📦 to pypi
on:
release:
types:
- published
jobs:
build-n-publish:
name: Build and publish Python 🐍 distributions 📦 to pypi
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/django-sql-explorer
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- name: Install dependencies
run: npm install
- name: Build client
run: npm run build
- name: Install pypa/build
run: >-
python -m
pip install
build
--user
- name: Build a binary wheel and a source tarball
run: >-
python -m
build
--sdist
--wheel
--outdir dist/
.
- name: Publish distribution 📦 to PyPI
if: startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@release/v1
================================================
FILE: .github/workflows/publish-test.yml
================================================
name: Publish Python 🐍 distributions 📦 to TestPyPI
on:
push:
branches:
- master
- support/3.x
jobs:
build-n-publish:
name: Build and publish Python 🐍 distributions 📦 to TestPyPI
runs-on: ubuntu-latest
environment:
name: test
url: https://test.pypi.org/p/django-sql-explorer
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install pypa/build
run: >-
python -m
pip install
build
--user
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- name: Install npm dependencies
run: npm install
- name: Build client
run: npm run build
- name: Build a binary wheel and a source tarball
run: >-
python -m
build
--sdist
--wheel
--outdir dist/
.
- name: Publish distribution 📦 to Test PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
skip_existing: true
================================================
FILE: .github/workflows/test.yml
================================================
name: Tests
on:
push:
branches:
- master
- support/3.x
pull_request:
concurrency:
group: ${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
tests:
name: Python ${{ matrix.python-version }}
runs-on: ubuntu-22.04
strategy:
matrix:
python-version:
- '3.10'
- '3.11'
- '3.12'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- name: Install dependencies
run: |
npm install
python -m pip install --upgrade pip setuptools wheel
python -m pip install --upgrade 'tox>=4.0.0rc3'
- name: Build client
run: npm run build
- name: Run tox targets for ${{ matrix.python-version }}
run: tox run -f py$(echo ${{ matrix.python-version }} | tr -d .)
- name: Upload coverage data
uses: actions/upload-artifact@v4
with:
name: coverage-data-${{ matrix.python-version }}
path: '.coverage*'
include-hidden-files: true
coverage:
name: Coverage
runs-on: ubuntu-22.04
needs: tests
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
run: |
npm install
python -m pip install --upgrade coverage[toml]
- name: Build client
run: npm run build
- name: Download data
uses: actions/download-artifact@v4
with:
pattern: coverage-data-*
merge-multiple: true
- name: Combine coverage
run: |
python -m coverage combine
python -m coverage html --skip-covered --skip-empty
python -m coverage report
- name: Upload HTML report
uses: actions/upload-artifact@v4
with:
name: html-report
path: htmlcov
================================================
FILE: .gitignore
================================================
/.idea/
*.pyc
*.db
/project/
/dist
*.egg-info
.DS_Store
/build
*#
*~
.coverage*
/htmlcov/
*.orig
tmp
venv/
.venv/
.tox/
node_modules/
explorer/static/
# Sphinx documentation
docs/_build/
.env
tst
tst2
user_dbs/*
tmp2
chinook.sqlite
model_data.json
tst1
tst1-journal
tst2
coverage-data-*
================================================
FILE: .nvmrc
================================================
20.15.1
================================================
FILE: .pre-commit-config.yaml
================================================
ci:
autofix_commit_msg: |
ci: auto fixes from pre-commit hooks
for more information, see https://pre-commit.ci
autofix_prs: false
autoupdate_commit_msg: "ci: pre-commit autoupdate"
autoupdate_schedule: monthly
default_language_version:
python: python3.12
repos:
- repo: https://github.com/asottile/pyupgrade
rev: v3.16.0
hooks:
- id: pyupgrade
args: ["--py38-plus"]
- repo: https://github.com/adamchainz/django-upgrade
rev: "1.19.0"
hooks:
- id: django-upgrade
args: [--target-version, "3.2"]
- repo: https://github.com/asottile/yesqa
rev: v1.5.0
hooks:
- id: yesqa
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: check-merge-conflict
- id: mixed-line-ending
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.5.0"
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format
- repo: https://github.com/remastr/pre-commit-django-check-migrations
rev: v0.1.0
hooks:
- id: check-migrations-created
args: [--manage-path=manage.py]
additional_dependencies: [django==4.1]
- repo: https://github.com/rstcheck/rstcheck
rev: v6.2.0
hooks:
- id: rstcheck
additional_dependencies:
- sphinx==6.1.3
- tomli==2.0.1
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.0.0
hooks:
- id: prettier
================================================
FILE: .readthedocs.yaml
================================================
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
version: 2
build:
os: ubuntu-22.04
tools:
python: "3.11"
sphinx:
configuration: docs/conf.py
fail_on_warning: false
formats:
- epub
- pdf
python:
install:
- requirements: docs/requirements.txt
================================================
FILE: AUTHORS
================================================
The following people have contributed to django-sql-explorer:
- Chris Clark
- Mark Walker
- Lee Brooks
- Artyom Chernyakov
- Rodney Hawkins
- Dane Hillard
- Wojtek Jurkowlaniec
- Lee Kagiso
- Phil Krylov
- Grant McConnaughey
- Josh Miller
- Jens Nistler
- Pietro Pilolli
- David Sanders
- Anton Shutik
- Nick Spacek
- Stanislav Tarazevich
- Ming Hsien Tseng
- Jared Proffitt
- Brad Melin
- Dara Adib
- Moe Elias
- Illia Volochii
- Amir Abedi
- Christian Clauss
- Shiyan Shirani
- Calum Smith
- Steven Luoma
A full list of contributors can be found on Github; https://github.com/explorerhq/sql-explorer/graphs/contributors
================================================
FILE: Dockerfile
================================================
# Build stage
FROM python:3.12.4 as builder
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements /app/requirements
RUN pip install --no-cache-dir -r requirements/dev.txt
# Install NVM and Node.js
RUN mkdir /usr/local/.nvm
ENV NVM_DIR /usr/local/.nvm
# This should match the version referenced below in the Run stage, and in entrypoint.sh
ENV NODE_VERSION 20.15.1
COPY package.json package-lock.json /app/
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash \
&& . "$NVM_DIR/nvm.sh" \
&& nvm install ${NODE_VERSION} \
&& nvm use v${NODE_VERSION} \
&& nvm alias default v${NODE_VERSION} \
&& npm install
# Runtime stage
FROM python:3.12.4
WORKDIR /app
# Copy Python environment from builder
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
# Copy Node.js environment from builder
COPY --from=builder /usr/local/.nvm /usr/local/.nvm
ENV NVM_DIR /usr/local/.nvm
# The version in this path should match the version referenced above in the Run stage, and in entrypoint.sh
ENV PATH $NVM_DIR/versions/node/v20.15.1/bin:$PATH
COPY --from=builder /app/node_modules /app/node_modules
COPY . /app
# Run migrations and create initial data
RUN python manage.py migrate && \
python manage.py shell <<ORM
from django.contrib.auth.models import User
u = User.objects.filter(username='admin').first()
if not u:
u = User(username='admin')
u.set_password('admin')
u.is_superuser = True
u.is_staff = True
u.save()
from explorer.models import Query
queries = Query.objects.all().count()
if queries == 0:
q = Query(sql='select * from explorer_query;', title='Sample Query')
q.save()
ORM
# Copy and set permissions for the entrypoint script
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Expose the ports the app runs on
EXPOSE 8000 5173
# Health check
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/ || exit 1
# Set the entrypoint
ENTRYPOINT ["/entrypoint.sh"]
================================================
FILE: HISTORY.rst
================================================
==========
Change Log
==========
This document records all notable changes to `SQL Explorer <https://github.com/explorerhq/sql-explorer>`_.
This project adheres to `Semantic Versioning <https://semver.org/>`_.
`5.3.0`_ (2024-09-24)
===========================
* `#664`_: Improvements to the AI SQL Assistant:
- Table Annotations: Write persistent table annotations with descriptive information that will get injected into the
prompt for the assistant. For example, if a table is commonly joined to another table through a non-obvious foreign
key, you can tell the assistant about it in plain english, as an annotation to that table. Every time that table is
deemed 'relevant' to an assistant request, that annotation will be included alongside the schema and sample data.
- Few-Shot Examples: Using the small checkbox on the bottom-right of any saved queries, you can designate certain
queries as 'few shot examples". When making an assistant request, any designated few-shot examples that reference
the same tables as your assistant request will get included as 'reference sql' in the prompt for the LLM.
- Autocomplete / multiselect when selecting tables info to send to the SQL Assistant. Much easier and more keyboard
focused.
- Relevant tables are added client-side visually, in real time, based on what's in the SQL editor and/or any tables
mentioned in the assistant request. The dependency on sql_metadata is therefore removed, as server-side SQL parsing
is no longer necessary.
- Ability to view Assistant request/response history.
- Improved system prompt that emphasizes the particular SQL dialect being used.
- Addresses issue #657.
* `#660`_: Userspace connection migration.
- This should be an invisible change, but represents a significant refactor of how connections function. Instead of a
weird blend of DatabaseConnection models and underlying Django models (which were the original Explorer
connections), this migrates all connections to DatabaseConnection models and implements proper foreign keys to them
on the Query and QueryLog models. A data migration creates new DatabaseConnection models based on the configured
settings.EXPLORER_CONNECTIONS. Going forward, admins can create new Django-backed DatabaseConnection models by
registering the connection in EXPLORER_CONNECTIONS, and then creating a DatabaseConnection model using the Django
admin or the user-facing /connections/new/ form, and entering the Django DB alias and setting the connection type
to "Django Connection".
- The Query.connection and QueryLog.connection fields are deprecated and will be removed in a future release. They
are kept around in this release in case there is an unforeseen issue with the migration. Preserving the fields for
now ensures there is no data loss in the event that a rollback to an earlier version is required.
* Fixed a bug when validating connections to uploaded files. Also added basic locking when downloading files from S3.
* On-boarding UI; if no connections or queries are created, the UI walks the user through it a bit.
* Keyboard shortcut for formatting the SQL in the editor.
- Cmd+Shift+F (Windows: Ctrl+Shift+F)
- The format button has been moved tobe a small icon towards the bottom-right of the SQL editor.
* `#675`_ - fail gracefully when building the schema if a particular table cant be accessed by the connection
`5.2.0`_ (2024-08-19)
===========================
* `#651`_: Ability to append an upload to a previously uploaded file/sqlite DB as a new table
* Good cache busting and detection of file changes on uploads
* Significant documentation improvements to uploads and connections
* Separate the upload UI from the 'add connection' UI, as they are materially different
* Fix a small bug with bar chart generation, when values are null
* Ability to refresh a connection's schema and data (if it's an upload) from the connections list view
* `#659`_: Search all queries, even if the header is collapsed. Addresses issue #464 (partially) and #658 (fully).
* `#662`_: Refactored dockerfile to use non-root directories. Addresses issue #661.
`5.1.1`_ (2024-07-30)
===========================
* `#654`_: Bugfix: Parameterized query does not work for viewers
* `#653`_: Bugfix: Schema search not visible anymore
* Bugfix: Error messages in query.html were floating in the wrong spot
* `#555`_: Prevent queries with many thousands of results from being punishingly slow. The number of data points in
the chart now matches the number of data points in the preview pane.
`5.1.0`_ (2024-07-30)
===========================
Major improvements:
* `#647`_: Upload json files as data sources (in addition to CSV and SQLite files). Both 'normal'
json files, and files structured as a list of json objects (one json object per line) are supported.
* `#643`_: Addresses #640 (Snowflake support). Additionally, supports an "extras" field on the
userspace DatabaseConnection object, which allows for arbitrary additional connection
params to get added. This allows engine-specific (or just more obscure) settings to
get injected into the connection.
* `#644`_: Dockerfile and docker-compose to run the test_project. Replaces the old start.sh script.
Minor improvements:
* `#647`_: In the schema explorer, clicking on a field name copies it to the clipboard
* `#647`_: Charts are limited to a maximum of 10 series. This significantly speeds up rendering
of 'wide' result-sets when charts are enabled.
* `#645`_: Removed pie charts, added bar charts. Replaced Seaborn with Matplotlib
because it's much lighter weight. Pie charts were overly finicky to get working.
Bars are more useful. Will look to continue to expand charting in the future.
* `#643`_: After uploading a csv/json/etc, the resulting connection is highlighted in the
connection list, making it much clearer what happened.
* `#643`_: Fixed some bugs in user connection stuff in general, and improved the UI.
Bugfixes and internal improvements:
* `#647`_: Robustness to the user uploads feature, in terms of the UI, error handling and logging, and test coverage.
* `#648`_: Backwards migration for 0016_alter_explorervalue_key.py
* `#649`_: Use a more reliable source of the static files URL
* `#635`_: Improved test coverage in tox, so that base requirements are properly used.
This would have prevented (for example) issue 631. Additionally, introduced a test
to verify that migrations are always generated, which would have prevented #633.
* `#636`_: Output rendering bugfix.
* `#567`_: Upgrade translate tags in templates to more modern style.
`5.0.2`_ (2024-07-3)
===========================
* `#633`_: Missing migration
* CSS tweaks to tighten up the Query UI
`5.0.1`_ (2024-06-26)
===========================
* `#631`_: Pandas is only required if EXPLORER_USER_UPLOADS_ENABLED is True
`5.0.0`_ (2024-06-25)
===========================
* Manage DB connections via the UI (and/or Django Admin). Set EXPLORER_DB_CONNECTIONS_ENABLED
to True in settings to enable user-facing connection management.
* Upload CSV or SQLite DBs directly, to create additional connections.
This functionality has additional dependencies which can be installed with
the 'uploads' extra (e.g. pip install django-sql-explorer[uploads]). Then set EXPLORER_USER_UPLOADS_ENABLED
to True, and make sure S3_BUCKET is also set up.
* The above functionality is managed by a new license, restricting the
ability of 3rd parties resell SQL Explorer (commercial usage is absolutely
still permitted).
* Query List home page is sortable
* Select all / deselect all with AI assistant
* Assistant tests run reliably in CI/CD
* Introduced some branding and styling improvements
`4.3.0`_ (2024-05-27)
===========================
* Keyboard shortcut to show schema hints (cmd+S / ctrl+S -- note that is a capital
"S" so the full kbd commands is cmd+shift+s)
* DB-managed LLM prompts (editable in django admin)
* Versioned .js bundles (for cache busting)
* Automatically populate assistant responses that contain code into the editor
* `#616`_: Update schema/assistant tables/autocomplete on connection drop-down change
* `#618`_: Import models so that migrations are properly understood by Django
* `#619`_: Get CSRF from DOM (instead of cookie) if CSRF_USE_SESSIONS is set
`4.2.0`_ (2024-04-26)
===========================
* `#609`_: Tracking should be opt-in and not use the SECRET_KEY
* `#610`_: Import error (sql_metadata) with 4.1 version
* `#612`_: Accessing the database during app initialization
* Regex-injection vulnerability
* Improved assistant UI
`4.1.0`_ (2024-04-23)
===========================
* SQL Assistant: Built in query help via OpenAI (or LLM of choice), with relevant schema
automatically injected into the prompt. Enable by setting EXPLORER_AI_API_KEY.
* Anonymous usage telemetry. Disable by setting EXPLORER_ENABLE_ANONYMOUS_STATS to False.
* Refactor pip requirements to make 'extras' more robust and easier to manage.
* `#592`_: Support user models with no email fields
* `#594`_: Eliminate <script> tags to prevent potential Content Security Policy issues.
`4.0.2`_ (2024-02-06)
===========================
* Add support for Django 5.0. Drop support for Python < 3.10.
* Basic code completion in the editor!
* Front-end must be built with Vite if installing from source.
* `#565`_: Front-end modernization. CodeMirror 6. Bootstrap5. Vite-based build
* `#566`_: Django 5 support & tests
* `#537`_: S3 signature version support
* `#562`_: Record and show whether the last run of each query was successful
* `#571`_: Replace isort and flake8 with Ruff (linting)
`4.0.0.beta1`_ (2024-02-01)
===========================
* Yanked due to a packaging version issue
`3.2.1`_ (2023-07-13)
=====================
* `#539`_: Test for SET PASSWORD
* `#544`_: Fix `User` primary key reference
`3.2.0`_ (2023-05-17)
=====================
* `#533`_: CSRF token httponly support + s3 destination for async results
`3.1.1`_ (2023-02-27)
=====================
* `#529`_: Added ``makemigrations --check`` pre-commit hook
* `#528`_: Add missing migration
`3.1.0`_ (2023-02-25)
=====================
* `#520`_: Favorite queries
* `#519`_: Add labels to params like ``$$paramName|label:defaultValue$$``
* `#517`_: Pivot export
* `#524`_: ci: pre-commit autoupdate
* `#523`_: ci: ran pre-commit on all files for ci bot integration
* `#522`_: ci: coverage update
* `#521`_: ci: Adding django 4.2 to the test suite
`3.0.1`_ (2022-12-16)
=====================
* `#515`_: Fix for running without optional packages
`3.0`_ (2022-12-15)
===================
* Add support for Django >3.2 and drop support for <3.2
* Add support for Python 3.9, 3.10 and 3.11 and drop support for <3.8
* `#496`_: Document breakage of "Format" button due to ``CSRF_COOKIE_HTTPONLY`` (`#492`_)
* `#497`_: Avoid execution of parameterised queries when viewing query
* `#498`_: Change sql blacklist functionality from regex to sqlparse
* `#500`_: Form display in popup now requires sanitize: false flag
* `#501`_: Updated celery support
* `#504`_: Added pre-commit hooks
* `#505`_: Feature/more s3 providers
* `#506`_: Check sql blacklist on execution as well as save
* `#508`_: Conditionally import optional packages
`2.5.0`_ (2022-10-09)
=====================
* `#494`_: Fixes Security hole in blacklist for MySQL (`#490`_)
* `#488`_: docs: Fix a few typos
* `#481`_: feat: Add pie and line chart tabs to query result preview
* `#478`_: feat: Improved templates to make easier to customize (Fix `#477`_)
`2.4.2`_ (2022-08-30)
=====================
* `#484`_: Added ``DEFAULT_AUTO_FIELD`` (Fix `#483`_)
* `#475`_: Add ``SET`` to blacklisted keywords
`2.4.1`_ (2022-03-10)
=====================
* `#471`_: Fix extra white space in description and SQL fields.
`2.4.0`_ (2022-02-10)
=====================
* `#470`_: Upgrade JS/CSS versions.
`2.3.0`_ (2021-07-24)
=====================
* `#450`_: Added Russian translations.
* `#449`_: Translates expression for duration
`2.2.0`_ (2021-06-14)
=====================
* Updated docs theme to `furo`_
* `#445`_: Added ``EXPLORER_NO_PERMISSION_VIEW`` setting to allow override of the "no permission" view (Fix `#440`_)
* `#444`_: Updated structure of the settings docs (Fix `#443`_)
`2.1.3`_ (2021-05-14)
=====================
* `#442`_: ``GET`` params passed to the fullscreen view (Fix `#433`_)
* `#441`_: Include BOM in CSV export (Fix `#430`_)
`2.1.2`_ (2021-01-19)
=====================
* `#431`_: Fix for hidden SQL panel on a new query
`2.1.1`_ (2021-01-19)
=====================
Mistake in release
`2.1.0`_ (2021-01-13)
=====================
* **BREAKING CHANGE**: ``request`` object now passed to ``EXPLORER_PERMISSION_CHANGE`` and ``EXPLORER_PERMISSION_VIEW`` (`#417`_ to fix `#396`_)
Major Changes
* `#413`_: Static assets now served directly from the application, not CDN. (`#418`_ also)
* `#414`_: Better blacklist checking - Fix `#371`_ and `#412`_
* `#415`_: Fix for MySQL following change for Oracle in `#337`_
Minor Changes
* `#370`_: Get the CSRF cookie name from django instead of a hardcoded value
* `#410`_ and `#416`_: Sphinx docs
* `#420`_: Formatting change in templates
* `#424`_: Collapsable SQL panel
* `#425`_: Ensure a `Query` object contains SQL
`2.0.0`_ (2020-10-09)
=====================
* **BREAKING CHANGE**: #403: Dropping support for EOL `Python 2.7 <https://www.python.org/doc/sunset-python-2/>`_ and `3.5 <https://pythoninsider.blogspot.com/2020/10/python-35-is-no-longer-supported.html>`_
Major Changes
* `#404`_: Add support for Django 3.1 and drop support for (EOL) <2.2
* `#408`_: Refactored the application, updating the URLs to use path and the views into a module
Minor Changes
* `#334`_: Django 2.1 support
* `#337`_: Fix Oracle query failure caused by `TextField` in a group by clause
* `#345`_: Added (some) Chinese translation
* `#366`_: Changes to Travis django versions
* `#372`_: Run queries as atomic requests
* `#382`_: Django 2.2 support
* `#383`_: Typo in the README
* `#385`_: Removed deprecated `render_to_response` usage
* `#386`_: Bump minimum django version to 2.2
* `#387`_: Django 3 support
* `#390`_: README formatting changes
* `#393`_: Added option to install `XlsxWriter` as an extra package
* `#397`_: Bump patch version of django 2.2
* `#406`_: Show some love to the README
* Fix `#341`_: PYC files excluded from build
`1.1.3`_ (2019-09-23)
=====================
* `#347`_: URL-friendly parameter encoding
* `#354`_: Updating dependency reference for Python 3 compatibility
* `#357`_: Include database views in list of tables
* `#359`_: Fix unicode issue when generating migration with py2 or py3
* `#363`_: Do not use "message" attribute on exception
* `#368`_: Update EXPLORER_SCHEMA_EXCLUDE_TABLE_PREFIXES
Minor Changes
* release checklist included in repo
* readme updated with new screenshots
* python dependencies/optional-dependencies updated to latest (six, xlsxwriter, factory-boy, sqlparse)
`1.1.2`_ (2018-08-14)
=====================
* Fix `#269`_
* Fix bug when deleting query
* Fix bug when invalid characters present in Excel worksheet name
Major Changes
* Django 2.0 compatibility
* Improved interface to database connection management
Minor Changes
* Documentation updates
* Load images over same protocol as originating page
`1.1.1`_ (2017-03-21)
=====================
* Fix `#288`_ (incorrect import)
`1.1.0`_ (2017-03-19)
=====================
* **BREAKING CHANGE**: ``EXPLORER_DATA_EXPORTERS`` setting is now a list of tuples instead of a dictionary.
This only affects you if you have customized this setting. This was to preserve ordering of the export buttons in the UI.
* **BREAKING CHANGE**: Values from the database are now escaped by default. Disable this behavior (enabling potential XSS attacks)
with the ``EXPLORER_UNSAFE_RENDERING setting``.
Major Changes
* Django 1.10 and 2.0 compatibility
* Theming & visual updates
* PDF export
* Query-param based authentication (`#254`_)
* Schema built via SQL querying rather than Django app/model introspection. Paves the way for the tool to be pointed at any DB, not just Django DBs
Minor Changes
* Switched from TinyS3 to Boto (will switch to Boto3 in next release)
* Optionally show row numbers in results preview pane
* Full-screen view (icon on top-right of preview pane)
* Moved 'open in playground' to icon on top-right on SQL editor
* Save-only option (does not execute query)
* Show the time that the query was rendered (useful if you've had a tab open a while)
`1.0.0`_ (2016-06-16)
=====================
* **BREAKING CHANGE**: Dropped support for Python 2.6. See ``.travis.yml`` for test matrix.
* **BREAKING CHANGE**: The 'export' methods have all changed. Those these weren't originally designed to be external APIs,
folks have written consuming code that directly called export code.
If you had code that looked like:
``explorer.utils.csv_report(query)``
You will now need to do something like:
``explorer.exporters.get_exporter_class('csv')(query).get_file_output()``
* There is a new export system! v1 is shipping with support for CSV, JSON, and Excel (xlsx). The availablility of these can be configured via the EXPLORER_DATA_EXPORTERS setting.
* `Note` that for Excel export to work, you will need to install ``xlsxwriter`` from ``optional-requirements.txt.``
* Introduced Query History link. Find it towards the top right of a saved query.
* Front end performance improvements and library upgrades.
* Allow non-admins with permission to log into explorer.
* Added a proper test_project for an easier entry-point for contributors, or folks who want to kick the tires.
* Loads of little bugfixes.
`0.9.2`_ (2016-02-02)
=====================
* Fixed readme issue (.1) and ``setup.py`` issue (.2)
`0.9.1`_ (2016-02-01)
=====================
Major changes
* Dropped support for Django 1.6, added support for Django 1.9.
See .travis.yml for test matrix.
* Dropped charted.js & visualization because it didn't work well.
* Client-side pivot tables with pivot.js. This is ridiculously cool!
Minor (but awesome!) changes
* Cmd-/ to comment/uncomment a block of SQL
* Quick 'shortcut' links to the corresponding querylog to more quickly share results.
Look at the top-right of the editor. Also works for playground!
* Prompt for unsaved changes before navigating away
* Support for default parameter values via $$paramName:defaultValue$$
* Optional Celery task for truncating query logs as entries build up
* Display historical average query runtime
* Increased default number of rows from 100 to 1000
* Increased SQL editor size (5 additional visible lines)
* CSS cleanup and streamlining (making better use of foundation)
* Various bugfixes (blacklist not enforced on playground being the big one)
* Upgraded front-end libraries
* Hide Celery-based features if tasks not enabled.
`0.8.0`_ (2015-10-21)
=====================
* Snapshots! Dump the csv results of a query to S3 on a regular schedule.
More details in readme.rst under 'features'.
* Async queries + email! If you have a query that takes a long time to run, execute it in the background and
Explorer will send you an email with the results when they are ready. More details in readme.rst
* Run counts! Explorer inspects the query log to see how many times a query has been executed.
* Column Statistics! Click the ... on top of numeric columns in the results pane to see min, max, avg, sum, count, and missing values.
* Python 3! * Django 1.9!
* Delimiters! Export with delimiters other than commas.
* Listings respect permissions! If you've given permission to queries to non-admins,
they will see only those queries on the listing page.
`0.7.0`_ (2015-02-18)
=====================
* Added search functionality to schema view and explorer view (using list.js).
* Python 2.6 compatibility.
* Basic charts via charted (from Medium via charted.co).
* SQL formatting function.
* Token authentication to retrieve csv version of queries.
* Fixed south_migrations packaging issue.
* Refactored front-end and pulled CSS and JS into dedicated files.
`0.6.0`_ (2014-11-05)
=====================
* Introduced Django 1.7 migrations. See readme.rst for info on how to run South migrations if you are not on Django 1.7 yet.
* Upgraded front-end libraries to latest versions.
* Added ability to grant selected users view permissions on selected queries via the ``EXPLORER_USER_QUERY_VIEWS`` parameter
* Example usage: ``EXPLORER_USER_QUERY_VIEWS = {1: [3,4], 2:[3]}``
* This would grant user with PK 1 read-only access to query with PK=3 and PK=4 and user 2 access to query 3.
* Bugfixes
* Navigating to an explorer URL without the trailing slash now redirects to the intended page (e.g. ``/logs`` -> ``/logs/``)
* Downloading a .csv and subsequently re-executing a query via a keyboard shortcut (cmd+enter) would re-submit the form and re-download the .csv. It now correctly just refreshes the query.
* Django 1.7 compatibility fix
`0.5.1`_ (2014-09-02)
=====================
Bugfixes
* Created_by_user not getting saved correctly
* Content-disposition .csv issue
* Issue with queries ending in ``...like '%...`` clauses
* Change the way customer user model is referenced
* Pseudo-folders for queries. Use "Foo * Ba1", "Foo * Bar2" for query names and the UI will build a little "Foo" pseudofolder for you in the query list.
`0.5.0`_ (2014-06-06)
=====================
* Query logs! Accessible via ``explorer/logs/``. You can look at previously executed queries (so you don't, for instance,
lose that playground query you were working, or have to worry about mucking up a recorded query).
It's quite usable now, and could be used for versioning and reverts in the future. It can be accessed at ``explorer/logs/``
* Actually captures the creator of the query via a ForeignKey relation, instead of just using a Char field.
* Re-introduced type information in the schema helpers.
* Proper relative URL handling after downloading a query as CSV.
* Users with view permissions can use query parameters. There is potential for SQL injection here.
I think about the permissions as being about preventing users from borking up queries, not preventing them from viewing data.
You've been warned.
* Refactored params handling for extra safety in multi-threaded environments.
`0.4.1`_ (2014-02-24)
=====================
* Renaming template blocks to prevent conflicts
`0.4`_ (2014-02-14 `Happy Valentine's Day!`)
============================================
* Templatized columns for easy linking
* Additional security config options for splitting create vs. view permissions
* Show many-to-many relation tables in schema helper
`0.3`_ (2014-01-25)
-------------------
* Query execution time shown in query preview
* Schema helper available as a sidebar in the query views
* Better defaults for sql blacklist
* Minor UI bug fixes
`0.2`_ (2014-01-05)
-------------------
* Support for parameters
* UI Tweaks
* Test coverage
`0.1.1`_ (2013-12-31)
=====================
Bug Fixes
* Proper SQL blacklist checks
* Downloading CSV from playground
`0.1`_ (2013-12-29)
-------------------
Initial Release
.. _0.1: https://github.com/explorerhq/sql-explorer/tree/0.1
.. _0.1.1: https://github.com/explorerhq/sql-explorer/compare/0.1...0.1.1
.. _0.2: https://github.com/explorerhq/sql-explorer/compare/0.1.1...0.2
.. _0.3: https://github.com/explorerhq/sql-explorer/compare/0.2...0.3
.. _0.4: https://github.com/explorerhq/sql-explorer/compare/0.3...0.4
.. _0.4.1: https://github.com/explorerhq/sql-explorer/compare/0.4...0.4.1
.. _0.5.0: https://github.com/explorerhq/sql-explorer/compare/0.4.1...0.5.0
.. _0.5.1: https://github.com/explorerhq/sql-explorer/compare/0.5.0...541148e7240e610f01dd0c260969c8d56e96a462
.. _0.6.0: https://github.com/explorerhq/sql-explorer/compare/0.5.0...0.6.0
.. _0.7.0: https://github.com/explorerhq/sql-explorer/compare/0.6.0...0.7.0
.. _0.8.0: https://github.com/explorerhq/sql-explorer/compare/0.7.0...0.8.0
.. _0.9.1: https://github.com/explorerhq/sql-explorer/compare/0.9.0...0.9.1
.. _0.9.2: https://github.com/explorerhq/sql-explorer/compare/0.9.1...0.9.2
.. _1.0.0: https://github.com/explorerhq/sql-explorer/compare/0.9.2...1.0.0
.. _1.1.0: https://github.com/explorerhq/sql-explorer/compare/1.0.0...1.1.1
.. _1.1.1: https://github.com/explorerhq/sql-explorer/compare/1.1.0...1.1.1
.. _1.1.2: https://github.com/explorerhq/sql-explorer/compare/1.1.1...1.1.2
.. _1.1.3: https://github.com/explorerhq/sql-explorer/compare/1.1.2...1.1.3
.. _2.0.0: https://github.com/explorerhq/sql-explorer/compare/1.1.3...2.0
.. _2.1.0: https://github.com/explorerhq/sql-explorer/compare/2.0...2.1.0
.. _2.1.1: https://github.com/explorerhq/sql-explorer/compare/2.1.0...2.1.1
.. _2.1.2: https://github.com/explorerhq/sql-explorer/compare/2.1.1...2.1.2
.. _2.1.3: https://github.com/explorerhq/sql-explorer/compare/2.1.2...2.1.3
.. _2.2.0: https://github.com/explorerhq/sql-explorer/compare/2.1.3...2.2.0
.. _2.3.0: https://github.com/explorerhq/sql-explorer/compare/2.2.0...2.3.0
.. _2.4.0: https://github.com/explorerhq/sql-explorer/compare/2.3.0...2.4.0
.. _2.4.1: https://github.com/explorerhq/sql-explorer/compare/2.4.0...2.4.1
.. _2.4.2: https://github.com/explorerhq/sql-explorer/compare/2.4.1...2.4.2
.. _2.5.0: https://github.com/explorerhq/sql-explorer/compare/2.4.2...2.5.0
.. _3.0: https://github.com/explorerhq/sql-explorer/compare/2.5.0...3.0
.. _3.0.1: https://github.com/explorerhq/sql-explorer/compare/3.0...3.0.1
.. _3.1.0: https://github.com/explorerhq/sql-explorer/compare/3.0.1...3.1.0
.. _3.1.1: https://github.com/explorerhq/sql-explorer/compare/3.1.0...3.1.1
.. _3.2.0: https://github.com/explorerhq/sql-explorer/compare/3.1.1...3.2.0
.. _3.2.1: https://github.com/explorerhq/sql-explorer/compare/3.2.0...3.2.1
.. _4.0.0.beta1: https://github.com/explorerhq/sql-explorer/compare/3.2.1...4.0.0.beta1
.. _4.0.2: https://github.com/explorerhq/sql-explorer/compare/4.0.0...4.0.2
.. _4.1.0: https://github.com/explorerhq/sql-explorer/compare/4.0.2...4.1.0
.. _4.2.0: https://github.com/explorerhq/sql-explorer/compare/4.1.0...4.2.0
.. _4.3.0: https://github.com/explorerhq/sql-explorer/compare/4.2.0...4.3.0
.. _5.0.0: https://github.com/explorerhq/sql-explorer/compare/4.3.0...5.0.0
.. _5.0.1: https://github.com/explorerhq/sql-explorer/compare/5.0.0...5.0.1
.. _5.0.2: https://github.com/explorerhq/sql-explorer/compare/5.0.1...5.0.2
.. _5.1.0: https://github.com/explorerhq/sql-explorer/compare/5.0.2...5.1.0
.. _5.1.1: https://github.com/explorerhq/sql-explorer/compare/5.1.0...5.1.1
.. _5.2.0: https://github.com/explorerhq/sql-explorer/compare/5.1.1...5.2.0
.. _5.3b1: https://github.com/explorerhq/sql-explorer/compare/5.2.0...5.3b1
.. _#254: https://github.com/explorerhq/sql-explorer/pull/254
.. _#334: https://github.com/explorerhq/sql-explorer/pull/334
.. _#337: https://github.com/explorerhq/sql-explorer/pull/337
.. _#345: https://github.com/explorerhq/sql-explorer/pull/345
.. _#347: https://github.com/explorerhq/sql-explorer/pull/347
.. _#354: https://github.com/explorerhq/sql-explorer/pull/354
.. _#357: https://github.com/explorerhq/sql-explorer/pull/357
.. _#359: https://github.com/explorerhq/sql-explorer/pull/359
.. _#363: https://github.com/explorerhq/sql-explorer/pull/363
.. _#366: https://github.com/explorerhq/sql-explorer/pull/366
.. _#368: https://github.com/explorerhq/sql-explorer/pull/368
.. _#370: https://github.com/explorerhq/sql-explorer/pull/370
.. _#372: https://github.com/explorerhq/sql-explorer/pull/372
.. _#382: https://github.com/explorerhq/sql-explorer/pull/382
.. _#383: https://github.com/explorerhq/sql-explorer/pull/383
.. _#385: https://github.com/explorerhq/sql-explorer/pull/385
.. _#386: https://github.com/explorerhq/sql-explorer/pull/386
.. _#387: https://github.com/explorerhq/sql-explorer/pull/387
.. _#390: https://github.com/explorerhq/sql-explorer/pull/390
.. _#393: https://github.com/explorerhq/sql-explorer/pull/393
.. _#397: https://github.com/explorerhq/sql-explorer/pull/397
.. _#404: https://github.com/explorerhq/sql-explorer/pull/404
.. _#406: https://github.com/explorerhq/sql-explorer/pull/406
.. _#408: https://github.com/explorerhq/sql-explorer/pull/408
.. _#410: https://github.com/explorerhq/sql-explorer/pull/410
.. _#413: https://github.com/explorerhq/sql-explorer/pull/413
.. _#414: https://github.com/explorerhq/sql-explorer/pull/414
.. _#416: https://github.com/explorerhq/sql-explorer/pull/416
.. _#415: https://github.com/explorerhq/sql-explorer/pull/415
.. _#417: https://github.com/explorerhq/sql-explorer/pull/417
.. _#418: https://github.com/explorerhq/sql-explorer/pull/418
.. _#420: https://github.com/explorerhq/sql-explorer/pull/420
.. _#424: https://github.com/explorerhq/sql-explorer/pull/424
.. _#425: https://github.com/explorerhq/sql-explorer/pull/425
.. _#441: https://github.com/explorerhq/sql-explorer/pull/441
.. _#442: https://github.com/explorerhq/sql-explorer/pull/442
.. _#444: https://github.com/explorerhq/sql-explorer/pull/444
.. _#445: https://github.com/explorerhq/sql-explorer/pull/445
.. _#449: https://github.com/explorerhq/sql-explorer/pull/449
.. _#450: https://github.com/explorerhq/sql-explorer/pull/450
.. _#470: https://github.com/explorerhq/sql-explorer/pull/470
.. _#471: https://github.com/explorerhq/sql-explorer/pull/471
.. _#475: https://github.com/explorerhq/sql-explorer/pull/475
.. _#478: https://github.com/explorerhq/sql-explorer/pull/478
.. _#481: https://github.com/explorerhq/sql-explorer/pull/481
.. _#484: https://github.com/explorerhq/sql-explorer/pull/484
.. _#488: https://github.com/explorerhq/sql-explorer/pull/488
.. _#494: https://github.com/explorerhq/sql-explorer/pull/494
.. _#496: https://github.com/explorerhq/sql-explorer/pull/496
.. _#497: https://github.com/explorerhq/sql-explorer/pull/497
.. _#498: https://github.com/explorerhq/sql-explorer/pull/498
.. _#500: https://github.com/explorerhq/sql-explorer/pull/500
.. _#501: https://github.com/explorerhq/sql-explorer/pull/501
.. _#504: https://github.com/explorerhq/sql-explorer/pull/504
.. _#505: https://github.com/explorerhq/sql-explorer/pull/505
.. _#506: https://github.com/explorerhq/sql-explorer/pull/506
.. _#508: https://github.com/explorerhq/sql-explorer/pull/508
.. _#515: https://github.com/explorerhq/sql-explorer/pull/515
.. _#517: https://github.com/explorerhq/sql-explorer/pull/517
.. _#519: https://github.com/explorerhq/sql-explorer/pull/519
.. _#520: https://github.com/explorerhq/sql-explorer/pull/520
.. _#521: https://github.com/explorerhq/sql-explorer/pull/521
.. _#522: https://github.com/explorerhq/sql-explorer/pull/522
.. _#523: https://github.com/explorerhq/sql-explorer/pull/523
.. _#524: https://github.com/explorerhq/sql-explorer/pull/524
.. _#528: https://github.com/explorerhq/sql-explorer/pull/528
.. _#529: https://github.com/explorerhq/sql-explorer/pull/529
.. _#533: https://github.com/explorerhq/sql-explorer/pull/533
.. _#537: https://github.com/explorerhq/sql-explorer/pull/537
.. _#539: https://github.com/explorerhq/sql-explorer/pull/539
.. _#544: https://github.com/explorerhq/sql-explorer/pull/544
.. _#562: https://github.com/explorerhq/sql-explorer/pull/562
.. _#565: https://github.com/explorerhq/sql-explorer/pull/565
.. _#566: https://github.com/explorerhq/sql-explorer/pull/566
.. _#571: https://github.com/explorerhq/sql-explorer/pull/571
.. _#594: https://github.com/explorerhq/sql-explorer/pull/594
.. _#647: https://github.com/explorerhq/sql-explorer/pull/647
.. _#643: https://github.com/explorerhq/sql-explorer/pull/643
.. _#644: https://github.com/explorerhq/sql-explorer/pull/644
.. _#645: https://github.com/explorerhq/sql-explorer/pull/645
.. _#648: https://github.com/explorerhq/sql-explorer/pull/648
.. _#649: https://github.com/explorerhq/sql-explorer/pull/649
.. _#635: https://github.com/explorerhq/sql-explorer/pull/635
.. _#636: https://github.com/explorerhq/sql-explorer/pull/636
.. _#555: https://github.com/explorerhq/sql-explorer/pull/555
.. _#651: https://github.com/explorerhq/sql-explorer/pull/651
.. _#659: https://github.com/explorerhq/sql-explorer/pull/659
.. _#662: https://github.com/explorerhq/sql-explorer/pull/662
.. _#660: https://github.com/explorerhq/sql-explorer/pull/660
.. _#664: https://github.com/explorerhq/sql-explorer/pull/664
.. _#269: https://github.com/explorerhq/sql-explorer/issues/269
.. _#288: https://github.com/explorerhq/sql-explorer/issues/288
.. _#341: https://github.com/explorerhq/sql-explorer/issues/341
.. _#371: https://github.com/explorerhq/sql-explorer/issues/371
.. _#396: https://github.com/explorerhq/sql-explorer/issues/396
.. _#412: https://github.com/explorerhq/sql-explorer/issues/412
.. _#430: https://github.com/explorerhq/sql-explorer/issues/430
.. _#431: https://github.com/explorerhq/sql-explorer/issues/431
.. _#433: https://github.com/explorerhq/sql-explorer/issues/433
.. _#440: https://github.com/explorerhq/sql-explorer/issues/440
.. _#443: https://github.com/explorerhq/sql-explorer/issues/443
.. _#477: https://github.com/explorerhq/sql-explorer/issues/477
.. _#483: https://github.com/explorerhq/sql-explorer/issues/483
.. _#490: https://github.com/explorerhq/sql-explorer/issues/490
.. _#492: https://github.com/explorerhq/sql-explorer/issues/492
.. _#592: https://github.com/explorerhq/sql-explorer/issues/592
.. _#609: https://github.com/explorerhq/sql-explorer/issues/609
.. _#610: https://github.com/explorerhq/sql-explorer/issues/610
.. _#612: https://github.com/explorerhq/sql-explorer/issues/612
.. _#616: https://github.com/explorerhq/sql-explorer/issues/616
.. _#618: https://github.com/explorerhq/sql-explorer/issues/618
.. _#619: https://github.com/explorerhq/sql-explorer/issues/619
.. _#631: https://github.com/explorerhq/sql-explorer/issues/631
.. _#633: https://github.com/explorerhq/sql-explorer/issues/633
.. _#567: https://github.com/explorerhq/sql-explorer/issues/567
.. _#654: https://github.com/explorerhq/sql-explorer/issues/654
.. _#653: https://github.com/explorerhq/sql-explorer/issues/653
.. _#675: https://github.com/explorerhq/sql-explorer/issues/675
.. _furo: https://github.com/pradyunsg/furo
================================================
FILE: LICENSE
================================================
* All content that resides under the "explorer/ee/" directory of this repository is licensed under the license defined
in "explorer/ee/LICENSE".
* Content outside of the above mentioned directory is provided under the "MIT" license as defined below.
** The MIT License (MIT) **
Copyright (c) 2024, SQL Explorer, Inc
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: MANIFEST.in
================================================
recursive-include explorer *
recursive-exclude * *.pyc __pycache__ .DS_Store
recursive-include requirements *
include package.json
include vite.config.mjs
include README.rst
================================================
FILE: README.rst
================================================
.. image:: https://readthedocs.org/projects/django-sql-explorer/badge/?version=latest
:target: https://django-sql-explorer.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status
.. image:: http://img.shields.io/pypi/v/django-sql-explorer.svg?style=flat-square
:target: https://pypi.python.org/pypi/django-sql-explorer/
:alt: Latest Version
.. image:: http://img.shields.io/pypi/dm/django-sql-explorer.svg?style=flat-square
:target: https://pypi.python.org/pypi/django-sql-explorer/
:alt: Downloads
.. image:: http://img.shields.io/pypi/l/django-sql-explorer.svg?style=flat-square
:target: https://pypi.python.org/pypi/django-sql-explorer/
:alt: License
SQL Explorer
============
* `Official Website <https://www.sqlexplorer.io/>`_
* `Live Demo <https://demo.sqlexplorer.io/>`_
* `Documentation <https://django-sql-explorer.readthedocs.io/en/latest/>`_
Video Tour
----------
.. |inline-image| image:: https://sql-explorer.s3.amazonaws.com/video-thumbnail.png
:target: https://sql-explorer.s3.amazonaws.com/Sql+Explorer+5.mp4
:height: 10em
|inline-image|
Quick Start
-----------
Included is a complete test project that you can use to kick the tires.
1. Run ``docker compose up``
2. Navigate to 127.0.0.1:8000/explorer/
3. log in with admin/admin
4. Begin exploring!
This will also run a Vite dev server with hot reloading for front-end changes.
About
-----
SQL Explorer aims to make the flow of data between people fast,
simple, and confusion-free. It is a Django-based application that you
can add to an existing Django site, or use as a standalone business
intelligence tool. It will happily connect to any SQL database that
`Django supports <https://docs.djangoproject.com/en/5.0/ref/databases/>`_
as well as user-uploaded CSV, JSON, or SQLite databases.
Quickly write and share SQL queries in a simple, usable SQL editor,
view the results in the browser, and keep the information flowing.
Add an OpenAI (or other provider) API key and get an LLM-powered
SQL assistant that can help write and debug queries. The assistant
will automatically add relevant context and schema into the underlying
LLM prompt.
SQL Explorer values simplicity, intuitive use, unobtrusiveness,
stability, and the principle of least surprise. The project is MIT
licensed, and pull requests are welcome.
Some key features include:
- Support for multiple connections, admin configured or user-provided.
- Users can upload and immediately query JSON or CSV files.
- AI-powered SQL assistant
- Quick access to schema information to make querying easier
(including autocomplete)
- Ability to snapshot queries on a regular schedule, capturing changing data
- Query history and logs
- Quick in-browser statistics, pivot tables, and scatter-plots (saving
a trip to Excel for simple analyses)
- Parameterized queries that automatically generate a friendly UI for
users who don't know SQL
- A playground area for quickly running ad-hoc queries
- Send query results via email
- Saved queries can be exposed as a quick-n-dirty JSON API if desired
- ...and more!
Screenshots
-----------
**Writing a query and viewing the schema helper**
.. image:: https://sql-explorer.s3.amazonaws.com/5.0-query-with-schema.png
------------------
**Using the SQL AI Assistant**
.. image:: https://sql-explorer.s3.amazonaws.com/5.0-assistant.png
------------------
**Viewing all queries**
.. image:: https://sql-explorer.s3.amazonaws.com/5.0-query-list.png
------------------
**Query results w/ stats summary**
.. image:: https://sql-explorer.s3.amazonaws.com/5.0-query-results.png
------------------
**Pivot in browser**
.. image:: https://sql-explorer.s3.amazonaws.com/5.0-pivot.png
------------------
**View logs**
.. image:: https://sql-explorer.s3.amazonaws.com/5.0-querylogs.png
================================================
FILE: docker-compose.yml
================================================
services:
web:
build: .
command: ["python", "manage.py", "runserver", "0.0.0.0:8000"]
ports:
- "8000:8000"
- "5173:5173"
volumes:
- .:/app
- node_modules:/app/node_modules
environment:
- DJANGO_SETTINGS_MODULE=test_project.settings
volumes:
node_modules:
================================================
FILE: docs/Makefile
================================================
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DjangoSQLExplorer.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DjangoSQLExplorer.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/DjangoSQLExplorer"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/DjangoSQLExplorer"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
================================================
FILE: docs/_static/.directory
================================================
================================================
FILE: docs/_templates/.directory
================================================
================================================
FILE: docs/conf.py
================================================
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import datetime
import os
import sys
import explorer
sys.path.insert(0, os.path.abspath("../"))
sys.path.insert(0, os.path.abspath("."))
# -- Project information -----------------------------------------------------
current_year = datetime.datetime.now().year
project = "Django SQL Explorer"
copyright = f"2016-{current_year}, SQL Explorer"
author = "Chris Clark"
version = explorer.get_version(True)
# The full version, including alpha/beta/rc tags.
release = explorer.__version__
# -- General configuration ---------------------------------------------------
master_doc = "index"
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
"sphinx_copybutton",
"sphinxext.opengraph",
"sphinx.ext.autodoc",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = "furo"
html_theme_options = {
"navigation_with_keys": True,
}
pygments_style = "sphinx"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
================================================
FILE: docs/dependencies.rst
================================================
Dependencies
============
An effort has been made to keep the number of dependencies to a
minimum.
Python
------
============================================================ ======= ================
Name Version License
============================================================ ======= ================
`sqlparse <https://github.com/andialbrecht/sqlparse/>`_ 0.4.0 BSD
`requests <https://requests.readthedocs.io/en/latest/>`_ 2.2.0 Apache 2.0
============================================================ ======= ================
- sqlparse is used for SQL formatting
- requests is used for anonymous usage tracking
**Python - Optional Dependencies**
==================================================================== =========== =============
Name Version License
==================================================================== =========== =============
`celery <http://www.celeryproject.org/>`_ >=3.1,<4 BSD
`django-celery <http://www.celeryproject.org/>`_ >=3.3.1 BSD
`Factory Boy <https://github.com/rbarrois/factory_boy>`_ >=3.1.0 MIT
`xlsxwriter <http://xlsxwriter.readthedocs.io/>`_ >=1.3.6 BSD
`boto <https://github.com/boto/boto>`_ >=2.49 MIT
==================================================================== =========== =============
- Factory Boy is required for tests
- celery is required for the 'email' feature, and for snapshots
- boto is required for snapshots
- xlsxwriter is required for Excel export (csv still works fine without it)
JavaScript & CSS
----------------
Please see package.json for the full list of JavaScript dependencies.
Vite builds the JS and CSS bundles for SQL Explorer.
The bundle for the SQL editor is fairly large at ~400kb, due primarily to CodeMirror. There is opportunity to reduce this by removing jQuery, which we hope to do in a future release.
The built front-end files are distributed in the PyPi release (and will be found by collectstatic). Instructions for building the front-end files are in :doc:`install`.
================================================
FILE: docs/development.rst
================================================
Running Locally (quick start)
-----------------------------
Whether you have cloned the repo, or installed via pip, included is a test_project that you can use to kick the tires.
Run:
``docker compose up``
You can now navigate to 127.0.0.1:8000/explorer/, log in with admin/admin, and begin exploring!
Installing From Source
----------------------
If you want to install SQL Explorer from source (e.g. not from the built PyPi package),
into an existing project, you can do so by cloning the repository and following the usual
:doc:`install` instructions, and then additionally building the front-end dependencies:
::
nvm install
nvm use
npm install
npm run build
The front-end assets will be built and placed in the /static/ folder
and collected properly by your Django installation during the `collect static`
phase. Copy the /explorer directory into site-packages and you're ready to go.
Tests
-----
Install the dev requirements:
``pip install -r requirements/dev.txt``
And then:
``python manage.py test --settings=explorer.tests.settings``
Or with coverage:
``coverage run --source='.' manage.py test --settings=explorer.tests.settings``
``coverage combine``
``coverage report``
================================================
FILE: docs/features.rst
================================================
Features
========
SQL Assistant
-------------
- Built in integration with OpenAI (or the LLM of your choosing)
to quickly get help with your query, with relevant schema
automatically injected into the prompt.
- The assistant tries hard to get relevant context into the prompt to the LLM, alongside your explicit request. You
can choose tables to include explicitly (and any tables you are reference in your SQL you will see get included as
well). When a table is "included", the prompt will include the schema of the table, 3 sample rows, any Table
Annotations you have added, and any designated "few shot examples". More on each of those below.
- Table Annotations: Write persistent table annotations with descriptive information that will get injected into the
prompt for the assistant. For example, if a table is commonly joined to another table through a non-obvious foreign
key, you can tell the assistant about it in plain english, as an annotation to that table. Every time that table is
deemed 'relevant' to an assistant request, that annotation will be included alongside the schema and sample data.
- Few-shot examples: Using the small checkbox on the bottom-right of any saved query, you can designate queries as
"Assistant Examples". When making an assistant request, the 'included tables' are intersected with tables referenced
by designated Example queries, and those queries are injected into the prompt, and the LLM is told that that these
are good reference queries.
Database Support
----------------
- Supports MySql, postgres (and, by extension, pg-connection-compatible DBs like Redshift), SQLite,
Oracle, MS SQL Server, MariaDB, and Snowflake
- Note for Snowflake or SQL Server, you will need to install the relevant Django connection package
(e.g. https://pypi.org/project/django-snowflake/, https://github.com/microsoft/mssql-django)
- Also supports ad-hoc data sources by uploading JSON, CSV, or SQLite files directly.
Snapshots
---------
- Tick the 'snapshot' box on a query, and Explorer will upload a
.csv snapshot of the query results to S3. Configure the snapshot
frequency via a celery cron task, e.g. for daily at 1am
(see test_project/celery_config.py for an example of this, along with test_project/__init__.py):
.. code-block:: python
app.conf.beat_schedule = {
"explorer.tasks.snapshot_queries": {
"task": "explorer.tasks.snapshot_queries",
"schedule": crontab(hour="1", minute="0")
},
}
- Requires celery, obviously. Also uses boto3. All
of these deps are optional and can be installed with
``pip install "django-sql-explorer[snapshots]"``
- The checkbox for opting a query into a snapshot is ALL THE WAY
on the bottom of the query view (underneath the results table).
- You must also have the setting ``EXPLORER_TASKS_ENABLED`` enabled.
Email query results
-------------------
- Click the email icon in the query listing view, enter an email
address, and the query results (zipped .csv) will be sent to you
asynchronously. Very handy for long-running queries.
- You must also have the setting ``EXPLORER_TASKS_ENABLED`` enabled.
Parameterized Queries
---------------------
- Use $$foo$$ in your queries and Explorer will build a UI to fill
out parameters. When viewing a query like ``SELECT * FROM table
WHERE id=$$id$$``, Explorer will generate UI for the ``id``
parameter.
- Parameters are stashed in the URL, so you can share links to
parameterized queries with colleagues
- Use ``$$paramName:defaultValue$$`` to provide default values for the
parameters.
- Use ``$$paramName|label$$`` to add a label (e.g. "User ID") to the
parameter.
- You can combine both a default and label to your parameter but you must
start with the label: ``$$paramName|label:defaultValue$$``.
Schema Helper
-------------
- ``/explorer/schema/<connection-alias>`` renders a list of your table
and column names + types that you can refer to while writing
queries. Apps can be excluded from this list so users aren't
bogged down with tons of irrelevant tables. See settings
documentation below for details.
- Autocomplete for table and column names in the Codemirror SQL editor
- This is available quickly as a sidebar helper while composing
queries (see screenshot)
- Quick search for the tables you are looking for. Just start
typing!
- Explorer uses Django DB introspection to generate the
schema. This can sometimes be slow, as it issues a separate
query for each table it introspects. Therefore, once generated,
Explorer caches the schema information. There is also the option
to generate the schema information asynchronously, via Celery. To
enable this, make sure Celery is installed and configured, and
set ``EXPLORER_ENABLE_TASKS`` and ``EXPLORER_ASYNC_SCHEMA`` to
``True``.
Template Columns
----------------
- Let's say you have a query like ``SELECT id, email FROM user`` and
you'd like to quickly drill through to the profile page for each
user in the result. You can create a ``template`` column to do
just that.
- Just set up a template column in your settings file:
.. code-block:: python
EXPLORER_TRANSFORMS = [
('user', '<a href="https://yoursite.com/profile/{0}/">{0}</a>')
]
- And change your query to ``SELECT id AS "user", email FROM
user``. Explorer will match the ``user`` column alias to the
transform and merge each cell in that column into the template
string. `Cool!`
- Note you **must** set ``EXPLORER_UNSAFE_RENDERING`` to ``True`` if you
want to see rendered HTML (vs string literals) in the output.
This will globally un-escape query results in the preview pane. E.g.
any queries that return HTML will render as HTML in the preview pane.
This could have cross-site scripting implications if you don't trust
the data source you are querying.
Pivot Table
-----------
- Go to the Pivot tab on query results to use the in-browser pivot
functionality (provided by Pivottable JS).
- Hit the link icon on the top right to get a URL to recreate the
exact pivot setup to share with colleagues.
- Download the pivot view as a CSV.
Displaying query results as charts
----------------------------------
If the results table has numeric columns, they can be displayed in a bar chart. The first column will always be used
as the x-axis labels. This is quite basic, but can be useful for quick visualization. Charts (if enabled) will render
for query results with ten or fewer numeric columns. With more series than that, the charts become a hot mess quickly.
To enable this feature, set ``EXPLORER_CHARTS_ENABLED`` setting to ``True`` and install the plotting library
``matplotlib`` with:
.. code-block:: console
pip install "django-sql-explorer[charts]"
This will add the "Line chart" and "Bar chart" tabs alongside the "Preview" and the "Pivot" tabs in the query results
view.
Query Logs
----------
- Explorer will save a snapshot of every query you execute so you
can recover lost ad-hoc queries, and see what you've been
querying.
- This also serves as cheap-and-dirty versioning of Queries, and
provides the 'run count' property and average duration in
milliseconds, by aggregating the logs.
- You can also quickly share playground queries by copying the
link to the playground's query log record -- look on the top
right of the sql editor for the link icon.
- If Explorer gets a lot of use, the logs can get
beefy. explorer.tasks contains the 'truncate_querylogs' task
that will remove log entries older than <days> (30 days and
older in the example below).
.. code-block:: python
app.conf.beat_schedule = {
"explorer.tasks.truncate_querylogs": {
"task": "explorer.tasks.truncate_querylogs",
"schedule": crontab(hour="1", minute="10"),
"kwargs": {"days": 30}
}
}
Multiple Connections
--------------------
- Have data in more than one database? No problemo. Just set up
multiple Django database connections, register them with
Explorer, and you can write, save, and view queries against all
of your different data sources. Compatible with any database
support by Django. Note that the target database does *not* have
to contain any Django schema, or be related to Django in any
way. See connections.py for more documentation on
multi-connection setup.
- SQL Explorer also supports user-provided connections in the form
of standard database connection details, or uploading CSV, JSON or SQLite
files.
File Uploads
------------
Upload CSV or JSON files, or SQLite databases to immediately create connections for querying.
**How it works**
1. Your file is uploaded to the web server. For CSV files, the first row is assumed to be a header.
2. It is read into a Pandas dataframe. Many fields end up as strings that are in fact numeric or datetimes.
3. During this step, if it is a json file, the json is 'normalized'. E.g. nested objects are flattened.
4. A customer parser runs type-detection on each column for richer typer information.
5. The dataframe is coerced to these more accurate types.
6. The dataframe is written to a SQLite file, which is present on the server, and uploaded to S3.
7. The SQLite database file will be named <filename>_<userid>.db to prevent conflicts if different users uploaded files
with the same name.
8. The SQLite database is added as a new connection to SQL Explorer and is available for querying just like any
other data source.
9. If the SQLite file is not available locally, it will be pulled on-demand from S3 to the app server when needed.
10. Local SQLite files are periodically cleaned up by a recurring task after (by default) 7 days of inactivity.
Note that if the upload is a SQLite database, steps 2-5 are skipped and the database is simply uploaded to S3 and made
available for querying.
**Adding tables to uploads**
You can also append uploaded files to previously uploaded data sources. For example, if you had a
'customers.csv' file and an 'orders.csv' file, you could upload customers.csv and create a new data source. You can
then go back and upload orders.csv with the 'Append' drop-down set to your newly-created customers database, and you
will have a resulting SQLite database connection with both tables available to be queried together. If you were to
upload a new 'orders.csv' and append it to customers, the table 'orders' would be *fully replaced* with the new file.
**File formats**
- Supports well-formed .csv, and .json files. Also supports .json files where each line of the file is a separate json
object. See /explorer/tests/json/ in the source for examples of what is supported.
- Supports SQLite files with a .db or .sqlite extension. The validity of the SQLite file is not fully checked until
a query is attempted.
**Configuration**
- See the 'User uploads' section of :doc:`settings` for configuration details.
Power tips
----------
- On the query listing page, focus gets set to a search box so you
can just navigate to ``/explorer`` and start typing the name of your
query to find it.
- Quick search also works after hitting "Show Schema" on a query
view.
- Command+Enter and Ctrl+Enter will execute a query when typing in
the SQL editor area.
- Cmd+Shift+F (Windows: Ctrl+Shift+F) to format the SQL in the editor.
- Use the Query Logs feature to share one-time queries that aren't
worth creating a persistent query for. Just run your SQL in the
playground, then navigate to ``/logs`` and share the link
(e.g. ``/explorer/play/?querylog_id=2428``)
- Click the 'history' link towards the top-right of a saved query
to filter the logs down to changes to just that query.
- If you need to download a query as something other than csv but
don't want to globally change delimiters via
``settings.EXPLORER_CSV_DELIMETER``, you can use
``/query/download?delim=|`` to get a pipe (or whatever) delimited
file. For a tab-delimited file, use ``delim=tab``. Note that the
file extension will remain .csv
- If a query is taking a long time to run (perhaps timing out) and
you want to get in there to optimize it, go to
``/query/123/?show=0``. You'll see the normal query detail page, but
the query won't execute.
- Set env vars for ``EXPLORER_TOKEN_AUTH_ENABLED=TRUE`` and
``EXPLORER_TOKEN=<SOME TOKEN>`` and you have an instant data
API. Just:
.. code-block:: console
curl --header "X-API-TOKEN: <TOKEN>" https://www.your-site.com/explorer/<QUERY_ID>/stream?format=csv
You can also pass the token with a query parameter like this:
.. code-block:: console
curl https://www.your-site.com/explorer/<QUERY_ID>/stream?format=csv&token=<TOKEN>
Security
--------
- It's recommended you setup read-only roles for each of your database
connections and only use these particular connections for your queries
through the ``EXPLORER_CONNECTIONS`` setting -- or set up userland
connections via DatabaseConnections in the Django admin, or the SQL
Explorer front-end.
- SQL Explorer supports three different permission checks for users of
the tool. Users passing the ``EXPLORER_PERMISSION_CHANGE`` test can
create, edit, delete, and execute queries. Users who do not pass
this test but pass the ``EXPLORER_PERMISSION_VIEW`` test can only
execute queries. Other users cannot access any part of
SQL Explorer. Both permission groups are set to is_staff by default
and can be overridden in your settings file. Lastly, the permission
``EXPLORER_PERMISSION_CONNECTIONS`` controls which users can manage
connections via the UI (if enabled). This is also set to is_staff by
default.
- Enforces a SQL blacklist so destructive queries don't get
executed (delete, drop, alter, update etc). This is not
a substitute for using a readonly connection -- but is better
than nothing for certain use cases where a readonly connection
may not be available.
================================================
FILE: docs/history.rst
================================================
.. include:: ../HISTORY.rst
================================================
FILE: docs/index.rst
================================================
.. rstcheck: ignore-next-code-block
.. Django SQL Explorer documentation master file, created by
sphinx-quickstart on Thu Oct 15 17:39:19 2020.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
###################
Django SQL Explorer
###################
.. include:: ../README.rst
.. toctree::
:maxdepth: 2
:caption: Contents:
features.rst
install.rst
development.rst
settings.rst
dependencies.rst
history.rst
================================================
FILE: docs/install.rst
================================================
Install
=======
* Requires Python 3.10 or higher.
* Requires Django 3.2 or higher.
Set up a Django project with the following:
.. code-block:: shell-session
$ pip install django
$ django-admin startproject project
More information in the `django tutorial <https://docs.djangoproject.com/en/3.1/intro/tutorial01/>`_.
Install with pip from pypi:
.. code-block:: shell-session
$ pip install django-sql-explorer
Take a look at available ``extras``
Add to your ``INSTALLED_APPS``, located in the ``settings.py`` file in your project folder:
.. code-block:: python
:emphasize-lines: 3
INSTALLED_APPS = (
'explorer',
)
Add the following to your urls.py (all Explorer URLs are restricted
via the ``EXPLORER_PERMISSION_VIEW`` and ``EXPLORER_PERMISSION_CHANGE``
settings. See Settings section below for further documentation.):
.. code-block:: python
:emphasize-lines: 5
from django.urls import path, include
urlpatterns = [
path('explorer/', include('explorer.urls')),
]
Run migrate to create the tables:
``python manage.py migrate``
Create a superuser:
``python manage.py createsuperuser``
And run the server:
``python manage.py runserver``
You can now browse to http://127.0.0.1:8000/explorer/. Add a database connection at /explorer/connections/new/, and you
are ready to start exploring! If you have a database in your settings.DATABASES you would like to query, you can create
a connection with the same alias and name and set the Engine to "Django Database".
Note that Explorer expects STATIC_URL to be set appropriately. This isn't a problem
with vanilla Django setups, but if you are using e.g. Django Storages with S3, you
must set your STATIC_URL to point to your S3 bucket (e.g. s3_bucket_url + '/static/')
AI SQL Assistant
----------------
To enable AI features, you must install the OpenAI SDK and Tiktoken library from
requirements/optional.txt. By default the Assistant is configured to use OpenAI and
the `gpt-4-0125-preview` model. To use those settings, set an OpenAI API token in
your project's settings.py file:
``EXPLORER_AI_API_KEY = 'your_openai_api_key'``
Or, more likely:
``EXPLORER_AI_API_KEY = os.environ.get("OPENAI_API_KEY")``
If you would prefer to use a different provider and/or different model, you can
also override the AI API URL root and default model. For example, this would configure
the Assistant to use OpenRouter and Mixtral 8x7B Instruct:
.. code-block:: python
:emphasize-lines: 5
EXPLORER_ASSISTANT_MODEL = {"name": "mistralai/mixtral-8x7b-instruct:nitro",
"max_tokens": 32768})
EXPLORER_ASSISTANT_BASE_URL = "https://openrouter.ai/api/v1"
EXPLORER_AI_API_KEY = os.environ.get("OPENROUTER_API_KEY")
Other Parameters
----------------
The default behavior when viewing a parameterized query is to autorun the associated
SQL with the default parameter values. This may perform poorly and you may want
a chance for your users to review the parameters before running. If so you may add
the following setting which will allow the user to view the query and adjust any
parameters before hitting "Save & Run"
.. code-block:: python
EXPLORER_AUTORUN_QUERY_WITH_PARAMS = False
There are a handful of features (snapshots, emailing queries) that
rely on Celery and the dependencies in optional-requirements.txt. If
you have Celery installed, set ``EXPLORER_TASKS_ENABLED=True`` in your
settings.py to enable these features.
Installing From Source
----------------------
Because the front-end assets must be built, installing SQL Explorer via pip
from github is not supported. The package will be installed, but the front-end
assets will be missing and will not be able to be built, as the necessary
configuration files are not included when github builds the wheel for pip.
To run from source, clone the repository and follow the :doc:`development`
instructions.
================================================
FILE: docs/make.bat
================================================
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd
================================================
FILE: docs/requirements.txt
================================================
django-sql-explorer>=2.0
furo
Sphinx>4
sphinx-copybutton
sphinxext-opengraph
================================================
FILE: docs/settings.rst
================================================
********
Settings
********
Here are all of the available settings with their default values.
SQL Blacklist
*************
Disallowed words in SQL queries to prevent destructive actions.
.. code-block:: python
EXPLORER_SQL_BLACKLIST = (
# DML
'COMMIT',
'DELETE',
'INSERT',
'MERGE',
'REPLACE',
'ROLLBACK',
'SET',
'START',
'UPDATE',
'UPSERT',
# DDL
'ALTER',
'CREATE',
'DROP',
'RENAME',
'TRUNCATE',
# DCL
'GRANT',
'REVOKE',
)
Default rows
************
The number of rows to show by default in the preview pane.
.. code-block:: python
EXPLORER_DEFAULT_ROWS = 1000
Include table prefixes
**********************
If not ``None``, show schema only for tables starting with these prefixes. "Wins" if in conflict with ``EXCLUDE``
.. code-block:: python
EXPLORER_SCHEMA_INCLUDE_TABLE_PREFIXES = None # shows all tables
Exclude table prefixes
**********************
Don't show schema for tables starting with these prefixes, in the schema helper.
.. code-block:: python
EXPLORER_SCHEMA_EXCLUDE_TABLE_PREFIXES = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.admin'
)
Include views
*************
Include database views
.. code-block:: python
EXPLORER_SCHEMA_INCLUDE_VIEWS = False
ASYNC schema
************
Generate DB schema asynchronously. Requires Celery and ``EXPLORER_TASKS_ENABLED``
.. code-block:: python
EXPLORER_ASYNC_SCHEMA = False
Database connections
********************
A dictionary of ``{'Friendly Name': 'django_db_alias'}``.
If you want to create a DatabaseConnection to a DB that is registered in settings.DATABASES, then add the alias to this
dictionary (with a friendly name of your choice), and then create a new connection (../connections/new/) with the alias
and name set to the alias of the Django database, and the Engine set to "Django Database".
.. code-block:: python
EXPLORER_CONNECTIONS = {}
Permission view
****************
Callback to check if the user is allowed to view and execute stored queries
.. code-block:: python
EXPLORER_PERMISSION_VIEW = lambda r: r.user.is_staff
Permission change
*****************
Callback to check if the user is allowed to add/change/delete queries
.. code-block:: python
EXPLORER_PERMISSION_CHANGE = lambda r: r.user.is_staff
Transforms
**********
List of tuples, see :ref:`Template Columns` more info.
.. code-block:: python
EXPLORER_TRANSFORMS = []
Recent query count
******************
The number of recent queries to show at the top of the query listing.
.. code-block:: python
EXPLORER_RECENT_QUERY_COUNT = 10
User query views
****************
A dict granting view permissions on specific queries of the form
.. code-block:: python
EXPLORER_GET_USER_QUERY_VIEWS = {userId: [queryId, ], }
**Default Value:**
.. code-block:: python
EXPLORER_GET_USER_QUERY_VIEWS = {}
Token Authentication
********************
Bool indicating whether token-authenticated requests should be enabled. See :ref:`Power Tips`.
.. code-block:: python
EXPLORER_TOKEN_AUTH_ENABLED = False
Token
*****
Access token for query results.
.. code-block:: python
EXPLORER_TOKEN = "CHANGEME"
Celery tasks
************
Turn on if you want to use the ``snapshot_queries`` celery task, or email report functionality in ``tasks.py``
.. code-block:: python
EXPLORER_TASKS_ENABLED = False
S3 access key
*************
S3 Access Key for snapshot upload
.. code-block:: python
EXPLORER_S3_ACCESS_KEY = None
S3 secret key
*************
S3 Secret Key for snapshot upload
.. code-block:: python
EXPLORER_S3_SECRET_KEY = None
S3 bucket
*********
S3 Bucket for snapshot upload
.. code-block:: python
EXPLORER_S3_BUCKET = None
S3 region
******************
S3 region. Defaults to us-east-1 if not specified.
.. code-block:: python
EXPLORER_S3_REGION = 'us-east-1'
S3 endpoint url
******************
S3 endpoint url. Normally not necessary to set.
Useful to set if you are using a non-AWS S3 service or you are using a private AWS endpoint.
.. code-block:: python
EXPLORER_S3_ENDPOINT_URL = 'https://accesspoint.vpce-abc123-abcdefgh.s3.us-east-1.vpce.amazonaws.com'
S3 destination path
********************
S3 destination path. Defaults to empty string.
Useful to set destination folder relative to S3 bucket.
Along with settings ``EXPLORER_S3_ENDPOINT_URL`` and ``EXPLORER_S3_BUCKET`` you can specify full destination path for async query results.
.. code-block:: python
EXPLORER_S3_DESTINATION = 'explorer/query'
# if
EXPLORER_S3_ENDPOINT_URL = 'https://amazonaws.com'
EXPLORER_S3_BUCKET = 'test-bucket'
# then files will be saved to
# https://amazonaws.com/test-bucket/explorer/query/filename1.csv
# where `filename1.csv` is generated filename
S3 link expiration
******************
S3 link expiration time. Defaults to 3600 seconds (1hr) if not specified.
Links are generated as presigned urls for security
.. code-block:: python
EXPLORER_S3_LINK_EXPIRATION = 3600
S3 signature version
********************
The signature version when signing requests.
As of ``boto3`` version 1.13.21 the default signature version used for generating presigned urls is still ``v2``.
To be able to access your s3 objects in all regions through presigned urls, explicitly set this to ``s3v4``.
.. code-block:: python
EXPLORER_S3_SIGNATURE_VERSION = 's3v4'
From email
**********
The default 'from' address when using async report email functionality
.. code-block:: python
EXPLORER_FROM_EMAIL = "django-sql-explorer@example.com"
Data exporters
**************
The export buttons to use. Default includes Excel, so xlsxwriter from ``requirements/optional.txt`` is needed
.. code-block:: python
EXPLORER_DATA_EXPORTERS = [
('csv', 'explorer.exporters.CSVExporter'),
('excel', 'explorer.exporters.ExcelExporter'),
('json', 'explorer.exporters.JSONExporter')
]
Unsafe rendering
****************
Disable auto escaping for rendering values from the database. Be wary of XSS attacks if querying unknown data.
.. code-block:: python
EXPLORER_UNSAFE_RENDERING = False
No permission view
******************
Path to a view used when the user does not have permission. By default, a basic login view is provided
but a dotted path to a python view can be used
.. code-block:: python
EXPLORER_NO_PERMISSION_VIEW = 'explorer.views.auth.safe_login_view_wrapper'
Anonymous Telemetry Collection
******************************
By default, anonymous usage statistics are collected. To disable this, set the following setting to False.
You can see what is being collected in telemetry.py.
.. code-block:: python
EXPLORER_ENABLE_ANONYMOUS_STATS = False
AI Settings (SQL Assistant)
***************************
The following three settings control the SQL Assistant. More information is available in :doc:`install` instructions.
.. code-block:: python
EXPLORER_AI_API_KEY = getattr(settings, "EXPLORER_AI_API_KEY", None)
EXPLORER_ASSISTANT_BASE_URL = getattr(settings, "EXPLORER_ASSISTANT_BASE_URL", "https://api.openai.com/v1")
EXPLORER_ASSISTANT_MODEL = getattr(settings, "EXPLORER_ASSISTANT_MODEL",
# Return the model name and max_tokens it supports
{"name": "gpt-4o",
"max_tokens": 128000})
User-Configured DB Connections
******************************
Set `EXPLORER_DB_CONNECTIONS_ENABLED` to `True` to enable DB connections to get configured in the browser (e.g. not
just in settings.py). This also allows uploading of CSV or SQLite files for instant querying.
If you are using a database driver that requires information beyond the basic alias/db name/user/password/host/port,
you can add arbitrary connection data in the 'extras' field. Provide a JSON object and it will get merged into the
final Django-style connection dictionary object. For example, for postgres, you could put this in the extras field:
.. code-block:: python
{'OPTIONS': {'server_side_binding': true}}
Which would enable this (obscure) postgres feature. Neato! Note this must be valid JSON.
User Uploads
************
With `EXPLORER_DB_CONNECTIONS_ENABLED` set to `True`, you can also set `EXPLORER_USER_UPLOADS_ENABLED` to allow users
to upload their own CSV and SQLite files directly to explorer as new connections.
Go to connections->Upload File. The uploaded files are limited in size by the
`EXPLORER_MAX_UPLOAD_SIZE` setting which is set to 500mb by default (500 * 1024 * 1024). SQLite files (in either .db or
.sqlite) will simply appear as connections. CSV files get run through a parser that infers the type of each field.
================================================
FILE: entrypoint.sh
================================================
#!/bin/bash
# entrypoint.sh
set -e
# Source the nvm script to set up the environment
# This should match the version referenced in Dockerfile
. /usr/local/.nvm/nvm.sh
nvm use 20.15.1
# Django
python manage.py migrate
python manage.py runserver 0.0.0.0:8000 &
echo "Django server started"
# Vite dev server
export APP_VERSION=$(python -c 'from explorer import __version__; print(__version__)')
echo "Starting Vite with APP_VERSION=${APP_VERSION}"
npx vite --config vite.config.mjs
================================================
FILE: explorer/__init__.py
================================================
__version_info__ = {
"major": 5,
"minor": 3,
"patch": 0,
"releaselevel": "final",
"serial": 0
}
def get_version(short=False):
assert __version_info__["releaselevel"] in ("alpha", "beta", "final")
vers = ["%(major)i.%(minor)i" % __version_info__, ]
if __version_info__["patch"]:
vers.append(".%(patch)i" % __version_info__)
if __version_info__["releaselevel"] != "final" and not short:
vers.append(
"%s%i" % (
__version_info__["releaselevel"][0],
__version_info__["serial"])
)
return "".join(vers)
__version__ = get_version()
================================================
FILE: explorer/actions.py
================================================
import tempfile
from collections import defaultdict
from datetime import date
from wsgiref.util import FileWrapper
from zipfile import ZipFile
from django.http import HttpResponse
from explorer.exporters import CSVExporter
def generate_report_action(description="Generate CSV file from SQL query",):
def generate_report(modeladmin, request, queryset):
results = [
report for report in queryset if report.passes_blacklist()[0]
]
queries = (len(results) > 0 and _package(results)) or defaultdict(int)
response = HttpResponse(
queries["data"],
content_type=queries["content_type"]
)
response["Content-Disposition"] = queries["filename"]
response["Content-Length"] = queries["length"]
return response
generate_report.short_description = description
return generate_report
def _package(queries):
ret = {}
is_one = len(queries) == 1
name_root = lambda n: f"attachment; filename={n}" # noqa
ret["content_type"] = (is_one and "text/csv") or "application/zip"
formatted = queries[0].title.replace(",", "")
day = date.today()
ret["filename"] = (
is_one and name_root(f"{formatted}.csv")
) or name_root(f"Report_{day}.zip")
ret["data"] = (
is_one and CSVExporter(queries[0]).get_output()
) or _build_zip(queries)
ret["length"] = (is_one and len(ret["data"]) or ret["data"].blksize)
return ret
def _build_zip(queries):
temp = tempfile.TemporaryFile()
zip_file = ZipFile(temp, "w")
for r in queries:
zip_file.writestr(
f"{r.title}.csv", CSVExporter(r).get_output() or "Error!"
)
zip_file.close()
ret = FileWrapper(temp)
temp.seek(0)
return ret
================================================
FILE: explorer/admin.py
================================================
from django.contrib import admin
from explorer.actions import generate_report_action
from explorer.models import Query, ExplorerValue
from explorer.ee.db_connections.admin import DatabaseConnectionAdmin # noqa
@admin.register(Query)
class QueryAdmin(admin.ModelAdmin):
list_display = ("title", "description", "created_by_user", "few_shot")
list_filter = ("title",)
raw_id_fields = ("created_by_user",)
actions = [generate_report_action()]
@admin.register(ExplorerValue)
class ExplorerValueAdmin(admin.ModelAdmin):
list_display = ("key", "value", "display_key")
list_filter = ("key",)
search_fields = ("key", "value")
def display_key(self, obj):
# Human-readable name for the key
return dict(ExplorerValue.EXPLORER_SETTINGS_CHOICES).get(obj.key, "")
display_key.short_description = "Setting Name"
================================================
FILE: explorer/app_settings.py
================================================
from pydoc import locate
from django.conf import settings
EXPLORER_CONNECTIONS = getattr(settings, "EXPLORER_CONNECTIONS", {})
# Deprecated as of 6.0. Will be removed in a future version.
EXPLORER_DEFAULT_CONNECTION = getattr(
settings, "EXPLORER_DEFAULT_CONNECTION", None
)
# Change the behavior of explorer
EXPLORER_SQL_BLACKLIST = getattr(
settings, "EXPLORER_SQL_BLACKLIST",
(
# DML
"COMMIT",
"DELETE",
"INSERT",
"MERGE",
"REPLACE",
"ROLLBACK",
"SET",
"START",
"UPDATE",
"UPSERT",
# DDL
"ALTER",
"CREATE",
"DROP",
"RENAME",
"TRUNCATE",
# DCL
"GRANT",
"REVOKE",
)
)
EXPLORER_DEFAULT_ROWS = getattr(settings, "EXPLORER_DEFAULT_ROWS", 1000)
EXPLORER_SCHEMA_EXCLUDE_TABLE_PREFIXES = getattr(
settings,
"EXPLORER_SCHEMA_EXCLUDE_TABLE_PREFIXES",
(
"auth_",
"contenttypes_",
"sessions_",
"admin_"
)
)
EXPLORER_SCHEMA_INCLUDE_TABLE_PREFIXES = getattr(
settings,
"EXPLORER_SCHEMA_INCLUDE_TABLE_PREFIXES",
None
)
EXPLORER_SCHEMA_INCLUDE_VIEWS = getattr(
settings,
"EXPLORER_SCHEMA_INCLUDE_VIEWS",
False
)
EXPLORER_TRANSFORMS = getattr(settings, "EXPLORER_TRANSFORMS", [])
EXPLORER_PERMISSION_VIEW = getattr(
settings, "EXPLORER_PERMISSION_VIEW", lambda r: r.user.is_staff
)
EXPLORER_PERMISSION_CHANGE = getattr(
settings, "EXPLORER_PERMISSION_CHANGE", lambda r: r.user.is_staff
)
EXPLORER_PERMISSION_CONNECTIONS = getattr(
settings, "EXPLORER_PERMISSION_CONNECTIONS", lambda r: r.user.is_staff
)
EXPLORER_RECENT_QUERY_COUNT = getattr(
settings, "EXPLORER_RECENT_QUERY_COUNT", 5
)
DEFAULT_EXPORTERS = [
("csv", "explorer.exporters.CSVExporter"),
("json", "explorer.exporters.JSONExporter"),
]
try:
import xlsxwriter # noqa
DEFAULT_EXPORTERS.insert(
1,
("excel", "explorer.exporters.ExcelExporter"),
)
except ImportError:
pass
EXPLORER_DATA_EXPORTERS = getattr(
settings, "EXPLORER_DATA_EXPORTERS", DEFAULT_EXPORTERS
)
CSV_DELIMETER = getattr(settings, "EXPLORER_CSV_DELIMETER", ",")
# API access
EXPLORER_TOKEN = getattr(settings, "EXPLORER_TOKEN", "CHANGEME")
# These are callable to aid testability by dodging the settings cache.
# There is surely a better pattern for this, but this'll hold for now.
EXPLORER_GET_USER_QUERY_VIEWS = lambda: getattr( # noqa
settings, "EXPLORER_USER_QUERY_VIEWS", {}
)
EXPLORER_TOKEN_AUTH_ENABLED = lambda: getattr( # noqa
settings, "EXPLORER_TOKEN_AUTH_ENABLED", False
)
EXPLORER_NO_PERMISSION_VIEW = lambda: locate( # noqa
getattr(
settings,
"EXPLORER_NO_PERMISSION_VIEW",
"explorer.views.auth.safe_login_view_wrapper",
),
)
# Async task related. Note that the EMAIL_HOST settings must be set up for
# email to work.
ENABLE_TASKS = getattr(settings, "EXPLORER_TASKS_ENABLED", False)
S3_ACCESS_KEY = getattr(settings, "EXPLORER_S3_ACCESS_KEY", None)
S3_SECRET_KEY = getattr(settings, "EXPLORER_S3_SECRET_KEY", None)
S3_BUCKET = getattr(settings, "EXPLORER_S3_BUCKET", None)
S3_LINK_EXPIRATION: int = getattr(settings, "EXPLORER_S3_LINK_EXPIRATION", 3600)
FROM_EMAIL = getattr(
settings, "EXPLORER_FROM_EMAIL", "django-sql-explorer@example.com"
)
S3_REGION = getattr(settings, "EXPLORER_S3_REGION", "us-east-1")
S3_ENDPOINT_URL = getattr(settings, "EXPLORER_S3_ENDPOINT_URL", None)
S3_DESTINATION = getattr(settings, "EXPLORER_S3_DESTINATION", "")
S3_SIGNATURE_VERSION = getattr(settings, "EXPLORER_S3_SIGNATURE_VERSION", "v4")
UNSAFE_RENDERING = getattr(settings, "EXPLORER_UNSAFE_RENDERING", False)
EXPLORER_CHARTS_ENABLED = getattr(settings, "EXPLORER_CHARTS_ENABLED", False)
EXPLORER_SHOW_SQL_BY_DEFAULT = getattr(settings, "EXPLORER_SHOW_SQL_BY_DEFAULT", True)
EXPLORER_ENABLE_ANONYMOUS_STATS = getattr(settings, "EXPLORER_ENABLE_ANONYMOUS_STATS", True)
EXPLORER_COLLECT_ENDPOINT_URL = "https://collect.sqlexplorer.io/stat"
# If set to True will autorun queries when viewed which is the historical behavior
# Default to True if not set in order to be backwards compatible
# If set to False will not autorun queries containing parameters when viewed
# - user will need to run by clicking the Save & Run Button to execute
EXPLORER_AUTORUN_QUERY_WITH_PARAMS = getattr(settings, "EXPLORER_AUTORUN_QUERY_WITH_PARAMS", True)
VITE_DEV_MODE = getattr(settings, "VITE_DEV_MODE", False)
# AI Assistant settings. Setting the first to an OpenAI key is the simplest way to enable the assistant
EXPLORER_AI_API_KEY = getattr(settings, "EXPLORER_AI_API_KEY", None)
EXPLORER_ASSISTANT_BASE_URL = getattr(settings, "EXPLORER_ASSISTANT_BASE_URL", "https://api.openai.com/v1")
# Deprecated. Will be removed in a future release. Please use EXPLORER_ASSISTANT_MODEL_NAME instead
EXPLORER_ASSISTANT_MODEL = getattr(settings, "EXPLORER_ASSISTANT_MODEL",
# Return the model name and max_tokens it supports
{"name": "gpt-4o",
"max_tokens": 128000})
EXPLORER_ASSISTANT_MODEL_NAME = getattr(settings, "EXPLORER_ASSISTANT_MODEL_NAME",
EXPLORER_ASSISTANT_MODEL["name"])
EXPLORER_DB_CONNECTIONS_ENABLED = getattr(settings, "EXPLORER_DB_CONNECTIONS_ENABLED", False)
EXPLORER_USER_UPLOADS_ENABLED = getattr(settings, "EXPLORER_USER_UPLOADS_ENABLED", False)
EXPLORER_PRUNE_LOCAL_UPLOAD_COPY_DAYS_INACTIVITY = getattr(settings,
"EXPLORER_PRUNE_LOCAL_UPLOAD_COPY_DAYS_INACTIVITY", 7)
# 500mb default max
EXPLORER_MAX_UPLOAD_SIZE = getattr(settings, "EXPLORER_MAX_UPLOAD_SIZE", 500 * 1024 * 1024)
EXPLORER_HOSTED = getattr(settings, "EXPLORER_HOSTED", False)
def has_assistant():
return EXPLORER_AI_API_KEY is not None
def db_connections_enabled():
return EXPLORER_DB_CONNECTIONS_ENABLED
def user_uploads_enabled():
return (EXPLORER_USER_UPLOADS_ENABLED and
EXPLORER_DB_CONNECTIONS_ENABLED and
S3_BUCKET is not None)
================================================
FILE: explorer/apps.py
================================================
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
from django.db import transaction, DEFAULT_DB_ALIAS, connections
class ExplorerAppConfig(AppConfig):
name = "explorer"
verbose_name = _("SQL Explorer")
default_auto_field = "django.db.models.AutoField"
# SQL Explorer DatabaseConnection models store connection info but are always translated into "django-style" connections
# before use, because we use Django's DB engine to run queries, gather schema information, etc.
# In general this isn't a problem; we cough up a django-style connection and all is well. The exception is when using
# the `with transaction.atomic(using=...):` context manager. The atomic() function takes a connection *alias* argument
# and the retrieves the connection from settings.DATABASES. But of course if we are providing a user-created connection
# alias, Django doesn't find it.
# The solution is to monkey-patch the `get_connection` function within transaction.atomic to make it aware of the
# user-created connections.
# This code should be double-checked against new versions of Django to make sure the original logic is still correct.
def new_get_connection(using=None):
from explorer.ee.db_connections.models import DatabaseConnection
if using is None:
using = DEFAULT_DB_ALIAS
if using in connections:
return connections[using]
return DatabaseConnection.objects.get(alias=using).as_django_connection()
transaction.get_connection = new_get_connection
================================================
FILE: explorer/assistant/__init__.py
================================================
================================================
FILE: explorer/assistant/forms.py
================================================
from django import forms
from explorer.assistant.models import TableDescription
from explorer.ee.db_connections.utils import default_db_connection
class TableDescriptionForm(forms.ModelForm):
class Meta:
model = TableDescription
fields = "__all__"
widgets = {
"database_connection": forms.Select(attrs={"class": "form-select"}),
"description": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not self.instance.pk: # Check if this is a new instance
# Set the default value for database_connection
self.fields["database_connection"].initial = default_db_connection()
if self.instance and self.instance.table_name:
choices = [(self.instance.table_name, self.instance.table_name)]
else:
choices = []
f = forms.ChoiceField(
choices=choices,
widget=forms.Select(attrs={"class": "form-select", "data-placeholder": "Select table"})
)
# We don't actually care about validating the 'choices' that the ChoiceField does by default.
# Really we are just using that field type in order to get a valid pre-populated Select widget on the client
# But also it can't be blank!
def valid_value_new(v):
return bool(v)
f.valid_value = valid_value_new
self.fields["table_name"] = f
if self.instance and self.instance.table_name:
self.fields["table_name"].initial = self.instance.table_name
================================================
FILE: explorer/assistant/models.py
================================================
from django.db import models
from django.conf import settings
from explorer.ee.db_connections.models import DatabaseConnection
class PromptLog(models.Model):
class Meta:
app_label = "explorer"
prompt = models.TextField(blank=True)
user_request = models.TextField(blank=True)
response = models.TextField(blank=True)
run_by_user = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
blank=True,
on_delete=models.CASCADE
)
run_at = models.DateTimeField(auto_now_add=True)
duration = models.FloatField(blank=True, null=True) # seconds
model = models.CharField(blank=True, max_length=128, default="")
error = models.TextField(blank=True, null=True)
database_connection = models.ForeignKey(to=DatabaseConnection, on_delete=models.SET_NULL, blank=True, null=True)
class TableDescription(models.Model):
class Meta:
app_label = "explorer"
unique_together = ("database_connection", "table_name")
database_connection = models.ForeignKey(to=DatabaseConnection, on_delete=models.CASCADE)
table_name = models.CharField(max_length=512)
description = models.TextField()
def __str__(self):
return f"{self.database_connection.alias} - {self.table_name}"
================================================
FILE: explorer/assistant/urls.py
================================================
from django.urls import path
from explorer.assistant.views import (TableDescriptionListView,
TableDescriptionCreateView,
TableDescriptionUpdateView,
TableDescriptionDeleteView,
AssistantHelpView,
AssistantHistoryApiView)
assistant_urls = [
path("assistant/", AssistantHelpView.as_view(), name="assistant"),
path("assistant/history/", AssistantHistoryApiView.as_view(), name="assistant_history"),
path("table-descriptions/", TableDescriptionListView.as_view(), name="table_description_list"),
path("table-descriptions/new/", TableDescriptionCreateView.as_view(), name="table_description_create"),
path("table-descriptions/<int:pk>/update/", TableDescriptionUpdateView.as_view(), name="table_description_update"),
path("table-descriptions/<int:pk>/delete/", TableDescriptionDeleteView.as_view(), name="table_description_delete"),
]
================================================
FILE: explorer/assistant/utils.py
================================================
from dataclasses import dataclass
from explorer import app_settings
from explorer.schema import schema_info
from explorer.models import ExplorerValue, Query
from django.db.utils import OperationalError
from django.db.models.functions import Lower
from django.db.models import Q
from explorer.assistant.models import TableDescription
OPENAI_MODEL = app_settings.EXPLORER_ASSISTANT_MODEL["name"]
ROW_SAMPLE_SIZE = 3
MAX_FIELD_SAMPLE_SIZE = 200 # characters
def openai_client():
from openai import OpenAI
return OpenAI(
api_key=app_settings.EXPLORER_AI_API_KEY,
base_url=app_settings.EXPLORER_ASSISTANT_BASE_URL
)
def do_req(prompt):
messages = [
{"role": "system", "content": prompt["system"]},
{"role": "user", "content": prompt["user"]},
]
resp = openai_client().chat.completions.create(
model=OPENAI_MODEL,
messages=messages
)
messages.append(resp.choices[0].message)
return messages
def extract_response(r):
return r[-1].content
def table_schema(db_connection, table_name):
schema = schema_info(db_connection)
s = [table for table in schema if table[0].lower() == table_name.lower()]
if len(s):
return s[0][1]
def sample_rows_from_table(connection, table_name):
"""
Fetches a sample of rows from the specified table and ensures that any field values
exceeding 200 characters (or bytes) are truncated. This is useful for handling fields
like "description" that might contain very long strings of text or binary data.
Truncating these fields prevents issues with displaying or processing overly large values.
An ellipsis ("...") is appended to indicate that the data has been truncated.
Args:
connection: The database connection.
table_name: The name of the table to sample rows from.
Returns:
A list of rows with field values truncated if they exceed 500 characters/bytes.
"""
cursor = connection.cursor()
try:
cursor.execute(f"SELECT * FROM {table_name} LIMIT {ROW_SAMPLE_SIZE}")
ret = [[header[0] for header in cursor.description]]
rows = cursor.fetchall()
for row in rows:
processed_row = []
for field in row:
new_val = field
if isinstance(field, str) and len(field) > MAX_FIELD_SAMPLE_SIZE:
new_val = field[:MAX_FIELD_SAMPLE_SIZE] + "..." # Truncate and add ellipsis
elif isinstance(field, (bytes, bytearray)):
new_val = "<binary_data>"
processed_row.append(new_val)
ret.append(processed_row)
return ret
except OperationalError as e:
return [[str(e)]]
def format_rows_from_table(rows):
""" Given an array of rows (a list of lists), returns e.g.
AlbumId | Title | ArtistId
1 | For Those About To Rock We Salute You | 1
2 | Let It Rip | 2
3 | Restless and Wild | 2
"""
return "\n".join([" | ".join([str(item) for item in row]) for row in rows])
def build_system_prompt(flavor):
bsp = ExplorerValue.objects.get_item(ExplorerValue.ASSISTANT_SYSTEM_PROMPT).value
bsp += f"\nYou are an expert at writing SQL, specifically for {flavor}, and account for the nuances of this dialect of SQL. You always respond with valid {flavor} SQL." # noqa
return bsp
def get_relevant_annotation(db_connection, t):
return TableDescription.objects.annotate(
table_name_lower=Lower("table_name")
).filter(
database_connection=db_connection,
table_name_lower=t.lower()
).first()
def get_relevant_few_shots(db_connection, included_tables):
included_tables_lower = [t.lower() for t in included_tables]
query_conditions = Q()
for table in included_tables_lower:
query_conditions |= Q(sql__icontains=table)
return Query.objects.annotate(
sql_lower=Lower("sql")
).filter(
database_connection=db_connection,
few_shot=True
).filter(query_conditions)
def get_few_shot_chunk(db_connection, included_tables):
included_tables = [t.lower() for t in included_tables]
few_shot_examples = get_relevant_few_shots(db_connection, included_tables)
if few_shot_examples:
return "## Relevant example queries, written by expert SQL analysts ##\n" + "\n\n".join(
[f"Description: {fs.title} - {fs.description}\nSQL:\n{fs.sql}"
for fs in few_shot_examples.all()]
)
@dataclass
class TablePromptData:
name: str
schema: list
sample: list
annotation: TableDescription
def render(self):
fmt_schema = "\n".join([str(field) for field in self.schema])
ret = f"""## Information for Table '{self.name}' ##
Schema:\n{fmt_schema}
Sample rows:\n{format_rows_from_table(self.sample)}"""
if self.annotation:
ret += f"\nUsage Notes:\n{self.annotation.description}"
return ret
def build_prompt(db_connection, assistant_request, included_tables, query_error=None, sql=None):
included_tables = [t.lower() for t in included_tables]
error_chunk = f"## Query Error ##\n{query_error}" if query_error else None
sql_chunk = f"## Existing User-Written SQL ##\n{sql}" if sql else None
request_chunk = f"## User's Request to Assistant ##\n{assistant_request}"
table_chunks = [
TablePromptData(
name=t,
schema=table_schema(db_connection, t),
sample=sample_rows_from_table(db_connection.as_django_connection(), t),
annotation=get_relevant_annotation(db_connection, t)
).render()
for t in included_tables
]
few_shot_chunk = get_few_shot_chunk(db_connection, included_tables)
chunks = [error_chunk, sql_chunk, *table_chunks, few_shot_chunk, request_chunk]
prompt = {
"system": build_system_prompt(db_connection.as_django_connection().vendor),
"user": "\n\n".join([c for c in chunks if c]),
}
return prompt
================================================
FILE: explorer/assistant/views.py
================================================
from django.http import JsonResponse
from django.views import View
from django.utils import timezone
from django.views.generic import ListView, CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
from .forms import TableDescriptionForm
from .models import TableDescription
import json
from explorer.views.auth import PermissionRequiredMixin
from explorer.views.mixins import ExplorerContextMixin
from explorer.telemetry import Stat, StatNames
from explorer.ee.db_connections.models import DatabaseConnection
from explorer.assistant.models import PromptLog
from explorer.assistant.utils import (
do_req, extract_response,
build_prompt
)
def run_assistant(request_data, user):
sql = request_data.get("sql")
included_tables = request_data.get("selected_tables", [])
connection_id = request_data.get("connection_id")
try:
conn = DatabaseConnection.objects.get(id=connection_id)
except DatabaseConnection.DoesNotExist:
return "Error: Connection not found"
assistant_request = request_data.get("assistant_request")
prompt = build_prompt(conn, assistant_request,
included_tables, request_data.get("db_error"), request_data.get("sql"))
start = timezone.now()
pl = PromptLog(
prompt=prompt,
run_by_user=user,
run_at=timezone.now(),
user_request=assistant_request,
database_connection=conn
)
response_text = None
try:
resp = do_req(prompt)
response_text = extract_response(resp)
pl.response = response_text
except Exception as e:
pl.error = str(e)
finally:
stop = timezone.now()
pl.duration = (stop - start).total_seconds()
pl.save()
Stat(StatNames.ASSISTANT_RUN, {
"included_table_count": len(included_tables),
"has_sql": bool(sql),
"duration": pl.duration,
}).track()
return response_text
class AssistantHelpView(View):
def post(self, request, *args, **kwargs):
try:
data = json.loads(request.body)
resp = run_assistant(data, request.user)
response_data = {
"status": "success",
"message": resp
}
return JsonResponse(response_data)
except json.JSONDecodeError:
return JsonResponse({"status": "error", "message": "Invalid JSON"}, status=400)
class TableDescriptionListView(PermissionRequiredMixin, ExplorerContextMixin, ListView):
model = TableDescription
permission_required = "view_permission"
template_name = "assistant/table_description_list.html"
context_object_name = "table_descriptions"
class TableDescriptionCreateView(PermissionRequiredMixin, ExplorerContextMixin, CreateView):
model = TableDescription
permission_required = "change_permission"
template_name = "assistant/table_description_form.html"
success_url = reverse_lazy("table_description_list")
form_class = TableDescriptionForm
class TableDescriptionUpdateView(PermissionRequiredMixin, ExplorerContextMixin, UpdateView):
model = TableDescription
permission_required = "change_permission"
template_name = "assistant/table_description_form.html"
success_url = reverse_lazy("table_description_list")
form_class = TableDescriptionForm
class TableDescriptionDeleteView(PermissionRequiredMixin, ExplorerContextMixin, DeleteView):
model = TableDescription
permission_required = "change_permission"
template_name = "assistant/table_description_confirm_delete.html"
success_url = reverse_lazy("table_description_list")
class AssistantHistoryApiView(View):
def post(self, request, *args, **kwargs):
try:
data = json.loads(request.body)
logs = PromptLog.objects.filter(
run_by_user=request.user,
database_connection_id=data["connection_id"]
).order_by("-run_at")[:5]
ret = [{
"user_request": log.user_request,
"response": log.response
} for log in logs]
return JsonResponse({"logs": ret})
except json.JSONDecodeError:
return JsonResponse({"status": "error", "message": "Invalid JSON"}, status=400)
================================================
FILE: explorer/charts.py
================================================
from io import BytesIO
from typing import Iterable, Optional
from .models import QueryResult
BAR_WIDTH = 0.2
def get_chart(result: QueryResult, chart_type: str, num_rows: int) -> Optional[str]:
import matplotlib.pyplot as plt
"""
Return a line or bar chart in SVG format if the result table adheres to the expected format.
A line chart is rendered if
* there is at least on row of in the result table
* there is at least one numeric column (the first column (with index 0) does not count)
The first column is used as x-axis labels.
All other numeric columns represent a line on the chart.
The name of the column is used as the name of the line in the legend.
Not numeric columns (except the first on) are ignored.
"""
if chart_type not in ("bar", "line"):
return
if len(result.data) < 1:
return None
data = result.data[:num_rows]
numeric_columns = [
c for c in range(1, len(data[0]))
if all([isinstance(col[c], (int, float)) or col[c] is None for col in data])
]
# Don't create charts for > 10 series. This is a lightweight visualization.
if len(numeric_columns) < 1 or len(numeric_columns) > 10:
return None
labels = [row[0] for row in data]
fig, ax = plt.subplots(figsize=(10, 3.8))
bars = []
bar_positions = []
for idx, col_num in enumerate(numeric_columns):
if chart_type == "bar":
values = [row[col_num] if row[col_num] is not None else 0 for row in data]
bar_container = ax.bar([x + idx * BAR_WIDTH
for x in range(len(labels))], values, BAR_WIDTH, label=result.headers[col_num])
bars.append(bar_container)
bar_positions.append([(rect.get_x(), rect.get_height()) for rect in bar_container])
if chart_type == "line":
ax.plot(labels, [row[col_num] for row in data], label=result.headers[col_num])
ax.set_xlabel(result.headers[0])
if chart_type == "bar":
ax.set_xticks([x + BAR_WIDTH * (len(numeric_columns) / 2 - 0.5) for x in range(len(labels))])
ax.set_xticklabels(labels)
ax.legend()
for label in ax.get_xticklabels():
label.set_rotation(20) # makes labels fit better
label.set_ha("right")
svg_str = get_svg(fig)
return svg_str
def get_svg(fig) -> str:
buffer = BytesIO()
fig.savefig(buffer, format="svg")
buffer.seek(0)
graph = buffer.getvalue().decode("utf-8")
buffer.close()
return graph
def is_numeric(column: Iterable) -> bool:
return all([isinstance(value, (int, float)) or value is None for value in column])
================================================
FILE: explorer/ee/LICENSE
================================================
** Additional License for "explorer/ee/" Directory **
All content that resides under the "explorer/ee/" directory of this repository is provided under the MIT License,
except that the following additional rights are reserved:
** "Commons Clause" License Condition v1.0 **
The Software may not be used as part of any commercial offering or service, such as hosting, service, or consulting
offerings. For purposes of the foregoing, "commercial offering" means the provision of the Software to third parties
for a fee or other consideration, including without limitation, providing the Software as part of a managed service,
as part of a platform as a service, or providing it to third parties on a software as a service basis.
This restriction does not apply to non-commercial use by any individual, nor does it prevent such individual from
providing services to third parties using the Software.
** Exceptions **
SQL Explorer, Inc. may grant a Commercial License Agreement to provide exceptions to the "Commons Clause" License
Condition. If you wish to obtain such a license, please contact SQL Explorer, Inc. at support@sqlexplorer.io. No
exception is granted implicitly or explicitly without a written and signed Commercial License Agreement from SQL
Explorer, Inc.
================================================
FILE: explorer/ee/__init__.py
================================================
================================================
FILE: explorer/ee/db_connections/__init__.py
================================================
================================================
FILE: explorer/ee/db_connections/admin.py
================================================
from django.contrib import admin
from explorer.models import DatabaseConnection
@admin.register(DatabaseConnection)
class DatabaseConnectionAdmin(admin.ModelAdmin):
pass
================================================
FILE: explorer/ee/db_connections/create_sqlite.py
================================================
import os
from io import BytesIO
from explorer.utils import secure_filename
from explorer.ee.db_connections.type_infer import get_parser
from explorer.ee.db_connections.utils import pandas_to_sqlite, uploaded_db_local_path
def get_names(file, append_conn=None, user_id=None):
s_filename = secure_filename(file.name)
table_name, _ = os.path.splitext(s_filename)
# f_name represents the filename of both the sqlite DB on S3, and on the local filesystem.
# If we are appending to an existing data source, then we re-use the same name.
# New connections get a new database name.
if append_conn:
f_name = os.path.basename(append_conn.name)
else:
f_name = f"{table_name}_{user_id}.db"
return table_name, f_name
def parse_to_sqlite(file, append_conn=None, user_id=None) -> (BytesIO, str):
table_name, f_name = get_names(file, append_conn, user_id)
# When appending, make sure the database exists locally so that we can write to it
if append_conn:
append_conn.download_sqlite_if_needed()
df_parser = get_parser(file)
if df_parser:
try:
df = df_parser(file.read())
local_path = uploaded_db_local_path(f_name)
f_bytes = pandas_to_sqlite(df, table_name, local_path)
except Exception as e: # noqa
raise ValueError(f"Error while parsing {f_name}: {e}") from e
else:
# If it's a SQLite file already, simply cough it up as a BytesIO object
return BytesIO(file.read()), f_name
return f_bytes, f_name
================================================
FILE: explorer/ee/db_connections/forms.py
================================================
from django import forms
from explorer.ee.db_connections.models import DatabaseConnection
import json
from django.core.exceptions import ValidationError
# This is very annoying, but Django renders the literal string 'null' in the form when displaying JSON
# via a TextInput widget. So this custom widget prevents that.
class JSONTextInput(forms.TextInput):
def render(self, name, value, attrs=None, renderer=None):
if value in (None, "", "null"):
value = ""
elif isinstance(value, dict):
value = json.dumps(value)
return super().render(name, value, attrs, renderer)
def value_from_datadict(self, data, files, name):
value = data.get(name)
if value in (None, "", "null"):
return None
try:
return json.loads(value)
except (TypeError, ValueError) as ex:
raise ValidationError("Enter a valid JSON") from ex
class DatabaseConnectionForm(forms.ModelForm):
class Meta:
model = DatabaseConnection
fields = "__all__"
widgets = {
"alias": forms.TextInput(attrs={"class": "form-control"}),
"engine": forms.Select(attrs={"class": "form-select"}),
"name": forms.TextInput(attrs={"class": "form-control"}),
"user": forms.TextInput(attrs={"class": "form-control"}),
"password": forms.PasswordInput(attrs={"class": "form-control"}),
"host": forms.TextInput(attrs={"class": "form-control"}),
"port": forms.TextInput(attrs={"class": "form-control"}),
"extras": JSONTextInput(attrs={"class": "form-control"}),
}
================================================
FILE: explorer/ee/db_connections/mime.py
================================================
import csv
import json
# These are 'shallow' checks. They are just to understand if the upload appears valid at surface-level.
# A deeper check will happen when pandas tries to parse the file.
# This is designed to be quick, and simply assigned the right (full) parsing function to the uploaded file.
def is_csv(file):
if file.content_type != "text/csv":
return False
try:
# Check if the file content can be read as a CSV
file.seek(0)
sample = file.read(1024).decode("utf-8")
csv.Sniffer().sniff(sample)
file.seek(0)
return True
except csv.Error:
return False
def is_json(file):
if file.content_type != "application/json":
return False
if not file.name.lower().endswith(".json"):
return False
return True
def is_json_list(file):
if not file.name.lower().endswith(".json"):
return False
file.seek(0)
first_line = file.readline()
file.seek(0)
try:
json.loads(first_line.decode("utf-8"))
return True
except ValueError:
return False
def is_sqlite(file):
if file.content_type not in ["application/x-sqlite3", "application/octet-stream"]:
return False
try:
# Check if the file starts with the SQLite file header
file.seek(0)
header = file.read(16)
file.seek(0)
return header == b"SQLite format 3\x00"
except Exception as e: # noqa
return False
================================================
FILE: explorer/ee/db_connections/models.py
================================================
import os
import json
from django.db import models, DatabaseError, connections, transaction
from django.db.utils import load_backend
from explorer.app_settings import EXPLORER_CONNECTIONS
from explorer.ee.db_connections.utils import quick_hash, uploaded_db_local_path
from django.core.cache import cache
from django_cryptography.fields import encrypt
class DatabaseConnectionManager(models.Manager):
def uploads(self):
return self.filter(engine=DatabaseConnection.SQLITE, host__isnull=False)
def non_uploads(self):
return self.exclude(engine=DatabaseConnection.SQLITE, host__isnull=False)
def default(self):
return self.filter(default=True).first()
class DatabaseConnection(models.Model):
objects = DatabaseConnectionManager()
SQLITE = "django.db.backends.sqlite3"
DJANGO = "django_connection"
DATABASE_ENGINES = (
(SQLITE, "SQLite3"),
("django.db.backends.postgresql", "PostgreSQL"),
("django.db.backends.mysql", "MySQL"),
("django.db.backends.oracle", "Oracle"),
("django.db.backends.mysql", "MariaDB"),
("django_cockroachdb", "CockroachDB"),
("mssql", "SQL Server (mssql-django)"),
("django_snowflake", "Snowflake"),
(DJANGO, "Django Connection"),
)
alias = models.CharField(max_length=255, unique=True)
engine = models.CharField(max_length=255, choices=DATABASE_ENGINES)
name = models.CharField(max_length=255, blank=True, null=True)
user = encrypt(models.CharField(max_length=255, blank=True, null=True))
password = encrypt(models.CharField(max_length=255, blank=True, null=True))
host = encrypt(models.CharField(max_length=255, blank=True))
port = models.CharField(max_length=255, blank=True, null=True)
extras = models.JSONField(blank=True, null=True)
upload_fingerprint = models.CharField(max_length=255, blank=True, null=True)
default = models.BooleanField(default=False)
def __str__(self):
return f"{self.alias}"
def update_fingerprint(self):
self.upload_fingerprint = self.local_fingerprint()
self.save()
def local_fingerprint(self):
if os.path.exists(self.local_name):
return quick_hash(self.local_name)
def _download_sqlite(self):
from explorer.utils import get_s3_bucket
s3 = get_s3_bucket()
s3.download_file(self.host, self.local_name)
def _download_needed(self):
# If the file doesn't exist, obviously we need to download it
# If it does exist, then check if it's out of date. But only check if in fact the DatabaseConnection has been
# saved to the DB. For example, we might be validating an unsaved connection, in which case the fingerprint
# won't be set yet.
return (not os.path.exists(self.local_name) or
(self.id is not None and self.local_fingerprint() != self.upload_fingerprint))
def download_sqlite_if_needed(self):
if self._download_needed():
cache_key = f"download_lock_{self.local_name}"
lock_acquired = cache.add(cache_key, "locked", timeout=300) # Timeout after 5 minutes
if lock_acquired:
try:
if self._download_needed():
self._download_sqlite()
self.update_fingerprint()
finally:
cache.delete(cache_key)
@property
def is_upload(self):
return self.engine == self.SQLITE and self.host
@property
def is_django_alias(self):
return self.engine == DatabaseConnection.DJANGO
@property
def local_name(self):
if self.is_upload:
return uploaded_db_local_path(self.name)
def delete_local_sqlite(self):
if self.is_upload and os.path.exists(self.local_name):
os.remove(self.local_name)
# See the comment in apps.py for a more in-depth explanation of what's going on here.
def as_django_connection(self):
if self.is_upload:
self.download_sqlite_if_needed()
# You can't access a Django-backed connection unless it has been registered in EXPLORER_CONNECTIONS.
# Otherwise, users with userspace DatabaseConnection rights could connect to underlying Django DB connections.
if self.is_django_alias:
if self.alias in EXPLORER_CONNECTIONS.values():
return connections[self.alias]
else:
raise DatabaseError("Django alias connections must be registered in EXPLORER_CONNECTIONS.")
connection_settings = {
"ENGINE": self.engine,
"NAME": self.name if not self.is_upload else self.local_name,
"USER": self.user,
"PASSWORD": self.password,
"HOST": self.host if not self.is_upload else None,
"PORT": self.port,
"TIME_ZONE": None,
"CONN_MAX_AGE": 0,
"CONN_HEALTH_CHECKS": False,
"OPTIONS": {},
"TEST": {},
"AUTOCOMMIT": True,
"ATOMIC_REQUESTS": False,
}
if self.extras:
extras_dict = json.loads(self.extras) if isinstance(self.extras, str) else self.extras
connection_settings.update(extras_dict)
try:
backend = load_backend(self.engine)
return backend.DatabaseWrapper(connection_settings, self.alias)
except DatabaseError as e:
raise DatabaseError(f"Failed to create explorer connection: {e}") from e
def save(self, *args, **kwargs):
# If this instance is marked as default, unset the default on all other instances
if self.default:
with transaction.atomic():
DatabaseConnection.objects.filter(default=True).update(default=False)
else:
# If there is no default set yet, make this newly created one the default.
has_default = DatabaseConnection.objects.filter(default=True).exists()
if not has_default:
self.default = True
super().save(*args, **kwargs)
================================================
FILE: explorer/ee/db_connections/type_infer.py
================================================
import io
import json
from explorer.ee.db_connections.mime import is_csv, is_json, is_sqlite, is_json_list
MAX_TYPING_SAMPLE_SIZE = 5000
SHORTEST_PLAUSIBLE_DATE_STRING = 5
def get_parser(file):
if is_csv(file):
return csv_to_typed_df
if is_json_list(file):
return json_list_to_typed_df
if is_json(file):
return json_to_typed_df
if is_sqlite(file):
return None
raise ValueError(f"File {file.content_type} not supported.")
def csv_to_typed_df(csv_bytes, delimiter=",", has_headers=True):
import pandas as pd
csv_file = io.BytesIO(csv_bytes)
df = pd.read_csv(csv_file, sep=delimiter, header=0 if has_headers else None)
return df_to_typed_df(df)
def json_list_to_typed_df(json_bytes):
import pandas as pd
data = []
for line in io.BytesIO(json_bytes).readlines():
data.append(json.loads(line.decode("utf-8")))
df = pd.json_normalize(data)
return df_to_typed_df(df)
def json_to_typed_df(json_bytes):
import pandas as pd
json_file = io.BytesIO(json_bytes)
json_content = json.load(json_file)
df = pd.json_normalize(json_content)
return df_to_typed_df(df)
def atof_custom(value):
# Remove any thousands separators and convert the decimal point
if "," in value and "." in value:
if value.index(",") < value.index("."):
# 0,000.00 format
value = value.replace(",", "")
else:
# 0.000,00 format
value = value.replace(".", "").replace(",", ".")
elif "," in value:
# No decimal point, only thousands separator
value = value.replace(",", "")
return float(value)
def df_to_typed_df(df): # noqa
import pandas as pd
from dateutil import parser
try:
for column in df.columns:
# If we somehow have an array within a field (e.g. from a json object) then convert it to a string
df[column] = df[column].apply(lambda x: str(x) if isinstance(x, list) else x)
values = df[column].dropna().unique()
if len(values) > MAX_TYPING_SAMPLE_SIZE:
values = pd.Series(values).sample(MAX_TYPING_SAMPLE_SIZE, random_state=42).to_numpy()
is_date = False
is_integer = True
is_float = True
for value in values:
try:
float_val = atof_custom(str(value))
if float_val == int(float_val):
continue # This is effectively an integer
else:
is_integer = False
except ValueError:
is_integer = False
is_float = False
break
if is_integer:
is_float = False
if not is_integer and not is_float:
is_date = True
# The dateutil parser is very aggressive and will interpret many short strings as dates.
# For example "12a" will be interpreted as 12:00 AM on the current date.
# That is not the behavior anyone wants. The shortest plausible date string is e.g. 1-1-23
try_parse = [v for v in values if len(str(v)) > SHORTEST_PLAUSIBLE_DATE_STRING]
if len(try_parse) > 0:
for value in try_parse:
try:
parser.parse(str(value))
except (ValueError, TypeError, OverflowError):
is_date = False
break
else:
is_date = False
if is_date:
df[column] = pd.to_datetime(df[column], errors="coerce", utc=True)
elif is_integer:
df[column] = df[column].apply(lambda x: int(atof_custom(str(x))) if pd.notna(x) else x)
# If there are NaN / blank values, the column will be converted to float
# Convert it back to integer
df[column] = df[column].astype("Int64")
elif is_float:
df[column] = df[column].apply(lambda x: atof_custom(str(x)) if pd.notna(x) else x)
else:
inferred_type = pd.api.types.infer_dtype(values)
if inferred_type == "integer":
df[column] = pd.to_numeric(df[column], errors="coerce", downcast="integer")
elif inferred_type == "floating":
df[column] = pd.to_numeric(df[column], errors="coerce")
return df
except pd.errors.ParserError as e:
return str(e)
================================================
FILE: explorer/ee/db_connections/utils.py
================================================
import os
import hashlib
import sqlite3
import io
def default_db_connection():
from explorer.ee.db_connections.models import DatabaseConnection
return DatabaseConnection.objects.default()
def default_db_connection_id():
return default_db_connection().id
# Uploading the same filename twice (from the same user) will overwrite the 'old' DB on S3
def upload_sqlite(db_bytes, path):
from explorer.utils import get_s3_bucket
bucket = get_s3_bucket()
bucket.put_object(Key=path, Body=db_bytes, ServerSideEncryption="AES256")
# Aliases have the user_id appended to them so that if two users upload files with the same name
# they don't step on one another. Without this, the *files* would get uploaded separately (because
# the DBs go into user-specific folders on s3), but the *aliases* would be the same. So one user
# could (innocently) upload a file with the same name, and any existing queries would be suddenly pointing
# to this new database connection. Oops!
def create_connection_for_uploaded_sqlite(filename, s3_path):
from explorer.ee.db_connections.models import DatabaseConnection
return DatabaseConnection.objects.create(
alias=filename,
engine=DatabaseConnection.SQLITE,
name=filename,
host=s3_path,
)
def user_dbs_local_dir():
d = os.path.normpath(os.path.join(os.getcwd(), "user_dbs"))
if not os.path.exists(d):
os.makedirs(d)
return d
def uploaded_db_local_path(name):
return os.path.join(user_dbs_local_dir(), name)
def sqlite_to_bytesio(local_path):
# Write the file to disk. It'll be uploaded to s3, and left here locally for querying
db_file = io.BytesIO()
with open(local_path, "rb") as f:
db_file.write(f.read())
db_file.seek(0)
return db_file
def pandas_to_sqlite(df, table_name, local_path):
# Write the DataFrame to a local SQLite database and return it as a BytesIO object.
# This intentionally leaves the sqlite db on the local disk so that it is ready to go for
# querying immediately after the connection has been created. Removing it would also be OK, since
# the system knows to re-download it if it's not available, but this saves an extra download from S3.
conn = sqlite3.connect(local_path)
try:
df.to_sql(table_name, conn, if_exists="replace", index=False)
finally:
conn.commit()
conn.close()
return sqlite_to_bytesio(local_path)
def quick_hash(file_path, num_samples=10, sample_size=1024):
hasher = hashlib.sha256()
file_size = os.path.getsize(file_path)
if file_size == 0:
return hasher.hexdigest()
sample_interval = file_size // num_samples
with open(file_path, "rb") as f:
for i in range(num_samples):
f.seek(i * sample_interval)
sample_data = f.read(sample_size)
if not sample_data:
break
hasher.update(sample_data)
return hasher.hexdigest()
================================================
FILE: explorer/ee/db_connections/views.py
================================================
import logging
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView, TemplateView
from django.views import View
from django.http import JsonResponse, HttpResponse
from django.urls import reverse_lazy
from django.db.utils import OperationalError, DatabaseError
from explorer.models import DatabaseConnection
from explorer.ee.db_connections.utils import (
upload_sqlite,
create_connection_for_uploaded_sqlite
)
from explorer.ee.db_connections.create_sqlite import parse_to_sqlite
from explorer.schema import clear_schema_cache
from explorer.app_settings import EXPLORER_MAX_UPLOAD_SIZE
from explorer.ee.db_connections.forms import DatabaseConnectionForm
from explorer.utils import delete_from_s3
from explorer.views.auth import PermissionRequiredMixin
from explorer.views.mixins import ExplorerContextMixin
from explorer.ee.db_connections.mime import is_sqlite
logger = logging.getLogger(__name__)
class UploadDbView(PermissionRequiredMixin, View):
permission_required = "connections_permission"
def post(self, request): # noqa
file = request.FILES.get("file")
if file:
# 'append' should be None, or the ID of the DatabaseConnection to append this table to.
# This is stored in DatabaseConnection.host of the previously uploaded connection
append = request.POST.get("append")
append_path = None
conn = None
if append:
conn = DatabaseConnection.objects.get(id=append)
append_path = conn.host
if file.size > EXPLORER_MAX_UPLOAD_SIZE:
friendly = EXPLORER_MAX_UPLOAD_SIZE / (1024 * 1024)
return JsonResponse({"error": f"File size exceeds the limit of {friendly} MB"}, status=400)
# You can't double stramp a triple stamp!
if append_path and is_sqlite(file):
msg = "Can't append a SQLite file to a SQLite file. Only CSV and JSON."
logger.error(msg)
return JsonResponse({"error": msg}, status=400)
try:
f_bytes, f_name = parse_to_sqlite(file, conn, request.user.id)
except ValueError as e:
logger.error(f"Error getting bytes for {file.name}: {e}")
return JsonResponse({"error": "File was not csv, json, or sqlite."}, status=400)
except TypeError as e:
logger.error(f"Error parse {file.name}: {e}")
return JsonResponse({"error": "Error parsing file."}, status=400)
if append_path:
s3_path = append_path
else:
s3_path = f"user_dbs/user_{request.user.id}/{f_name}"
try:
upload_sqlite(f_bytes, s3_path)
except Exception as e: # noqa
logger.exception(f"Exception while uploading file {f_name}: {e}")
return JsonResponse({"error": "Error while uploading file to S3."}, status=400)
# If we're not appending, then need to create a new DatabaseConnection
if not append_path:
conn = create_connection_for_uploaded_sqlite(f_name, s3_path)
clear_schema_cache(conn)
conn.update_fingerprint()
return JsonResponse({"success": True})
else:
return JsonResponse({"error": "No file provided"}, status=400)
class DatabaseConnectionsListView(PermissionRequiredMixin, ExplorerContextMixin, ListView):
permission_required = "connections_permission"
template_name = "connections/connections.html"
model = DatabaseConnection
class DatabaseConnectionDetailView(PermissionRequiredMixin, ExplorerContextMixin, DetailView):
permission_required = "connections_permission"
model = DatabaseConnection
template_name = "connections/database_connection_detail.html"
class DatabaseConnectionCreateView(PermissionRequiredMixin, ExplorerContextMixin, CreateView):
permission_required = "connections_permission"
model = DatabaseConnection
form_class = DatabaseConnectionForm
template_name = "connections/database_connection_form.html"
success_url = reverse_lazy("explorer_connections")
class DatabaseConnectionUploadCreateView(TemplateView):
template_name = "connections/connection_upload.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["valid_connections"] = DatabaseConnection.objects.filter(engine=DatabaseConnection.SQLITE,
host__isnull=False)
return context
class DatabaseConnectionUpdateView(PermissionRequiredMixin, ExplorerContextMixin, UpdateView):
permission_required = "connections_permission"
model = DatabaseConnection
form_class = DatabaseConnectionForm
template_name = "connections/database_connection_form.html"
success_url = reverse_lazy("explorer_connections")
class DatabaseConnectionDeleteView(PermissionRequiredMixin, DeleteView):
permission_required = "connections_permission"
model = DatabaseConnection
template_name = "connections/database_connection_confirm_delete.html"
success_url = reverse_lazy("explorer_connections")
def delete(self, request, *args, **kwargs):
connection = self.get_object()
if connection.is_upload:
delete_from_s3(connection.host)
return super().delete(request, *args, **kwargs)
class DatabaseConnectionRefreshView(PermissionRequiredMixin, View):
permission_required = "connections_permission"
success_url = reverse_lazy("explorer_connections")
def get(self, request, pk): # noqa
conn = DatabaseConnection.objects.get(id=pk)
conn.delete_local_sqlite()
clear_schema_cache(conn)
message = f"Deleted schema cache for {conn.alias}. Schema will be regenerated on next use."
if conn.is_upload:
message += "\nRemoved local SQLite DB. Will be re-downloaded from S3 on next use."
message += "\nPlease hit back to return to the application."
return HttpResponse(content_type="text/plain", content=message)
class DatabaseConnectionValidateView(PermissionRequiredMixin, View):
permission_required = "connections_permission"
# pk param is ignored, in order to deal with having 2 URL patterns
def post(self, request, pk=None): # noqa
form = DatabaseConnectionForm(request.POST)
instance = DatabaseConnection.objects.filter(alias=request.POST["alias"]).first()
if instance:
form = DatabaseConnectionForm(request.POST, instance=instance)
if form.is_valid():
connection_data = form.cleaned_data
explorer_connection = DatabaseConnection(
alias=connection_data["alias"],
engine=connection_data["engine"],
name=connection_data["name"],
user=connection_data["user"],
password=connection_data["password"],
host=connection_data["host"],
port=connection_data["port"],
extras=connection_data["extras"]
)
try:
conn = explorer_connection.as_django_connection()
with conn.cursor() as cursor:
cursor.execute("SELECT 1")
return JsonResponse({"success": True})
except OperationalError as e:
return JsonResponse({"success": False, "error": str(e)})
except DatabaseError as e:
return JsonResponse({"success": False, "error": str(e)})
else:
return JsonResponse({"success": False, "error": "Invalid form data"})
================================================
FILE: explorer/ee/urls.py
================================================
from django.urls import path
from explorer.ee.db_connections.views import (
UploadDbView,
DatabaseConnectionsListView,
DatabaseConnectionCreateView,
DatabaseConnectionDetailView,
DatabaseConnectionUpdateView,
DatabaseConnectionDeleteView,
DatabaseConnectionValidateView,
DatabaseConnectionUploadCreateView,
DatabaseConnectionRefreshView
)
ee_urls = [
path("connections/", DatabaseConnectionsListView.as_view(), name="explorer_connections"),
path("connections/upload/", UploadDbView.as_view(), name="explorer_upload"),
path("connections/<int:pk>/", DatabaseConnectionDetailView.as_view(), name="explorer_connection_detail"),
path("connections/new/", DatabaseConnectionCreateView.as_view(), name="explorer_connection_create"),
path("connections/create_upload/", DatabaseConnectionUploadCreateView.as_view(), name="explorer_upload_create"),
path("connections/<int:pk>/edit/", DatabaseConnectionUpdateView.as_view(), name="explorer_connection_update"),
path("connections/<int:pk>/delete/", DatabaseConnectionDeleteView.as_view(), name="explorer_connection_delete"),
path("connections/validate/", DatabaseConnectionValidateView.as_view(), name="explorer_connection_validate"),
path("connections/<int:pk>/refresh/", DatabaseConnectionRefreshView.as_view(),
name="explorer_connection_refresh")
]
================================================
FILE: explorer/exporters.py
================================================
import codecs
import csv
import json
import uuid
from datetime import datetime
from io import BytesIO, StringIO
from django.core.serializers.json import DjangoJSONEncoder
from django.utils.module_loading import import_string
from django.utils.text import get_valid_filename, slugify
from explorer import app_settings
def get_exporter_class(format):
class_str = dict(app_settings.EXPLORER_DATA_EXPORTERS)[format]
return import_string(class_str)
class BaseExporter:
name = ""
content_type = ""
file_extension = ""
def __init__(self, query):
self.query = query
def get_output(self, **kwargs):
value = self.get_file_output(**kwargs).getvalue()
return value
def get_file_output(self, **kwargs):
res = self.query.execute_query_only()
return self._get_output(res, **kwargs)
def _get_output(self, res, **kwargs):
"""
:param res: QueryResult
:param kwargs: Optional. Any exporter-specific arguments.
:return: File-like object
"""
raise NotImplementedError
def get_filename(self):
return get_valid_filename(self.query.title or "") + self.file_extension
class CSVExporter(BaseExporter):
name = "CSV"
content_type = "text/csv"
file_extension = ".csv"
def _get_output(self, res, **kwargs):
delim = kwargs.get("delim") or app_settings.CSV_DELIMETER
delim = "\t" if delim == "tab" else str(delim)
delim = app_settings.CSV_DELIMETER if len(delim) > 1 else delim
csv_data = StringIO()
csv_data.write(codecs.BOM_UTF8.decode("utf-8"))
writer = csv.writer(csv_data, delimiter=delim)
writer.writerow(res.headers)
for row in res.data:
writer.writerow(row)
return csv_data
class JSONExporter(BaseExporter):
name = "JSON"
content_type = "application/json"
file_extension = ".json"
def _get_output(self, res, **kwargs):
data = []
for row in res.data:
data.append(
dict(zip(
[str(h) if h is not None else "" for h in res.headers],
row
))
)
json_data = json.dumps(data, cls=DjangoJSONEncoder)
return StringIO(json_data)
class ExcelExporter(BaseExporter):
name = "Excel"
content_type = "application/vnd.ms-excel"
file_extension = ".xlsx"
def _get_output(self, res, **kwargs):
import xlsxwriter
output = BytesIO()
wb = xlsxwriter.Workbook(output, {"in_memory": True})
ws = wb.add_worksheet(name=self._format_title())
# Write headers
row = 0
col = 0
header_style = wb.add_format({"bold": True})
for header in res.header_strings:
ws.write(row, col, header, header_style)
col += 1
# Write data
row = 1
col = 0
for data_row in res.data:
for data in data_row:
# xlsxwriter can't handle timezone-aware datetimes or
# UUIDs, so we help out here and just cast it to a
# string
if isinstance(data, (datetime, uuid.UUID)):
data = str(data)
# JSON and Array fields
if isinstance(data, (dict, list)):
data = json.dumps(data)
ws.write(row, col, data)
col += 1
row += 1
col = 0
wb.close()
return output
def _format_title(self):
# XLSX writer won't allow sheet names > 31 characters or that
# contain invalid characters
# https://github.com/jmcnamara/XlsxWriter/blob/master/xlsxwriter/
# test/workbook/test_check_sheetname.py
title = slugify(self.query.title)
return title[:31]
================================================
FILE: explorer/forms.py
================================================
from django.forms import BooleanField, CharField, ModelForm, ValidationError
from django.forms.widgets import CheckboxInput, Select
from django.db.models import Value, IntegerField, When, Case
from explorer.models import MSG_FAILED_BLACKLIST, Query
from explorer.ee.db_connections.models import DatabaseConnection
from explorer.ee.db_connections.utils import default_db_connection
class SqlField(CharField):
def validate(self, value):
"""
Ensure that the SQL passes the blacklist.
:param value: The SQL for this Query model.
"""
super().validate(value)
query = Query(sql=value)
passes_blacklist, failing_words = query.passes_blacklist()
error = MSG_FAILED_BLACKLIST % ", ".join(
failing_words) if not passes_blacklist else None
if error:
raise ValidationError(
error,
code="InvalidSql"
)
class QueryForm(ModelForm):
sql = SqlField()
snapshot = BooleanField(widget=CheckboxInput, required=False)
few_shot = BooleanField(widget=CheckboxInput, required=False)
database_connection = CharField(widget=Select, required=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["database_connection"].widget.choices = self.connections
if not self.instance.database_connection:
default_db = default_db_connection()
self.initial["database_connection"] = default_db_connection().alias if default_db else None
self.fields["database_connection"].widget.attrs["class"] = "form-select"
def clean(self):
# Don't overwrite created_by_user
if self.instance and self.instance.created_by_user:
self.cleaned_data["created_by_user"] = \
self.instance.created_by_user
return super().clean()
def clean_database_connection(self):
connection_id = self.cleaned_data.get("database_connection")
if connection_id:
try:
return DatabaseConnection.objects.get(id=connection_id)
except DatabaseConnection.DoesNotExist as e:
raise ValidationError("Invalid database connection selected.") from e
return None
@property
def created_at_time(self):
return self.instance.created_at.strftime("%Y-%m-%d")
@property
def connections(self):
default_db = default_db_connection()
if default_db is None:
return []
# Ensure the default connection appears first in the dropdown in the form
result = DatabaseConnection.objects.annotate(
custom_order=Case(
When(id=default_db_connection().id, then=Value(0)),
default=Value(1),
output_field=IntegerField(),
)
).order_by("custom_order", "id")
return [(c.id, c.alias) for c in result.all()]
class Meta:
model = Query
fields = ["title", "sql", "description", "snapshot", "database_connection", "few_shot"]
================================================
FILE: explorer/locale/ru/LC_MESSAGES/django.po
================================================
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-07-24 16:19-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n"
"%100>=11 && n%100<=14)? 2 : 3);\n"
#: explorer/apps.py:10 explorer/templates/explorer/base.html:9
#: explorer/templates/explorer/base.html:34
#: explorer/templates/explorer/fullscreen.html:8
msgid "SQL Explorer"
msgstr "SQL Навигатор"
#: explorer/models.py:40
msgid "Include in snapshot task (if enabled)"
msgstr "Включить в задачу снятия снепшота (если разрешено)"
#: explorer/models.py:47
msgid ""
"Name of DB connection (as specified in settings) to use for this query.Will "
"use EXPLORER_DEFAULT_CONNECTION if left blank"
msgstr ""
"Название соединения БД (как указано в настройках), чтобы использовать для "
"этого запроса.Если не использовать, будет использоваться соединение по "
"умолчанию"
#: explorer/models.py:60 explorer/templates/explorer/query_list.html:22
#: explorer/templates/explorer/query_list.html:57
msgid "Query"
msgstr "Запрос"
#: explorer/models.py:61
msgid "Queries"
msgstr "Запросы"
#: explorer/templates/explorer/fullscreen.html:20
#: explorer/templates/explorer/play.html:6
#: explorer/templates/explorer/query.html:7
#: explorer/templates/explorer/query.html:26
#: explorer/templates/explorer/query_list.html:6
#: explorer/templates/explorer/querylog_list.html:6
msgid "New Query"
msgstr "Новый запрос"
#: explorer/templates/explorer/fullscreen.html:46
#: explorer/templates/explorer/preview_pane.html:106
msgid "Empty Resultset"
msgstr "Результат запроса пуст"
#: explorer/templates/explorer/play.html:7
#: explorer/templates/explorer/play.html:15
#: explorer/templates/explorer/query.html:9
#: explorer/templates/explorer/query_list.html:7
#: explorer/templates/explorer/querylog_list.html:7
#: explorer/templates/explorer/querylog_list.html:23
#: explorer/templates/explorer/querylog_list.html:41
msgid "Playground"
msgstr "Полигон"
#: explorer/templates/explorer/play.html:8
#: explorer/templates/explorer/query.html:14
#: explorer/templates/explorer/query_list.html:8
#: explorer/templates/explorer/querylog_list.html:8
msgid "Logs"
msgstr "Журналы"
#: explorer/templates/explorer/play.html:17
msgid ""
"The playground is for experimenting and writing ad-hoc queries. By default, "
"nothing you do here will be saved."
msgstr ""
"Полигон предназначен для экспериментов и написания специальных запросов. По "
"умолчанию тут ничего не будет сохраняться"
#: explorer/templates/explorer/play.html:27
#: explorer/templates/explorer/query.html:61
msgid "Connection"
msgstr "Соединение"
#: explorer/templates/explorer/play.html:42
msgid "Playground SQL"
msgstr "Экспериментальный SQL"
#: explorer/templates/explorer/play.html:62
#: explorer/templates/explorer/query.html:147
msgid "Refresh"
msgstr "Обновить"
#: explorer/templates/explorer/play.html:65
#: explorer/templates/explorer/play.html:77
#: explorer/templates/explorer/query.html:127
#: explorer/templates/explorer/query.html:154
msgid "Toggle Dropdown"
msgstr "Включить выпадающий список"
#: explorer/templates/explorer/play.html:68
msgid "Save As New Query"
msgstr "Сохранить как новый запрос"
#: explorer/templates/explorer/play.html:73
#: explorer/templates/explorer/query.html:150
msgid "Download"
msgstr "Скачать"
#: explorer/templates/explorer/play.html:85
#: explorer/templates/explorer/query.html:141
msgid "Show Schema"
msgstr "Показать схему базы"
#: explorer/templates/explorer/play.html:88
#: explorer/templates/explorer/query.html:144
msgid "Hide Schema"
msgstr "Скрыть схему"
#: explorer/templates/explorer/play.html:91
#: explorer/templates/explorer/query.html:138
msgid "Format"
msgstr "Отформатировать запрос"
#: explorer/templates/explorer/play.html:95
msgid "Playground Query"
msgstr "Экспериментальный запрос"
#: explorer/templates/explorer/preview_pane.html:10
msgid "Preview"
msgstr "Предпросмотр"
#: explorer/templates/explorer/preview_pane.html:16
msgid "Snapshots"
msgstr "Варианты"
#: explorer/templates/explorer/preview_pane.html:23
#: explorer/templates/explorer/preview_pane.html:139
msgid "Pivot"
msgstr "Сводная таблица"
#: explorer/templates/explorer/preview_pane.html:38
#, python-format
msgid "Execution time: %(duration)s ms"
msgstr "Время выполнения: %(duration)s мс"
#: explorer/templates/explorer/preview_pane.html:46
msgid "Showing"
msgstr "Отображение"
#: explorer/templates/explorer/preview_pane.html:48
msgid "First"
msgstr "Первый"
#: explorer/templates/explorer/preview_pane.html:50
#, python-format
msgid "of %(total_rows)s total rows."
msgstr "из %(total_rows)s выбранных записей."
#: explorer/templates/explorer/query.html:12
msgid "Query Detail"
msgstr "Подробности запроса"
#: explorer/templates/explorer/query.html:33
msgid "History"
msgstr "История"
#: explorer/templates/explorer/query.html:54
msgid "Title"
msgstr "Название"
#: explorer/templates/explorer/query.html:77
msgid "Description"
msgstr "Описание"
#: explorer/templates/explorer/query.html:124
msgid "Save & Run"
msgstr "Сохранить и запустить"
#: explorer/templates/explorer/query.html:133
msgid "Save Only"
msgstr "Сохранить"
#: explorer/templates/explorer/query.html:176
msgid "Snapshot"
msgstr "Вариант"
#: explorer/templates/explorer/query.html:182
#, python-format
msgid ""
"Avg. execution: %(avg_duration|floatformat:2)sms. Query created by "
"%(user_email)s on %(created)s."
msgstr ""
"Среднее время выполнения: %(avg_duration|floatformat:2)s мс. Запрос в "
"%(created)s. от «%(user_email)s»"
#: explorer/templates/explorer/query_confirm_delete.html:7
#, python-format
msgid "Are you sure you want to delete \"%(title)s\"?"
msgstr "Вы уверены в удалении «%(title)s»?"
#: explorer/templates/explorer/query_list.html:15
#, python-format
msgid "Recently Run by You"
msgstr "Ваши последние запуски запросов, их %(qlen)s"
#: explorer/templates/explorer/query_list.html:23
msgid "Last Run"
msgstr "Последний запуск"
#: explorer/templates/explorer/query_list.html:48
msgid "All Queries"
msgstr "Все запросы"
#: explorer/templates/explorer/query_list.html:51
#: explorer/templates/explorer/schema.html:10
msgid "Search"
msgstr "Поиск"
#: explorer/templates/explorer/query_list.html:58
msgid "Created"
msgstr "Создан"
#: explorer/templates/explorer/query_list.html:60
msgid "Email"
msgstr "Емейл"
#: explorer/templates/explorer/query_list.html:62
msgid "CSV"
msgstr "CSV"
#: explorer/templates/explorer/query_list.html:64
msgid "Play"
msgstr "Запустить"
#: explorer/templates/explorer/query_list.html:65
msgid "Delete"
msgstr "Удалить"
#: explorer/templates/explorer/query_list.html:67
msgid "Run Count"
msgstr "Число запусков"
#: explorer/templates/explorer/query_list.html:87
#, python-format
msgid "by %(cuser)s"
msgstr " пользователем «%(cuser)s»"
#: explorer/templates/explorer/querylog_list.html:13
#, python-format
msgid "Recent Query Logs - Page %(pagenum)s"
msgstr "Журнал последних запросов — страница %(pagenum)s "
#: explorer/templates/explorer/querylog_list.html:18
msgid "Run At"
msgstr "Время запуска"
#: explorer/templates/explorer/querylog_list.html:19
msgid "Run By"
msgstr "Запущено пользователем"
#: explorer/templates/explorer/querylog_list.html:20
msgid "Duration"
msgstr "Длительность"
#: explorer/templates/explorer/querylog_list.html:22
msgid "Query ID"
msgstr "ID запроса"
#: explorer/templates/explorer/querylog_list.html:36
#, python-format
msgid "Query %(query_id)s"
msgstr "пользователем «%(query_id)s»"
#: explorer/templates/explorer/querylog_list.html:48
msgid "Open"
msgstr "Открыть"
#: explorer/templates/explorer/querylog_list.html:63
#, python-format
msgid "Page %(pnum)s of %(anum)s."
msgstr "Страница %(pnum)s из %(anum)s."
#: explorer/templates/explorer/schema.html:7
msgid "Schema"
msgstr "Схема"
#: explorer/templates/explorer/schema.html:15
msgid "Collapse All"
msgstr "Свернуть все"
#: explorer/templates/explorer/schema.html:20
msgid "Expand All"
msgstr "Развернуть все"
#: explorer/templates/explorer/schema_building.html:6
msgid "Schema is building..."
msgstr "Строится схема…"
#: explorer/templates/explorer/schema_building.html:7
msgid "Please wait a minute, and refresh."
msgstr "Подождите минутку и обновите."
#: explorer/views/query.py:125
msgid "Query saved."
msgstr "Запрос сохранен."
#~ msgid "SQL"
#~ msgstr "SQL"
#~ msgid "Avg. execution:"
#~ msgstr "Среднее время запроса"
#~ msgid "Query created by"
#~ msgstr "Запрос создан"
#~ msgid "on"
#~ msgstr "в"
================================================
FILE: explorer/locale/zh_Hans/LC_MESSAGES/django.po
================================================
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-10-21 13:58+0800\n"
"PO-Revision-Date: 2018-10-21 13:58+0806\n"
"Last-Translator: b' <admin@xx.com>'\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Translated-Using: django-rosetta 0.9.0\n"
#: apps.py:10 templates/explorer/base.html:9 templates/explorer/base.html:33
msgid "SQL Explorer"
msgstr "SQL浏览器"
#: models.py:38
msgid "Include in snapshot task (if enabled)"
msgstr "包含在快照任务中(如果启用了的话)"
#: models.py:40
msgid ""
"Name of DB connection (as specified in settings) to use for this query. Will"
" use EXPLORER_DEFAULT_CONNECTION if left blank"
msgstr "该查询需要使用的数据库连接名字(配置文件中设定的),默认使用 EXPLORER_DEFAULT_CONNECTION"
#: models.py:49 templates/explorer/query_list.html:49
msgid "Query"
msgstr "查询语句"
#: models.py:50
msgid "Queries"
msgstr "查询语句"
#: templates/explorer/play.html:7 templates/explorer/query.html:7
#: templates/explorer/query.html:17 templates/explorer/query_list.html:6
#: templates/explorer/querylog_list.html:6
msgid "New Query"
msgstr "新增查询"
#: templates/explorer/play.html:8 templates/explorer/play.html:16
#: templates/explorer/query.html:8 templates/explorer/query_list.html:7
#: templates/explorer/querylog_list.html:7
msgid "Playground"
msgstr "实验场"
#: templates/explorer/play.html:9 templates/explorer/query.html:11
#: templates/explorer/query_list.html:8
#: templates/explorer/querylog_list.html:8
msgid "Logs"
msgstr "日志"
#: templates/explorer/play.html:17
msgid ""
"The playground is for experimenting and writing ad-hoc queries. By default, "
"nothing you do here will be saved."
msgstr "试验场用于实验以及编写临时查询语句。默认情况下,这里所有操作都不会被保存。"
#: templates/explorer/play.html:24 templates/explorer/query.html:47
msgid "Connection"
msgstr "连接"
#: templates/explorer/play.html:39
msgid "Playground SQL"
msgstr "实验SQL"
#: templates/explorer/play.html:55 templates/explorer/query.html:110
msgid "Refresh"
msgstr "刷新"
#: templates/explorer/play.html:58 templates/explorer/play.html:68
#: templates/explorer/query.html:98 templates/explorer/query.html:115
msgid "Toggle Dropdown"
msgstr "切换下拉框"
#: templates/explorer/play.html:61
msgid "Save As New Query"
msgstr "保存为新的查询语句"
#: templates/explorer/play.html:65 templates/explorer/query.html:112
msgid "Download"
msgstr "下载"
#: templates/explorer/play.html:75 templates/explorer/query.html:106
msgid "Show Schema"
msgstr "显示表结构"
#: templates/explorer/play.html:76 templates/explorer/query.html:107
msgid "Hide Schema"
msgstr "隐藏表结构"
#: templates/explorer/play.html:77 templates/explorer/query.html:108
msgid "Format"
msgstr "格式"
#: templates/explorer/play.html:80
msgid "Playground Query"
msgstr "实验查询语句"
#: templates/explorer/preview_pane.html:7
msgid "Preview"
msgstr "预览"
#: templates/explorer/preview_pane.html:8
msgid "Snapshots"
msgstr "快照"
#: templates/explorer/preview_pane.html:9
msgid "Pivot"
msgstr ""
#: templates/explorer/query.html:10
msgid "Query Detail"
msgstr "查询细节"
#: templates/explorer/query.html:20
msgid "History"
msgstr "历史"
#: templates/explorer/query.html:62
msgid "Description"
msgstr "描述"
#: templates/explorer/query.html:95
msgid "Save & Run"
msgstr "保存并运行"
#: templates/explorer/query.html:103
msgid "Save Only"
msgstr "保存"
#: templates/explorer/query_list.html:40
msgid "All Queries"
msgstr "所有查询语句"
#: templates/explorer/query_list.html:43
msgid "Search"
msgstr "搜索"
#: templates/explorer/query_list.html:50
msgid "Created"
msgstr "创建时间"
#: templates/explorer/query_list.html:52
msgid "Email"
msgstr "邮箱"
#: templates/explorer/query_list.html:54
msgid "CSV"
msgstr "CSV"
#: templates/explorer/query_list.html:56
msgid "Play"
msgstr "执行"
#: templates/explorer/query_list.html:57
msgid "Delete"
msgstr "删除"
#: templates/explorer/query_list.html:59
msgid "Run Count"
msgstr "运行次数"
#: templates/explorer/querylog_list.html:13
#, python-format
msgid "Recent Query Logs - Page %(page_obj.number)s"
msgstr "最近的查询日志 - %(page_obj.number)s页"
#: templates/explorer/schema.html:14
msgid "Collapse All"
msgstr "全部收起"
#: templates/explorer/schema.html:17
msgid "Expand All"
msgstr "全部展开"
================================================
FILE: explorer/migrations/0001_initial.py
================================================
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Query',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('title', models.CharField(max_length=255)),
('sql', models.TextField(blank=True)),
('description', models.TextField(blank=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('last_run_date', models.DateTimeField(auto_now=True)),
('created_by_user', models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE)),
],
options={
'ordering': ['title'],
'verbose_name_plural': 'Queries',
},
bases=(models.Model,),
),
migrations.CreateModel(
name='QueryLog',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('sql', models.TextField(blank=True)),
('is_playground', models.BooleanField(default=False)),
('run_at', models.DateTimeField(auto_now_add=True)),
('query', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, blank=True, to='explorer.Query', null=True)),
('run_by_user', models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE)),
],
options={
'ordering': ['-run_at'],
},
bases=(models.Model,),
),
]
================================================
FILE: explorer/migrations/0002_auto_20150501_1515.py
================================================
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('explorer', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='querylog',
name='is_playground',
),
migrations.AlterField(
model_name='querylog',
name='sql',
field=models.TextField(blank=True),
),
]
================================================
FILE: explorer/migrations/0003_query_snapshot.py
================================================
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('explorer', '0002_auto_20150501_1515'),
]
operations = [
migrations.AddField(
model_name='query',
name='snapshot',
field=models.BooleanField(default=False, help_text=b'Include in snapshot task (if enabled)'),
),
]
================================================
FILE: explorer/migrations/0004_querylog_duration.py
================================================
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('explorer', '0003_query_snapshot'),
]
operations = [
migrations.AddField(
model_name='querylog',
name='duration',
field=models.FloatField(null=True, blank=True),
),
]
================================================
FILE: explorer/migrations/0005_auto_20160105_2052.py
================================================
# Generated by Django 1.9 on 2016-01-05 20:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('explorer', '0004_querylog_duration'),
]
operations = [
migrations.AlterField(
model_name='query',
name='snapshot',
field=models.BooleanField(default=False, help_text='Include in snapshot task (if enabled)'),
),
]
================================================
FILE: explorer/migrations/0006_query_connection.py
================================================
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('explorer', '0005_auto_20160105_2052'),
]
operations = [
migrations.AddField(
model_name='query',
name='connection',
field=models.CharField(help_text=b'Name of DB connection (as specified in settings) to use for this query. Will use EXPLORER_DEFAULT_CONNECTION if left blank', max_length=128, null=True, blank=True),
),
]
================================================
FILE: explorer/migrations/0007_querylog_connection.py
================================================
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('explorer', '0006_query_connection'),
]
operations = [
migrations.AddField(
model_name='querylog',
name='connection',
field=models.CharField(max_length=128, null=True, blank=True),
),
]
================================================
FILE: explorer/migrations/0008_auto_20190308_1642.py
================================================
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('explorer', '0007_querylog_connection'),
]
operations = [
migrations.AlterField(
model_name='query',
name='connection',
field=models.CharField(blank=True, help_text='Name of DB connection (as specified in settings) to use for this query. Will use EXPLORER_DEFAULT_CONNECTION if left blank', max_length=128, null=True),
),
]
================================================
FILE: explorer/migrations/0009_auto_20201009_0547.py
================================================
# Generated by Django 3.1.2 on 2020-10-09 05:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('explorer', '0008_auto_20190308_1642'),
]
operations = [
migrations.AlterModelOptions(
name='query',
options={'ordering': ['title'], 'verbose_name': 'Query', 'verbose_name_plural': 'Queries'},
),
migrations.AlterField(
model_name='query',
name='connection',
field=models.CharField(blank=True, default='', help_text='Name of DB connection (as specified in settings) to use for this query.Will use EXPLORER_DEFAULT_CONNECTION if left blank', max_length=128),
),
migrations.AlterField(
model_name='querylog',
name='connection',
field=models.CharField(blank=True, default='', max_length=128),
),
]
================================================
FILE: explorer/migrations/0010_sql_required.py
================================================
# Generated by Django 3.0.8 on 2020-12-18 06:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('explorer', '0009_auto_20201009_0547'),
]
operations = [
migrations.AlterField(
model_name='query',
name='sql',
field=models.TextField(),
),
]
================================================
FILE: explorer/migrations/0011_query_favorites.py
================================================
# Generated by Django 3.2.16 on 2023-01-12 18:24
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('explorer', '0010_sql_required'),
]
operations = [
migrations.CreateModel(
name='QueryFavorite',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('query', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='explorer.query')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('query', 'user')},
},
),
]
================================================
FILE: explorer/migrations/0012_alter_queryfavorite_query_alter_queryfavorite_user.py
================================================
# Generated by Django 4.1.7 on 2023-02-27 08:37
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("explorer", "0011_query_favorites"),
]
operations = [
migrations.AlterField(
model_name="queryfavorite",
name="query",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="favorites",
to="explorer.query",
),
),
migrations.AlterField(
model_name="queryfavorite",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="favorites",
to=settings.AUTH_USER_MODEL,
),
),
]
================================================
FILE: explorer/migrations/0013_querylog_error_querylog_success.py
================================================
# Generated by Django 4.2.8 on 2023-12-15 09:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('explorer', '0012_alter_queryfavorite_query_alter_queryfavorite_user'),
]
operations = [
migrations.AddField(
model_name='querylog',
name='error',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='querylog',
name='success',
field=models.BooleanField(default=True),
),
]
================================================
FILE: explorer/migrations/0014_promptlog.py
================================================
# Generated by Django 4.2.8 on 2024-01-11 08:22
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('explorer', '0013_querylog_error_querylog_success'),
]
operations = [
migrations.CreateModel(
name='PromptLog',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('prompt', models.TextField(blank=True)),
('response', models.TextField(blank=True)),
('run_at', models.DateTimeField(auto_now_add=True)),
('duration', models.FloatField(blank=True, null=True)),
('model', models.CharField(blank=True, default='', max_length=128)),
('error', models.TextField(blank=True, null=True)),
('run_by_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]
================================================
FILE: explorer/migrations/0015_explorervalue.py
================================================
# Generated by Django 4.2.8 on 2024-04-25 13:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('explorer', '0014_promptlog'),
]
operations = [
migrations.CreateModel(
name='ExplorerValue',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('key', models.CharField(choices=[('UUID', 'Install Unique ID'), ('SMLS', 'Startup metric last send')], max_length=5)),
('value', models.TextField(blank=True, null=True)),
],
),
]
================================================
FILE: explorer/migrations/0016_alter_explorervalue_key.py
================================================
# Generated by Django 4.2.8 on 2024-04-26 13:05
from django.db import migrations, models
def insert_assistant_prompt(apps, schema_editor):
ExplorerValue = apps.get_model('explorer', 'ExplorerValue')
ExplorerValue.objects.get_or_create(
key="ASP",
value="""You are a data analyst's assistant and will be asked write or modify a SQL query to assist a business
user with their analysis. The user will provide a prompt of what they are looking for help with, and may also
provide SQL they have written so far, relevant table schema, and sample rows from the tables they are querying.
For complex requests, you may use Common Table Expressions (CTEs) to break down the problem into smaller parts.
CTEs are not needed for simpler requests.
"""
)
def insert_assistant_prompt_reverse(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
('explorer', '0015_explorervalue'),
]
operations = [
migrations.AlterField(
model_name='explorervalue',
name='key',
field=models.CharField(choices=[('UUID', 'Install Unique ID'), ('SMLS', 'Startup metric last send'), ('ASP', 'System prompt for SQL Assistant')], max_length=5, unique=True),
),
migrations.RunPython(insert_assistant_prompt, reverse_code=insert_assistant_prompt_reverse),
]
================================================
FILE: explorer/migrations/0017_databaseconnection.py
================================================
# Generated by Django 5.0.4 on 2024-05-07 18:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('explorer', '0016_alter_explorervalue_key'),
]
operations = [
migrations.CreateModel(
name='DatabaseConnection',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('alias', models.CharField(max_length=255, unique=True)),
('engine', models.CharField(choices=[('django.db.backends.sqlite3', 'SQLite3'), ('django.db.backends.postgresql_psycopg2', 'PostgreSQL'), ('django.db.backends.mysql', 'MySQL'), ('django.db.backends.oracle', 'Oracle')], max_length=255)),
('name', models.CharField(max_length=255)),
('user', models.CharField(blank=True, max_length=255)),
('password', models.CharField(blank=True, max_length=255)),
('host', models.CharField(blank=True, max_length=255)),
('port', models.CharField(blank=True, max_length=255)),
],
),
]
================================================
FILE: explorer/migrations/0018_alter_databaseconnection_host_and_more.py
================================================
# Generated by Django 5.0.4 on 2024-05-14 15:55
import django_cryptography.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('explorer', '0017_databaseconnection'),
]
operations = [
migrations.AlterField(
model_name='databaseconnection',
name='host',
field=django_cryptography.fields.encrypt(models.CharField(blank=True, max_length=255)),
),
migrations.AlterField(
model_name='databaseconnection',
name='password',
field=django_cryptography.fields.encrypt(models.CharField(blank=True, max_length=255)),
),
migrations.AlterField(
model_name='databaseconnection',
name='user',
field=django_cryptography.fields.encrypt(models.CharField(blank=True, max_length=255)),
),
]
================================================
FILE: explorer/migrations/0019_alter_databaseconnection_engine.py
================================================
# Generated by Django 5.0.5 on 2024-07-03 12:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('explorer', '0018_alter_databaseconnection_host_and_more'),
]
operations = [
migrations.AlterField(
model_name='databaseconnection',
name='engine',
field=models.CharField(choices=[('django.db.backends.sqlite3', 'SQLite3'), ('django.db.backends.postgresql', 'PostgreSQL'), ('django.db.backends.mysql', 'MySQL'), ('django.db.backends.oracle', 'Oracle'), ('django.db.backends.mysql', 'MariaDB'), ('django_cockroachdb', 'CockroachDB'), ('django.db.backends.sqlserver', 'SQL Server (mssql-django)')], max_length=255),
),
]
================================================
FILE: explorer/migrations/0020_databaseconnection_extras_and_more.py
================================================
# Generated by Django 5.0.4 on 2024-07-08 01:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('explorer', '0019_alter_databaseconnection_engine'),
]
operations = [
migrations.AddField(
model_name='databaseconnection',
name='extras',
field=models.JSONField(blank=True, null=True),
),
migrations.AlterField(
model_name='databaseconnection',
name='engine',
field=models.CharField(choices=[('django.db.backends.sqlite3', 'SQLite3'), ('django.db.backends.postgresql', 'PostgreSQL'), ('django.db.backends.mysql', 'MySQL'), ('django.db.backends.oracle', 'Oracle'), ('django.db.backends.mysql', 'MariaDB'), ('django_cockroachdb', 'CockroachDB'), ('mssql', 'SQL Server (mssql-django)'), ('django_snowflake', 'Snowflake')], max_length=255),
),
]
================================================
FILE: explorer/migrations/0021_alter_databaseconnection_password_and_more.py
================================================
# Generated by Django 5.0.4 on 2024-07-16 01:43
import django_cryptography.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('explorer', '0020_databaseconnection_extras_and_more'),
]
operations = [
migrations.AlterField(
model_name='databaseconnection',
name='password',
field=django_cryptography.fields.encrypt(models.CharField(blank=True, max_length=255, null=True)),
),
migrations.AlterField(
model_name='databaseconnection',
name='user',
field=django_cryptography.fields.encrypt(models.CharField(blank=True, max_length=255, null=True)),
),
]
================================================
FILE: explorer/migrations/0022_databaseconnection_upload_fingerprint.py
================================================
# Generated by Django 5.0.4 on 2024-07-24 20:08
from django.db import m
gitextract_ainzhnq4/ ├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── codeql-analysis.yml │ ├── docs.yml │ ├── lint.yml │ ├── publish-pypi.yml │ ├── publish-test.yml │ └── test.yml ├── .gitignore ├── .nvmrc ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── AUTHORS ├── Dockerfile ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docker-compose.yml ├── docs/ │ ├── Makefile │ ├── _static/ │ │ └── .directory │ ├── _templates/ │ │ └── .directory │ ├── conf.py │ ├── dependencies.rst │ ├── development.rst │ ├── features.rst │ ├── history.rst │ ├── index.rst │ ├── install.rst │ ├── make.bat │ ├── requirements.txt │ └── settings.rst ├── entrypoint.sh ├── explorer/ │ ├── __init__.py │ ├── actions.py │ ├── admin.py │ ├── app_settings.py │ ├── apps.py │ ├── assistant/ │ │ ├── __init__.py │ │ ├── forms.py │ │ ├── models.py │ │ ├── urls.py │ │ ├── utils.py │ │ └── views.py │ ├── charts.py │ ├── ee/ │ │ ├── LICENSE │ │ ├── __init__.py │ │ ├── db_connections/ │ │ │ ├── __init__.py │ │ │ ├── admin.py │ │ │ ├── create_sqlite.py │ │ │ ├── forms.py │ │ │ ├── mime.py │ │ │ ├── models.py │ │ │ ├── type_infer.py │ │ │ ├── utils.py │ │ │ └── views.py │ │ └── urls.py │ ├── exporters.py │ ├── forms.py │ ├── locale/ │ │ ├── ru/ │ │ │ └── LC_MESSAGES/ │ │ │ ├── django.mo │ │ │ └── django.po │ │ └── zh_Hans/ │ │ └── LC_MESSAGES/ │ │ ├── django.mo │ │ └── django.po │ ├── migrations/ │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20150501_1515.py │ │ ├── 0003_query_snapshot.py │ │ ├── 0004_querylog_duration.py │ │ ├── 0005_auto_20160105_2052.py │ │ ├── 0006_query_connection.py │ │ ├── 0007_querylog_connection.py │ │ ├── 0008_auto_20190308_1642.py │ │ ├── 0009_auto_20201009_0547.py │ │ ├── 0010_sql_required.py │ │ ├── 0011_query_favorites.py │ │ ├── 0012_alter_queryfavorite_query_alter_queryfavorite_user.py │ │ ├── 0013_querylog_error_querylog_success.py │ │ ├── 0014_promptlog.py │ │ ├── 0015_explorervalue.py │ │ ├── 0016_alter_explorervalue_key.py │ │ ├── 0017_databaseconnection.py │ │ ├── 0018_alter_databaseconnection_host_and_more.py │ │ ├── 0019_alter_databaseconnection_engine.py │ │ ├── 0020_databaseconnection_extras_and_more.py │ │ ├── 0021_alter_databaseconnection_password_and_more.py │ │ ├── 0022_databaseconnection_upload_fingerprint.py │ │ ├── 0023_query_database_connection_and_more.py │ │ ├── 0024_auto_20240803_1135.py │ │ ├── 0025_alter_query_database_connection_alter_querylog_database_connection.py │ │ ├── 0026_tabledescription.py │ │ ├── 0027_query_few_shot.py │ │ ├── 0028_promptlog_database_connection_promptlog_user_request.py │ │ └── __init__.py │ ├── models.py │ ├── permissions.py │ ├── schema.py │ ├── src/ │ │ ├── js/ │ │ │ ├── assistant.js │ │ │ ├── codemirror-config.js │ │ │ ├── csrf.js │ │ │ ├── explorer.js │ │ │ ├── favorites.js │ │ │ ├── main.js │ │ │ ├── pivot-setup.js │ │ │ ├── pivot.js │ │ │ ├── query-list.js │ │ │ ├── schema.js │ │ │ ├── schemaService.js │ │ │ ├── table-to-csv.js │ │ │ ├── tableDescription.js │ │ │ └── uploads.js │ │ └── scss/ │ │ ├── assistant.scss │ │ ├── choices.scss │ │ ├── explorer.scss │ │ ├── pivot.css │ │ ├── styles.scss │ │ └── variables.scss │ ├── tasks.py │ ├── telemetry.py │ ├── templates/ │ │ ├── assistant/ │ │ │ ├── table_description_confirm_delete.html │ │ │ ├── table_description_form.html │ │ │ └── table_description_list.html │ │ ├── connections/ │ │ │ ├── connection_upload.html │ │ │ ├── connections.html │ │ │ ├── database_connection_confirm_delete.html │ │ │ ├── database_connection_detail.html │ │ │ └── database_connection_form.html │ │ └── explorer/ │ │ ├── assistant.html │ │ ├── base.html │ │ ├── export_buttons.html │ │ ├── fullscreen.html │ │ ├── params.html │ │ ├── pdf_template.html │ │ ├── play.html │ │ ├── preview_pane.html │ │ ├── query.html │ │ ├── query_confirm_delete.html │ │ ├── query_favorite_button.html │ │ ├── query_favorites.html │ │ ├── query_list.html │ │ ├── querylog_list.html │ │ ├── schema.html │ │ └── schema_error.html │ ├── templatetags/ │ │ ├── __init__.py │ │ ├── explorer_tags.py │ │ └── vite.py │ ├── tests/ │ │ ├── __init__.py │ │ ├── csvs/ │ │ │ ├── all_types.csv │ │ │ ├── dates.csv │ │ │ ├── floats.csv │ │ │ ├── integers.csv │ │ │ ├── mixed.csv │ │ │ ├── rc_sample.csv │ │ │ └── test_case1.csv │ │ ├── factories.py │ │ ├── json/ │ │ │ ├── github.json │ │ │ ├── kings.json │ │ │ └── list.json │ │ ├── settings.py │ │ ├── settings_base.py │ │ ├── test_actions.py │ │ ├── test_apps.py │ │ ├── test_assistant.py │ │ ├── test_create_sqlite.py │ │ ├── test_csrf_cookie_name.py │ │ ├── test_db_connection_utils.py │ │ ├── test_exporters.py │ │ ├── test_forms.py │ │ ├── test_mime.py │ │ ├── test_models.py │ │ ├── test_schema.py │ │ ├── test_tasks.py │ │ ├── test_telemetry.py │ │ ├── test_type_infer.py │ │ ├── test_utils.py │ │ └── test_views.py │ ├── urls.py │ ├── utils.py │ └── views/ │ ├── __init__.py │ ├── auth.py │ ├── create.py │ ├── delete.py │ ├── download.py │ ├── email.py │ ├── export.py │ ├── format_sql.py │ ├── list.py │ ├── mixins.py │ ├── query.py │ ├── query_favorite.py │ ├── schema.py │ ├── stream.py │ └── utils.py ├── manage.py ├── package.json ├── public_key.pem ├── pypi-release-checklist.md ├── requirements/ │ ├── base.txt │ ├── dev.txt │ ├── extra/ │ │ ├── assistant.txt │ │ ├── charts.txt │ │ ├── snapshots.txt │ │ ├── uploads.txt │ │ └── xls.txt │ └── tests.txt ├── ruff.toml ├── setup.cfg ├── setup.py ├── test_project/ │ ├── __init__.py │ ├── celery_config.py │ ├── settings.py │ └── urls.py ├── tox.ini └── vite.config.mjs
SYMBOL INDEX (755 symbols across 102 files)
FILE: explorer/__init__.py
function get_version (line 10) | def get_version(short=False):
FILE: explorer/actions.py
function generate_report_action (line 12) | def generate_report_action(description="Generate CSV file from SQL query...
function _package (line 31) | def _package(queries):
function _build_zip (line 50) | def _build_zip(queries):
FILE: explorer/admin.py
class QueryAdmin (line 9) | class QueryAdmin(admin.ModelAdmin):
class ExplorerValueAdmin (line 17) | class ExplorerValueAdmin(admin.ModelAdmin):
method display_key (line 22) | def display_key(self, obj):
FILE: explorer/app_settings.py
function has_assistant (line 176) | def has_assistant():
function db_connections_enabled (line 180) | def db_connections_enabled():
function user_uploads_enabled (line 184) | def user_uploads_enabled():
FILE: explorer/apps.py
class ExplorerAppConfig (line 6) | class ExplorerAppConfig(AppConfig):
function new_get_connection (line 26) | def new_get_connection(using=None):
FILE: explorer/assistant/forms.py
class TableDescriptionForm (line 6) | class TableDescriptionForm(forms.ModelForm):
class Meta (line 7) | class Meta:
method __init__ (line 15) | def __init__(self, *args, **kwargs):
FILE: explorer/assistant/models.py
class PromptLog (line 6) | class PromptLog(models.Model):
class Meta (line 8) | class Meta:
class TableDescription (line 27) | class TableDescription(models.Model):
class Meta (line 29) | class Meta:
method __str__ (line 37) | def __str__(self):
FILE: explorer/assistant/utils.py
function openai_client (line 16) | def openai_client():
function do_req (line 24) | def do_req(prompt):
function extract_response (line 37) | def extract_response(r):
function table_schema (line 41) | def table_schema(db_connection, table_name):
function sample_rows_from_table (line 48) | def sample_rows_from_table(connection, table_name):
function format_rows_from_table (line 85) | def format_rows_from_table(rows):
function build_system_prompt (line 97) | def build_system_prompt(flavor):
function get_relevant_annotation (line 103) | def get_relevant_annotation(db_connection, t):
function get_relevant_few_shots (line 112) | def get_relevant_few_shots(db_connection, included_tables):
function get_few_shot_chunk (line 127) | def get_few_shot_chunk(db_connection, included_tables):
class TablePromptData (line 138) | class TablePromptData:
method render (line 144) | def render(self):
function build_prompt (line 156) | def build_prompt(db_connection, assistant_request, included_tables, quer...
FILE: explorer/assistant/views.py
function run_assistant (line 23) | def run_assistant(request_data, user):
class AssistantHelpView (line 64) | class AssistantHelpView(View):
method post (line 66) | def post(self, request, *args, **kwargs):
class TableDescriptionListView (line 79) | class TableDescriptionListView(PermissionRequiredMixin, ExplorerContextM...
class TableDescriptionCreateView (line 86) | class TableDescriptionCreateView(PermissionRequiredMixin, ExplorerContex...
class TableDescriptionUpdateView (line 94) | class TableDescriptionUpdateView(PermissionRequiredMixin, ExplorerContex...
class TableDescriptionDeleteView (line 102) | class TableDescriptionDeleteView(PermissionRequiredMixin, ExplorerContex...
class AssistantHistoryApiView (line 109) | class AssistantHistoryApiView(View):
method post (line 111) | def post(self, request, *args, **kwargs):
FILE: explorer/charts.py
function get_chart (line 8) | def get_chart(result: QueryResult, chart_type: str, num_rows: int) -> Op...
function get_svg (line 60) | def get_svg(fig) -> str:
function is_numeric (line 69) | def is_numeric(column: Iterable) -> bool:
FILE: explorer/ee/db_connections/admin.py
class DatabaseConnectionAdmin (line 7) | class DatabaseConnectionAdmin(admin.ModelAdmin):
FILE: explorer/ee/db_connections/create_sqlite.py
function get_names (line 9) | def get_names(file, append_conn=None, user_id=None):
function parse_to_sqlite (line 24) | def parse_to_sqlite(file, append_conn=None, user_id=None) -> (BytesIO, s...
FILE: explorer/ee/db_connections/forms.py
class JSONTextInput (line 9) | class JSONTextInput(forms.TextInput):
method render (line 10) | def render(self, name, value, attrs=None, renderer=None):
method value_from_datadict (line 17) | def value_from_datadict(self, data, files, name):
class DatabaseConnectionForm (line 27) | class DatabaseConnectionForm(forms.ModelForm):
class Meta (line 28) | class Meta:
FILE: explorer/ee/db_connections/mime.py
function is_csv (line 9) | def is_csv(file):
function is_json (line 23) | def is_json(file):
function is_json_list (line 31) | def is_json_list(file):
function is_sqlite (line 44) | def is_sqlite(file):
FILE: explorer/ee/db_connections/models.py
class DatabaseConnectionManager (line 11) | class DatabaseConnectionManager(models.Manager):
method uploads (line 13) | def uploads(self):
method non_uploads (line 16) | def non_uploads(self):
method default (line 19) | def default(self):
class DatabaseConnection (line 23) | class DatabaseConnection(models.Model):
method __str__ (line 53) | def __str__(self):
method update_fingerprint (line 56) | def update_fingerprint(self):
method local_fingerprint (line 60) | def local_fingerprint(self):
method _download_sqlite (line 64) | def _download_sqlite(self):
method _download_needed (line 69) | def _download_needed(self):
method download_sqlite_if_needed (line 77) | def download_sqlite_if_needed(self):
method is_upload (line 92) | def is_upload(self):
method is_django_alias (line 96) | def is_django_alias(self):
method local_name (line 100) | def local_name(self):
method delete_local_sqlite (line 104) | def delete_local_sqlite(self):
method as_django_connection (line 109) | def as_django_connection(self):
method save (line 147) | def save(self, *args, **kwargs):
FILE: explorer/ee/db_connections/type_infer.py
function get_parser (line 10) | def get_parser(file):
function csv_to_typed_df (line 22) | def csv_to_typed_df(csv_bytes, delimiter=",", has_headers=True):
function json_list_to_typed_df (line 29) | def json_list_to_typed_df(json_bytes):
function json_to_typed_df (line 39) | def json_to_typed_df(json_bytes):
function atof_custom (line 47) | def atof_custom(value):
function df_to_typed_df (line 63) | def df_to_typed_df(df): # noqa
FILE: explorer/ee/db_connections/utils.py
function default_db_connection (line 8) | def default_db_connection():
function default_db_connection_id (line 13) | def default_db_connection_id():
function upload_sqlite (line 18) | def upload_sqlite(db_bytes, path):
function create_connection_for_uploaded_sqlite (line 29) | def create_connection_for_uploaded_sqlite(filename, s3_path):
function user_dbs_local_dir (line 39) | def user_dbs_local_dir():
function uploaded_db_local_path (line 46) | def uploaded_db_local_path(name):
function sqlite_to_bytesio (line 50) | def sqlite_to_bytesio(local_path):
function pandas_to_sqlite (line 59) | def pandas_to_sqlite(df, table_name, local_path):
function quick_hash (line 75) | def quick_hash(file_path, num_samples=10, sample_size=1024):
FILE: explorer/ee/db_connections/views.py
class UploadDbView (line 25) | class UploadDbView(PermissionRequiredMixin, View):
method post (line 29) | def post(self, request): # noqa
class DatabaseConnectionsListView (line 83) | class DatabaseConnectionsListView(PermissionRequiredMixin, ExplorerConte...
class DatabaseConnectionDetailView (line 90) | class DatabaseConnectionDetailView(PermissionRequiredMixin, ExplorerCont...
class DatabaseConnectionCreateView (line 96) | class DatabaseConnectionCreateView(PermissionRequiredMixin, ExplorerCont...
class DatabaseConnectionUploadCreateView (line 104) | class DatabaseConnectionUploadCreateView(TemplateView):
method get_context_data (line 107) | def get_context_data(self, **kwargs):
class DatabaseConnectionUpdateView (line 114) | class DatabaseConnectionUpdateView(PermissionRequiredMixin, ExplorerCont...
class DatabaseConnectionDeleteView (line 122) | class DatabaseConnectionDeleteView(PermissionRequiredMixin, DeleteView):
method delete (line 128) | def delete(self, request, *args, **kwargs):
class DatabaseConnectionRefreshView (line 135) | class DatabaseConnectionRefreshView(PermissionRequiredMixin, View):
method get (line 140) | def get(self, request, pk): # noqa
class DatabaseConnectionValidateView (line 151) | class DatabaseConnectionValidateView(PermissionRequiredMixin, View):
method post (line 156) | def post(self, request, pk=None): # noqa
FILE: explorer/exporters.py
function get_exporter_class (line 15) | def get_exporter_class(format):
class BaseExporter (line 20) | class BaseExporter:
method __init__ (line 26) | def __init__(self, query):
method get_output (line 29) | def get_output(self, **kwargs):
method get_file_output (line 33) | def get_file_output(self, **kwargs):
method _get_output (line 37) | def _get_output(self, res, **kwargs):
method get_filename (line 45) | def get_filename(self):
class CSVExporter (line 49) | class CSVExporter(BaseExporter):
method _get_output (line 55) | def _get_output(self, res, **kwargs):
class JSONExporter (line 68) | class JSONExporter(BaseExporter):
method _get_output (line 74) | def _get_output(self, res, **kwargs):
class ExcelExporter (line 88) | class ExcelExporter(BaseExporter):
method _get_output (line 94) | def _get_output(self, res, **kwargs):
method _format_title (line 131) | def _format_title(self):
FILE: explorer/forms.py
class SqlField (line 10) | class SqlField(CharField):
method validate (line 12) | def validate(self, value):
class QueryForm (line 33) | class QueryForm(ModelForm):
method __init__ (line 40) | def __init__(self, *args, **kwargs):
method clean (line 48) | def clean(self):
method clean_database_connection (line 55) | def clean_database_connection(self):
method created_at_time (line 65) | def created_at_time(self):
method connections (line 69) | def connections(self):
class Meta (line 84) | class Meta:
FILE: explorer/migrations/0001_initial.py
class Migration (line 6) | class Migration(migrations.Migration):
FILE: explorer/migrations/0002_auto_20150501_1515.py
class Migration (line 4) | class Migration(migrations.Migration):
FILE: explorer/migrations/0003_query_snapshot.py
class Migration (line 4) | class Migration(migrations.Migration):
FILE: explorer/migrations/0004_querylog_duration.py
class Migration (line 4) | class Migration(migrations.Migration):
FILE: explorer/migrations/0005_auto_20160105_2052.py
class Migration (line 6) | class Migration(migrations.Migration):
FILE: explorer/migrations/0006_query_connection.py
class Migration (line 4) | class Migration(migrations.Migration):
FILE: explorer/migrations/0007_querylog_connection.py
class Migration (line 4) | class Migration(migrations.Migration):
FILE: explorer/migrations/0008_auto_20190308_1642.py
class Migration (line 4) | class Migration(migrations.Migration):
FILE: explorer/migrations/0009_auto_20201009_0547.py
class Migration (line 6) | class Migration(migrations.Migration):
FILE: explorer/migrations/0010_sql_required.py
class Migration (line 6) | class Migration(migrations.Migration):
FILE: explorer/migrations/0011_query_favorites.py
class Migration (line 8) | class Migration(migrations.Migration):
FILE: explorer/migrations/0012_alter_queryfavorite_query_alter_queryfavorite_user.py
class Migration (line 8) | class Migration(migrations.Migration):
FILE: explorer/migrations/0013_querylog_error_querylog_success.py
class Migration (line 6) | class Migration(migrations.Migration):
FILE: explorer/migrations/0014_promptlog.py
class Migration (line 8) | class Migration(migrations.Migration):
FILE: explorer/migrations/0015_explorervalue.py
class Migration (line 6) | class Migration(migrations.Migration):
FILE: explorer/migrations/0016_alter_explorervalue_key.py
function insert_assistant_prompt (line 6) | def insert_assistant_prompt(apps, schema_editor):
function insert_assistant_prompt_reverse (line 20) | def insert_assistant_prompt_reverse(apps, schema_editor):
class Migration (line 24) | class Migration(migrations.Migration):
FILE: explorer/migrations/0017_databaseconnection.py
class Migration (line 6) | class Migration(migrations.Migration):
FILE: explorer/migrations/0018_alter_databaseconnection_host_and_more.py
class Migration (line 7) | class Migration(migrations.Migration):
FILE: explorer/migrations/0019_alter_databaseconnection_engine.py
class Migration (line 6) | class Migration(migrations.Migration):
FILE: explorer/migrations/0020_databaseconnection_extras_and_more.py
class Migration (line 6) | class Migration(migrations.Migration):
FILE: explorer/migrations/0021_alter_databaseconnection_password_and_more.py
class Migration (line 7) | class Migration(migrations.Migration):
FILE: explorer/migrations/0022_databaseconnection_upload_fingerprint.py
class Migration (line 6) | class Migration(migrations.Migration):
FILE: explorer/migrations/0023_query_database_connection_and_more.py
class Migration (line 7) | class Migration(migrations.Migration):
FILE: explorer/migrations/0024_auto_20240803_1135.py
function populate_new_foreign_key (line 8) | def populate_new_foreign_key(apps, _):
class Migration (line 67) | class Migration(migrations.Migration):
FILE: explorer/migrations/0025_alter_query_database_connection_alter_querylog_database_connection.py
class Migration (line 7) | class Migration(migrations.Migration):
FILE: explorer/migrations/0026_tabledescription.py
class Migration (line 7) | class Migration(migrations.Migration):
FILE: explorer/migrations/0027_query_few_shot.py
class Migration (line 6) | class Migration(migrations.Migration):
FILE: explorer/migrations/0028_promptlog_database_connection_promptlog_user_request.py
class Migration (line 7) | class Migration(migrations.Migration):
FILE: explorer/models.py
class Query (line 29) | class Query(models.Model):
method __init__ (line 62) | def __init__(self, *args, **kwargs):
class Meta (line 67) | class Meta:
method __str__ (line 72) | def __str__(self):
method get_run_count (line 75) | def get_run_count(self):
method last_run_log (line 78) | def last_run_log(self):
method avg_duration_display (line 82) | def avg_duration_display(self):
method avg_duration (line 88) | def avg_duration(self):
method passes_blacklist (line 93) | def passes_blacklist(self):
method final_sql (line 96) | def final_sql(self):
method execute_query_only (line 99) | def execute_query_only(self):
method execute_with_logging (line 116) | def execute_with_logging(self, executing_user):
method execute (line 132) | def execute(self):
method available_params (line 137) | def available_params(self):
method available_params_w_labels (line 152) | def available_params_w_labels(self):
method get_absolute_url (line 168) | def get_absolute_url(self):
method params_for_url (line 172) | def params_for_url(self):
method log (line 175) | def log(self, user=None):
method shared (line 189) | def shared(self):
method snapshots (line 195) | def snapshots(self):
method is_favorite (line 207) | def is_favorite(self, user):
class SnapShot (line 214) | class SnapShot:
method __init__ (line 216) | def __init__(self, url, last_modified):
class QueryLog (line 221) | class QueryLog(models.Model):
method is_playground (line 246) | def is_playground(self):
class Meta (line 249) | class Meta:
class QueryFavorite (line 253) | class QueryFavorite(models.Model):
class Meta (line 265) | class Meta:
class QueryResult (line 269) | class QueryResult:
method __init__ (line 271) | def __init__(self, sql, connection):
method data (line 288) | def data(self):
method headers (line 292) | def headers(self):
method header_strings (line 296) | def header_strings(self):
method _get_headers (line 299) | def _get_headers(self):
method _get_numerics (line 304) | def _get_numerics(self):
method _get_transforms (line 318) | def _get_transforms(self):
method column (line 325) | def column(self, ix):
method process (line 328) | def process(self):
method process_columns (line 336) | def process_columns(self):
method process_rows (line 340) | def process_rows(self):
method execute_query (line 347) | def execute_query(self):
class ColumnHeader (line 361) | class ColumnHeader:
method __init__ (line 363) | def __init__(self, title):
method add_summary (line 367) | def add_summary(self, column):
method __str__ (line 370) | def __str__(self):
class ColumnStat (line 374) | class ColumnStat:
method __init__ (line 376) | def __init__(self, label, statfn, precision=2, handles_null=False):
method __call__ (line 382) | def __call__(self, coldata):
method __str__ (line 387) | def __str__(self):
class ColumnSummary (line 391) | class ColumnSummary:
method __init__ (line 393) | def __init__(self, header, col):
method stats (line 411) | def stats(self):
method __str__ (line 414) | def __str__(self):
class ExplorerValueManager (line 418) | class ExplorerValueManager(models.Manager):
method get_uuid (line 420) | def get_uuid(self):
method get_startup_last_send (line 431) | def get_startup_last_send(self):
method set_startup_last_send (line 441) | def set_startup_last_send(self, ts):
method get_item (line 450) | def get_item(self, key):
class ExplorerValue (line 454) | class ExplorerValue(models.Model):
FILE: explorer/permissions.py
function view_permission (line 5) | def view_permission(request, **kwargs):
function view_permission_list (line 19) | def view_permission_list(request, *args, **kwargs):
function change_permission (line 24) | def change_permission(request, *args, **kwargs):
function connections_permission (line 28) | def connections_permission(request, *args, **kwargs):
FILE: explorer/schema.py
function _get_includes (line 13) | def _get_includes():
function _get_excludes (line 17) | def _get_excludes():
function _include_views (line 21) | def _include_views():
function _include_table (line 25) | def _include_table(t):
function connection_schema_cache_key (line 31) | def connection_schema_cache_key(connection_id):
function connection_schema_json_cache_key (line 35) | def connection_schema_json_cache_key(connection_id):
function transform_to_json_schema (line 39) | def transform_to_json_schema(schema_info):
function schema_json_info (line 48) | def schema_json_info(db_connection):
function schema_info (line 62) | def schema_info(db_connection):
function clear_schema_cache (line 71) | def clear_schema_cache(db_connection):
function build_schema_info (line 79) | def build_schema_info(db_connection):
FILE: explorer/src/js/assistant.js
function getErrorMessage (line 8) | function getErrorMessage() {
function debounce (line 13) | function debounce(func, delay) {
function setupTableList (line 21) | function setupTableList() {
function selectRelevantTablesSql (line 94) | function selectRelevantTablesSql(choices, keys) {
function selectRelevantTablesRequest (line 101) | function selectRelevantTablesRequest(choices, keys) {
function setUpAssistant (line 108) | function setUpAssistant(expand = false) {
function getAssistantHistory (line 142) | function getAssistantHistory() {
function submitAssistantAsk (line 229) | function submitAssistantAsk() {
function setUpCopyButtons (line 288) | function setUpCopyButtons(){
FILE: explorer/src/js/codemirror-config.js
method keydown (line 25) | keydown(event, view) {
function displaySchemaTooltip (line 38) | function displaySchemaTooltip(content) {
function fetchAndShowSchema (line 56) | function fetchAndShowSchema(view) {
FILE: explorer/src/js/csrf.js
function getCsrfToken (line 6) | function getCsrfToken() {
FILE: explorer/src/js/explorer.js
function updateSchema (line 17) | function updateSchema() {
function editorFromTextArea (line 32) | function editorFromTextArea(textarea) {
function selectConnection (line 47) | function selectConnection() {
class ExplorerEditor (line 59) | class ExplorerEditor {
method constructor (line 60) | constructor(queryId) {
method getParams (line 107) | getParams() {
method serializeParams (line 119) | serializeParams(params) {
method updateQueryString (line 127) | updateQueryString(key, value, url) {
method formatSql (line 156) | formatSql() {
method showRows (line 185) | showRows() {
method showSchema (line 192) | showSchema(noAutofocus) {
method hideSchema (line 206) | hideSchema() {
method bind (line 225) | bind() {
FILE: explorer/src/js/favorites.js
function toggleFavorite (line 3) | async function toggleFavorite() {
FILE: explorer/src/js/pivot-setup.js
function pivotSetup (line 4) | function pivotSetup($) {
function savePivotState (line 33) | function savePivotState(state) {
FILE: explorer/src/js/pivot.js
function pivotJq (line 18) | function pivotJq($) {
FILE: explorer/src/js/query-list.js
function searchFocus (line 6) | function searchFocus() {
function expandAll (line 12) | function expandAll(param) {
function setupQueryList (line 20) | function setupQueryList() {
function setUpEmailCsv (line 38) | function setUpEmailCsv() {
FILE: explorer/src/js/schema.js
function searchFocus (line 3) | function searchFocus() {
function setupSchema (line 10) | function setupSchema() {
FILE: explorer/src/js/schemaService.js
function getConnElement (line 29) | function getConnElement() {
FILE: explorer/src/js/table-to-csv.js
function tableToCSV (line 1) | function tableToCSV(tableEl) {
function csvFromTable (line 19) | function csvFromTable(className) {
FILE: explorer/src/js/tableDescription.js
function populateTableList (line 5) | function populateTableList() {
function updateSchema (line 38) | function updateSchema() {
function setupTableDescription (line 42) | function setupTableDescription() {
FILE: explorer/src/js/uploads.js
function setupUploads (line 3) | function setupUploads() {
FILE: explorer/tasks.py
function execute_query (line 33) | def execute_query(query_id, email_address):
function convert_csv_to_bytesio (line 56) | def convert_csv_to_bytesio(csv_exporter):
function snapshot_query (line 65) | def snapshot_query(query_id):
function snapshot_queries (line 80) | def snapshot_queries():
function truncate_querylogs (line 90) | def truncate_querylogs(days):
function build_schema_cache_async (line 99) | def build_schema_cache_async(db_connection_id):
function remove_unused_sqlite_dbs (line 114) | def remove_unused_sqlite_dbs():
function build_async_schemas (line 125) | def build_async_schemas():
FILE: explorer/telemetry.py
function instance_identifier (line 19) | def instance_identifier():
class SelfNamedEnum (line 29) | class SelfNamedEnum(Enum):
method _generate_next_value_ (line 32) | def _generate_next_value_(name, start, count, last_values):
class StatNames (line 36) | class StatNames(SelfNamedEnum):
class Stat (line 44) | class Stat:
method __init__ (line 49) | def __init__(self, name: StatNames, value):
method is_summary (line 56) | def is_summary(self):
method should_send_summary_stats (line 59) | def should_send_summary_stats(self):
method send_summary_stats (line 67) | def send_summary_stats(self):
method track (line 73) | def track(self):
function _send (line 96) | def _send(data):
function _get_install_quarter (line 106) | def _get_install_quarter():
function _gather_summary_stats (line 119) | def _gather_summary_stats():
FILE: explorer/templatetags/explorer_tags.py
function export_buttons (line 11) | def export_buttons(query=None):
function query_favorite_button (line 23) | def query_favorite_button(query_id, is_favorite, extra_classes):
FILE: explorer/templatetags/vite.py
function get_css_link (line 17) | def get_css_link(file: str) -> str:
function get_script (line 26) | def get_script(file: str) -> str:
function get_asset (line 35) | def get_asset(file: str) -> str:
function vite_asset (line 43) | def vite_asset(filename: str):
function vite_hmr_client (line 58) | def vite_hmr_client():
FILE: explorer/tests/factories.py
class UserFactory (line 10) | class UserFactory(DjangoModelFactory):
class Meta (line 12) | class Meta:
class SimpleQueryFactory (line 19) | class SimpleQueryFactory(DjangoModelFactory):
class Meta (line 21) | class Meta:
class QueryLogFactory (line 31) | class QueryLogFactory(DjangoModelFactory):
class Meta (line 33) | class Meta:
FILE: explorer/tests/settings.py
class PrimaryDatabaseRouter (line 39) | class PrimaryDatabaseRouter:
method allow_migrate (line 40) | def allow_migrate(self, db, app_label, model_name=None, **hints):
FILE: explorer/tests/test_actions.py
class TestSqlQueryActions (line 10) | class TestSqlQueryActions(TestCase):
method test_single_query_is_csv_file (line 12) | def test_single_query_is_csv_file(self):
method test_multiple_queries_are_zip_file (line 20) | def test_multiple_queries_are_zip_file(self):
method test_packaging_removes_commas_from_file_name (line 38) | def test_packaging_removes_commas_from_file_name(self):
FILE: explorer/tests/test_apps.py
class PendingMigrationsTests (line 7) | class PendingMigrationsTests(TestCase):
method test_no_pending_migrations (line 9) | def test_no_pending_migrations(self):
FILE: explorer/tests/test_assistant.py
function conn (line 27) | def conn():
class TestAssistantViews (line 32) | class TestAssistantViews(TestCase):
method setUp (line 34) | def setUp(self):
method test_do_modify_query (line 46) | def test_do_modify_query(self, mocked_openai_client):
method test_assistant_help (line 56) | def test_assistant_help(self, mocked_openai_client):
class TestBuildPrompt (line 66) | class TestBuildPrompt(TestCase):
method test_build_prompt_with_vendor_only (line 69) | def test_build_prompt_with_vendor_only(self, mock_get_item):
method test_build_prompt_with_sql_and_annotation (line 78) | def test_build_prompt_with_sql_and_annotation(self, mock_get_item, moc...
method test_build_prompt_with_few_shot (line 92) | def test_build_prompt_with_few_shot(self, mock_get_item, mock_table_sc...
method test_build_prompt_with_sql_and_error (line 106) | def test_build_prompt_with_sql_and_error(self, mock_get_item, mock_sam...
method test_build_prompt_with_extra_tables_fitting_window (line 120) | def test_build_prompt_with_extra_tables_fitting_window(self, mock_get_...
class TestPromptContext (line 133) | class TestPromptContext(TestCase):
method test_retrieves_sample_rows (line 135) | def test_retrieves_sample_rows(self):
method test_truncates_long_strings (line 143) | def test_truncates_long_strings(self):
method test_binary_data (line 159) | def test_binary_data(self):
method test_handles_various_data_types (line 177) | def test_handles_various_data_types(self):
method test_handles_operational_error (line 194) | def test_handles_operational_error(self):
method test_format_rows_from_table (line 205) | def test_format_rows_from_table(self):
method test_schema_info_from_table_names (line 214) | def test_schema_info_from_table_names(self):
method test_schema_info_from_table_names_case_invariant (line 230) | def test_schema_info_from_table_names_case_invariant(self):
class TestAssistantUtils (line 248) | class TestAssistantUtils(TestCase):
method test_sample_rows_from_table (line 250) | def test_sample_rows_from_table(self):
method test_sample_rows_from_tables_no_table_match (line 261) | def test_sample_rows_from_tables_no_table_match(self):
method test_relevant_few_shots (line 268) | def test_relevant_few_shots(self):
method test_get_relevant_annotations (line 283) | def test_get_relevant_annotations(self):
class TestAssistantHistoryApiView (line 306) | class TestAssistantHistoryApiView(TestCase):
method setUp (line 308) | def setUp(self):
method test_assistant_history_api_view (line 314) | def test_assistant_history_api_view(self):
method test_assistant_history_api_view_invalid_json (line 349) | def test_assistant_history_api_view_invalid_json(self):
method test_assistant_history_api_view_no_logs (line 357) | def test_assistant_history_api_view_no_logs(self):
method test_assistant_history_api_view_filtered_results (line 369) | def test_assistant_history_api_view_filtered_results(self):
FILE: explorer/tests/test_create_sqlite.py
function write_sqlite_and_get_row (line 14) | def write_sqlite_and_get_row(f_bytes, table_name):
class TestCreateSqlite (line 29) | class TestCreateSqlite(TestCase):
method test_parse_to_sqlite (line 33) | def test_parse_to_sqlite(self):
method test_parse_to_sqlite_with_no_parser (line 41) | def test_parse_to_sqlite_with_no_parser(self):
class TestGetNames (line 50) | class TestGetNames(TestCase):
method setUp (line 51) | def setUp(self):
method test_no_append_conn (line 60) | def test_no_append_conn(self):
method test_with_append_conn (line 65) | def test_with_append_conn(self):
method test_secure_filename (line 70) | def test_secure_filename(self):
method test_empty_filename (line 76) | def test_empty_filename(self):
method test_invalid_extension (line 81) | def test_invalid_extension(self):
FILE: explorer/tests/test_csrf_cookie_name.py
class TestCsrfCookieName (line 13) | class TestCsrfCookieName(TestCase):
method test_csrf_cookie_name_in_context (line 14) | def test_csrf_cookie_name_in_context(self):
method test_custom_csrf_cookie_name (line 22) | def test_custom_csrf_cookie_name(self):
FILE: explorer/tests/test_db_connection_utils.py
class TestSQLiteConnection (line 17) | class TestSQLiteConnection(TestCase):
method test_get_sqlite_for_connection_downloads_file_if_not_exists (line 20) | def test_get_sqlite_for_connection_downloads_file_if_not_exists(self, ...
method test_get_sqlite_for_connection_skips_download_if_exists (line 38) | def test_get_sqlite_for_connection_skips_download_if_exists(self, mock...
class TestDjangoStyleConnection (line 63) | class TestDjangoStyleConnection(TestCase):
method test_create_django_style_connection_with_extras (line 66) | def test_create_django_style_connection_with_extras(self, mock_load_ba...
class TestPandasToSQLite (line 87) | class TestPandasToSQLite(TestCase):
method test_pandas_to_sqlite (line 89) | def test_pandas_to_sqlite(self):
method test_cant_create_connection_for_unregistered_django_alias (line 121) | def test_cant_create_connection_for_unregistered_django_alias(self):
FILE: explorer/tests/test_exporters.py
class TestCsv (line 16) | class TestCsv(TestCase):
method test_writing_unicode (line 18) | def test_writing_unicode(self):
method test_custom_delimiter (line 33) | def test_custom_delimiter(self):
method test_writing_bom (line 42) | def test_writing_bom(self):
class TestJson (line 49) | class TestJson(TestCase):
method test_writing_json (line 51) | def test_writing_json(self):
method test_writing_datetimes (line 64) | def test_writing_datetimes(self):
class TestExcel (line 78) | class TestExcel(TestCase):
method test_writing_excel (line 81) | def test_writing_excel(self):
method test_writing_dict_fields (line 115) | def test_writing_dict_fields(self):
FILE: explorer/tests/test_forms.py
class TestFormValidation (line 11) | class TestFormValidation(TestCase):
method test_form_is_valid_with_valid_sql (line 13) | def test_form_is_valid_with_valid_sql(self):
method test_form_fails_null (line 18) | def test_form_fails_null(self):
method test_form_fails_blank (line 22) | def test_form_fails_blank(self):
method test_form_fails_blacklist (line 28) | def test_form_fails_blacklist(self):
class QueryFormTestCase (line 35) | class QueryFormTestCase(TestCase):
method test_valid_form_submission (line 37) | def test_valid_form_submission(self):
method test_default_connection_first (line 56) | def test_default_connection_first(self, mocked_default_db_connection):
FILE: explorer/tests/test_mime.py
class TestIsCsvFunction (line 9) | class TestIsCsvFunction(TestCase):
method test_is_csv_with_csv_file (line 11) | def test_is_csv_with_csv_file(self):
method test_is_csv_with_non_csv_file (line 16) | def test_is_csv_with_non_csv_file(self):
method test_is_csv_with_empty_content_type (line 21) | def test_is_csv_with_empty_content_type(self):
class TestIsJsonFunction (line 27) | class TestIsJsonFunction(TestCase):
method test_is_json_with_valid_json (line 29) | def test_is_json_with_valid_json(self):
method test_is_json_with_non_json_file (line 34) | def test_is_json_with_non_json_file(self):
method test_is_json_with_wrong_extension (line 38) | def test_is_json_with_wrong_extension(self):
method test_is_json_with_empty_content_type (line 43) | def test_is_json_with_empty_content_type(self):
class TestIsJsonListFunction (line 49) | class TestIsJsonListFunction(TestCase):
method test_is_json_list_with_valid_json_lines (line 51) | def test_is_json_list_with_valid_json_lines(self):
method test_is_json_list_with_multiline_json (line 56) | def test_is_json_list_with_multiline_json(self):
method test_is_json_list_with_non_json_file (line 61) | def test_is_json_list_with_non_json_file(self):
method test_is_json_list_with_invalid_json_lines (line 65) | def test_is_json_list_with_invalid_json_lines(self):
method test_is_json_list_with_wrong_extension (line 72) | def test_is_json_list_with_wrong_extension(self):
method test_is_json_list_with_empty_file (line 77) | def test_is_json_list_with_empty_file(self):
class IsSqliteTestCase (line 82) | class IsSqliteTestCase(TestCase):
method setUp (line 83) | def setUp(self):
method test_is_sqlite_with_valid_sqlite_file (line 107) | def test_is_sqlite_with_valid_sqlite_file(self):
method test_is_sqlite_with_invalid_sqlite_file_content_type (line 112) | def test_is_sqlite_with_invalid_sqlite_file_content_type(self):
method test_is_sqlite_with_invalid_sqlite_file_header (line 117) | def test_is_sqlite_with_invalid_sqlite_file_header(self):
method test_is_sqlite_with_exception_handling (line 123) | def test_is_sqlite_with_exception_handling(self):
FILE: explorer/tests/test_models.py
class TestQueryModel (line 15) | class TestQueryModel(TestCase):
method test_params_get_merged (line 17) | def test_params_get_merged(self):
method test_default_params_used (line 22) | def test_default_params_used(self):
method test_default_params_used_even_with_labels (line 26) | def test_default_params_used_even_with_labels(self):
method test_default_params_and_labels (line 30) | def test_default_params_and_labels(self):
method test_query_log (line 34) | def test_query_log(self):
method test_query_logs_final_sql (line 45) | def test_query_logs_final_sql(self):
method test_playground_query_log (line 53) | def test_playground_query_log(self):
method test_shared (line 59) | def test_shared(self):
method test_get_run_count (line 66) | def test_get_run_count(self):
method test_avg_duration (line 74) | def test_avg_duration(self):
method test_log_saves_duration (line 86) | def test_log_saves_duration(self):
method test_log_saves_errors (line 94) | def test_log_saves_errors(self):
method test_get_snapshots_sorts_snaps (line 109) | def test_get_snapshots_sorts_snaps(self, mocked_get_s3_bucket, mocked_...
method test_final_sql_uses_merged_params (line 127) | def test_final_sql_uses_merged_params(self):
method test_final_sql_fails_blacklist_with_bad_param (line 133) | def test_final_sql_fails_blacklist_with_bad_param(self):
method test_query_will_execute_with_null_database_connection (line 141) | def test_query_will_execute_with_null_database_connection(self):
class TestQueryResults (line 150) | class TestQueryResults(TestCase):
method setUp (line 152) | def setUp(self):
method test_column_access (line 156) | def test_column_access(self):
method test_headers (line 160) | def test_headers(self):
method test_data (line 164) | def test_data(self):
method test_unicode_with_nulls (line 167) | def test_unicode_with_nulls(self):
method test_summary_gets_built (line 174) | def test_summary_gets_built(self):
method test_summary_gets_built_for_multiple_cols (line 180) | def test_summary_gets_built_for_multiple_cols(self):
method test_numeric_detection (line 189) | def test_numeric_detection(self):
method test_transforms_are_identified (line 192) | def test_transforms_are_identified(self):
method test_transform_alters_row (line 197) | def test_transform_alters_row(self):
method test_multiple_transforms (line 203) | def test_multiple_transforms(self):
method test_get_headers_no_results (line 209) | def test_get_headers_no_results(self):
class TestColumnSummary (line 214) | class TestColumnSummary(TestCase):
method test_executes (line 216) | def test_executes(self):
method test_handles_null_as_zero (line 220) | def test_handles_null_as_zero(self):
method test_empty_data (line 224) | def test_empty_data(self):
class TestDatabaseConnection (line 229) | class TestDatabaseConnection(TestCase):
method test_cant_create_a_connection_with_conflicting_name (line 231) | def test_cant_create_a_connection_with_conflicting_name(self):
method test_local_name_calls_user_dbs_local_dir (line 243) | def test_local_name_calls_user_dbs_local_dir(self, mock_getcwd, mock_e...
method test_single_download_triggered (line 262) | def test_single_download_triggered(self, mock_cache, mock_get_s3_bucket):
method test_skip_download_when_locked (line 285) | def test_skip_download_when_locked(self, mock_cache, mock_get_s3_bucket):
method test_not_downloaded_if_file_exists_and_model_is_unsaved (line 307) | def test_not_downloaded_if_file_exists_and_model_is_unsaved(self, mock...
method test_fingerprint_is_updated_after_download_and_download_is_not_called_again (line 335) | def test_fingerprint_is_updated_after_download_and_download_is_not_cal...
method test_default_is_set (line 390) | def test_default_is_set(self):
FILE: explorer/tests/test_schema.py
function conn (line 11) | def conn():
class TestSchemaInfo (line 15) | class TestSchemaInfo(TestCase):
method setUp (line 17) | def setUp(self):
method test_schema_info_returns_valid_data (line 22) | def test_schema_info_returns_valid_data(self, mocked_excludes,
method test_table_exclusion_list (line 36) | def test_table_exclusion_list(self, mocked_excludes, mocked_includes):
method test_app_inclusion_list (line 45) | def test_app_inclusion_list(self, mocked_excludes, mocked_includes):
method test_app_inclusion_list_excluded (line 55) | def test_app_inclusion_list_excluded(self, mocked_excludes,
method test_app_include_views (line 65) | def test_app_include_views(self, mocked_include_views):
method test_app_exclude_views (line 73) | def test_app_exclude_views(self, mocked_include_views):
method test_transform_to_json (line 80) | def test_transform_to_json(self):
function setup_sample_database_view (line 92) | def setup_sample_database_view():
FILE: explorer/tests/test_tasks.py
class TestTasks (line 19) | class TestTasks(TestCase):
method test_async_results (line 23) | def test_async_results(self, mocked_upload):
method test_async_results_fails_with_message (line 49) | def test_async_results_fails_with_message(self, mocked_upload):
method test_snapshots (line 64) | def test_snapshots(self, mocked_upload):
method test_truncating_querylogs (line 76) | def test_truncating_querylogs(self):
class RemoveUnusedSQLiteDBsTestCase (line 92) | class RemoveUnusedSQLiteDBsTestCase(TestCase):
method set_up_the_things (line 94) | def set_up_the_things(self, offset):
method test_remove_unused_sqlite_dbs (line 115) | def test_remove_unused_sqlite_dbs(self):
method test_do_not_remove_recently_used_db (line 122) | def test_do_not_remove_recently_used_db(self):
FILE: explorer/tests/test_telemetry.py
class TestTelemetry (line 8) | class TestTelemetry(TestCase):
method setUp (line 10) | def setUp(self):
method test_instance_identifier (line 13) | def test_instance_identifier(self):
method test_gather_summary_stats (line 21) | def test_gather_summary_stats(self):
method test_stats_not_sent_too_frequently (line 28) | def test_stats_not_sent_too_frequently(self, mocked_app_settings, mock...
method test_stats_not_sent_if_disabled (line 54) | def test_stats_not_sent_if_disabled(self, mocked_app_settings, mocked_...
method test_get_install_quarter_with_no_migrations (line 61) | def test_get_install_quarter_with_no_migrations(self, mock_filter):
method test_get_install_quarter_edge_cases (line 67) | def test_get_install_quarter_edge_cases(self, mock_filter):
FILE: explorer/tests/test_type_infer.py
function _get_csv (line 10) | def _get_csv(csv_name):
function _get_json (line 21) | def _get_json(json_name):
class TestCsvToTypedDf (line 33) | class TestCsvToTypedDf(TestCase):
method test_mixed_types (line 35) | def test_mixed_types(self):
method test_all_types (line 41) | def test_all_types(self):
method test_integer_parsing (line 48) | def test_integer_parsing(self):
method test_float_parsing (line 53) | def test_float_parsing(self):
method test_date_parsing (line 57) | def test_date_parsing(self):
class TestJsonToTypedDf (line 69) | class TestJsonToTypedDf(TestCase):
method test_basic_json (line 71) | def test_basic_json(self):
method test_nested_json (line 77) | def test_nested_json(self):
method test_json_list (line 84) | def test_json_list(self):
FILE: explorer/tests/test_utils.py
class TestSqlBlacklist (line 13) | class TestSqlBlacklist(TestCase):
method setUp (line 15) | def setUp(self):
method tearDown (line 18) | def tearDown(self):
method test_overriding_blacklist (line 21) | def test_overriding_blacklist(self):
method test_not_overriding_blacklist (line 27) | def test_not_overriding_blacklist(self):
method test_select_keywords_as_literals (line 33) | def test_select_keywords_as_literals(self):
method test_select_containing_drop_in_word (line 38) | def test_select_containing_drop_in_word(self):
method test_select_with_case (line 42) | def test_select_with_case(self):
method test_select_with_subselect (line 57) | def test_select_with_subselect(self):
method test_select_with_replace_function (line 68) | def test_select_with_replace_function(self):
method test_dml_commit (line 73) | def test_dml_commit(self):
method test_dml_delete (line 78) | def test_dml_delete(self):
method test_dml_insert (line 85) | def test_dml_insert(self):
method test_dml_merge (line 90) | def test_dml_merge(self):
method test_dml_replace (line 101) | def test_dml_replace(self):
method test_dml_rollback (line 106) | def test_dml_rollback(self):
method test_dml_set (line 111) | def test_dml_set(self):
method test_dml_start (line 116) | def test_dml_start(self):
method test_dml_update (line 121) | def test_dml_update(self):
method test_dml_upsert (line 128) | def test_dml_upsert(self):
method test_ddl_alter (line 133) | def test_ddl_alter(self):
method test_ddl_create (line 143) | def test_ddl_create(self):
method test_ddl_drop (line 155) | def test_ddl_drop(self):
method test_ddl_rename (line 160) | def test_ddl_rename(self):
method test_ddl_truncate (line 165) | def test_ddl_truncate(self):
method test_dcl_grant (line 170) | def test_dcl_grant(self):
method test_dcl_revoke (line 175) | def test_dcl_revoke(self):
method test_dcl_revoke_bad_syntax (line 180) | def test_dcl_revoke_bad_syntax(self):
class TestParams (line 186) | class TestParams(TestCase):
method test_swappable_params_are_built_correctly (line 188) | def test_swappable_params_are_built_correctly(self):
method test_params_get_swapped (line 192) | def test_params_get_swapped(self):
method test_empty_params_does_nothing (line 199) | def test_empty_params_does_nothing(self):
method test_non_string_param_gets_swapper (line 205) | def test_non_string_param_gets_swapper(self):
method _assertSwap (line 212) | def _assertSwap(self, tuple):
method test_extracting_params (line 215) | def test_extracting_params(self):
method test_shared_dict_update (line 236) | def test_shared_dict_update(self):
method test_get_params_from_url (line 241) | def test_get_params_from_url(self):
method test_get_params_for_request (line 248) | def test_get_params_for_request(self):
method test_get_params_for_request_empty (line 260) | def test_get_params_for_request_empty(self):
class TestSecureFilename (line 265) | class TestSecureFilename(TestCase):
method test_basic_ascii (line 266) | def test_basic_ascii(self):
method test_special_characters (line 269) | def test_special_characters(self):
method test_leading_trailing_underscores (line 272) | def test_leading_trailing_underscores(self):
method test_unicode_characters (line 277) | def test_unicode_characters(self):
method test_empty_filename (line 281) | def test_empty_filename(self):
method test_bad_extension (line 285) | def test_bad_extension(self):
method test_empty_extension (line 289) | def test_empty_extension(self):
method test_spaces (line 293) | def test_spaces(self):
FILE: explorer/tests/test_views.py
function reload_app_settings (line 29) | def reload_app_settings():
class TestQueryListView (line 37) | class TestQueryListView(TestCase):
method setUp (line 39) | def setUp(self):
method test_admin_required (line 45) | def test_admin_required(self):
method test_headers (line 50) | def test_headers(self):
method test_permissions_show_only_allowed_queries (line 60) | def test_permissions_show_only_allowed_queries(self):
method test_run_count (line 72) | def test_run_count(self):
class TestQueryCreateView (line 80) | class TestQueryCreateView(TestCase):
method setUp (line 82) | def setUp(self):
method test_change_permission_required (line 90) | def test_change_permission_required(self):
method test_renders_with_title (line 95) | def test_renders_with_title(self):
function custom_view (line 102) | def custom_view(request):
class TestQueryDetailView (line 106) | class TestQueryDetailView(TestCase):
method setUp (line 109) | def setUp(self):
method test_query_with_bad_sql_renders_error (line 115) | def test_query_with_bad_sql_renders_error(self):
method test_query_with_bad_sql_renders_error_on_save (line 123) | def test_query_with_bad_sql_renders_error_on_save(self):
method test_posting_query_saves_correctly (line 132) | def test_posting_query_saves_correctly(self):
method test_change_permission_required_to_save_query (line 143) | def test_change_permission_required_to_save_query(self):
method test_modified_date_gets_updated_after_viewing_query (line 157) | def test_modified_date_gets_updated_after_viewing_query(self):
method test_doesnt_render_results_if_show_is_none (line 166) | def test_doesnt_render_results_if_show_is_none(self):
method test_doesnt_render_results_if_show_is_none_on_post (line 176) | def test_doesnt_render_results_if_show_is_none_on_post(self):
method test_doesnt_render_results_if_params_and_no_autorun (line 187) | def test_doesnt_render_results_if_params_and_no_autorun(self):
method test_does_render_results_if_params_and_autorun (line 199) | def test_does_render_results_if_params_and_autorun(self):
method test_does_render_label_if_params_and_autorun (line 211) | def test_does_render_label_if_params_and_autorun(self):
method test_admin_required (line 223) | def test_admin_required(self):
method test_admin_required_with_explorer_no_permission_setting (line 231) | def test_admin_required_with_explorer_no_permission_setting(self):
method test_individual_view_permission (line 243) | def test_individual_view_permission(self):
method test_header_token_auth (line 257) | def test_header_token_auth(self):
method test_url_token_auth (line 270) | def test_url_token_auth(self):
method test_user_query_views (line 284) | def test_user_query_views(self):
method test_query_snapshot_renders (line 304) | def test_query_snapshot_renders(self, mocked_conn):
method test_failing_blacklist_means_query_doesnt_execute (line 323) | def test_failing_blacklist_means_query_doesnt_execute(self):
method test_fullscreen (line 338) | def test_fullscreen(self):
method test_multiple_connections_integration (line 347) | def test_multiple_connections_integration(self):
class TestDownloadView (line 379) | class TestDownloadView(TestCase):
method setUp (line 380) | def setUp(self):
method test_admin_required (line 387) | def test_admin_required(self):
method test_params_in_download (line 394) | def test_params_in_download(self):
method test_download_defaults_to_csv (line 403) | def test_download_defaults_to_csv(self):
method test_download_csv (line 412) | def test_download_csv(self):
method test_bad_query_gives_500 (line 421) | def test_bad_query_gives_500(self):
method test_download_json (line 429) | def test_download_json(self):
class TestQueryPlayground (line 444) | class TestQueryPlayground(TestCase):
method setUp (line 446) | def setUp(self):
method test_empty_playground_renders (line 452) | def test_empty_playground_renders(self):
method test_playground_renders_with_query_sql (line 457) | def test_playground_renders_with_query_sql(self):
method test_playground_renders_with_posted_sql (line 465) | def test_playground_renders_with_posted_sql(self):
method test_playground_doesnt_render_with_posted_sql_if_show_is_none (line 473) | def test_playground_doesnt_render_with_posted_sql_if_show_is_none(self):
method test_playground_renders_with_empty_posted_sql (line 481) | def test_playground_renders_with_empty_posted_sql(self):
method test_query_with_no_resultset_doesnt_throw_error (line 486) | def test_query_with_no_resultset_doesnt_throw_error(self):
method test_admin_required (line 493) | def test_admin_required(self):
method test_admin_required_with_no_permission_view_setting (line 498) | def test_admin_required_with_no_permission_view_setting(self):
method test_loads_query_from_log (line 508) | def test_loads_query_from_log(self):
method test_fails_blacklist (line 517) | def test_fails_blacklist(self):
method test_fullscreen (line 525) | def test_fullscreen(self):
class TestCSVFromSQL (line 536) | class TestCSVFromSQL(TestCase):
method setUp (line 538) | def setUp(self):
method test_admin_required (line 544) | def test_admin_required(self):
method test_downloading_from_playground (line 549) | def test_downloading_from_playground(self):
method test_stream_csv_from_query (line 560) | def test_stream_csv_from_query(self):
class TestSQLDownloadViews (line 568) | class TestSQLDownloadViews(TestCase):
method setUp (line 571) | def setUp(self):
method test_sql_download_csv (line 577) | def test_sql_download_csv(self):
method test_sql_download_respects_connection (line 585) | def test_sql_download_respects_connection(self):
method test_sql_download_csv_with_custom_delim (line 612) | def test_sql_download_csv_with_custom_delim(self):
method test_sql_download_csv_with_tab_delim (line 623) | def test_sql_download_csv_with_tab_delim(self):
method test_sql_download_csv_with_bad_delim (line 632) | def test_sql_download_csv_with_bad_delim(self):
method test_sql_download_json (line 641) | def test_sql_download_json(self):
class TestSchemaView (line 650) | class TestSchemaView(TestCase):
method setUp (line 652) | def setUp(self):
method test_returns_schema_contents (line 659) | def test_returns_schema_contents(self):
method test_returns_schema_contents_json (line 666) | def test_returns_schema_contents_json(self):
method test_returns_404_if_conn_doesnt_exist (line 673) | def test_returns_404_if_conn_doesnt_exist(self):
method test_admin_required (line 679) | def test_admin_required(self):
class TestFormat (line 687) | class TestFormat(TestCase):
method setUp (line 689) | def setUp(self):
method test_returns_formatted_sql (line 695) | def test_returns_formatted_sql(self):
class TestParamsInViews (line 705) | class TestParamsInViews(TestCase):
method setUp (line 707) | def setUp(self):
method test_retrieving_query_works_with_params (line 714) | def test_retrieving_query_works_with_params(self):
method test_saving_non_executing_query_with__wrong_url_params_works (line 722) | def test_saving_non_executing_query_with__wrong_url_params_works(self):
method test_users_without_change_permissions_can_use_params (line 732) | def test_users_without_change_permissions_can_use_params(self):
class TestCreatedBy (line 741) | class TestCreatedBy(TestCase):
method setUp (line 743) | def setUp(self):
method test_query_update_doesnt_change_created_user (line 756) | def test_query_update_doesnt_change_created_user(self):
method test_new_query_gets_created_by_logged_in_user (line 765) | def test_new_query_gets_created_by_logged_in_user(self):
class TestQueryLog (line 771) | class TestQueryLog(TestCase):
method setUp (line 773) | def setUp(self):
method test_playground_saves_query_to_log (line 779) | def test_playground_saves_query_to_log(self):
method test_creating_query_does_not_save_to_log (line 786) | def test_creating_query_does_not_save_to_log(self):
method test_query_saves_to_log (line 791) | def test_query_saves_to_log(self):
method test_query_gets_logged_and_appears_on_log_page (line 801) | def test_query_gets_logged_and_appears_on_log_page(self):
method test_admin_required (line 812) | def test_admin_required(self):
method test_is_playground (line 817) | def test_is_playground(self):
class TestEmailQuery (line 824) | class TestEmailQuery(TestCase):
method setUp (line 826) | def setUp(self):
method test_email_calls_task (line 833) | def test_email_calls_task(self, mocked_execute):
method test_no_email (line 842) | def test_no_email(self):
class TestQueryFavorites (line 852) | class TestQueryFavorites(TestCase):
method setUp (line 854) | def setUp(self):
method test_returns_favorite_list (line 862) | def test_returns_favorite_list(self):
class TestQueryFavorite (line 869) | class TestQueryFavorite(TestCase):
method setUp (line 871) | def setUp(self):
method test_toggle (line 878) | def test_toggle(self):
class UploadDbViewTest (line 892) | class UploadDbViewTest(TestCase):
method setUp (line 894) | def setUp(self):
method test_post_csv_file (line 901) | def test_post_csv_file(self):
method test_upload_file (line 922) | def test_upload_file(self, mock_upload_sqlite):
method test_post_no_file (line 961) | def test_post_no_file(self):
method test_delete_existing_connection (line 967) | def test_delete_existing_connection(self):
method test_delete_non_existing_connection (line 982) | def test_delete_non_existing_connection(self):
method test_post_file_too_large (line 988) | def test_post_file_too_large(self):
method test_bad_parse_type (line 998) | def test_bad_parse_type(self, patched_parse):
method test_bad_parse_mime (line 1005) | def test_bad_parse_mime(self):
method test_cant_append_sqlite_to_file (line 1012) | def test_cant_append_sqlite_to_file(self, patched_is_sqlite):
class DatabaseConnectionValidateViewTestCase (line 1026) | class DatabaseConnectionValidateViewTestCase(TestCase):
method setUp (line 1028) | def setUp(self):
method test_validate_connection_success (line 1054) | def test_validate_connection_success(self):
method test_validate_connection_invalid_form (line 1059) | def test_validate_connection_invalid_form(self):
method test_update_existing_connection (line 1064) | def test_update_existing_connection(self):
method test_database_connection_error (line 1071) | def test_database_connection_error(self, mock_load):
class TestDatabaseConnectionRefreshView (line 1079) | class TestDatabaseConnectionRefreshView(TestCase):
method setUp (line 1081) | def setUp(self):
method test_refresh_connection (line 1097) | def test_refresh_connection(self):
method tearDown (line 1119) | def tearDown(self):
class SimpleViewTests (line 1126) | class SimpleViewTests(TestCase):
method setUp (line 1128) | def setUp(self):
method test_database_connection_detail_view (line 1143) | def test_database_connection_detail_view(self):
method test_database_connection_create_view (line 1147) | def test_database_connection_create_view(self):
method test_database_connection_update_view (line 1151) | def test_database_connection_update_view(self):
method test_database_connections_list_view (line 1155) | def test_database_connections_list_view(self):
method test_database_connection_delete_view (line 1159) | def test_database_connection_delete_view(self):
method test_database_connection_upload_view (line 1163) | def test_database_connection_upload_view(self):
method test_table_description_list_view (line 1167) | def test_table_description_list_view(self):
FILE: explorer/utils.py
function passes_blacklist (line 22) | def passes_blacklist(sql: str) -> Tuple[bool, Iterable[str]]:
function walk_tokens (line 42) | def walk_tokens(token: TokenList) -> Iterable[Token]:
function _format_field (line 56) | def _format_field(field):
function param (line 60) | def param(name):
function swap_params (line 64) | def swap_params(sql, params):
function extract_params (line 73) | def extract_params(text):
function safe_login_prompt (line 85) | def safe_login_prompt(request):
function shared_dict_update (line 98) | def shared_dict_update(target, source):
function safe_cast (line 105) | def safe_cast(val, to_type, default=None):
function get_int_from_request (line 112) | def get_int_from_request(request, name, default):
function get_params_from_request (line 117) | def get_params_from_request(request):
function get_params_for_url (line 130) | def get_params_for_url(query):
function url_get_rows (line 135) | def url_get_rows(request):
function url_get_query_id (line 141) | def url_get_query_id(request):
function url_get_log_id (line 145) | def url_get_log_id(request):
function url_get_show (line 149) | def url_get_show(request):
function url_get_fullscreen (line 153) | def url_get_fullscreen(request):
function url_get_params (line 157) | def url_get_params(request):
function allowed_query_pks (line 161) | def allowed_query_pks(user_id):
function user_can_see_query (line 165) | def user_can_see_query(request, **kwargs):
function fmt_sql (line 171) | def fmt_sql(sql):
function noop_decorator (line 175) | def noop_decorator(f):
class InvalidExplorerConnectionException (line 179) | class InvalidExplorerConnectionException(Exception):
function delete_from_s3 (line 183) | def delete_from_s3(s3_path):
function get_s3_bucket (line 194) | def get_s3_bucket():
function s3_csv_upload (line 219) | def s3_csv_upload(key, data):
function s3_url (line 227) | def s3_url(bucket, key):
function is_xls_writer_available (line 235) | def is_xls_writer_available():
function secure_filename (line 243) | def secure_filename(filename):
FILE: explorer/views/auth.py
class PermissionRequiredMixin (line 8) | class PermissionRequiredMixin:
method handle_no_permission (line 13) | def handle_no_permission(request):
method get_permission_required (line 16) | def get_permission_required(self):
method has_permission (line 25) | def has_permission(self, request, *args, **kwargs):
method dispatch (line 33) | def dispatch(self, request, *args, **kwargs):
class SafeLoginView (line 39) | class SafeLoginView(LoginView):
function safe_login_view_wrapper (line 43) | def safe_login_view_wrapper(request):
FILE: explorer/views/create.py
class CreateQueryView (line 8) | class CreateQueryView(PermissionRequiredMixin, ExplorerContextMixin,
method form_valid (line 15) | def form_valid(self, form):
FILE: explorer/views/delete.py
class DeleteQueryView (line 9) | class DeleteQueryView(PermissionRequiredMixin, ExplorerContextMixin,
FILE: explorer/views/download.py
class DownloadQueryView (line 10) | class DownloadQueryView(PermissionRequiredMixin, View):
method get (line 14) | def get(self, request, query_id, *args, **kwargs):
class DownloadFromSqlView (line 19) | class DownloadFromSqlView(PermissionRequiredMixin, View):
method post (line 23) | def post(self, request, *args, **kwargs):
FILE: explorer/views/email.py
class EmailCsvQueryView (line 8) | class EmailCsvQueryView(PermissionRequiredMixin, View):
method post (line 12) | def post(self, request, query_id, *args, **kwargs):
FILE: explorer/views/export.py
function _export (line 8) | def _export(request, query, download=True):
FILE: explorer/views/format_sql.py
function format_sql (line 8) | def format_sql(request):
FILE: explorer/views/list.py
class ListQueryView (line 15) | class ListQueryView(PermissionRequiredMixin, ExplorerContextMixin, ListV...
method recently_viewed (line 19) | def recently_viewed(self):
method get_context_data (line 37) | def get_context_data(self, **kwargs):
method get_queryset (line 46) | def get_queryset(self):
method _build_queries_and_headers (line 61) | def _build_queries_and_headers(self):
class ListQueryLogView (line 129) | class ListQueryLogView(PermissionRequiredMixin, ExplorerContextMixin, Li...
method get_queryset (line 135) | def get_queryset(self):
FILE: explorer/views/mixins.py
class ExplorerContextMixin (line 7) | class ExplorerContextMixin:
method gen_ctx (line 9) | def gen_ctx(self):
method get_context_data (line 29) | def get_context_data(self, **kwargs):
method render_template (line 34) | def render_template(self, template, ctx):
FILE: explorer/views/query.py
class PlayQueryView (line 21) | class PlayQueryView(PermissionRequiredMixin, ExplorerContextMixin, View):
method get (line 24) | def get(self, request):
method post (line 37) | def post(self, request):
method render (line 58) | def render(self):
method render_with_sql (line 67) | def render_with_sql(self, request, query, run_query=True, error=None):
class QueryView (line 89) | class QueryView(PermissionRequiredMixin, ExplorerContextMixin, View):
method get (line 92) | def get(self, request, query_id):
method post (line 122) | def post(self, request, query_id):
method get_instance_and_form (line 151) | def get_instance_and_form(request, query_id):
FILE: explorer/views/query_favorite.py
class QueryFavoritesView (line 9) | class QueryFavoritesView(PermissionRequiredMixin, ExplorerContextMixin, ...
method get (line 12) | def get(self, request):
class QueryFavoriteView (line 20) | class QueryFavoriteView(PermissionRequiredMixin, ExplorerContextMixin, V...
method build_favorite_response (line 24) | def build_favorite_response(user, query_id):
method get (line 33) | def get(self, request, query_id):
method post (line 36) | def post(self, request, query_id):
FILE: explorer/views/schema.py
class SchemaView (line 13) | class SchemaView(PermissionRequiredMixin, View):
method dispatch (line 18) | def dispatch(self, *args, **kwargs):
method get (line 21) | def get(self, request, *args, **kwargs):
class SchemaJsonView (line 42) | class SchemaJsonView(PermissionRequiredMixin, View):
method get (line 46) | def get(self, request, *args, **kwargs):
FILE: explorer/views/stream.py
class StreamQueryView (line 10) | class StreamQueryView(PermissionRequiredMixin, View):
method get (line 14) | def get(self, request, query_id, *args, **kwargs):
FILE: explorer/views/utils.py
function query_viewmodel (line 12) | def query_viewmodel(request, query, title=None, form=None, message=None,
FILE: setup.py
function requirements (line 19) | def requirements(fname):
Condensed preview — 214 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (762K chars).
[
{
"path": ".dockerignore",
"chars": 13,
"preview": "node_modules\n"
},
{
"path": ".editorconfig",
"chars": 281,
"preview": "# http://editorconfig.org\n\nroot = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\ntrim_trailing_w"
},
{
"path": ".eslintignore",
"chars": 33,
"preview": "/explorer/static/js/src/pivot.js\n"
},
{
"path": ".github/dependabot.yml",
"chars": 473,
"preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
},
{
"path": ".github/workflows/codeql-analysis.yml",
"chars": 2578,
"preview": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# Y"
},
{
"path": ".github/workflows/docs.yml",
"chars": 549,
"preview": "name: Docs\n\non: [push, pull_request]\n\nconcurrency:\n group: ${{ github.workflow }}-${{ github.ref }}\n cancel-in-progres"
},
{
"path": ".github/workflows/lint.yml",
"chars": 211,
"preview": "name: Ruff\n\non:\n push:\n pull_request:\n\njobs:\n ruff:\n runs-on: ubuntu-latest\n\n steps:\n - uses: actions/checko"
},
{
"path": ".github/workflows/publish-pypi.yml",
"chars": 1085,
"preview": "name: Publish Python 🐍 distributions 📦 to pypi\n\non:\n release:\n types:\n - published\n\njobs:\n build-n-publish:\n "
},
{
"path": ".github/workflows/publish-test.yml",
"chars": 1172,
"preview": "name: Publish Python 🐍 distributions 📦 to TestPyPI\n\non:\n push:\n branches:\n - master\n - support/3.x\n\njobs:\n"
},
{
"path": ".github/workflows/test.yml",
"chars": 2107,
"preview": "name: Tests\n\non:\n push:\n branches:\n - master\n - support/3.x\n pull_request:\n\nconcurrency:\n group: ${{ githu"
},
{
"path": ".gitignore",
"chars": 287,
"preview": "/.idea/\n*.pyc\n*.db\n/project/\n/dist\n*.egg-info\n.DS_Store\n/build\n*#\n*~\n.coverage*\n/htmlcov/\n*.orig\ntmp\nvenv/\n.venv/\n.tox/\n"
},
{
"path": ".nvmrc",
"chars": 8,
"preview": "20.15.1\n"
},
{
"path": ".pre-commit-config.yaml",
"chars": 1483,
"preview": "ci:\n autofix_commit_msg: |\n ci: auto fixes from pre-commit hooks\n\n for more information, see https://pre-commit.c"
},
{
"path": ".readthedocs.yaml",
"chars": 330,
"preview": "# Read the Docs configuration file\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details\nversion: "
},
{
"path": "AUTHORS",
"chars": 624,
"preview": "The following people have contributed to django-sql-explorer:\n\n- Chris Clark\n- Mark Walker\n- Lee Brooks\n- Artyom Chernya"
},
{
"path": "Dockerfile",
"chars": 2297,
"preview": "# Build stage\nFROM python:3.12.4 as builder\n\nWORKDIR /app\n\n# Install system dependencies\nRUN apt-get update && apt-get i"
},
{
"path": "HISTORY.rst",
"chars": 34249,
"preview": "==========\nChange Log\n==========\n\nThis document records all notable changes to `SQL Explorer <https://github.com/explore"
},
{
"path": "LICENSE",
"chars": 1343,
"preview": "* All content that resides under the \"explorer/ee/\" directory of this repository is licensed under the license defined\ni"
},
{
"path": "MANIFEST.in",
"chars": 174,
"preview": "recursive-include explorer *\nrecursive-exclude * *.pyc __pycache__ .DS_Store\nrecursive-include requirements *\ninclude pa"
},
{
"path": "README.rst",
"chars": 3822,
"preview": ".. image:: https://readthedocs.org/projects/django-sql-explorer/badge/?version=latest\n :target: https://django-sql-exp"
},
{
"path": "docker-compose.yml",
"chars": 310,
"preview": "services:\n web:\n build: .\n command: [\"python\", \"manage.py\", \"runserver\", \"0.0.0.0:8000\"]\n ports:\n - \"8000"
},
{
"path": "docs/Makefile",
"chars": 6806,
"preview": "# Makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS =\nSPHINXBUILD "
},
{
"path": "docs/_static/.directory",
"chars": 0,
"preview": ""
},
{
"path": "docs/_templates/.directory",
"chars": 0,
"preview": ""
},
{
"path": "docs/conf.py",
"chars": 2296,
"preview": "# Configuration file for the Sphinx documentation builder.\n#\n# This file only contains a selection of the most common op"
},
{
"path": "docs/dependencies.rst",
"chars": 2281,
"preview": "Dependencies\n============\n\nAn effort has been made to keep the number of dependencies to a\nminimum.\n\nPython\n------\n\n===="
},
{
"path": "docs/development.rst",
"chars": 1214,
"preview": "Running Locally (quick start)\n-----------------------------\n\nWhether you have cloned the repo, or installed via pip, inc"
},
{
"path": "docs/features.rst",
"chars": 13906,
"preview": "Features\n========\n\nSQL Assistant\n-------------\n- Built in integration with OpenAI (or the LLM of your choosing)\n to qui"
},
{
"path": "docs/history.rst",
"chars": 28,
"preview": ".. include:: ../HISTORY.rst\n"
},
{
"path": "docs/index.rst",
"chars": 509,
"preview": ".. rstcheck: ignore-next-code-block\n.. Django SQL Explorer documentation master file, created by\n sphinx-quickstart on"
},
{
"path": "docs/install.rst",
"chars": 3936,
"preview": "Install\n=======\n\n* Requires Python 3.10 or higher.\n* Requires Django 3.2 or higher.\n\nSet up a Django project with the fo"
},
{
"path": "docs/make.bat",
"chars": 760,
"preview": "@ECHO OFF\n\npushd %~dp0\n\nREM Command file for Sphinx documentation\n\nif \"%SPHINXBUILD%\" == \"\" (\n\tset SPHINXBUILD=sphinx-bu"
},
{
"path": "docs/requirements.txt",
"chars": 77,
"preview": "django-sql-explorer>=2.0\nfuro\nSphinx>4\nsphinx-copybutton\nsphinxext-opengraph\n"
},
{
"path": "docs/settings.rst",
"chars": 8936,
"preview": "********\nSettings\n********\n\nHere are all of the available settings with their default values.\n\n\nSQL Blacklist\n**********"
},
{
"path": "entrypoint.sh",
"chars": 484,
"preview": "#!/bin/bash\n# entrypoint.sh\n\nset -e\n\n# Source the nvm script to set up the environment\n# This should match the version r"
},
{
"path": "explorer/__init__.py",
"chars": 637,
"preview": "__version_info__ = {\n \"major\": 5,\n \"minor\": 3,\n \"patch\": 0,\n \"releaselevel\": \"final\",\n \"serial\": 0\n}\n\n\nde"
},
{
"path": "explorer/actions.py",
"chars": 1776,
"preview": "import tempfile\nfrom collections import defaultdict\nfrom datetime import date\nfrom wsgiref.util import FileWrapper\nfrom "
},
{
"path": "explorer/admin.py",
"chars": 856,
"preview": "from django.contrib import admin\n\nfrom explorer.actions import generate_report_action\nfrom explorer.models import Query,"
},
{
"path": "explorer/app_settings.py",
"chars": 6143,
"preview": "from pydoc import locate\n\nfrom django.conf import settings\n\n\nEXPLORER_CONNECTIONS = getattr(settings, \"EXPLORER_CONNECTI"
},
{
"path": "explorer/apps.py",
"chars": 1529,
"preview": "from django.apps import AppConfig\nfrom django.utils.translation import gettext_lazy as _\nfrom django.db import transacti"
},
{
"path": "explorer/assistant/__init__.py",
"chars": 1,
"preview": "\n"
},
{
"path": "explorer/assistant/forms.py",
"chars": 1630,
"preview": "from django import forms\nfrom explorer.assistant.models import TableDescription\nfrom explorer.ee.db_connections.utils im"
},
{
"path": "explorer/assistant/models.py",
"chars": 1278,
"preview": "from django.db import models\nfrom django.conf import settings\nfrom explorer.ee.db_connections.models import DatabaseConn"
},
{
"path": "explorer/assistant/urls.py",
"chars": 1045,
"preview": "from django.urls import path\nfrom explorer.assistant.views import (TableDescriptionListView,\n "
},
{
"path": "explorer/assistant/utils.py",
"chars": 6000,
"preview": "from dataclasses import dataclass\nfrom explorer import app_settings\nfrom explorer.schema import schema_info\nfrom explore"
},
{
"path": "explorer/assistant/views.py",
"chars": 4312,
"preview": "from django.http import JsonResponse\nfrom django.views import View\nfrom django.utils import timezone\nfrom django.views.g"
},
{
"path": "explorer/charts.py",
"chars": 2693,
"preview": "from io import BytesIO\nfrom typing import Iterable, Optional\nfrom .models import QueryResult\n\nBAR_WIDTH = 0.2\n\n\ndef get_"
},
{
"path": "explorer/ee/LICENSE",
"chars": 1273,
"preview": "** Additional License for \"explorer/ee/\" Directory **\n\nAll content that resides under the \"explorer/ee/\" directory of th"
},
{
"path": "explorer/ee/__init__.py",
"chars": 1,
"preview": "\n"
},
{
"path": "explorer/ee/db_connections/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "explorer/ee/db_connections/admin.py",
"chars": 178,
"preview": "from django.contrib import admin\n\nfrom explorer.models import DatabaseConnection\n\n\n@admin.register(DatabaseConnection)\nc"
},
{
"path": "explorer/ee/db_connections/create_sqlite.py",
"chars": 1559,
"preview": "import os\nfrom io import BytesIO\n\nfrom explorer.utils import secure_filename\nfrom explorer.ee.db_connections.type_infer "
},
{
"path": "explorer/ee/db_connections/forms.py",
"chars": 1654,
"preview": "from django import forms\nfrom explorer.ee.db_connections.models import DatabaseConnection\nimport json\nfrom django.core.e"
},
{
"path": "explorer/ee/db_connections/mime.py",
"chars": 1476,
"preview": "import csv\nimport json\n\n# These are 'shallow' checks. They are just to understand if the upload appears valid at surface"
},
{
"path": "explorer/ee/db_connections/models.py",
"chars": 6130,
"preview": "import os\nimport json\nfrom django.db import models, DatabaseError, connections, transaction\nfrom django.db.utils import "
},
{
"path": "explorer/ee/db_connections/type_infer.py",
"chars": 4639,
"preview": "import io\nimport json\nfrom explorer.ee.db_connections.mime import is_csv, is_json, is_sqlite, is_json_list\n\n\nMAX_TYPING_"
},
{
"path": "explorer/ee/db_connections/utils.py",
"chars": 2982,
"preview": "import os\n\nimport hashlib\nimport sqlite3\nimport io\n\n\ndef default_db_connection():\n from explorer.ee.db_connections.mo"
},
{
"path": "explorer/ee/db_connections/views.py",
"chars": 7779,
"preview": "import logging\nfrom django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView, TemplateView\nf"
},
{
"path": "explorer/ee/urls.py",
"chars": 1372,
"preview": "from django.urls import path\n\nfrom explorer.ee.db_connections.views import (\n UploadDbView,\n DatabaseConnectionsLi"
},
{
"path": "explorer/exporters.py",
"chars": 3861,
"preview": "import codecs\nimport csv\nimport json\nimport uuid\nfrom datetime import datetime\nfrom io import BytesIO, StringIO\n\nfrom dj"
},
{
"path": "explorer/forms.py",
"chars": 3086,
"preview": "from django.forms import BooleanField, CharField, ModelForm, ValidationError\nfrom django.forms.widgets import CheckboxIn"
},
{
"path": "explorer/locale/ru/LC_MESSAGES/django.po",
"chars": 9012,
"preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
},
{
"path": "explorer/locale/zh_Hans/LC_MESSAGES/django.po",
"chars": 4461,
"preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
},
{
"path": "explorer/migrations/0001_initial.py",
"chars": 1905,
"preview": "import django.db.models.deletion\nfrom django.conf import settings\nfrom django.db import migrations, models\n\n\nclass Migra"
},
{
"path": "explorer/migrations/0002_auto_20150501_1515.py",
"chars": 435,
"preview": "from django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n\n dependencies = [\n ('explor"
},
{
"path": "explorer/migrations/0003_query_snapshot.py",
"chars": 392,
"preview": "from django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n\n dependencies = [\n ('explor"
},
{
"path": "explorer/migrations/0004_querylog_duration.py",
"chars": 345,
"preview": "from django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n\n dependencies = [\n ('explor"
},
{
"path": "explorer/migrations/0005_auto_20160105_2052.py",
"chars": 439,
"preview": "# Generated by Django 1.9 on 2016-01-05 20:52\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.Mig"
},
{
"path": "explorer/migrations/0006_query_connection.py",
"chars": 500,
"preview": "from django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n\n dependencies = [\n ('explor"
},
{
"path": "explorer/migrations/0007_querylog_connection.py",
"chars": 364,
"preview": "from django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n\n dependencies = [\n ('explor"
},
{
"path": "explorer/migrations/0008_auto_20190308_1642.py",
"chars": 502,
"preview": "from django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n\n dependencies = [\n ('explor"
},
{
"path": "explorer/migrations/0009_auto_20201009_0547.py",
"chars": 913,
"preview": "# Generated by Django 3.1.2 on 2020-10-09 05:47\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
},
{
"path": "explorer/migrations/0010_sql_required.py",
"chars": 370,
"preview": "# Generated by Django 3.0.8 on 2020-12-18 06:24\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
},
{
"path": "explorer/migrations/0011_query_favorites.py",
"chars": 914,
"preview": "# Generated by Django 3.2.16 on 2023-01-12 18:24\n\nimport django.db.models.deletion\nfrom django.conf import settings\nfrom"
},
{
"path": "explorer/migrations/0012_alter_queryfavorite_query_alter_queryfavorite_user.py",
"chars": 971,
"preview": "# Generated by Django 4.1.7 on 2023-02-27 08:37\n\nimport django.db.models.deletion\nfrom django.conf import settings\nfrom "
},
{
"path": "explorer/migrations/0013_querylog_error_querylog_success.py",
"chars": 582,
"preview": "# Generated by Django 4.2.8 on 2023-12-15 09:34\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
},
{
"path": "explorer/migrations/0014_promptlog.py",
"chars": 1165,
"preview": "# Generated by Django 4.2.8 on 2024-01-11 08:22\n\nfrom django.conf import settings\nfrom django.db import migrations, mode"
},
{
"path": "explorer/migrations/0015_explorervalue.py",
"chars": 657,
"preview": "# Generated by Django 4.2.8 on 2024-04-25 13:34\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
},
{
"path": "explorer/migrations/0016_alter_explorervalue_key.py",
"chars": 1373,
"preview": "# Generated by Django 4.2.8 on 2024-04-26 13:05\n\nfrom django.db import migrations, models\n\n\ndef insert_assistant_prompt("
},
{
"path": "explorer/migrations/0017_databaseconnection.py",
"chars": 1151,
"preview": "# Generated by Django 5.0.4 on 2024-05-07 18:41\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
},
{
"path": "explorer/migrations/0018_alter_databaseconnection_host_and_more.py",
"chars": 908,
"preview": "# Generated by Django 5.0.4 on 2024-05-14 15:55\n\nimport django_cryptography.fields\nfrom django.db import migrations, mod"
},
{
"path": "explorer/migrations/0019_alter_databaseconnection_engine.py",
"chars": 745,
"preview": "# Generated by Django 5.0.5 on 2024-07-03 12:38\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
},
{
"path": "explorer/migrations/0020_databaseconnection_extras_and_more.py",
"chars": 921,
"preview": "# Generated by Django 5.0.4 on 2024-07-08 01:17\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
},
{
"path": "explorer/migrations/0021_alter_databaseconnection_password_and_more.py",
"chars": 734,
"preview": "# Generated by Django 5.0.4 on 2024-07-16 01:43\n\nimport django_cryptography.fields\nfrom django.db import migrations, mod"
},
{
"path": "explorer/migrations/0022_databaseconnection_upload_fingerprint.py",
"chars": 457,
"preview": "# Generated by Django 5.0.4 on 2024-07-24 20:08\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
},
{
"path": "explorer/migrations/0023_query_database_connection_and_more.py",
"chars": 1996,
"preview": "# Generated by Django 5.0.4 on 2024-08-03 16:54\n\nimport django.db.models.deletion\nfrom django.db import migrations, mode"
},
{
"path": "explorer/migrations/0024_auto_20240803_1135.py",
"chars": 2677,
"preview": "# Generated by Django 5.0.4 on 2024-08-03 16:35\n\nfrom django.db import migrations, connection\nfrom django.db import conn"
},
{
"path": "explorer/migrations/0025_alter_query_database_connection_alter_querylog_database_connection.py",
"chars": 754,
"preview": "# Generated by Django 5.0.4 on 2024-08-13 13:45\n\nimport django.db.models.deletion\nfrom django.db import migrations, mode"
},
{
"path": "explorer/migrations/0026_tabledescription.py",
"chars": 917,
"preview": "# Generated by Django 5.0.4 on 2024-08-22 01:24\n\nimport django.db.models.deletion\nfrom django.db import migrations, mode"
},
{
"path": "explorer/migrations/0027_query_few_shot.py",
"chars": 488,
"preview": "# Generated by Django 5.0.4 on 2024-08-25 21:26\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
},
{
"path": "explorer/migrations/0028_promptlog_database_connection_promptlog_user_request.py",
"chars": 676,
"preview": "# Generated by Django 5.0.4 on 2024-08-27 18:59\n\nimport django.db.models.deletion\nfrom django.db import migrations, mode"
},
{
"path": "explorer/migrations/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "explorer/models.py",
"chars": 14063,
"preview": "import logging\nfrom time import time\nimport uuid\n\nfrom django.conf import settings\nfrom django.core.exceptions import Va"
},
{
"path": "explorer/permissions.py",
"chars": 1092,
"preview": "from explorer import app_settings\nfrom explorer.utils import allowed_query_pks, user_can_see_query\n\n\ndef view_permission"
},
{
"path": "explorer/schema.py",
"chars": 3661,
"preview": "from django.core.cache import cache\nfrom django.db import ProgrammingError\n\nfrom explorer.app_settings import (\n EXPL"
},
{
"path": "explorer/src/js/assistant.js",
"chars": 10950,
"preview": "import {getCsrfToken} from \"./csrf\";\nimport { marked } from \"marked\";\nimport DOMPurify from \"dompurify\";\nimport * as boo"
},
{
"path": "explorer/src/js/codemirror-config.js",
"chars": 4581,
"preview": "import {\n keymap, highlightSpecialChars, drawSelection, highlightActiveLine, dropCursor,\n lineNumbers, highlightAc"
},
{
"path": "explorer/src/js/csrf.js",
"chars": 430,
"preview": "import cookie from \"cookiejs\";\n\nconst csrfCookieName = document.getElementById('csrfCookieName').value;\nconst csrfTokenI"
},
{
"path": "explorer/src/js/explorer.js",
"chars": 13484,
"preview": "import $ from 'jquery';\nimport { EditorView } from \"codemirror\";\nimport { explorerSetup } from \"./codemirror-config\";\nim"
},
{
"path": "explorer/src/js/favorites.js",
"chars": 1056,
"preview": "import {getCsrfToken} from \"./csrf\";\n\nexport async function toggleFavorite() {\n let queryId = this.dataset.id;\n le"
},
{
"path": "explorer/src/js/main.js",
"chars": 2040,
"preview": "/*\nThis is the entrypoint for the client code, and for the Vite build. The basic\nidea is to map a function to each page/"
},
{
"path": "explorer/src/js/pivot-setup.js",
"chars": 1313,
"preview": "import {pivotJq} from \"./pivot\";\nimport {csvFromTable} from \"./table-to-csv\";\nimport $ from \"jquery\";\nexport function pi"
},
{
"path": "explorer/src/js/pivot.js",
"chars": 77235,
"preview": "import Sortable from 'sortablejs';\n\n\nlet indexOf = [].indexOf || function (item) {\n for (var i = 0, l = this.length; "
},
{
"path": "explorer/src/js/query-list.js",
"chars": 3879,
"preview": "import List from \"list.js\";\nimport {getCsrfToken} from \"./csrf\";\nimport {toggleFavorite} from \"./favorites\";\nimport * as"
},
{
"path": "explorer/src/js/schema.js",
"chars": 1716,
"preview": "import List from \"list.js\";\n\nfunction searchFocus() {\n let schemaFrame = window.parent.document.getElementById(\"schem"
},
{
"path": "explorer/src/js/schemaService.js",
"chars": 815,
"preview": "const schemaCache = {};\n\nconst fetchSchema = async () => {\n\n const conn = getConnElement().value;\n\n if (schemaCach"
},
{
"path": "explorer/src/js/table-to-csv.js",
"chars": 884,
"preview": "function tableToCSV(tableEl) {\n let csv_data = [];\n\n let rows = tableEl.getElementsByTagName('tr');\n for (let i"
},
{
"path": "explorer/src/js/tableDescription.js",
"chars": 1380,
"preview": "import {getConnElement, SchemaSvc} from \"./schemaService\"\nimport Choices from \"choices.js\"\n\n\nfunction populateTableList("
},
{
"path": "explorer/src/js/uploads.js",
"chars": 3832,
"preview": "import { getCsrfToken } from \"./csrf\";\n\nexport function setupUploads() {\n var dropArea = document.getElementById('dro"
},
{
"path": "explorer/src/scss/assistant.scss",
"chars": 491,
"preview": "#assistant_description_label {\n white-space: pre-wrap;\n}\n\n#assistant_response pre {\n position: relative;\n backg"
},
{
"path": "explorer/src/scss/choices.scss",
"chars": 8709,
"preview": "@import \"variables\";\n\n.choices {\n position: relative;\n overflow: hidden;\n margin-bottom: 24px;\n}\n.choices:focus {\n o"
},
{
"path": "explorer/src/scss/explorer.scss",
"chars": 1838,
"preview": "a {\n text-decoration: none;\n}\n\n.cm-editor {\n outline: none !important;\n}\n\n.cm-scroller {\n overflow: auto;\n m"
},
{
"path": "explorer/src/scss/pivot.css",
"chars": 2597,
"preview": ".pvtUi { color: #333; }\n\n\ntable.pvtTable {\n font-size: 8pt;\n text-align: left;\n border-collapse: collapse;\n}\nta"
},
{
"path": "explorer/src/scss/styles.scss",
"chars": 275,
"preview": "@import \"variables\";\n\n@import \"~bootstrap/scss/bootstrap\";\n\n$bootstrap-icons-font-dir: \"../../../node_modules/bootstrap-"
},
{
"path": "explorer/src/scss/variables.scss",
"chars": 683,
"preview": "@import \"../../../node_modules/bootstrap/scss/functions\";\n\n$orange: rgb(255, 80, 1);\n$blue: rgb(3, 68, 220);\n$green: rgb"
},
{
"path": "explorer/tasks.py",
"chars": 4479,
"preview": "import io\nimport random\nimport string\nimport os\nfrom datetime import date, datetime, timedelta\n\nfrom django.core.cache i"
},
{
"path": "explorer/telemetry.py",
"chars": 5397,
"preview": "# Anonymous usage stats\n# Opt-out by setting EXPLORER_ENABLE_ANONYMOUS_STATS = False in settings\n\nimport logging\nimport "
},
{
"path": "explorer/templates/assistant/table_description_confirm_delete.html",
"chars": 485,
"preview": "{% extends \"explorer/base.html\" %}\n\n{% block sql_explorer_content %}\n<div class=\"container mt-5\">\n <h1>Confirm Delete</"
},
{
"path": "explorer/templates/assistant/table_description_form.html",
"chars": 1995,
"preview": "{% extends \"explorer/base.html\" %}\n\n{% block sql_explorer_content %}\n<div class=\"container mt-5\">\n <div class=\"row\">\n"
},
{
"path": "explorer/templates/assistant/table_description_list.html",
"chars": 1868,
"preview": "{% extends \"explorer/base.html\" %}\n\n{% block sql_explorer_content %}\n<div class=\"container\">\n <h3>Table Annotations</"
},
{
"path": "explorer/templates/connections/connection_upload.html",
"chars": 1869,
"preview": "{% extends 'explorer/base.html' %}\n{% block sql_explorer_content %}\n<div class=\"container mt-5\">\n <div class=\"pt-3\">\n"
},
{
"path": "explorer/templates/connections/connections.html",
"chars": 3332,
"preview": "{% extends \"explorer/base.html\" %}\n{% load explorer_tags i18n %}\n\n{% block sql_explorer_content %}\n<div class=\"container"
},
{
"path": "explorer/templates/connections/database_connection_confirm_delete.html",
"chars": 435,
"preview": "{% extends 'explorer/base.html' %}\n{% block sql_explorer_content %}\n<div class=\"container mt-5\">\n <h2>Delete Database"
},
{
"path": "explorer/templates/connections/database_connection_detail.html",
"chars": 1172,
"preview": "{% extends \"explorer/base.html\" %}\n{% block sql_explorer_content %}\n<div class=\"container mt-5\">\n <h2>Connection Deta"
},
{
"path": "explorer/templates/connections/database_connection_form.html",
"chars": 2622,
"preview": "{% extends 'explorer/base.html' %}\n{% block sql_explorer_content %}\n<div class=\"container mt-5\">\n <h2>{% if object %}"
},
{
"path": "explorer/templates/explorer/assistant.html",
"chars": 3737,
"preview": "{% load i18n %}\n<div class =\"accordion accordion-flush mt-4\" id=\"assistant_accordion\">\n <div class=\"accordion-item\">\n"
},
{
"path": "explorer/templates/explorer/base.html",
"chars": 4976,
"preview": "{% load i18n static %}\n{% load vite %}\n\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta nam"
},
{
"path": "explorer/templates/explorer/export_buttons.html",
"chars": 1126,
"preview": "{% load explorer_tags i18n %}\n\n<div class=\"btn-group\" role=\"group\">\n <button id=\"download_options\"\n class="
},
{
"path": "explorer/templates/explorer/fullscreen.html",
"chars": 1535,
"preview": "{% load i18n static %}\n{% load vite %}\n\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta nam"
},
{
"path": "explorer/templates/explorer/params.html",
"chars": 571,
"preview": "{% if params %}\n <div class=\"mt-3 row\">\n <label class=\"form-label\">Params</label>\n {% for k, v in param"
},
{
"path": "explorer/templates/explorer/pdf_template.html",
"chars": 548,
"preview": "<html>\n<head>\n <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n</head>\n<body>\n <table>\n "
},
{
"path": "explorer/templates/explorer/play.html",
"chars": 4000,
"preview": "{% extends \"explorer/base.html\" %}\n{% load explorer_tags i18n %}\n\n{% block sql_explorer_content %}\n<div class=\"container"
},
{
"path": "explorer/templates/explorer/preview_pane.html",
"chars": 10142,
"preview": "{% load i18n %}\n\n{% if headers %}\n<div class=\"container mt-4\">\n <nav>\n <div class=\"nav nav-tabs\" role=\"tablist"
},
{
"path": "explorer/templates/explorer/query.html",
"chars": 8564,
"preview": "{% extends \"explorer/base.html\" %}\n{% load explorer_tags i18n %}\n\n{% block sql_explorer_content %}\n\n<input type=\"hidden\""
},
{
"path": "explorer/templates/explorer/query_confirm_delete.html",
"chars": 560,
"preview": "{% extends \"explorer/base.html\" %}\n{% load i18n %}\n\n{% block sql_explorer_content %}\n <div class=\"container\">\n "
},
{
"path": "explorer/templates/explorer/query_favorite_button.html",
"chars": 314,
"preview": "{% if is_favorite %}\n <i class=\"bi-heart-fill text-danger {{ extra_classes }}\" data-id=\"{{ query_id }}\" data-url=\"{% "
},
{
"path": "explorer/templates/explorer/query_favorites.html",
"chars": 897,
"preview": "{% extends \"explorer/base.html\" %}\n{% load i18n %}\n\n{% block sql_explorer_content %}\n <div class=\"container\">\n "
},
{
"path": "explorer/templates/explorer/query_list.html",
"chars": 11646,
"preview": "{% extends \"explorer/base.html\" %}\n{% load explorer_tags i18n static %}\n\n{% block sql_explorer_content %}\n <div style"
},
{
"path": "explorer/templates/explorer/querylog_list.html",
"chars": 3094,
"preview": "{% extends \"explorer/base.html\" %}\n{% load i18n %}\n\n{% block sql_explorer_content %}\n <div class=\"container\">\n "
},
{
"path": "explorer/templates/explorer/schema.html",
"chars": 1735,
"preview": "{% extends \"explorer/base.html\" %}\n{% load i18n %}\n\n\n{% block sql_explorer_content_takeover %}\n<div class=\"schema-wrappe"
},
{
"path": "explorer/templates/explorer/schema_error.html",
"chars": 390,
"preview": "{% extends \"explorer/base.html\" %}\n{% load i18n %}\n\n{% block sql_explorer_content_takeover %}\n <div class=\"schema-wra"
},
{
"path": "explorer/templatetags/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "explorer/templatetags/explorer_tags.py",
"chars": 756,
"preview": "from django import template\nfrom django.utils.module_loading import import_string\n\nfrom explorer import app_settings\n\n\nr"
},
{
"path": "explorer/templatetags/vite.py",
"chars": 2240,
"preview": "import os\n\nfrom django import template\nfrom django.conf import settings\nfrom django.contrib.staticfiles.storage import s"
},
{
"path": "explorer/tests/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "explorer/tests/csvs/all_types.csv",
"chars": 227,
"preview": "Dates,Integers,Floats,Strings\n2020-01-31,0,42.952198961732414,THNVT\n2020-02-29,1,27.66862453654746,JXPSY\n2020-03-31,2,79"
},
{
"path": "explorer/tests/csvs/dates.csv",
"chars": 282,
"preview": "Dates,Values\n2024-01-24,0\n2024-01-24T18:45:00Z,1\n01/24/2024,2\n01/24/2024 6:45 PM,3\n24/01/2024,4\n24/01/2024 18:45,5\n24/01"
},
{
"path": "explorer/tests/csvs/floats.csv",
"chars": 147,
"preview": "Floats,Values\n61.3410231760637,0\n79.2367071213973,1\n96.93217099083482,2\n69.50523191870069,3\n,4\n\"69.42719764194946\",4\n\"2,"
},
{
"path": "explorer/tests/csvs/integers.csv",
"chars": 60,
"preview": "Integers,More_integers\n\"5,000\",0\n\"6000\",1\n\"\",0\n\"7,200,200\",2"
},
{
"path": "explorer/tests/csvs/mixed.csv",
"chars": 89,
"preview": "Value1,Value2,Value3\n2020-01-32,abc,123\nVariety of other dates,def,123\n,,\nAnother,123,12a"
},
{
"path": "explorer/tests/csvs/rc_sample.csv",
"chars": 4501,
"preview": "name,material_type,seating_type,speed,height,length,num_inversions,manufacturer,park,status\nGoudurix,Steel,Sit Down,75.0"
},
{
"path": "explorer/tests/csvs/test_case1.csv",
"chars": 12888,
"preview": "STORE,CONTENT_TYPE,EMAIL,CUSTOMER_ID,CREATED_DATE,SHIP_MONTH,SHIP_DATE,SHOPIFY_ORDER_NUMBER,SKU,SHOPIFY_PRODUCT_ID,SHOPI"
},
{
"path": "explorer/tests/factories.py",
"chars": 964,
"preview": "from django.conf import settings\n\nfrom factory import Sequence, SubFactory, LazyFunction\nfrom factory.django import Djan"
},
{
"path": "explorer/tests/json/github.json",
"chars": 40400,
"preview": "[\n {\n \"id\": 6104546,\n \"node_id\": \"MDEwOlJlcG9zaXRvcnk2MTA0NTQ2\",\n \"name\": \"-REPONAME\",\n \"full_name\": \"mrale"
},
{
"path": "explorer/tests/json/kings.json",
"chars": 673,
"preview": "[\n {\n \"Name\": \"Edward the Elder\",\n \"Country\": \"United Kingdom\",\n \"House\": \"House of Wessex\",\n \"Reign\": \"899"
},
{
"path": "explorer/tests/json/list.json",
"chars": 2563,
"preview": "{\"Item\":{\"instanceId\":{\"S\":\"ba4afa8857d1f836701c4ce8e54b95d84a8e3b92ee52292c7e5b2fb6fe6f1a69\"},\"time\":{\"N\":\"1713969118.9"
},
{
"path": "explorer/tests/settings.py",
"chars": 1029,
"preview": "from test_project.settings import * # noqa\n\nEXPLORER_ENABLE_ANONYMOUS_STATS = False\nEXPLORER_TASKS_ENABLED = True\nEXPLO"
},
{
"path": "explorer/tests/settings_base.py",
"chars": 175,
"preview": "from explorer.tests.settings import * # noqa\n\nEXPLORER_TASKS_ENABLED = False\nEXPLORER_USER_UPLOADS_ENABLED = False\nEXPL"
},
{
"path": "explorer/tests/test_actions.py",
"chars": 1433,
"preview": "import io\nfrom zipfile import ZipFile\n\nfrom django.test import TestCase\n\nfrom explorer.actions import generate_report_ac"
},
{
"path": "explorer/tests/test_apps.py",
"chars": 492,
"preview": "from io import StringIO\n\nfrom django.test import TestCase\nfrom django.core.management import call_command\n\n\nclass Pendin"
},
{
"path": "explorer/tests/test_assistant.py",
"chars": 18166,
"preview": "from explorer.tests.factories import SimpleQueryFactory, QueryLogFactory\nfrom unittest.mock import patch, Mock, MagicMoc"
},
{
"path": "explorer/tests/test_create_sqlite.py",
"chars": 35587,
"preview": "from django.test import TestCase\nfrom django.core.files.uploadedfile import SimpleUploadedFile\nfrom unittest import skip"
},
{
"path": "explorer/tests/test_csrf_cookie_name.py",
"chars": 1144,
"preview": "from django.test import TestCase, override_settings\n\n\ntry:\n from django.urls import reverse\nexcept ImportError:\n f"
},
{
"path": "explorer/tests/test_db_connection_utils.py",
"chars": 4137,
"preview": "from django.test import TestCase\nfrom unittest import skipIf\nfrom explorer.app_settings import EXPLORER_USER_UPLOADS_ENA"
},
{
"path": "explorer/tests/test_exporters.py",
"chars": 4338,
"preview": "import json\nimport unittest\nfrom datetime import date, datetime\n\nfrom django.core.serializers.json import DjangoJSONEnco"
},
{
"path": "explorer/tests/test_forms.py",
"chars": 2428,
"preview": "from django.db.utils import IntegrityError\nfrom django.forms.models import model_to_dict\nfrom django.test import TestCas"
},
{
"path": "explorer/tests/test_mime.py",
"chars": 6369,
"preview": "from django.test import TestCase\nfrom django.core.files.uploadedfile import SimpleUploadedFile\nfrom explorer.ee.db_conne"
},
{
"path": "explorer/tests/test_models.py",
"chars": 14736,
"preview": "import unittest\nimport os\nfrom unittest.mock import Mock, patch, MagicMock\n\nfrom django.core.exceptions import Validatio"
},
{
"path": "explorer/tests/test_schema.py",
"chars": 3588,
"preview": "from unittest.mock import patch\n\nfrom django.core.cache import cache\nfrom django.db import connection\nfrom django.test i"
},
{
"path": "explorer/tests/test_tasks.py",
"chars": 4531,
"preview": "import unittest\nfrom datetime import datetime, timedelta\nfrom io import StringIO\nfrom unittest.mock import patch\nimport "
},
{
"path": "explorer/tests/test_telemetry.py",
"chars": 3396,
"preview": "from django.test import TestCase\nfrom explorer.telemetry import instance_identifier, _gather_summary_stats, Stat, StatNa"
},
{
"path": "explorer/tests/test_type_infer.py",
"chars": 3576,
"preview": "from django.test import TestCase\nfrom unittest import skipIf\nfrom explorer.app_settings import EXPLORER_USER_UPLOADS_ENA"
},
{
"path": "explorer/tests/test_utils.py",
"chars": 10889,
"preview": "from unittest.mock import Mock\n\nfrom django.test import TestCase\n\nfrom explorer import app_settings\nfrom explorer.tests."
},
{
"path": "explorer/tests/test_views.py",
"chars": 45164,
"preview": "import importlib\nimport json\nimport time\nimport unittest\nimport os\nfrom unittest.mock import Mock, patch, MagicMock\nfrom"
},
{
"path": "explorer/urls.py",
"chars": 1774,
"preview": "from django.urls import path\n\nfrom explorer.ee.urls import ee_urls\nfrom explorer.views import (\n CreateQueryView, Del"
},
{
"path": "explorer/utils.py",
"chars": 6877,
"preview": "import re\nimport os\nimport unicodedata\nfrom collections import deque\nfrom typing import Iterable, Tuple\n\nfrom django.con"
},
{
"path": "explorer/views/__init__.py",
"chars": 910,
"preview": "from .auth import PermissionRequiredMixin, SafeLoginView\nfrom .create import CreateQueryView\nfrom .delete import DeleteQ"
},
{
"path": "explorer/views/auth.py",
"chars": 1625,
"preview": "from django.contrib.auth import REDIRECT_FIELD_NAME\nfrom django.contrib.auth.views import LoginView\nfrom django.core.exc"
},
{
"path": "explorer/views/create.py",
"chars": 546,
"preview": "from django.views.generic import CreateView\n\nfrom explorer.forms import QueryForm\nfrom explorer.views.auth import Permis"
},
{
"path": "explorer/views/delete.py",
"chars": 447,
"preview": "from django.urls import reverse_lazy\nfrom django.views.generic import DeleteView\n\nfrom explorer.models import Query\nfrom"
},
{
"path": "explorer/views/download.py",
"chars": 1024,
"preview": "from django.shortcuts import get_object_or_404\nfrom django.views.generic.base import View\n\nfrom explorer.models import Q"
},
{
"path": "explorer/views/email.py",
"chars": 639,
"preview": "from django.http import JsonResponse\nfrom django.views import View\n\nfrom explorer.tasks import execute_query\nfrom explor"
},
{
"path": "explorer/views/export.py",
"chars": 874,
"preview": "from django.db import DatabaseError\nfrom django.http import HttpResponse\n\nfrom explorer.exporters import get_exporter_cl"
},
{
"path": "explorer/views/format_sql.py",
"chars": 285,
"preview": "from django.http import JsonResponse\nfrom django.views.decorators.http import require_POST\n\nfrom explorer.utils import f"
},
{
"path": "explorer/views/list.py",
"chars": 5608,
"preview": "import re\nfrom collections import Counter\n\nfrom django.forms.models import model_to_dict\nfrom django.views.generic impor"
},
{
"path": "explorer/views/mixins.py",
"chars": 1291,
"preview": "from django.conf import settings\nfrom django.shortcuts import render\n\nfrom explorer import app_settings\n\n\nclass Explorer"
},
{
"path": "explorer/views/query.py",
"chars": 5498,
"preview": "from django.core.exceptions import ValidationError\nfrom django.http import HttpResponseRedirect\nfrom django.shortcuts im"
},
{
"path": "explorer/views/query_favorite.py",
"chars": 1636,
"preview": "from django.http import JsonResponse\nfrom django.views import View\n\nfrom explorer.models import QueryFavorite\nfrom explo"
},
{
"path": "explorer/views/schema.py",
"chars": 1816,
"preview": "from django.http import Http404, JsonResponse\nfrom django.shortcuts import render, get_object_or_404\nfrom django.utils.d"
},
{
"path": "explorer/views/stream.py",
"chars": 634,
"preview": "from django.shortcuts import get_object_or_404\nfrom django.views import View\n\nfrom explorer.models import Query\nfrom exp"
},
{
"path": "explorer/views/utils.py",
"chars": 3125,
"preview": "from django.db import DatabaseError\nimport logging\nfrom explorer import app_settings\nfrom explorer.charts import get_cha"
},
{
"path": "manage.py",
"chars": 283,
"preview": "#!/usr/bin/env python\nimport os\nimport sys\n\nfrom django.core import management\n\nsys.path.append(os.path.join(os.path.dir"
},
{
"path": "package.json",
"chars": 1431,
"preview": "{\n \"name\": \"django-sql-explorer\",\n \"description\": \"Django SQL Explorer\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\":"
},
{
"path": "public_key.pem",
"chars": 451,
"preview": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr1Lfhtla6gpaBlKb2r9s\n5udyW6zHt0my9924sFtJ8OWzWpUF"
},
{
"path": "pypi-release-checklist.md",
"chars": 687,
"preview": "- [x] Update HISTORY\n- [x] Update README and check formatting with http://rst.ninjs.org/\n- [x] Make sure any new files a"
},
{
"path": "requirements/base.txt",
"chars": 94,
"preview": "Django>=3.2\nsqlparse>=0.4.0\nrequests>=2.2\ndjango-cryptography-django5==2.2\ncryptography>=42.0\n"
},
{
"path": "requirements/dev.txt",
"chars": 301,
"preview": "-r ./base.txt\n-r ./extra/assistant.txt\n-r ./extra/charts.txt\n-r ./extra/snapshots.txt\n-r ./extra/xls.txt\n-r ./extra/uplo"
},
{
"path": "requirements/extra/assistant.txt",
"chars": 14,
"preview": "openai>=1.6.1\n"
},
{
"path": "requirements/extra/charts.txt",
"chars": 16,
"preview": "matplotlib>=3.9\n"
},
{
"path": "requirements/extra/snapshots.txt",
"chars": 26,
"preview": "boto3>=1.30.0\ncelery>=4.0\n"
}
]
// ... and 14 more files (download for full content)
About this extraction
This page contains the full source code of the explorerhq/django-sql-explorer GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 214 files (692.0 KB), approximately 182.6k tokens, and a symbol index with 755 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.