diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 232a34c7..fb390a44 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
+ python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"]
steps:
- uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }}
@@ -62,7 +62,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v6
with:
- python-version: '3.10'
+ python-version: '3.11'
cache: pip
cache-dependency-path: pyproject.toml
- name: Install dependencies
diff --git a/.github/workflows/test-pyodide.yml b/.github/workflows/test-pyodide.yml
index 449855f3..35dc8542 100644
--- a/.github/workflows/test-pyodide.yml
+++ b/.github/workflows/test-pyodide.yml
@@ -19,10 +19,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- - name: Set up Python 3.10
+ - name: Set up Python 3.11
uses: actions/setup-python@v6
with:
- python-version: "3.10"
+ python-version: "3.11"
cache: 'pip'
cache-dependency-path: '**/pyproject.toml'
- name: Cache Playwright browsers
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 8176a630..6dd07b19 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -20,7 +20,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"]
+ python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"]
include:
- python-version: "3.14"
coverage: true
diff --git a/README.md b/README.md
index 1f79778f..6d7b7276 100644
--- a/README.md
+++ b/README.md
@@ -36,7 +36,7 @@ You can also install it using `pip` or `pipx`:
pip install datasette
-Datasette requires Python 3.10 or higher. We also have [detailed installation instructions](https://docs.datasette.io/en/stable/installation.html) covering other options such as Docker.
+Datasette requires Python 3.11 or higher. We also have [detailed installation instructions](https://docs.datasette.io/en/stable/installation.html) covering other options such as Docker.
## Basic usage
diff --git a/datasette/app.py b/datasette/app.py
index 001ce9b3..b2cb24b7 100644
--- a/datasette/app.py
+++ b/datasette/app.py
@@ -2212,7 +2212,7 @@ ORDER BY allowed.parent, allowed.child
# Log every check (including cache hits) for the debug page,
# dependencies before the actions that required them
- when = datetime.datetime.now(datetime.timezone.utc).isoformat()
+ when = datetime.datetime.now(datetime.UTC).isoformat()
for name in reversed(expanded):
self._permission_checks.append(
PermissionCheck(
diff --git a/datasette/background_tasks.py b/datasette/background_tasks.py
index 6b34fd85..6777a163 100644
--- a/datasette/background_tasks.py
+++ b/datasette/background_tasks.py
@@ -36,7 +36,7 @@ logger = logging.getLogger("datasette.background_tasks")
def _utcnow_iso() -> str:
- return datetime.datetime.now(datetime.timezone.utc).isoformat()
+ return datetime.datetime.now(datetime.UTC).isoformat()
def _function_path(func: Callable) -> str:
diff --git a/datasette/default_magic_parameters.py b/datasette/default_magic_parameters.py
index bff5f0a7..239637e7 100644
--- a/datasette/default_magic_parameters.py
+++ b/datasette/default_magic_parameters.py
@@ -25,12 +25,9 @@ def now(key, request):
if key == "epoch":
return int(time.time())
elif key == "date_utc":
- return datetime.datetime.now(datetime.timezone.utc).date().isoformat()
+ return datetime.datetime.now(datetime.UTC).date().isoformat()
elif key == "datetime_utc":
- return (
- datetime.datetime.now(datetime.timezone.utc).strftime(r"%Y-%m-%dT%H:%M:%S")
- + "Z"
- )
+ return datetime.datetime.now(datetime.UTC).strftime(r"%Y-%m-%dT%H:%M:%S") + "Z"
else:
raise KeyError
diff --git a/datasette/events.py b/datasette/events.py
index 5f3fd06e..8746d7ac 100644
--- a/datasette/events.py
+++ b/datasette/events.py
@@ -1,6 +1,6 @@
from abc import ABC, abstractproperty
from dataclasses import asdict, dataclass, field
-from datetime import datetime, timezone
+from datetime import UTC, datetime
from datasette.hookspecs import hookimpl
@@ -11,9 +11,7 @@ class Event(ABC):
def name(self):
pass
- created: datetime = field(
- init=False, default_factory=lambda: datetime.now(timezone.utc)
- )
+ created: datetime = field(init=False, default_factory=lambda: datetime.now(UTC))
actor: dict | None
def properties(self):
diff --git a/datasette/utils/sql_analysis.py b/datasette/utils/sql_analysis.py
index 6325e890..5b636404 100644
--- a/datasette/utils/sql_analysis.py
+++ b/datasette/utils/sql_analysis.py
@@ -1,4 +1,3 @@
-import sys
from dataclasses import dataclass
from typing import Literal
@@ -193,20 +192,6 @@ _AUTHORIZER_ACTION_NAMES = {
}
-def _allow_authorizer_action(*args):
- return sqlite3.SQLITE_OK
-
-
-def _disable_authorizer(conn):
- # Python 3.11 added support for unregistering an authorizer using None.
- # On Python 3.10, None is installed as the callback instead, and the next
- # statement fails with "not authorized" when sqlite3 tries to call it.
- if sys.version_info >= (3, 11):
- conn.set_authorizer(None)
- else:
- conn.set_authorizer(_allow_authorizer_action)
-
-
def analyze_sql_tables(
conn,
sql: str,
@@ -451,9 +436,7 @@ def analyze_sql_tables(
explain_rows = conn.execute(
"EXPLAIN " + sql, params if params is not None else {}
).fetchall()
- # Passing None before these lookups leaves a failing callback installed
- # on Python 3.10, so use a permissive callback until they are complete.
- conn.set_authorizer(_allow_authorizer_action)
+ conn.set_authorizer(None)
if not operations:
vacuum_row = next((row for row in explain_rows if row[1] == "Vacuum"), None)
@@ -495,7 +478,7 @@ def analyze_sql_tables(
conn, key.table, schema=key.sqlite_schema
)
finally:
- _disable_authorizer(conn)
+ conn.set_authorizer(None)
has_schema_operation = any(
key.target_type in {"table", "index", "view", "trigger", "virtual-table"}
diff --git a/docs/contributing.rst b/docs/contributing.rst
index 2de1fce0..bbd47865 100644
--- a/docs/contributing.rst
+++ b/docs/contributing.rst
@@ -20,7 +20,7 @@ General guidelines
Setting up a development environment
------------------------------------
-If you have Python 3.10 or higher installed on your computer (on OS X the quickest way to do this `is using homebrew `__) you can install an editable copy of Datasette using the following steps.
+If you have Python 3.11 or higher installed on your computer (on OS X the quickest way to do this `is using homebrew `__) you can install an editable copy of Datasette using the following steps.
If you want to use GitHub to publish your changes, first `create a fork of datasette `__ under your own GitHub account.
diff --git a/docs/installation.rst b/docs/installation.rst
index ceec7f23..52e72c0f 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -47,7 +47,7 @@ If the latest packaged release of Datasette has not yet been made available thro
Using pip
---------
-Datasette requires Python 3.10 or higher. The `Python.org Python For Beginners `__ page has instructions for getting started.
+Datasette requires Python 3.11 or higher. The `Python.org Python For Beginners `__ page has instructions for getting started.
You can install Datasette and its dependencies using ``pip``::
@@ -274,7 +274,7 @@ Use ``which datasette`` to confirm that ``datasette`` will run that version. The
/usr/local/opt/datasette/bin/datasette
-If you get a different location here such as ``/Library/Frameworks/Python.framework/Versions/3.10/bin/datasette`` you can run the following command to cause ``datasette`` to execute the Homebrew version instead::
+If you get a different location here such as ``/Library/Frameworks/Python.framework/Versions/3.11/bin/datasette`` you can run the following command to cause ``datasette`` to execute the Homebrew version instead::
alias datasette=$(echo $(brew --prefix datasette)/bin/datasette)
diff --git a/pyproject.toml b/pyproject.toml
index cd8b5513..271d695b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,7 +7,7 @@ authors = [
{ name = "Simon Willison" },
]
license = "Apache-2.0"
-requires-python = ">=3.10"
+requires-python = ">=3.11"
classifiers = [
"Development Status :: 4 - Beta",
"Framework :: Datasette",
@@ -15,11 +15,11 @@ classifiers = [
"Intended Audience :: Science/Research",
"Intended Audience :: End Users/Desktop",
"Topic :: Database",
- "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
+ "Programming Language :: Python :: 3.15",
]
dependencies = [
diff --git a/ruff.toml b/ruff.toml
index 3c4345bf..b9ee4612 100644
--- a/ruff.toml
+++ b/ruff.toml
@@ -1,7 +1,7 @@
line-length = 160
-target-version = "py310"
+target-version = "py311"
[lint.flake8-bugbear]
# from_extra() returns a dataclasses.field(), so it is safe as a dataclass
# default - ruff cannot see through the wrapper (RUF009)
-extend-immutable-calls = ["datasette.views.from_extra"]
\ No newline at end of file
+extend-immutable-calls = ["datasette.views.from_extra"]
diff --git a/tests/test_plugins.py b/tests/test_plugins.py
index 1084d270..58e154a2 100644
--- a/tests/test_plugins.py
+++ b/tests/test_plugins.py
@@ -1727,7 +1727,7 @@ async def test_hook_track_event():
# Should have a recent created as well
created = event.created
assert isinstance(created, datetime.datetime)
- assert created.tzinfo == datetime.timezone.utc
+ assert created.tzinfo == datetime.UTC
@pytest.mark.asyncio
diff --git a/tests/test_utils_sql_analysis.py b/tests/test_utils_sql_analysis.py
index 363814d7..4201fb0d 100644
--- a/tests/test_utils_sql_analysis.py
+++ b/tests/test_utils_sql_analysis.py
@@ -455,5 +455,4 @@ def test_analyze_disables_authorizer_on_error():
with pytest.raises(sqlite3.OperationalError):
analyze_sql_tables(conn, "bad SQL")
- final_authorizer = conn.authorizers[-1]
- assert final_authorizer is None or final_authorizer() == sqlite3.SQLITE_OK
+ assert conn.authorizers[-1] is None
diff --git a/tests/test_write_wrapper.py b/tests/test_write_wrapper.py
index 45eea483..b65e54b2 100644
--- a/tests/test_write_wrapper.py
+++ b/tests/test_write_wrapper.py
@@ -523,7 +523,7 @@ async def test_write_wrapper_set_authorizer(datasette, actor, table, should_deny
try:
yield
finally:
- conn.set_authorizer(lambda *args: sqlite3.SQLITE_OK)
+ conn.set_authorizer(None)
return wrapper