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 b0640ae8..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@v6
+ 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
index b8fb8aaa..a54bd83a 100644
--- a/.github/workflows/documentation-links.yml
+++ b/.github/workflows/documentation-links.yml
@@ -1,6 +1,6 @@
name: Read the Docs Pull Request Preview
on:
- pull_request:
+ pull_request_target:
types:
- opened
diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml
deleted file mode 100644
index 5275ddef..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@v6
- - 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@v5
- 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@v5
- 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 735e14e9..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@v6
- - uses: actions/cache@v5
+ 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 87300593..bf67a115 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -12,18 +12,18 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v6
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
- cache-dependency-path: pyproject.toml
+ cache-dependency-path: setup.py
- name: Install dependencies
run: |
- pip install . --group dev
+ pip install -e '.[test]'
- name: Run tests
run: |
pytest
@@ -35,13 +35,13 @@ jobs:
permissions:
id-token: write
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v4
- name: Set up Python
- uses: actions/setup-python@v6
+ uses: actions/setup-python@v5
with:
python-version: '3.13'
cache: pip
- cache-dependency-path: pyproject.toml
+ cache-dependency-path: setup.py
- name: Install dependencies
run: |
pip install setuptools wheel build
@@ -56,16 +56,16 @@ jobs:
needs: [deploy]
if: "!github.event.release.prerelease"
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v4
- name: Set up Python
- uses: actions/setup-python@v6
+ uses: actions/setup-python@v5
with:
- python-version: '3.10'
+ python-version: '3.9'
cache: pip
- cache-dependency-path: pyproject.toml
+ cache-dependency-path: setup.py
- 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 +73,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 +91,7 @@ jobs:
needs: [deploy]
if: "!github.event.release.prerelease"
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v4
- 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 e622ef4c..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@v6
+ - 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 9a808194..907104b8 100644
--- a/.github/workflows/spellcheck.yml
+++ b/.github/workflows/spellcheck.yml
@@ -9,16 +9,16 @@ jobs:
spellcheck:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
+ - 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
diff --git a/.github/workflows/stable-docs.yml b/.github/workflows/stable-docs.yml
deleted file mode 100644
index 59b5fbc0..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@v6
- 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 c514048e..32654a93 100644
--- a/.github/workflows/test-coverage.yml
+++ b/.github/workflows/test-coverage.yml
@@ -15,17 +15,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out datasette
- uses: actions/checkout@v6
+ uses: actions/checkout@v4
- name: Set up Python
- uses: actions/setup-python@v6
+ uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
- cache-dependency-path: '**/pyproject.toml'
+ cache-dependency-path: '**/setup.py'
- 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: |-
diff --git a/.github/workflows/test-pyodide.yml b/.github/workflows/test-pyodide.yml
index 5162c47a..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@v6
+ - 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@v5
+ 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
index 23fce459..7882e05d 100644
--- a/.github/workflows/test-sqlite-support.yml
+++ b/.github/workflows/test-sqlite-support.yml
@@ -12,7 +12,7 @@ jobs:
strategy:
matrix:
platform: [ubuntu-latest]
- python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
+ python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12"]
sqlite-version: [
#"3", # latest version
"3.46",
@@ -25,14 +25,14 @@ jobs:
#"3.23.1" # 2018-04-10, before UPSERT
]
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v6
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
cache: pip
- cache-dependency-path: pyproject.toml
+ cache-dependency-path: setup.py
- name: Set up SQLite ${{ matrix.sqlite-version }}
uses: asg017/sqlite-versions@71ea0de37ae739c33e447af91ba71dda8fcf22e6
with:
@@ -45,7 +45,7 @@ jobs:
(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: |
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 9e47db6f..d6398d33 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -9,24 +9,23 @@ 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", "3.13"]
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v6
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
cache: pip
- cache-dependency-path: pyproject.toml
+ cache-dependency-path: setup.py
- 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 +33,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 a033cd92..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@v6
+ - 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 72af1eec..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@v6
+ - 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 5fcd9afd..172de444 100644
--- a/Justfile
+++ b/Justfile
@@ -5,72 +5,38 @@ 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
+ pipenv run codespell tests --ignore-words docs/codespell-ignore-words.txt
-# Run linters: black, ruff, cog
+# Run linters: black, flake8, mypy, cog
@lint: codespell
- uv run black datasette tests --check
- uv run ruff check datasette tests
- 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..662f2a11 100644
--- a/README.md
+++ b/README.md
@@ -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 eb18e59e..47d2b4f6 100644
--- a/datasette/__init__.py
+++ b/datasette/__init__.py
@@ -1,7 +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, TokenRestrictions # noqa
from datasette.utils.asgi import Forbidden, NotFound, Request, Response # noqa
from datasette.utils import actor_matches_allow # noqa
from datasette.views import Context # 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 9c9b7de4..d1d6c345 100644
--- a/datasette/app.py
+++ b/datasette/app.py
@@ -1,17 +1,13 @@
-from __future__ import annotations
-
+from asgi_csrf import Errors
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 +30,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.database import database_download, DatabaseView, TableCreateView, QueryView
from .views.index import IndexView
from .views.special import (
JsonDataView,
PatternPortfolioView,
- AutocompleteDebugView,
AuthTokenView,
ApiExplorerView,
CreateTokenView,
@@ -79,21 +49,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,7 +62,6 @@ from .url_builder import Urls
from .database import Database, QueryInterrupted
from .utils import (
- PaginatedResources,
PrefixedUrlString,
SPATIALITE_FUNCTIONS,
StartupError,
@@ -122,9 +81,7 @@ from .utils import (
parse_metadata,
resolve_env_secrets,
resolve_routes,
- sha256_file,
tilde_decode,
- tilde_encode,
to_css_class,
urlsafe_components,
redact_keys,
@@ -145,7 +102,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,
@@ -155,44 +111,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"),
@@ -297,24 +220,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"},
)
@@ -331,57 +242,6 @@ def _to_string(value):
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..034e9678 100644
--- a/datasette/templates/_facet_results.html
+++ b/datasette/templates/_facet_results.html
@@ -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 53a824f1..00000000
--- a/datasette/templates/_permission_ui_styles.html
+++ /dev/null
@@ -1,145 +0,0 @@
-
diff --git a/datasette/templates/_permissions_debug_tabs.html b/datasette/templates/_permissions_debug_tabs.html
deleted file mode 100644
index d7203c1e..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 %}| {{ column }} | {% endfor %}
-
-
-
- {% for row in display_rows %}
-
- {% for column, td in zip(columns, row) %}
- | {{ td }} |
- {% endfor %}
-
- {% endfor %}
-
-
-{% 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..a1329ba7 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 %}
- |
+ |
{% if not column.sortable %}
{{ column.name }}
{% else %}
@@ -22,7 +22,7 @@
|
{% for row in display_rows %}
-
+
{% for cell in row %}
| {{ cell.value }} |
{% endfor %}
@@ -31,7 +31,6 @@
-{% endif %}
-{% if not display_rows %}
+{% else %}
0 records
{% endif %}
diff --git a/datasette/templates/allow_debug.html b/datasette/templates/allow_debug.html
index 1ecc92df..610417d2 100644
--- a/datasette/templates/allow_debug.html
+++ b/datasette/templates/allow_debug.html
@@ -33,9 +33,6 @@ 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.
GET
-