diff --git a/.github/actions/setup-sqlite-version/action.yml b/.github/actions/setup-sqlite-version/action.yml deleted file mode 100644 index fdbc71c9..00000000 --- a/.github/actions/setup-sqlite-version/action.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: "Setup SQLite version" -description: "Build and activate a specific SQLite version from its amalgamation archive" -inputs: - version: - description: "The SQLite version to install" - required: true - cflags: - description: "CFLAGS to use when compiling SQLite" - required: false - default: "" - skip-activate: - description: "Set to true to skip modifying the library path" - required: false - default: "false" - fallback-urls: - description: "Whitespace-separated fallback download URLs to try after sqlite.org" - required: false - default: "" -outputs: - sqlite-location: - description: "Directory containing the compiled SQLite library" - value: ${{ steps.build.outputs.sqlite-location }} -runs: - using: "composite" - steps: - - shell: bash - run: mkdir -p "$RUNNER_TEMP/sqlite-versions/downloads" - - uses: actions/cache@v6 - with: - path: ${{ runner.temp }}/sqlite-versions/downloads - key: setup-sqlite-version-${{ inputs.version }}-amalgamation-v1 - - id: build - shell: bash - run: bash "$GITHUB_ACTION_PATH/setup-sqlite-version.sh" - env: - SQLITE_VERSION: ${{ inputs.version }} - SQLITE_CFLAGS: ${{ inputs.cflags }} - SQLITE_SKIP_ACTIVATE: ${{ inputs.skip-activate }} - SQLITE_EXTRA_FALLBACK_URLS: ${{ inputs.fallback-urls }} diff --git a/.github/actions/setup-sqlite-version/setup-sqlite-version.sh b/.github/actions/setup-sqlite-version/setup-sqlite-version.sh deleted file mode 100644 index 03d6a68f..00000000 --- a/.github/actions/setup-sqlite-version/setup-sqlite-version.sh +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -version_spec="${SQLITE_VERSION:?SQLITE_VERSION is required}" -cflags="${SQLITE_CFLAGS:-}" -skip_activate="${SQLITE_SKIP_ACTIVATE:-false}" -extra_fallback_urls="${SQLITE_EXTRA_FALLBACK_URLS:-}" - -case "$version_spec" in - 3.46 | 3.46.0) - sqlite_version="3.46.0" - sqlite_year="2024" - amalgamation_id="3460000" - builtin_fallback_urls="https://static.simonwillison.net/static/2026/sqlite-amalgamation-3460000.zip" - ;; - 3.25 | 3.25.0) - sqlite_version="3.25.0" - sqlite_year="2018" - amalgamation_id="3250000" - builtin_fallback_urls="https://static.simonwillison.net/static/2026/sqlite-amalgamation-3250000.zip?v=1" - ;; - *) - echo "::error::Unsupported SQLite version '$version_spec'. Add its release year and amalgamation id to $GITHUB_ACTION_PATH/setup-sqlite-version.sh." - exit 1 - ;; -esac - -case "$(uname -s)" in - Linux) - library_name="libsqlite3.so.0" - library_path_var="LD_LIBRARY_PATH" - ;; - Darwin) - library_name="libsqlite3.dylib" - library_path_var="DYLD_LIBRARY_PATH" - ;; - *) - echo "::error::Unsupported platform $(uname -s)" - exit 1 - ;; -esac - -runner_temp="${RUNNER_TEMP:-}" -if [ -z "$runner_temp" ]; then - runner_temp="$(mktemp -d)" -fi - -filename="sqlite-amalgamation-${amalgamation_id}" -official_url="https://www.sqlite.org/${sqlite_year}/${filename}.zip" -download_dir="${runner_temp}/sqlite-versions/downloads" -source_root="${runner_temp}/sqlite-versions/source" -source_dir="${source_root}/${filename}" -build_dir="${runner_temp}/sqlite-versions/build/${sqlite_version}" -archive_path="${download_dir}/${filename}.zip" - -mkdir -p "$download_dir" "$source_root" "$build_dir" - -download_archive() { - local url - local candidate_path="${archive_path}.tmp" - local urls=("$official_url") - - for url in $builtin_fallback_urls $extra_fallback_urls; do - urls+=("$url") - done - - rm -f "$candidate_path" - for url in "${urls[@]}"; do - echo "Downloading SQLite ${sqlite_version} amalgamation from ${url}" - if curl \ - --fail \ - --location \ - --show-error \ - --retry 5 \ - --retry-delay 2 \ - --retry-max-time 180 \ - --retry-all-errors \ - --connect-timeout 20 \ - --max-time 240 \ - --output "$candidate_path" \ - "$url"; then - mv "$candidate_path" "$archive_path" - return 0 - fi - - echo "::warning::Download failed from ${url}" - rm -f "$candidate_path" - done - - echo "::error::Could not download SQLite ${sqlite_version} amalgamation" - return 1 -} - -if [ ! -f "${source_dir}/sqlite3.c" ]; then - if [ ! -f "$archive_path" ]; then - download_archive - fi - - rm -rf "$source_dir" - unzip -q "$archive_path" -d "$source_root" -fi - -if [ ! -f "${source_dir}/sqlite3.c" ]; then - echo "::error::Expected ${source_dir}/sqlite3.c after extracting ${archive_path}" - exit 1 -fi - -read -r -a cflag_args <<< "$cflags" - -echo "Compiling SQLite ${sqlite_version} to ${build_dir}/${library_name}" -gcc \ - -fPIC \ - -shared \ - "${cflag_args[@]}" \ - "${source_dir}/sqlite3.c" \ - "-I${source_dir}" \ - -o "${build_dir}/${library_name}" - -if [ "$library_name" = "libsqlite3.so.0" ]; then - ln -sf "$library_name" "${build_dir}/libsqlite3.so" -fi - -if [ -n "${GITHUB_OUTPUT:-}" ]; then - echo "sqlite-location=${build_dir}" >> "$GITHUB_OUTPUT" -else - echo "sqlite-location=${build_dir}" -fi - -case "$(printf '%s' "$skip_activate" | tr '[:upper:]' '[:lower:]')" in - true | 1 | yes) - echo "Skipping ${library_path_var} activation" - ;; - *) - existing_value="${!library_path_var:-}" - if [ -n "${GITHUB_ENV:-}" ]; then - if [ -n "$existing_value" ]; then - echo "${library_path_var}=${build_dir}:${existing_value}" >> "$GITHUB_ENV" - else - echo "${library_path_var}=${build_dir}" >> "$GITHUB_ENV" - fi - fi - echo "Added ${build_dir} to ${library_path_var}" - ;; -esac diff --git a/.github/workflows/deploy-branch-preview.yml b/.github/workflows/deploy-branch-preview.yml new file mode 100644 index 00000000..872aff71 --- /dev/null +++ b/.github/workflows/deploy-branch-preview.yml @@ -0,0 +1,35 @@ +name: Deploy a Datasette branch preview to Vercel + +on: + workflow_dispatch: + inputs: + branch: + description: "Branch to deploy" + required: true + type: string + +jobs: + deploy-branch-preview: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.11 + uses: actions/setup-python@v4 + with: + python-version: "3.11" + - name: Install dependencies + run: | + pip install datasette-publish-vercel + - name: Deploy the preview + env: + VERCEL_TOKEN: ${{ secrets.BRANCH_PREVIEW_VERCEL_TOKEN }} + run: | + export BRANCH="${{ github.event.inputs.branch }}" + wget https://latest.datasette.io/fixtures.db + datasette publish vercel fixtures.db \ + --branch $BRANCH \ + --project "datasette-preview-$BRANCH" \ + --token $VERCEL_TOKEN \ + --scope datasette \ + --about "Preview of $BRANCH" \ + --about_url "https://github.com/simonw/datasette/tree/$BRANCH" diff --git a/.github/workflows/deploy-latest.yml b/.github/workflows/deploy-latest.yml index 3fc83438..e0405440 100644 --- a/.github/workflows/deploy-latest.yml +++ b/.github/workflows/deploy-latest.yml @@ -1,11 +1,10 @@ name: Deploy latest.datasette.io on: - workflow_dispatch: push: branches: - main - # - 1.0-dev + - 1.0-dev permissions: contents: read @@ -15,16 +14,24 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out datasette - uses: actions/checkout@v7 + uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v4 + # gcloud commmand breaks on higher Python versions, so stick with 3.9: with: - python-version: "3.13" - cache: pip + python-version: "3.9" + - uses: actions/cache@v3 + name: Configure pip caching + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- - name: Install Python dependencies run: | python -m pip install --upgrade pip - python -m pip install . --group dev + python -m pip install -e .[test] + python -m pip install -e .[docs] python -m pip install sphinx-to-sqlite==0.1a1 - name: Run tests if: ${{ github.ref == 'refs/heads/main' }} @@ -57,7 +64,7 @@ jobs: db.route = "alternative-route" ' > plugins/alternative_route.py cp fixtures.db fixtures2.db - - name: And the counters writable stored query demo + - name: And the counters writable canned query demo run: | cat > plugins/counters.py < metadata.json # cat metadata.json - - id: auth - name: Authenticate to Google Cloud - uses: google-github-actions/auth@v3 + - name: Set up Cloud Run + uses: google-github-actions/setup-gcloud@v0 with: - credentials_json: ${{ secrets.GCP_SA_KEY }} - - name: Set up Cloud SDK - uses: google-github-actions/setup-gcloud@v3 + version: '318.0.0' + service_account_email: ${{ secrets.GCP_SA_EMAIL }} + service_account_key: ${{ secrets.GCP_SA_KEY }} - name: Deploy to Cloud Run env: LATEST_DATASETTE_SECRET: ${{ secrets.LATEST_DATASETTE_SECRET }} @@ -117,7 +122,7 @@ jobs: --plugins-dir=plugins \ --branch=$GITHUB_SHA \ --version-note=$GITHUB_SHA \ - --extra-options="--setting template_debug 1 --setting trace_debug 1 --crossdb --root" \ + --extra-options="--setting template_debug 1 --setting trace_debug 1 --crossdb" \ --install 'datasette-ephemeral-tables>=0.2.2' \ --service "datasette-latest$SUFFIX" \ --secret $LATEST_DATASETTE_SECRET diff --git a/.github/workflows/documentation-links.yml b/.github/workflows/documentation-links.yml new file mode 100644 index 00000000..a54bd83a --- /dev/null +++ b/.github/workflows/documentation-links.yml @@ -0,0 +1,16 @@ +name: Read the Docs Pull Request Preview +on: + pull_request_target: + types: + - opened + +permissions: + pull-requests: write + +jobs: + documentation-links: + runs-on: ubuntu-latest + steps: + - uses: readthedocs/actions/preview@v1 + with: + project-slug: "datasette" diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml deleted file mode 100644 index f5b8dbf6..00000000 --- a/.github/workflows/playwright.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: Playwright - -on: - push: - pull_request: - workflow_dispatch: - -permissions: - contents: read - -jobs: - test: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - browser: [chromium, firefox, webkit] - steps: - - uses: actions/checkout@v7 - - name: Set up Python 3.14 - uses: actions/setup-python@v6 - with: - python-version: "3.14" - allow-prereleases: true - cache: pip - cache-dependency-path: pyproject.toml - - name: Cache uv - uses: actions/cache@v6 - with: - path: ~/.cache/uv - key: ${{ runner.os }}-py3.14-uv-${{ hashFiles('pyproject.toml') }} - restore-keys: | - ${{ runner.os }}-py3.14-uv- - - name: Cache Playwright browsers - uses: actions/cache@v6 - with: - path: ~/.cache/ms-playwright/ - key: ${{ runner.os }}-playwright-${{ matrix.browser }}-${{ hashFiles('pyproject.toml') }} - restore-keys: | - ${{ runner.os }}-playwright-${{ matrix.browser }}- - - name: Install uv - run: python -m pip install uv - - name: Install dependencies - run: uv sync --group dev --group playwright - - name: Install ${{ matrix.browser }} - run: uv run --group dev --group playwright playwright install --with-deps ${{ matrix.browser }} - - name: Run Playwright tests - run: uv run --group dev --group playwright pytest tests/test_playwright.py --playwright --browser ${{ matrix.browser }} diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml index d92ab82b..ded41040 100644 --- a/.github/workflows/prettier.yml +++ b/.github/workflows/prettier.yml @@ -10,8 +10,8 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repo - uses: actions/checkout@v7 - - uses: actions/cache@v6 + uses: actions/checkout@v2 + - uses: actions/cache@v2 name: Configure npm caching with: path: ~/.npm diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 21ed4c12..64a03a77 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -12,18 +12,23 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.8", "3.9", "3.10", "3.11"] steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - cache: pip - cache-dependency-path: pyproject.toml + - uses: actions/cache@v3 + name: Configure pip caching + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- - name: Install dependencies run: | - pip install . --group dev + pip install -e '.[test]' - name: Run tests run: | pytest @@ -31,41 +36,50 @@ jobs: deploy: runs-on: ubuntu-latest needs: [test] - environment: release - permissions: - id-token: write steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v4 with: - python-version: '3.13' - cache: pip - cache-dependency-path: pyproject.toml + python-version: '3.11' + - uses: actions/cache@v3 + name: Configure pip caching + with: + path: ~/.cache/pip + key: ${{ runner.os }}-publish-pip-${{ hashFiles('**/setup.py') }} + restore-keys: | + ${{ runner.os }}-publish-pip- - name: Install dependencies run: | - pip install setuptools wheel build - - name: Build - run: | - python -m build + pip install setuptools wheel twine - name: Publish - uses: pypa/gh-action-pypi-publish@release/v1 + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + run: | + python setup.py sdist bdist_wheel + twine upload dist/* deploy_static_docs: runs-on: ubuntu-latest needs: [deploy] if: "!github.event.release.prerelease" steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v2 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v2 with: - python-version: '3.10' - cache: pip - cache-dependency-path: pyproject.toml + python-version: '3.9' + - uses: actions/cache@v2 + name: Configure pip caching + with: + path: ~/.cache/pip + key: ${{ runner.os }}-publish-pip-${{ hashFiles('**/setup.py') }} + restore-keys: | + ${{ runner.os }}-publish-pip- - name: Install dependencies run: | - python -m pip install . --group dev + python -m pip install -e .[docs] python -m pip install sphinx-to-sqlite==0.1a1 - name: Build docs.db run: |- @@ -73,13 +87,12 @@ jobs: DISABLE_SPHINX_INLINE_TABS=1 sphinx-build -b xml . _build sphinx-to-sqlite ../docs.db _build cd .. - - id: auth - name: Authenticate to Google Cloud - uses: google-github-actions/auth@v2 + - name: Set up Cloud Run + uses: google-github-actions/setup-gcloud@v0 with: - credentials_json: ${{ secrets.GCP_SA_KEY }} - - name: Set up Cloud SDK - uses: google-github-actions/setup-gcloud@v3 + version: '318.0.0' + service_account_email: ${{ secrets.GCP_SA_EMAIL }} + service_account_key: ${{ secrets.GCP_SA_KEY }} - name: Deploy stable-docs.datasette.io to Cloud Run run: |- gcloud config set run/region us-central1 @@ -92,7 +105,7 @@ jobs: needs: [deploy] if: "!github.event.release.prerelease" steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v2 - name: Build and push to Docker Hub env: DOCKER_USER: ${{ secrets.DOCKER_USER }} diff --git a/.github/workflows/push_docker_tag.yml b/.github/workflows/push_docker_tag.yml index c5a4f0db..afe8d6b2 100644 --- a/.github/workflows/push_docker_tag.yml +++ b/.github/workflows/push_docker_tag.yml @@ -13,7 +13,7 @@ jobs: deploy_docker: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v2 - name: Build and push to Docker Hub env: DOCKER_USER: ${{ secrets.DOCKER_USER }} diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index 58635025..0ce9e10c 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -9,19 +9,18 @@ jobs: spellcheck: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v4 with: python-version: '3.11' cache: 'pip' - cache-dependency-path: '**/pyproject.toml' + cache-dependency-path: '**/setup.py' - name: Install dependencies run: | - pip install . --group dev + pip install -e '.[docs]' - name: Check spelling run: | codespell README.md --ignore-words docs/codespell-ignore-words.txt codespell docs/*.rst --ignore-words docs/codespell-ignore-words.txt codespell datasette -S datasette/static --ignore-words docs/codespell-ignore-words.txt - codespell tests --ignore-words docs/codespell-ignore-words.txt diff --git a/.github/workflows/stable-docs.yml b/.github/workflows/stable-docs.yml deleted file mode 100644 index ecde5940..00000000 --- a/.github/workflows/stable-docs.yml +++ /dev/null @@ -1,76 +0,0 @@ -name: Update Stable Docs - -on: - release: - types: [published] - push: - branches: - - main - -permissions: - contents: write - -jobs: - update_stable_docs: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v7 - with: - fetch-depth: 0 # We need all commits to find docs/ changes - - name: Set up Git user - run: | - git config user.name "Automated" - git config user.email "actions@users.noreply.github.com" - - name: Create stable branch if it does not yet exist - run: | - if ! git ls-remote --heads origin stable | grep -qE '\bstable\b'; then - # Make sure we have all tags locally - git fetch --tags --quiet - - # Latest tag that is just numbers and dots (optionally prefixed with 'v') - # e.g., 0.65.2 or v0.65.2 — excludes 1.0a20, 1.0-rc1, etc. - LATEST_RELEASE=$( - git tag -l --sort=-v:refname \ - | grep -E '^v?[0-9]+(\.[0-9]+){1,3}$' \ - | head -n1 - ) - - git checkout -b stable - - # If there are any stable releases, copy docs/ from the most recent - if [ -n "$LATEST_RELEASE" ]; then - rm -rf docs/ - git checkout "$LATEST_RELEASE" -- docs/ || true - fi - - git commit -m "Populate docs/ from $LATEST_RELEASE" || echo "No changes" - git push -u origin stable - fi - - name: Handle Release - if: github.event_name == 'release' && !github.event.release.prerelease - run: | - git fetch --all - git checkout stable - git reset --hard ${GITHUB_REF#refs/tags/} - git push origin stable --force - - name: Handle Commit to Main - if: contains(github.event.head_commit.message, '!stable-docs') - run: | - git fetch origin - git checkout -b stable origin/stable - # Get the list of modified files in docs/ from the current commit - FILES=$(git diff-tree --no-commit-id --name-only -r ${{ github.sha }} -- docs/) - # Check if the list of files is non-empty - if [[ -n "$FILES" ]]; then - # Checkout those files to the stable branch to over-write with their contents - for FILE in $FILES; do - git checkout ${{ github.sha }} -- $FILE - done - git add docs/ - git commit -m "Doc changes from ${{ github.sha }}" - git push origin stable - else - echo "No changes to docs/ in this commit." - exit 0 - fi diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml index e9bd4bab..bd720664 100644 --- a/.github/workflows/test-coverage.yml +++ b/.github/workflows/test-coverage.yml @@ -15,23 +15,28 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out datasette - uses: actions/checkout@v7 + uses: actions/checkout@v2 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v2 with: - python-version: '3.12' - cache: 'pip' - cache-dependency-path: '**/pyproject.toml' + python-version: 3.9 + - uses: actions/cache@v2 + name: Configure pip caching + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- - name: Install Python dependencies run: | python -m pip install --upgrade pip - python -m pip install . --group dev + python -m pip install -e .[test] python -m pip install pytest-cov - name: Run tests run: |- ls -lah cat .coveragerc - pytest -m "not serial" --cov=datasette --cov-config=.coveragerc --cov-report xml:coverage.xml --cov-report term -x + pytest --cov=datasette --cov-config=.coveragerc --cov-report xml:coverage.xml --cov-report term ls -lah - name: Upload coverage report uses: codecov/codecov-action@v1 diff --git a/.github/workflows/test-pyodide.yml b/.github/workflows/test-pyodide.yml index 5e81ed82..bc9593a8 100644 --- a/.github/workflows/test-pyodide.yml +++ b/.github/workflows/test-pyodide.yml @@ -12,15 +12,15 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v3 - name: Set up Python 3.10 - uses: actions/setup-python@v6 + uses: actions/setup-python@v3 with: python-version: "3.10" cache: 'pip' - cache-dependency-path: '**/pyproject.toml' + cache-dependency-path: '**/setup.py' - name: Cache Playwright browsers - uses: actions/cache@v6 + uses: actions/cache@v2 with: path: ~/.cache/ms-playwright/ key: ${{ runner.os }}-browsers diff --git a/.github/workflows/test-sqlite-support.yml b/.github/workflows/test-sqlite-support.yml deleted file mode 100644 index 2fdb3a40..00000000 --- a/.github/workflows/test-sqlite-support.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Test SQLite versions - -on: [push, pull_request] - -permissions: - contents: read - -jobs: - test: - runs-on: ${{ matrix.platform }} - continue-on-error: true - strategy: - matrix: - platform: [ubuntu-latest] - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] - sqlite-version: [ - #"3", # latest version - "3.46", - #"3.45", - #"3.27", - #"3.26", - "3.25", - #"3.25.3", # 2018-09-25, window functions breaks test_upsert for some reason on 3.10, skip for now - #"3.24", # 2018-06-04, added UPSERT support - #"3.23.1" # 2018-04-10, before UPSERT - ] - steps: - - uses: actions/checkout@v7 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 - with: - python-version: ${{ matrix.python-version }} - allow-prereleases: true - cache: pip - cache-dependency-path: pyproject.toml - - name: Set up SQLite ${{ matrix.sqlite-version }} - uses: ./.github/actions/setup-sqlite-version - with: - version: ${{ matrix.sqlite-version }} - cflags: "-DSQLITE_ENABLE_DESERIALIZE -DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_FTS4 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_RTREE -DSQLITE_ENABLE_JSON1" - - run: python3 -c "import sqlite3; print(sqlite3.sqlite_version)" - - run: echo $LD_LIBRARY_PATH - - name: Build extension for --load-extension test - run: |- - (cd tests && gcc ext.c -fPIC -shared -o ext.so) - - name: Install dependencies - run: | - pip install . --group dev - pip freeze - - name: Run tests - run: | - pytest -n auto -m "not serial" - pytest -m "serial" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 751eedfd..656b0b1c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,24 +9,28 @@ jobs: test: runs-on: ubuntu-latest strategy: - fail-fast: false matrix: - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} allow-prereleases: true - cache: pip - cache-dependency-path: pyproject.toml + - uses: actions/cache@v3 + name: Configure pip caching + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- - name: Build extension for --load-extension test run: |- (cd tests && gcc ext.c -fPIC -shared -o ext.so) - name: Install dependencies run: | - pip install . --group dev + pip install -e '.[test]' pip freeze - name: Run tests run: | @@ -34,16 +38,16 @@ jobs: pytest -m "serial" # And the test that exceeds a localhost HTTPS server tests/test_datasette_https_server.sh - - name: Black + - name: Install docs dependencies on Python 3.9+ + if: matrix.python-version != '3.8' run: | - black --version - black --check . - - name: Ruff - run: ruff check datasette tests + pip install -e '.[docs]' - name: Check if cog needs to be run + if: matrix.python-version != '3.8' run: | cog --check docs/*.rst - name: Check if blacken-docs needs to be run + if: matrix.python-version != '3.8' run: | # This fails on syntax errors, or a diff was applied blacken-docs -l 60 docs/*.rst diff --git a/.github/workflows/tmate-mac.yml b/.github/workflows/tmate-mac.yml index f2c074a6..fcee0f21 100644 --- a/.github/workflows/tmate-mac.yml +++ b/.github/workflows/tmate-mac.yml @@ -10,6 +10,6 @@ jobs: build: runs-on: macos-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v2 - name: Setup tmate session uses: mxschmitt/action-tmate@v3 diff --git a/.github/workflows/tmate.yml b/.github/workflows/tmate.yml index 5b8818c3..9792245d 100644 --- a/.github/workflows/tmate.yml +++ b/.github/workflows/tmate.yml @@ -5,14 +5,11 @@ on: permissions: contents: read - models: read jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v2 - name: Setup tmate session uses: mxschmitt/action-tmate@v3 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 2a7f6620..277ff653 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,10 @@ build-metadata.json datasets.json -.playwright-mcp - scratchpad -ignored/ - .vscode -uv.lock -data.db - -# test databases -*.db - # We don't use Pipfile, so ignore them Pipfile Pipfile.lock @@ -133,6 +123,4 @@ node_modules # include it in source control. tests/*.dylib tests/*.so -tests/*.dll - -.idea +tests/*.dll \ No newline at end of file diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 8b3e54aa..5b30e75a 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,17 +1,16 @@ version: 2 -sphinx: - configuration: docs/conf.py - build: - os: ubuntu-24.04 + os: ubuntu-20.04 tools: - python: "3.13" - jobs: - install: - - pip install --upgrade pip - - pip install . --group dev + python: "3.11" -formats: -- pdf -- epub +sphinx: + configuration: docs/conf.py + +python: + install: + - method: pip + path: . + extra_requirements: + - docs diff --git a/Justfile b/Justfile index 6ffff870..d349ec51 100644 --- a/Justfile +++ b/Justfile @@ -5,73 +5,37 @@ export DATASETTE_SECRET := "not_a_secret" # Setup project @init: - uv sync + pipenv run pip install -e '.[test,docs]' # Run pytest with supplied options -@test *options: init - uv run pytest -n auto {{options}} - -# Install Playwright browser support, Chromium by default -@playwright-install browser="chromium": - uv run --group playwright playwright install {{browser}} - -# Install all Playwright browsers used by the test suite -@playwright-install-all: - uv run --group playwright playwright install chromium firefox webkit - -# Run Playwright tests, Chromium by default -@playwright browser="chromium" *options: - uv run --group playwright pytest tests/test_playwright.py --playwright --browser {{browser}} {{options}} - -# Run Playwright tests against all supported browsers -@playwright-all *options: - uv run --group playwright pytest tests/test_playwright.py --playwright --browser chromium --browser firefox --browser webkit {{options}} +@test *options: + pipenv run pytest {{options}} @codespell: - uv run codespell README.md --ignore-words docs/codespell-ignore-words.txt - uv run codespell docs/*.rst --ignore-words docs/codespell-ignore-words.txt - uv run codespell datasette -S datasette/static --ignore-words docs/codespell-ignore-words.txt - uv run codespell tests --ignore-words docs/codespell-ignore-words.txt + pipenv run codespell README.md --ignore-words docs/codespell-ignore-words.txt + pipenv run codespell docs/*.rst --ignore-words docs/codespell-ignore-words.txt + pipenv run codespell datasette -S datasette/static --ignore-words docs/codespell-ignore-words.txt -# Run linters: black, ruff, prettier, cog +# Run linters: black, flake8, mypy, cog @lint: codespell - uv run black datasette tests --check - uv run ruff check datasette tests - npm run prettier -- --check - uv run cog --check README.md docs/*.rst - -# Apply ruff fixes -@fix: - uv run ruff check --fix datasette tests + pipenv run black . --check + pipenv run flake8 + pipenv run cog --check README.md docs/*.rst # Rebuild docs with cog @cog: - uv run cog -r README.md docs/*.rst + pipenv run cog -r README.md docs/*.rst # Serve live docs on localhost:8000 -@docs: cog blacken-docs - uv run make -C docs livehtml - -# Build docs as static HTML -@docs-build: cog blacken-docs - rm -rf docs/_build && cd docs && uv run make html +@docs: cog + pipenv run blacken-docs -l 60 docs/*.rst + cd docs && pipenv run make livehtml # Apply Black @black: - uv run black datasette tests + pipenv run black . -# Apply blacken-docs -@blacken-docs: - uv run blacken-docs -l 60 docs/*.rst - -# Apply prettier -@prettier: - npm run fix - -# Format code with both black and prettier -@format: black prettier blacken-docs - -@serve *options: - uv run sqlite-utils create-database data.db - uv run sqlite-utils create-table data.db docs id integer title text --pk id --ignore - uv run python -m datasette data.db --root --reload {{options}} +@serve: + pipenv run sqlite-utils create-database data.db + pipenv run sqlite-utils create-table data.db docs id integer title text --pk id --ignore + pipenv run python -m datasette data.db --root --reload diff --git a/README.md b/README.md index 393e8e5c..57f17a5c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Datasette [![PyPI](https://img.shields.io/pypi/v/datasette.svg)](https://pypi.org/project/datasette/) -[![Changelog](https://img.shields.io/github/v/release/simonw/datasette?label=changelog)](https://docs.datasette.io/en/latest/changelog.html) +[![Changelog](https://img.shields.io/github/v/release/simonw/datasette?label=changelog)](https://docs.datasette.io/en/stable/changelog.html) [![Python 3.x](https://img.shields.io/pypi/pyversions/datasette.svg?logo=python&logoColor=white)](https://pypi.org/project/datasette/) [![Tests](https://github.com/simonw/datasette/workflows/Test/badge.svg)](https://github.com/simonw/datasette/actions?query=workflow%3ATest) [![Documentation Status](https://readthedocs.org/projects/datasette/badge/?version=latest)](https://docs.datasette.io/en/latest/?badge=latest) @@ -15,7 +15,7 @@ Datasette is a tool for exploring and publishing data. It helps people take data Datasette is aimed at data journalists, museum curators, archivists, local governments, scientists, researchers and anyone else who has data that they wish to share with the world. -[Explore a demo](https://datasette.io/global-power-plants/global-power-plants), watch [a video about the project](https://simonwillison.net/2021/Feb/7/video/) or try it out [on GitHub Codespaces](https://github.com/datasette/datasette-studio). +[Explore a demo](https://global-power-plants.datasettes.com/global-power-plants/global-power-plants), watch [a video about the project](https://simonwillison.net/2021/Feb/7/video/) or try it out by [uploading and publishing your own CSV data](https://docs.datasette.io/en/stable/getting_started.html#try-datasette-without-installing-anything-using-glitch). * [datasette.io](https://datasette.io/) is the official project website * Latest [Datasette News](https://datasette.io/news) diff --git a/datasette/__init__.py b/datasette/__init__.py index e0022178..271e09ad 100644 --- a/datasette/__init__.py +++ b/datasette/__init__.py @@ -1,14 +1,6 @@ from datasette.permissions import Permission # noqa from datasette.version import __version_info__, __version__ # noqa -from datasette.events import Event # noqa -from datasette.tokens import TokenHandler, TokenInvalid, TokenRestrictions # noqa -from datasette.utils.asgi import ( # noqa - Forbidden, - NotFound, - PayloadTooLarge, - Request, - Response, -) +from datasette.utils.asgi import Forbidden, NotFound, Request, Response # noqa from datasette.utils import actor_matches_allow # noqa from datasette.views import Context # noqa from .hookspecs import hookimpl # noqa diff --git a/datasette/_pytest_plugin.py b/datasette/_pytest_plugin.py deleted file mode 100644 index 103c616d..00000000 --- a/datasette/_pytest_plugin.py +++ /dev/null @@ -1,123 +0,0 @@ -""" -Pytest plugin that automatically closes any Datasette instances constructed -during a pytest test — both in the test body and in function-scoped -fixtures. Instances constructed by session-, module-, class- or package- -scoped fixtures are left alone, because other tests in the session will -still want to use them. - -Registered as a pytest11 entry point in pyproject.toml so that downstream -projects using Datasette get the same FD-safety net for their own tests. - -Opt out by setting ``datasette_autoclose = false`` in pytest.ini (or the -equivalent ini file). -""" - -from __future__ import annotations - -import contextvars -import weakref - -import pytest - -_active_instances: contextvars.ContextVar[list | None] = contextvars.ContextVar( - "datasette_active_instances", default=None -) - -_original_init = None - - -def _install_tracking(): - # datasette.app is imported lazily here rather than at module level: - # as a pytest11 entry point this module is imported during pytest - # startup, before pytest-cov starts measuring, so a module-level - # import would drag in all of datasette and make every import-time - # line in the package invisible to coverage - global _original_init - if _original_init is not None: - return - from datasette.app import Datasette - - _original_init = Datasette.__init__ - - def _tracking_init(self, *args, **kwargs): - _original_init(self, *args, **kwargs) - instances = _active_instances.get() - if instances is not None: - instances.append(weakref.ref(self)) - - Datasette.__init__ = _tracking_init - - -def pytest_configure(config): - if _enabled(config): - _install_tracking() - - -def pytest_addoption(parser): - parser.addini( - "datasette_autoclose", - help=( - "Automatically close Datasette instances created inside test " - "bodies and function-scoped fixtures (default: true)." - ), - default="true", - ) - - -def _enabled(config) -> bool: - value = config.getini("datasette_autoclose") - if isinstance(value, bool): - return value - return str(value).strip().lower() not in ("false", "0", "no", "off") - - -@pytest.hookimpl(hookwrapper=True) -def pytest_runtest_protocol(item, nextitem): - """Track Datasette instances across setup, call and teardown; close at end.""" - if not _enabled(item.config): - yield - return - refs: list[weakref.ref] = [] - token = _active_instances.set(refs) - try: - yield - finally: - _active_instances.reset(token) - for ref in reversed(refs): - ds = ref() - if ds is None: - continue - try: - ds.close() - except Exception as e: - item.warn( - pytest.PytestUnraisableExceptionWarning( - f"Error closing Datasette instance: {e!r}" - ) - ) - - -@pytest.hookimpl(hookwrapper=True) -def pytest_fixture_setup(fixturedef, request): - """Exempt instances created by non-function-scoped fixtures. - - Session-, module-, class- and package-scoped fixtures produce Datasette - instances that must survive beyond the current test — other tests in - the session will still use them. When such a fixture creates one or - more Datasette instances during its setup, we snapshot the tracking - list before the fixture runs and subtract off any instances that were - added during its setup, so they don't get closed at test teardown. - """ - refs = _active_instances.get() - if refs is None: - yield - return - before_ids = {id(ref) for ref in refs} - yield - if fixturedef.scope != "function": - new_refs = [ref for ref in refs if id(ref) not in before_ids] - for new_ref in new_refs: - try: - refs.remove(new_ref) - except ValueError: - pass diff --git a/datasette/app.py b/datasette/app.py index 0e31273d..f33865e4 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -1,17 +1,12 @@ -from __future__ import annotations - import asyncio -import contextvars -from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Sequence - -if TYPE_CHECKING: - from datasette.permissions import Resource - from datasette.tokens import TokenRestrictions +from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Union +import asgi_csrf import collections import dataclasses import datetime import functools import glob +import hashlib import httpx import importlib.metadata import inspect @@ -34,44 +29,18 @@ from jinja2 import ( ChoiceLoader, Environment, FileSystemLoader, - pass_context, PrefixLoader, ) from jinja2.environment import Template from jinja2.exceptions import TemplateNotFound -from .events import Event -from .column_types import SQLiteType -from . import stored_queries, write_sql from .views import Context -from .views.database import ( - database_download, - DatabaseView, - QueryView, -) -from .views.table_create_alter import ( - DatabaseForeignKeyTargetsView, - TableAlterView, - TableCreateView, - TableForeignKeySuggestionsView, -) -from .views.execute_write import ExecuteWriteAnalyzeView, ExecuteWriteView -from .views.stored_queries import ( - QueryCreateAnalyzeView, - QueryDeleteView, - QueryDefinitionView, - QueryEditView, - GlobalQueryListView, - QueryListView, - QueryParametersView, - QueryStoreView, - QueryUpdateView, -) +from .views.base import ureg +from .views.database import database_download, DatabaseView, TableCreateView from .views.index import IndexView from .views.special import ( JsonDataView, PatternPortfolioView, - AutocompleteDebugView, AuthTokenView, ApiExplorerView, CreateTokenView, @@ -79,21 +48,11 @@ from .views.special import ( AllowDebugView, PermissionsDebugView, MessagesDebugView, - AllowedResourcesView, - PermissionRulesView, - PermissionCheckView, - JumpView, - InstanceSchemaView, - DatabaseSchemaView, - TableSchemaView, ) from .views.table import ( - TableAutocompleteView, TableInsertView, TableUpsertView, - TableSetColumnTypeView, TableDropView, - TableFragmentView, table_view, ) from .views.row import RowView, RowDeleteView, RowUpdateView @@ -102,39 +61,29 @@ from .url_builder import Urls from .database import Database, QueryInterrupted from .utils import ( - PaginatedResources, PrefixedUrlString, SPATIALITE_FUNCTIONS, StartupError, async_call_with_supported_arguments, await_me_maybe, - baseconv, call_with_supported_arguments, - detect_json1, - add_cors_headers, display_actor, escape_css_string, escape_sqlite, find_spatialite, format_bytes, module_from_path, - move_plugins_and_allow, - move_table_config, parse_metadata, resolve_env_secrets, resolve_routes, - sha256_file, + fail_if_plugins_in_metadata, tilde_decode, - tilde_encode, to_css_class, urlsafe_components, - redact_keys, row_sql_params_pks, ) -from .tokens import TokenInvalid from .utils.asgi import ( AsgiLifespan, - BadRequest, Forbidden, NotFound, DatabaseNotFound, @@ -148,7 +97,6 @@ from .utils.asgi import ( asgi_send_file, asgi_send_redirect, ) -from .csrf import CrossOriginProtectionMiddleware from .utils.internal_db import init_internal_db, populate_schema_tables from .utils.sqlite import ( sqlite3, @@ -158,44 +106,11 @@ from .tracer import AsgiTracer from .plugins import pm, DEFAULT_PLUGINS, get_plugins from .version import __version__ -from .resources import DatabaseResource, TableResource - app_root = Path(__file__).parent.parent - -# Context variable to track when code is executing within a datasette.client request -_in_datasette_client = contextvars.ContextVar("in_datasette_client", default=False) - - -class _DatasetteClientContext: - """Context manager to mark code as executing within a datasette.client request.""" - - def __enter__(self): - self.token = _in_datasette_client.set(True) - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - _in_datasette_client.reset(self.token) - return False - - -@dataclasses.dataclass -class PermissionCheck: - """Represents a logged permission check for debugging purposes.""" - - when: str - actor: Dict[str, Any] | None - action: str - parent: str | None - child: str | None - result: bool - - # https://github.com/simonw/datasette/issues/283#issuecomment-781591015 SQLITE_LIMIT_ATTACHED = 10 -INTERNAL_DB_NAME = "__INTERNAL__" - Setting = collections.namedtuple("Setting", ("name", "default", "help")) SETTINGS = ( Setting("default_page_size", 100, "Default page size for the table view"), @@ -209,11 +124,6 @@ SETTINGS = ( 100, "Maximum rows that can be inserted at a time using the bulk insert API", ), - Setting( - "max_post_body_bytes", - 2 * 1024 * 1024, - "Maximum size in bytes for a POST body read into memory, e.g. JSON API requests - set 0 to disable this limit", - ), Setting( "num_sql_threads", 3, @@ -305,24 +215,12 @@ FAVICON_PATH = app_root / "datasette" / "static" / "favicon.png" DEFAULT_NOT_SET = object() -ResourcesSQL = collections.namedtuple("ResourcesSQL", ("sql", "params")) - - -def _permission_cache_key(actor, action, parent, child): - # Key on the full serialized actor so actors differing in any field - # (e.g. token restrictions) never share cache entries - actor_key = ( - json.dumps(actor, sort_keys=True, default=repr) if actor is not None else None - ) - return (actor_key, action, parent, child) - - async def favicon(request, send): await asgi_send_file( send, str(FAVICON_PATH), content_type="image/png", - headers={"Cache-Control": "max-age=3600, public"}, + headers={"Cache-Control": "max-age=3600, immutable, public"}, ) @@ -332,64 +230,6 @@ ResolvedRow = collections.namedtuple( ) -def _to_string(value): - if isinstance(value, str): - return value - else: - return json.dumps(value, default=str) - - -def _template_context_json_default(value): - if dataclasses.is_dataclass(value) and not isinstance(value, type): - return { - field.name: getattr(value, field.name) - for field in dataclasses.fields(value) - } - return repr(value) - - -@pass_context -def _legacy_template_csrftoken(context): - request = context.get("request") - if request and "csrftoken" in request.scope: - return request.scope["csrftoken"]() - return "" - - -def _resolve_static_asset_path(root_path, path): - root = Path(root_path).resolve() - full_path = (root / path).resolve() - try: - full_path.relative_to(root) - except ValueError: - raise ValueError("Static asset path cannot escape static root") from None - return full_path - - -# Documentation for the variables Datasette.render_template() adds to the -# context for every page. This is part of the documented template contract: -# keys added in render_template() must be documented here - the contract -# tests in tests/test_template_context.py enforce this, and the docs in -# docs/template_context.rst are generated from it. -TEMPLATE_BASE_CONTEXT = { - "request": "The current :ref:`Request object `, or None. Common properties include ``request.path``, ``request.args``, ``request.actor``, ``request.url_vars`` and ``request.host``.", - "crumb_items": 'Async function returning breadcrumb navigation items for the current page. Call it with ``request=request`` plus optional ``database=`` and ``table=`` arguments; it returns a list of ``{"href": url, "label": label}`` dictionaries.', - "urls": "Object with methods for constructing URLs within Datasette. Common methods include ``urls.instance()``, ``urls.database(database)``, ``urls.table(database, table)``, ``urls.query(database, query)``, ``urls.row(database, table, row_path)`` and ``urls.static(path)`` - see :ref:`internals_datasette_urls`.", - "actor": "The currently authenticated actor dictionary, or None. Actors usually include an ``id`` key and may include any other keys supplied by authentication plugins.", - "menu_links": "Async function returning links for the Datasette application menu, including links added by plugins. Each item is a link dictionary with ``href`` and ``label`` keys. See :ref:`plugin_hook_menu_links`; for page action menus that can also include JavaScript-backed buttons, see :ref:`plugin_actions`.", - "display_actor": "Function that accepts an actor dictionary and returns the display string used in the navigation menu.", - "show_logout": "True if the logout link should be shown in the navigation menu", - "zip": "Python's ``zip()`` builtin, made available to template logic", - "body_scripts": 'List of JavaScript snippets contributed by plugins using :ref:`plugin_hook_extra_body_script`. Each item is a dictionary with ``script`` containing JavaScript source and ``module`` indicating whether Datasette will wrap it in `` diff --git a/datasette/templates/_codemirror.html b/datasette/templates/_codemirror.html index 75c16168..c4629aeb 100644 --- a/datasette/templates/_codemirror.html +++ b/datasette/templates/_codemirror.html @@ -1,5 +1,5 @@ - - + + diff --git a/datasette/templates/_facet_results.html b/datasette/templates/_facet_results.html index 570bb37e..24d386bd 100644 --- a/datasette/templates/_facet_results.html +++ b/datasette/templates/_facet_results.html @@ -1,4 +1,4 @@ -
+
{% for facet_info in sorted_facet_results %}

