master 7627b426e0b8 cached
214 files
692.0 KB
182.6k tokens
755 symbols
1 requests
Download .txt
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
Download .txt
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
Download .txt
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.

Copied to clipboard!