@@ -12,9 +12,9 @@

    {% for facet_value in facet_info.results %} {% if not facet_value.selected %} -
  • {{ (facet_value.label | string()) or "-" }} {{ "{:,}".format(facet_value.count) }}
  • +
  • {{ (facet_value.label | string()) or "-" }} {{ "{:,}".format(facet_value.count) }}
  • {% else %} -
  • {{ facet_value.label or "-" }} · {{ "{:,}".format(facet_value.count) }}
  • +
  • {{ facet_value.label or "-" }} · {{ "{:,}".format(facet_value.count) }}
  • {% endif %} {% endfor %} {% if facet_info.truncated %} diff --git a/datasette/templates/_permission_ui_styles.html b/datasette/templates/_permission_ui_styles.html deleted file mode 100644 index 21a2ea8f..00000000 --- a/datasette/templates/_permission_ui_styles.html +++ /dev/null @@ -1,191 +0,0 @@ - diff --git a/datasette/templates/_permissions_debug_tabs.html b/datasette/templates/_permissions_debug_tabs.html deleted file mode 100644 index 8e0f486e..00000000 --- a/datasette/templates/_permissions_debug_tabs.html +++ /dev/null @@ -1,54 +0,0 @@ -{% if has_debug_permission %} -{% set query_string = '?' + request.query_string if request.query_string else '' %} - - - - -{% endif %} diff --git a/datasette/templates/_query_form_styles.html b/datasette/templates/_query_form_styles.html deleted file mode 100644 index cf2dd42c..00000000 --- a/datasette/templates/_query_form_styles.html +++ /dev/null @@ -1,138 +0,0 @@ - diff --git a/datasette/templates/_query_results.html b/datasette/templates/_query_results.html deleted file mode 100644 index 5e1e2f72..00000000 --- a/datasette/templates/_query_results.html +++ /dev/null @@ -1,20 +0,0 @@ -{% if display_rows %} -
    - - - {% for column in columns %}{% endfor %} - - - - {% for row in display_rows %} - - {% for column, td in zip(columns, row) %} - - {% endfor %} - - {% endfor %} - -
    {{ column }}
    {{ td }}
    -{% elif show_zero_results %} -

    0 results

    -{% endif %} diff --git a/datasette/templates/_sql_parameter_scripts.html b/datasette/templates/_sql_parameter_scripts.html deleted file mode 100644 index 9b83889e..00000000 --- a/datasette/templates/_sql_parameter_scripts.html +++ /dev/null @@ -1,307 +0,0 @@ - diff --git a/datasette/templates/_sql_parameter_styles.html b/datasette/templates/_sql_parameter_styles.html deleted file mode 100644 index bc6838f5..00000000 --- a/datasette/templates/_sql_parameter_styles.html +++ /dev/null @@ -1,58 +0,0 @@ - diff --git a/datasette/templates/_sql_parameters.html b/datasette/templates/_sql_parameters.html deleted file mode 100644 index b5c1bde8..00000000 --- a/datasette/templates/_sql_parameters.html +++ /dev/null @@ -1,10 +0,0 @@ -{% set sql_parameter_name_prefix = sql_parameter_name_prefix|default("") %} -
    - {% if parameter_names %} -

    Parameters

    - {% for parameter in parameter_names %} - {% set parameter_id = (sql_parameter_id_prefix|default("qp")) ~ loop.index %} -

    {% if sql_parameters_allow_expand|default(false) %} {% endif %}

    - {% endfor %} - {% endif %} -
    diff --git a/datasette/templates/_table.html b/datasette/templates/_table.html index 171b6442..9b57990e 100644 --- a/datasette/templates/_table.html +++ b/datasette/templates/_table.html @@ -1,12 +1,12 @@
    -{% if display_columns %} -
    +{% if display_rows %} +
    {% for column in display_columns %} - {% for row in display_rows %} - + {% for cell in row %} {% endfor %} @@ -31,7 +31,6 @@
    + {% if not column.sortable %} {{ column.name }} {% else %} @@ -22,7 +22,7 @@
    {{ cell.value }}
    -{% endif %} -{% if not display_rows %} +{% else %}

    0 records

    {% endif %} diff --git a/datasette/templates/allow_debug.html b/datasette/templates/allow_debug.html index fda4032c..04181531 100644 --- a/datasette/templates/allow_debug.html +++ b/datasette/templates/allow_debug.html @@ -3,11 +3,29 @@ {% block title %}Debug allow rules{% endblock %} {% block extra_head %} -{% include "_permission_ui_styles.html" %} {% endblock %} @@ -15,33 +33,26 @@ p.message-warning {

    Debug allow rules

    -{% set current_tab = "allow_debug" %} -{% include "_permissions_debug_tabs.html" %} -

    Use this tool to try out different actor and allow combinations. See Defining permissions with "allow" blocks for documentation.

    -
    -
    -
    -
    - - -
    -
    - - -
    -
    -
    - -
    -
    +
    +
    +

    + +
    +
    +

    + +
    +
    + +
    +
    - {% if error %}

    {{ error }}

    {% endif %} +{% if error %}

    {{ error }}

    {% endif %} - {% if result == "True" %}

    Result: allow

    {% endif %} +{% if result == "True" %}

    Result: allow

    {% endif %} - {% if result == "False" %}

    Result: deny

    {% endif %} -
    +{% if result == "False" %}

    Result: deny

    {% endif %} {% endblock %} diff --git a/datasette/templates/api_explorer.html b/datasette/templates/api_explorer.html index 4927cb8d..109fb1e9 100644 --- a/datasette/templates/api_explorer.html +++ b/datasette/templates/api_explorer.html @@ -3,7 +3,7 @@ {% block title %}API Explorer{% endblock %} {% block extra_head %} - + {% endblock %} {% block content %} @@ -19,7 +19,7 @@

    GET -
    +
    @@ -29,7 +29,7 @@
    POST - +
    diff --git a/datasette/templates/base.html b/datasette/templates/base.html index 18288439..7da0484e 100644 --- a/datasette/templates/base.html +++ b/datasette/templates/base.html @@ -1,14 +1,15 @@ {% import "_crumbs.html" as crumbs with context %} - + {% block title %}{% endblock %} - + + {% for url in extra_css_urls %} {% endfor %} - + {% for url in extra_js_urls %} {% endfor %} @@ -19,8 +20,8 @@
    {% endif %} {% if actor %}
    {{ display_actor(actor) }} @@ -70,7 +73,5 @@ {% endfor %} {% if select_templates %}{% endif %} - - diff --git a/datasette/templates/create_token.html b/datasette/templates/create_token.html index 270d9c86..2be98d38 100644 --- a/datasette/templates/create_token.html +++ b/datasette/templates/create_token.html @@ -39,7 +39,7 @@ {% endfor %} {% endif %} -
    +
    +
    Restrict actions that can be performed using this token

    All databases and tables

      - {% for permission in all_actions %} + {% for permission in all_permissions %}
    • {% endfor %}
    @@ -64,7 +65,7 @@ {% for database in database_with_tables %}

    All tables in "{{ database.name }}"

      - {% for permission in database_actions %} + {% for permission in database_permissions %}
    • {% endfor %}
    @@ -74,7 +75,7 @@ {% for table in database.tables %}

    {{ database.name }}: {{ table.name }}

      - {% for permission in child_actions %} + {% for permission in resource_permissions %}
    • {% endfor %}
    diff --git a/datasette/templates/csrf_error.html b/datasette/templates/csrf_error.html deleted file mode 100644 index b84749d3..00000000 --- a/datasette/templates/csrf_error.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends "base.html" %} -{% block title %}CSRF check failed{% endblock %} -{% block content %} -

    Form origin check failed

    - -

    Your request's origin could not be validated. Please return to the form and submit it again.

    - -
    Technical details -

    Developers: consult Datasette's CSRF protection documentation.

    -

    Reason: {{ reason }}

    -
    - -{% endblock %} diff --git a/datasette/templates/database.html b/datasette/templates/database.html index a9fad8dd..3d4dae07 100644 --- a/datasette/templates/database.html +++ b/datasette/templates/database.html @@ -5,38 +5,41 @@ {% block extra_head %} {{- super() -}} {% include "_codemirror.html" %} -{% include "_sql_parameter_styles.html" %} -{% if database_page_data.createTable %} - - -{% endif %} {% endblock %} {% block body_class %}db db-{{ database|to_css_class }}{% endblock %} -{% block crumbs %} -{{ crumbs.nav(request=request, database=database) }} -{% endblock %} - {% block content %} -{% set action_links, action_title = database_actions(), "Database actions" %} -{% include "_action_menu.html" %} - -{{ top_database() }} + {% set links = database_actions() %}{% if links %} + {% endif %} +
    {% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %} {% if allow_execute_sql %} - +

    Custom SQL query

    -

    - {% set parameter_names = [] %} - {% set parameter_values = {} %} - {% set sql_parameters_allow_expand = false %} - {% include "_sql_parameters.html" %} +

    @@ -49,7 +52,7 @@

    The following databases are attached to this connection, and can be used for cross-database joins:

      {% for db_name in attached_databases %} -
    • {{ db_name }} - tables
    • +
    • {{ db_name }} - tables
    • {% endfor %}
    @@ -62,13 +65,10 @@
  • {{ query.title or query.name }}{% if query.private %} 🔒{% endif %}
  • {% endfor %}
- {% if queries_more %} -

View {{ "{:,}".format(queries_count) }} quer{% if queries_count == 1 %}y{% else %}ies{% endif %}

- {% endif %} {% endif %} {% if tables %} -

Tables schema

+

Tables

{% endif %} {% for table in tables %} @@ -76,7 +76,7 @@

{{ table.name }}{% if table.private %} 🔒{% endif %}{% if table.hidden %} (hidden){% endif %}

{% for column in table.columns %}{{ column }}{% if not loop.last %}, {% endif %}{% endfor %}

-

{% if table.count is none %}Many rows{% elif table.count_truncated %}>{{ "{:,}".format(table.count - 1) }} rows{% else %}{{ "{:,}".format(table.count) }} row{% if table.count == 1 %}{% else %}s{% endif %}{% endif %}

+

{% if table.count is none %}Many rows{% else %}{{ "{:,}".format(table.count) }} row{% if table.count == 1 %}{% else %}s{% endif %}{% endif %}

{% endif %} {% endfor %} @@ -95,15 +95,9 @@ {% endif %} {% if allow_download %} -

Download SQLite DB: {{ database }}.db {{ format_bytes(size) }}

+

Download SQLite DB: {{ database }}.db {{ format_bytes(size) }}

{% endif %} {% include "_codemirror_foot.html" %} -{% include "_sql_parameter_scripts.html" %} - {% endblock %} diff --git a/datasette/templates/debug_actions.html b/datasette/templates/debug_actions.html deleted file mode 100644 index c9dccaaa..00000000 --- a/datasette/templates/debug_actions.html +++ /dev/null @@ -1,43 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Registered Actions{% endblock %} - -{% block content %} -

Registered actions

- -{% set current_tab = "actions" %} -{% include "_permissions_debug_tabs.html" %} - -

- This Datasette instance has registered {{ data.actions|length }} action{{ data.actions|length != 1 and "s" or "" }}. - Actions are used by the permission system to control access to different features. -

- - - - - - - - - - - - - - - {% for action in data.actions %} - - - - - - - - - - {% endfor %} - -
NameAbbrDescriptionResourceTakes ParentTakes ChildAlso Requires
{{ action.name }}{% if action.abbr %}{{ action.abbr }}{% endif %}{{ action.description or "" }}{% if action.resource_class %}{{ action.resource_class }}{% endif %}{% if action.takes_parent %}✓{% endif %}{% if action.takes_child %}✓{% endif %}{% if action.also_requires %}{{ action.also_requires }}{% endif %}
- -{% endblock %} diff --git a/datasette/templates/debug_allowed.html b/datasette/templates/debug_allowed.html deleted file mode 100644 index 80249d9c..00000000 --- a/datasette/templates/debug_allowed.html +++ /dev/null @@ -1,229 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Allowed Resources{% endblock %} - -{% block extra_head %} - -{% include "_permission_ui_styles.html" %} -{% include "_debug_common_functions.html" %} -{% endblock %} - -{% block content %} -

Allowed resources

- -{% set current_tab = "allowed" %} -{% include "_permissions_debug_tabs.html" %} - -

Use this tool to check which resources the current actor is allowed to access for a given permission action. It queries the /-/allowed.json API endpoint.

- -{% if request.actor %} -

Current actor: {{ request.actor.get("id", "anonymous") }}

-{% else %} -

Current actor: anonymous (not logged in)

-{% endif %} - -
- -
- - - Only certain actions are supported by this endpoint -
- -
- - - Filter results to a specific parent resource -
- -
- - - Filter results to a specific child resource (requires parent to be set) -
- -
- - - Number of results per page (max 200) -
- -
- -
- -
- - - - - -{% endblock %} diff --git a/datasette/templates/debug_autocomplete.html b/datasette/templates/debug_autocomplete.html deleted file mode 100644 index 380639a3..00000000 --- a/datasette/templates/debug_autocomplete.html +++ /dev/null @@ -1,78 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Debug autocomplete{% endblock %} - -{% block extra_head %} -{{ super() }} - -{% endblock %} - -{% block content %} -

Debug autocomplete

- -
-

- - -

-

- - -

-

-
- -{% if error %} -

{{ error }}

-{% elif autocomplete_url %} -

{{ database_name }} / {{ table_name }}

- {% if label_column %} -

Label column: {{ label_column }}

- {% else %} -

No label column detected. Results will use primary key values.

- {% endif %} -
- - - - -
-

Selected row

-
No row selected.
- -{% else %} -

Suggested tables

- {% if suggestions %} -

Showing up to five tables with a detected label column.

- - - - - - - - - - {% for suggestion in suggestions %} - - - - - - {% endfor %} - -
DatabaseTableLabel column
{{ suggestion.database }}{{ suggestion.table }}{{ suggestion.label_column }}
- {% else %} -

No tables with detected label columns found.

- {% endif %} -

Scanned {{ scanned }} table{% if scanned != 1 %}s{% endif %}{% if reached_scan_limit %}; stopped at the 100 table scan limit{% endif %}.

-{% endif %} - -{% endblock %} diff --git a/datasette/templates/debug_check.html b/datasette/templates/debug_check.html deleted file mode 100644 index b9fc636a..00000000 --- a/datasette/templates/debug_check.html +++ /dev/null @@ -1,318 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Explain a permission decision{% endblock %} - -{% block extra_head %} - -{% include "_permission_ui_styles.html" %} -{% include "_debug_common_functions.html" %} - -{% endblock %} - -{% block content %} -

Explain a permission decision

- -{% set current_tab = "check" %} -{% include "_permissions_debug_tabs.html" %} - -

Test an actor, action and resource. The result explains which rules matched, which specificity level won, and whether actor restrictions or required actions changed the verdict.

- -
-
-
- - - Use null for an anonymous actor. This actor is simulated; it does not change who you are signed in as. -
- -
- - - The operation to evaluate -
- -
- - - The database or other parent resource -
- -
- - - The table, query or other child resource -
- -
- -
-
-
- - - - -{% endblock %} diff --git a/datasette/templates/debug_permissions_playground.html b/datasette/templates/debug_permissions_playground.html deleted file mode 100644 index 8b0cbbcf..00000000 --- a/datasette/templates/debug_permissions_playground.html +++ /dev/null @@ -1,150 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Permission activity{% endblock %} - -{% block extra_head %} -{% include "_permission_ui_styles.html" %} - -{% endblock %} - -{% block content %} -

Permission activity

- -{% set current_tab = "permissions" %} -{% include "_permissions_debug_tabs.html" %} - -

Raw simulator

- -

This form runs a hypothetical permission check and returns its raw explanation JSON. Use the Explain tool for a visual explanation of the same decision.

- -
-
-
-
-
- - -
-
-
-
- - -
-
- - -
-
- - -
-
-
-
- -
-

-    
-
- - - -

Recent permission checks

- -

- {% if filter != "all" %}All{% else %}All{% endif %}, - {% if filter != "exclude-yours" %}Exclude yours{% else %}Exclude yours{% endif %}, - {% if filter != "only-yours" %}Only yours{% else %}Only yours{% endif %} -

- -{% if permission_checks %} - - - - - - - - - - - - - {% for check in permission_checks %} - - - - - - - - - {% endfor %} - -
WhenActionParentChildActorResult
{{ check.when.split('T', 1)[0] }}
{{ check.when.split('T', 1)[1].split('+', 1)[0].split('-', 1)[0].split('Z', 1)[0] }}
{{ check.action }}{{ check.parent or '—' }}{{ check.child or '—' }}{% if check.actor %}{{ check.actor|tojson }}{% else %}anonymous{% endif %}{% if check.result %}Allowed{% elif check.result is none %}No opinion{% else %}Denied{% endif %}
-{% else %} -

No permission checks have been recorded yet.

-{% endif %} - -{% endblock %} diff --git a/datasette/templates/debug_rules.html b/datasette/templates/debug_rules.html deleted file mode 100644 index 233c0e94..00000000 --- a/datasette/templates/debug_rules.html +++ /dev/null @@ -1,203 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Permission Rules{% endblock %} - -{% block extra_head %} - -{% include "_permission_ui_styles.html" %} -{% include "_debug_common_functions.html" %} -{% endblock %} - -{% block content %} -

Permission rules

- -{% set current_tab = "rules" %} -{% include "_permissions_debug_tabs.html" %} - -

Use this tool to view the permission rules that allow the current actor to access resources for a given permission action. It queries the /-/rules.json API endpoint.

- -{% if request.actor %} -

Current actor: {{ request.actor.get("id", "anonymous") }}

-{% else %} -

Current actor: anonymous (not logged in)

-{% endif %} - -
-
-
- - - The permission action to check -
- -
- - - Number of results per page (max 200) -
- -
- -
-
-
- - - - - -{% endblock %} diff --git a/datasette/templates/execute_write.html b/datasette/templates/execute_write.html deleted file mode 100644 index 592577f8..00000000 --- a/datasette/templates/execute_write.html +++ /dev/null @@ -1,337 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Write to this database{% endblock %} - -{% block extra_head %} -{{- super() -}} -{% include "_codemirror.html" %} - -{% include "_execute_write_analysis_styles.html" %} -{% include "_sql_parameter_styles.html" %} -{% endblock %} - -{% block body_class %}execute-write db-{{ database|to_css_class }}{% endblock %} - -{% block crumbs %} -{{ crumbs.nav(request=request, database=database) }} -{% endblock %} - -{% block content %} - -

Write to this database

- -

Execute SQL to insert, update or delete rows in this database.

- -{% if execution_message %} -

{{ execution_message }}{% for link in execution_links %} {{ link.label }}{% endfor %}

-{% endif %} - -{% if execute_write_returns_rows %} -

Returned rows

- {% if execute_write_truncated %} -

Only the first {{ "{:,}".format(execute_write_display_rows|length) }} returned rows are shown.

- {% endif %} - {% set columns = execute_write_columns %} - {% set display_rows = execute_write_display_rows %} - {% set show_zero_results = true %} - {% include "_query_results.html" %} -{% endif %} - -
- {% if write_create_table_template_sql or write_template_tables %} -
-
- Start with a template -

- {% if write_create_table_template_sql %} - - {% endif %} - {% if write_template_tables %} - - - {% for operation in write_template_operations %} - - {% endfor %} - {% endif %} -

-
-
- {% else %} -

There are no tables that you can currently edit.

- {% endif %} - -

- - {% set sql_parameters_section_id = "execute-write-parameters-section" %} - {% set sql_parameters_allow_expand = true %} - {% include "_sql_parameters.html" %} - -
-

Query operations

- {% if analysis_error %} -

{{ analysis_error }}

- {% elif analysis_rows %} -
- - - - - - - - - - - {% for row in analysis_rows %} - - - - - - - - {% endfor %} - -
OperationDatabaseTableRequired permissionAllowed
{{ row.operation }}{{ row.database }}{{ row.table }}{% if row.required_permission %}{{ row.required_permission }}{% endif %}{% if row.allowed is none %}{% elif row.allowed %}yes{% else %}no{% endif %}
- {% else %} -

Analysis will show each affected table and required permission.

- {% endif %} -
- -

- - {{ execute_disabled_reason or "" }} - {% if save_query_url %}Save this query{% endif %} -

-
- -{% include "_codemirror_foot.html" %} -{% include "_sql_parameter_scripts.html" %} -{% include "_execute_write_analysis_scripts.html" %} - - - -{% if write_create_table_template_sql or write_template_tables %} - -{% endif %} - -{% endblock %} diff --git a/datasette/templates/index.html b/datasette/templates/index.html index 03349279..06e09635 100644 --- a/datasette/templates/index.html +++ b/datasette/templates/index.html @@ -2,26 +2,17 @@ {% block title %}{{ metadata.title or "Datasette" }}: {% for database in databases %}{{ database.name }}{% if not loop.last %}, {% endif %}{% endfor %}{% endblock %} -{% block extra_head %} -{% if noindex %}{% endif %} -{% endblock %} - {% block body_class %}index{% endblock %} {% block content %}

{{ metadata.title or "Datasette" }}{% if private %} 🔒{% endif %}

-{% set action_links, action_title = homepage_actions, "Homepage actions" %} -{% include "_action_menu.html" %} - -{{ top_homepage() }} - {% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %} {% for database in databases %}

{{ database.name }}{% if database.private %} 🔒{% endif %}

- {% if database.show_table_row_counts %}{{ "{:,}".format(database.table_rows_sum) }} rows in {% endif %}{{ database.tables_count }} table{% if database.tables_count != 1 %}s{% endif %}{% if database.hidden_tables_count %}, {% endif -%} + {% if database.show_table_row_counts %}{{ "{:,}".format(database.table_rows_sum) }} rows in {% endif %}{{ database.tables_count }} table{% if database.tables_count != 1 %}s{% endif %}{% if database.tables_count and database.hidden_tables_count %}, {% endif -%} {% if database.hidden_tables_count -%} {% if database.show_table_row_counts %}{{ "{:,}".format(database.hidden_table_rows_sum) }} rows in {% endif %}{{ database.hidden_tables_count }} hidden table{% if database.hidden_tables_count != 1 %}s{% endif -%} {% endif -%} diff --git a/datasette/templates/logout.html b/datasette/templates/logout.html index a99870e6..4c4a7d11 100644 --- a/datasette/templates/logout.html +++ b/datasette/templates/logout.html @@ -8,8 +8,9 @@

You are logged in as {{ display_actor(actor) }}

-
+
+
diff --git a/datasette/templates/messages_debug.html b/datasette/templates/messages_debug.html index 891cf915..e0ab9a40 100644 --- a/datasette/templates/messages_debug.html +++ b/datasette/templates/messages_debug.html @@ -8,7 +8,7 @@

Set a message:

-
+
@@ -19,6 +19,7 @@
+
diff --git a/datasette/templates/patterns.html b/datasette/templates/patterns.html index 075c0117..9905df2c 100644 --- a/datasette/templates/patterns.html +++ b/datasette/templates/patterns.html @@ -1,17 +1,17 @@ - + Datasette: Pattern Portfolio - + -