# pypi_packages

name: datasette-debug-actors-from-ids
summary: Datasette plugin for trying out the actors_from_ids hook
classifiers: ["Framework :: Datasette"]
author: Simon Willison
description_content_type: text/markdown
dynamic: ["License-File"]
license_expression: Apache-2.0
license_files: ["LICENSE"]
package_url: https://pypi.org/project/datasette-debug-actors-from-ids/
project_url: https://pypi.org/project/datasette-debug-actors-from-ids/
provides_extra: ["test"]
release_url: https://pypi.org/project/datasette-debug-actors-from-ids/0.1a3/
version: 0.1a3
description:
  # datasette-debug-actors-from-ids

  [![PyPI](https://img.shields.io/pypi/v/datasette-debug-actors-from-ids.svg)](https://pypi.org/project/datasette-debug-actors-from-ids/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-debug-actors-from-ids?include_prereleases&label=changelog)](https://github.com/datasette/datasette-debug-actors-from-ids/releases)
  [![Tests](https://github.com/datasette/datasette-debug-actors-from-ids/workflows/Test/badge.svg)](https://github.com/datasette/datasette-debug-actors-from-ids/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-debug-actors-from-ids/blob/main/LICENSE)

  A Datasette plugin for debugging the [new actors_from_ids plugin hook](https://github.com/simonw/datasette/issues/2180).

  ## Installation

  ```bash
  datasette install datasette-debug-actors-from-ids
  ```

  ## Usage

  Adds a URL at `/-/debug-actors-from-ids`. Call it with `?ids=1,3,4` to exercise the plugin hook.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:
  ```bash
  cd datasette-debug-actors-from-ids
  python3 -m venv venv
  source venv/bin/activate
  ```
  Now install the dependencies and test dependencies:
  ```
  pip install -e '.[test]'
  ```
  ```
  To run the tests:
  ```bash
  pytest
  ```

project_urls:
  {"CI": "https://github.com/datasette/datasette-debug-actors-from-ids/actions", "Changelog": "https://github.com/datasette/datasette-debug-actors-from-ids/releases", "Homepage": "https://github.com/datasette/datasette-debug-actors-from-ids", "Issues": "https://github.com/datasette/datasette-debug-actors-from-ids/issues"}

requires_dist:
  ["datasette>=1.0a21", "pytest; extra == \"test\"", "pytest-httpx; extra == \"test\"", "pytest-asyncio; extra == \"test\""]


name: datasette-debug-asgi
summary: Datasette plugin for dumping out the ASGI scope
classifiers: []
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-debug-asgi
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-debug-asgi/
project_url: https://pypi.org/project/datasette-debug-asgi/
release_url: https://pypi.org/project/datasette-debug-asgi/1.1/
requires_dist: ["datasette (>=0.50)", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'"]
requires_python: >=3.6
version: 1.1
description:
  # datasette-debug-asgi

  [![PyPI](https://img.shields.io/pypi/v/datasette-debug-asgi.svg)](https://pypi.org/project/datasette-debug-asgi/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-debug-asgi?include_prereleases&label=changelog)](https://github.com/simonw/datasette-debug-asgi/releases)
  [![Tests](https://github.com/simonw/datasette-debug-asgi/workflows/Test/badge.svg)](https://github.com/simonw/datasette-debug-asgi/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-debug-asgi/blob/main/LICENSE)

  Datasette plugin for dumping out the ASGI scope.

  Adds a new URL at `/-/asgi-scope` which shows the current ASGI scope. Demo here: https://datasette.io/-/asgi-scope

  ## Installation

      pip install datasette-debug-asgi

  ## Usage

  Visit `/-/asgi-scope` to see debug output showing the ASGI scope.

  You can add query string parameters such as `/-/asgi-scope?q=hello`.

  You can also add extra path components such as `/-/asgi-scope/more/path/here`.

project_urls:
  {"CI": "https://github.com/simonw/datasette-debug-asgi/actions", "Changelog": "https://github.com/simonw/datasette-debug-asgi/releases", "Homepage": "https://github.com/simonw/datasette-debug-asgi", "Issues": "https://github.com/simonw/datasette-debug-asgi/issues"}


name: datasette-debug-events
summary: Print Datasette events to standard error
classifiers: ["Framework :: Datasette"]
author: Simon Willison
description_content_type: text/markdown
dynamic: ["License-File"]
license_expression: Apache-2.0
license_files: ["LICENSE"]
package_url: https://pypi.org/project/datasette-debug-events/
project_url: https://pypi.org/project/datasette-debug-events/
provides_extra: ["test"]
release_url: https://pypi.org/project/datasette-debug-events/0.1a2/
requires_dist: ["datasette>=1.0a21", "pytest; extra == \"test\"", "pytest-asyncio; extra == \"test\""]
requires_python: >=3.10
version: 0.1a2
description:
  # datasette-debug-events

  [![PyPI](https://img.shields.io/pypi/v/datasette-debug-events.svg)](https://pypi.org/project/datasette-debug-events/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-debug-events?include_prereleases&label=changelog)](https://github.com/datasette/datasette-debug-events/releases)
  [![Tests](https://github.com/datasette/datasette-debug-events/actions/workflows/test.yml/badge.svg)](https://github.com/datasette/datasette-debug-events/actions/workflows/test.yml)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-debug-events/blob/main/LICENSE)

  Print Datasette events to standard error

  ## Installation

  Install this plugin in the same environment as Datasette.
  ```bash
  datasette install datasette-debug-events
  ```
  ## Usage

  Once installed, any events fired by the [Datasette events mechanism](https://docs.datasette.io/en/latest/events.html) (introduced [in Datasette 1.0a8](https://simonwillison.net/2024/Feb/7/datasette-1a8/#datasette-events)) will be sent to standard error and should display in your terminal.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:
  ```bash
  cd datasette-debug-events
  python -m venv venv
  source venv/bin/activate
  ```
  Now install the dependencies and test dependencies:
  ```bash
  pip install -e '.[test]'
  ```
  To run the tests:
  ```bash
  python -m pytest
  ```

project_urls:
  {"CI": "https://github.com/datasette/datasette-debug-events/actions", "Changelog": "https://github.com/datasette/datasette-debug-events/releases", "Homepage": "https://github.com/datasette/datasette-debug-events", "Issues": "https://github.com/datasette/datasette-debug-events/issues"}


name: datasette-debug-permissions
summary: A Datasette plugin that outputs debug information about permission checks
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Simon Willison
description_content_type: text/markdown
license: Apache-2.0
package_url: https://pypi.org/project/datasette-debug-permissions/
project_url: https://pypi.org/project/datasette-debug-permissions/
release_url: https://pypi.org/project/datasette-debug-permissions/0.1/
requires_dist: ["datasette", "pytest ; extra == 'test'"]
version: 0.1
description:
  # datasette-debug-permissions

  [![PyPI](https://img.shields.io/pypi/v/datasette-debug-permissions.svg)](https://pypi.org/project/datasette-debug-permissions/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-debug-permissions?include_prereleases&label=changelog)](https://github.com/datasette/datasette-debug-permissions/releases)
  [![Tests](https://github.com/datasette/datasette-debug-permissions/workflows/Test/badge.svg)](https://github.com/datasette/datasette-debug-permissions/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-debug-permissions/blob/main/LICENSE)

  A Datasette plugin that outputs debug information about permission checks.

  ## Installation

  ```bash
  datasette install datasette-debug-permissions
  ```

  ## Usage

  This plugin adds debugging output to standard error as Datasette is running showing any times the `permission_allowed()` plugin hook is called.

  See [Authentication and permissions](https://docs.datasette.io/en/stable/authentication.html) in the Datasette documentation for more on why this is useful.

  ## Example output

  ```
  INFO:     Uvicorn running on http://127.0.0.1:8833 (Press CTRL+C to quit)
  permission_allowed: action=view-instance, resource=<None>, actor=<None>
  permission_allowed: action=view-database, resource=_internal, actor=<None>
  permission_allowed: action=view-database, resource=mydatabase, actor=<None>
  permission_allowed: action=view-database, resource=mydatabase, actor=<None>
  permission_allowed: action=view-table, resource=('mydatabase', 'mytable'), actor=<None>
  permission_allowed: action=view-table, resource=('mydatabase', 'mytable'), actor=<None>
  permission_allowed: action=view-instance, resource=<None>, actor=<None>
  permission_allowed: action=view-instance, resource=<None>, actor=<None>
  permission_allowed: action=debug-menu, resource=<None>, actor=<None>
  ```

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:
  ```bash
  cd datasette-debug-permissions
  python3 -m venv venv
  source venv/bin/activate
  ```
  Now install the dependencies and test dependencies:
  ```
  pip install -e '.[test]'
  ```
  To install the JavaScript build dependencies, run this:
  ```bash
  npm install
  ```
  You can use the [Just](https://github.com/casey/just) command runner to build the TypeScript to minified JavaScript like this:
  ```bash
  just js
  ```
  To run the tests:
  ```bash
  pytest
  ```

project_urls:
  {"CI": "https://github.com/datasette/datasette-debug-permissions/actions", "Changelog": "https://github.com/datasette/datasette-debug-permissions/releases", "Homepage": "https://github.com/datasette/datasette-debug-permissions", "Issues": "https://github.com/datasette/datasette-debug-permissions/issues"}


name: datasette-demo-dbs
summary: Fetch missing demo DBs from URLs when Datasette first starts
classifiers: ["Framework :: Datasette"]
author: Simon Willison
description_content_type: text/markdown
dynamic: ["License-File"]
license_expression: Apache-2.0
license_files: ["LICENSE"]
package_url: https://pypi.org/project/datasette-demo-dbs/
project_url: https://pypi.org/project/datasette-demo-dbs/
provides_extra: ["test"]
release_url: https://pypi.org/project/datasette-demo-dbs/0.1.1/
requires_python: >=3.9
version: 0.1.1
description:
  # datasette-demo-dbs

  [![PyPI](https://img.shields.io/pypi/v/datasette-demo-dbs.svg)](https://pypi.org/project/datasette-demo-dbs/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-demo-dbs?include_prereleases&label=changelog)](https://github.com/datasette/datasette-demo-dbs/releases)
  [![Tests](https://github.com/datasette/datasette-demo-dbs/actions/workflows/test.yml/badge.svg)](https://github.com/datasette/datasette-demo-dbs/actions/workflows/test.yml)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-demo-dbs/blob/main/LICENSE)

  Fetch missing demo DBs from URLs when Datasette first starts

  ## Installation

  Install this plugin in the same environment as Datasette.
  ```bash
  datasette install datasette-demo-dbs
  ```
  ## Configuration

  Configure the plugin in your Datasette metadata (YAML or JSON). You specify:

  - `path`: directory on disk where downloaded .db files will be stored (defaults to current directory)
  - `dbs`: a mapping of database name to the URL of the SQLite database file

  Example metadata.yml:

  ```yaml
  plugins:
    datasette-demo-dbs:
      path: ./demo-dbs
      dbs:
        demo: https://example.com/path/to/demo.db
        census: https://example.com/path/to/census.db
  ```

  Equivalent metadata.json:

  ```json
  {
    "plugins": {
      "datasette-demo-dbs": {
        "path": "./demo-dbs",
        "dbs": {
          "demo": "https://example.com/path/to/demo.db",
          "census": "https://example.com/path/to/census.db"
        }
      }
    }
  }
  ```
  ## How it works

  On Datasette startup, for each entry in `dbs`:

  1. If `path/<name>.db` already exists, it is left as-is and loaded into Datasette.
  2. If a marker file `path/<name>.deleted` exists, the download is skipped.
  3. Otherwise, the plugin downloads the content from the URL (following redirects), streams it to `path/<name>.db`, and registers that database in Datasette under the given name.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:
  ```bash
  cd datasette-demo-dbs
  python -m venv venv
  source venv/bin/activate
  ```
  Now install the dependencies and test dependencies:
  ```bash
  pip install -e '.[test]'
  ```
  To run the tests:
  ```bash
  python -m pytest
  ```

project_urls:
  {"CI": "https://github.com/datasette/datasette-demo-dbs/actions", "Changelog": "https://github.com/datasette/datasette-demo-dbs/releases", "Homepage": "https://github.com/datasette/datasette-demo-dbs", "Issues": "https://github.com/datasette/datasette-demo-dbs/issues"}

requires_dist:
  ["datasette", "pytest; extra == \"test\"", "pytest-asyncio; extra == \"test\"", "pytest-httpx; extra == \"test\""]


name: datasette-demo-for-llm-accountant
summary: Demo app for LLM accountant
classifiers: ["Framework :: Datasette"]
author: Simon Willison
description_content_type: text/markdown
dynamic: ["License-File"]
license_expression: Apache-2.0
license_files: ["LICENSE"]
package_url: https://pypi.org/project/datasette-demo-for-llm-accountant/
project_url: https://pypi.org/project/datasette-demo-for-llm-accountant/
provides_extra: ["test"]
release_url: https://pypi.org/project/datasette-demo-for-llm-accountant/0.1a0/
requires_python: >=3.10
version: 0.1a0
description:
  # datasette-demo-for-llm-accountant

  [![PyPI](https://img.shields.io/pypi/v/datasette-demo-for-llm-accountant.svg)](https://pypi.org/project/datasette-demo-for-llm-accountant/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-demo-for-llm-accountant?include_prereleases&label=changelog)](https://github.com/datasette/datasette-demo-for-llm-accountant/releases)
  [![Tests](https://github.com/datasette/datasette-demo-for-llm-accountant/actions/workflows/test.yml/badge.svg)](https://github.com/datasette/datasette-demo-for-llm-accountant/actions/workflows/test.yml)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-demo-for-llm-accountant/blob/main/LICENSE)

  Demo app for LLM accountant

  This is a demonstration plugin showing how to use [datasette-llm-accountant](https://github.com/datasette/datasette-llm-accountant) to track and log LLM API costs.

  ## Features

  - **Interactive LLM Form**: Submit prompts to any available LLM model via a web interface
  - **Cost Tracking**: All LLM API calls are tracked with nanocent precision
  - **Transaction Log**: View a running total of all LLM requests and their costs
  - **In-Memory Accountant**: Demonstrates implementing a custom `Accountant` subclass

  ## Installation

  Install this plugin in the same environment as Datasette.
  ```bash
  datasette install datasette-demo-for-llm-accountant
  ```

  ## Usage

  Once installed, navigate to `/-/llm-accountant` in your Datasette instance.

  You'll see:
  1. A form to select an LLM model and enter a prompt
  2. The response from the LLM
  3. A transaction log showing all requests and their costs

  Each request reserves $0.50 by default, and the actual cost is calculated based on token usage and settled in the transaction log.

  ### Running Locally

  ```bash
  datasette
  ```

  Then visit http://127.0.0.1:8001/-/llm-accountant

  **Note**: You'll need LLM API keys configured for this to work. See the [llm documentation](https://llm.datasette.io/) for details on configuring API keys.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:
  ```bash
  cd datasette-demo-for-llm-accountant
  python -m venv venv
  source venv/bin/activate
  ```
  Now install the dependencies and test dependencies:
  ```bash
  pip install -e '.[test]'
  ```
  To run the tests:
  ```bash
  python -m pytest
  ```

project_urls:
  {"CI": "https://github.com/datasette/datasette-demo-for-llm-accountant/actions", "Changelog": "https://github.com/datasette/datasette-demo-for-llm-accountant/releases", "Homepage": "https://github.com/datasette/datasette-demo-for-llm-accountant", "Issues": "https://github.com/datasette/datasette-demo-for-llm-accountant/issues"}

requires_dist:
  ["datasette>=1.0a21", "llm", "datasette-llm-accountant>=0.1a0", "pytest; extra == \"test\"", "pytest-asyncio; extra == \"test\""]


name: datasette-dns
summary: Custom SQL function for making DNS lookups
classifiers: []
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-dns
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-dns/
project_url: https://pypi.org/project/datasette-dns/
release_url: https://pypi.org/project/datasette-dns/0.1a1/
version: 0.1a1
description:
  # datasette-dns

  [![PyPI](https://img.shields.io/pypi/v/datasette-dns.svg)](https://pypi.org/project/datasette-dns/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-dns?include_prereleases&label=changelog)](https://github.com/simonw/datasette-dns/releases)
  [![Tests](https://github.com/simonw/datasette-dns/workflows/Test/badge.svg)](https://github.com/simonw/datasette-dns/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-dns/blob/main/LICENSE)

  Custom SQL function for making DNS lookups

  ## Installation

  Install this plugin in the same environment as Datasette.

      $ datasette install datasette-dns

  ## Usage

  So far the only supported SQL function is:

      select dns_txt("oreilly.com");

  Try this out in [the live demo](https://datasette-dns-demo.vercel.app/empty?sql=select+dns_txt%28%22oreilly.com%22%29%3B).

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:

      cd datasette-dns
      python3 -mvenv venv
      source venv/bin/activate

  Or if you are using `pipenv`:

      pipenv shell

  Now install the dependencies and tests:

      pip install -e '.[test]'

  To run the tests:

      pytest

project_urls:
  {"CI": "https://github.com/simonw/datasette-dns/actions", "Changelog": "https://github.com/simonw/datasette-dns/releases", "Homepage": "https://github.com/simonw/datasette-dns", "Issues": "https://github.com/simonw/datasette-dns/issues"}

requires_dist:
  ["datasette", "dnspython", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'", "httpx ; extra == 'test'", "pytest-mock ; extra == 'test'"]


name: datasette-edit-schema
summary: Datasette plugin for modifying table schemas
classifiers: []
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-edit-schema
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-edit-schema/
project_url: https://pypi.org/project/datasette-edit-schema/
project_urls: {"Homepage": "https://github.com/simonw/datasette-edit-schema"}
release_url: https://pypi.org/project/datasette-edit-schema/0.7.1/
requires_python: >=3.7
version: 0.7.1
description:
  # datasette-edit-schema

  [![PyPI](https://img.shields.io/pypi/v/datasette-edit-schema.svg)](https://pypi.org/project/datasette-edit-schema/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-edit-schema?include_prereleases&label=changelog)](https://github.com/simonw/datasette-edit-schema/releases)
  [![Tests](https://github.com/simonw/datasette-edit-schema/workflows/Test/badge.svg)](https://github.com/simonw/datasette-edit-schema/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-edit-schema/blob/master/LICENSE)

  Datasette plugin for modifying table schemas

  ## Features

  * Add new columns to a table
  * Rename columns in a table
  * Modify the type of columns in a table
  * Re-order the columns in a table
  * Rename a table
  * Delete a table
  * Change the primary key of a table to another column containing unique values
  * Update the foreign key constraints on a table
  * Add an index (or unique index) to a column on a table
  * Drop an index from a table

  ## Installation

  Install this plugin in the same environment as Datasette.
  ```bash
  pip install datasette-edit-schema
  ```
  ## Usage

  Navigate to `/-/edit-schema/dbname/tablename` on your Datasette instance to edit a specific table.

  Use `/-/edit-schema/dbname` to create a new table in a specific database.

  By default only [the root actor](https://datasette.readthedocs.io/en/stable/authentication.html#using-the-root-actor) can access the page - so you'll need to run Datasette with the `--root` option and click on the link shown in the terminal to sign in and access the page.

  ## Permissions

  The `edit-schema` permission governs access. You can use permission plugins such as [datasette-permissions-sql](https://github.com/simonw/datasette-permissions-sql) to grant additional access to the write interface.

  These permission checks will call the `permission_allowed()` plugin hook with three arguments:

  - `action` will be the string `"edit-schema"`
  - `actor` will be the currently authenticated actor - usually a dictionary
  - `resource` will be the string name of the database

  ## Screenshot

  ![datasette-edit-schema interface](https://raw.githubusercontent.com/simonw/datasette-edit-schema/main/datasette-edit-schema.png)

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:
  ```bash
  cd datasette-edit-schema
  python3 -mvenv venv
  source venv/bin/activate
  ```
  Or if you are using `pipenv`:
  ```bash
  pipenv shell
  ```
  Now install the dependencies and tests:
  ```bash
  pip install -e '.[test]'
  ```
  To run the tests:
  ```bash
  pytest
  ```

requires_dist:
  ["datasette >=0.63", "sqlite-utils >=3.35", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'", "beautifulsoup4 ; extra == 'test'", "html5lib ; extra == 'test'"]


name: datasette-edit-templates
summary: Plugin allowing Datasette templates to be edited within Datasette
classifiers: []
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-edit-templates
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-edit-templates/
project_url: https://pypi.org/project/datasette-edit-templates/
release_url: https://pypi.org/project/datasette-edit-templates/0.4.3/
requires_python: >=3.8
version: 0.4.3
description:
  # datasette-edit-templates

  [![PyPI](https://img.shields.io/pypi/v/datasette-edit-templates.svg)](https://pypi.org/project/datasette-edit-templates/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-edit-templates?include_prereleases&label=changelog)](https://github.com/simonw/datasette-edit-templates/releases)
  [![Tests](https://github.com/simonw/datasette-edit-templates/workflows/Test/badge.svg)](https://github.com/simonw/datasette-edit-templates/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-edit-templates/blob/main/LICENSE)

  Plugin allowing Datasette templates to be edited within Datasette.

  ## Installation

  Install this plugin in the same environment as Datasette.
  ```bash
  datasette install datasette-edit-templates
  ```
  ## Usage

  On startup. a `_templates_` table will be created in the database you are running Datasette against.

  Use the app menu to navigate to the `/-/edit-templates` page, and edit templates there.

  Changes should become visible instantly, and will be persisted to your database.

  The interface is only available to users with the `edit-templates` permission.

  The `root` user is granted this permission by default. You can sign in as the root user using `datasette mydb.db --root`.

  ## Configuration

  To put the `_templates_` table in a specific database, set the `datasette-edit-templates: database` plugin configuration option:

  ```json
  {
      "plugins": {
          "datasette-edit-templates": {
              "database": "some_database"
          }
      }
  }
  ```
  On Datasette [1.0a5](https://docs.datasette.io/en/latest/changelog.html#a5-2023-08-29) or higher you can use the [internal database](https://docs.datasette.io/en/latest/internals.html#internals-internal) with `"internal_db: true":`

  ```json
  {
      "plugins": {
          "datasette-edit-templates": {
              "internal_db": true
          }
      }
  }
  ```

  By default the [prepare_jinja2_environment()](https://docs.datasette.io/en/stable/plugin_hooks.html#prepare-jinja2-environment-env-datasette) hook will be used to load the custom templates.

  You can disable this behavior using the `skip_prepare_jinja2_environment` plugin configuration option:

  ```json
  {
      "plugins": {
          "datasette-edit-templates": {
              "skip_prepare_jinja2_environment": true
          }
      }
  }
  ```
  Set this option if you want to further customize how the templates are loaded using another plugin.

  The menu item used to access this plugin is labeled "Edit templates" by default. You can customize this using the `menu_label` plugin configuration option:

  ```json
  {
      "plugins": {
          "datasette-edit-templates": {
              "menu_label": "Custom templates"
          }
      }
  }
  ```
  Set that to `null` to hide the menu option entirely.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:
  ```bash
  cd datasette-edit-templates
  python3 -mvenv venv
  source venv/bin/activate
  ```
  Or if you are using `pipenv`:
  ```bash
  pipenv shell
  ```
  Now install the dependencies and tests:
  ```bash
  pip install -e '.[test]'
  ```
  To run the tests:
  ```bash
  pytest
  ```

project_urls:
  {"CI": "https://github.com/simonw/datasette-edit-templates/actions", "Changelog": "https://github.com/simonw/datasette-edit-templates/releases", "Homepage": "https://github.com/simonw/datasette-edit-templates", "Issues": "https://github.com/simonw/datasette-edit-templates/issues"}

requires_dist:
  ["datasette >=0.63", "pytest-playwright ; extra == 'playwright'", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'", "sqlite-utils ; extra == 'test'", "datasette-test >=0.2 ; extra == 'test'", "nest-asyncio ; extra == 'test'"]


name: datasette-embeddings
summary: Store and query embedding vectors in Datasette tables
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Datasette
description_content_type: text/markdown
license: Apache-2.0
package_url: https://pypi.org/project/datasette-embeddings/
project_url: https://pypi.org/project/datasette-embeddings/
release_url: https://pypi.org/project/datasette-embeddings/0.1a3/
requires_python: >=3.8
version: 0.1a3
description:
  # datasette-embeddings

  [![PyPI](https://img.shields.io/pypi/v/datasette-embeddings.svg)](https://pypi.org/project/datasette-embeddings/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-embeddings?include_prereleases&label=changelog)](https://github.com/datasette/datasette-embeddings/releases)
  [![Tests](https://github.com/datasette/datasette-embeddings/actions/workflows/test.yml/badge.svg)](https://github.com/datasette/datasette-embeddings/actions/workflows/test.yml)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-embeddings/blob/main/LICENSE)

  Store and query embedding vectors in Datasette tables

  ## Installation

  Install this plugin in the same environment as Datasette.
  ```bash
  datasette install datasette-embeddings
  ```
  ## Usage

  Adds an [enrichment](https://enrichments.datasette.io/) for calculating and storing OpenAI embedding vectors for content in a table.

  Users get to select the embedding model and the template (e.g. `{{ title }} {{ body }}`) for the columns they would like to embed.

  Embeddings are stored as binary values in columns in a new table called `_embeddings_NAME`, where `NAME` is the name of the original source table.

  The vectors are stored in columns that match the name of the embedding model, for example `emb_text_embedding_3_large_256` for the `text-embedding-3-large-256` model.

  If you do not configure an OpenAI API key users will be asked for one any time they run the enrichment.

  You can set an API key with plugin configuration like this:

  ```yaml
  plugins:
    datasette-embeddings:
      api_key:
        $env: OPENAI_API_KEY
  ```
  Then set the `OPENAI_API_KEY` environment variable before you start Datasette.

  This plugin adds a "Semantic search against this table" table action item for tables with embeddings, but only if the API key environment variable has been configured as the key is needed to calculate embeddings for the user's search query.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:
  ```bash
  cd datasette-embeddings
  python3 -m venv venv
  source venv/bin/activate
  ```
  Now install the dependencies and test dependencies:
  ```bash
  pip install -e '.[test]'
  ```
  To run the tests:
  ```bash
  pytest
  ```
  The tests use captured examples of embedding APIs. The easiest way to re-generate these is to do the following:

  - `rm -rf tests/cassettes` to remove the previous recordings
  - `export OPENAPI_API_KEY='...'` to set an OpenAI API key
  - `pytest --record-mode once` to recreate the cassettes

project_urls:
  {"CI": "https://github.com/datasette/datasette-embeddings/actions", "Changelog": "https://github.com/datasette/datasette-embeddings/releases", "Homepage": "https://github.com/datasette/datasette-embeddings", "Issues": "https://github.com/datasette/datasette-embeddings/issues"}

requires_dist:
  ["datasette", "datasette-enrichments", "sqlite-utils", "httpx", "pytest; extra == \"test\"", "pytest-asyncio; extra == \"test\"", "pytest-recording; extra == \"test\"", "datasette-test; extra == \"test\""]


name: datasette-endpoints
summary: Plugin to add a /-/endpoints debug page listing all configured endpoints
classifiers: ["Framework :: Datasette"]
author: Datasette
description_content_type: text/markdown
dynamic: ["License-File"]
license_expression: Apache-2.0
license_files: ["LICENSE"]
package_url: https://pypi.org/project/datasette-endpoints/
project_url: https://pypi.org/project/datasette-endpoints/
release_url: https://pypi.org/project/datasette-endpoints/0.1/
requires_dist: ["datasette"]
requires_python: >=3.10
version: 0.1
description:
  # datasette-endpoints

  [![PyPI](https://img.shields.io/pypi/v/datasette-endpoints.svg)](https://pypi.org/project/datasette-endpoints/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-endpoints?include_prereleases&label=changelog)](https://github.com/datasette/datasette-endpoints/releases)
  [![Tests](https://github.com/datasette/datasette-endpoints/actions/workflows/test.yml/badge.svg)](https://github.com/datasette/datasette-endpoints/actions/workflows/test.yml)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-endpoints/blob/main/LICENSE)

  Plugin to add a /-/endpoints debug page listing all configured endpoints

  ## Installation

  Install this plugin in the same environment as Datasette.
  ```bash
  datasette install datasette-endpoints
  ```
  ## Usage

  Once installed, this plugin adds two endpoints to your Datasette instance:

  - `/-/endpoints.json` - JSON list of all registered endpoints
  - `/-/endpoints` - HTML page showing all registered endpoints in a table

  ### JSON API

  Visit `/-/endpoints.json` to get a JSON array of all registered routes:

  ```json
  [
      {
          "path": "/-/plugins",
          "view": "JsonDataView",
          "pattern": "/-/plugins(\\.(?P<format>json))?$"
      },
      {
          "path": "/{database}/{table}",
          "view": "TableView",
          "pattern": "/(?P<database>[^\\/\\.]+)/(?P<table>[^\\/\\.]+)(\\.(?P<format>\\w+))?$"
      }
  ]
  ```

  Each endpoint object includes:

  - **path** - A simplified, human-readable URL path with `{name}` placeholders for URL parameters
  - **view** - The name of the view class or function handling the route
  - **pattern** - The raw regex pattern used for URL matching

  ### HTML page

  Visit `/-/endpoints` to see a table of all registered endpoints with their paths, view names, and regex patterns. This includes both built-in Datasette routes and any routes added by plugins.

  ## Development

  To set up this plugin locally, first checkout the code. You can confirm it is available like this:
  ```bash
  cd datasette-endpoints
  # Confirm the plugin is visible
  uv run datasette plugins
  ```
  To run the tests:
  ```bash
  uv run pytest
  ```

project_urls:
  {"CI": "https://github.com/datasette/datasette-endpoints/actions", "Changelog": "https://github.com/datasette/datasette-endpoints/releases", "Homepage": "https://github.com/datasette/datasette-endpoints", "Issues": "https://github.com/datasette/datasette-endpoints/issues"}


name: datasette-enrichments-gpt
summary: Datasette enrichment for analyzing row data using OpenAI's GPT models
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Simon Willison
description_content_type: text/markdown
license: Apache-2.0
package_url: https://pypi.org/project/datasette-enrichments-gpt/
project_url: https://pypi.org/project/datasette-enrichments-gpt/
release_url: https://pypi.org/project/datasette-enrichments-gpt/0.5/
requires_python: >=3.8
version: 0.5
description:
  # datasette-enrichments-gpt

  [![PyPI](https://img.shields.io/pypi/v/datasette-enrichments-gpt.svg)](https://pypi.org/project/datasette-enrichments-gpt/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-enrichments-gpt?include_prereleases&label=changelog)](https://github.com/datasette/datasette-enrichments-gpt/releases)
  [![Tests](https://github.com/datasette/datasette-enrichments-gpt/workflows/Test/badge.svg)](https://github.com/datasette/datasette-enrichments-gpt/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-enrichments-gpt/blob/main/LICENSE)

  Datasette enrichment for analyzing row data using OpenAI's GPT models

  ## Installation

  Install this plugin in the same environment as Datasette.
  ```bash
  datasette install datasette-enrichments-gpt
  ```
  ## Configuration

  This plugin can optionally be configured with an [OpenAI API key](https://platform.openai.com/api-keys). You can set this as an environment variable:
  ```bash
  export DATASETTE_SECRETS_OPENAPI_API_KEY=sk-..
  ```
  Or you can configure it using the [datasette-secrets](https://datasette.io/plugins/datasette-secrets) plugin.

  If you do not configure an OpenAI API key users will be asked to enter one any time they execute the enrichment. The key they provide will not be stored anywhere other than in-memory during the enrichment run.

  ## Usage

  Once installed, this plugin will allow users to select rows to enrich and run them through prompts using `gpt-3.5-turbo` or `gpt-4o`, saving the result of the prompt in the specified column.

  The plugin also provides `gpt-4o vision`, which can run prompts against an image identified by a URL.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:
  ```bash
  cd datasette-enrichments-gpt
  python3 -m venv venv
  source venv/bin/activate
  ```
  Now install the dependencies and test dependencies:
  ```bash
  pip install -e '.[test]'
  ```
  To run the tests:
  ```bash
  pytest
  ```

project_urls:
  {"CI": "https://github.com/datasette/datasette-enrichments-gpt/actions", "Changelog": "https://github.com/datasette/datasette-enrichments-gpt/releases", "Homepage": "https://github.com/datasette/datasette-enrichments-gpt", "Issues": "https://github.com/datasette/datasette-enrichments-gpt/issues"}

requires_dist:
  ["datasette-enrichments>=0.4.1", "sqlite-utils", "datasette-test; extra == \"test\"", "pytest; extra == \"test\"", "pytest-asyncio; extra == \"test\"", "pytest-recording; extra == \"test\""]


name: datasette-enrichments-jinja
summary: Datasette enrichment for evaluating templates in a Jinja sandbox
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Simon Willison
description_content_type: text/markdown
license: Apache-2.0
package_url: https://pypi.org/project/datasette-enrichments-jinja/
project_url: https://pypi.org/project/datasette-enrichments-jinja/
release_url: https://pypi.org/project/datasette-enrichments-jinja/0.1/
requires_python: >=3.8
version: 0.1
description:
  # datasette-enrichments-jinja

  [![PyPI](https://img.shields.io/pypi/v/datasette-enrichments-jinja.svg)](https://pypi.org/project/datasette-enrichments-jinja/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-enrichments-jinja?include_prereleases&label=changelog)](https://github.com/datasette/datasette-enrichments-jinja/releases)
  [![Tests](https://github.com/datasette/datasette-enrichments-jinja/workflows/Test/badge.svg)](https://github.com/datasette/datasette-enrichments-jinja/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-enrichments-jinja/blob/main/LICENSE)

  [Datasette enrichment](https://github.com/simonw/datasette-enrichments) for evaluating templates in a Jinja sandbox

  ## Installation

  Install this plugin in the same environment as Datasette.
  ```bash
  datasette install datasette-enrichments-jinja
  ```
  ## Usage

  This enrichment allows you to select rows from a table and specify a Jinja template to use to generate new text for each of those rows.

  The text can then be saved to a new or existing column on the table.

  Code runs in a [Jinja sandbox](https://jinja.palletsprojects.com/en/3.1.x/sandbox/). This should protect against innocent mistakes, but may not be robust against malicious attackers - so only make this enrichment available to users who you trust not to abuse it.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:
  ```bash
  cd datasette-enrichments-jinja
  python3 -m venv venv
  source venv/bin/activate
  ```
  Now install the dependencies and test dependencies:
  ```bash
  pip install -e '.[test]'
  ```
  To run the tests:
  ```bash
  pytest
  ```

project_urls:
  {"CI": "https://github.com/datasette/datasette-enrichments-jinja/actions", "Changelog": "https://github.com/datasette/datasette-enrichments-jinja/releases", "Homepage": "https://github.com/datasette/datasette-enrichments-jinja", "Issues": "https://github.com/datasette/datasette-enrichments-jinja/issues"}

requires_dist:
  ["datasette-enrichments >=0.1a4", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'", "sqlite-utils ; extra == 'test'"]


name: datasette-enrichments-llm
summary: Enrich data by prompting LLMs
classifiers: ["Framework :: Datasette"]
author: Simon Willison
description_content_type: text/markdown
dynamic: ["License-File"]
license_expression: Apache-2.0
license_files: ["LICENSE"]
package_url: https://pypi.org/project/datasette-enrichments-llm/
project_url: https://pypi.org/project/datasette-enrichments-llm/
release_url: https://pypi.org/project/datasette-enrichments-llm/0.2a1/
requires_dist: ["datasette>=1.0a21", "datasette-enrichments>=0.6a0", "datasette-llm>=0.1a3"]
requires_python: >=3.10
version: 0.2a1
description:
  # datasette-enrichments-llm

  [![PyPI](https://img.shields.io/pypi/v/datasette-enrichments-llm.svg)](https://pypi.org/project/datasette-enrichments-llm/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-enrichments-llm?include_prereleases&label=changelog)](https://github.com/datasette/datasette-enrichments-llm/releases)
  [![Tests](https://github.com/datasette/datasette-enrichments-llm/actions/workflows/test.yml/badge.svg)](https://github.com/datasette/datasette-enrichments-llm/actions/workflows/test.yml)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-enrichments-llm/blob/main/LICENSE)

  Enrich data by prompting LLMs

  This is an **early alpha**.

  ## Installation

  Install this plugin in the same environment as Datasette.
  ```bash
  datasette install datasette-enrichments-llm
  ```
  This plugin depends on [datasette-llm](https://github.com/datasette/datasette-llm) for LLM model management, API key handling, and model provider integration. See the [datasette-llm README](https://github.com/datasette/datasette-llm/blob/main/README.md) for instructions on installing model providers and configuring API keys.

  ## Configuration

  datasette-enrichments-llm registers an `enrichments` purpose with datasette-llm. You can optionally configure which models are available and set a default model for enrichments using datasette-llm's purpose-specific configuration:

  ```yaml
  plugins:
    datasette-llm:
      purposes:
        enrichments:
          model: gpt-5.4-mini
          models:
          - gpt-5.4-nano
          - claude-opus-4.6
  ```

  ## Usage

  Enrichments can be run against any [LLM](https://llm.datasette.io/) model that has an LLM plugin providing [asynchronous support](https://llm.datasette.io/en/stable/plugins/advanced-model-plugins.html#async-models) for that model.

  Multi-modal models are supported via the `media_url` parameter.

  ## Development

  To set up this plugin locally, first checkout the code. Run the tests like this:
  ```bash
  cd datasette-enrichments-llm
  uv run pytest
  ```
  To try the plugin:
  ```bash
  echo 'id,name\n1,Bob\n2,Kate' | uvx sqlite-utils insert data.db people - --csv
  ```
  Then:
  ```bash
  uv run datasette --root --secret 1 data.db
  ```

project_urls:
  {"CI": "https://github.com/datasette/datasette-enrichments-llm/actions", "Changelog": "https://github.com/datasette/datasette-enrichments-llm/releases", "Homepage": "https://github.com/datasette/datasette-enrichments-llm", "Issues": "https://github.com/datasette/datasette-enrichments-llm/issues"}


name: datasette-enrichments-opencage
summary: Geocoding enrichment using OpenCage
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Simon Willison
description_content_type: text/markdown
license: Apache-2.0
package_url: https://pypi.org/project/datasette-enrichments-opencage/
project_url: https://pypi.org/project/datasette-enrichments-opencage/
release_url: https://pypi.org/project/datasette-enrichments-opencage/0.1.1/
requires_python: >=3.8
version: 0.1.1
description:
  # datasette-enrichments-opencage

  [![PyPI](https://img.shields.io/pypi/v/datasette-enrichments-opencage.svg)](https://pypi.org/project/datasette-enrichments-opencage/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-enrichments-opencage?include_prereleases&label=changelog)](https://github.com/datasette/datasette-enrichments-opencage/releases)
  [![Tests](https://github.com/datasette/datasette-enrichments-opencage/workflows/Test/badge.svg)](https://github.com/datasette/datasette-enrichments-opencage/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-enrichments-opencage/blob/main/LICENSE)

  Geocoding enrichment using OpenCage

  ## Installation

  Install this plugin in the same environment as Datasette.
  ```bash
  datasette install datasette-enrichments-opencage
  ```
  ## Usage

  This plugin adds an enrichment for geocoding using the [OpenCage Geocoder](https://opencagedata.com/).

  You will need an API key from OpenCage - you can sign up for a free trial at https://opencagedata.com/users/sign_up.

  Filter for data that you wish to geocode, then apply the OpenCage geocoder enrichment.

  You'll need to specify a template to be passed to the geocoder, specifying which templates should be used as the input.

  If you have a single column containing the address, you can use this:

      {{ address }}

  If you have separate columns for the street, city, state and country, you can use this:

      {{ street }}, {{ city }}, {{ state }}, {{ country }}

  If your address column is missing the country, but all of the addresses are in the USA, you could use this:

      {{ address }}, USA

  See the [OpenCage guide](https://opencagedata.com/guides/how-to-format-your-geocoding-query) for tips on how to get the best results.

  By default only the latitude and longitude from the geocoder will be stored, in the `latitude` and `longitude` columns on your table. These columns will be created if they do not yet exist.

  You can optionally specify a column to store the full JSON output of the geocoder. This column will also be created if it does not exist.

  The full JSON format is [described here](https://opencagedata.com/api#response).

  ## Configuration

  You can use this plugin without configuration, but you'll need to enter your API key every time you run an enrichment.

  To avoid that, you can set your API key as plugin configuration like this:

  ```bash
  export OPENCAGE_API_KEY="your-api-key"
  ```
  Then in `metadata.yml`:
  ```yaml
  plugins:
    datasette-enrichments-opencage:
      api_key:
        $env: OPENCAGE_API_KEY
  ```
  Then run Datasette like this:
  ```bash
  datasette mydatabase.db -m metadata.yml --root
  ```
  This well give you a URL to sign in as the "root" user, which grants you access to the enrichment.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:
  ```bash
  cd datasette-enrichments-opencage
  python3 -m venv venv
  source venv/bin/activate
  ```
  Now install the dependencies and test dependencies:
  ```bash
  pip install -e '.[test]'
  ```
  To run the tests:
  ```bash
  pytest
  ```

project_urls:
  {"CI": "https://github.com/datasette/datasette-enrichments-opencage/actions", "Changelog": "https://github.com/datasette/datasette-enrichments-opencage/releases", "Homepage": "https://github.com/datasette/datasette-enrichments-opencage", "Issues": "https://github.com/datasette/datasette-enrichments-opencage/issues"}

requires_dist:
  ["datasette", "datasette-enrichments >=0.2", "sqlite-utils", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'", "pytest-httpx ; extra == 'test'"]


name: datasette-enrichments-quickjs
summary: Enrich data with a custom JavaScript function
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Simon Willison
description_content_type: text/markdown
license: Apache-2.0
package_url: https://pypi.org/project/datasette-enrichments-quickjs/
project_url: https://pypi.org/project/datasette-enrichments-quickjs/
release_url: https://pypi.org/project/datasette-enrichments-quickjs/0.1a2/
requires_python: >=3.8
version: 0.1a2
description:
  # datasette-enrichments-quickjs

  [![PyPI](https://img.shields.io/pypi/v/datasette-enrichments-quickjs.svg)](https://pypi.org/project/datasette-enrichments-quickjs/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-enrichments-quickjs?include_prereleases&label=changelog)](https://github.com/datasette/datasette-enrichments-quickjs/releases)
  [![Tests](https://github.com/datasette/datasette-enrichments-quickjs/actions/workflows/test.yml/badge.svg)](https://github.com/datasette/datasette-enrichments-quickjs/actions/workflows/test.yml)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-enrichments-quickjs/blob/main/LICENSE)

  [Datasette enrichment](https://github.com/simonw/datasette-enrichments) for enriching data with a custom JavaScript function

  ## Installation

  Install this plugin in the same environment as Datasette.
  ```bash
  datasette install datasette-enrichments-quickjs
  ```
  ## Usage

  This enrichment allows you to select rows from a table and specify a custom JavaScript function to use to generate a value for each of those rows, storing that value in a specified column and creating that column if it does not exist.

  Code runs in a [QuickJS sandbox](https://github.com/PetterS/quickjs) with a 0.1s time limit for the execution of each function and a 4MB memory limit.

  Enrichment JavaScript functions look like this:

  ```javascript
  function enrich(row) {
      return row["title"] + "!";
  }
  ```
  The return value of your function will be stored in the output column of your choice.

  Instead of picking an output column, you can have your function return an object with keys and values.

  This example takes a `point` column with values like `37.7749,-122.4194 and splits it into `latitude` and `longitude` columns:

  ```javascript
  function enrich(row) {
      const bits = row.point.split(",");
      return {
          "latitude": parseFloat(bits[0]),
          "longitude": parseFloat(bits[1])
      }
  }
  ```
  The enrichment will then create new columns in the table for each key in the object returned by that function.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:
  ```bash
  cd datasette-enrichments-quickjs
  python3 -m venv venv
  source venv/bin/activate
  ```
  Now install the dependencies and test dependencies:
  ```bash
  pip install -e '.[test]'
  ```
  To run the tests:
  ```bash
  pytest
  ```

project_urls:
  {"CI": "https://github.com/datasette/datasette-enrichments-quickjs/actions", "Changelog": "https://github.com/datasette/datasette-enrichments-quickjs/releases", "Homepage": "https://github.com/datasette/datasette-enrichments-quickjs", "Issues": "https://github.com/datasette/datasette-enrichments-quickjs/issues"}

requires_dist:
  ["datasette", "datasette-enrichments", "quickjs", "sqlite-utils", "pytest; extra == \"test\"", "pytest-asyncio; extra == \"test\"", "pytest-timeout; extra == \"test\""]


name: datasette-enrichments-re2
summary: Enrich data using regular expressions
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Simon Willison
description_content_type: text/markdown
license: Apache-2.0
package_url: https://pypi.org/project/datasette-enrichments-re2/
project_url: https://pypi.org/project/datasette-enrichments-re2/
release_url: https://pypi.org/project/datasette-enrichments-re2/0.1/
requires_python: >=3.8
version: 0.1
description:
  # datasette-enrichments-re2

  [![PyPI](https://img.shields.io/pypi/v/datasette-enrichments-re2.svg)](https://pypi.org/project/datasette-enrichments-re2/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-enrichments-re2?include_prereleases&label=changelog)](https://github.com/datasette/datasette-enrichments-re2/releases)
  [![Tests](https://github.com/datasette/datasette-enrichments-re2/workflows/Test/badge.svg)](https://github.com/datasette/datasette-enrichments-re2/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-enrichments-re2/blob/main/LICENSE)

  Enrich data using regular expressions

  ## Installation

  Install this plugin in the same environment as Datasette.
  ```bash
  datasette install datasette-enrichments-re2
  ```
  ## Usage

  This enrichment allows you to run a regular expression against a column and perform one of the following:

  - Execute a search and replace against that column
  - Extract the first matching result and store that in the specified column (adding a column if necessary)
  - Extract all matching results and store them as a JSON array in the specified column. If the regular expression uses named capture groups this will be an array of objects, otherwise it will be an array of strings.
  - Execute a regular expression with named capture groups and store the results in multiple columns, one for each of those named groups

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:
  ```bash
  cd datasette-enrichments-re2
  python3 -m venv venv
  source venv/bin/activate
  ```
  Now install the dependencies and test dependencies:
  ```bash
  pip install -e '.[test]'
  ```
  To run the tests:
  ```bash
  pytest
  ```

project_urls:
  {"CI": "https://github.com/datasette/datasette-enrichments-re2/actions", "Changelog": "https://github.com/datasette/datasette-enrichments-re2/releases", "Homepage": "https://github.com/datasette/datasette-enrichments-re2", "Issues": "https://github.com/datasette/datasette-enrichments-re2/issues"}

requires_dist:
  ["datasette", "datasette-enrichments >=0.2", "google-re2", "sqlite-utils", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'"]


name: datasette-enrichments-slow
summary: An enrichment on a slow loop to help debug progress bars
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Simon Willison
description_content_type: text/markdown
license: Apache-2.0
package_url: https://pypi.org/project/datasette-enrichments-slow/
project_url: https://pypi.org/project/datasette-enrichments-slow/
provides_extra: ["test"]
release_url: https://pypi.org/project/datasette-enrichments-slow/0.3/
requires_python: >=3.9
version: 0.3
description:
  # datasette-enrichments-slow

  [![PyPI](https://img.shields.io/pypi/v/datasette-enrichments-slow.svg)](https://pypi.org/project/datasette-enrichments-slow/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-enrichments-slow?include_prereleases&label=changelog)](https://github.com/datasette/datasette-enrichments-slow/releases)
  [![Tests](https://github.com/datasette/datasette-enrichments-slow/actions/workflows/test.yml/badge.svg)](https://github.com/datasette/datasette-enrichments-slow/actions/workflows/test.yml)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-enrichments-slow/blob/main/LICENSE)

  An enrichment on a slow loop to help debug progress bars

  ## Installation

  Install this plugin in the same environment as Datasette.
  ```bash
  datasette install datasette-enrichments-slow
  ```
  ## Usage

  Select a table (or rows from a table) and run the enrichment. You can specify how many seconds to sleep for each row.

  No changes will be recorded to your table. This plugin is for debugging purposes only.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:
  ```bash
  cd datasette-enrichments-slow
  python -m venv venv
  source venv/bin/activate
  ```
  Now install the dependencies and test dependencies:
  ```bash
  pip install -e '.[test]'
  ```
  To run the tests:
  ```bash
  python -m pytest
  ```

project_urls:
  {"CI": "https://github.com/datasette/datasette-enrichments-slow/actions", "Changelog": "https://github.com/datasette/datasette-enrichments-slow/releases", "Homepage": "https://github.com/datasette/datasette-enrichments-slow", "Issues": "https://github.com/datasette/datasette-enrichments-slow/issues"}

requires_dist:
  ["datasette", "datasette-enrichments>=0.5", "pytest; extra == \"test\"", "pytest-asyncio; extra == \"test\""]


name: datasette-enrichments
summary: Tools for running enrichments against data stored in Datasette
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-enrichments
license: Apache License, Version 2.0
license_files: ["LICENSE"]
package_url: https://pypi.org/project/datasette-enrichments/
project_url: https://pypi.org/project/datasette-enrichments/
provides_extra: ["test", "docs"]
release_url: https://pypi.org/project/datasette-enrichments/0.5.1/
requires_python: >=3.7
version: 0.5.1
description:
  # datasette-enrichments

  [![PyPI](https://img.shields.io/pypi/v/datasette-enrichments.svg)](https://pypi.org/project/datasette-enrichments/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-enrichments?include_prereleases&label=changelog)](https://github.com/simonw/datasette-enrichments/releases)
  [![Tests](https://github.com/simonw/datasette-enrichments/workflows/Test/badge.svg)](https://github.com/simonw/datasette-enrichments/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-enrichments/blob/main/LICENSE)

  Tools for running enrichments against data stored in Datasette

  Potential use-cases for enrichments include:

  - Geocoding an address and populating a latitude and longitude column
  - Executing a template to generate output based on the values in each row
  - Fetching data from a URL and populating a column with the result
  - Executing OCR against a linked image or PDF file

  Documentation for this plugin lives at **[enrichments.datasette.io](https://enrichments.datasette.io/)**.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:
  ```bash
  cd datasette-enrichments
  python3 -mvenv venv
  source venv/bin/activate
  ```
  Or if you are using `pipenv`:
  ```bash
  pipenv shell
  ```
  Now install the dependencies and test dependencies:
  ```bash
  pip install -e '.[test]'
  ```
  To run the tests:
  ```bash
  pytest
  ```

dynamic:
  ["Author", "Classifier", "Description", "Description-Content-Type", "Home-Page", "License", "License-File", "Project-Url", "Provides-Extra", "Requires-Dist", "Requires-Python", "Summary"]

project_urls:
  {"CI": "https://github.com/simonw/datasette-enrichments/actions", "Changelog": "https://github.com/simonw/datasette-enrichments/releases", "Homepage": "https://github.com/simonw/datasette-enrichments", "Issues": "https://github.com/simonw/datasette-enrichments/issues"}

requires_dist:
  ["datasette", "WTForms", "datasette-secrets>=0.2", "pytest; extra == \"test\"", "pytest-asyncio; extra == \"test\"", "black; extra == \"test\"", "ruff; extra == \"test\"", "packaging; extra == \"test\"", "sphinx==7.2.6; extra == \"docs\"", "furo==2023.9.10; extra == \"docs\"", "sphinx-autobuild; extra == \"docs\"", "sphinx-copybutton; extra == \"docs\"", "myst-parser; extra == \"docs\"", "cogapp; extra == \"docs\""]


name: datasette-ephemeral-tables
summary: Provide tables that expire after a time limit
classifiers: ["Framework :: Datasette"]
author: Simon Willison
description_content_type: text/markdown
dynamic: ["License-File"]
license_expression: Apache-2.0
license_files: ["LICENSE"]
package_url: https://pypi.org/project/datasette-ephemeral-tables/
project_url: https://pypi.org/project/datasette-ephemeral-tables/
provides_extra: ["test"]
release_url: https://pypi.org/project/datasette-ephemeral-tables/0.2.3/
requires_dist: ["datasette", "pytest; extra == \"test\"", "pytest-asyncio; extra == \"test\""]
requires_python: >=3.10
version: 0.2.3
description:
  # datasette-ephemeral-tables

  [![PyPI](https://img.shields.io/pypi/v/datasette-ephemeral-tables.svg)](https://pypi.org/project/datasette-ephemeral-tables/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-ephemeral-tables?include_prereleases&label=changelog)](https://github.com/simonw/datasette-ephemeral-tables/releases)
  [![Tests](https://github.com/simonw/datasette-ephemeral-tables/workflows/Test/badge.svg)](https://github.com/simonw/datasette-ephemeral-tables/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-ephemeral-tables/blob/main/LICENSE)

  Provide tables that expire after a time limit

  ## Installation

  Install this plugin in the same environment as Datasette.

      datasette install datasette-ephemeral-tables

  ## Usage

  Once installed, this plugin will ensure Datasette starts running with a new named in-memory database called `ephemeral`.

  Any tables created in this database will be automatically dropped five minutes after creation.

  ## Configuration

  All plugin settings are optional. The full set of settings in `metadata.yml` looks like this:

  ```yaml
  plugins:
      datasette-ephemeral-tables:
          # The name of the in-memory database created by the plugin:
          database: ephemeral
          # After how many seconds should tables be dropped?
          table_ttl: 300
          # How often should the plugin check for expired tables (in seconds)?
          check_interval: 2
  ```
  The figures shown here are the default values.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:

      cd datasette-ephemeral-tables
      python3 -m venv venv
      source venv/bin/activate

  Now install the dependencies and test dependencies:

      pip install -e '.[test]'

  To run the tests:

      pytest

project_urls:
  {"CI": "https://github.com/simonw/datasette-ephemeral-tables/actions", "Changelog": "https://github.com/simonw/datasette-ephemeral-tables/releases", "Homepage": "https://github.com/simonw/datasette-ephemeral-tables", "Issues": "https://github.com/simonw/datasette-ephemeral-tables/issues"}


name: datasette-events-db
summary: Log Datasette events to a database table
classifiers: ["Framework :: Datasette"]
author: Simon Willison
description_content_type: text/markdown
dynamic: ["License-File"]
license_expression: Apache-2.0
license_files: ["LICENSE"]
package_url: https://pypi.org/project/datasette-events-db/
project_url: https://pypi.org/project/datasette-events-db/
provides_extra: ["test"]
release_url: https://pypi.org/project/datasette-events-db/0.1a2/
requires_dist: ["datasette>=1.0a21", "pytest; extra == \"test\"", "pytest-asyncio; extra == \"test\""]
requires_python: >=3.10
version: 0.1a2
description:
  # datasette-events-db

  [![PyPI](https://img.shields.io/pypi/v/datasette-events-db.svg)](https://pypi.org/project/datasette-events-db/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-events-db?include_prereleases&label=changelog)](https://github.com/datasette/datasette-events-db/releases)
  [![Tests](https://github.com/datasette/datasette-events-db/actions/workflows/test.yml/badge.svg)](https://github.com/datasette/datasette-events-db/actions/workflows/test.yml)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-events-db/blob/main/LICENSE)

  Log Datasette events to a database table

  ## Installation

  Install this plugin in the same environment as Datasette.
  ```bash
  datasette install datasette-events-db
  ```
  This plugin depends on [Datasette 1.0a8](https://docs.datasette.io/en/latest/changelog.html#a8-2024-02-07) or higher.
  ## Usage

  Once installed, all [Datasette events](https://docs.datasette.io/en/latest/events.html) will be logged to a table called `datasette_events`. This table will be created in the private internal database, but can be moved to another database using the following plugin configuration option:

  ```yaml
  plugins:
    datasette-events-db:
      database: my_database
  ```

  The table will be created when Datasette starts up, if it does not already exist.

  ## Table schema

  ```sql
  create table if not exists datasette_events (
      id integer primary key,
      event text,
      created text,
      actor_id text,
      database_name text,
      table_name text,
      properties text -- JSON other properties
  )
  ```
  - `event` is the text name of the event, for example `create-table`
  - `created` is an ISO formatted UTC timestamp
  - `actor_id` will be populated with the ID of the responsible actor, or `null` if not available
  - `database_name` will be the `database` property recorded by the event, if present
  - `table_name` will be the `table` property recorded by the event, if present
  - `properties` will be a JSON object containing any other properties recorded by the event

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:
  ```bash
  cd datasette-events-db
  python3 -m venv venv
  source venv/bin/activate
  ```
  Now install the dependencies and test dependencies:
  ```bash
  pip install -e '.[test]'
  ```
  To run the tests:
  ```bash
  pytest
  ```

project_urls:
  {"CI": "https://github.com/datasette/datasette-events-db/actions", "Changelog": "https://github.com/datasette/datasette-events-db/releases", "Homepage": "https://github.com/datasette/datasette-events-db", "Issues": "https://github.com/datasette/datasette-events-db/issues"}


name: datasette-events-forward
summary: Forward Datasette events to another instance
classifiers: ["Framework :: Datasette"]
author: Simon Willison
description_content_type: text/markdown
dynamic: ["License-File"]
license_expression: Apache-2.0
license_files: ["LICENSE"]
package_url: https://pypi.org/project/datasette-events-forward/
project_url: https://pypi.org/project/datasette-events-forward/
provides_extra: ["test"]
release_url: https://pypi.org/project/datasette-events-forward/0.1a4/
requires_python: >=3.10
version: 0.1a4
description:
  # datasette-events-forward

  [![PyPI](https://img.shields.io/pypi/v/datasette-events-forward.svg)](https://pypi.org/project/datasette-events-forward/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-events-forward?include_prereleases&label=changelog)](https://github.com/datasette/datasette-events-forward/releases)
  [![Tests](https://github.com/datasette/datasette-events-forward/actions/workflows/test.yml/badge.svg)](https://github.com/datasette/datasette-events-forward/actions/workflows/test.yml)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-events-forward/blob/main/LICENSE)

  Forward Datasette events to another instance

  ## Installation

  Install this plugin in the same environment as Datasette.
  ```bash
  datasette install datasette-events-forward
  ```

  ## Configuration

  Configure the plugin like so:

  ```json
  {
      "plugins": {
          "datasette-events-forward": {
              "api_token": "***",
              "api_url": "https://stats.datasette.cloud/data/-/create",
              "instance": "localhost"
          }
      }
  }
  ```
  The plugin will then gather all events and forward them to the specified instance, adding them to a table called `datasette_events` which will be created if it does not exist.

  The `instance` key can be used to differentiate different instances that report to the same backend. Events are identified with a ULID to ensure they are unique even across different instances.

  Events are forwarded in batches of up to 10, no more than once every 10 seconds.

  Full list of configuration settings:

  - `api_url`: The write API URL of a Datasette instance to forward the events to.
  - `api_token`: The API token to use when sending events. Use `{"$env": "FORWARD_TOKEN"}` to read the token from the `FORWARD_TOKEN` environment variable.
  - `instance`: A string to identify the instance that is sending the events.

  The `api_url` can be either a `https://datasette.example.com/data/datasette_events/-/insert` endpoint for inserting rows, or a `https://datasette.example.com/data/-/create` endpoint for creating a table and inserting rows into it. If the table does not yet exist you should use the `/-/create` variant, otherwise use the `/-/insert` variant.

  If you use `/-/insert` your API token just needs `insert-row` permissions. For `/-/create` you will need `create-table` permissions as well.

  And to control the rate at which batches of events are sent to the Datasette write API:

  - `batch_limit`: The number of events to send in each batch, defaults to 10. The Datasette write API has a 100 row limit by default so this should be set to a value less than that.
  - `max_rate`: The maximum number of deliver HTTP requests to send in the specified time period, defaults to 1.
  - `time_period`: The time period for the rate limiting in seconds, defaults to 10.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:
  ```bash
  cd datasette-events-forward
  python3 -m venv venv
  source venv/bin/activate
  ```
  Now install the dependencies and test dependencies:
  ```bash
  pip install -e '.[test]'
  ```
  To run the tests:
  ```bash
  pytest
  ```

project_urls:
  {"CI": "https://github.com/datasette/datasette-events-forward/actions", "Changelog": "https://github.com/datasette/datasette-events-forward/releases", "Homepage": "https://github.com/datasette/datasette-events-forward", "Issues": "https://github.com/datasette/datasette-events-forward/issues"}

requires_dist:
  ["datasette>=1.0a20", "python-ulid", "aiolimiter", "typing_extensions", "pytest; extra == \"test\"", "pytest-asyncio; extra == \"test\"", "pytest-httpx; extra == \"test\"", "datasette-test; extra == \"test\""]


name: datasette-examples
summary: Load example SQL scripts into Datasette on startup
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Simon Willison
description_content_type: text/markdown
license: Apache-2.0
package_url: https://pypi.org/project/datasette-examples/
project_url: https://pypi.org/project/datasette-examples/
provides_extra: ["test"]
release_url: https://pypi.org/project/datasette-examples/0.1a0/
requires_python: >=3.8
version: 0.1a0
description:
  # datasette-examples

  [![PyPI](https://img.shields.io/pypi/v/datasette-examples.svg)](https://pypi.org/project/datasette-examples/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-examples?include_prereleases&label=changelog)](https://github.com/datasette/datasette-examples/releases)
  [![Tests](https://github.com/datasette/datasette-examples/actions/workflows/test.yml/badge.svg)](https://github.com/datasette/datasette-examples/actions/workflows/test.yml)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-examples/blob/main/LICENSE)

  Load example SQL scripts into Datasette on startup

  ## Installation

  Install this plugin in the same environment as Datasette.

  ```bash
  pip install datasette-examples
  ```

  ## Usage

  This plugin is configured using `metadata.yaml` (or `metadata.json` or `datasette.yml` in Datasette 1.0). 

  ### Loading example SQL

  Add a block like this that specifies the example SQL scripts you would like to load:

  ```yaml
  plugins:
    datasette-examples:
      startup:
        examples:
        - url: https://example.com/path/to/example1.sql
          if_not_table: table_name_1
        - url: https://example.com/path/to/example2.sql
          if_not_table: table_name_2
  ```

  When Datasette starts running, it will:

  1. Check if the specified table (`if_not_table`) exists in the `examples.db` database
  2. If the table doesn't exist, it will download the SQL file from the provided URL
  3. Execute the SQL against `examples.db`

  The `if_not_table` should be set to a table that is created by the script, to prevent the script from executing more than once.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:
  ```bash
  cd datasette-examples
  python3 -m venv venv
  source venv/bin/activate
  ```
  Now install the dependencies and test dependencies:
  ```bash
  pip install -e '.[test]'
  ```
  To run the tests:
  ```bash
  pytest
  ```

project_urls:
  {"CI": "https://github.com/datasette/datasette-examples/actions", "Changelog": "https://github.com/datasette/datasette-examples/releases", "Homepage": "https://github.com/datasette/datasette-examples", "Issues": "https://github.com/datasette/datasette-examples/issues"}

requires_dist:
  ["datasette", "pytest; extra == \"test\"", "pytest-asyncio; extra == \"test\"", "pytest-httpx; extra == \"test\""]


name: datasette-explain
summary: Explain SQL queries executed using Datasette
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Simon Willison
description_content_type: text/markdown
license: Apache-2.0
package_url: https://pypi.org/project/datasette-explain/
project_url: https://pypi.org/project/datasette-explain/
release_url: https://pypi.org/project/datasette-explain/0.2.1/
requires_dist: ["datasette", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'"]
requires_python: >=3.8
version: 0.2.1
description:
  # datasette-explain

  [![PyPI](https://img.shields.io/pypi/v/datasette-explain.svg)](https://pypi.org/project/datasette-explain/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-explain?include_prereleases&label=changelog)](https://github.com/simonw/datasette-explain/releases)
  [![Tests](https://github.com/simonw/datasette-explain/workflows/Test/badge.svg)](https://github.com/simonw/datasette-explain/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-explain/blob/main/LICENSE)

  Explain SQL queries executed using Datasette

  ## Installation

  Install this plugin in the same environment as Datasette.
  ```bash
  datasette install datasette-explain
  ```
  ## Usage

  The plugin adds JavaScript to the query editor page which will constantly update the page with information gained from running EXPLAIN QUERY PLAN queries against the entered SQL.

  This may result in an error message, or it may show the query plan along with any tables used by the query.

  ## Demo

  You can see this plugin in action on [datasette.simonwillison.net](https://datasette.simonwillison.net/simonwillisonblog?sql=select%0D%0A++blog_tag.tag%2C%0D%0A++blog_blogmark.link_title%2C%0D%0A++blog_blogmark.link_url%0D%0Afrom%0D%0A++blog_blogmark_tags%0D%0A++join+blog_tag+on+tag_id+%3D+blog_tag.id%0D%0A++join+blog_blogmark+on+blog_blogmark_tags.blogmark_id+%3D+blog_blogmark.id%0D%0Aorder+by+blog_blogmark.id+desc).

  <img src="https://raw.githubusercontent.com/simonw/datasette-explain/main/datasette-explain-screenshot.jpg" alt="Demo of this plugin - a SQL query shows the explain plan as well as the three tables that were used in the query (their names and columns)" width="832">

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:
  ```bash
  cd datasette-explain
  python3 -m venv venv
  source venv/bin/activate
  ```
  Now install the dependencies and test dependencies:
  ```bash
  pip install -e '.[test]'
  ```
  To run the tests:
  ```bash
  pytest
  ```

project_urls:
  {"CI": "https://github.com/simonw/datasette-explain/actions", "Changelog": "https://github.com/simonw/datasette-explain/releases", "Homepage": "https://datasette.io/plugins/datasette-explain", "Issues": "https://github.com/simonw/datasette-explain/issues"}


name: datasette-export-database
summary: Export a copy of a SQLite database on demand
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Simon Willison
description_content_type: text/markdown
license: Apache-2.0
package_url: https://pypi.org/project/datasette-export-database/
project_url: https://pypi.org/project/datasette-export-database/
release_url: https://pypi.org/project/datasette-export-database/0.2.1/
requires_dist: ["datasette", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'"]
requires_python: >=3.8
version: 0.2.1
description:
  # datasette-export-database

  [![PyPI](https://img.shields.io/pypi/v/datasette-export-database.svg)](https://pypi.org/project/datasette-export-database/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-export-database?include_prereleases&label=changelog)](https://github.com/datasette/datasette-export-database/releases)
  [![Tests](https://github.com/datasette/datasette-export-database/actions/workflows/test.yml/badge.svg)](https://github.com/datasette/datasette-export-database/actions/workflows/test.yml)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-export-database/blob/main/LICENSE)

  Export a copy of a SQLite database on demand

  ## Installation

  Install this plugin in the same environment as Datasette.
  ```bash
  datasette install datasette-export-database
  ```
  ## Usage

  Users with the `export-database` permission will be able to download a fresh snapshot of any on-disk (not in-memory) database using a new option in the database action menu.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:
  ```bash
  cd datasette-export-database
  python3 -m venv venv
  source venv/bin/activate
  ```
  Now install the dependencies and test dependencies:
  ```bash
  pip install -e '.[test]'
  ```
  To run the tests:
  ```bash
  pytest
  ```

project_urls:
  {"CI": "https://github.com/datasette/datasette-export-database/actions", "Changelog": "https://github.com/datasette/datasette-export-database/releases", "Homepage": "https://github.com/datasette/datasette-export-database", "Issues": "https://github.com/datasette/datasette-export-database/issues"}


name: datasette-export-notebook
summary: Datasette plugin providing instructions for exporting data to Jupyter or Observable
classifiers: []
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-export-notebook
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-export-notebook/
project_url: https://pypi.org/project/datasette-export-notebook/
release_url: https://pypi.org/project/datasette-export-notebook/1.0.1/
requires_python: >=3.6
version: 1.0.1
description:
  # datasette-export-notebook

  [![PyPI](https://img.shields.io/pypi/v/datasette-export-notebook.svg)](https://pypi.org/project/datasette-export-notebook/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-export-notebook?include_prereleases&label=changelog)](https://github.com/simonw/datasette-export-notebook/releases)
  [![Tests](https://github.com/simonw/datasette-export-notebook/workflows/Test/badge.svg)](https://github.com/simonw/datasette-export-notebook/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-export-notebook/blob/main/LICENSE)

  Datasette plugin providing instructions for exporting data to a [Jupyter](https://jupyter.org/) or [Observable](https://observablehq.com/) notebook.

  ## Installation

  Install this plugin in the same environment as Datasette.

      $ datasette install datasette-export-notebook

  ## Usage

  Once installed, the plugin will add a `.Notebook` export option to every table and query. Clicking on this link will show instructions for exporting the data to Jupyter or Observable.

  ## Demo

  You can see this plugin in action on the [latest-with-plugins.datasette.io](https://latest-with-plugins.datasette.io/) Datasette instance - for example on [/github/commits.Notebook](https://latest-with-plugins.datasette.io/github/commits.Notebook).

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:

      cd datasette-export-notebook
      python3 -mvenv venv
      source venv/bin/activate

  Or if you are using `pipenv`:

      pipenv shell

  Now install the dependencies and tests:

      pip install -e '.[test]'

  To run the tests:

      pytest

project_urls:
  {"CI": "https://github.com/simonw/datasette-export-notebook/actions", "Changelog": "https://github.com/simonw/datasette-export-notebook/releases", "Homepage": "https://github.com/simonw/datasette-export-notebook", "Issues": "https://github.com/simonw/datasette-export-notebook/issues"}

requires_dist:
  ["datasette", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'", "sqlite-utils ; extra == 'test'"]


name: datasette-export
summary: Export pages from Datasette to files on disk
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-export
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-export/
project_url: https://pypi.org/project/datasette-export/
release_url: https://pypi.org/project/datasette-export/0.1a0/
requires_dist: ["datasette", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'"]
requires_python: >=3.7
version: 0.1a0
description:
  # datasette-export

  [![PyPI](https://img.shields.io/pypi/v/datasette-export.svg)](https://pypi.org/project/datasette-export/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-export?include_prereleases&label=changelog)](https://github.com/simonw/datasette-export/releases)
  [![Tests](https://github.com/simonw/datasette-export/workflows/Test/badge.svg)](https://github.com/simonw/datasette-export/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-export/blob/main/LICENSE)

  Export pages from Datasette to files on disk

  ## Installation

  Install this plugin in the same environment as Datasette.

      datasette install datasette-export

  ## Usage

  This plugin adds a new `export` command. You can use this to export one or more pages from Datasette to files on disk.

  Pass the `--path` option one or more times to specify pages:

      datasette export mydata.db --path / --path /mydata.json --path /mydata/table1.csv

  This will create an `export/` directory and save the following files to it:

  - `index.html`
  - `mydata.json`
  - `mydata/table1.csv`

  Use `--output` to specify an alternative directory for the export:

      datasette export mydata.db --path / --output mywebsite/

  In addition to specifying paths, you can pass one or more SQL queries that can return lists of pages to be exported:

      datasette export mydata.db --sql "select path from pages"

  For example, to export JSON for every row in the `fixtures/facetable` table:

      datasette export fixtures.db \
        --sql "select '/fixtures/facetable/' || pk || '.json' from facetable"

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:

      cd datasette-export
      python3 -m venv venv
      source venv/bin/activate

  Now install the dependencies and test dependencies:

      pip install -e '.[test]'

  To run the tests:

      pytest

project_urls:
  {"CI": "https://github.com/simonw/datasette-export/actions", "Changelog": "https://github.com/simonw/datasette-export/releases", "Homepage": "https://github.com/simonw/datasette-export", "Issues": "https://github.com/simonw/datasette-export/issues"}


name: datasette-expose-env
summary: Datasette plugin to expose selected environment variables at /-/env for debugging
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-expose-env
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-expose-env/
project_url: https://pypi.org/project/datasette-expose-env/
release_url: https://pypi.org/project/datasette-expose-env/0.2/
requires_python: >=3.8
version: 0.2
description:
  # datasette-expose-env

  [![PyPI](https://img.shields.io/pypi/v/datasette-expose-env.svg)](https://pypi.org/project/datasette-expose-env/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-expose-env?include_prereleases&label=changelog)](https://github.com/simonw/datasette-expose-env/releases)
  [![Tests](https://github.com/simonw/datasette-expose-env/workflows/Test/badge.svg)](https://github.com/simonw/datasette-expose-env/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-expose-env/blob/main/LICENSE)

  Datasette plugin to expose selected environment variables at `/-/env` for debugging

  ## Installation

  Install this plugin in the same environment as Datasette.
  ```bash
  datasette install datasette-expose-env
  ```
  ## Configuration

  Decide on a list of environment variables you would like to expose, then add the following to your `metadata.yml` configuration:

  ```yaml
  plugins:
    datasette-expose-env:
      variables:
      - ENV_VAR_1
      - ENV_VAR_2
      - ENV_VAR_3
  ```

  If you are using JSON in a `metadata.json` file use the following:

  ```json
  {
    "plugins": {
      "datasette-expose-env": {
        "variables": [
          "ENV_VAR_1",
          "ENV_VAR_2",
          "ENV_VAR_3"
        ]
      }
    }
  }
  ```
  To show a full list of redacted environment variables use `"show_all_redacted": true`:

  ```yaml
  plugins:
    datasette-expose-env:
      show_all_redacted: true
      variables:
      - ENV_VAR_1
      - ENV_VAR_2
  ```
  This will show the values for `ENV_VAR_1` and `ENV_VAR_2` and then a full list of other variables with their value shown as `...`.


  Visit `/-/env` on your Datasette instance to see the values of the environment variables.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:
  ```bash
  cd datasette-expose-env
  python3 -m venv venv
  source venv/bin/activate
  ```
  Now install the dependencies and test dependencies:
  ```bash
  pip install -e '.[test]'
  ```
  To run the tests:
  ```bash
  pytest
  ```

project_urls:
  {"CI": "https://github.com/simonw/datasette-expose-env/actions", "Changelog": "https://github.com/simonw/datasette-expose-env/releases", "Homepage": "https://github.com/simonw/datasette-expose-env", "Issues": "https://github.com/simonw/datasette-expose-env/issues"}

requires_dist:
  ["datasette", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'", "datasette-test ; extra == 'test'"]


name: datasette-external-links-new-tabs
summary: Datasette plugin to open external links in new tabs
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Jacob Weisz
description_content_type: text/markdown
home_page: https://github.com/ocdtrekkie/datasette-external-links-new-tabs
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-external-links-new-tabs/
project_url: https://pypi.org/project/datasette-external-links-new-tabs/
release_url: https://pypi.org/project/datasette-external-links-new-tabs/0.1/
requires_dist: ["datasette", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'"]
requires_python: >=3.7
version: 0.1
description:
  # datasette-external-links-new-tabs

  [![PyPI](https://img.shields.io/pypi/v/datasette-external-links-new-tabs.svg)](https://pypi.org/project/datasette-external-links-new-tabs/)
  [![Changelog](https://img.shields.io/github/v/release/ocdtrekkie/datasette-external-links-new-tabs?include_prereleases&label=changelog)](https://github.com/ocdtrekkie/datasette-external-links-new-tabs/releases)
  [![Tests](https://github.com/ocdtrekkie/datasette-external-links-new-tabs/workflows/Test/badge.svg)](https://github.com/ocdtrekkie/datasette-external-links-new-tabs/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/ocdtrekkie/datasette-external-links-new-tabs/blob/main/LICENSE)

  Datasette plugin to open external links in new tabs

  ## Installation

  Install this plugin in the same environment as Datasette.

      datasette install datasette-external-links-new-tabs

  ## Usage

  There are no usage instructions, it simply opens external links in a new tab.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:

      cd datasette-external-links-new-tabs
      python3 -m venv venv
      source venv/bin/activate

  Now install the dependencies and test dependencies:

      pip install -e '.[test]'

  To run the tests:

      pytest

project_urls:
  {"CI": "https://github.com/ocdtrekkie/datasette-external-links-new-tabs/actions", "Changelog": "https://github.com/ocdtrekkie/datasette-external-links-new-tabs/releases", "Homepage": "https://github.com/ocdtrekkie/datasette-external-links-new-tabs", "Issues": "https://github.com/ocdtrekkie/datasette-external-links-new-tabs/issues"}


name: datasette-extract
summary: Import unstructured data (text and images) into structured tables
classifiers: ["Framework :: Datasette"]
author: Simon Willison
description_content_type: text/markdown
dynamic: ["License-File"]
license_expression: Apache-2.0
license_files: ["LICENSE"]
package_url: https://pypi.org/project/datasette-extract/
project_url: https://pypi.org/project/datasette-extract/
release_url: https://pypi.org/project/datasette-extract/0.3a0/
requires_dist: ["datasette>=1.0a26", "sqlite-utils", "ijson", "python-ulid", "datasette-llm>=0.1a5"]
requires_python: >=3.10
version: 0.3a0
description:
  # datasette-extract

  [![PyPI](https://img.shields.io/pypi/v/datasette-extract.svg)](https://pypi.org/project/datasette-extract/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-extract?include_prereleases&label=changelog)](https://github.com/datasette/datasette-extract/releases)
  [![Tests](https://github.com/datasette/datasette-extract/workflows/Test/badge.svg)](https://github.com/datasette/datasette-extract/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-extract/blob/main/LICENSE)

  Import unstructured data (text and images) into structured tables

  ## Installation

  Install this plugin in the same environment as [Datasette](https://datasette.io/).
  ```bash
  datasette install datasette-extract
  ```

  This plugin depends on [datasette-llm](https://github.com/datasette/datasette-llm) for LLM model management, API key handling, and model provider integration. See the [datasette-llm README](https://github.com/datasette/datasette-llm/blob/main/README.md) for instructions on installing model providers and configuring API keys.

  ## Configuration

  datasette-extract registers an `extract` purpose with datasette-llm. You can optionally configure which models are available and set a default model for extraction using datasette-llm's purpose-specific configuration:

  ```yaml
  plugins:
    datasette-llm:
      purposes:
        extract:
          model: gpt-5.4-mini
          models:
          - gpt-5.4-nano
          - gpt-5.4
          - claude-opus-4.6
  ```

  The model selector in the UI is only shown if more than one model is available.

  ## Usage

  This plugin provides the following features:

  - In the database action cog menu for a database select "Create table with extracted data" to create a new table with data extracted from text or an image
  - In the table action cog menu select "Extract data into this table" to extract data into an existing table

  When creating a table you can specify the column names, types and provide an optional hint (like "YYYY-MM-DD" for dates) to influence how the data should be extracted.

  When populating an existing table you can provide hints and select which columns should be populated.

  Text input can be pasted directly into the textarea.

  Drag and drop a PDF or text file onto the textarea to populate it with the contents of that file. PDF files will have their text extracted, but only if the file contains text as opposed to scanned images.

  Drag and drop a single image onto the textarea - or select it with the image file input box - to process an image.

  ## Permissions

  Users must have the `datasette-extract` permission to use this tool.

  In order to create tables they also need the `create-table` permission.

  To insert rows into an existing table they need `insert-row`.

  ## Development

  The recommended way to develop this plugin uses [uv](https://github.com/astral-sh/uv). To run the tests:
  ```bash
  cd datasette-extract
  uv run pytest
  ```
  To run a development server with an OpenAI API key:
  ```bash
  DATASETTE_SECRETS_OPENAI_API_KEY="sk-..." \
    uv run datasette data.db --create --root --secret 1 \
    -s plugins.datasette-llm.purposes.extract.models '["gpt-5.4-mini"]' \
    --internal internal.db --reload
  ```

project_urls:
  {"CI": "https://github.com/datasette/datasette-extract/actions", "Changelog": "https://github.com/datasette/datasette-extract/releases", "Homepage": "https://github.com/datasette/datasette-extract", "Issues": "https://github.com/datasette/datasette-extract/issues"}


name: datasette-faiss
summary: Maintain a FAISS index for specified Datasette tables
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-faiss
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-faiss/
project_url: https://pypi.org/project/datasette-faiss/
provides_extra: ["test"]
release_url: https://pypi.org/project/datasette-faiss/0.2.1/
requires_python: >=3.7
version: 0.2.1
description:
  # datasette-faiss

  [![PyPI](https://img.shields.io/pypi/v/datasette-faiss.svg)](https://pypi.org/project/datasette-faiss/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-faiss?include_prereleases&label=changelog)](https://github.com/simonw/datasette-faiss/releases)
  [![Tests](https://github.com/simonw/datasette-faiss/workflows/Test/badge.svg)](https://github.com/simonw/datasette-faiss/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-faiss/blob/main/LICENSE)

  Maintain a [FAISS index](https://github.com/facebookresearch/faiss) for specified Datasette tables

  See [Semantic search answers: Q&A against documentation with GPT3 + OpenAI embeddings](https://simonwillison.net/2023/Jan/13/semantic-search-answers/) for background on this project.

  ## Installation

  Install this plugin in the same environment as Datasette.
  ```bash
  datasette install datasette-faiss
  ```
  ## Usage

  This plugin creates in-memory FAISS indexes for specified tables on startup, using an `IndexFlatL2` [FAISS index type](https://github.com/facebookresearch/faiss/wiki/Faiss-indexes).

  If the tables are modified after the server has started the indexes will not (yet) pick up those changes.

  ### Configuration

  The tables to be indexed must have `id` and `embedding` columns. The `embedding` column must be a `blob` containing embeddings that are arrays of floating point numbers that have been encoded using the following Python function:

  ```python
  def encode(vector):
      return struct.pack("f" * len(vector), *vector)
  ```
  You can import that function from this package like so:
  ```python
  from datasette_faiss import encode
  ```
  You can specify which tables should have indexes created for them by adding this to `metadata.json`:
  ```json
  {
      "plugins": {
          "datasette-faiss": {
              "tables": [
                  ["blog", "embeddings"]
              ]
          }
      }
  }
  ```
  Each table is an array listing the database name and the table name.

  If you are using `metadata.yml` the configuration should look like this:

  ```yaml
  plugins:
    datasette-faiss:
      tables:
      - ["blog", "embeddings"]
  ```

  ### SQL functions

  The plugin makes four new SQL functions available within Datasette:

  #### faiss_search(database, table, embedding, k)
    
  Returns the `k` nearest neighbors to the `embedding` found in the specified database and table. For example:
  ```sql
  select faiss_search('blog', 'embeddings', (select embedding from embeddings where id = 3), 5)
  ```
  This will return a JSON array of the five IDs of the records in the `embeddings` table in the `blog` database that are closest to the specified embedding. The returned value looks like this:

  ```json
  ["1", "1249", "1011", "5", "10"]
  ```
  You can use the SQLite `json_each()` function to turn that into a table-like sequence that you can join against.

  Here's an example query that does that:

  ```sql
  with related as (
    select value from json_each(
      faiss_search(
        'blog',
        'embeddings',
        (select embedding from embeddings limit 1),
        5
      )
    )
  )
  select * from blog_entry, related
  where id = value
  ```
  #### faiss_search_with_scores(database, table, embedding, k)

  Takes the same arguments as above, but the return value is a JSON array of pairs, each with an ID and a score - something like this:

  ```json
  [
      ["1", 0.0],
      ["1249", 0.21042244136333466],
      ["1011", 0.29391372203826904],
      ["5", 0.29505783319473267],
      ["10", 0.31554925441741943]
  ]
  ```

  #### faiss_encode(json_vector)

  Given a JSON array of floats, returns the binary embedding blob that can be used with the other functions:

  ```sql
  select faiss_encode('[2.4, 4.1, 1.8]')
  -- Returns a 12 byte blob
  select hex(faiss_encode('[2.4, 4.1, 1.8]'))
  -- Returns 9A991940333383406666E63F
  ```

  #### faiss_decode(vector_blob)

  The opposite of `faiss_encode()`.

  ```sql
  select faiss_decode(X'9A991940333383406666E63F')
  ```
  Returns:
  ```json
  [2.4000000953674316, 4.099999904632568, 1.7999999523162842]
  ```
  Note that floating point arithmetic results in numbers that don't quite round-trip to the exact same expected value.

  #### faiss_agg(id, embedding, compare_embedding, k)

  This aggregate function can be used to find the `k` nearest neighbors to `compare_embedding` for each unique value of `id` in the table. For example:

  ```sql
  select faiss_agg(
      id, embedding, (select embedding from embeddings where id = 3), 5
  ) from embeddings
  ```
  Unlike the `faiss_search()` function, this does not depend on the per-table index that the plugin creates when it first starts running. Instead, an index is built every time the aggregation function is run.

  This means that it should only be used on smaller sets of values - once you get above 10,000 or so the performance from this function is likely to become prohibitively expensive.

  The function returns a JSON array of IDs representing the `k` rows with the closest distance scores, like this:

  ```json
  [1324, 344, 5562, 553, 2534]
  ```
  You can use the `json_each()` function to turn that into a table-like sequence that you can join against.

  [Try an example fais_agg() query](https://datasette.simonwillison.net/simonwillisonblog?sql=with+last_500+as+%28%0D%0A++select%0D%0A++++id%2C%0D%0A++++embedding%0D%0A++from%0D%0A++++blog_entry_embeddings%0D%0A++order+by%0D%0A++++id+desc%0D%0A++limit%0D%0A++++500%0D%0A%29%2C+faiss+as+%28%0D%0A++select%0D%0A++++faiss_agg%28%0D%0A++++++id%2C%0D%0A++++++embedding%2C%0D%0A++++++%28%0D%0A++++++++select%0D%0A++++++++++embedding%0D%0A++++++++from%0D%0A++++++++++blog_entry_embeddings%0D%0A++++++++where%0D%0A++++++++++id+%3D+%3Aid%0D%0A++++++%29%2C%0D%0A++++++10%0D%0A++++%29+as+results%0D%0A++from%0D%0A++++last_500%0D%0A%29%2C%0D%0Aids+as+%28%0D%0A++select%0D%0A++++value+as+id%0D%0A++from%0D%0A++++json_each%28faiss.results%29%2C%0D%0A++++faiss%0D%0A%29%0D%0Aselect%0D%0A++blog_entry.id%2C%0D%0A++blog_entry.title%2C%0D%0A++blog_entry.created%0D%0Afrom%0D%0A++ids%0D%0A++join+blog_entry+on+ids.id+%3D+blog_entry.id&id=8214).

  #### faiss_agg_with_scores(id, embedding, compare_embedding, k)

  This is similar to the `faiss_agg()` aggregate function but it returns a list of pairs, each with an ID and the corresponding score - something that looks like this (if `k` was 2):

  ```json
  [[2412, 0.25], [1245, 24.25]]
  ```
  [Try an example fais_agg_with_scores() query](https://datasette.simonwillison.net/simonwillisonblog?sql=with+last_500+as+%28%0D%0A++select%0D%0A++++id%2C%0D%0A++++embedding%0D%0A++from%0D%0A++++blog_entry_embeddings%0D%0A++order+by%0D%0A++++id+desc%0D%0A++limit%0D%0A++++500%0D%0A%29%2C+ids_and_scores+as+%28%0D%0A++select%0D%0A++++faiss_agg_with_scores%28%0D%0A++++++id%2C%0D%0A++++++embedding%2C%0D%0A++++++%28%0D%0A++++++++select%0D%0A++++++++++embedding%0D%0A++++++++from%0D%0A++++++++++blog_entry_embeddings%0D%0A++++++++where%0D%0A++++++++++id+%3D+%3Aid%0D%0A++++++%29%2C+10%0D%0A++++%29+as+s%0D%0A++from%0D%0A++++last_500%0D%0A%29%2C%0D%0Aresults+as+%28%0D%0A++select%0D%0A++++json_extract%28value%2C+%27%24%5B0%5D%27%29+as+id%2C%0D%0A++++json_extract%28value%2C+%27%24%5B1%5D%27%29+as+score%0D%0A++from%0D%0A++++json_each%28ids_and_scores.s%29%2C%0D%0A++++ids_and_scores%0D%0A%29%0D%0Aselect%0D%0A++results.score%2C%0D%0A++blog_entry.id%2C%0D%0A++blog_entry.title%2C%0D%0A++blog_entry.created%0D%0Afrom%0D%0A++results%0D%0A++join+blog_entry+on+results.id+%3D+blog_entry.id&id=8214).

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:
  ```bash
  cd datasette-faiss
  python3 -m venv venv
  source venv/bin/activate
  ```
  Now install the dependencies and test dependencies:
  ```bash
  pip install -e '.[test]'
  ```
  To run the tests:
  ```bash
  pytest
  ```

project_urls:
  {"CI": "https://github.com/simonw/datasette-faiss/actions", "Changelog": "https://github.com/simonw/datasette-faiss/releases", "Homepage": "https://github.com/simonw/datasette-faiss", "Issues": "https://github.com/simonw/datasette-faiss/issues"}

requires_dist:
  ["datasette", "faiss-cpu", "numpy<2.0,>=1.0", "pytest; extra == \"test\"", "pytest-asyncio; extra == \"test\""]


name: datasette-files-s3
summary: datasette-files S3 backend
classifiers: ["Framework :: Datasette"]
author: Simon Willison
description_content_type: text/markdown
dynamic: ["License-File"]
license: Apache-2.0
license_files: ["LICENSE"]
package_url: https://pypi.org/project/datasette-files-s3/
project_url: https://pypi.org/project/datasette-files-s3/
release_url: https://pypi.org/project/datasette-files-s3/0.1a2/
requires_dist: ["datasette-files>=0.1a2", "aioboto3"]
requires_python: >=3.10
version: 0.1a2
description:
  # datasette-files-s3

  [![PyPI](https://img.shields.io/pypi/v/datasette-files-s3.svg)](https://pypi.org/project/datasette-files-s3/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-files-s3?include_prereleases&label=changelog)](https://github.com/datasette/datasette-files-s3/releases)
  [![Tests](https://github.com/datasette/datasette-files-s3/actions/workflows/test.yml/badge.svg)](https://github.com/datasette/datasette-files-s3/actions/workflows/test.yml)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-files-s3/blob/main/LICENSE)

  S3 storage backend for [datasette-files](https://github.com/datasette/datasette-files).

  ## Installation

  Install this plugin in the same environment as Datasette.
  ```bash
  datasette install datasette-files-s3
  ```

  ## Usage

  Configure a datasette-files source to use S3 storage by setting `"storage": "s3"` and providing the required configuration options:

  ```yaml
  plugins:
    datasette-files:
      sources:
        my-s3-files:
          storage: s3
          config:
            bucket: my-bucket-name
            region: us-east-1
            access_key_id: AKIAIOSFODNN7EXAMPLE
            secret_access_key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
  ```

  Or using Datasette's `-s` flag:

  ```bash
  datasette data.db \
      -s plugins.datasette-files.sources.my-s3-files.storage s3 \
      -s plugins.datasette-files.sources.my-s3-files.config.bucket my-bucket-name \
      -s plugins.datasette-files.sources.my-s3-files.config.region us-east-1
  ```

  You can also use a credentials broker that returns temporary AWS credentials plus the S3 folder to use:

  ```yaml
  plugins:
    datasette-files:
      sources:
        my-s3-files:
          storage: s3
          config:
            credentials_url: https://example.com/api/s3-credentials
            credentials_url_secret: shared-secret
            region: us-east-1
  ```

  ### Credentials Broker Response

  When `credentials_url` is configured, the plugin sends a `POST` request with
  `secret=...` as form-encoded data and expects a JSON response shaped like this:

  ```json
  {
    "AccessKeyId": "ASIAWXFXAIOZGGVU5O6Y",
    "SecretAccessKey": "...",
    "SessionToken": "...",
    "Expiration": "2026-03-20T11:56:23Z",
    "S3Folder": "s3://datasettecloud-dev-files/team-1/"
  }
  ```

  ### Configuration options

  - **bucket** (required unless `credentials_url` is used): The name of the S3 bucket.
  - **region** (optional, default `us-east-1`): The AWS region.
  - **prefix** (optional): A prefix to add to all S3 object keys. This allows you to store files under a specific path within the bucket. A trailing slash will be added automatically if not provided - `"uploads"` and `"uploads/"` are equivalent.
  - **endpoint_url** (optional): A custom S3 endpoint URL, for use with S3-compatible services.
  - **access_key_id** (optional): AWS access key ID.
  - **secret_access_key** (optional): AWS secret access key.
  - **session_token** (optional): AWS session token, for temporary credentials supplied directly in config.
  - **credentials_url** (optional): URL to `POST` to for temporary credentials. The plugin sends `secret=...` as form-encoded data and expects a JSON response containing `AccessKeyId`, `SecretAccessKey`, `SessionToken`, `Expiration`, and `S3Folder`.
  - **credentials_url_secret** (required with `credentials_url`): Shared secret sent to the credentials endpoint as the form field `secret`.

  ### Authentication

  The plugin resolves AWS credentials using the following priority:

  1. **Credentials broker**: If `credentials_url` is configured, the plugin fetches temporary credentials by `POST`ing `secret=...` to that URL. It stores the returned `AccessKeyId`, `SecretAccessKey`, `SessionToken`, `Expiration`, and `S3Folder`, and automatically fetches a fresh set after the expiration time passes. The returned `S3Folder` also sets the active bucket and prefix for the source.
  2. **Direct configuration**: `access_key_id`, `secret_access_key`, and optional `session_token` in the config block.
  3. **Default AWS credential chain**: If no credentials are provided through the above methods, the plugin falls back to the default AWS credential chain (environment variables, IAM roles, etc.).

  ### Prefix

  The `prefix` option lets you scope all files to a specific path within the bucket. For example, with `prefix: "uploads/"`, a file uploaded as `photo.jpg` will be stored at the S3 key `uploads/photo.jpg`.

  It does not matter whether you include a trailing slash or not - `"uploads"` and `"uploads/"` will both result in files stored under `uploads/`.

  When using `credentials_url`, the returned `S3Folder` behaves like a dynamically supplied bucket + prefix. For example, `s3://datasettecloud-dev-files/team-1/` means the plugin will use bucket `datasettecloud-dev-files` and prefix `team-1/`.

  ## Development

  To set up this plugin locally, first checkout the code.
  ```bash
  cd datasette-files-s3
  ```

  Run tests like this:
  ```bash
  uv run pytest
  ```

  You can use [SeaweedFS](https://github.com/seaweedfs/seaweedfs) to run a local development server against a local imitation of the S3 API:
  ```bash
  brew install seaweedfs
  ./dev-server.sh
  ```
  To run a local development server against a real S3 bucket, create a `dev-s3.sh` script (this file is in `.gitignore`):

  ```bash
  #!/bin/bash
  set -e

  BUCKET="your-bucket-name"
  REGION="us-east-1"
  ACCESS_KEY="your-access-key-id"
  SECRET_KEY="your-secret-access-key"

  uv run datasette data.db --create --internal internal.db --root --secret 1 --reload \
      -s plugins.datasette-files.sources.s3-live.storage s3 \
      -s plugins.datasette-files.sources.s3-live.config.bucket "$BUCKET" \
      -s plugins.datasette-files.sources.s3-live.config.region "$REGION" \
      -s plugins.datasette-files.sources.s3-live.config.access_key_id "$ACCESS_KEY" \
      -s plugins.datasette-files.sources.s3-live.config.secret_access_key "$SECRET_KEY" \
      -s plugins.datasette-files.sources.s3-live.config.prefix "demo-prefix/" \
      -s permissions.files-browse true \
      -s permissions.files-upload true \
      -s permissions.files-edit true
  ```

  Then run it with `bash dev-s3.sh` and follow the login token URL printed to the console.

project_urls:
  {"CI": "https://github.com/datasette/datasette-files-s3/actions", "Changelog": "https://github.com/datasette/datasette-files-s3/releases", "Homepage": "https://github.com/datasette/datasette-files-s3", "Issues": "https://github.com/datasette/datasette-files-s3/issues"}


name: datasette-files
summary: File management for Datasette
classifiers: ["Framework :: Datasette"]
author: Datasette
description_content_type: text/markdown
dynamic: ["License-File"]
license_expression: Apache-2.0
license_files: ["LICENSE"]
package_url: https://pypi.org/project/datasette-files/
project_url: https://pypi.org/project/datasette-files/
release_url: https://pypi.org/project/datasette-files/0.1a3/
requires_dist: ["datasette>=1.0a26", "python-ulid", "sqlite-utils", "typing_extensions", "Pillow"]
requires_python: >=3.10
version: 0.1a3
description:
  # datasette-files

  [![PyPI](https://img.shields.io/pypi/v/datasette-files.svg)](https://pypi.org/project/datasette-files/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-files?include_prereleases&label=changelog)](https://github.com/datasette/datasette-files/releases)
  [![Tests](https://github.com/datasette/datasette-files/actions/workflows/test.yml/badge.svg)](https://github.com/datasette/datasette-files/actions/workflows/test.yml)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-files/blob/main/LICENSE)

  File management for Datasette. Upload, serve, search and manage files through a pluggable storage backend system. Ships with built-in filesystem storage and a plugin hook for adding custom backends (S3, Google Cloud Storage, etc.).

  ## Installation

  Install this plugin in the same environment as Datasette.
  ```bash
  datasette install datasette-files
  ```

  ## Usage

  datasette-files manages files through **sources** — named connections to storage backends. Each source has a slug, a storage type, and backend-specific configuration.

  The default `filesystem` source stores files in a directory on disk. You can install additional plugins to add support for extra sources such as [datasette-files-s3](https://github.com/datasette/datasette-files-s3).

  ### Configuring sources

  Define sources in your `datasette.yaml` under the `datasette-files` plugin config:

  ```yaml
  plugins:
    datasette-files:
      sources:
        my-files:
          storage: filesystem
          config:
            root: /data/uploads
  ```

  This creates a source called `my-files` backed by a local directory at `/data/uploads`. The directory will be created if it doesn't exist.

  You can configure multiple sources:

  ```yaml
  plugins:
    datasette-files:
      sources:
        photos:
          storage: filesystem
          config:
            root: /data/photos
        documents:
          storage: filesystem
          config:
            root: /data/documents
  ```

  ### Permissions

  All access is **denied by default**. You must explicitly grant permissions in the `permissions:` block of your `datasette.yaml`.

  There are four permission actions:

  | Action | Description | Scoped to |
  |--------|-------------|-----------|
  | `files-browse` | Browse, search, view, and download files | Source |
  | `files-upload` | Upload files to a source | Source |
  | `files-edit` | Edit file metadata (e.g. search text) | File |
  | `files-delete` | Delete files from a source | File |

  `files-browse` and `files-upload` are scoped to a source — granting them allows the action on all files in that source. `files-edit` and `files-delete` are scoped to individual files, but a source-level grant cascades to all files within it.

  CSV/TSV import also requires Datasette's built-in `create-table` and `insert-row` permissions on the target database. See [Built-in CSV import action](#built-in-csv-import-action) for details.

  **Grant access to everyone (all sources):**

  ```yaml
  permissions:
    files-browse: true
    files-upload: true
  ```

  **Grant access to a specific user:**

  ```yaml
  permissions:
    files-browse:
      id: alice
    files-upload:
      id: alice
  ```

  **Per-source permissions:**

  ```yaml
  permissions:
    files-browse:
      public-files:
        allow: true
      private-files:
        allow:
          id: alice
    files-upload:
      public-files:
        allow:
          id: alice
  ```

  #### Owner permissions

  By default, anyone with `files-edit` or `files-delete` permission on a source can edit or delete any file in that source. You can restrict edit and delete so that users can only act on files they uploaded themselves:

  ```yaml
  plugins:
    datasette-files:
      owners_can_edit: true
      owners_can_delete: true
      sources:
        my-files:
          storage: filesystem
          config:
            root: /data/uploads
  ```

  With these settings, the uploader of each file gains edit or delete permission on their own files — without needing a source-level `files-edit` or `files-delete` grant. Actors who *do* have a source-level grant (e.g. admins) can still act on any file in that source.

  Files uploaded without an authenticated actor have no owner, so they can only be managed by actors with source-level grants.

  ### Uploading files

  Visit `/-/files/upload/{source_slug}` for a dedicated drag-and-drop upload page. It supports multiple files, per-file progress bars, and SVG file-type previews.

  The same upload component is shown on `/-/files/source/{source_slug}` when you have `files-upload` permission for that source.

  #### Upload API

  The upload UI and the file picker dialog both use a three-step flow: **prepare**, **upload**, **complete**. For the built-in `filesystem` backend, step 2 uploads the file bytes to Datasette.

  **Step 1: Prepare**

  ```bash
  curl -X POST "http://localhost:8001/-/files/upload/my-files/-/prepare" \
    -H "Content-Type: application/json" \
    -d '{"filename": "photo.jpg", "content_type": "image/jpeg", "size": 48210}'
  ```

  Returns upload instructions:

  ```json
  {
    "ok": true,
    "upload_token": "tok_01j5...",
    "upload_url": "/-/files/upload/my-files/-/upload",
    "upload_method": "POST",
    "upload_headers": {},
    "upload_fields": {"upload_token": "tok_01j5..."}
  }
  ```

  **Step 2: Upload** — send the file to the `upload_url` from step 1:

  ```bash
  curl -X POST "http://localhost:8001/-/files/upload/my-files/-/upload" \
    -F "upload_token=tok_01j5..." \
    -F "file=@photo.jpg"
  ```

  **Step 3: Complete** — finalize the upload and register the file:

  ```bash
  curl -X POST "http://localhost:8001/-/files/upload/my-files/-/complete" \
    -H "Content-Type: application/json" \
    -d '{"upload_token": "tok_01j5..."}'
  ```

  Returns the registered file:

  ```json
  {
    "ok": true,
    "file": {
      "id": "df-01j5a3b4c5d6e7f8g9h0jkmnpq",
      "filename": "photo.jpg",
      "content_type": "image/jpeg",
      "content_hash": "sha256:...",
      "size": 48210,
      "width": null,
      "height": null,
      "source_slug": "my-files",
      "uploaded_by": null,
      "created_at": "2026-03-13 23:23:24",
      "url": "/-/files/df-01j5a3b4c5d6e7f8g9h0jkmnpq",
      "download_url": "/-/files/df-01j5a3b4c5d6e7f8g9h0jkmnpq/download",
      "thumbnail_url": "/-/files/df-01j5a3b4c5d6e7f8g9h0jkmnpq/thumbnail"
    }
  }
  ```

  File IDs use the format `df-{ULID}` — the `df-` prefix makes them recognizable when stored in database columns.

  ### Deleting files

  ```bash
  curl -X POST "http://localhost:8001/-/files/df-01j5.../-/delete" \
    -H "Content-Type: application/json" \
    -d '{}'
  ```

  Requires `files-delete` permission on the file (or a source-level `files-delete` grant).

  Deletion also depends on the storage backend supporting `can_delete`.

  ### Updating file metadata

  ```bash
  curl -X POST "http://localhost:8001/-/files/df-01j5.../-/update" \
    -H "Content-Type: application/json" \
    -d '{"update": {"search_text": "Annual report 2025"}}'
  ```

  Requires `files-edit` permission on the file (or a source-level `files-edit` grant). Only `search_text` can be updated through this endpoint for now, and the response returns the updated file record.

  ### Viewing files

  Each file has an HTML info page at `/-/files/{file_id}` showing its metadata, a preview (for images), and a download link.

  Download the file content directly at `/-/files/{file_id}/download`.

  Get file metadata as JSON at `/-/files/{file_id}.json`.

  ### Searching files

  Visit `/-/files/search` to search across all files you have permission to browse. The search page supports full-text search over filenames, content types, and custom search text.

  The search endpoint is also available as JSON at `/-/files/search.json?q=query&source=source-slug`.

  Each file has an editable `search_text` field (requires `files-edit` permission) that is included in the full-text search index. This can be used to add descriptions, tags, or transcriptions to make files more discoverable.

  ### Batch metadata

  Fetch metadata for multiple files in a single request:

  ```
  GET /-/files/batch.json?id=df-abc123&id=df-def456
  ```

  This returns metadata for all requested files that the current user has permission to browse. This endpoint is used internally by the `render_cell` web component to efficiently load file information for table views.

  ### Listing sources

  View all configured sources and their capabilities:

  ```
  GET /-/files/sources.json
  ```

  This returns each source's `slug`, `storage_type`, and capability flags such as `can_upload`, `can_delete`, `can_list`, `can_generate_signed_urls`, and `requires_proxy_download`.

  ### Table cell integration

  `datasette-files` uses Datasette's [column_types system](https://docs.datasette.io/en/latest/configuration.html#column-types) to decide which columns should be treated as files.

  Columns assigned the `file` column type will render `df-...` file IDs as rich file references in Datasette's table and row views. The plugin registers a `file` column type and uses that assignment to replace matching values with a `<datasette-file>` web component that displays the filename, content type, and a thumbnail. Built-in Pillow thumbnails are used for common raster image formats, plugins can register thumbnail generators for any file type, and files with no generated thumbnail fall back to an SVG file-type icon.

  The `file` column type is intended for `TEXT` columns. You can assign it in `datasette.yaml` like this:

  ```yaml
  databases:
    mydb:
      tables:
        mytable:
          column_types:
            attachment: file
  ```

  You can also assign it at runtime using Datasette 1.0a26+'s column type UI: open the table page, use the `Column actions` menu for that column, then choose the `file` type. This requires the `set-column-type` permission.

  Once a column is assigned the `file` type, store a `df-...` ID returned from the upload endpoint in that column and it will render as a file link automatically. If the column is not assigned the `file` type, Datasette will show the raw `df-...` text instead.

  ## Endpoint reference

  | Method | Endpoint | Description |
  |--------|----------|-------------|
  | `GET` | `/-/files` | Files index page (HTML) |
  | `GET` | `/-/files/source/{source_slug}` | Source file listing page, with upload UI if allowed (HTML) |
  | `GET` | `/-/files/search` | Search files (HTML) |
  | `GET` | `/-/files/search.json?q=&source=` | Search files (JSON) |
  | `GET` | `/-/files/sources.json` | List configured sources |
  | `GET` | `/-/files/batch.json?id=df-...&id=df-...` | Bulk file metadata |
  | `GET` | `/-/files/upload/{source_slug}` | Dedicated upload page (HTML) |
  | `POST` | `/-/files/upload/{source_slug}/-/prepare` | Prepare upload (get instructions) |
  | `POST` | `/-/files/upload/{source_slug}/-/upload` | Upload file content |
  | `POST` | `/-/files/upload/{source_slug}/-/complete` | Complete upload (register file) |
  | `POST` | `/-/files/{file_id}/-/delete` | Delete a file |
  | `POST` | `/-/files/{file_id}/-/update` | Update file metadata |
  | `GET` | `/-/files/{file_id}` | File info page (HTML) |
  | `GET` | `/-/files/{file_id}.json` | File metadata (JSON) |
  | `GET` | `/-/files/{file_id}/thumbnail` | Generated thumbnail or SVG file-type icon |
  | `GET` | `/-/files/{file_id}/download` | Download file content |
  | `GET` | `/-/files/import/{file_id}` | CSV import preview page |
  | `POST` | `/-/files/import/{file_id}` | Start CSV import job |
  | `GET` | `/-/files/import/{file_id}/{import_id}` | Import progress page (HTML) |
  | `GET` | `/-/files/import/{file_id}/{import_id}.json` | Import progress (JSON) |

  ## Python API

  Other Datasette plugins can use the `get_file()` function to access files managed by datasette-files.

  ### `get_file(datasette, file_id)`

  Look up a file by its ID and return a `File` object, or `None` if the file was not found.

  ```python
  from datasette_files import get_file

  file = await get_file(datasette, "df-01j5a3b4c5d6e7f8g9h0jkmnpq")
  if file is None:
      # File not found
      ...
  ```

  The `File` object has the following attributes:

  | Attribute | Type | Description |
  |-----------|------|-------------|
  | `id` | `str` | The file ID |
  | `filename` | `str` | Original filename |
  | `content_type` | `str` | MIME type |
  | `size` | `int` | File size in bytes |
  | `source_slug` | `str` | The source this file belongs to |
  | `uploaded_by` | `str` or `None` | Actor ID of the uploader |
  | `created_at` | `datetime` | UTC timezone-aware datetime of upload |
  | `metadata` | `dict` | Arbitrary metadata dict |

  ### `file.read(max_bytes=None)`

  Read file content as bytes. Pass `max_bytes` to limit how much is read — useful to avoid loading very large files into memory.

  ```python
  # Read the entire file
  content = await file.read()

  # Read at most 1MB
  content = await file.read(max_bytes=1_000_000)
  ```

  ### `file.open()`

  Open the file for streaming reads. Returns an async context manager that yields an async iterator of bytes chunks. Use this for large files where you want to avoid loading the entire content into memory.

  ```python
  async with file.open() as stream:
      async for chunk in stream:
          process(chunk)
  ```

  ### Example: using `get_file()` from another plugin

  ```python
  from datasette_files import get_file

  async def my_view(datasette, request):
      file = await get_file(datasette, request.args["file_id"])
      if file is None:
          raise NotFound("File not found")

      if file.content_type.startswith("image/"):
          image_bytes = await file.read(max_bytes=20_000_000)
          # Process image...
      elif file.size and file.size > 10_000_000:
          # Stream large files
          async with file.open() as stream:
              async for chunk in stream:
                  ...
      else:
          content = await file.read()
          text = content.decode("utf-8")
  ```

  Note: `get_file()` does not perform any permission checks — the calling plugin is responsible for its own authorization.

  ## Plugin hook: `file_actions`

  The `file_actions` hook lets plugins add custom action links to the file info page. These appear in a "File actions" dropdown menu below the filename heading.

  ### Hook signature

  ```python
  def file_actions(datasette, actor, file, preview_bytes):
      ...
  ```

  | Parameter | Description |
  |-----------|-------------|
  | `datasette` | The Datasette instance |
  | `actor` | The current actor dict (or `None`) |
  | `file` | A dict with the file's metadata: `id`, `filename`, `content_type`, `size`, `content_hash`, `source_id`, etc. |
  | `preview_bytes` | The first 2048 bytes of the file content (useful for sniffing file type) |

  Return a list of dicts, each with:

  | Key | Required | Description |
  |-----|----------|-------------|
  | `href` | Yes | URL the action links to |
  | `label` | Yes | Display text for the action |
  | `description` | No | Short description shown below the label |

  Return an empty list (or `None`) if your plugin has no actions for this file.

  The hook can be a regular function or an `async` function.

  ### Example: add a "Convert to PDF" action for text files

  ```python
  from datasette import hookimpl

  @hookimpl
  def file_actions(datasette, actor, file, preview_bytes):
      if file["content_type"] and file["content_type"].startswith("text/"):
          return [
              {
                  "href": f"/-/convert-pdf/{file['id']}",
                  "label": "Convert to PDF",
                  "description": "Convert this text file to PDF format",
              }
          ]
      return []
  ```

  ### Built-in CSV import action

  datasette-files ships with a built-in `file_actions` implementation that adds an "Import as table" action for CSV files. When a file has a `text/csv` content type or a `.csv` filename extension, the dropdown will include a link to `/-/files/import/{file_id}` which provides:

  1. A preview page showing detected columns and sample rows
  2. A POST endpoint that imports the CSV into a new database table with automatic type detection (integers, floats, and text)
  3. A progress page showing import status

  Importing requires `files-browse` permission on the file's source **plus** Datasette's `create-table` and `insert-row` permissions on the target database. The import will be rejected if the target table already exists.

  ## Plugin hook: `register_files_storage_types`

  datasette-files uses a plugin hook to allow other Datasette plugins to provide custom storage backends. This is how you would build plugins like `datasette-files-s3` or `datasette-files-gcs`.

  ### How it works

  Your plugin returns a list of `Storage` subclasses (not instances). datasette-files handles instantiation, configuration, and lifecycle management.

  ```python
  from datasette import hookimpl

  @hookimpl
  def register_files_storage_types(datasette):
      from my_plugin.storage import S3Storage
      return [S3Storage]
  ```

  When a source in `datasette.yaml` references your storage type, datasette-files will:

  1. Instantiate your class (calling `S3Storage()`)
  2. Call `await storage.configure(config, get_secret)` with the source's config dict
  3. Use your storage instance for all file operations on that source

  ## Plugin hook: `register_thumbnail_generators`

  datasette-files uses a second plugin hook to allow plugins to provide thumbnail generators for files. The built-in Pillow generator handles common raster image formats, and other plugins can add generators for PDFs, videos, office documents, or any other file type.

  ### How it works

  Your plugin returns a list of thumbnail generator instances.

  ```python
  from datasette import hookimpl

  @hookimpl
  def register_thumbnail_generators(datasette):
      from my_plugin.thumbnails import PdfThumbnailGenerator
      return [PdfThumbnailGenerator()]
  ```

  When `/-/files/{file_id}/thumbnail` is requested, datasette-files will:

  1. Check the internal thumbnail cache table for an existing thumbnail
  2. Ask each registered generator if it can handle the file's `content_type` and `filename`
  3. Read the source file once and try matching generators in order
  4. Cache the first successful thumbnail result in the internal database
  5. Fall back to an SVG file-type icon if no generator returns a thumbnail

  The same generation path is also attempted eagerly after uploads complete, so generators can populate the cache before the first thumbnail request.

  ### The `ThumbnailGenerator` base class

  Import the base class and result dataclass from `datasette_files.base`:

  ```python
  from datasette_files.base import ThumbnailGenerator, ThumbnailResult
  ```

  Implement these methods:

  ```python
  class ThumbnailGenerator(ABC):
      name: str

      async def can_generate(self, content_type: str, filename: str) -> bool:
          ...

      async def generate(
          self,
          file_bytes: bytes,
          content_type: str,
          filename: str,
          max_width: int = 200,
          max_height: int = 200,
      ) -> Optional[ThumbnailResult]:
          ...
  ```

  `generate()` returns a `ThumbnailResult` dataclass or `None`:

  ```python
  @dataclass
  class ThumbnailResult:
      thumb_bytes: bytes
      content_type: str
      width: int
      height: int
  ```

  - `name`: Short identifier stored alongside generated thumbnails in the cache table
  - `can_generate(content_type, filename)`: Return `True` if this generator can handle the file
  - `generate(file_bytes, content_type, filename, max_width, max_height)`: Return a `ThumbnailResult` or `None`

  ### Example: PDF thumbnail generator

  ```python
  from datasette import hookimpl
  from datasette_files.base import ThumbnailGenerator, ThumbnailResult


  class PdfThumbnailGenerator(ThumbnailGenerator):
      name = "pdf-preview"

      async def can_generate(self, content_type, filename):
          return content_type == "application/pdf" or filename.lower().endswith(".pdf")

      async def generate(
          self, file_bytes, content_type, filename, max_width=200, max_height=200
      ):
          # Render the first page and return PNG or JPEG bytes
          return ThumbnailResult(
              thumb_bytes=thumbnail_bytes,
              content_type="image/png",
              width=width,
              height=height,
          )


  @hookimpl
  def register_thumbnail_generators(datasette):
      return [PdfThumbnailGenerator()]
  ```

  ### The `Storage` base class

  Import the base class and supporting dataclasses from `datasette_files.base`:

  ```python
  from datasette_files.base import Storage, StorageCapabilities, FileMetadata
  ```

  #### `StorageCapabilities`

  A dataclass declaring what your storage backend supports:

  ```python
  @dataclass
  class StorageCapabilities:
      can_upload: bool = False
      can_delete: bool = False
      can_list: bool = False
      can_generate_signed_urls: bool = False
      requires_proxy_download: bool = False
      max_file_size: Optional[int] = None
  ```

  - `can_upload`: The backend can receive file uploads via `receive_upload()`
  - `can_delete`: The backend can delete files via `delete_file()`
  - `can_list`: The backend can list files via `list_files()`
  - `can_generate_signed_urls`: The backend can produce expiring download URLs via `download_url()` — if `True`, file downloads will use a 302 redirect to the signed URL instead of proxying content through Datasette
  - `requires_proxy_download`: File content must be proxied through Datasette (e.g. filesystem storage) rather than redirecting to an external URL
  - `max_file_size`: Optional maximum file size in bytes (defaults to 100 MB)

  #### `FileMetadata`

  Returned by several storage methods to describe a file:

  ```python
  @dataclass
  class FileMetadata:
      path: str                              # Path within the storage backend
      filename: str                          # Human-readable filename
      content_type: Optional[str] = None     # MIME type
      content_hash: Optional[str] = None     # e.g. "sha256:abcdef..."
      size: Optional[int] = None             # Size in bytes
      width: Optional[int] = None            # Image width in pixels
      height: Optional[int] = None           # Image height in pixels
      created_at: Optional[str] = None
      metadata: dict = field(default_factory=dict)
  ```

  #### Required methods

  Every `Storage` subclass must implement these:

  **`storage_type`** (property) — A unique string identifier for this storage type, used in source configuration. This is how datasette-files matches a source's `storage: s3` to your class.

  ```python
  @property
  def storage_type(self) -> str:
      return "s3"
  ```

  **`capabilities`** (property) — Return a `StorageCapabilities` instance declaring what this backend supports.

  ```python
  @property
  def capabilities(self) -> StorageCapabilities:
      return StorageCapabilities(
          can_upload=True,
          can_delete=True,
          can_generate_signed_urls=True,
      )
  ```

  **`configure(config, get_secret)`** — Called once at startup with the source's `config` dict from `datasette.yaml` and a `get_secret` callable for retrieving secrets from `datasette-secrets`.

  ```python
  async def configure(self, config: dict, get_secret) -> None:
      self.bucket = config["bucket"]
      self.prefix = config.get("prefix", "")
      self.region = config.get("region", "us-east-1")
  ```

  **`get_file_metadata(path)`** — Return a `FileMetadata` for the given path, or `None` if the file doesn't exist.

  ```python
  async def get_file_metadata(self, path: str) -> Optional[FileMetadata]:
      # Check if the file exists in your backend and return its metadata
      ...
  ```

  **`read_file(path)`** — Return the full content of a file as bytes. Raise `FileNotFoundError` if missing.

  ```python
  async def read_file(self, path: str) -> bytes:
      # Read and return the file content
      ...
  ```

  #### Optional methods

  Override these based on the capabilities you declared:

  **`receive_upload(path, stream, content_type)`** — Store file content streamed as chunks. `stream` is an `AsyncIterator[bytes]` — consume it incrementally to avoid buffering the entire file in memory. Return a `FileMetadata` with at least the `content_hash` and `size` populated. Required if `can_upload` is `True`.

  ```python
  async def receive_upload(self, path: str, stream: AsyncIterator[bytes], content_type: str) -> FileMetadata:
      # Consume stream chunks, store the file, and return metadata
      sha256 = hashlib.sha256()
      size = 0
      async for chunk in stream:
          # write chunk to storage
          sha256.update(chunk)
          size += len(chunk)
      ...
  ```

  **`delete_file(path)`** — Delete a file. Required if `can_delete` is `True`.

  **`list_files(prefix, cursor, limit)`** — List files, returning `(files, next_cursor)`. Required if `can_list` is `True`.

  **`download_url(path, expires_in)`** — Return a signed/expiring download URL. Required if `can_generate_signed_urls` is `True`.

  **`read_bytes(path, num_bytes)`** — Return up to `num_bytes` from the start of a file. The default implementation reads the full file with `read_file()` and slices it. Storage backends should override this to avoid downloading entire files — for example, S3 backends can use an HTTP `Range` header to fetch only the requested bytes. Used by the file info page to provide `preview_bytes` to `file_actions` hooks.

  **`stream_file(path)`** — Yield file content in chunks as an async iterator. This method is used by the file download endpoint to stream files to clients without loading the entire file into memory. The default implementation reads the entire file with `read_file()` and yields it as a single chunk — storage backends should override this to yield smaller chunks for efficient memory usage with large files.

  ### Full example: S3 storage plugin

  Here's a complete example of what a `datasette-files-s3` plugin would look like:

  ```python
  # datasette_files_s3/__init__.py
  from datasette import hookimpl
  from datasette_files.base import Storage, StorageCapabilities, FileMetadata
  import boto3
  import hashlib
  from typing import Optional


  class S3Storage(Storage):
      storage_type = "s3"
      capabilities = StorageCapabilities(
          can_upload=True,
          can_delete=True,
          can_list=True,
          can_generate_signed_urls=True,
          requires_proxy_download=False,
      )

      async def configure(self, config: dict, get_secret) -> None:
          self.bucket = config["bucket"]
          self.prefix = config.get("prefix", "")
          self.region = config.get("region", "us-east-1")
          self.client = boto3.client("s3", region_name=self.region)

      def _key(self, path: str) -> str:
          return f"{self.prefix}{path}" if self.prefix else path

      async def get_file_metadata(self, path: str) -> Optional[FileMetadata]:
          try:
              resp = self.client.head_object(
                  Bucket=self.bucket, Key=self._key(path)
              )
              return FileMetadata(
                  path=path,
                  filename=path.split("/")[-1],
                  content_type=resp.get("ContentType"),
                  size=resp.get("ContentLength"),
              )
          except self.client.exceptions.ClientError:
              return None

      async def read_file(self, path: str) -> bytes:
          resp = self.client.get_object(
              Bucket=self.bucket, Key=self._key(path)
          )
          return resp["Body"].read()

      async def read_bytes(self, path: str, num_bytes: int = 2048) -> bytes:
          resp = self.client.get_object(
              Bucket=self.bucket,
              Key=self._key(path),
              Range=f"bytes=0-{num_bytes - 1}",
          )
          return resp["Body"].read()

      async def stream_file(self, path: str):
          resp = self.client.get_object(
              Bucket=self.bucket, Key=self._key(path)
          )
          for chunk in resp["Body"].iter_chunks(chunk_size=65536):
              yield chunk

      async def receive_upload(
          self, path: str, stream, content_type: str
      ) -> FileMetadata:
          # Collect chunks, computing hash incrementally
          chunks = []
          sha256 = hashlib.sha256()
          size = 0
          async for chunk in stream:
              chunks.append(chunk)
              sha256.update(chunk)
              size += len(chunk)
          content = b"".join(chunks)
          self.client.put_object(
              Bucket=self.bucket,
              Key=self._key(path),
              Body=content,
              ContentType=content_type,
          )
          return FileMetadata(
              path=path,
              filename=path.split("/")[-1],
              content_type=content_type,
              content_hash="sha256:" + sha256.hexdigest(),
              size=size,
          )

      async def download_url(self, path: str, expires_in: int = 300) -> str:
          return self.client.generate_presigned_url(
              "get_object",
              Params={"Bucket": self.bucket, "Key": self._key(path)},
              ExpiresIn=expires_in,
          )

      async def delete_file(self, path: str) -> None:
          self.client.delete_object(
              Bucket=self.bucket, Key=self._key(path)
          )

      async def list_files(
          self, prefix: str = "", cursor: Optional[str] = None, limit: int = 100
      ) -> tuple[list[FileMetadata], Optional[str]]:
          kwargs = {
              "Bucket": self.bucket,
              "Prefix": self._key(prefix),
              "MaxKeys": limit,
          }
          if cursor:
              kwargs["ContinuationToken"] = cursor
          resp = self.client.list_objects_v2(**kwargs)
          files = [
              FileMetadata(
                  path=obj["Key"].removeprefix(self.prefix),
                  filename=obj["Key"].split("/")[-1],
                  size=obj["Size"],
              )
              for obj in resp.get("Contents", [])
          ]
          next_cursor = resp.get("NextContinuationToken")
          return files, next_cursor


  @hookimpl
  def register_files_storage_types(datasette):
      return [S3Storage]
  ```

  The plugin's `pyproject.toml` would register itself as a Datasette plugin:

  ```toml
  [project.entry-points.datasette]
  files_s3 = "datasette_files_s3"
  ```

  Then configure it in `datasette.yaml`:

  ```yaml
  plugins:
    datasette-files:
      sources:
        product-images:
          storage: s3
          config:
            bucket: my-photos-bucket
            prefix: "uploads/"
            region: us-west-2
  ```

  ### Built-in filesystem storage reference

  The built-in `FilesystemStorage` stores files on the local filesystem. It supports upload, delete, and listing but does not support signed URLs — file downloads are proxied through Datasette.

  **Configuration options:**

  | Key | Required | Description |
  |-----|----------|-------------|
  | `root` | Yes | Absolute path to the directory where files are stored |
  | `max_file_size` | No | Maximum upload size in bytes (defaults to 100 MB) |

  **Capabilities:**

  | Capability | Value |
  |-----------|-------|
  | `can_upload` | `True` |
  | `can_delete` | `True` |
  | `can_list` | `True` |
  | `can_generate_signed_urls` | `False` |
  | `requires_proxy_download` | `True` |

  ## Development

  To set up this plugin locally, first checkout the code. Run the tests with `uv`:
  ```bash
  cd datasette-files
  uv run pytest
  ```

  Recommendation to run a test server:
  ```bash
  ./dev-server.sh
  ```

project_urls:
  {"CI": "https://github.com/datasette/datasette-files/actions", "Changelog": "https://github.com/datasette/datasette-files/releases", "Homepage": "https://github.com/datasette/datasette-files", "Issues": "https://github.com/datasette/datasette-files/issues"}


name: datasette-geojson-map
summary: Render a map for any query with a geometry column
classifiers: []
author: Chris Amico
description_content_type: text/markdown
home_page: https://github.com/eyeseast/datasette-geojson-map
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-geojson-map/
project_url: https://pypi.org/project/datasette-geojson-map/
provides_extra: ["test"]
release_url: https://pypi.org/project/datasette-geojson-map/0.5.0/
requires_python: >=3.6
version: 0.5.0
description:
  # datasette-geojson-map

  [![PyPI](https://img.shields.io/pypi/v/datasette-geojson-map.svg)](https://pypi.org/project/datasette-geojson-map/)
  [![Changelog](https://img.shields.io/github/v/release/eyeseast/datasette-geojson-map?include_prereleases&label=changelog)](https://github.com/eyeseast/datasette-geojson-map/releases)
  [![Tests](https://github.com/eyeseast/datasette-geojson-map/workflows/Test/badge.svg)](https://github.com/eyeseast/datasette-geojson-map/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/eyeseast/datasette-geojson-map/blob/main/LICENSE)

  Render a map for any query with a geometry column

  ## Installation

  Install this plugin in the same environment as Datasette.

      $ datasette install datasette-geojson-map

  ## Usage

  Start by loading a GIS file.

  For example, you might use [geojson-to-sqlite](https://pypi.org/project/geojson-to-sqlite/) or [shapefile-to-sqlite](https://pypi.org/project/shapefile-to-sqlite/) to load [neighborhood boundaries](https://bostonopendata-boston.opendata.arcgis.com/datasets/3525b0ee6e6b427f9aab5d0a1d0a1a28_0/explore) into a SQLite database.

  ```sh
  wget -O neighborhoods.geojson https://opendata.arcgis.com/datasets/3525b0ee6e6b427f9aab5d0a1d0a1a28_0.geojson
  geojson-to-sqlite boston.db neighborhoods neighborhoods.geojson
  ```

  (The command above uses Spatialite, but that's not required.)

  Start up `datasette` and navigate to the `neighborhoods` table.

  ```sh
  datasette serve boston.db

  # in another terminal tab
  open http://localhost:8001/boston/neighborhoods
  ```

  You should see a map centered on Boston with each neighborhood outlined. Clicking a boundary will bring up a popup with details on that feature.

  ![Boston neighbhorhoods map](https://raw.githubusercontent.com/eyeseast/datasette-geojson-map/main/img/boston-neighborhoods-map.png)

  This plugin relies on (and will install) [datasette-geojson](https://github.com/eyeseast/datasette-geojson). Any query that includes a `geometry` column will produce a map of the results. This also includes single row views.

  Run the included `demo` project to see it live.

  ## Configuration

  This project uses the same map configuration as [datasette-cluster-map](https://github.com/simonw/datasette-cluster-map). Here's how you would use [Stamen's terrain tiles](http://maps.stamen.com/terrain/#12/37.7706/-122.3782):

  ```yaml
  plugins:
    datasette-geojson-map:
      tile_layer: https://stamen-tiles-{s}.a.ssl.fastly.net/terrain/{z}/{x}/{y}.{ext}
      tile_layer_options:
        attribution: >-
          Map tiles by <a href="http://stamen.com">Stamen Design</a>, 
          under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. 
          Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, 
          under <a href="http://www.openstreetmap.org/copyright">ODbL</a>.
        subdomains: abcd
        minZoom: 1
        maxZoom: 16
        ext: jpg
  ```

  Options:

  - `tile_layer`: Use a URL template that can be passed to a [Leaflet Tilelayer](https://leafletjs.com/reference-1.7.1.html#tilelayer)
  - `tile_layer_options`: All options will be passed to the tile layer. See [Leaflet documentation](https://leafletjs.com/reference-1.7.1.html#tilelayer) for more on possible values here.

  ## Styling map features

  Map features can be styled using the [simplestyle-spec](https://github.com/mapbox/simplestyle-spec). This requires setting specific fields on returned rows. Here's an example:

  ```sql
  SELECT Name, geometry, "#ff0000" as fill, "#0000ff" as stroke, 0.2 as stroke-width,  from neighborhoods
  ```

  That will render a neighborhood map where each polygon is filled in red, outlined in blue and lines are 0.2 pixels wide.

  A more useful approach would use the `CASE` statement to color features based on data:

  ```sql
  SELECT
    Name,
    geometry,
    CASE
      Name
      WHEN "Roslindale" THEN "#ff0000"
      WHEN "Dorchester" THEN "#0000ff"
      ELSE "#dddddd"
    END fill
  FROM
    neighborhoods
  ```

  This will fill Roslindale in red, Dorchester in blue and all other neighborhoods in gray.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:

      cd datasette-geojson-map
      python3 -mvenv venv
      source venv/bin/activate

  Or if you are using `pipenv`:

      pipenv shell

  Now install the dependencies and test dependencies:

      pip install -e '.[test]'

  To run the tests:

      pytest

project_urls:
  {"CI": "https://github.com/eyeseast/datasette-geojson-map/actions", "Changelog": "https://github.com/eyeseast/datasette-geojson-map/releases", "Homepage": "https://github.com/eyeseast/datasette-geojson-map", "Issues": "https://github.com/eyeseast/datasette-geojson-map/issues"}

requires_dist:
  ["datasette", "datasette-geojson", "datasette-leaflet", "pytest; extra == \"test\"", "pytest-asyncio; extra == \"test\"", "geojson-to-sqlite; extra == \"test\""]


name: datasette-geojson
summary: Add GeoJSON as an output option
classifiers: []
author: Chris Amico
description_content_type: text/markdown
home_page: https://github.com/eyeseast/datasette-geojson
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-geojson/
project_url: https://pypi.org/project/datasette-geojson/
release_url: https://pypi.org/project/datasette-geojson/0.4.0/
requires_python: >=3.7
version: 0.4.0
description:
  # datasette-geojson

  [![PyPI](https://img.shields.io/pypi/v/datasette-geojson.svg)](https://pypi.org/project/datasette-geojson/)
  [![Changelog](https://img.shields.io/github/v/release/eyeseast/datasette-geojson?include_prereleases&label=changelog)](https://github.com/eyeseast/datasette-geojson/releases)
  [![Tests](https://github.com/eyeseast/datasette-geojson/workflows/Test/badge.svg)](https://github.com/eyeseast/datasette-geojson/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/eyeseast/datasette-geojson/blob/main/LICENSE)

  Add GeoJSON as an output option for datasette queries.

  ## Installation

  Install this plugin in the same environment as Datasette.

      datasette install datasette-geojson

  ## Usage

  To render GeoJSON, add a `.geojson` extension to any query URL that includes a `geometry` column. That column should be a valid [GeoJSON geometry](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1).

  For example, you might use [geojson-to-sqlite](https://pypi.org/project/geojson-to-sqlite/) or [shapefile-to-sqlite](https://pypi.org/project/shapefile-to-sqlite/) to load [neighborhood boundaries](https://bostonopendata-boston.opendata.arcgis.com/datasets/3525b0ee6e6b427f9aab5d0a1d0a1a28_0/explore) into a SQLite database.

  ```sh
  wget -O neighborhoods.geojson https://opendata.arcgis.com/datasets/3525b0ee6e6b427f9aab5d0a1d0a1a28_0.geojson
  geojson-to-sqlite boston.db neighborhoods neighborhoods.geojson --spatial-index # create a spatial index
  datasette serve boston.db --load-extension spatialite
  ```

  If you're using Spatialite, the geometry column will be in a binary format. If not, make sure the `geometry` column is a well-formed [GeoJSON geometry](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1). If you used `geojson-to-sqlite` or `shapefile-to-sqlite`, you should be all set.

  Run this query in Datasette and you'll see a link to download GeoJSON:

  ```sql
  select
    rowid,
    OBJECTID,
    Name,
    Acres,
    Neighborhood_ID,
    SqMiles,
    ShapeSTArea,
    ShapeSTLength,
    geometry
  from
    neighborhoods
  order by
    rowid
  limit
    101
  ```

  Note that the geometry column needs to be explicitly _named_ `geometry` or you won't get the option to export GeoJSON. If you want to use a different column, rename it with `AS`: `SELECT other AS geometry FROM my_table`.

  ![export geojson](img/export-options.png)

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:

      cd datasette-geojson
      python3 -mvenv venv
      source venv/bin/activate

  Or if you are using `pipenv`:

      pipenv shell

  Now install the dependencies and tests:

      pip install -e '.[test]'

  To run the tests:

      pytest

project_urls:
  {"CI": "https://github.com/eyeseast/datasette-geojson/actions", "Changelog": "https://github.com/eyeseast/datasette-geojson/releases", "Homepage": "https://github.com/eyeseast/datasette-geojson", "Issues": "https://github.com/eyeseast/datasette-geojson/issues"}

requires_dist:
  ["datasette", "geojson", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'", "geojson-to-sqlite ; extra == 'test'"]


name: datasette-glitch
summary: Utilities to help run Datasette on Glitch
classifiers: []
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-glitch
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-glitch/
project_url: https://pypi.org/project/datasette-glitch/
release_url: https://pypi.org/project/datasette-glitch/0.1/
version: 0.1
description:
  # datasette-glitch

  [![PyPI](https://img.shields.io/pypi/v/datasette-glitch.svg)](https://pypi.org/project/datasette-glitch/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-glitch?label=changelog)](https://github.com/simonw/datasette-glitch/releases)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-glitch/blob/master/LICENSE)

  Utilities to help run Datasette on Glitch

  ## Installation

  Install this plugin in the same environment as Datasette.

      $ pip install datasette-glitch

  ## Usage

  This plugin outputs a special link which will sign you into Datasette as the root user.

  Click Tools -> Logs in the Glitch editor interface after your app starts to see the link.

project_urls:
  {"CI": "https://github.com/simonw/datasette-glitch/actions", "Changelog": "https://github.com/simonw/datasette-glitch/releases", "Homepage": "https://github.com/simonw/datasette-glitch", "Issues": "https://github.com/simonw/datasette-glitch/issues"}

requires_dist:
  ["datasette (>=0.45)", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'", "httpx ; extra == 'test'"]


name: datasette-granian
summary: Run Datasette using the Granian HTTP server
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-granian
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-granian/
project_url: https://pypi.org/project/datasette-granian/
release_url: https://pypi.org/project/datasette-granian/0.1/
requires_python: >=3.8
version: 0.1
description:
  # datasette-granian

  [![PyPI](https://img.shields.io/pypi/v/datasette-granian.svg)](https://pypi.org/project/datasette-granian/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-granian?include_prereleases&label=changelog)](https://github.com/simonw/datasette-granian/releases)
  [![Tests](https://github.com/simonw/datasette-granian/workflows/Test/badge.svg)](https://github.com/simonw/datasette-granian/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-granian/blob/main/LICENSE)

  Run Datasette using the Granian HTTP server

  ## Installation

  Install this plugin in the same environment as Datasette.

      datasette install datasette-granian

  ## Usage

  Run `datasette granian ...` instead of `datasette serve ...` - most options should remain the same.

  ```
  % datasette granian fixtures.db content.db --crossdb
  [INFO] Starting granian
  [INFO] Listening at: 127.0.0.1:8001
  [INFO] Booting worker-1 with pid: 92793
  [INFO] Started worker-1
  [INFO] Started worker-1 runtime-1
  ```

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:

      cd datasette-granian
      python3 -m venv venv
      source venv/bin/activate

  Now install the dependencies and test dependencies:

      pip install -e '.[test]'

  To run the tests:

      pytest

project_urls:
  {"CI": "https://github.com/simonw/datasette-granian/actions", "Changelog": "https://github.com/simonw/datasette-granian/releases", "Homepage": "https://github.com/simonw/datasette-granian", "Issues": "https://github.com/simonw/datasette-granian/issues"}

requires_dist:
  ["datasette", "granian", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'", "datasette-test ; extra == 'test'"]


name: datasette-graphql
summary: Datasette plugin providing an automatic GraphQL API for your SQLite databases
classifiers: []
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-graphql
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-graphql/
project_url: https://pypi.org/project/datasette-graphql/
release_url: https://pypi.org/project/datasette-graphql/2.2/
version: 2.2
description:
  # datasette-graphql

  [![PyPI](https://img.shields.io/pypi/v/datasette-graphql.svg)](https://pypi.org/project/datasette-graphql/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-graphql?include_prereleases&label=changelog)](https://github.com/simonw/datasette-graphql/releases)
  [![Tests](https://github.com/simonw/datasette-graphql/workflows/Test/badge.svg)](https://github.com/simonw/datasette-graphql/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-graphql/blob/main/LICENSE)

  **Datasette plugin providing an automatic GraphQL API for your SQLite databases**

  Read more about this project: [GraphQL in Datasette with the new datasette-graphql plugin](https://simonwillison.net/2020/Aug/7/datasette-graphql/)

  Try out a live demo at [datasette-graphql-demo.datasette.io/graphql](https://datasette-graphql-demo.datasette.io/graphql?query=%7B%0A%20%20repos(first%3A10%2C%20search%3A%20%22sql%22%2C%20sort_desc%3A%20created_at)%20%7B%0A%20%20%20%20totalCount%0A%20%20%20%20pageInfo%20%7B%0A%20%20%20%20%20%20endCursor%0A%20%20%20%20%20%20hasNextPage%0A%20%20%20%20%7D%0A%20%20%20%20nodes%20%7B%0A%20%20%20%20%20%20full_name%0A%20%20%20%20%20%20description_%0A%20%20%20%20%09stargazers_count%0A%20%20%20%20%20%20created_at%0A%20%20%20%20%20%20owner%20%7B%0A%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%20%20html_url%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A)

  <!-- toc -->

  - [Installation](#installation)
  - [Configuration](#configuration)
  - [Usage](#usage)
    * [Querying for tables and columns](#querying-for-tables-and-columns)
    * [Fetching a single record](#fetching-a-single-record)
    * [Accessing nested objects](#accessing-nested-objects)
    * [Accessing related objects](#accessing-related-objects)
    * [Filtering tables](#filtering-tables)
    * [Sorting](#sorting)
    * [Pagination](#pagination)
    * [Search](#search)
    * [Columns containing JSON strings](#columns-containing-json-strings)
    * [Auto camelCase](#auto-camelcase)
    * [CORS](#cors)
    * [Execution limits](#execution-limits)
  - [The graphql() template function](#the-graphql-template-function)
  - [Adding custom fields with plugins](#adding-custom-fields-with-plugins)
  - [Development](#development)

  <!-- tocstop -->

  ![GraphiQL animated demo](https://static.simonwillison.net/static/2020/graphiql.gif)

  ## Installation

  Install this plugin in the same environment as Datasette.

      $ datasette install datasette-graphql

  ## Configuration

  By default this plugin adds the GraphQL API at `/graphql`. You can configure a different path using the `path` plugin setting, for example by adding this to `metadata.json`:
  ```json
  {
    "plugins": {
      "datasette-graphql": {
        "path": "/-/graphql"
      }
    }
  }
  ```
  This will set the GraphQL API to live at `/-/graphql` instead.

  ## Usage

  This plugin sets up `/graphql` as a GraphQL endpoint for the first attached database.

  If you have multiple attached databases each will get its own endpoint at `/graphql/name_of_database`.

  The automatically generated GraphQL schema is available at `/graphql/name_of_database.graphql` - here's [an example](https://datasette-graphql-demo.datasette.io/graphql/github.graphql).

  ### Querying for tables and columns

  Individual tables (and SQL views) can be queried like this:

  ```graphql
  {
    repos {
      nodes {
        id
        full_name
        description_
      }
    }
  }
  ```
  [Try this query](https://datasette-graphql-demo.datasette.io/graphql?query=%0A%7B%0A%20%20repos%20%7B%0A%20%20%20%20nodes%20%7B%0A%20%20%20%20%20%20id%0A%20%20%20%20%20%20full_name%0A%20%20%20%20%20%20description_%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A)

  In this example query the underlying database table is called `repos` and its columns include `id`, `full_name` and `description`. Since `description` is a reserved word the query needs to ask for `description_` instead.

  ### Fetching a single record

  If you only want to fetch a single record - for example if you want to fetch a row by its primary key - you can use the `tablename_row` field:

  ```graphql
  {
    repos_row(id: 107914493) {
      id
      full_name
      description_
    }
  }
  ```
  [Try this query](https://datasette-graphql-demo.datasette.io/graphql?query=%0A%7B%0A%20%20repos_row%28id%3A%20107914493%29%20%7B%0A%20%20%20%20id%0A%20%20%20%20full_name%0A%20%20%20%20description_%0A%20%20%7D%0A%7D%0A)

  The `tablename_row` field accepts the primary key column (or columns) as arguments. It also supports the same `filter:`, `search:`, `sort:` and `sort_desc:` arguments as the `tablename` field, described below.

  ### Accessing nested objects

  If a column is a foreign key to another table, you can request columns from the table pointed to by that foreign key using a nested query like this:

  ```graphql
  {
    repos {
      nodes {
        id
        full_name
        owner {
          id
          login
        }
      }
    }
  }
  ```
  [Try this query](https://datasette-graphql-demo.datasette.io/graphql?query=%0A%7B%0A%20%20repos%20%7B%0A%20%20%20%20nodes%20%7B%0A%20%20%20%20%20%20id%0A%20%20%20%20%20%20full_name%0A%20%20%20%20%20%20owner%20%7B%0A%20%20%20%20%20%20%20%20id%0A%20%20%20%20%20%20%20%20login%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A)

  ### Accessing related objects

  If another table has a foreign key back to the table you are accessing, you can fetch rows from that related table.

  Consider a `users` table which is related to `repos` - a repo has a foreign key back to the user that owns the repository. The `users` object type will have a `repos_by_owner_list` field which can be used to access those related repos:

  ```graphql
  {
    users(first: 1, search: "simonw") {
      nodes {
        name
        repos_by_owner_list(first: 5) {
          totalCount
          nodes {
            full_name
          }
        }
      }
    }
  }
  ```
  [Try this query](https://datasette-graphql-demo.datasette.io/graphql?query=%0A%7B%0A%20%20users%28first%3A%201%2C%20search%3A%20%22simonw%22%29%20%7B%0A%20%20%20%20nodes%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%20%20repos_by_owner_list%28first%3A%205%29%20%7B%0A%20%20%20%20%20%20%20%20totalCount%0A%20%20%20%20%20%20%20%20nodes%20%7B%0A%20%20%20%20%20%20%20%20%20%20full_name%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A)


  ### Filtering tables

  You can filter the rows returned for a specific table using the `filter:` argument. This accepts a filter object mapping columns to operations. For example, to return just repositories with the Apache 2 license and more than 10 stars:

  ```graphql
  {
    repos(filter: {license: {eq: "apache-2.0"}, stargazers_count: {gt: 10}}) {
      nodes {
        full_name
        stargazers_count
        license {
          key
        }
      }
    }
  }
  ```
  [Try this query](https://datasette-graphql-demo.datasette.io/graphql?query=%0A%7B%0A%20%20repos%28filter%3A%20%7Blicense%3A%20%7Beq%3A%20%22apache-2.0%22%7D%2C%20stargazers_count%3A%20%7Bgt%3A%2010%7D%7D%29%20%7B%0A%20%20%20%20nodes%20%7B%0A%20%20%20%20%20%20full_name%0A%20%20%20%20%20%20stargazers_count%0A%20%20%20%20%20%20license%20%7B%0A%20%20%20%20%20%20%20%20key%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A)

  See [table filters examples](https://github.com/simonw/datasette-graphql/blob/main/examples/filters.md) for more operations, and [column filter arguments](https://docs.datasette.io/en/stable/json_api.html#column-filter-arguments) in the Datasette documentation for details of how those operations work.

  These same filters can be used on nested relationships, like so:

  ```graphql
  {
    users_row(id: 9599) {
      name
      repos_by_owner_list(filter: {name: {startswith: "datasette-"}}) {
        totalCount
        nodes {
          full_name
        }
      }
    }
  }
  ```
  [Try this query](https://datasette-graphql-demo.datasette.io/graphql?query=%0A%7B%0A%20%20users_row%28id%3A%209599%29%20%7B%0A%20%20%20%20name%0A%20%20%20%20repos_by_owner_list%28filter%3A%20%7Bname%3A%20%7Bstartswith%3A%20%22datasette-%22%7D%7D%29%20%7B%0A%20%20%20%20%20%20totalCount%0A%20%20%20%20%20%20nodes%20%7B%0A%20%20%20%20%20%20%20%20full_name%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A)


  The `where:` argument can be used as an alternative to `filter:` when the thing you are expressing is too complex to be modeled using a filter expression. It accepts a string fragment of SQL that will be included in the `WHERE` clause of the SQL query.

  ```graphql
  {
    repos(where: "name='sqlite-utils' or name like 'datasette-%'") {
      totalCount
      nodes {
        full_name
      }
    }
  }
  ```
  [Try this query](https://datasette-graphql-demo.datasette.io/graphql?query=%0A%7B%0A%20%20repos%28where%3A%20%22name%3D%27sqlite-utils%27%20or%20name%20like%20%27datasette-%25%27%22%29%20%7B%0A%20%20%20%20totalCount%0A%20%20%20%20nodes%20%7B%0A%20%20%20%20%20%20full_name%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A)

  ### Sorting

  You can set a sort order for results from a table using the `sort:` or `sort_desc:` arguments. The value for this argument should be the name of the column you wish to sort (or sort-descending) by.

  ```graphql
  {
    repos(sort_desc: stargazers_count) {
      nodes {
        full_name
        stargazers_count
      }
    }
  }
  ```
  [Try this query](https://datasette-graphql-demo.datasette.io/graphql?query=%0A%7B%0A%20%20repos%28sort_desc%3A%20stargazers_count%29%20%7B%0A%20%20%20%20nodes%20%7B%0A%20%20%20%20%20%20full_name%0A%20%20%20%20%20%20stargazers_count%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A)

  ### Pagination

  By default the first 10 rows will be returned. You can control this using the `first:` argument.

  ```graphql
  {
    repos(first: 20) {
      totalCount
      pageInfo {
        hasNextPage
        endCursor
      }
      nodes {
        full_name
        stargazers_count
        license {
          key
        }
      }
    }
  }
  ```
  [Try this query](https://datasette-graphql-demo.datasette.io/graphql?query=%0A%7B%0A%20%20repos%28first%3A%2020%29%20%7B%0A%20%20%20%20totalCount%0A%20%20%20%20pageInfo%20%7B%0A%20%20%20%20%20%20hasNextPage%0A%20%20%20%20%20%20endCursor%0A%20%20%20%20%7D%0A%20%20%20%20nodes%20%7B%0A%20%20%20%20%20%20full_name%0A%20%20%20%20%20%20stargazers_count%0A%20%20%20%20%20%20license%20%7B%0A%20%20%20%20%20%20%20%20key%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A)

  The `totalCount` field returns the total number of records that match the query.

  Requesting the `pageInfo.endCursor` field provides you with the value you need to request the next page. You can pass this to the `after:` argument to request the next page.

  ```graphql
  {
    repos(first: 20, after: "134874019") {
      totalCount
      pageInfo {
        hasNextPage
        endCursor
      }
      nodes {
        full_name
        stargazers_count
        license {
          key
        }
      }
    }
  }
  ```
  [Try this query](https://datasette-graphql-demo.datasette.io/graphql?query=%0A%7B%0A%20%20repos%28first%3A%2020%2C%20after%3A%20%22134874019%22%29%20%7B%0A%20%20%20%20totalCount%0A%20%20%20%20pageInfo%20%7B%0A%20%20%20%20%20%20hasNextPage%0A%20%20%20%20%20%20endCursor%0A%20%20%20%20%7D%0A%20%20%20%20nodes%20%7B%0A%20%20%20%20%20%20full_name%0A%20%20%20%20%20%20stargazers_count%0A%20%20%20%20%20%20license%20%7B%0A%20%20%20%20%20%20%20%20key%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A)

  The `hasNextPage` field tells you if there are any more records.

  ### Search

  If a table has been configured to use SQLite full-text search you can execute searches against it using the `search:` argument:

  ```graphql
  {
    repos(search: "datasette") {
      totalCount
      pageInfo {
        hasNextPage
        endCursor
      }
      nodes {
        full_name
        description_
      }
    }
  }
  ```
  [Try this query](https://datasette-graphql-demo.datasette.io/graphql?query=%0A%7B%0A%20%20repos%28search%3A%20%22datasette%22%29%20%7B%0A%20%20%20%20totalCount%0A%20%20%20%20pageInfo%20%7B%0A%20%20%20%20%20%20hasNextPage%0A%20%20%20%20%20%20endCursor%0A%20%20%20%20%7D%0A%20%20%20%20nodes%20%7B%0A%20%20%20%20%20%20full_name%0A%20%20%20%20%20%20description_%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A)

  The [sqlite-utils](https://sqlite-utils.datasette.io/) Python library and CLI tool can be used to add full-text search to an existing database table.

  ### Columns containing JSON strings

  If your table has a column that contains data encoded as JSON, `datasette-graphql` will make that column available as an encoded JSON string. Clients calling your API will need to parse the string as JSON in order to access the data.

  You can return the data as a nested structure by configuring that column to be treated as a JSON column. The [plugin configuration](https://docs.datasette.io/en/stable/plugins.html#plugin-configuration) for that in `metadata.json` looks like this:

  ```json
  {
      "databases": {
          "test": {
              "tables": {
                  "repos": {
                      "plugins": {
                          "datasette-graphql": {
                              "json_columns": [
                                  "tags"
                              ]
                          }
                      }
                  }
              }
          }
      }
  }
  ```

  ### Auto camelCase

  The names of your columns and tables default to being matched by their representations in GraphQL.

  If you have tables with `names_like_this` you may want to work with them in GraphQL using `namesLikeThis`, for consistency with GraphQL and JavaScript conventions.

  You can turn on automatic camelCase using the `"auto_camelcase"` plugin configuration setting in `metadata.json`, like this:

  ```json
  {
      "plugins": {
          "datasette-graphql": {
              "auto_camelcase": true
          }
      }
  }
  ```

  ### CORS

  This plugin obeys the `--cors` option passed to the `datasette` command-line tool. If you pass `--cors` it adds the following CORS HTTP headers to allow JavaScript running on other domains to access the GraphQL API:

      access-control-allow-headers: content-type
      access-control-allow-method: POST
      access-control-allow-origin: *

  ### Execution limits

  The plugin implements two limits by default:

  - The total time spent executing all of the underlying SQL queries that make up the GraphQL execution must not exceed 1000ms (one second)
  - The total number of SQL table queries executed as a result of nested GraphQL fields must not exceed 100

  These limits can be customized using the `num_queries_limit` and `time_limit_ms` plugin configuration settings, for example in `metadata.json`:

  ```json
  {
      "plugins": {
          "datasette-graphql": {
              "num_queries_limit": 200,
              "time_limit_ms": 5000
          }
      }
  }
  ```
  Setting these to `0` will disable the limit checks entirely.

  ## The graphql() template function

  The plugin also makes a Jinja template function available called `graphql()`. You can use that function in your Datasette [custom templates](https://docs.datasette.io/en/stable/custom_templates.html#custom-templates) like so:

  ```html+jinja
  {% set users = graphql("""
  {
      users {
          nodes {
              name
              points
              score
          }
      }
  }
  """)["users"] %}
  {% for user in users.nodes %}
      <p>{{ user.name }} - points: {{ user.points }}, score = {{ user.score }}</p>
  {% endfor %}
  ```

  The function executes a GraphQL query against the generated schema and returns the results. You can assign those results to a variable in your template and then loop through and display them.

  By default the query will be run against the first attached database. You can use the optional second argument to the function to specify a different database - for example, to run against an attached `github.db` database you would do this:

  ```html+jinja
  {% set user = graphql("""
  {
      users_row(id:9599) {
          name
          login
          avatar_url
      }
  }
  """, "github")["users_row"] %}

  <h1>Hello, {{ user.name }}</h1>
  ```

  You can use [GraphQL variables](https://graphql.org/learn/queries/#variables) in these template calls by passing them to the `variables=` argument:

  ```html+jinja
  {% set user = graphql("""
  query ($id: Int) {
      users_row(id: $id) {
          name
          login
          avatar_url
      }
  }
  """, database="github", variables={"id": 9599})["users_row"] %}

  <h1>Hello, {{ user.name }}</h1>
  ```
  ## Adding custom fields with plugins

  `datasette-graphql` adds a new [plugin hook](https://docs.datasette.io/en/stable/writing_plugins.html) to Datasette which can be used to add custom fields to your GraphQL schema.

  The plugin hook looks like this:

  ```python
  @hookimpl
  def graphql_extra_fields(datasette, database):
      "A list of (name, field_type) tuples to include in the GraphQL schema"
  ```

  You can use this hook to return a list of tuples describing additional fields that should be exposed in your schema. Each tuple should consist of a string naming the new field, plus a [Graphene Field object](https://docs.graphene-python.org/en/latest/types/objecttypes/) that specifies the schema and provides a `resolver` function.

  This example implementation uses `pkg_resources` to return a list of currently installed Python packages:

  ```python
  import graphene
  from datasette import hookimpl
  import pkg_resources


  @hookimpl
  def graphql_extra_fields():
      class Package(graphene.ObjectType):
          "An installed package"
          name = graphene.String()
          version = graphene.String()

      def resolve_packages(root, info):
          return [
              {"name": d.project_name, "version": d.version}
              for d in pkg_resources.working_set
          ]

      return [
          (
              "packages",
              graphene.Field(
                  graphene.List(Package),
                  description="List of installed packages",
                  resolver=resolve_packages,
              ),
          ),
      ]
  ```

  With this plugin installed, the following GraphQL query can be used to retrieve a list of installed packages:

  ```graphql
  {
    packages {
      name
      version
    }
  }
  ```

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:

      cd datasette-graphql
      python3 -mvenv venv
      source venv/bin/activate

  Or if you are using `pipenv`:

      pipenv shell

  Now install the dependencies and tests:

      pip install -e '.[test]'

  To run the tests:

      pytest

project_urls:
  {"CI": "https://github.com/simonw/datasette-graphql/actions", "Changelog": "https://github.com/simonw/datasette-graphql/releases", "Homepage": "https://github.com/simonw/datasette-graphql", "Issues": "https://github.com/simonw/datasette-graphql/issues"}

requires_dist:
  ["datasette (>=0.58.1)", "graphene (<4.0,>=3.1.0)", "graphql-core (>=3.2.1)", "sqlite-utils", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'"]


name: datasette-gunicorn
summary: Run a Datasette server using Gunicorn
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-gunicorn
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-gunicorn/
project_url: https://pypi.org/project/datasette-gunicorn/
release_url: https://pypi.org/project/datasette-gunicorn/0.1.1/
requires_python: >=3.7
version: 0.1.1
description:
  # datasette-gunicorn

  [![PyPI](https://img.shields.io/pypi/v/datasette-gunicorn.svg)](https://pypi.org/project/datasette-gunicorn/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-gunicorn?include_prereleases&label=changelog)](https://github.com/simonw/datasette-gunicorn/releases)
  [![Tests](https://github.com/simonw/datasette-gunicorn/workflows/Test/badge.svg)](https://github.com/simonw/datasette-gunicorn/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-gunicorn/blob/main/LICENSE)

  Run a [Datasette](https://datasette.io/) server using [Gunicorn](https://gunicorn.org/)

  ## Installation

  Install this plugin in the same environment as Datasette.

      datasette install datasette-gunicorn

  ## Usage

  The plugin adds a new `datasette gunicorn` command. This takes most of the same options as `datasette serve`, plus one more option for setting the number of Gunicorn workers to start:

  `-w/--workers X` - set the number of workers. Defaults to 1.

  To start serving a database using 4 workers, run the following:

      datasette gunicorn fixtures.db -w 4

  It is advisable to switch your datasette [into WAL mode](https://til.simonwillison.net/sqlite/enabling-wal-mode) to get the best performance out of this configuration:

      sqlite3 fixtures.db 'PRAGMA journal_mode=WAL;'

  Run `datasette gunicorn --help` for a full list of options (which are the same as `datasette serve --help`, with the addition of the new `-w` option).

  ## datasette gunicorn --help

  Not all of the options to `datasette serve` are supported. Here's the full list of available options:

  <!-- [[[cog
  import cog
  from datasette import cli
  from click.testing import CliRunner
  runner = CliRunner()
  result = runner.invoke(cli.cli, ["gunicorn", "--help"])
  help = result.output.replace("Usage: cli", "Usage: datasette")
  cog.out(
      "```\n{}\n```".format(help)
  )
  ]]] -->
  ```
  Usage: datasette gunicorn [OPTIONS] [FILES]...

    Start a Gunicorn server running to serve Datasette

  Options:
    -i, --immutable PATH      Database files to open in immutable mode
    -h, --host TEXT           Host for server. Defaults to 127.0.0.1 which means
                              only connections from the local machine will be
                              allowed. Use 0.0.0.0 to listen to all IPs and allow
                              access from other machines.
    -p, --port INTEGER RANGE  Port for server, defaults to 8001. Use -p 0 to
                              automatically assign an available port.
                              [0<=x<=65535]
    --cors                    Enable CORS by serving Access-Control-Allow-Origin:
                              *
    --load-extension TEXT     Path to a SQLite extension to load
    --inspect-file TEXT       Path to JSON file created using "datasette inspect"
    -m, --metadata FILENAME   Path to JSON/YAML file containing license/source
                              metadata
    --template-dir DIRECTORY  Path to directory containing custom templates
    --plugins-dir DIRECTORY   Path to directory containing custom plugins
    --static MOUNT:DIRECTORY  Serve static files from this directory at /MOUNT/...
    --memory                  Make /_memory database available
    --config CONFIG           Deprecated: set config option using
                              configname:value. Use --setting instead.
    --setting SETTING...      Setting, see
                              docs.datasette.io/en/stable/settings.html
    --secret TEXT             Secret used for signing secure values, such as
                              signed cookies
    --version-note TEXT       Additional note to show on /-/versions
    --help-settings           Show available settings
    --create                  Create database files if they do not exist
    --crossdb                 Enable cross-database joins using the /_memory
                              database
    --nolock                  Ignore locking, open locked files in read-only mode
    -w, --workers INTEGER     Number of Gunicorn workers  [default: 1]
    --help                    Show this message and exit.

  ```
  <!-- [[[end]]] -->

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:

      cd datasette-gunicorn
      python3 -m venv venv
      source venv/bin/activate

  Now install the dependencies and test dependencies:

      pip install -e '.[test]'

  To run the tests:

      pytest

project_urls:
  {"CI": "https://github.com/simonw/datasette-gunicorn/actions", "Changelog": "https://github.com/simonw/datasette-gunicorn/releases", "Homepage": "https://github.com/simonw/datasette-gunicorn", "Issues": "https://github.com/simonw/datasette-gunicorn/issues"}

requires_dist:
  ["datasette (>0.63.2)", "gunicorn", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'", "cogapp ; extra == 'test'"]


name: datasette-gzip
summary: Add gzip compression to Datasette
classifiers: ["Framework :: Datasette"]
author: Simon Willison
description_content_type: text/markdown
license_expression: Apache-2.0
package_url: https://pypi.org/project/datasette-gzip/
project_url: https://pypi.org/project/datasette-gzip/
release_url: https://pypi.org/project/datasette-gzip/0.3/
requires_dist: ["datasette", "asgi-gzip>=0.3"]
requires_python: >=3.10
version: 0.3
description:
  # datasette-gzip

  [![PyPI](https://img.shields.io/pypi/v/datasette-gzip.svg)](https://pypi.org/project/datasette-gzip/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-gzip?include_prereleases&label=changelog)](https://github.com/simonw/datasette-gzip/releases)
  [![Tests](https://github.com/simonw/datasette-gzip/workflows/Test/badge.svg)](https://github.com/simonw/datasette-gzip/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-gzip/blob/main/LICENSE)

  Add gzip compression to Datasette

  ## Installation

  Install this plugin in the same environment as Datasette.

      datasette install datasette-gzip

  ## Usage

  Once installed, Datasette will obey the `Accept-Encoding:` header sent by browsers or other user agents and return content compressed in the most appropriate way.

  This plugin is a thin wrapper for the [asgi-gzip library](https://github.com/simonw/asgi-gzip), which extracts the [GzipMiddleware](https://www.starlette.io/middleware/#gzipmiddleware) from Starlette.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:

      cd datasette-gzip
      python3 -mvenv venv
      source venv/bin/activate

  Now install the dependencies and test dependencies:

      pip install -e '.[test]'

  To run the tests:

      pytest

project_urls:
  {"CI": "https://github.com/simonw/datasette-gzip/actions", "Changelog": "https://github.com/simonw/datasette-gzip/releases", "Homepage": "https://github.com/simonw/datasette-gzip", "Issues": "https://github.com/simonw/datasette-gzip/issues"}


name: datasette-hashed-urls
summary: Optimize Datasette performance behind a caching proxy
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-hashed-urls
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-hashed-urls/
project_url: https://pypi.org/project/datasette-hashed-urls/
release_url: https://pypi.org/project/datasette-hashed-urls/0.4/
requires_python: >=3.7
version: 0.4
description:
  # datasette-hashed-urls

  [![PyPI](https://img.shields.io/pypi/v/datasette-hashed-urls.svg)](https://pypi.org/project/datasette-hashed-urls/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-hashed-urls?include_prereleases&label=changelog)](https://github.com/simonw/datasette-hashed-urls/releases)
  [![Tests](https://github.com/simonw/datasette-hashed-urls/workflows/Test/badge.svg)](https://github.com/simonw/datasette-hashed-urls/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-hashed-urls/blob/main/LICENSE)

  Optimize Datasette performance behind a caching proxy

  When you open a database file in immutable mode using the `-i` option, Datasette calculates a SHA-256 hash of the contents of that file on startup.

  This content hash can then optionally be used to create URLs that are guaranteed to change if the contents of the file changes in the future.

  The result is pages  that can be cached indefinitely by both browsers and caching proxies - providing a significant performance boost.

  ## Demo

  A demo of this plugin is running at https://datasette-hashed-urls.vercel.app/

  ## Installation

  Install this plugin in the same environment as Datasette.

      $ datasette install datasette-hashed-urls

  ## Usage

  Once installed, this plugin will act on any immutable database files that are loaded into Datasette:

      datasette -i fixtures.db

  The database will automatically be renamed to incorporate a hash of the contents of the SQLite file - so the above database would be served as:

      http://127.0.0.1:8001/fixtures-aa7318b

  Every page that accesss that database, including JSON endpoints, will be served with the following far-future cache expiry header:

      cache-control: max-age=31536000, public

  Here `max-age=31536000` is the number of seconds in a year.

  A caching proxy such as Cloudflare can then be used to cache and accelerate content served by Datasette.

  When the database file is updated and the server is restarted, the hash will change and content will be served from a new URL. Any hits to the previous hashed URLs will be automatically redirected.

  If you run Datasette using the `--crossdb` option to enable [cross-database queries](https://docs.datasette.io/en/stable/sql_queries.html#cross-database-queries) the `_memory` database will also have a hash added to its URL - in this case, the hash will be a combination of the hashes of the other attached databases.

  ## Configuration

  You can use the `max_age` plugin configuration setting to change the cache duration specified in the `cache-control` HTTP header.

  To set the cache expiry time to one hour you would add this to your Datasette `metadata.json` configuration file:

  ```json
  {
      "plugins": {
          "datasette-hashed-urls": {
              "max_age": 3600
          }
      }
  }
  ```

  ## History

  This functionality used to ship as part of Datasette itself, as a feature called [Hashed URL mode](https://docs.datasette.io/en/0.60.2/performance.html#hashed-url-mode).

  That feature has been deprecated and will be removed in Datasette 1.0. This plugin should be used as an alternative.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:

      cd datasette-hashed-urls
      python3 -mvenv venv
      source venv/bin/activate

  Or if you are using `pipenv`:

      pipenv shell

  Now install the dependencies and test dependencies:

      pip install -e '.[test]'

  To run the tests:

      pytest

project_urls:
  {"CI": "https://github.com/simonw/datasette-hashed-urls/actions", "Changelog": "https://github.com/simonw/datasette-hashed-urls/releases", "Homepage": "https://github.com/simonw/datasette-hashed-urls", "Issues": "https://github.com/simonw/datasette-hashed-urls/issues"}

requires_dist:
  ["datasette (>=0.61.1)", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'", "sqlite-utils ; extra == 'test'"]


name: datasette-haversine
summary: Datasette plugin that adds a custom SQL function for haversine distances
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Simon Willison
description_content_type: text/markdown
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-haversine/
project_url: https://pypi.org/project/datasette-haversine/
release_url: https://pypi.org/project/datasette-haversine/0.2.1/
requires_dist: ["datasette", "haversine", "pytest ; extra == 'test'"]
requires_python: >=3.8
version: 0.2.1
description:
  # datasette-haversine

  [![PyPI](https://img.shields.io/pypi/v/datasette-haversine.svg)](https://pypi.org/project/datasette-haversine/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-haversine?include_prereleases&label=changelog)](https://github.com/simonw/datasette-haversine/releases)
  [![Tests](https://github.com/simonw/datasette-haversine/workflows/Test/badge.svg)](https://github.com/simonw/datasette-haversine/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-haversine/blob/main/LICENSE)

  Datasette plugin that adds a custom SQL function for haversine distances

  Install this plugin in the same environment as Datasette to enable the `haversine()` SQL function.
  ```bash
  datasette install datasette-haversine
  ```
  The plugin is built on top of the [haversine](https://github.com/mapado/haversine) library.

  ## haversine() to calculate distances

  ```sql
  select haversine(lat1, lon1, lat2, lon2);
  ```

  This will return the distance in kilometers between the point defined by `(lat1, lon1)` and the point defined by `(lat2, lon2)`.

  ## Demo

  You can try this function [here using Datasette Lite](https://lite.datasette.io/?install=datasette-haversine#/content?sql=select+haversine%28%0A++40.7128%2C+-74.0060%2C+--+New+York%0A++37.7749%2C+-122.4194%2C+--+San+Francisco%0A++%27mi%27%0A%29+as+distance_in_miles%3B).

  ```sql
  select haversine(
    40.7128, -74.0060, -- New York
    37.7749, -122.4194, -- San Francisco
    'mi'
  ) as distance_in_miles;
  ```

  ## Custom units

  By default `haversine()` returns results in km. You can pass an optional third argument to get results in a different unit:

  - `ft` for feet
  - `m` for meters
  - `in` for inches
  - `mi` for miles
  - `nmi` for nautical miles
  - `km` for kilometers (the default)

  ```sql
  select haversine(lat1, lon1, lat2, lon2, 'mi');
  ```

project_urls:
  {"CI": "https://github.com/simonw/datasette-haversine/actions", "Changelog": "https://github.com/simonw/datasette-haversine/releases", "Homepage": "https://github.com/simonw/datasette-haversine", "Issues": "https://github.com/simonw/datasette-haversine/issues"}


name: datasette-hello-world
summary: The hello world of Datasette plugins
classifiers: []
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-hello-world
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-hello-world/
project_url: https://pypi.org/project/datasette-hello-world/
release_url: https://pypi.org/project/datasette-hello-world/0.1.1/
requires_dist: ["datasette", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'"]
requires_python: >=3.6
version: 0.1.1
description:
  <img src="https://datasette.io/static/datasette-logo.svg" alt="Datasette">

  # datasette-hello-world

  [![PyPI](https://img.shields.io/pypi/v/datasette-hello-world.svg)](https://pypi.org/project/datasette-hello-world/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-hello-world?include_prereleases&label=changelog)](https://github.com/simonw/datasette-hello-world/releases)
  [![Tests](https://github.com/simonw/datasette-hello-world/workflows/Test/badge.svg)](https://github.com/simonw/datasette-hello-world/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-hello-world/blob/main/LICENSE)

  The hello world of Datasette plugins

  ## Installation

  Install this plugin in the same environment as Datasette.

      $ datasette install datasette-hello-world

  ## Usage

  Run this to see "Hello world":

      datasette hello-world

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:

      cd datasette-hello-world
      python3 -mvenv venv
      source venv/bin/activate

  Or if you are using `pipenv`:

      pipenv shell

  Now install the dependencies and test dependencies:

      pip install -e '.[test]'

  To run the tests:

      pytest

project_urls:
  {"CI": "https://github.com/simonw/datasette-hello-world/actions", "Changelog": "https://github.com/simonw/datasette-hello-world/releases", "Homepage": "https://github.com/simonw/datasette-hello-world", "Issues": "https://github.com/simonw/datasette-hello-world/issues"}


name: datasette-homepage-table
summary: Show a specific Datasette table on the homepage
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Simon Willison
description_content_type: text/markdown
license: Apache-2.0
package_url: https://pypi.org/project/datasette-homepage-table/
project_url: https://pypi.org/project/datasette-homepage-table/
release_url: https://pypi.org/project/datasette-homepage-table/0.2/
requires_python: >=3.8
version: 0.2
description:
  # datasette-homepage-table

  [![PyPI](https://img.shields.io/pypi/v/datasette-homepage-table.svg)](https://pypi.org/project/datasette-homepage-table/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-homepage-table?include_prereleases&label=changelog)](https://github.com/datasette/datasette-homepage-table/releases)
  [![Tests](https://github.com/datasette/datasette-homepage-table/actions/workflows/test.yml/badge.svg)](https://github.com/datasette/datasette-homepage-table/actions/workflows/test.yml)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-homepage-table/blob/main/LICENSE)

  Show a specific Datasette table on the homepage

  ## Installation

  Install this plugin in the same environment as Datasette.
  ```bash
  datasette install datasette-homepage-table
  ```
  ## Usage

  This plugin changes the Datasette homepage to display a table instead of the default list of databases.

  Without configuration, it will take the first table in the first attached database.

  Alternatively you can configure it like this:

  ```json
  {
      "plugins": {
          "datasette-homepage-table": {
              "database": "mydatabase",
              "table": "mytable"
          }
      }
  }
  ```
  Both keys are optional: if you omit `database` it will use the first attached database, and if you omit `table` it will use the first table in the database.

  You can customize the `table.html` template to [change the appearance of the table](https://docs.datasette.io/en/stable/custom_templates.html#custom-templates).

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:
  ```bash
  cd datasette-homepage-table
  python3 -m venv venv
  source venv/bin/activate
  ```
  Now install the dependencies and test dependencies:
  ```bash
  pip install -e '.[test]'
  ```
  To run the tests:
  ```bash
  pytest
  ```

project_urls:
  {"CI": "https://github.com/datasette/datasette-homepage-table/actions", "Changelog": "https://github.com/datasette/datasette-homepage-table/releases", "Homepage": "https://github.com/datasette/datasette-homepage-table", "Issues": "https://github.com/datasette/datasette-homepage-table/issues"}

requires_dist:
  ["datasette", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'", "datasette-test ; extra == 'test'"]


name: datasette-hovercards
summary: Add preview hovercards to links in Datasette
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-hovercards
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-hovercards/
project_url: https://pypi.org/project/datasette-hovercards/
release_url: https://pypi.org/project/datasette-hovercards/0.1a0/
requires_dist: ["datasette", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'"]
requires_python: >=3.6
version: 0.1a0
description:
  # datasette-hovercards

  [![PyPI](https://img.shields.io/pypi/v/datasette-hovercards.svg)](https://pypi.org/project/datasette-hovercards/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-hovercards?include_prereleases&label=changelog)](https://github.com/simonw/datasette-hovercards/releases)
  [![Tests](https://github.com/simonw/datasette-hovercards/workflows/Test/badge.svg)](https://github.com/simonw/datasette-hovercards/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-hovercards/blob/main/LICENSE)

  Add preview hovercards to links in Datasette

  ## Installation

  Install this plugin in the same environment as Datasette.

      $ datasette install datasette-hovercards

  ## Usage

  Once installed, hovering over a link to a row within the Datasette interface - for example a foreign key reference on the table page - should show a hovercard with a preview of that row.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:

      cd datasette-hovercards
      python3 -mvenv venv
      source venv/bin/activate

  Or if you are using `pipenv`:

      pipenv shell

  Now install the dependencies and test dependencies:

      pip install -e '.[test]'

  To run the tests:

      pytest

project_urls:
  {"CI": "https://github.com/simonw/datasette-hovercards/actions", "Changelog": "https://github.com/simonw/datasette-hovercards/releases", "Homepage": "https://github.com/simonw/datasette-hovercards", "Issues": "https://github.com/simonw/datasette-hovercards/issues"}


name: datasette-ics
summary: Datasette plugin for outputting iCalendar files
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-ics
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-ics/
project_url: https://pypi.org/project/datasette-ics/
release_url: https://pypi.org/project/datasette-ics/0.5.2/
version: 0.5.2
description:
  # datasette-ics

  [![PyPI](https://img.shields.io/pypi/v/datasette-ics.svg)](https://pypi.org/project/datasette-ics/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-ics?include_prereleases&label=changelog)](https://github.com/simonw/datasette-ics/releases)
  [![Tests](https://github.com/simonw/datasette-ics/workflows/Test/badge.svg)](https://github.com/simonw/datasette-ics/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-ics/blob/main/LICENSE)

  Datasette plugin that adds support for generating [iCalendar .ics files](https://tools.ietf.org/html/rfc5545) with the results of a SQL query.

  ## Installation

  Install this plugin in the same environment as Datasette to enable the `.ics` output extension.

      $ pip install datasette-ics

  ## Usage

  To create an iCalendar file you need to define a custom SQL query that returns a required set of columns:

  * `event_name` - the short name for the event
  * `event_dtstart` - when the event starts

  The following columns are optional:

  * `event_dtend` - when the event ends
  * `event_duration` - the duration of the event (use instead of `dtend`)
  * `event_description` - a longer description of the event
  * `event_uid` - a globally unique identifier for this event
  * `event_tzid` - the timezone for the event, e.g. `America/Chicago`

  A query that returns these columns can then be returned as an ics feed by adding the `.ics` extension.

  ## Demo

  [This SQL query]([https://www.rockybeaches.com/data?sql=with+inner+as+(%0D%0A++select%0D%0A++++datetime%2C%0D%0A++++substr(datetime%2C+0%2C+11)+as+date%2C%0D%0A++++mllw_feet%2C%0D%0A++++lag(mllw_feet)+over+win+as+previous_mllw_feet%2C%0D%0A++++lead(mllw_feet)+over+win+as+next_mllw_feet%0D%0A++from%0D%0A++++tide_predictions%0D%0A++where%0D%0A++++station_id+%3D+%3Astation_id%0D%0A++++and+datetime+%3E%3D+date()%0D%0A++++window+win+as+(%0D%0A++++++order+by%0D%0A++++++++datetime%0D%0A++++)%0D%0A++order+by%0D%0A++++datetime%0D%0A)%2C%0D%0Alowest_tide_per_day+as+(%0D%0A++select%0D%0A++++date%2C%0D%0A++++datetime%2C%0D%0A++++mllw_feet%0D%0A++from%0D%0A++++inner%0D%0A++where%0D%0A++++mllw_feet+%3C%3D+previous_mllw_feet%0D%0A++++and+mllw_feet+%3C%3D+next_mllw_feet%0D%0A)%0D%0Aselect%0D%0A++min(datetime)+as+event_dtstart%2C%0D%0A++%27Low+tide%3A+%27+||+mllw_feet+||+%27+feet%27+as+event_name%2C%0D%0A++%27America%2FLos_Angeles%27+as+event_tzid%0D%0Afrom%0D%0A++lowest_tide_per_day%0D%0Agroup+by%0D%0A++date%0D%0Aorder+by%0D%0A++date&station_id=9414131) calculates the lowest tide per day at Pillar Point in Half Moon Bay, California.

  Since the query returns `event_name`, `event_dtstart` and `event_tzid` columns it produces [this ICS feed](https://www.rockybeaches.com/data.ics?sql=with+inner+as+(%0D%0A++select%0D%0A++++datetime%2C%0D%0A++++substr(datetime%2C+0%2C+11)+as+date%2C%0D%0A++++mllw_feet%2C%0D%0A++++lag(mllw_feet)+over+win+as+previous_mllw_feet%2C%0D%0A++++lead(mllw_feet)+over+win+as+next_mllw_feet%0D%0A++from%0D%0A++++tide_predictions%0D%0A++where%0D%0A++++station_id+%3D+%3Astation_id%0D%0A++++and+datetime+%3E%3D+date()%0D%0A++++window+win+as+(%0D%0A++++++order+by%0D%0A++++++++datetime%0D%0A++++)%0D%0A++order+by%0D%0A++++datetime%0D%0A)%2C%0D%0Alowest_tide_per_day+as+(%0D%0A++select%0D%0A++++date%2C%0D%0A++++datetime%2C%0D%0A++++mllw_feet%0D%0A++from%0D%0A++++inner%0D%0A++where%0D%0A++++mllw_feet+%3C%3D+previous_mllw_feet%0D%0A++++and+mllw_feet+%3C%3D+next_mllw_feet%0D%0A)%0D%0Aselect%0D%0A++min(datetime)+as+event_dtstart%2C%0D%0A++%27Low+tide%3A+%27+||+mllw_feet+||+%27+feet%27+as+event_name%2C%0D%0A++%27America%2FLos_Angeles%27+as+event_tzid%0D%0Afrom%0D%0A++lowest_tide_per_day%0D%0Agroup+by%0D%0A++date%0D%0Aorder+by%0D%0A++date&station_id=9414131). If you subscribe to that in a calendar application such as Apple Calendar you get something that looks like this:

  ![Apple Calendar showing low tides at Pillar Point during a week](https://user-images.githubusercontent.com/9599/173158984-e5ec6bd0-33fc-4fc0-ba9d-17ae674f310a.jpg)

  ## Using a canned query

  Datasette's [canned query mechanism](https://datasette.readthedocs.io/en/stable/sql_queries.html#canned-queries) can be used to configure calendars. If a canned query definition has a `title` that will be used as the title of the calendar.

  Here's an example, defined using a `metadata.yaml` file:

  ```yaml
  databases:
    mydatabase:
      queries:
        calendar:
          title: My Calendar
          sql: |-
            select
              title as event_name,
              start as event_dtstart,
              description as event_description
            from
              events
            order by
              start
            limit
              100
  ```
  This will result in a calendar feed at `http://localhost:8001/mydatabase/calendar.ics`

project_urls:
  {"CI": "https://github.com/simonw/datasette-ics/actions", "Changelog": "https://github.com/simonw/datasette-ics/releases", "Homepage": "https://github.com/simonw/datasette-ics", "Issues": "https://github.com/simonw/datasette-ics/issues"}

requires_dist:
  ["datasette (>=0.49)", "ics (==0.7.2)", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'"]


name: datasette-import-table
summary: Datasette plugin for importing tables from other Datasette instances
classifiers: []
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-import-table
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-import-table/
project_url: https://pypi.org/project/datasette-import-table/
release_url: https://pypi.org/project/datasette-import-table/0.3/
version: 0.3
description:
  # datasette-import-table

  [![PyPI](https://img.shields.io/pypi/v/datasette-import-table.svg)](https://pypi.org/project/datasette-import-table/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-import-table?include_prereleases&label=changelog)](https://github.com/simonw/datasette-import-table/releases)
  [![Tests](https://github.com/simonw/datasette-import-table/workflows/Test/badge.svg)](https://github.com/simonw/datasette-import-table/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-import-table/blob/main/LICENSE)

  Datasette plugin for importing tables from other Datasette instances

  ## Installation

  Install this plugin in the same environment as Datasette.

      $ datasette install datasette-import-table

  ## Usage

  Visit `/-/import-table` for the interface. Paste in the URL to a table page on another Datasette instance and click the button to import that table.

  By default only [the root actor](https://datasette.readthedocs.io/en/stable/authentication.html#using-the-root-actor) can access the page - so you'll need to run Datasette with the `--root` option and click on the link shown in the terminal to sign in and access the page.

  The `import-table` permission governs access. You can use permission plugins such as [datasette-permissions-sql](https://github.com/simonw/datasette-permissions-sql) to grant additional access to the write interface.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:

      cd datasette-import-table
      python3 -mvenv venv
      source venv/bin/activate

  Or if you are using `pipenv`:

      pipenv shell

  Now install the dependencies and tests:

      pip install -e '.[test]'

  To run the tests:

      pytest

project_urls:
  {"CI": "https://github.com/simonw/datasette-import-table/actions", "Changelog": "https://github.com/simonw/datasette-import-table/releases", "Homepage": "https://github.com/simonw/datasette-import-table", "Issues": "https://github.com/simonw/datasette-import-table/issues"}

requires_dist:
  ["datasette", "httpx", "sqlite-utils", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'", "httpx ; extra == 'test'", "pytest-httpx ; extra == 'test'"]


name: datasette-import
summary: Tools for importing data into Datasette
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Datasette
description_content_type: text/markdown
dynamic: ["License-File"]
license: Apache-2.0
license_files: ["LICENSE"]
package_url: https://pypi.org/project/datasette-import/
project_url: https://pypi.org/project/datasette-import/
provides_extra: ["test"]
release_url: https://pypi.org/project/datasette-import/0.1a6/
requires_dist: ["datasette==1.0a19", "pytest; extra == \"test\"", "pytest-asyncio; extra == \"test\""]
requires_python: >=3.8
version: 0.1a6
description:
  # datasette-import

  [![PyPI](https://img.shields.io/pypi/v/datasette-import.svg)](https://pypi.org/project/datasette-import/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-import?include_prereleases&label=changelog)](https://github.com/datasette/datasette-import/releases)
  [![Tests](https://github.com/datasette/datasette-import/actions/workflows/test.yml/badge.svg)](https://github.com/datasette/datasette-import/actions/workflows/test.yml)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-import/blob/main/LICENSE)

  Tools for importing data into Datasette

  ## Installation

  Install this plugin in the same environment as Datasette.
  ```bash
  datasette install datasette-import
  ```
  ## Usage

  This plugin adds a database action item called "Create table with imported data".

  This action is available to users with the `create-table` permission.

  It links to a page that allows users to upload files or paste in CSV, TSV or JSON data, and then use that to create and populate a new table in Datasette.

  CSV and TSV data must have headers on the first row.

  JSON data must be an array of objects with the same keys, or a container object object where one of the keys is an array of objects.

  ## Credits

  The CSV and TSV parsing is performed using [Papa Parse](https://www.papaparse.com/), an MIT licensed JavaScript library that is bundled with this plugin.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:
  ```bash
  cd datasette-import
  python3 -m venv venv
  source venv/bin/activate
  ```
  Now install the dependencies and test dependencies:
  ```bash
  pip install -e '.[test]'
  ```
  To run the tests:
  ```bash
  pytest
  ```

project_urls:
  {"CI": "https://github.com/datasette/datasette-import/actions", "Changelog": "https://github.com/datasette/datasette-import/releases", "Homepage": "https://github.com/datasette/datasette-import", "Issues": "https://github.com/datasette/datasette-import/issues"}


name: datasette-indieauth
summary: Datasette authentication using IndieAuth and RelMeAuth
classifiers: []
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-indieauth
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-indieauth/
project_url: https://pypi.org/project/datasette-indieauth/
release_url: https://pypi.org/project/datasette-indieauth/1.2.2/
requires_python: >=3.7
version: 1.2.2
description:
  # datasette-indieauth

  [![PyPI](https://img.shields.io/pypi/v/datasette-indieauth.svg)](https://pypi.org/project/datasette-indieauth/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-indieauth?include_prereleases&label=changelog)](https://github.com/simonw/datasette-indieauth/releases)
  [![codecov](https://codecov.io/gh/simonw/datasette-indieauth/branch/main/graph/badge.svg)](https://codecov.io/gh/simonw/datasette-indieauth)
  [![Tests](https://github.com/simonw/datasette-indieauth/workflows/Test/badge.svg)](https://github.com/simonw/datasette-indieauth/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-indieauth/blob/main/LICENSE)

  Datasette authentication using [IndieAuth](https://indieauth.net/).

  ## Demo

  You can try out the latest version of this plugin at [datasette-indieauth-demo.datasette.io](https://datasette-indieauth-demo.datasette.io/-/indieauth)

  ## Installation

  Install this plugin in the same environment as Datasette.

      $ datasette install datasette-indieauth

  ## Usage

  Ensure you have a website with a domain that supports IndieAuth or RelMeAuth. The easiest way to do that is to add the following HTML to your homepage, linking to your personal GitHub profile:

  ```html
  <link href="https://github.com/simonw" rel="me">
  <link rel="authorization_endpoint" href="https://indieauth.com/auth">
  ```
  Your GitHub profile needs to link back to your website, to prove that your GitHub account should be a valid identifier for that page.

  Now visit `/-/indieauth` on your Datasette instance to begin the sign-in progress.

  ## Actor

  When a user signs in using IndieAuth they will be recieve a signed `ds_actor` cookie identifying them as an [actor](https://docs.datasette.io/en/stable/authentication.html#actors) that looks like this:

  ```json
  {
      "me": "https://simonwillison.net/",
      "display": "simonwillison.net"
  }
  ```

  If the IndieAuth server returned additional `"profile"` fields those will be merged into the actor. You can visit `/-/actor` on your Datasette instance to see the full actor you are currently signed in as.

  ## Restricting access with the restrict_access plugin configuration

  You can use [Datasette's permissions system](https://docs.datasette.io/en/stable/authentication.html#permissions) to control permissions of authenticated users - by default, an authenticated user will be able to perform the same actions as an unauthenticated user.

  As a shortcut if you want to lock down access to your instance entirely to just specific users, you can use the `restrict_access` plugin configuration option like this:

  ```json
  {
      "plugins": {
          "datasette-indieauth": {
              "restrict_access": "https://simonwillison.net/"
          }
      }
  }
  ```

  This can be a string or a list of user identifiers. It can also be a space separated list, which means you can use it with the [datasette publish](https://docs.datasette.io/en/stable/publish.html#datasette-publish) `--plugin-secret` configuration option to set permissions as part of a deployment, like this:
  ```
  datasette publish vercel mydb.db --project my-secret-db \
      --install datasette-indieauth \
      --plugin-secret datasette-indieauth restrict_access https://simonwillison.net/
  ```
  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:

      cd datasette-indieauth
      python3 -mvenv venv
      source venv/bin/activate

  Or if you are using `pipenv`:

      pipenv shell

  Now install the dependencies and tests:

      pip install -e '.[test]'

  To run the tests:

      pytest

project_urls:
  {"CI": "https://github.com/simonw/datasette-indieauth/actions", "Changelog": "https://github.com/simonw/datasette-indieauth/releases", "Homepage": "https://github.com/simonw/datasette-indieauth", "Issues": "https://github.com/simonw/datasette-indieauth/issues"}

requires_dist:
  ["datasette", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'", "httpx ; extra == 'test'", "pytest-httpx ; extra == 'test'", "mf2py ; extra == 'test'"]


name: datasette-init
summary: Ensure specific tables and views exist on startup
classifiers: []
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-init
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-init/
project_url: https://pypi.org/project/datasette-init/
release_url: https://pypi.org/project/datasette-init/0.2/
version: 0.2
description:
  # datasette-init

  [![PyPI](https://img.shields.io/pypi/v/datasette-init.svg)](https://pypi.org/project/datasette-init/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-init?label=changelog)](https://github.com/simonw/datasette-init/releases)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-init/blob/master/LICENSE)

  Ensure specific tables and views exist on startup

  ## Installation

  Install this plugin in the same environment as Datasette.

      $ pip install datasette-init

  ## Usage

  This plugin is configured using `metadata.json` (or `metadata.yaml`). 

  ### Creating tables

  Add a block like this that specifies the tables you would like to ensure exist:

  ```json
  {
    "plugins": {
      "datasette-init": {
        "my_database": {
          "tables": {
            "dogs": {
              "columns": {
                "id": "integer",
                "name": "text",
                "age": "integer",
                "weight": "float"
              },
              "pk": "id"
            }
          }
        }
      }
    }
  }
  ```

  Any tables that do not yet exist will be created when Datasette first starts.

  Valid column types are `"integer"`, `"text"`, `"float"` and `"blob"`.

  The `"pk"` is optional, and is used to define the primary key. To define a compound primary key (across more than one column) use a list of column names here:
  ```json
      "pk": ["id1", "id2"]
  ```

  ### Creating views

  The plugin can also be used to create views:

  ```json
  {
    "plugins": {
      "datasette-init": {
        "my_database": {
          "views": {
            "my_view": "select 1 + 1"
          }
        }
      }
    }
  }
  ```

  Each view in the ``"views"`` block will be created when the Database first starts. If a view with the same name already exists it will be replaced with the new definition.


  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:

      cd datasette-init
      python3 -mvenv venv
      source venv/bin/activate

  Or if you are using `pipenv`:

      pipenv shell

  Now install the dependencies and tests:

      pip install -e '.[test]'

  To run the tests:

      pytest

project_urls:
  {"CI": "https://github.com/simonw/datasette-init/actions", "Changelog": "https://github.com/simonw/datasette-init/releases", "Homepage": "https://github.com/simonw/datasette-init", "Issues": "https://github.com/simonw/datasette-init/issues"}

requires_dist:
  ["datasette (>=0.45)", "sqlite-utils", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'", "httpx ; extra == 'test'"]


name: datasette-insert-unsafe
summary: Unsafe permissions for datasette-insert - allows all actions without authentication
classifiers: []
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-insert-unsafe
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-insert-unsafe/
project_url: https://pypi.org/project/datasette-insert-unsafe/
release_url: https://pypi.org/project/datasette-insert-unsafe/0.1/
version: 0.1
description:
  # datasette-insert-unsafe

  [![PyPI](https://img.shields.io/pypi/v/datasette-insert-unsafe.svg)](https://pypi.org/project/datasette-insert-unsafe/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-insert-unsafe?include_prereleases&label=changelog)](https://github.com/simonw/datasette-insert-unsafe/releases)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-insert-unsafe/blob/master/LICENSE)

  Unsafe permissions for datasette-insert - allows all actions without authentication

  ## Installation

  Install this plugin in the same environment as Datasette.

      $ pip install datasette-insert-unsafe

  ## Usage

  Once installed, all actions performed using [datasette-insert](https://github.com/simonw/datasette-insert) will be allowed without authentication.

  This is **not safe** if you are running Datasette with `datasette-insert` on the public internet. You should only use this plugin if you are running Datasette on your own local machine, or on a private network.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:

      cd datasette-insert-unsafe
      python3 -mvenv venv
      source venv/bin/activate

  Or if you are using `pipenv`:

      pipenv shell

  Now install the dependencies and tests:

      pip install -e '.[test]'

  To run the tests:

      pytest

project_urls:
  {"CI": "https://github.com/simonw/datasette-insert-unsafe/actions", "Changelog": "https://github.com/simonw/datasette-insert-unsafe/releases", "Homepage": "https://github.com/simonw/datasette-insert-unsafe", "Issues": "https://github.com/simonw/datasette-insert-unsafe/issues"}

requires_dist:
  ["datasette", "datasette-insert (>=0.6)", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'", "httpx ; extra == 'test'", "sqlite-utils ; extra == 'test'"]


name: datasette-insert
summary: Datasette plugin for inserting and updating data
classifiers: []
author: Simon Willison
description_content_type: text/markdown
home_page: https://datasette.io/plugins/datasette-insert
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-insert/
project_url: https://pypi.org/project/datasette-insert/
release_url: https://pypi.org/project/datasette-insert/0.8/
requires_python: >=3.7
version: 0.8
description:
  # datasette-insert

  [![PyPI](https://img.shields.io/pypi/v/datasette-insert.svg)](https://pypi.org/project/datasette-insert/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-insert?include_prereleases&label=changelog)](https://github.com/simonw/datasette-insert/releases)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-insert/blob/master/LICENSE)

  Datasette plugin for inserting and updating data

  ## Installation

  Install this plugin in the same environment as Datasette.

      $ pip install datasette-insert

  This plugin should always be deployed with additional configuration to prevent unauthenticated access, see notes below.

  If you are trying it out on your own local machine, you can `pip install` the [datasette-insert-unsafe](https://github.com/simonw/datasette-insert-unsafe) plugin to allow access without needing to set up authentication or permissions separately.

  ## Inserting data and creating tables

  Start datasette and make sure it has a writable SQLite database attached to it. If you have not yet created a database file you can use this:

      datasette data.db --create

  The `--create` option will create a new empty `data.db` database file if it does not already exist.

  The plugin adds an endpoint that allows data to be inserted or updated and tables to be created by POSTing JSON data to the following URL:

      /-/insert/name-of-database/name-of-table

  The JSON should look like this:

  ```json
  [
      {
          "id": 1,
          "name": "Cleopaws",
          "age": 5
      },
      {
          "id": 2,
          "name": "Pancakes",
          "age": 5
      }
  ]
  ```

  The first time data is posted to the URL a table of that name will be created if it does not aready exist, with the desired columns.

  You can specify which column should be used as the primary key using the `?pk=` URL argument.

  Here's how to POST to a database and create a new table using the Python `requests` library:

  ```python
  import requests

  requests.post("http://localhost:8001/-/insert/data/dogs?pk=id", json=[
      {
          "id": 1,
          "name": "Cleopaws",
          "age": 5
      },
      {
          "id": 2,
          "name": "Pancakes",
          "age": 4
      }
  ])
  ```
  And here's how to do the same thing using `curl`:

  ```
  curl --request POST \
    --data '[
        {
          "id": 1,
          "name": "Cleopaws",
          "age": 5
        },
        {
          "id": 2,
          "name": "Pancakes",
          "age": 4
        }
      ]' \
      'http://localhost:8001/-/insert/data/dogs?pk=id'
  ```
  Or by piping in JSON like so:

      cat dogs.json | curl --request POST -d @- \
          'http://localhost:8001/-/insert/data/dogs?pk=id'

  ### Inserting a single row

  If you are inserting a single row you can optionally send it as a dictionary rather than a list with a single item:

  ```
  curl --request POST \
    --data '{
        "id": 1,
        "name": "Cleopaws",
        "age": 5
      }' \
      'http://localhost:8001/-/insert/data/dogs?pk=id'
  ```

  ### Automatically adding new columns

  If you send data to an existing table with keys that are not reflected by the existing columns, you will get an HTTP 400 error with a JSON response like this:

  ```json
  {
      "status": 400,
      "error": "Unknown keys: 'foo'",
      "error_code": "unknown_keys"
  }
  ```

  If you add `?alter=1` to the URL you are posting to any missing columns will be automatically added:

  ```
  curl --request POST \
    --data '[
        {
          "id": 3,
          "name": "Boris",
          "age": 1,
          "breed": "Husky"
        }
      ]' \
      'http://localhost:8001/-/insert/data/dogs?alter=1'
  ```

  ## Upserting data

  An "upsert" operation can be used to partially update a record. With upserts you can send a subset of the keys and, if the ID matches the specified primary key, they will be used to update an existing record.

  Upserts can be sent to the `/-/upsert` API endpoint.

  This example will update the dog with ID=1's age from 5 to 7:
  ```
  curl --request POST \
    --data '{
        "id": 1,
        "age": 7
      }' \
      'http://localhost:3322/-/upsert/data/dogs?pk=id'
  ```
  Like the `/-/insert` endpoint, the `/-/upsert` endpoint can accept an array of objects too. It also supports the `?alter=1` option.

  ## Permissions and authentication

  This plugin defaults to denying all access, to help ensure people don't accidentally deploy it on the open internet in an unsafe configuration.

  You can read about [Datasette's approach to authentication](https://datasette.readthedocs.io/en/stable/authentication.html) in the Datasette manual.

  You can install the `datasette-insert-unsafe` plugin to run in unsafe mode, where all access is allowed by default.

  I recommend using this plugin in conjunction with [datasette-auth-tokens](https://github.com/simonw/datasette-auth-tokens), which provides a mechanism for making authenticated calls using API tokens.

  You can then use ["allow" blocks](https://datasette.readthedocs.io/en/stable/authentication.html#defining-permissions-with-allow-blocks) in the `datasette-insert` plugin configuration to specify which authenticated tokens are allowed to make use of the API.

  Here's an example `metadata.json` file which restricts access to the `/-/insert` API to an API token defined in an `INSERT_TOKEN` environment variable:

  ```json
  {
      "plugins": {
          "datasette-insert": {
              "allow": {
                  "bot": "insert-bot"
              }
          },
          "datasette-auth-tokens": {
              "tokens": [
                  {
                      "token": {
                          "$env": "INSERT_TOKEN"
                      },
                      "actor": {
                          "bot": "insert-bot"
                      }
                  }
              ]
          }
      }
  }
  ```
  With this configuration in place you can start Datasette like this:

      INSERT_TOKEN=abc123 datasette data.db -m metadata.json

  You can now send data to the API using `curl` like this:

  ```
  curl --request POST \
    -H "Authorization: Bearer abc123" \
    --data '[
        {
          "id": 3,
          "name": "Boris",
          "age": 1,
          "breed": "Husky"
        }
      ]' \
      'http://localhost:8001/-/insert/data/dogs'
  ```

  Or using the Python `requests` library like so:

  ```python
  requests.post(
      "http://localhost:8001/-/insert/data/dogs",
      json={"id": 1, "name": "Cleopaws", "age": 5},
      headers={"Authorization": "bearer abc123"},
  )
  ```

  ### Finely grained permissions

  Using an `"allow"` block as described above grants full permission to the features enabled by the API.

  The API implements several new Datasett permissions, which other plugins can use to make more finely grained decisions.

  The full set of permissions are as follows:

  - `insert:all` - all permissions - this is used by the `"allow"` block described above. Argument: `database_name`
  - `insert:insert-update` - the ability to insert data into an existing table, or to update data by its primary key. Arguments: `(database_name, table_name)`
  - `insert:create-table` - the ability to create a new table. Argument: `database_name`
  - `insert:alter-table` - the ability to add columns to an existing table (using `?alter=1`). Arguments: `(database_name, table_name)`

  You can use plugins like [datasette-permissions-sql](https://github.com/simonw/datasette-permissions-sql) to hook into these more detailed permissions for finely grained control over what actions each authenticated actor can take.

  Plugins that implement the [permission_allowed()](https://datasette.readthedocs.io/en/stable/plugin_hooks.html#plugin-hook-permission-allowed) plugin hook can take full control over these permission decisions.

  ## CORS

  If you start Datasette with the `datasette --cors` option the following HTTP headers will be added to resources served by this plugin:

      Access-Control-Allow-Origin: *
      Access-Control-Allow-Headers: content-type,authorization
      Access-Control-Allow-Methods: POST

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:

      cd datasette-insert
      python3 -m venv venv
      source venv/bin/activate

  Now install the dependencies and tests:

      pip install -e '.[test]'

  To run the tests:

      pytest

project_urls:
  {"CI": "https://github.com/simonw/datasette-insert/actions", "Changelog": "https://github.com/simonw/datasette-insert/releases", "Homepage": "https://datasette.io/plugins/datasette-insert", "Issues": "https://github.com/simonw/datasette-insert/issues"}

requires_dist:
  ["datasette (>=0.46)", "sqlite-utils", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'", "httpx ; extra == 'test'", "datasette-auth-tokens ; extra == 'test'"]


name: datasette-ip-rate-limit
summary: Rate limit Datasette requests by client IP address
classifiers: ["Framework :: Datasette"]
author: Simon Willison
description_content_type: text/markdown
dynamic: ["License-File"]
license_expression: Apache-2.0
license_files: ["LICENSE"]
package_url: https://pypi.org/project/datasette-ip-rate-limit/
project_url: https://pypi.org/project/datasette-ip-rate-limit/
release_url: https://pypi.org/project/datasette-ip-rate-limit/0.1a0/
requires_dist: ["datasette"]
requires_python: >=3.10
version: 0.1a0
description:
  # datasette-ip-rate-limit

  [![PyPI](https://img.shields.io/pypi/v/datasette-ip-rate-limit.svg)](https://pypi.org/project/datasette-ip-rate-limit/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-ip-rate-limit?include_prereleases&label=changelog)](https://github.com/datasette/datasette-ip-rate-limit/releases)
  [![Tests](https://github.com/datasette/datasette-ip-rate-limit/actions/workflows/test.yml/badge.svg)](https://github.com/datasette/datasette-ip-rate-limit/actions/workflows/test.yml)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-ip-rate-limit/blob/main/LICENSE)

  Rate limit Datasette requests by client IP address.

  This plugin is designed for Datasette instances that need a lightweight in-process guard against one client sending too many expensive requests.

  ## Installation

  Install this plugin in the same environment as Datasette.

  ```bash
  datasette install datasette-ip-rate-limit
  ```

  ## Configuration

  Configure the plugin in `datasette.yaml`:

  ```yaml
  plugins:
    datasette-ip-rate-limit:
      debug: true
      rules:
      - paths:
        - "/*"
        block_seconds: 20
  ```

  This applies rate limiting to every non-debug path and enables
  `/-/ip-rate-limit-debug` so you can inspect the current in-memory state.

  For a more targeted configuration:

  ```yaml
  plugins:
    datasette-ip-rate-limit:
      max_keys: 10000
      exempt_paths:
      - "/static/*"
      - "/-/turnstile*"
      rules:
      - name: expensive-table-pages
        paths:
        - "/global-power-plants/*"
        - "/legislators/*"
        query_params_min: 2
        window_seconds: 60
        max_requests: 120
        block_seconds: 600
  ```

  This allows each IP address to make 120 matching requests per 60 seconds. If
  an IP exceeds that rate, it receives `429 Too Many Requests` responses for 600
  seconds with a `Retry-After` header.

  The example only applies to matching table pages with at least two query string parameters, so ordinary page views are left alone.

  By default, the client IP address is read from the ASGI connection. If
  Datasette is running behind a trusted proxy, set `header` to the request header
  that proxy uses for the client IP address.

  ## Options

  - `header`: Optional request header used for the client IP instead of the ASGI
    connection address.
  - `max_keys`: Maximum number of `(rule, IP)` entries kept in memory. Defaults to `10000`.
  - `exempt_paths`: Path patterns that should never be rate limited.
  - `rules`: List of rate limit rules. The first matching rule is used.
  - `debug`: Set to `true` to enable `/-/ip-rate-limit-debug`, a debug page
    showing the current in-memory rate limit state as pretty-printed JSON. This
    includes client IP addresses, so it should only be enabled while debugging.

  Each rule supports:

  - `name`: Stable rule name used as part of the in-memory key. Defaults to the rule index.
  - `paths`: Path patterns to match. `*` matches any characters.
  - `methods`: Optional list of HTTP methods to match.
  - `query_params_min`: Optional minimum number of query string parameters required before the rule applies.
  - `window_seconds`: Token refill window. Defaults to `60`.
  - `max_requests`: Number of requests allowed per window. Defaults to `60`.
  - `block_seconds`: How long to block an IP after it exceeds the rate. Defaults to `300`.

  ## Memory Behavior

  The limiter uses a token bucket, not a sliding request log. It stores one small record for each tracked `(rule, IP)` pair: token count, last update time, and optional blocked-until time.

  The `max_keys` setting caps the number of tracked records. If more keys are seen, the least recently used keys are evicted. That bounds RAM usage even if a crawler rotates through many IP addresses.

  This is intentionally fail-open for evicted keys: if a key is evicted and later appears again, it starts with a fresh bucket.

  ## Development

  To run the tests:

  ```bash
  uv run pytest
  ```

project_urls:
  {"CI": "https://github.com/datasette/datasette-ip-rate-limit/actions", "Changelog": "https://github.com/datasette/datasette-ip-rate-limit/releases", "Homepage": "https://github.com/datasette/datasette-ip-rate-limit", "Issues": "https://github.com/datasette/datasette-ip-rate-limit/issues"}


name: datasette-jellyfish
summary: Datasette plugin adding SQL functions for fuzzy text matching powered by Jellyfish
classifiers: []
author: Simon Willison
description_content_type: text/markdown
home_page: https://datasette.io/plugins/datasette-jellyfish
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-jellyfish/
project_url: https://pypi.org/project/datasette-jellyfish/
release_url: https://pypi.org/project/datasette-jellyfish/2.0/
requires_dist: ["datasette", "jellyfish >0.9.0", "pytest ; extra == 'test'"]
version: 2.0
description:
  # datasette-jellyfish

  [![PyPI](https://img.shields.io/pypi/v/datasette-jellyfish.svg)](https://pypi.org/project/datasette-jellyfish/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-jellyfish?include_prereleases&label=changelog)](https://github.com/simonw/datasette-jellyfish/releases)
  [![Tests](https://github.com/simonw/datasette-jellyfish/workflows/Test/badge.svg)](https://github.com/simonw/datasette-jellyfish/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-jellyfish/blob/main/LICENSE)

  Datasette plugin that adds custom SQL functions for fuzzy string matching, built on top of the [Jellyfish](https://github.com/jamesturk/jellyfish) Python library by James Turk and Michael Stephens.

  Interactive demos:

  * [soundex, metaphone, nysiis, match_rating_codex comparison](https://latest-with-plugins.datasette.io/fixtures?sql=SELECT%0D%0A++++soundex%28%3As%29%2C+%0D%0A++++metaphone%28%3As%29%2C+%0D%0A++++nysiis%28%3As%29%2C+%0D%0A++++match_rating_codex%28%3As%29&s=demo).
  * [distance functions comparison](https://latest-with-plugins.datasette.io/fixtures?sql=SELECT%0D%0A++++levenshtein_distance%28%3As1%2C+%3As2%29%2C%0D%0A++++damerau_levenshtein_distance%28%3As1%2C+%3As2%29%2C%0D%0A++++hamming_distance%28%3As1%2C+%3As2%29%2C%0D%0A++++jaro_similarity%28%3As1%2C+%3As2%29%2C%0D%0A++++jaro_winkler_similarity%28%3As1%2C+%3As2%29%2C%0D%0A++++match_rating_comparison%28%3As1%2C+%3As2%29%3B&s1=barrack+obama&s2=barrack+h+obama)

  Examples:

      SELECT soundex("hello");
          -- Outputs H400
      SELECT metaphone("hello");
          -- Outputs HL
      SELECT nysiis("hello");
          -- Outputs HAL
      SELECT match_rating_codex("hello");
          -- Outputs HLL
      SELECT levenshtein_distance("hello", "hello world");
          -- Outputs 6
      SELECT damerau_levenshtein_distance("hello", "hello world");
          -- Outputs 6
      SELECT hamming_distance("hello", "hello world");
          -- Outputs 6
      SELECT jaro_similarity("hello", "hello world");
          -- Outputs 0.8181818181818182
      SELECT jaro_winkler_similarity("hello", "hello world");
          -- Outputs 0.890909090909091
      SELECT match_rating_comparison("hello", "helloo");
          -- Outputs 1

  See [the Jellyfish documentation](https://jellyfish.readthedocs.io/en/latest/) for an explanation of each of these functions.

project_urls:
  {"CI": "https://github.com/simonw/datasette-jellyfish/actions", "Changelog": "https://github.com/simonw/datasette-jellyfish/releases", "Homepage": "https://datasette.io/plugins/datasette-jellyfish", "Issues": "https://github.com/simonw/datasette-jellyfish/issues"}


name: datasette-jq
summary: Datasette plugin that adds custom SQL functions for executing jq expressions against JSON values
classifiers: []
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-jq
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-jq/
project_url: https://pypi.org/project/datasette-jq/
project_urls: {"Homepage": "https://github.com/simonw/datasette-jq"}
release_url: https://pypi.org/project/datasette-jq/0.2.1/
requires_dist: ["datasette", "pyjq", "six", "pytest ; extra == 'test'"]
version: 0.2.1
description:
  # datasette-jq

  [![PyPI](https://img.shields.io/pypi/v/datasette-jq.svg)](https://pypi.org/project/datasette-jq/)
  [![CircleCI](https://circleci.com/gh/simonw/datasette-jq.svg?style=svg)](https://circleci.com/gh/simonw/datasette-jq)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-jq/blob/master/LICENSE)

  Datasette plugin that adds custom SQL functions for executing [jq](https://stedolan.github.io/jq/) expressions against JSON values.

  Install this plugin in the same environment as Datasette to enable the `jq()` SQL function.

  Usage:

      select jq(
          column_with_json,
          "{top_3: .classifiers[:3], v: .version}"
      )

  See [the jq manual](https://stedolan.github.io/jq/manual/#Basicfilters) for full details of supported expression syntax.

  ## Interactive demo

  You can try this plugin out at [datasette-jq-demo.datasette.io](https://datasette-jq-demo.datasette.io/)

  Sample query:

      select package, "https://pypi.org/project/" || package || "/" as url,
      jq(info, "{summary: .info.summary, author: .info.author, versions: .releases|keys|reverse}")
      from packages

  [Try this query out](https://datasette-jq-demo.datasette.io/demo?sql=select+package%2C+%22https%3A%2F%2Fpypi.org%2Fproject%2F%22+%7C%7C+package+%7C%7C+%22%2F%22+as+url%2C%0D%0Ajq%28info%2C+%22%7Bsummary%3A+.info.summary%2C+author%3A+.info.author%2C+versions%3A+.releases%7Ckeys%7Creverse%7D%22%29%0D%0Afrom+packages) in the interactive demo.


name: datasette-json-html
summary: Datasette plugin for rendering HTML based on JSON values
classifiers: []
author: Simon Willison
description_content_type: text/markdown
home_page: https://datasette.io/plugins/datasette-json-html
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-json-html/
project_url: https://pypi.org/project/datasette-json-html/
release_url: https://pypi.org/project/datasette-json-html/1.0.1/
version: 1.0.1
description:
  # datasette-json-html

  [![PyPI](https://img.shields.io/pypi/v/datasette-json-html.svg)](https://pypi.org/project/datasette-json-html/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-json-html?include_prereleases&label=changelog)](https://github.com/simonw/datasette-json-html/releases)
  [![Tests](https://github.com/simonw/datasette-json-html/workflows/Test/badge.svg)](https://github.com/simonw/datasette-remote-metadata/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-json-html/blob/main/LICENSE)

  Datasette plugin for rendering HTML based on JSON values, using the [render_cell plugin hook](https://docs.datasette.io/en/stable/plugin_hooks.html#render-cell-value-column-table-database-datasette).

  This plugin looks for cell values that match a very specific JSON format and converts them into HTML when they are rendered by the Datasette interface.

  ## Links

      {
          "href": "https://simonwillison.net/",
          "label": "Simon Willison"
      }

  Will be rendered as an `<a href="">` link:

      <a href="https://simonwillison.net/">Simon Willison</a>

  You can set a tooltip on the link using a `"title"` key:


      {
          "href": "https://simonwillison.net/",
          "label": "Simon Willison",
          "title": "My blog"
      }

  Produces:

      <a href="https://simonwillison.net/" title="My blog">Simon Willison</a>

  You can also include a description, which will be displayed below the link. If descriptions include newlines they will be converted to `<br>` elements:

      select json_object(
          "href", "https://simonwillison.net/",
          "label", "Simon Willison",
          "description", "This can contain" || x'0a' || "newlines"
      )

  Produces:

      <strong><a href="https://simonwillison.net/">Simon Willison</a></strong><br>This can contain<br>newlines

  * [Literal JSON link demo](https://datasette-json-html.datasette.io/demo?sql=select+%27%7B%0D%0A++++%22href%22%3A+%22https%3A%2F%2Fsimonwillison.net%2F%22%2C%0D%0A++++%22label%22%3A+%22Simon+Willison%22%2C%0D%0A++++%22title%22%3A+%22My+blog%22%0D%0A%7D%27)

  ## List of links

      [
          {
              "href": "https://simonwillison.net/",
              "label": "Simon Willison"
          },
          {
              "href": "https://github.com/simonw/datasette",
              "label": "Datasette"
          }
      ]

  Will be rendered as a comma-separated list of `<a href="">` links:

      <a href="https://simonwillison.net/">Simon Willison</a>,
      <a href="https://github.com/simonw/datasette">Datasette</a>

  The `href` property must begin with `https://` or `http://` or `/`, to avoid potential XSS injection attacks (for example URLs that begin with `javascript:`).

  Lists of links cannot include `"description"` keys.

  * [Literal list of links demo](https://datasette-json-html.datasette.io/demo?sql=select+%27%5B%0D%0A++++%7B%0D%0A++++++++%22href%22%3A+%22https%3A%2F%2Fsimonwillison.net%2F%22%2C%0D%0A++++++++%22label%22%3A+%22Simon+Willison%22%0D%0A++++%7D%2C%0D%0A++++%7B%0D%0A++++++++%22href%22%3A+%22https%3A%2F%2Fgithub.com%2Fsimonw%2Fdatasette%22%2C%0D%0A++++++++%22label%22%3A+%22Datasette%22%0D%0A++++%7D%0D%0A%5D%27)

  ## Images

  The image tag is more complex. The most basic version looks like this:

      {
          "img_src": "https://placekitten.com/200/300"
      }

  This will render as:

      <img src="https://placekitten.com/200/300">

  But you can also include one or more of `alt`, `caption`, `width` and `href`.

  If you include width or alt, they will be added as attributes:

      {
          "img_src": "https://placekitten.com/200/300",
          "alt": "Kitten",
          "width": 200
      }

  Produces:

      <img src="https://placekitten.com/200/300"
          alt="Kitten" width="200">

  * [Literal image demo](https://datasette-json-html.datasette.io/demo?sql=select+%27%7B%0D%0A++++%22img_src%22%3A+%22https%3A%2F%2Fplacekitten.com%2F200%2F300%22%2C%0D%0A++++%22alt%22%3A+%22Kitten%22%2C%0D%0A++++%22width%22%3A+200%0D%0A%7D%27)

  The `href` key will cause the image to be wrapped in a link:

      {
          "img_src": "https://placekitten.com/200/300",
          "href": "http://www.example.com"
      }

  Produces:

      <a href="http://www.example.com">
          <img src="https://placekitten.com/200/300">
      </a>

  The `caption` key wraps everything in a fancy figure/figcaption block:

      {
          "img_src": "https://placekitten.com/200/300",
          "caption": "Kitten caption"
      }

  Produces:

      <figure>
          <img src="https://placekitten.com/200/300"></a>
          <figcaption>Kitten caption</figcaption>
      </figure>

  ## Preformatted text

  You can use `{"pre": "text"}` to render text in a `<pre>` HTML tag:

      {
          "pre": "This\nhas\nnewlines"
      }

  Produces:

      <pre>This
      has
      newlines</pre>

  If the value attached to the `"pre"` key is itself a JSON object, that JSON will be pretty-printed:

      {
          "pre": {
              "this": {
                  "object": ["is", "nested"]
              }
          }
      }

  Produces:

      <pre>{
        &#34;this&#34;: {
          &#34;object&#34;: [
            &#34;is&#34;,
            &#34;nested&#34;
          ]
        }
      }</pre>

  * [Preformatted text with JSON demo](https://datasette-json-html.datasette.io/demo?sql=select+%27%7B%0D%0A++++%22pre%22%3A+%7B%0D%0A++++++++%22this%22%3A+%7B%0D%0A++++++++++++%22object%22%3A+%5B%22is%22%2C+%22nested%22%5D%0D%0A++++++++%7D%0D%0A++++%7D%0D%0A%7D%27)
  * [Preformatted text demo showing the Mandelbrot Set](https://datasette-json-html.datasette.io/demo?sql=WITH+RECURSIVE%0D%0A++xaxis%28x%29+AS+%28VALUES%28-2.0%29+UNION+ALL+SELECT+x%2B0.05+FROM+xaxis+WHERE+x%3C1.2%29%2C%0D%0A++yaxis%28y%29+AS+%28VALUES%28-1.0%29+UNION+ALL+SELECT+y%2B0.1+FROM+yaxis+WHERE+y%3C1.0%29%2C%0D%0A++m%28iter%2C+cx%2C+cy%2C+x%2C+y%29+AS+%28%0D%0A++++SELECT+0%2C+x%2C+y%2C+0.0%2C+0.0+FROM+xaxis%2C+yaxis%0D%0A++++UNION+ALL%0D%0A++++SELECT+iter%2B1%2C+cx%2C+cy%2C+x*x-y*y+%2B+cx%2C+2.0*x*y+%2B+cy+FROM+m+%0D%0A+++++WHERE+%28x*x+%2B+y*y%29+%3C+4.0+AND+iter%3C28%0D%0A++%29%2C%0D%0A++m2%28iter%2C+cx%2C+cy%29+AS+%28%0D%0A++++SELECT+max%28iter%29%2C+cx%2C+cy+FROM+m+GROUP+BY+cx%2C+cy%0D%0A++%29%2C%0D%0A++a%28t%29+AS+%28%0D%0A++++SELECT+group_concat%28+substr%28%27+.%2B*%23%27%2C+1%2Bmin%28iter%2F7%2C4%29%2C+1%29%2C+%27%27%29+%0D%0A++++FROM+m2+GROUP+BY+cy%0D%0A++%29%0D%0ASELECT+json_object%28%27pre%27%2C+group_concat%28rtrim%28t%29%2Cx%270a%27%29%29+FROM+a%3B) using [this example](https://www.sqlite.org/lang_with.html#outlandish_recursive_query_examples) from the SQLite documentation

  ## Using these with SQLite JSON functions

  The most powerful way to make use of this plugin is in conjunction with SQLite's [JSON functions](https://www.sqlite.org/json1.html). For example:

      select json_object(
          "href", "https://simonwillison.net/",
          "label", "Simon Willison"
      );

  * [json_object() link demo](https://datasette-json-html.datasette.io/demo?sql=select+json_object%28%0D%0A++++%22href%22%2C+%22https%3A%2F%2Fsimonwillison.net%2F%22%2C%0D%0A++++%22label%22%2C+%22Simon+Willison%22%0D%0A%29%3B)

  You can use these functions to construct JSON objects that work with the plugin from data in a table:

      select id, json_object(
          "href", url, "label", text
      ) from mytable;

  * [Demo that builds links against a table](https://datasette-json-html.datasette.io/demo?sql=select+json_object%28%22href%22%2C+url%2C+%22label%22%2C+package%2C+%22title%22%2C+package+%7C%7C+%22+%22+%7C%7C+url%29+as+package+from+packages)

  The `json_group_array()` function is an aggregate function similar to `group_concat()` - it allows you to construct lists of JSON objects in conjunction with a `GROUP BY` clause.

  This means you can use it to construct dynamic lists of links, for example:

      select
          substr(package, 0, 12) as prefix,
          json_group_array(
              json_object(
                  "href", url,
                  "label", package
              )
          ) as package_links
      from packages
      group by prefix

  * [Demo of json_group_array()](https://datasette-json-html.datasette.io/demo?sql=select%0D%0A++++substr%28package%2C+0%2C+12%29+as+prefix%2C%0D%0A++++json_group_array%28%0D%0A++++++++json_object%28%0D%0A++++++++++++%22href%22%2C+url%2C%0D%0A++++++++++++%22label%22%2C+package%0D%0A++++++++%29%0D%0A++++%29+as+package_links%0D%0Afrom+packages%0D%0Agroup+by+prefix)

  ## The `urllib_quote_plus()` SQL function

  Since this plugin is designed to be used with SQL that constructs the underlying JSON structure, it is likely you will need to construct dynamic URLs from results returned by a SQL query.

  This plugin registers a custom SQLite function called `urllib_quote_plus()` to help you do that. It lets you use Python's [urllib.parse.quote\_plus() function](https://docs.python.org/3/library/urllib.parse.html#urllib.parse.quote_plus) from within a SQL query.

  Here's an example of how you might use it:

      select id, json_object(
          "href",
          "/mydatabase/other_table?_search=" || urllib_quote_plus(text),
          "label", text
      ) from mytable;

project_urls:
  {"CI": "https://github.com/simonw/datasette-json-html/actions", "Changelog": "https://github.com/simonw/datasette-json-html/releases", "Homepage": "https://datasette.io/plugins/datasette-json-html", "Issues": "https://github.com/simonw/datasette-json-html/issues"}

requires_dist:
  ["datasette", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'", "httpx ; extra == 'test'"]


name: datasette-json-preview
summary: Preview of new JSON default format for Datasette, see issue #782
classifiers: []
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-json-preview
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-json-preview/
project_url: https://pypi.org/project/datasette-json-preview/
release_url: https://pypi.org/project/datasette-json-preview/0.3/
version: 0.3
description:
  # datasette-json-preview

  [![PyPI](https://img.shields.io/pypi/v/datasette-json-preview.svg)](https://pypi.org/project/datasette-json-preview/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-json-preview?include_prereleases&label=changelog)](https://github.com/simonw/datasette-json-preview/releases)
  [![Tests](https://github.com/simonw/datasette-json-preview/workflows/Test/badge.svg)](https://github.com/simonw/datasette-json-preview/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-json-preview/blob/main/LICENSE)

  Preview of new JSON default format for Datasette, see [issue #782](https://github.com/simonw/datasette/issues/782)

  This plugin will continue to change while the new output format is explored.

  ## Installation

  Install this plugin in the same environment as Datasette.

      $ datasette install datasette-json-preview

  ## Usage

  Use the `.json-preview` extension to preview the new JSON API design.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:

      cd datasette-json-preview
      python3 -mvenv venv
      source venv/bin/activate

  Or if you are using `pipenv`:

      pipenv shell

  Now install the dependencies and tests:

      pip install -e '.[test]'

  To run the tests:

      pytest

project_urls:
  {"CI": "https://github.com/simonw/datasette-json-preview/actions", "Changelog": "https://github.com/simonw/datasette-json-preview/releases", "Homepage": "https://github.com/simonw/datasette-json-preview", "Issues": "https://github.com/simonw/datasette-json-preview/issues"}

requires_dist:
  ["datasette (>=0.55)", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'", "httpx ; extra == 'test'", "sqlite-utils ; extra == 'test'"]


name: datasette-jsonschema-forms
classifiers: ["Framework :: Datasette"]
author: Alex Garcia
description_content_type: text/markdown
dynamic: ["License-File"]
license_expression: Apache-2.0
license_files: ["LICENSE"]
package_url: https://pypi.org/project/datasette-jsonschema-forms/
project_url: https://pypi.org/project/datasette-jsonschema-forms/
provides_extra: ["test"]
release_url: https://pypi.org/project/datasette-jsonschema-forms/0.0.3/
requires_python: >=3.9
version: 0.0.3
description:
  # datasette-jsonschema-forms

  [![PyPI](https://img.shields.io/pypi/v/datasette-jsonschema-forms.svg)](https://pypi.org/project/datasette-jsonschema-forms/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-jsonschema-forms?include_prereleases&label=changelog)](https://github.com/datasette/datasette-jsonschema-forms/releases)
  [![Tests](https://github.com/datasette/datasette-jsonschema-forms/actions/workflows/test.yml/badge.svg)](https://github.com/datasette/datasette-jsonschema-forms/actions/workflows/test.yml)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-jsonschema-forms/blob/main/LICENSE)



  ## Installation

  Install this plugin in the same environment as Datasette.
  ```bash
  datasette install datasette-jsonschema-forms
  ```
  ## Usage

  Usage instructions go here.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:
  ```bash
  cd datasette-jsonschema-forms
  python -m venv venv
  source venv/bin/activate
  ```
  Now install the dependencies and test dependencies:
  ```bash
  pip install -e '.[test]'
  ```
  To run the tests:
  ```bash
  python -m pytest
  ```

project_urls:
  {"CI": "https://github.com/datasette/datasette-jsonschema-forms/actions", "Changelog": "https://github.com/datasette/datasette-jsonschema-forms/releases", "Homepage": "https://github.com/datasette/datasette-jsonschema-forms", "Issues": "https://github.com/datasette/datasette-jsonschema-forms/issues"}

requires_dist:
  ["datasette>=1.0a19", "sqlite-migrate>=0.1b0", "sqlite-utils>=3.38", "pydantic>=2", "pytest; extra == \"test\"", "pytest-asyncio; extra == \"test\""]


name: datasette-jupyterlite
summary: JupyterLite as a Datasette plugin
classifiers: []
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-jupyterlite
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-jupyterlite/
project_url: https://pypi.org/project/datasette-jupyterlite/
release_url: https://pypi.org/project/datasette-jupyterlite/0.1a1/
requires_python: >=3.7
version: 0.1a1
description:
  # datasette-jupyterlite

  [![PyPI](https://img.shields.io/pypi/v/datasette-jupyterlite.svg)](https://pypi.org/project/datasette-jupyterlite/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-jupyterlite?include_prereleases&label=changelog)](https://github.com/simonw/datasette-jupyterlite/releases)
  [![Tests](https://github.com/simonw/datasette-jupyterlite/workflows/Test/badge.svg)](https://github.com/simonw/datasette-jupyterlite/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-jupyterlite/blob/main/LICENSE)

  [JupyterLite](https://jupyterlite.readthedocs.io/en/latest/) as a Datasette plugin

  ## Installation

  Install this plugin in the same environment as Datasette.

      $ datasette install datasette-jupyterlite

  ## Demo

  You can try out a demo of the plugin here: https://latest-with-plugins.datasette.io/jupyterlite/

  Run this example code in a Pyolite notebook to pull all of the data from the [github/stars](https://latest-with-plugins.datasette.io/github/stars) table into a Pandas DataFrame:

  ```python
  import pandas, pyodide

  df = pandas.read_csv(pyodide.open_url(
    "https://latest-with-plugins.datasette.io/github/stars.csv?_labels=on&_stream=on&_size=max")
  )
  ```

  ## Usage

  Once installed, visit `/jupyterlite/` to access JupyterLite served from your Datasette instance.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:

      cd datasette-jupyterlite
      python3 -mvenv venv
      source venv/bin/activate

  Or if you are using `pipenv`:

      pipenv shell

  Now install the dependencies and test dependencies:

      pip install -e '.[test]'

  To run the tests:

      pytest

project_urls:
  {"CI": "https://github.com/simonw/datasette-jupyterlite/actions", "Changelog": "https://github.com/simonw/datasette-jupyterlite/releases", "Homepage": "https://github.com/simonw/datasette-jupyterlite", "Issues": "https://github.com/simonw/datasette-jupyterlite/issues"}

requires_dist:
  ["datasette", "jupyterlite", "importlib-resources", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'"]


name: datasette-leaflet-freedraw
summary: Draw polygons on maps in Datasette
classifiers: []
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-leaflet-freedraw
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-leaflet-freedraw/
project_url: https://pypi.org/project/datasette-leaflet-freedraw/
release_url: https://pypi.org/project/datasette-leaflet-freedraw/0.3.1/
requires_python: >=3.6
version: 0.3.1
description:
  # datasette-leaflet-freedraw

  [![PyPI](https://img.shields.io/pypi/v/datasette-leaflet-freedraw.svg)](https://pypi.org/project/datasette-leaflet-freedraw/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-leaflet-freedraw?include_prereleases&label=changelog)](https://github.com/simonw/datasette-leaflet-freedraw/releases)
  [![Tests](https://github.com/simonw/datasette-leaflet-freedraw/workflows/Test/badge.svg)](https://github.com/simonw/datasette-leaflet-freedraw/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-leaflet-freedraw/blob/main/LICENSE)

  Draw polygons on maps in Datasette

  Project background: [Drawing shapes on a map to query a SpatiaLite database](https://simonwillison.net/2021/Jan/24/drawing-shapes-spatialite/).

  ## Installation

  Install this plugin in the same environment as Datasette.

      $ datasette install datasette-leaflet-freedraw

  ## Usage

  If a table has a SpatiaLite `geometry` column, the plugin will add a map interface to the table page allowing users to draw a shape on the map to find rows with a geometry that intersects that shape.

  The plugin can also work with arbitrary SQL queries. There it looks for input fields with a name of `freedraw` or that ends in `_freedraw` and replaces them with a map interface.

  The map interface uses the [FreeDraw](https://freedraw.herokuapp.com/) Leaflet plugin.

  ## Demo

  You can try out this plugin to run searches against the GreenInfo Network California Protected Areas Database. Here's [an example query](https://calands.datasettes.com/calands?sql=select%0D%0A++AsGeoJSON%28geometry%29%2C+*%0D%0Afrom%0D%0A++CPAD_2020a_SuperUnits%0D%0Awhere%0D%0A++PARK_NAME+like+%27%25mini%25%27+and%0D%0A++Intersects%28GeomFromGeoJSON%28%3Afreedraw%29%2C+geometry%29+%3D+1%0D%0A++and+CPAD_2020a_SuperUnits.rowid+in+%28%0D%0A++++select%0D%0A++++++rowid%0D%0A++++from%0D%0A++++++SpatialIndex%0D%0A++++where%0D%0A++++++f_table_name+%3D+%27CPAD_2020a_SuperUnits%27%0D%0A++++++and+search_frame+%3D+GeomFromGeoJSON%28%3Afreedraw%29%0D%0A++%29&freedraw=%7B%22type%22%3A%22MultiPolygon%22%2C%22coordinates%22%3A%5B%5B%5B%5B-122.42202758789064%2C37.82280243352759%5D%2C%5B-122.39868164062501%2C37.823887203271454%5D%2C%5B-122.38220214843751%2C37.81846319511331%5D%2C%5B-122.35061645507814%2C37.77071473849611%5D%2C%5B-122.34924316406251%2C37.74465712069939%5D%2C%5B-122.37258911132814%2C37.703380457832374%5D%2C%5B-122.39044189453125%2C37.690340943717715%5D%2C%5B-122.41241455078126%2C37.680559803205135%5D%2C%5B-122.44262695312501%2C37.67295135774715%5D%2C%5B-122.47283935546876%2C37.67295135774715%5D%2C%5B-122.52502441406251%2C37.68382032669382%5D%2C%5B-122.53463745117189%2C37.6892542140253%5D%2C%5B-122.54699707031251%2C37.690340943717715%5D%2C%5B-122.55798339843751%2C37.72945260537781%5D%2C%5B-122.54287719726564%2C37.77831314799672%5D%2C%5B-122.49893188476564%2C37.81303878836991%5D%2C%5B-122.46185302734376%2C37.82822612280363%5D%2C%5B-122.42889404296876%2C37.82822612280363%5D%2C%5B-122.42202758789064%2C37.82280243352759%5D%5D%5D%5D%7D) showing mini parks in San Francisco:

  ```sql
  select
    AsGeoJSON(geometry), *
  from
    CPAD_2020a_SuperUnits
  where
    PARK_NAME like '%mini%' and
    Intersects(GeomFromGeoJSON(:freedraw), geometry) = 1
    and CPAD_2020a_SuperUnits.rowid in (
      select
        rowid
      from
        SpatialIndex
      where
        f_table_name = 'CPAD_2020a_SuperUnits'
        and search_frame = GeomFromGeoJSON(:freedraw)
    )
  ```

  ![Screenshot of the plugin in action](https://static.simonwillison.net/static/2021/datasette-leaflet-freedraw.png)

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:

      cd datasette-leaflet-freedraw
      python3 -mvenv venv
      source venv/bin/activate

  Or if you are using `pipenv`:

      pipenv shell

  Now install the dependencies and tests:

      pip install -e '.[test]'

  To run the tests:

      pytest

project_urls:
  {"CI": "https://github.com/simonw/datasette-leaflet-freedraw/actions", "Changelog": "https://github.com/simonw/datasette-leaflet-freedraw/releases", "Homepage": "https://github.com/simonw/datasette-leaflet-freedraw", "Issues": "https://github.com/simonw/datasette-leaflet-freedraw/issues"}

requires_dist:
  ["datasette (>=0.60)", "datasette-leaflet (>=0.2)", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'"]


name: datasette-leaflet-geojson
summary: A Datasette plugin that renders GeoJSON columns using Leaflet
classifiers: []
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-leaflet-geojson
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-leaflet-geojson/
project_url: https://pypi.org/project/datasette-leaflet-geojson/
project_urls: {"Homepage": "https://github.com/simonw/datasette-leaflet-geojson"}
release_url: https://pypi.org/project/datasette-leaflet-geojson/0.8.2/
version: 0.8.2
description:
  # datasette-leaflet-geojson

  [![PyPI](https://img.shields.io/pypi/v/datasette-leaflet-geojson.svg)](https://pypi.org/project/datasette-leaflet-geojson/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-leaflet-geojson?include_prereleases&label=changelog)](https://github.com/simonw/datasette-leaflet-geojson/releases)
  [![Tests](https://github.com/simonw/datasette-leaflet-geojson/workflows/Test/badge.svg)](https://github.com/simonw/datasette-leaflet-geojson/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-leaflet-geojson/blob/main/LICENSE)

  Datasette plugin that replaces any GeoJSON column values with a Leaflet map

  ## Installation

  Install this plugin in the same environment as Datasette.

      $ datasette install datasette-leaflet-geojson

  ## Usage

  Any columns containing valid GeoJSON strings will have their contents replaced with a Leaflet map when they are displayed in the Datasette interface.

  ## Demo

  You can try this plugin out at https://calands.datasettes.com/calands/superunits_with_maps

  ![datasette-leaflet-geojson in action](https://raw.github.com/simonw/datasette-leaflet-geojson/main/datasette-leaflet-geojson.png)

  ## Configuration

  By default this plugin displays maps for the first ten rows, and shows a "Click to load map" prompt for rows past the first ten.

  You can change this limit using the `default_maps_to_load` plugin configuration setting. Add this to your `metadata.json`:

  ```json
  {
      "plugins": {
          "datasette-leaflet-geojson": {
              "default_maps_to_load": 20
          }
      }
  }
  ```
  Then run Datasette with `datasette mydb.db -m metadata.json`.

requires_dist:
  ["datasette>=0.54", "datasette-leaflet>=0.2", "pytest; extra == \"test\"", "pytest-asyncio; extra == \"test\""]


name: datasette-leaflet
summary: A plugin that bundles Leaflet.js for Datasette
classifiers: []
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-leaflet
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-leaflet/
project_url: https://pypi.org/project/datasette-leaflet/
release_url: https://pypi.org/project/datasette-leaflet/0.2.2/
requires_dist: ["datasette", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'"]
requires_python: >=3.6
version: 0.2.2
description:
  # datasette-leaflet

  [![PyPI](https://img.shields.io/pypi/v/datasette-leaflet.svg)](https://pypi.org/project/datasette-leaflet/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-leaflet?include_prereleases&label=changelog)](https://github.com/simonw/datasette-leaflet/releases)
  [![Tests](https://github.com/simonw/datasette-leaflet/workflows/Test/badge.svg)](https://github.com/simonw/datasette-leaflet/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-leaflet/blob/main/LICENSE)

  Datasette plugin adding the [Leaflet](https://leafletjs.com/) JavaScript library.

  A growing number of Datasette plugins depend on the Leaflet JavaScript mapping library. They each have their own way of loading Leaflet, which could result in loading it multiple times (with multiple versions) if more than one plugin is installed.

  This library is intended to solve this problem, by providing a single plugin they can all depend on that loads Leaflet in a reusable way.

  Plugins that use this:

  - [datasette-leaflet-freedraw](https://datasette.io/plugins/datasette-leaflet-freedraw)
  - [datasette-leaflet-geojson](https://datasette.io/plugins/datasette-leaflet-geojson)
  - [datasette-cluster-map](https://datasette.io/plugins/datasette-cluster-map)

  ## Installation

  You can install this plugin like so:

      datasette install datasette-leaflet

  Usually this plugin will be a dependency of other plugins, so it should be installed automatically when you install them.

  ## Usage

  The plugin makes `leaflet.js` and `leaflet.css` available as static files. It provides two custom template variables with the URLs of those two files.

  - `{{ datasette_leaflet_url }}` is the URL to the JavaScript
  - `{{ datasette_leaflet_css_url }}` is the URL to the CSS

  These URLs are also made available as global JavaScript constants:

  - `datasette.leaflet.JAVASCRIPT_URL`
  - `datasette.leaflet.CSS_URL`

  The JavaScript is packaed as a [JavaScript module](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules). You can dynamically import the JavaScript from a custom template like this:

  ```html+jinja
  <script type="module">
  import('{{ datasette_leaflet_url }}')
    .then((leaflet) => {
      /* Use leaflet here */
    });
  </script>
  ```

  You can load the CSS like this:

  ```html+jinja
  <link rel="stylesheet" href="{{ datasette_leaflet_css_url }}">
  ```

  Or dynamically like this:

  ```html+jinja
  <script>
  let link = document.createElement('link');
  link.rel = 'stylesheet';
  link.href = '{{ datasette_leaflet_css_url }}';
  document.head.appendChild(link);
  </script>
  ```

  Here's a full example that loads the JavaScript and CSS and renders a map:

  ```html+jinja
  <script type="module">
  window.DATASETTE_CLUSTER_MAP_TILE_LAYER = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
  window.DATASETTE_CLUSTER_MAP_TILE_LAYER_OPTIONS = {"maxZoom": 19, "detectRetina": true, "attribution": "&copy; <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors"};
  let link = document.createElement('link');
  link.rel = 'stylesheet';
  link.href = '{{ datasette_leaflet_css_url }}';
  document.head.appendChild(link);
  import('{{ datasette_leaflet_url }}')
    .then((leaflet) => {
      let div = document.createElement('div');
      div.style.height = '400px';
      document.querySelector('.content').appendChild(div);
      let tiles = leaflet.tileLayer(
          window.DATASETTE_CLUSTER_MAP_TILE_LAYER,
          window.DATASETTE_CLUSTER_MAP_TILE_LAYER_OPTIONS
      );
      let map = leaflet.map(div, {
          center: leaflet.latLng(0, 0),
          zoom: 2,
          layers: [tiles]
      });
    });
  </script>
  ```

project_urls:
  {"CI": "https://github.com/simonw/datasette-leaflet/actions", "Changelog": "https://github.com/simonw/datasette-leaflet/releases", "Homepage": "https://github.com/simonw/datasette-leaflet", "Issues": "https://github.com/simonw/datasette-leaflet/issues"}


name: datasette-llm-accountant
summary: Accounting for LLM token usage
classifiers: ["Framework :: Datasette"]
author: Simon Willison
description_content_type: text/markdown
dynamic: ["License-File"]
license_expression: Apache-2.0
license_files: ["LICENSE"]
package_url: https://pypi.org/project/datasette-llm-accountant/
project_url: https://pypi.org/project/datasette-llm-accountant/
release_url: https://pypi.org/project/datasette-llm-accountant/0.1a4/
requires_dist: ["datasette>=1.0a21", "datasette-llm>=0.1a8", "llm>=0.32a1", "httpx"]
requires_python: >=3.10
version: 0.1a4
description:
  # datasette-llm-accountant

  [![PyPI](https://img.shields.io/pypi/v/datasette-llm-accountant.svg)](https://pypi.org/project/datasette-llm-accountant/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-llm-accountant?include_prereleases&label=changelog)](https://github.com/datasette/datasette-llm-accountant/releases)
  [![Tests](https://github.com/datasette/datasette-llm-accountant/actions/workflows/test.yml/badge.svg)](https://github.com/datasette/datasette-llm-accountant/actions/workflows/test.yml)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-llm-accountant/blob/main/LICENSE)

  Budget management and cost tracking for LLM usage in Datasette.

  ## Installation

  Install this plugin in the same environment as Datasette:

  ```bash
  datasette install datasette-llm-accountant
  ```

  This plugin works alongside [datasette-llm](https://github.com/datasette/datasette-llm) to provide automatic cost tracking and budget enforcement for LLM prompts.

  ## Overview

  This plugin provides:

  - **Automatic cost calculation** based on token usage and model pricing
  - **Reserve/settle pattern** for budget enforcement
  - **Accountant plugin system** for custom spending trackers
  - **Custom pricing providers** with access to the full response object
  - **Hook integration** with datasette-llm for transparent accounting

  When installed, all prompts made through `datasette-llm` are automatically wrapped with accounting logic. Accountants can enforce spending limits, log usage, and track costs.

  ## How It Works

  1. When a prompt is made via `datasette-llm`, this plugin's hooks intercept the call
  2. A reservation is made with all registered accountants for the estimated cost
  3. The prompt executes
  4. The actual cost is calculated via `PricingProvider.calculate_cost_from_response()`
  5. Accountants are settled with the real cost (refunding any unused reservation)

  ## Configuration

  Configure reservation amounts in `datasette.yaml`:

  ```yaml
  plugins:
    datasette-llm-accountant:
      # Default reservation for single prompts
      auto_reservation_usd: 0.10

      # Default reservation for grouped prompts
      default_reservation_usd: 0.50

      # Purpose-specific reservations
      purposes:
        enrichments:
          reservation_usd: 5.00
        query-assistant:
          reservation_usd: 0.25
  ```

  ## Nanocents

  All monetary amounts use the `Nanocents` type — an `int` subclass that makes units explicit and prevents accidentally passing raw dollar amounts where nanocents are expected.

  - 1 nanocent = 1/1,000,000,000 of a cent
  - 1 USD = 100 cents = 100,000,000,000 nanocents
  - This allows tracking costs down to fractions of a cent without floating-point errors

  ```python
  from datasette_llm_accountant import Nanocents

  # Create from USD or cents
  cost = Nanocents.from_usd(1.50)   # 150,000,000,000
  cost = Nanocents.from_cents(50)   # 50,000,000,000

  # Convert back
  cost.to_usd()    # 1.5
  cost.to_cents()  # 150.0

  # Works like a regular int for arithmetic
  total = cost + Nanocents.from_usd(0.50)
  ```

  ## Creating an Accountant Plugin

  Accountants track and enforce LLM spending. Create a plugin that implements the `register_llm_accountants` hook:

  ```python
  from datasette import hookimpl
  from datasette_llm_accountant import Accountant, Tx, Nanocents, InsufficientBalanceError

  class MyAccountant(Accountant):
      """Custom accountant that tracks spending."""

      def __init__(self, datasette):
          self.datasette = datasette

      async def reserve(
          self,
          nanocents: Nanocents,
          model_id: str = None,
          purpose: str = None,
          actor_id: str = None,
      ) -> Tx:
          """Reserve the specified amount. Raise InsufficientBalanceError to block."""
          if not await self.has_sufficient_balance(nanocents):
              raise InsufficientBalanceError("Insufficient balance")
          tx_id = await self.create_reservation(nanocents, model_id, purpose)
          return Tx(tx_id)

      async def settle(
          self,
          tx: Tx,
          nanocents: Nanocents,
          model_id: str = None,
          purpose: str = None,
          actor_id: str = None,
      ):
          """Settle a transaction for the actual amount spent."""
          await self.record_settlement(tx, nanocents, model_id, purpose)

      async def rollback(self, tx: Tx):
          """Optional: Release a reservation without charging."""
          await self.settle(tx, Nanocents(0))

  @hookimpl
  def register_llm_accountants(datasette):
      return [MyAccountant(datasette)]
  ```

  See [datasette-llm-allowance](https://github.com/datasette/datasette-llm-allowance) for a complete implementation that uses Datasette's internal database to track a spending allowance.

  ## Multiple Accountants

  Multiple accountants can be registered. When a reservation is made:

  1. All accountants are called in sequence to reserve the amount
  2. If any accountant fails (e.g., `InsufficientBalanceError`), previous reservations are rolled back
  3. When the prompt completes, all accountants are settled with the actual cost

  This enables layered accounting (per-user limits, per-project budgets, global caps, etc.).

  ## Custom Pricing Providers

  The default pricing provider fetches model prices from [llm-prices.com](https://www.llm-prices.com/). You can register a custom provider to control how costs are calculated:

  ```python
  from datasette import hookimpl
  from datasette_llm_accountant import PricingProvider, Nanocents

  class MyPricingProvider(PricingProvider):
      async def calculate_cost_from_response(self, model_id, usage, response):
          """
          Calculate cost from a completed response.

          Args:
              model_id: The model identifier
              usage: An llm.Usage object with input/output token counts
              response: The llm.AsyncResponse object — use this to access
                        provider-specific metadata (e.g., generation IDs
                        for exact cost lookups from the provider's API)
          Returns:
              Cost as a Nanocents value
          """
          input_tokens = usage.input or 0
          output_tokens = usage.output or 0
          cost_usd = input_tokens * 0.01 / 1_000_000 + output_tokens * 0.03 / 1_000_000
          return Nanocents.from_usd(cost_usd)

      async def supported_models(self):
          """Return the set of model IDs this provider can price,
          or None to indicate all models are supported.

          Used to filter the model list — models not in this set
          won't be available when accountants are registered.
          """
          return self.known_models

  @hookimpl
  def register_llm_accountant_pricing(datasette):
      return MyPricingProvider()
  ```

  ## API Reference

  ### Accountant Base Class

  ```python
  class Accountant(ABC):
      @abstractmethod
      async def reserve(
          self,
          nanocents: Nanocents,
          model_id: str = None,
          purpose: str = None,
          actor_id: str = None,
      ) -> Tx:
          """Reserve an amount, return transaction ID."""

      @abstractmethod
      async def settle(
          self,
          tx: Tx,
          nanocents: Nanocents,
          model_id: str = None,
          purpose: str = None,
          actor_id: str = None,
      ):
          """Settle a transaction for the actual amount."""

      async def rollback(self, tx: Tx):
          """Release a reservation (default: settle for 0)."""
          await self.settle(tx, Nanocents(0))
  ```

  ### PricingProvider Base Class

  ```python
  class PricingProvider(ABC):
      @abstractmethod
      async def calculate_cost_from_response(
          self, model_id: str, usage: Usage, response: AsyncResponse
      ) -> Nanocents:
          """Calculate cost from a completed response."""

      async def supported_models(self) -> Optional[set[str]]:
          """Return set of supported model IDs, or None for all. Default: None."""
          return None
  ```

  ### Exceptions

  - `InsufficientBalanceError` - Raised when an accountant cannot reserve the requested amount
  - `ReservationExceededError` - Raised when actual cost exceeds the reserved amount
  - `ModelPricingNotFoundError` - Raised when pricing data is not available for a model

  ## Development

  ```bash
  cd datasette-llm-accountant
  pip install -e '.[dev]'
  pytest
  ```

project_urls:
  {"CI": "https://github.com/datasette/datasette-llm-accountant/actions", "Changelog": "https://github.com/datasette/datasette-llm-accountant/releases", "Homepage": "https://github.com/datasette/datasette-llm-accountant", "Issues": "https://github.com/datasette/datasette-llm-accountant/issues"}


name: datasette-llm-embed
summary: llm_embed(model_id, text) SQL function for Datasette
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Simon Willison
description_content_type: text/markdown
license: Apache-2.0
package_url: https://pypi.org/project/datasette-llm-embed/
project_url: https://pypi.org/project/datasette-llm-embed/
release_url: https://pypi.org/project/datasette-llm-embed/0.2/
requires_dist: ["datasette", "llm", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'"]
version: 0.2
description:
  # datasette-llm-embed

  [![PyPI](https://img.shields.io/pypi/v/datasette-llm-embed.svg)](https://pypi.org/project/datasette-llm-embed/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-llm-embed?include_prereleases&label=changelog)](https://github.com/simonw/datasette-llm-embed/releases)
  [![Tests](https://github.com/simonw/datasette-llm-embed/workflows/Test/badge.svg)](https://github.com/simonw/datasette-llm-embed/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-llm-embed/blob/main/LICENSE)

  Datasette plugin adding a `llm_embed(model_id, text)` SQL function.

  ## Installation

  ```bash
  datasette install datasette-llm-embed
  ```

  ## Usage

  Adds a SQL function that can be called like this:
  ```sql
  select llm_embed('sentence-transformers/all-mpnet-base-v2', 'This is some text')
  ```
  This embeds the provided text using the specified embedding model and returns a binary blob, suitable for use with plugins such as [datasette-faiss](https://datasette.io/plugins/datasette-faiss).

  The models need to be installed using [LLM](https://llm.datasette.io/) plugins such as [llm-sentence-transformers](https://github.com/simonw/llm-sentence-transformers).

  Use `llm_embed_cosine(a, b)` to calculate cosine similarity between two vector blobs:

  ```sql
  select llm_embed_cosine(
      llm_embed('sentence-transformers/all-mpnet-base-v2', 'This is some text'),
      llm_embed('sentence-transformers/all-mpnet-base-v2', 'This is some other text')
  )
  ```

  The `llm_embed_decode()` function can be used to decode a binary BLOB into a JSON array of floats:

  ```sql
  select llm_embed_decode(
      llm_embed('sentence-transformers/all-mpnet-base-v2', 'This is some text')
  )
  ```

  ## Models that require API keys

  If your embedding model needs an API key - for example the `ada-002` model from OpenAI - you can configure that key in `metadata.yml` (or JSON) like this:

  ```yaml
  plugins:
    datasette-llm-embed:
      keys:
        ada-002:
          $env: OPENAI_API_KEY
  ```
  The key here should be the full model ID of the model - not an alias.

  You can then set the `OPENAI_API_KEY` environment variable to the key you want to use before starting Datasette:
  ```bash
  export OPENAI_API_KEY=sk-1234567890
  ```
  Once configured, calls like this will use the API key that has been provided:
  ```sql
  select llm_embed('ada-002', 'This is some text')
  ```

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:
  ```bash
  cd datasette-llm-embed
  python3 -m venv venv
  source venv/bin/activate
  ```
  Now install the dependencies and test dependencies:
  ```
  pip install -e '.[test]'
  ```
  ```
  To run the tests:
  ```bash
  pytest
  ```

project_urls:
  {"CI": "https://github.com/simonw/datasette-llm-embed/actions", "Changelog": "https://github.com/simonw/datasette-llm-embed/releases", "Homepage": "https://github.com/simonw/datasette-llm-embed", "Issues": "https://github.com/simonw/datasette-llm-embed/issues"}


name: datasette-llm-limits
summary: Plugin for configuring periodic limits on LLM usage in Datasette
classifiers: ["Framework :: Datasette"]
author: Datasette
description_content_type: text/markdown
dynamic: ["License-File"]
license_expression: Apache-2.0
license_files: ["LICENSE"]
package_url: https://pypi.org/project/datasette-llm-limits/
project_url: https://pypi.org/project/datasette-llm-limits/
release_url: https://pypi.org/project/datasette-llm-limits/0.1a1/
requires_dist: ["datasette>=1.0a28", "datasette-llm-accountant"]
requires_python: >=3.10
version: 0.1a1
description:
  # datasette-llm-limits

  [![PyPI](https://img.shields.io/pypi/v/datasette-llm-limits.svg)](https://pypi.org/project/datasette-llm-limits/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-llm-limits?include_prereleases&label=changelog)](https://github.com/datasette/datasette-llm-limits/releases)
  [![Tests](https://github.com/datasette/datasette-llm-limits/actions/workflows/test.yml/badge.svg)](https://github.com/datasette/datasette-llm-limits/actions/workflows/test.yml)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-llm-limits/blob/main/LICENSE)

  A Datasette plugin for enforcing **windowed spending caps** on LLM usage.

  `datasette-llm-limits` plugs into [`datasette-llm-accountant`](https://github.com/datasette/datasette-llm-accountant)
  as an `Accountant` implementation, so every prompt issued through `datasette-llm`
  is checked against your configured caps before it is allowed to run.

  It is designed for the case of **time-windowed caps that reset automatically** —
  "no single user may spend more than $1.00 per day", or "the whole instance is
  capped at $50 per calendar month".

  For a top-up-style refillable balance, see the sibling project
  `datasette-llm-allowance`.

  ## Installation

  Install this plugin in the same environment as Datasette:

  ```bash
  datasette install datasette-llm-limits
  ```

  This will also install `datasette-llm-accountant`, which is required.

  ## Configuration

  All configuration lives in `datasette.yaml` under
  `plugins.datasette-llm-limits.limits`. Each entry is a named limit; on every
  reservation the plugin checks **every matching limit** and rejects the call if
  any one would be exceeded.

  ```yaml
  plugins:
    datasette-llm-limits:
      limits:
        # Per-user, rolling 24 hours
        per-user-daily:
          scope: actor
          window: rolling-24h
          amount_usd: 1.00

        # Per-user, calendar month (resets at UTC midnight on day 1)
        per-user-monthly:
          scope: actor
          window: calendar-month
          amount_usd: 20.00

        # Per-actor cap that only applies to one purpose
        enrichments-per-user-daily:
          scope: actor
          window: rolling-24h
          amount_usd: 5.00
          purpose: enrichments

        # Instance-wide cap, any actor, any purpose
        instance-monthly:
          scope: instance
          window: calendar-month
          amount_usd: 250.00

        # Per-model cap (e.g. limit expensive models per actor)
        gpt5-pro-per-user-weekly:
          scope: actor
          window: rolling-7d
          amount_usd: 10.00
          model_id: gpt-5-pro
  ```

  ### Field reference

  | Field        | Type     | Required | Description |
  |--------------|----------|----------|-------------|
  | `scope`      | string   | yes      | One of `actor` or `instance`. `actor` partitions usage by `actor_id`; `instance` aggregates across every caller. |
  | `window`     | string   | yes      | One of `rolling-24h`, `rolling-7d`, `rolling-30d`, `calendar-day`, `calendar-week`, `calendar-month`. Rolling windows look back N seconds from now. Calendar windows reset at UTC midnight on the appropriate boundary. |
  | `amount_usd` | number   | yes      | Cap in US dollars. Stored internally in nanocents. |
  | `purpose`    | string   | no       | If set, the limit only applies when the prompt's `purpose` matches. Omit to apply regardless of purpose. |
  | `model_id`   | string   | no       | If set, the limit only applies when the prompt's `model_id` matches. Omit to apply regardless of model. |

  A limit "matches" a reservation when:

  - `purpose` is unset OR equals the prompt's purpose, AND
  - `model_id` is unset OR equals the prompt's model id, AND
  - `scope` is `instance`, OR (`scope` is `actor` AND the reservation has a
    non-empty `actor_id`).

  Reservations made by unauthenticated callers (no `actor_id`) skip any
  `scope: actor` limits but still count toward `scope: instance` limits.

  ### Validation

  Configuration is validated at startup. Unknown fields, unknown `scope` or
  `window` values, missing required fields, or non-positive `amount_usd` raise
  `ValueError` before Datasette starts.

  ## How rejections look

  When a reservation would exceed any matching limit, the call is rejected with an
  `InsufficientBalanceError` whose message follows this format:

  ```
  Limit "per-user-daily" exceeded: $0.95 used of $1.00 in rolling-24h.
  ```

  For calendar windows the message also tells the caller when the window resets:

  ```
  Limit "per-user-monthly" exceeded: $19.80 used of $20.00 in calendar-month.
  Try again after 2026-04-01T00:00:00Z.
  ```

  `datasette-llm` will surface this message to the end user.

  ## The `/-/llm-limits` inspection view

  The plugin registers a read-only view at `/-/llm-limits` that lists every
  configured limit. Instance-scoped limits include their current usage, remaining
  headroom, and the time of the next reset (for calendar windows). Actor-scoped
  limits show their cap as a per-actor cap, with usage broken out in a separate
  actor usage table. The view also shows the 50 most recent transactions.

  The view supports both HTML (default) and JSON:

  ```bash
  curl -H "Accept: application/json" http://localhost:8001/-/llm-limits
  # or:
  curl http://localhost:8001/-/llm-limits?_format=json
  ```

  ### Permissions

  The view is gated by a plugin-defined permission, `datasette-llm-limits-view`,
  which defaults to **deny**. Grant it in `datasette.yaml`:

  ```yaml
  permissions:
    datasette-llm-limits-view:
      id: "*"          # any signed-in actor
      # or: id: ["github:9599"] for specific actor ids
  ```

  Anyone without this permission gets a 403.

  ## Storage

  The plugin keeps its state in Datasette's internal database (the path passed
  via `--internal`). On startup it creates an `llm_limits_tx` table that records
  every reservation and settlement. Rows for rolled-back transactions remain in
  the audit trail with `settled_nanocents = 0`.

  ```sql
  CREATE TABLE llm_limits_tx (
      id TEXT PRIMARY KEY,                -- ULID for monotonic ordering
      created_at TEXT NOT NULL,           -- ISO-8601 UTC
      settled_at TEXT,                    -- ISO-8601 UTC; NULL while reserved
      actor_id TEXT,                      -- NULL for unauthenticated
      purpose TEXT,                       -- NULL when not provided
      model_id TEXT,                      -- NULL when not provided
      reserved_nanocents INTEGER NOT NULL,
      settled_nanocents INTEGER,          -- NULL while reserved
      matched_limits TEXT NOT NULL        -- JSON array of limit names
  );
  ```

  If you have `view-database` on `_internal`, you can query `llm_limits_tx`
  directly to build your own reports.

  ## How it fits with `datasette-llm-accountant`

  `datasette-llm-limits` does not perform pricing or wrap LLM calls itself — that
  is the job of `datasette-llm-accountant`. The accountant calls into this
  plugin's `reserve` / `settle` / `rollback` methods, and this plugin enforces
  the configured caps.

  You may stack multiple accountants: for example, `datasette-llm-allowance` for
  a refillable balance plus `datasette-llm-limits` for a daily cap. A prompt only
  goes through if **every** registered accountant approves it.

  ## Development

  To set up this plugin locally, check out the code, then:

  ```bash
  cd datasette-llm-limits
  # Confirm the plugin is visible
  uv run datasette plugins
  ```

  To run the tests:

  ```bash
  uv run pytest
  ```

  To format and lint:

  ```bash
  uv run --with black black --target-version py311 .
  uv run --with ruff ruff check .
  ```

project_urls:
  {"CI": "https://github.com/datasette/datasette-llm-limits/actions", "Changelog": "https://github.com/datasette/datasette-llm-limits/releases", "Homepage": "https://github.com/datasette/datasette-llm-limits", "Issues": "https://github.com/datasette/datasette-llm-limits/issues"}


name: datasette-llm-usage
summary: Track usage of LLM tokens in a SQLite table
classifiers: ["Framework :: Datasette"]
author: Simon Willison
description_content_type: text/markdown
license_expression: Apache-2.0
package_url: https://pypi.org/project/datasette-llm-usage/
project_url: https://pypi.org/project/datasette-llm-usage/
release_url: https://pypi.org/project/datasette-llm-usage/0.2a0/
requires_dist: ["datasette>=1.0a21", "datasette-llm>=0.1a5", "sqlite-migrate"]
requires_python: >=3.10
version: 0.2a0
description:
  # datasette-llm-usage

  [![PyPI](https://img.shields.io/pypi/v/datasette-llm-usage.svg)](https://pypi.org/project/datasette-llm-usage/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-llm-usage?include_prereleases&label=changelog)](https://github.com/datasette/datasette-llm-usage/releases)
  [![Tests](https://github.com/datasette/datasette-llm-usage/actions/workflows/test.yml/badge.svg)](https://github.com/datasette/datasette-llm-usage/actions/workflows/test.yml)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-llm-usage/blob/main/LICENSE)

  Track usage of LLM tokens in a SQLite table

  ## Installation

  Install this plugin in the same environment as Datasette.
  ```bash
  datasette install datasette-llm-usage
  ```
  ## Usage

  This plugin tracks LLM token usage in Datasette via the [datasette-llm](https://github.com/datasette/datasette-llm) `llm_prompt_context` hook. It creates two tables:

  - `llm_usage`: Tracks token usage per request (model, purpose, actor, input/output tokens)
  - `llm_usage_prompt_log`: Optionally logs full prompts and responses

  ### Configuration

  By default the tables are created in the internal database passed to Datasette using `--internal internal.db`. You can change that by setting the following in your Datasette plugin configuration:

  ```json
  {
      "plugins": {
          "datasette-llm-usage": {
              "database": "your_database_name"
          }
      }
  }
  ```

  To enable full prompt and response logging, set `log_prompts` to `true`:

  ```json
  {
      "plugins": {
          "datasette-llm-usage": {
              "log_prompts": true
          }
      }
  }
  ```

  ### Prompt testing tool

  The plugin also provides a simple demo page at `/-/llm-usage-simple-prompt` which can be used to execute prompts for testing.

  That page requires the user to have the `llm-usage-simple-prompt` permission.

  ## Development

  Clone this repo and run the tests:

  ```bash
  cd datasette-llm-usage
  uv run pytest
  ```

project_urls:
  {"CI": "https://github.com/datasette/datasette-llm-usage/actions", "Changelog": "https://github.com/datasette/datasette-llm-usage/releases", "Homepage": "https://github.com/datasette/datasette-llm-usage", "Issues": "https://github.com/datasette/datasette-llm-usage/issues"}


name: datasette-llm
summary: LLM integration for Datasette
classifiers: ["Framework :: Datasette"]
author: Simon Willison
description_content_type: text/markdown
dynamic: ["License-File"]
license_expression: Apache-2.0
license_files: ["LICENSE"]
package_url: https://pypi.org/project/datasette-llm/
project_url: https://pypi.org/project/datasette-llm/
release_url: https://pypi.org/project/datasette-llm/0.1a8/
requires_dist: ["datasette>=1.0a23", "llm>=0.32a1", "datasette-secrets"]
requires_python: >=3.10
version: 0.1a8
description:
  # datasette-llm

  [![PyPI](https://img.shields.io/pypi/v/datasette-llm.svg)](https://pypi.org/project/datasette-llm/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-llm?include_prereleases&label=changelog)](https://github.com/datasette/datasette-llm/releases)
  [![Tests](https://github.com/datasette/datasette-llm/actions/workflows/test.yml/badge.svg)](https://github.com/datasette/datasette-llm/actions/workflows/test.yml)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-llm/blob/main/LICENSE)

  LLM integration for Datasette plugins.

  This plugin provides a standard interface for Datasette plugins to use LLM models via the [llm](https://llm.datasette.io/) library, with:

  - **Model management**: Control which models are available, with filtering, defaults, and model options
  - **API key management**: Integration with [datasette-secrets](https://github.com/datasette/datasette-secrets) for secure key storage
  - **Hooks for extensibility**: Track usage, enforce policies, implement accounting

  ## Installation

  Install this plugin in the same environment as Datasette:

  ```bash
  datasette install datasette-llm
  ```

  You'll also need at least one LLM model plugin installed:

  ```bash
  # For OpenAI models
  datasette install llm

  # For Anthropic models
  datasette install llm-anthropic

  # For testing without API calls
  datasette install llm-echo
  ```

  ## Configuration

  Configure the plugin in your `datasette.yaml`:

  ```yaml
  plugins:
    datasette-llm:
      # Default model when none specified
      default_model: gpt-5.4-mini

      # Purpose-specific configuration
      purposes:
        enrichments:
          model: gpt-5.4-nano      # Default model for bulk operations
        sql-assistant:
          model: gpt-5.4           # Smarter for complex queries
          models:                  # Only these models for SQL assistance
          - gpt-5.4
          - gpt-5.4-mini
        extract:
          model: claude-sonnet-4.6
          blocked_models:          # Block specific models for extraction
          - gpt-5.4-pro
        chat:
          model: claude-sonnet-4.6

      # Model availability (optional)
      models:                      # Allowlist - only these models available
      - gpt-5.4
      - gpt-5.4-mini
      - gpt-5.4-nano
      - claude-sonnet-4.6

      # Or use a blocklist instead
      blocked_models:
      - gpt-5.4-pro                # Too expensive

      # Only show models with API keys configured (default: true)
      require_keys: true
  ```

  ### Model references with custom API keys and options

  Anywhere a model name string is accepted in configuration (`default_model`, `purposes.<name>.model`, or entries in `purposes.<name>.models`), you can use a dictionary instead.

  The dictionary supports these fields:

  - **`model`**: Required model ID, e.g. `"gpt-5.4-mini"`.
  - **`key`**: Optional [datasette-secrets](https://github.com/datasette/datasette-secrets) secret name to use as the API key for that model.
  - **`options`**: Optional dictionary of default prompt options for that model. These are passed through to the underlying [llm](https://llm.datasette.io/) model plugin, so supported option names depend on the selected model.

  ```yaml
  plugins:
    datasette-llm:
      # Default model with a custom key and default options
      default_model:
        model: gpt-5.4-mini
        key: CUSTOM_OPENAI_KEY
        options:
          temperature: 0.2

      purposes:
        # Pin a purpose to one model with its own billing key and options
        query-assistant:
          model:
            model: gpt-5.4-mini
            key: QUERY_ASSISTANT_KEY
            options:
              temperature: 0

        # Multiple models, each with their own key and/or options
        enrichments:
          model:
            model: gpt-5.4-nano
            key: ENRICHMENTS_NANO_KEY
            options:
              max_tokens: 1000
          models:
            - model: gpt-5.4
              key: ENRICHMENTS_GPT5_KEY
            - model: gpt-5.4-mini
              key: ENRICHMENTS_MINI_KEY
              options:
                temperature: 0.1
            - claude-sonnet-4.6      # Falls through to default key resolution
  ```

  Configured `options` become defaults for calls made through `await llm.model()` or `llm.group()`. Per-call options override configured defaults:

  ```python
  model = await llm.model()

  # Uses temperature: 0.2 from datasette.yaml
  response = await model.prompt("Suggest three tags")

  # Overrides the configured temperature for this one prompt
  response = await model.prompt("Suggest three tags", temperature=0.7)
  ```

  For chained prompts, pass per-call overrides in the `options=` dictionary:

  ```python
  responses = model.chain(
      "Suggest three tags",
      options={"temperature": 0.7},
  )
  ```

  The `key` field is resolved through datasette-secrets, so you can set it via environment variables:

  ```bash
  export DATASETTE_SECRETS_QUERY_ASSISTANT_KEY=sk-...
  export DATASETTE_SECRETS_ENRICHMENTS_NANO_KEY=sk-...
  ```

  When a model is used for a purpose, key resolution follows this order:

  1. Key from the purpose's configuration (a matching dict in `model` or `models`)
  2. Key from the `default_model` config (if it's a dict and matches)
  3. Standard datasette-secrets resolution (`<PROVIDER>_API_KEY`)
  4. llm's key resolution (keys.json, environment variables)

  The `models` (global allowlist) and `blocked_models` fields remain plain string lists — custom keys and options are only supported in `default_model`, `purposes.<name>.model`, and `purposes.<name>.models`.

  Model options use the same configuration resolution order as custom keys: the matching purpose-specific model reference is checked first, then the global `default_model` reference if it names the same model.

  ### Model filtering

  The `models` and `blocked_models` keys control which models are available. Use `models` to define an allowlist (only these models will be available) or `blocked_models` to define a blocklist (all models except these will be available). If both are set, the allowlist is applied first and the blocklist removes from the result. The `default_model` is automatically included in the allowlist, so you don't need to repeat it there.

  ### Purpose-specific configuration

  Plugins register **purposes** to describe what they use LLM models for (e.g. `"extract"`, `"enrichments"`, `"sql-assistant"`). Each purpose can have its own configuration under `purposes.<name>`:

  - **`model`**: The default model for this purpose, used when a plugin calls `await llm.model(purpose="extract")` without specifying a model ID.
  - **`models`**: An allowlist of models for this purpose. When set, this **overrides** the global `models` allowlist — so a model can be available for a specific purpose even if it is not in the global list. The purpose's default `model` is automatically included in this allowlist, so you don't need to repeat it. This filtering is applied when a plugin calls `await llm.models(purpose="extract")`.
  - **`blocked_models`**: A blocklist of models for this purpose. These are removed even if the model is globally allowed.

  When no purpose-specific `models` list is set, the global `models` allowlist applies. The global `blocked_models` always applies regardless of purpose configuration.

  ## API Key Management

  datasette-llm integrates with [datasette-secrets](https://github.com/datasette/datasette-secrets) for API key management. Keys are automatically registered for all installed model providers.

  ### Setting up keys

  1. **Via environment variables** (recommended for deployment):
     ```bash
     export DATASETTE_SECRETS_OPENAI_API_KEY=sk-...
     export DATASETTE_SECRETS_ANTHROPIC_API_KEY=sk-ant-...
     ```

  2. **Via the web interface**: Navigate to `/-/secrets` (requires `manage-secrets` permission)

  3. **Via llm CLI** (fallback): Keys set with `llm keys set openai` are also used

  ### Key resolution order

  1. datasette-secrets (env var `DATASETTE_SECRETS_<PROVIDER>_API_KEY` or encrypted database)
  2. llm's keys.json (`~/.config/io.datasette.llm/keys.json`)
  3. llm's environment variables (e.g., `OPENAI_API_KEY`)

  ## Usage

  All examples below assume you have created an `LLM` instance:

  ```python
  from datasette_llm import LLM

  llm = LLM(datasette)
  ```

  ### `llm.model()`

  ```python
  async def model(
      model_id: Optional[str] = None,
      purpose: Optional[str] = None,
      actor: Optional[dict] = None,
  ) -> WrappedAsyncModel
  ```

  Get an async model wrapped with hook support. Returns a `WrappedAsyncModel` that invokes hooks around prompts.

  Parameters:

  - **`model_id`** (`Optional[str]`): The model to use, e.g. `"gpt-5.4-mini"` or `"claude-sonnet-4.6"`. If not provided, the default model is resolved from configuration — first checking the purpose-specific `model` setting, then the global `default_model`.
  - **`purpose`** (`Optional[str]`): Identifies what this model will be used for, e.g. `"enrichments"`, `"sql-assistant"`. This selects the purpose-specific default model and is passed through to hooks like `llm_prompt_context` for auditing and accounting.
  - **`actor`** (`Optional[dict]`): The Datasette actor dictionary for the current user. Pass this to enable two things: **per-user model filtering**, where plugins using the `llm_filter_models` or `llm_default_model` hooks can restrict or customize models based on who is making the request; and **audit logging**, where auditing plugins that implement `llm_prompt_context` can record which actor ran which prompts.

  ```python
  async def my_plugin_view(datasette, request):
      llm = LLM(datasette)

      # Get the default model
      model = await llm.model()

      # Specify a model explicitly
      model = await llm.model("gpt-5.4-mini")

      # With purpose and actor for auditing and filtering
      model = await llm.model(
          purpose="sql-assistant",
          actor=request.actor,
      )

      # Execute a prompt
      response = await model.prompt("What is the capital of France?")
      text = await response.text()
  ```

  ### `llm.models()`

  ```python
  async def models(
      actor: Optional[dict] = None,
      purpose: Optional[str] = None,
  ) -> List
  ```

  Get available models, filtered by configuration, API key availability, and hooks. Returns a list of model objects.

  Parameters:

  - **`actor`** (`Optional[dict]`): The Datasette actor dictionary. When provided, the `llm_filter_models` hook can use this to return only models the actor is allowed to use — for example, restricting anonymous users to cheaper models, or looking up per-user model allowlists in a database. Auditing plugins can also use this to log which actors are querying model availability.
  - **`purpose`** (`Optional[str]`): When provided, purpose-specific `models` and `blocked_models` configuration is applied. A purpose-specific `models` allowlist **overrides** the global allowlist, so models can be made available for a specific purpose even if they aren't globally listed.

  ```python
  llm = LLM(datasette)

  # Get all available models (filtered by config and key availability)
  models = await llm.models()
  for model in models:
      print(model.model_id)

  # Filter by actor (for per-user permissions)
  models = await llm.models(actor=request.actor)

  # Filter by purpose (applies purpose-specific models/blocked_models config)
  models = await llm.models(purpose="enrichments")

  # Both together — e.g. to populate a model picker for the current user
  models = await llm.models(actor=request.actor, purpose="sql-assistant")
  ```

  ### Streaming responses

  ```python
  model = await llm.model("gpt-5.4-mini")
  response = await model.prompt("Tell me a story")

  # Non-streaming - wait for complete response
  text = await response.text()

  # Streaming - process chunks as they arrive
  async for chunk in response:
      print(chunk, end="", flush=True)
  ```

  ### Grouping prompts

  Use `group()` for batch operations where multiple prompts are logically related:

  ```python
  async def enrich_rows(datasette, rows):
      llm = LLM(datasette)

      # Model determined by purpose configuration
      async with llm.group(purpose="enrichments") as model:
          results = []
          for row in rows:
              response = await model.prompt(f"Summarize: {row['content']}")
              text = await response.text()
              results.append(text)

      # All responses guaranteed complete here
      return results
  ```

  Benefits of `group()`:
  - **Transactional semantics**: All responses forced to complete on exit
  - **Shared context**: Hooks can treat grouped prompts together (e.g., shared budget reservation)
  - **Cleanup**: The `llm_group_exit` hook is called for settlement/logging

  ## Plugin Hooks

  datasette-llm provides hooks for other plugins to extend LLM operations.

  ### `llm_prompt_context`

  Wrap prompt execution with custom logic. The same hook is used for direct
  `prompt()` calls and for each response yielded by `chain()`:

  ```python
  from datasette import hookimpl
  from contextlib import asynccontextmanager

  @hookimpl
  def llm_prompt_context(datasette, model_id, prompt, purpose, actor):
      @asynccontextmanager
      async def wrapper(result):
          # Before the prompt executes
          actor_id = actor.get("id") if actor else None
          print(f"Starting prompt to {model_id} by {actor_id}")

          yield

          # After prompt() returns or chain() is initialized
          async def on_complete(response):
              usage = await response.usage()
              print(f"Used {usage.input} input, {usage.output} output tokens")

          await result.on_response_done(on_complete)

      return wrapper
  ```

  `result.response` continues to expose the first response, while
  `result.responses` contains all responses seen so far. The
  `await result.on_response_done(callback)` helper attaches a callback to all
  existing responses and any future responses produced by a chain.

  ### `llm_group_exit`

  Called when a `group()` context manager exits:

  ```python
  @hookimpl
  def llm_group_exit(datasette, group):
      # Can return a coroutine for async cleanup
      async def cleanup():
          print(f"Group for {group.purpose} completed")
          print(f"Processed {len(group._responses)} prompts")
      return cleanup()
  ```

  ### `register_llm_purposes`

  Register purpose strings that your plugin uses, along with documentation explaining what they mean.

  ```python
  from datasette import hookimpl
  from datasette_llm import Purpose

  @hookimpl
  def register_llm_purposes(datasette):
      return [
          Purpose(
              name="query-assistant",
              description="Assists users with writing SQL queries",
          ),
          Purpose(
              name="suggest-table-names",
              description="Suggests names for tables based on imported CSV files",
          ),
      ]
  ```

  Registered purposes can be retrieved by other plugins (e.g., to build an admin UI for model assignment):

  ```python
  from datasette_llm import get_purposes

  purposes = get_purposes(datasette)
  for purpose in purposes:
      print(f"{purpose.name}: {purpose.description}")
  ```

  If multiple plugins register the same purpose name, the first registration wins.

  ### `llm_filter_models`

  Influence the models that are returned from the `await llm.models()` method. Plugins can use this to add custom logic informing which models are available, taking into account both the actor and the purpose of the prompt.

  - `models` is a list of available model objects from all of the installed [LLM plugins](https://llm.datasette.io/en/stable/plugins/directory.html).
  - `actor` is an actor dictionary or `None`
  - `purpose` is a purpose string or `None`

  The `actor` and `purpose` are the ones that were passed to the `llm.models(actor=..., purpose=...)` method.

  ```python
  @hookimpl
  async def llm_filter_models(datasette, models, actor, purpose):
      if not actor:
          # Anonymous users get limited models
          return [m for m in models if m.model_id == "gpt-5.4-mini"]

      # Check database for user's allowed models
      db = datasette.get_database()
      result = await db.execute(
          "SELECT model_id FROM user_models WHERE user_id = ?",
          [actor["id"]]
      )
      allowed = {row["model_id"] for row in result.rows}
      return [m for m in models if m.model_id in allowed]
  ```

  ### `llm_default_model`

  This plugin hook is used when `await llm.model()` is called without any arguments - or with a `purpose` and/or `actor` specified. Plugins can use this to control which default model is used, including for a given purpose.

  ```python
  @hookimpl
  async def llm_default_model(datasette, purpose, actor):
      if actor:
          # Check user's preferred model
          db = datasette.get_database()
          result = await db.execute(
              "SELECT preferred_model FROM user_prefs WHERE user_id = ?",
              [actor["id"]]
          )
          row = result.first()
          if row:
              return row["preferred_model"]
      return None  # Use config defaults
  ```

  ## Related Plugins

  - **[datasette-secrets](https://github.com/datasette/datasette-secrets)**: Secure API key storage (required dependency)
  - **[datasette-llm-accountant](https://github.com/datasette/datasette-llm-accountant)**: Budget management and cost tracking

  ## Development

  To set up this plugin locally:

  ```bash
  cd datasette-llm
  uv sync

  # Confirm the plugin is visible
  uv run datasette plugins
  ```

  To run the tests:

  ```bash
  uv run pytest
  ```

  The test suite uses the `llm-echo` model which echoes back prompts without making API calls.

project_urls:
  {"CI": "https://github.com/datasette/datasette-llm/actions", "Changelog": "https://github.com/datasette/datasette-llm/releases", "Homepage": "https://github.com/datasette/datasette-llm", "Issues": "https://github.com/datasette/datasette-llm/issues"}


name: datasette-load
summary: API and UI for bulk loading data into Datasette from a URL
classifiers: ["Framework :: Datasette"]
author: Datasette
description_content_type: text/markdown
dynamic: ["License-File"]
license_expression: Apache-2.0
license_files: ["LICENSE"]
package_url: https://pypi.org/project/datasette-load/
project_url: https://pypi.org/project/datasette-load/
provides_extra: ["test"]
release_url: https://pypi.org/project/datasette-load/0.1a5/
requires_python: >=3.10
version: 0.1a5
description:
  # datasette-load

  [![PyPI](https://img.shields.io/pypi/v/datasette-load.svg)](https://pypi.org/project/datasette-load/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-load?include_prereleases&label=changelog)](https://github.com/datasette/datasette-load/releases)
  [![Tests](https://github.com/datasette/datasette-load/actions/workflows/test.yml/badge.svg)](https://github.com/datasette/datasette-load/actions/workflows/test.yml)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-load/blob/main/LICENSE)

  API and UI for bulk loading data into Datasette from a URL

  ## Installation

  Install this plugin in the same environment as Datasette.
  ```bash
  datasette install datasette-load
  ```

  ## Configuration

  This plugin does not require configuration - by default it downloads files to the system temp directory and swaps them into the current working directory once they have been verified as valid SQLite.

  The plugin provides two optional settings to control which directories are used here:

  ```yaml
  plugins:
    datasette-load:
      staging_directory: /tmp
      database_directory: /home/location
  ```

  `staging_directory` is used for the initial download. Files will be deleted from here if the download fails.

  If the download succeeds (and the database integrity check passes) the file will be moved into the `database_directory` folder. This defaults to the directory in which the Datasette application was started if you do not otherwise configure it.

  To enable WAL mode on the database once it has been saved to the `database_directory` include the `enable_wal: true` option:

  ```yaml
  plugins:
    datasette-load:
      database_directory: /home/location
      enable_wal: true
  ```

  ## Usage

  Users and API tokens with the `datasette-load` permission can visit `/-/load` where they can provide a URL to a SQLite database file and the name it should use within Datasette to trigger a download of that SQLite database.

  You can assign that permission to the `root` user by starting Datasette like this:

  ```bash
  datasette -s permissions.datasette-load.id root --root
  ```
  Or with the following configuration in the `datasette -c datasette.yaml` file:
  ```yaml
  permissions:
    datasette-load:
      id: root
  ```
  API tokens with that permission can use this API:

  ```
  POST /-/load
  {"url": "https://s3.amazonaws.com/til.simonwillison.net/tils.db", "name": "tils"}
  ```
  You can optionally include additional HTTP headers to be used when fetching the URL:
  ```
  POST /-/load
  {
    "url": "https://example.com/db.sqlite",
    "name": "db",
    "headers": {"Authorization": "Bearer XXX"}
  }
  ```
  This tells Datasette to download the SQLite database from the given URL and use it to create (or replace) the `/tils` database in the Datasette instance.

  That API endpoint returns:
  ```json
  {
    "id": "1D2A2328-199E-4D4D-AF3B-967131ADB795",
    "url": "https://s3.amazonaws.com/til.simonwillison.net/tils.db",
    "name": "tils",
    "done": false,
    "error": null,
    "todo_bytes": 20250624,
    "done_bytes": 0,
    "status_url": "https://blah.datasette/-/load/status/1D2A2328-199E-4D4D-AF3B-967131ADB795"
  }
  ```
  The `status_url` can be polled for completion. It will return the same JSON format.

  When the download has finished the API will return `"done": true` and either `"error": null` if it worked or `"error": "error description"` if something went wrong.

  ## Zip support

  The URL can point to either a SQLite database file or a zip file containing a SQLite database - if a zip file is provided, the largest file in the archive will be extracted and used (after verifying it is a valid SQLite database). For security, the plugin will reject zip files where the largest file would extract to more than 5x the size of the zip file itself.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:
  ```bash
  cd datasette-load
  python -m venv venv
  source venv/bin/activate
  ```
  Now install the dependencies and test dependencies:
  ```bash
  pip install -e '.[test]'
  ```
  To run the tests:
  ```bash
  python -m pytest
  ```

project_urls:
  {"CI": "https://github.com/datasette/datasette-load/actions", "Changelog": "https://github.com/datasette/datasette-load/releases", "Homepage": "https://github.com/datasette/datasette-load", "Issues": "https://github.com/datasette/datasette-load/issues"}

requires_dist:
  ["datasette>=1.0a21", "pytest; extra == \"test\"", "pytest-asyncio; extra == \"test\"", "pytest-httpx; extra == \"test\""]


name: datasette-low-disk-space-hook
summary: Datasette plugin providing the low_disk_space hook for other plugins to check for low disk space
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-low-disk-space-hook
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-low-disk-space-hook/
project_url: https://pypi.org/project/datasette-low-disk-space-hook/
release_url: https://pypi.org/project/datasette-low-disk-space-hook/0.1/
requires_dist: ["datasette", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'"]
requires_python: >=3.7
version: 0.1
description:
  # datasette-low-disk-space-hook

  [![PyPI](https://img.shields.io/pypi/v/datasette-low-disk-space-hook.svg)](https://pypi.org/project/datasette-low-disk-space-hook/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-low-disk-space-hook?include_prereleases&label=changelog)](https://github.com/simonw/datasette-low-disk-space-hook/releases)
  [![Tests](https://github.com/simonw/datasette-low-disk-space-hook/workflows/Test/badge.svg)](https://github.com/simonw/datasette-low-disk-space-hook/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-low-disk-space-hook/blob/main/LICENSE)

  Datasette plugin providing the low_disk_space hook for other plugins to check for low disk space

  ## Installation

  Install this plugin in the same environment as Datasette.

      datasette install datasette-low-disk-space-hook

  You are unlikely to need to install this plugin directly though: it will usually be automatically be installed as a dependency of another plugin.

  ## Usage

  This plugin adds a new plugin hook to Datasette called `low_disk_space(datasette=datasette)`.

  It also adds a new `space_is_running_low(datasette)` utility function which can be called to check if the Datasette instance is running out of space:

  ```python
  from datasette_low_disk_space_hook import space_is_running_low

  if await space_is_running_low(datasette):
      print("Disk space is running low")
  ```
  The idea is for plugins such as `datasette-upload-csvs` and `datasette-socrata` to call this hook before writing any new data into Datasette, to check if they should continue.

  Other plugins can then implement the hook to warn when Datasette is running out of space.

  Working together, this will help plugins avoid filling the disk entirely with data which could cause Datasette instances to crash.

  ## Implementing the hook

  An implementation of this hook (in yet another plugin) looks like this:

  ```python
  from datasette import hookimpl
  import shutil

  @hookimpl
  def low_disk_space(self, datasette):
      usage = shutil.disk_usage("/mnt")
      # Fail at 95% or more used
      if (usage.used / usage.total) > 0.95:
          return True
  ```
  The plugin can also return an `async` function if it needs to use `await` as part of its execution:
  ```python
  from datasette import hookimpl
  from somewhere import check_disk_usage_percentage_async

  @hookimpl
  def low_disk_space(self, datasette):
      async def inner():
          usage = await check_disk_usage_percentage_async("/mnt")
          if usage > 0.95:
              return True
      return inner
  ```

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:

      cd datasette-low-disk-space-hook
      python3 -m venv venv
      source venv/bin/activate

  Now install the dependencies and test dependencies:

      pip install -e '.[test]'

  To run the tests:

      pytest

project_urls:
  {"CI": "https://github.com/simonw/datasette-low-disk-space-hook/actions", "Changelog": "https://github.com/simonw/datasette-low-disk-space-hook/releases", "Homepage": "https://github.com/simonw/datasette-low-disk-space-hook", "Issues": "https://github.com/simonw/datasette-low-disk-space-hook/issues"}


name: datasette-matomo
summary: Add Matomo Web Analytics tracking code to Datasette
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: CentreForDigitalHumanities
author_email: digitalhumanities@uu.nl
description_content_type: text/markdown
home_page: https://github.com/CentreForDigitalHumanities/datasette-matomo
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-matomo/
project_url: https://pypi.org/project/datasette-matomo/
release_url: https://pypi.org/project/datasette-matomo/0.1.2/
requires_dist: ["datasette", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'"]
requires_python: >=3.7
version: 0.1.2
description:
  # datasette-matomo

  [![PyPI](https://img.shields.io/pypi/v/datasette-matomo.svg)](https://pypi.org/project/datasette-matomo/)
  [![Changelog](https://img.shields.io/github/v/release/UUDigitalHumanitieslab/datasette-matomo?include_prereleases&label=changelog)](https://github.com/UUDigitalHumanitieslab/datasette-matomo/releases)
  [![Tests](https://github.com/UUDigitalHumanitieslab/datasette-matomo/workflows/Test/badge.svg)](https://github.com/UUDigitalHumanitieslab/datasette-matomo/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/UUDigitalHumanitieslab/datasette-matomo/blob/main/LICENSE)

  [Datasette plugin](https://datasette.io/) that adds [Matomo Web Analytics](https://matomo.org/) JavaScript tracking code to a Datasette application.

  ## Installation

  Install this plugin in the same environment as Datasette.

      datasette install datasette-matomo

  ## Usage

  Make your configuration available to the Datasette process as environment variables:

      DATASETTE_MATOMO_SERVER_URL=https://example.com/
      DATASETTE_MATOMO_SITE_ID=1

  The server URL must include a final slash.

  If you use `datasette publish`:

      datasette publish heroku example.db --install datasette-matomo \
          --plugin-secret datasette-matomo server_url http://example.com/ \
          --plugin-secret datasette-matomo site_id 1

  Note: not all `datasette publish` providers currently support the `--plugin-secret` option.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:

      cd datasette-matomo
      python3 -m venv venv
      source venv/bin/activate

  Now install the dependencies and test dependencies:

      pip install -e '.[test]'

  To run the tests:

      pytest

project_urls:
  {"CI": "https://github.com/CentreForDigitalHumanities/datasette-matomo/actions", "Changelog": "https://github.com/CentreForDigitalHumanities/datasette-matomo/releases", "Homepage": "https://github.com/CentreForDigitalHumanities/datasette-matomo", "Issues": "https://github.com/CentreForDigitalHumanities/datasette-matomo/issues"}


name: datasette-media
summary: Datasette plugin for serving media based on a SQL query
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-media
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-media/
project_url: https://pypi.org/project/datasette-media/
release_url: https://pypi.org/project/datasette-media/0.5.1/
version: 0.5.1
description:
  # datasette-media

  [![PyPI](https://img.shields.io/pypi/v/datasette-media.svg)](https://pypi.org/project/datasette-media/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-media?include_prereleases&label=changelog)](https://github.com/simonw/datasette-media/releases)
  [![Tests](https://github.com/simonw/datasette-media/workflows/Test/badge.svg)](https://github.com/simonw/datasette-media/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-media/blob/main/LICENSE)

  Datasette plugin for serving media based on a SQL query.

  Use this when you have a database table containing references to files on disk - or binary content stored in BLOB columns - that you would like to be able to serve to your users.

  ## Installation

  Install this plugin in the same environment as Datasette.

      $ pip install datasette-media

  ### HEIC image support

  Modern iPhones save their photos using the [HEIC image format](https://en.wikipedia.org/wiki/High_Efficiency_Image_File_Format). Processing these images requires an additional dependency, [pyheif](https://pypi.org/project/pyheif/). You can include this dependency by running:

      $ pip install datasette-media[heif]

  ## Usage

  You can use this plugin to configure Datasette to serve static media based on SQL queries to an underlying database table.

  Media will be served from URLs that start with `/-/media/`. The full URL to each media asset will look like this:

      /-/media/type-of-media/media-key

  `type-of-media` will correspond to a configured SQL query, and might be something like `photo`. `media-key` will be an identifier that is used as part of the underlying SQL query to find which file should be served.

  ### Serving static files from disk

  The following ``metadata.json`` configuration will cause this plugin to serve files from disk, based on queries to a database table called `apple_photos`.

  ```json
  {
      "plugins": {
          "datasette-media": {
              "photo": {
                  "sql": "select filepath from apple_photos where uuid=:key"
              }
          }
      }
  }
  ```

  A request to `/-/media/photo/CF972D33-5324-44F2-8DAE-22CB3182CD31` will execute the following SQL query:

  ```sql
  select filepath from apple_photos where uuid=:key
  ```

  The value from the URL -  in this case `CF972D33-5324-44F2-8DAE-22CB3182CD31` - will be passed as the `:key` parameter to the query.

  The query returns a `filepath` value that has been read from the table. The plugin will then read that file from disk and serve it in response to the request.

  SQL queries default to running against the first connected database. You can specify a different database to execute the query against using `"database": "name_of_db"`. To execute against `photos.db`, use this:

  ```json
  {
      "plugins": {
          "datasette-media": {
              "photo": {
                  "sql": "select filepath from apple_photos where uuid=:key",
                  "database": "photos"
              }
          }
      }
  }
  ```

  See [dogsheep-photos](https://github.com/dogsheep/dogsheep-photos) for an example of an application that can benefit from this plugin.

  ### Serving binary content from BLOB columns

  If your SQL query returns a `content` column, this will be served directly to the user:

  ```json
  {
      "plugins": {
          "datasette-media": {
              "photo": {
                  "sql": "select thumbnail as content from photos where uuid=:key",
                  "database": "thumbs"
              }
          }
      }
  }
  ```

  You can also return a `content_type` column which will be used as the `Content-Type` header served to the user:

  ```json
  {
      "plugins": {
          "datasette-media": {
              "photo": {
                  "sql": "select body as content, 'text/html;charset=utf-8' as content_type from documents where id=:key",
                  "database": "documents"
              }
          }
      }
  }
  ```

  If you do not specify a `content_type` the default of `application/octet-stream` will be used.

  ### Serving content proxied from a URL

  To serve content that is itself fetched from elsewhere, return a `content_url` column. This can be particularly useful when combined with the ability to resize images (described in the next section).

  ```json
  {
      "plugins": {
          "datasette-media": {
              "photos": {
                  "sql": "select photo_url as content_url from photos where id=:key",
                  "database": "photos",
                  "enable_transform": true
              }
          }
      }
  }
  ```

  Now you can access resized versions of images from that URL like so:

      /-/media/photos/13?w=200

  ### Setting a download file name

  The `content_filename` column can be returned to force browsers to download the content using a specific file name.

  ```json
  {
      "plugins": {
          "datasette-media": {
              "hello": {
                  "sql": "select 'Hello ' || :key as content, 'hello.txt' as content_filename"
              }
          }
      }
  }
  ```

  Visiting `/-/media/hello/Groot` will cause your browser to download a file called `hello.txt` containing the text `Hello Groot`.

  ### Resizing or transforming images

  Your SQL query can specify that an image should be resized and/or converted to another format by returning additional columns. All three are optional.

  * `resize_width` - the width to resize the image to
  * `resize_width` - the height to resize the image to
  * `output_format` - the output format to use (e.g. `jpeg` or `png`) - any output format [supported by Pillow](https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html) is allowed here.

  If you specify one but not the other of `resize_width` or `resize_height` the unspecified one will be calculated automatically to maintain the aspect ratio of the image.

  Here's an example configuration that will resize all images to be JPEGs that are 200 pixels in height:

  ```json
  {
      "plugins": {
          "datasette-media": {
              "photo": {
                  "sql": "select filepath, 200 as resize_height, 'jpeg' as output_format from apple_photos where uuid=:key",
                  "database": "photos"
              }
          }
      }
  }
  ```

  If you enable the `enable_transform` configuration option you can instead specify transform parameters at runtime using querystring parameters. For example:

  - `/-/media/photo/CF972D33?w=200` to resize to a fixed width
  - `/-/media/photo/CF972D33?h=200` to resize to a fixed height
  - `/-/media/photo/CF972D33?format=jpeg` to convert to JPEG

  That option is added like so:

  ```json
  {
      "plugins": {
          "datasette-media": {
              "photo": {
                  "sql": "select filepath from apple_photos where uuid=:key",
                  "database": "photos",
                  "enable_transform": true
              }
          }
      }
  }
  ```

  The maximum allowed height or width is 4000 pixels. You can change this limit using the `"max_width_height"` option:

  ```json
  {
      "plugins": {
          "datasette-media": {
              "photo": {
                  "sql": "select filepath from apple_photos where uuid=:key",
                  "database": "photos",
                  "enable_transform": true,
                  "max_width_height": 1000
              }
          }
      }
  }
  ```

  ## Configuration

  In addition to the different named content types, the following special plugin configuration setting is available:

  - `transform_threads` - number of threads to use for running transformations (e.g. resizing). Defaults to 4.

  This can be used like this:

  ```json
  {
      "plugins": {
          "datasette-media": {
              "photo": {
                  "sql": "select filepath from apple_photos where uuid=:key",
                  "database": "photos"
              },
              "transform_threads": 8
          }
      }
  }
  ```

project_urls:
  {"CI": "https://github.com/simonw/datasette-media/actions", "Changelog": "https://github.com/simonw/datasette-media/releases", "Homepage": "https://github.com/simonw/datasette-media", "Issues": "https://github.com/simonw/datasette-media/issues"}

requires_dist:
  ["datasette (>=0.44)", "Pillow (>=7.1.2)", "httpx (>=0.13.3)", "pyheif (>=0.4) ; extra == 'heif'", "asgiref ; extra == 'test'", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'", "sqlite-utils ; extra == 'test'", "pytest-httpx (>=0.4.0) ; extra == 'test'"]


name: datasette-ml
summary: A Datasette plugin providing an MLOps platform to train, eval and predict machine learning models
author: Romain Clement
description_content_type: text/markdown
home_page: https://github.com/rclement/datasette-ml
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-ml/
project_url: https://pypi.org/project/datasette-ml/
release_url: https://pypi.org/project/datasette-ml/0.1.2/
requires_dist: ["datasette", "sqlite-ml (==0.1.2)"]
requires_python: >=3.8.1,<4.0.0
version: 0.1.2
classifiers:
  ["License :: Other/Proprietary License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.9"]

description:
  # Datasette ML

  > Bringing Machine Learning models near your data, not the other way around!

  Datasette ML is a [Datasette](https://datasette.io) plugin providing an MLOps
  platform to train, evaluate and make predictions from machine learning models.

  All the underlying features are provided by [`sqlite-ml`](https://github.com/rclement/sqlite-ml).

  [![PyPI](https://img.shields.io/pypi/v/datasette-ml.svg)](https://pypi.org/project/datasette-ml/)
  [![CI/CD](https://github.com/rclement/datasette-ml/actions/workflows/ci-cd.yml/badge.svg)](https://github.com/rclement/datasette-ml/actions/workflows/ci-cd.yml)
  [![Coverage Status](https://img.shields.io/codecov/c/github/rclement/datasette-ml)](https://codecov.io/gh/rclement/datasette-ml)
  [![License](https://img.shields.io/github/license/rclement/datasette-ml)](https://github.com/rclement/datasette-ml/blob/master/LICENSE)

  <!-- Try out a live demo at [https://datasette-ml-demo.vercel.app](https://datasette-ml-demo.vercel.app/-/dashboards) -->

  **WARNING**: this plugin is still experimental and not ready for production.
  Some breaking changes might happen between releases before reaching a stable version.
  Use it at your own risks!

  <!-- ![Datasette ML Demo](https://raw.githubusercontent.com/rclement/datasette-ml/master/demo/datasette-ml-demo.png) -->

  ## Installation

  Install this plugin in the same environment as Datasette:

  ```bash
  $ datasette install datasette-ml
  ```

  ## Usage

  Define configuration within `metadata.yml` / `metadata.json`:

  ```yaml
  plugins:
    datasette-ml:
      db: sqml
  ```

  A new menu entry is now available, pointing at `/-/ml` to access the MLOps dashboard.

  ### Configuration properties

  | Property | Type     | Description                                     |
  | -------- | -------- | ----------------------------------------------- |
  | `db`     | `string` | Database to store ML models (default is `sqml`) |

  ## Tutorial

  Using `datasette-ml` you can start training Machine Learning models directly
  along your data, simply by using custom SQL functions! Let's get started by
  training a classifier against the famous "Iris Dataset" to predict flower types.

  ### Loading the dataset

  First let's load our data. For a real world project, your data may live with its
  own table or being accessed through an SQL view. For the purpose of this tutorial,
  we can use the `sqml_load_dataset` function to load
  [standard Scikit-Learn datasets](https://scikit-learn.org/stable/modules/classes.html#module-sklearn.datasets):

  ```sql
  SELECT sqml_load_dataset('iris') AS dataset;
  ```

  It will return the following data:

  | dataset |
  | --- |
  | {"table": "dataset_iris", "feature_names": ["sepal length (cm)", "sepal width (cm)", "petal length (cm)", "petal width (cm)"], "target_names": ["setosa", "versicolor", "virginica"], "size": 150} |

  The Iris dataset is loaded into a table nammed `dataset_iris`,
  containing 150 examples, 4 features and 3 classes to be predicted.

  ### Training a classifier

  Now that our dataset is ready, let's train a first machine learning model to
  perform a classification task using the `sqml_train` function:

  ```sql
  SELECT sqml_train(
    'Iris prediction',
    'classification',
    'logistic_regression',
    'dataset_iris',
    'target'
  ) AS training;
  ```

  It will return the following data:

  | training |
  | --- |
  | {"experiment_name": "Iris prediction", "prediction_type": "classification", "algorithm": "logistic_regression", "deployed": true, "score": 0.9473684210526315} |

  We have just trained our first machine learning model! The output data informs us
  that our model has been trained, yields a score of 0.94 and has been deployed.

  ### Performing predictions

  Now that we have trained our classifier, let's use it to make predictions!

  Predict the target label for the first row of `dataset_iris` using the
  `sqml_predict` function:

  ```sql
  SELECT
    dataset_iris.*,
    sqml_predict(
      'Iris prediction',
      json_object(
        'sepal length (cm)', [sepal length (cm)],
        'sepal width (cm)', [sepal width (cm)],
        'petal length (cm)', [petal length (cm)],
        'petal width (cm)', [petal width (cm)]
      )
    ) AS prediction
  FROM dataset_iris
  LIMIT 1;
  ```

  This will output the following data:

  | sepal length (cm) | sepal width (cm) | petal length (cm) | petal width (cm) | target | prediction |
  | --- | --- | --- | --- | --- | --- |
  | 5.1 | 3.5 | 1.4 | 0.2 | 0.0 | 0.0 |

  Yay! Our prediction is matching the target label!

  Let's see if we can find some predictions not matching the target label.
  To perform lots of predictions, we will use `sqml_predict_batch` which is more
  efficient than `sqml_predict`:

  ```sql
  SELECT
    dataset_iris.*,
    batch.value AS prediction,
    dataset_iris.target = batch.value AS match
  FROM
    dataset_iris
    JOIN json_each (
      (
        SELECT
          sqml_predict_batch(
            'Iris prediction',
            json_group_array(
              json_object(
                'sepal length (cm)', [sepal length (cm)],
                'sepal width (cm)', [sepal width (cm)],
                'petal length (cm)', [petal length (cm)],
                'petal width (cm)', [petal width (cm)]
              )
            )
          )
        FROM
          dataset_iris
      )
    ) batch ON (batch.rowid + 1) = dataset_iris.rowid
  WHERE match = FALSE;
  ```

  This will yield the following output data:

  | sepal length (cm) | sepal width (cm) | petal length (cm) | petal width (cm) | target | prediction | match |
  | --- | --- | --- | --- | --- | --- | --- |
  | 5.9 | 3.2 | 4.8 | 1.8 | 1.0 | 2.0 | 0 |
  | 6.7 | 3.0 | 5.0 | 1.7 | 1.0 | 2.0 | 0 |
  | 6.0 | 2.7 | 5.1 | 1.6 | 1.0 | 2.0 | 0 |
  | 4.9 | 2.5 | 4.5 | 1.7 | 2.0 | 1.0 | 0 |

  Oh no! 4 predictions have not predicted the correct target label!

  Let's see if we can train a better algorithm to enhance the prediction quality.

  ### Training a new model

  Let's use a Support Vector Machine algorithm, usually yielding better results
  compared to the more simplistic Logistic Regression:

  ```sql
  SELECT sqml_train(
    'Iris prediction',
    'classification',
    'svc',
    'dataset_iris',
    'target'
  ) AS training;
  ```

  This will yield the following data:

  | training |
  | --- |
  | {"experiment_name": "Iris prediction", "prediction_type": "classification", "algorithm": "svc", "deployed": true, "score": 0.9736842105263158} |

  We can already see that the score of this new model is higher than the previous one and it has been deployed.

  Let's try our new classifier on the same dataset:

  ```sql
  SELECT
    dataset_iris.*,
    batch.value AS prediction,
    dataset_iris.target = batch.value AS match
  FROM
    dataset_iris
    JOIN json_each (
      (
        SELECT
          sqml_predict_batch(
            'Iris prediction',
            json_group_array(
              json_object(
                'sepal length (cm)', [sepal length (cm)],
                'sepal width (cm)', [sepal width (cm)],
                'petal length (cm)', [petal length (cm)],
                'petal width (cm)', [petal width (cm)]
              )
            )
          )
        FROM
          dataset_iris
      )
    ) batch ON (batch.rowid + 1) = dataset_iris.rowid
  WHERE match = FALSE;
  ```

  This will lead the following results:

  | sepal length (cm) | sepal width (cm) | petal length (cm) | petal width (cm) | target | prediction | match |
  | --- | --- | --- | --- | --- | --- | --- |
  | 5.9 | 3.2 | 4.8 | 1.8 | 1.0 | 2.0 | 0 |
  | 6.7 | 3.0 | 5.0 | 1.7 | 1.0 | 2.0 | 0 |
  | 6.0 | 2.7 | 5.1 | 1.6 | 1.0 | 2.0 | 0 |

  Yay! We manage to predict one more target label with this new model!

  Also note that we did not have to do anything to switch to the better model:
  exactly the same query is used to perform the prediction without having to
  specify anything about the new model! This is because new models are deployed
  automatically for the current experiment only if their score outperforms the
  score of the previously deployed model.

  ### SQL functions

  This plugin registers a few SQL functions to perform machine learning model training and predictions:

  `sqml_load_dataset(name, table)`
  - `name: str`: name of the dataset to load
  - `table: str`: (optional) custom table name destination for the dataset

  `sqml_train(experiment_name, prediction_type, algorithm, dataset, target, test_size, split_strategy)`:
  - `experiment_name: str`: name of the experiment to train the model within
  - `prediction_type: str`: prediction task type to be performed for this experiment (`regression`, `classification`)
  - `algorithm: str`: algorithm type to be trained
  - `dataset: str`: name of the table or view containing the dataset
  - `target: str`: name of the column to be treated as target label
  - `test_size: float`: (optional) dataset test size ratio (default is `0.25`)
  - `split_strategy: str`: (optional) dataset train/test split strategy (default is `shuffle`)

  `sqml_predict(experiment_name, features)`
  - `experiment_name: str`: name of the experiment to train the model within
  - `features: json object`: JSON object containing the features

  `sqml_predict_batch(experiment_name, features)`
  - `experiment_name: str`: name of the experiment to train the model within
  - `features: json list`: JSON list containing all feature objects

  ## Development

  To set up this plugin locally, first checkout the code.
  Then create a new virtual environment and the required dependencies:

  ```bash
  poetry shell
  poetry install
  ```

  To run the QA suite:

  ```bash
  black --check datasette_ml tests
  flake8 datasette_ml tests
  mypy datasette_ml tests
  pytest -v --cov=datasette_ml --cov=tests --cov-branch --cov-report=term-missing tests
  ```

  ## Demo

  With the developmnent environment setup, you can run the demo locally:

  ```bash
  python demo/generate.py
  datasette --metadata demo/metadata.yml demo/sqml.db
  ```

  ## Inspiration

  All the things on the internet that have been inspiring this project:

  - [PostgresML](https://postgresml.org)
  - [MLFlow](https://mlflow.org)
  - [SQLite  Run-Time Loadable Extensions](https://www.sqlite.org/loadext.html)
  - [Alex Garcia's `sqlite-loadable-rs`](https://github.com/asg017/sqlite-loadable-rs)
  - [Alex Garcia's SQLite extensions](https://github.com/asg017)
  - [Alex Garcia, "Making SQLite extensions pip install-able"](https://observablehq.com/@asg017/making-sqlite-extensions-pip-install-able)
  - [Max Halford, "Online gradient descent written in SQL"](https://maxhalford.github.io/blog/ogd-in-sql/)
  - [Ricardo Anderegg, "Extending SQLite with Rust"](https://ricardoanderegg.com/posts/extending-sqlite-with-rust/)

  ## License

  Licensed under Apache License, Version 2.0

  Copyright (c) 2023 - present Romain Clement

project_urls:
  {"Homepage": "https://github.com/rclement/datasette-ml", "Repository": "https://github.com/rclement/datasette-ml"}


name: datasette-mp3-audio
summary: Turn .mp3 URLs into an audio player in the Datasette interface
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-mp3-audio
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-mp3-audio/
project_url: https://pypi.org/project/datasette-mp3-audio/
release_url: https://pypi.org/project/datasette-mp3-audio/0.2/
requires_python: >=3.7
version: 0.2
description:
  # datasette-mp3-audio

  [![PyPI](https://img.shields.io/pypi/v/datasette-mp3-audio.svg)](https://pypi.org/project/datasette-mp3-audio/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-mp3-audio?include_prereleases&label=changelog)](https://github.com/simonw/datasette-mp3-audio/releases)
  [![Tests](https://github.com/simonw/datasette-mp3-audio/workflows/Test/badge.svg)](https://github.com/simonw/datasette-mp3-audio/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-mp3-audio/blob/main/LICENSE)

  Turn .mp3 URLs into an audio player in the Datasette interface

  ## Installation

  Install this plugin in the same environment as Datasette.

      datasette install datasette-mp3-audio

  ## Demo

  Try this plugin at [https://scotrail.datasette.io/scotrail/announcements](https://scotrail.datasette.io/scotrail/announcements)

  The demo uses ScotRail train announcements from [matteason/scotrail-announcements-june-2022](https://github.com/matteason/scotrail-announcements-june-2022).

  ## Usage

  Once installed, any cells with a value that ends in `.mp3` and starts with either `http://` or `/` or `https://` will be turned into an embedded HTML audio element like this:

  ```html
  <audio controls src="... value ...">Audio not supported</audio>
  ```

  A "Play X MP3s on this page" button will be added to athe top of any table page listing more than one MP3.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:

      cd datasette-mp3-audio
      python3 -m venv venv
      source venv/bin/activate

  Now install the dependencies and test dependencies:

      pip install -e '.[test]'

  To run the tests:

      pytest

project_urls:
  {"CI": "https://github.com/simonw/datasette-mp3-audio/actions", "Changelog": "https://github.com/simonw/datasette-mp3-audio/releases", "Homepage": "https://github.com/simonw/datasette-mp3-audio", "Issues": "https://github.com/simonw/datasette-mp3-audio/issues"}

requires_dist:
  ["datasette", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'", "sqlite-utils ; extra == 'test'"]


name: datasette-multiline-links
summary: Make multiple newline separated URLs clickable in Datasette
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-multiline-links
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-multiline-links/
project_url: https://pypi.org/project/datasette-multiline-links/
release_url: https://pypi.org/project/datasette-multiline-links/0.1/
requires_dist: ["datasette", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'"]
requires_python: >=3.7
version: 0.1
description:
  # datasette-multiline-links

  [![PyPI](https://img.shields.io/pypi/v/datasette-multiline-links.svg)](https://pypi.org/project/datasette-multiline-links/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-multiline-links?include_prereleases&label=changelog)](https://github.com/simonw/datasette-multiline-links/releases)
  [![Tests](https://github.com/simonw/datasette-multiline-links/workflows/Test/badge.svg)](https://github.com/simonw/datasette-multiline-links/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-multiline-links/blob/main/LICENSE)

  Make multiple newline separated URLs clickable in Datasette

  ## Installation

  Install this plugin in the same environment as Datasette.

      datasette install datasette-multiline-links

  ## Usage

  Once installed, if a cell has contents like this:
  ```
  https://example.com
  Not a link
  https://google.com
  ```
  It will be rendered as:
  ```html
  <a href="https://example.com">https://example.com</a>
  Not a link
  <a href="https://google.com">https://google.com</a>
  ```
  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:

      cd datasette-multiline-links
      python3 -m venv venv
      source venv/bin/activate

  Now install the dependencies and test dependencies:

      pip install -e '.[test]'

  To run the tests:

      pytest

project_urls:
  {"CI": "https://github.com/simonw/datasette-multiline-links/actions", "Changelog": "https://github.com/simonw/datasette-multiline-links/releases", "Homepage": "https://github.com/simonw/datasette-multiline-links", "Issues": "https://github.com/simonw/datasette-multiline-links/issues"}


name: datasette-no-truncate
summary: Tiny Datasette plugin to disable text truncation in table displays
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-no-truncate
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-no-truncate/
project_url: https://pypi.org/project/datasette-no-truncate/
release_url: https://pypi.org/project/datasette-no-truncate/0.1/
requires_dist: ["datasette", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'"]
requires_python: >=3.7
version: 0.1
description:
  # datasette-no-truncate

  [![PyPI](https://img.shields.io/pypi/v/datasette-no-truncate.svg)](https://pypi.org/project/datasette-no-truncate/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-no-truncate?include_prereleases&label=changelog)](https://github.com/simonw/datasette-no-truncate/releases)
  [![Tests](https://github.com/simonw/datasette-no-truncate/workflows/Test/badge.svg)](https://github.com/simonw/datasette-no-truncate/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-no-truncate/blob/main/LICENSE)

  Tiny Datasette plugin to disable text truncation in table displays

  ## Installation

  Install this plugin in the same environment as Datasette.

      datasette install datasette-no-truncate

  ## Usage

  Once installed, text values should no longer be truncated when a table is displayed.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:

      cd datasette-no-truncate
      python3 -m venv venv
      source venv/bin/activate

  Now install the dependencies and test dependencies:

      pip install -e '.[test]'

  To run the tests:

      pytest

project_urls:
  {"CI": "https://github.com/simonw/datasette-no-truncate/actions", "Changelog": "https://github.com/simonw/datasette-no-truncate/releases", "Homepage": "https://github.com/simonw/datasette-no-truncate", "Issues": "https://github.com/simonw/datasette-no-truncate/issues"}


name: datasette-notebook
summary: A markdown wiki and dashboarding system for Datasette
classifiers: []
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-notebook
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-notebook/
project_url: https://pypi.org/project/datasette-notebook/
release_url: https://pypi.org/project/datasette-notebook/0.2a0/
requires_python: >=3.6
version: 0.2a0
description:
  # datasette-notebook

  [![PyPI](https://img.shields.io/pypi/v/datasette-notebook.svg)](https://pypi.org/project/datasette-notebook/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-notebook?include_prereleases&label=changelog)](https://github.com/simonw/datasette-notebook/releases)
  [![Tests](https://github.com/simonw/datasette-notebook/workflows/Test/badge.svg)](https://github.com/simonw/datasette-notebook/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-notebook/blob/main/LICENSE)

  A markdown wiki and dashboarding system for Datasette

  This is an **experimental alpha** and everything about it is likely to change.

  ## Installation

  Install this plugin in the same environment as Datasette.

      $ datasette install datasette-notebook

  ## Usage

  Start Datasette with a SQLite file called `notebook.db`:

      datasette notebook.db --create

  Here the `--create` option will create that file if it does not yet exist.

  Visit `/n` to create an index page. Visit `/n/name` to create a page with that name.

  You can link to other pages using `[[WikiLink]]` syntax. This will create a link to `/n/WikiLink` - spaces will be converted to underscores, and you can link to nested pages such as `[[nested/page]]`.

  ## Configuration

  You can use a file other than `notebook.db` by configuring it using `metadata.yml`. To use a database file called `otherfile.db` you would use this:

  ```yaml
  plugins:
    datasette-notebook:
      database: otherfile
  ```
  Then start Datasette like so:

      datasette otherfile.db


  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:

      cd datasette-notebook
      python3 -mvenv venv
      source venv/bin/activate

  Or if you are using `pipenv`:

      pipenv shell

  Now install the dependencies and test dependencies:

      pip install -e '.[test]'

  To run the tests:

      pytest

project_urls:
  {"CI": "https://github.com/simonw/datasette-notebook/actions", "Changelog": "https://github.com/simonw/datasette-notebook/releases", "Homepage": "https://github.com/simonw/datasette-notebook", "Issues": "https://github.com/simonw/datasette-notebook/issues"}

requires_dist:
  ["datasette", "sqlite-utils", "markdown", "bleach", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'"]


name: datasette-nteract-data-explorer
summary: automatic visual data explorer for datasette
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Cameron Yick
description_content_type: text/markdown
home_page: https://github.com/hydrosquall/datasette-nteract-data-explorer
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-nteract-data-explorer/
project_url: https://pypi.org/project/datasette-nteract-data-explorer/
release_url: https://pypi.org/project/datasette-nteract-data-explorer/0.5.1/
requires_dist: ["datasette", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'"]
requires_python: >=3.7
version: 0.5.1
description:
  # datasette-nteract-data-explorer

  [![PyPI](https://img.shields.io/pypi/v/datasette-nteract-data-explorer.svg)](https://pypi.org/project/datasette-nteract-data-explorer/)
  [![Changelog](https://img.shields.io/github/v/release/hydrosquall/datasette-nteract-data-explorer?include_prereleases&label=changelog)](https://github.com/hydrosquall/datasette-nteract-data-explorer/releases)
  [![Tests](https://github.com/hydrosquall/datasette-nteract-data-explorer/workflows/Test/badge.svg)](https://github.com/hydrosquall/datasette-nteract-data-explorer/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/hydrosquall/datasette-nteract-data-explorer/blob/main/LICENSE)

  An automatic data visualization plugin for the [Datasette](https://datasette.io/) ecosystem. See your dataset from multiple views with an easy-to-use, customizable menu-based interface.

  ## Demo

  Try the [live demo](https://datasette-nteract-data-explorer.vercel.app/happy_planet_index/hpi_cleaned?_size=137)

  ![screenshot](https://p-qkfgo2.t2.n0.cdn.getcloudapp.com/items/yAuK9LRE/6802f849-315d-4a21-93b4-61c94d066bdc.jpg?v=f1ceee5ed70832d74e745b6508baeffb)

  _Running Datasette with the Happy Planet Index dataset_

  ## Installation

  Install this plugin in the same Python environment as Datasette.

  ```bash
  datasette install datasette-nteract-data-explorer
  ```

  ## Usage

  - Click "View in Data Explorer" to expand the visualization panel
  - Click the icons on the right side to change the visualization type.
  - Use the menus underneath the graphing area to configure your graph (e.g. change which columns to graph, colors to use, etc)
  - Use "advanced settings" mode to override the inferred column types. For example, you may want to treat a number as a "string" to be able to use it as a category.
  - See a [live demo](https://data-explorer.nteract.io/) of the original Nteract data-explorer component used in isolation.

  You can run a minimal demo after the installation step

  ```bash
  datasette -i demo/happy_planet_index.db
  ```

  If you're interested in improving the demo site, you can run a copy of the site the extra metadata/plugins used in the [published demo](https://datasette-nteract-data-explorer.vercel.app).

  ```bash
  make run-demo
  ```

  Thank you for reading this far! If you use the Data Explorer in your own site and would like others to find it, you can [mention it here](https://github.com/hydrosquall/datasette-nteract-data-explorer/discussions/10).

  ## Development

  See [contributing docs](./docs/CONTRIBUTING.md).

  ## Acknowledgements

  - The [Data Explorer](https://github.com/nteract/data-explorer) was designed by Elijah Meeks. I co-maintain this project as part of the [Nteract](https://nteract.io/) open-source team. You can read about the design behind this tool [here](https://blog.nteract.io/designing-the-nteract-data-explorer-f4476d53f897)
  - The data model is based on the [Frictionless Data Spec](https://specs.frictionlessdata.io/).
  - This plugin was bootstrapped by Simon Willison's [Datasette plugin template](https://simonwillison.net/2020/Jun/20/cookiecutter-plugins/)
  - Demo dataset from the [Happy Planet Index](https://happyplanetindex.org/) was cleaned by Doris Lee. This dataset was chosen because of its global appeal, modest size, and variety in column datatypes (numbers, low cardinality and high cardinality strings, booleans).
  - Hosting for the demo site is provided by Vercel.

  [![site hosted by vercel](https://www.datocms-assets.com/31049/1618983297-powered-by-vercel.svg)](https://vercel.com/?utm_source=datasette-visualization-plugin-demos&utm_campaign=oss)

project_urls:
  {"CI": "https://github.com/hydrosquall/datasette-nteract-data-explorer/actions", "Changelog": "https://github.com/hydrosquall/datasette-nteract-data-explorer/releases", "Homepage": "https://github.com/hydrosquall/datasette-nteract-data-explorer", "Issues": "https://github.com/hydrosquall/datasette-nteract-data-explorer/issues"}


name: datasette-oauth
summary: Datasette as an OAuth provider
classifiers: ["Framework :: Datasette"]
author: Datasette
description_content_type: text/markdown
dynamic: ["License-File"]
license_expression: Apache-2.0
license_files: ["LICENSE"]
package_url: https://pypi.org/project/datasette-oauth/
project_url: https://pypi.org/project/datasette-oauth/
release_url: https://pypi.org/project/datasette-oauth/0.1a0/
requires_dist: ["datasette>=1.0a25"]
requires_python: >=3.10
version: 0.1a0
description:
  # datasette-oauth

  [![PyPI](https://img.shields.io/pypi/v/datasette-oauth.svg)](https://pypi.org/project/datasette-oauth/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-oauth?include_prereleases&label=changelog)](https://github.com/datasette/datasette-oauth/releases)
  [![Tests](https://github.com/datasette/datasette-oauth/actions/workflows/test.yml/badge.svg)](https://github.com/datasette/datasette-oauth/actions/workflows/test.yml)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-oauth/blob/main/LICENSE)

  Datasette as an OAuth provider. Allows third-party applications to request access to a Datasette instance on behalf of signed-in users, using the OAuth 2.0 Authorization Code flow.

  Access tokens are standard Datasette restricted API tokens (`dstok_...`), so all existing permission checks work automatically.

  ## Installation

  Install this plugin in the same environment as Datasette.

  ```bash
  datasette install datasette-oauth
  ```

  ## Permissions

  This plugin registers two permissions that must be granted before users can access the corresponding features. Both default to deny, so installing the plugin does not change any behavior until permissions are explicitly granted.

  ### `oauth-manage-clients`

  Controls access to the client management UI and API — registering, listing, editing, and deleting OAuth clients. Grant it in `datasette.yaml`:

  ```yaml
  permissions:
    oauth-manage-clients:
      id: "*"
  ```

  ### `oauth-device-tokens`

  Controls whether a user can authorize device token requests at `/-/oauth/device/verify`. Grant it in `datasette.yaml`:

  ```yaml
  permissions:
    oauth-device-tokens:
      id: "*"
  ```

  The root user is **denied** this permission by default, even when `--root` is enabled. To allow root to authorize device tokens, set `allow_root_device_tokens` in the plugin configuration:

  ```yaml
  plugins:
    datasette-oauth:
      allow_root_device_tokens: true
  ```

  ## Plugin configuration

  The device authorization flow is disabled by default. To enable it, set `enable_device_flow` in your `datasette.yaml`:

  ```yaml
  plugins:
    datasette-oauth:
      enable_device_flow: true
  ```

  When disabled (the default), all device flow endpoints return a 403 error. This prevents unauthenticated writes to the internal database.

  ## How it works (authorization code flow)

  Before the OAuth flow can begin, a user with the `oauth-manage-clients` permission must register a client application via the `/-/oauth/clients` management UI or the `POST /-/oauth/clients.json` API. This produces a `client_id` and `client_secret` that the third-party app will use.

  Once the client is registered:

  1. The third-party app redirects the user to `GET /-/oauth/authorize` with `client_id`, `redirect_uri`, `scope`, `state`, and `response_type=code`
  2. Datasette shows the user a consent screen with the app name and requested permissions
  3. The user approves (or denies) the request
  4. Datasette redirects back to the app's `redirect_uri` with an authorization code
  5. The app exchanges the code for an access token via `POST /-/oauth/token`
  6. Datasette returns a `dstok_...` API token restricted to the approved permissions

  ## Authorization code endpoints

  ### Authorization: `GET /-/oauth/authorize`

  Redirect the user here to request authorization. Parameters:

  | Parameter | Required | Description |
  |---|---|---|
  | `client_id` | Yes | The registered client ID |
  | `redirect_uri` | Yes | Must exactly match the registered redirect URI |
  | `scope` | Yes | JSON array of scope arrays (see below) |
  | `state` | Yes | Opaque value passed back to prevent CSRF |
  | `response_type` | Yes | Must be `code` |

  The user sees a consent screen showing the app name and requested permissions, each with a checkbox. They can uncheck permissions they don't want to grant.

  ### Process consent: `POST /-/oauth/authorize`

  When the user clicks "Authorize", they are redirected back to the `redirect_uri` with:

  ```
  https://myapp.example.com/callback?code=abc123...&state=your-state
  ```

  If the user clicks "Deny":

  ```
  https://myapp.example.com/callback?error=access_denied&state=your-state
  ```

  ### Exchange code for token: `POST /-/oauth/token`

  ```bash
  curl -X POST 'https://datasette.example.com/-/oauth/token' \
    -d 'grant_type=authorization_code' \
    -d 'code=abc123...' \
    -d 'client_id=a1b2c3...' \
    -d 'client_secret=d4e5f6...' \
    -d 'redirect_uri=https://myapp.example.com/callback'
  ```

  Response:

  ```json
  {
    "access_token": "dstok_...",
    "token_type": "bearer"
  }
  ```

  Authorization codes expire after 10 minutes and are single-use.

  ## Scope format

  Scopes are JSON arrays describing permissions at different levels:

  | Scope | Meaning |
  |---|---|
  | `["view-instance"]` | Global permission |
  | `["view-database", "mydb"]` | Permission on a specific database |
  | `["view-table", "mydb", "users"]` | Permission on a specific table |

  Multiple scopes are passed as a JSON array of arrays:

  ```json
  [
    ["view-instance"],
    ["view-database", "mydb"],
    ["view-table", "mydb", "users"],
    ["insert-row", "mydb", "logs"]
  ]
  ```

  This maps directly to Datasette's existing token restriction system (`restrict_all`, `restrict_database`, `restrict_resource`).

  ## Using the access token

  The access token is a standard Datasette API token. Use it with the `Authorization` header:

  ```bash
  curl -H 'Authorization: Bearer dstok_...' \
    'https://datasette.example.com/mydb/users.json'
  ```

  The token is restricted to only the permissions the user approved on the consent screen.

  ## Client management

  ### Client management UI: `/-/oauth/clients`

  Users with the `oauth-manage-clients` permission can visit `/-/oauth/clients` in their browser to register, edit, and delete OAuth client applications. The client secret is displayed once at registration time.

  The same operations are available via the JSON API below.

  ### Register a client: `POST /-/oauth/clients.json`

  Requires authentication and the `oauth-manage-clients` permission. Creates a new OAuth client application.

  ```bash
  curl -X POST 'https://datasette.example.com/-/oauth/clients.json' \
    -H 'Cookie: ds_actor=...' \
    -d 'client_name=My App&redirect_uri=https://myapp.example.com/callback'
  ```

  Response:

  ```json
  {
    "client_id": "a1b2c3...",
    "client_secret": "d4e5f6...",
    "client_name": "My App",
    "redirect_uri": "https://myapp.example.com/callback"
  }
  ```

  The `client_secret` is shown **once** at registration time. It is stored as a SHA-256 hash.

  ### List your clients: `GET /-/oauth/clients.json`

  Requires authentication and the `oauth-manage-clients` permission. Returns clients registered by the current user.

  ```json
  [
    {
      "client_id": "a1b2c3...",
      "client_name": "My App",
      "redirect_uri": "https://myapp.example.com/callback",
      "created_by": "user-id",
      "created_at": "2025-01-15T10:30:00Z"
    }
  ]
  ```

  ## Device authorization flow

  The device authorization flow allows CLI tools and headless applications to obtain access tokens without a browser redirect. This implements [RFC 8628](https://datatracker.ietf.org/doc/html/rfc8628).

  > [!CAUTION]
  > **Enable with caution.** The device flow relies on a user correctly verifying that they initiated the request. An attacker could generate a device code and trick a user into approving it — for example by sending them a link with the code pre-filled, or by social-engineering them into entering the code. If your Datasette instance has users who may not understand the implications of approving a device authorization request, consider warning them or restricting the `oauth-device-tokens` permission to trusted users only.

  This flow must be explicitly enabled with the `enable_device_flow` plugin setting.

  1. The CLI app requests a device code via `POST /-/oauth/device`
  2. Datasette returns a `device_code`, a short `user_code` (e.g. `ABCD-EFGH`), and a `verification_uri`
  3. The CLI app displays the user code and verification URL to the user
  4. The user visits the URL in a browser, enters the code, and approves the request
  5. Meanwhile, the CLI app polls `POST /-/oauth/token` with the device code
  6. Once approved, the token endpoint returns an access token

  ### Step 1: Request a device code

  ```bash
  curl -X POST 'https://datasette.example.com/-/oauth/device' \
    -d 'scope=[["view-instance"],["view-database","mydb"]]'
  ```

  Response:

  ```json
  {
    "device_code": "a1b2c3d4...",
    "user_code": "ABCD-EFGH",
    "verification_uri": "https://datasette.example.com/-/oauth/device/verify",
    "expires_in": 900,
    "interval": 5
  }
  ```

  The `scope` parameter is optional. If omitted, the token will be unrestricted.

  ### Step 2: Direct the user to verify

  Display the `user_code` and `verification_uri` to the user. They visit the URL in a browser, enter the code, review the requested permissions, choose a token time limit, and click "Authorize device" or "Deny".

  The user must be signed in and have the `oauth-device-tokens` permission.

  ### Step 3: Poll for the token

  While the user verifies, poll the token endpoint:

  ```bash
  curl -X POST 'https://datasette.example.com/-/oauth/token' \
    -d 'grant_type=urn:ietf:params:oauth:grant-type:device_code' \
    -d 'device_code=a1b2c3d4...'
  ```

  Poll every `interval` seconds (5 by default). Possible responses:

  | Response | Meaning |
  |---|---|
  | `{"error": "authorization_pending"}` | User hasn't completed verification yet — keep polling |
  | `{"error": "access_denied"}` | User denied the request |
  | `{"error": "expired_token"}` | Device code expired (15 minutes) |

  On success:

  ```json
  {
    "access_token": "dstok_...",
    "token_type": "bearer",
    "expires_in": 3600
  }
  ```

  Device flow tokens have an expiry time chosen by the user during verification. Options range from 15 minutes to 30 days, with a default of 1 hour. The `expires_in` field indicates the token lifetime in seconds.

  Tokens issued through the standard authorization code flow do not expire.

  ## Security

  - **Client secrets** are 64 random hex characters, shown once at registration and stored as SHA-256 hashes
  - **Authorization codes** expire after 10 minutes and are single-use
  - **Redirect URIs** must exactly match the registered URI
  - Only actors with an `id` can authorize (same check as `/-/create-token`)
  - Token-authenticated requests cannot be used to authorize new clients

  ## Development

  To set up this plugin locally, first checkout the code. You can confirm it is available like this:

  ```bash
  cd datasette-oauth
  # Confirm the plugin is visible
  uv run datasette plugins
  ```

  To run the tests:

  ```bash
  uv run pytest
  ```

project_urls:
  {"CI": "https://github.com/datasette/datasette-oauth/actions", "Changelog": "https://github.com/datasette/datasette-oauth/releases", "Homepage": "https://github.com/datasette/datasette-oauth", "Issues": "https://github.com/datasette/datasette-oauth/issues"}


name: datasette-openai
summary: SQL functions for calling OpenAI APIs
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-openai
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-openai/
project_url: https://pypi.org/project/datasette-openai/
release_url: https://pypi.org/project/datasette-openai/0.2/
requires_dist: ["datasette", "regex", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'"]
requires_python: >=3.7
version: 0.2
description:
  # datasette-openai

  [![PyPI](https://img.shields.io/pypi/v/datasette-openai.svg)](https://pypi.org/project/datasette-openai/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-openai?include_prereleases&label=changelog)](https://github.com/simonw/datasette-openai/releases)
  [![Tests](https://github.com/simonw/datasette-openai/workflows/Test/badge.svg)](https://github.com/simonw/datasette-openai/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-openai/blob/main/LICENSE)

  SQL functions for calling OpenAI APIs

  See [Semantic search answers: Q&A against documentation with GPT3 + OpenAI embeddings](https://simonwillison.net/2023/Jan/13/semantic-search-answers/) for background on this project.

  ## Installation

  Install this plugin in the same environment as Datasette.

      datasette install datasette-openai

  ## ⚠️ Warning ⚠️

  This plugin allows you to call a commercial, priced API using SQL queries.

  Use this with care! You could accidentally spend a lot of money.

  For example, the following query:

  ```sql
  select
    openai_davinci(
      'Summarize this text: ' || content, 200, 1, :api_key
  ) as summary
  from documents
  ```
  Would execute one paid API call for every item in the `documents` database. This could become very expensive.

  Be sure to familiarize yourself with [OpenAI pricing](https://openai.com/api/pricing/). You will need to obtain an [API key](https://beta.openai.com/account/api-keys).

  ## Usage

  This extension provides three new SQL functions:

  ### openai_davinci(prompt, max_tokens, temperature, api_key)

  This function runs a `text-davinci-003` completion against the provided prompt, with the specified values for max tokens and temperature.

  Da Vinci is currently 2 cents per thousand tokens.

  ### openai_embedding(text, api_key)

  This calls the OpenAI embedding endpoint and returns a binary object representing the floating point embedding for the provided text.

  ```sql
  select openai_embedding(:query, :api_key)
  ```
  An embedding is an array of 1536 floating point values. The returned value from this is a `blob` encoding of those values.

  It's mainly useful for using with the `openai_embedding_similarity()` function.

  The embedding API is very inexpensive: at time of writing, $0.0004 cents per thousand tokens, where a token is more-or-less a single word.

  ### openai_embedding_similarity(a, b)

  This function does not make any API calls. It takes two embedding blobs and returns the cosine similarity between the two.

  This function is particularly useful if you have stored embeddings of documents in a database table, and you want to find the most similar documents to a query or to another document.

  A simple search query could look like this:
  ```sql
  with query as (
    select
      openai_embedding(:query, :token) as q
  )
  select
    id,
    title,
    openai_embedding_similarity(query.q, embedding) as score
  from
    content, query
  order by
    score desc
  limit 10
  ```

  ## openai_build_prompt(text, prefix, suffix, completion_tokens, token_limit=4000)

  This aggregate function helps build a prompt from a number of inputs in a way that fits the GPT-3 prompt size limit.

  It takes the following argument:

  - `text` - this is the column that is being aggregated, so the function expects to have multiple values for this. All other arguments will only be read the first time they are passed, so should be consistent across all calls to the function.
  - `prefix` - text to use for the prefix of the prompt
  - `suffix` - text to use for the suffix of the prompt
  - `completion_tokens` - the number of tokens to reserve for the prompt response - this will be subtracted from the token limit
  - `token_limit` - this value is optional (there are 4-argument and 5-argument versions of the function registered). It defaults to the GPT-3 Da Vinci size limit of 4,000 tokens but can be changed if the model the prompt is being used with has a different size limit.

  Here's an example usage of this function, adapted from [this article](https://simonwillison.net/2023/Jan/13/semantic-search-answers/):

  ```sql
  with top_n as (
    select body from blog_entry order by id desc limit 3
  )
  select openai_build_prompt(body, 'Context:
  ------------
  ', '
  ------------
  Given the above context, answer the following question: ' || :question,
    500,
    2000
    ) from top_n
  ```
  [Try that here](https://datasette.simonwillison.net/simonwillisonblog?sql=with+top_n+as+%28%0D%0A++select+body+from+blog_entry+order+by+id+desc+limit+5%0D%0A%29%0D%0Aselect+openai_build_prompt%28body%2C+%27Context%3A%0D%0A------------%0D%0A%27%2C+%27%0D%0A------------%0D%0AGiven+the+above+context%2C+answer+the+following+question%3A+%27+%7C%7C+%3Aquestion%2C%0D%0A++500%2C%0D%0A++2000%0D%0A++%29+from+top_n&question=Examples+of+a+language+model%3F).

  This query first retrieves the three most recent blog entries, then constructs a prompt that with the provided prefix and suffix designed to fit 1500 tokens (2000 total, minus 500 reserved for the response).

  The output looks something like this (truncated for space):

  ```
  Context:
  ------------
  < p > If you 've spent any time with GPT - 3 or ChatGPT , you 've likely thought about how ...
  I release Datasette 0 . 64 this morning . This release is mainly a response to the realization that it 's not safe to run Datasette with the SpatiaLite extension loaded if that Datasette instance is configured to enable arbitrary SQL queries from untrusted users ...
  In lieu of my regular weeknotes ( I took two weeks off for the holidays ) here 's a look back at 2022 , mainly in terms of projects and things I 've written about ...
  ------------
  Given the above context, answer the following question: Examples of a language model?
  ```
  The body of each entry has been truncated to the number of tokens that will allow examples from all three entries to be included in the generated prompt.

  ## openai_strip_tags(text)

  Sometimes it can be useful to strip HTML tags from text in order to reduce the number of tokens used. This function does a very simple version of tag stripping - just removing anything that matches `<...>`.

  ## openai_tokenize(text)

  Returns a JSON array of tokens for the provided text.

  This uses a regular expression [extracted from OpenAI's GPT-2](https://github.com/openai/gpt-2/blob/a74da5d99abaaba920de8131d64da2862a8f213b/src/encoder.py#L53).

  ## openai_count_tokens(text)

  Returns a count of the number of tokens in the provided text.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:

      cd datasette-openai
      python3 -m venv venv
      source venv/bin/activate

  Now install the dependencies and test dependencies:

      pip install -e '.[test]'

  To run the tests:

      pytest

project_urls:
  {"CI": "https://github.com/simonw/datasette-openai/actions", "Changelog": "https://github.com/simonw/datasette-openai/releases", "Homepage": "https://github.com/simonw/datasette-openai", "Issues": "https://github.com/simonw/datasette-openai/issues"}


name: datasette-os-info
summary: Provide information about the current OS at /-/os
classifiers: ["Framework :: Datasette"]
author: Simon Willison
description_content_type: text/markdown
dynamic: ["License-File"]
license_expression: Apache-2.0
license_files: ["LICENSE"]
package_url: https://pypi.org/project/datasette-os-info/
project_url: https://pypi.org/project/datasette-os-info/
provides_extra: ["test"]
release_url: https://pypi.org/project/datasette-os-info/0.1/
requires_dist: ["datasette", "pytest; extra == \"test\"", "pytest-asyncio; extra == \"test\""]
requires_python: >=3.10
version: 0.1
description:
  # datasette-os-info

  [![PyPI](https://img.shields.io/pypi/v/datasette-os-info.svg)](https://pypi.org/project/datasette-os-info/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-os-info?include_prereleases&label=changelog)](https://github.com/datasette/datasette-os-info/releases)
  [![Tests](https://github.com/datasette/datasette-os-info/actions/workflows/test.yml/badge.svg)](https://github.com/datasette/datasette-os-info/actions/workflows/test.yml)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-os-info/blob/main/LICENSE)

  Provide information about the current OS at `/-/os`

  ## Installation

  Install this plugin in the same environment as Datasette.
  ```bash
  datasette install datasette-os-info
  ```
  ## Usage

  This plugin adds a `/-/os` page to your Datasette instance that returns detailed information about the operating system, formatted as pretty-printed JSON.

  **⚠️ Security Warning**: This endpoint exposes system information including hostnames, usernames, filesystem paths, and environment variables. Review the output before deploying on a public instance.

  Visit `/-/os` on your Datasette instance to see comprehensive OS information including:

  - **Platform details**: System type, release, version, machine architecture, processor
  - **Python information**: Version, implementation, executable path
  - **Container detection**: Docker, Kubernetes, LXC detection
  - **Linux-specific information** (when running on Linux):
    - Distribution details from `/etc/os-release` (name, version, codename)
    - Debian/Alpine/RedHat version information
    - Kernel information
    - CPU model and count
    - Memory statistics
    - Docker base image detection
  - **macOS-specific information** (when running on macOS):
    - macOS version details
  - **Windows-specific information** (when running on Windows):
    - Windows version and edition
  - **Environment variables**: Useful variables like SHELL, TERM, USER, HOME, PATH, LANG, VIRTUAL_ENV

  Example:
  ```bash
  datasette install datasette-os-info
  datasette
  # Then visit http://localhost:8001/-/os
  ```

  You can also view the output from the command line:
  ```bash
  datasette --get /-/os
  ```

  The JSON output is automatically pretty-printed for easy reading.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:
  ```bash
  cd datasette-os-info
  python -m venv venv
  source venv/bin/activate
  ```
  Now install the dependencies and test dependencies:
  ```bash
  pip install -e '.[test]'
  ```
  To run the tests:
  ```bash
  python -m pytest
  ```

project_urls:
  {"CI": "https://github.com/datasette/datasette-os-info/actions", "Changelog": "https://github.com/datasette/datasette-os-info/releases", "Homepage": "https://github.com/datasette/datasette-os-info", "Issues": "https://github.com/datasette/datasette-os-info/issues"}


name: datasette-packages
summary: Show a list of currently installed Python packages
classifiers: ["Framework :: Datasette"]
author: Simon Willison
description_content_type: text/markdown
license_expression: Apache-2.0
package_url: https://pypi.org/project/datasette-packages/
project_url: https://pypi.org/project/datasette-packages/
release_url: https://pypi.org/project/datasette-packages/0.3/
requires_dist: ["datasette"]
requires_python: >=3.10
version: 0.3
description:
  # datasette-packages

  [![PyPI](https://img.shields.io/pypi/v/datasette-packages.svg)](https://pypi.org/project/datasette-packages/)
  [![Changelog](https://img.shields.io/github/v/release/simonw/datasette-packages?include_prereleases&label=changelog)](https://github.com/simonw/datasette-packages/releases)
  [![Tests](https://github.com/simonw/datasette-packages/workflows/Test/badge.svg)](https://github.com/simonw/datasette-packages/actions?query=workflow%3ATest)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-packages/blob/main/LICENSE)

  Show a list of currently installed Python packages

  ## Installation

  Install this plugin in the same environment as Datasette.
  ```bash
  datasette install datasette-packages
  ```
  ## Usage

  Visit `/-/packages` to see a list of installed Python packages. Each package links to a detail page at `/-/packages/package-name` showing the full metadata and README for that package.

  Visit `/-/packages.json` to get the list back as JSON.

  ## Demo

  The output of this plugin can be seen here:

  - https://latest-with-plugins.datasette.io/-/packages
  - https://latest-with-plugins.datasette.io/-/packages.json

  ## With datasette-graphql

  if you have version 2.1 or higher of the [datasette-graphql](https://datasette.io/plugins/datasette-graphql) plugin installed you can also query the list of packages using this GraphQL query:

  ```graphql
  {
    packages {
      name
      version
    }
  }
  ```
  [Demo of this query](https://latest-with-plugins.datasette.io/graphql?query=%7B%0A%20%20packages%20%7B%0A%20%20%20%20name%0A%20%20%20%20version%0A%20%20%7D%0A%7D).

  ## Development

  Run the tests using `uv run pytest`:
  ```bash
  cd datasette-packages
  uv run pytest
  ```
  To try the plugin locally, use:
  ```bash
  uv run datasette
  ```
  Then visit `http://localhost:8001/-/packages`

project_urls:
  {"CI": "https://github.com/simonw/datasette-packages/actions", "Changelog": "https://github.com/simonw/datasette-packages/releases", "Homepage": "https://github.com/simonw/datasette-packages", "Issues": "https://github.com/simonw/datasette-packages/issues"}


name: datasette-paste
summary: Paste data to create tables in Datasette
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Simon Willison
description_content_type: text/markdown
license: Apache-2.0
package_url: https://pypi.org/project/datasette-paste/
project_url: https://pypi.org/project/datasette-paste/
release_url: https://pypi.org/project/datasette-paste/0.1a5/
requires_dist: ["datasette>=1.0a13", "pytest; extra == \"test\"", "pytest-asyncio; extra == \"test\""]
requires_python: >=3.8
version: 0.1a5
description:
  # datasette-paste

  [![PyPI](https://img.shields.io/pypi/v/datasette-paste.svg)](https://pypi.org/project/datasette-paste/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-paste?include_prereleases&label=changelog)](https://github.com/datasette/datasette-paste/releases)
  [![Tests](https://github.com/datasette/datasette-paste/actions/workflows/test.yml/badge.svg)](https://github.com/datasette/datasette-paste/actions/workflows/test.yml)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-paste/blob/main/LICENSE)

  Paste data to create tables in Datasette

  **This plugin is no longer being developed.** It has been replaced by [datasette-import](https://github.com/datasette/datasette-import).

  ## Installation

  Install this plugin in the same environment as Datasette.
  ```bash
  datasette install datasette-paste
  ```
  ## Usage

  This plugin adds a database action item called "Create table with pasted data".

  This action is available to users with the `create-table` permission.

  It links to a page that allows users to paste in CSV, TSV or JSON data, and then use that to create and populate a new table in Datasette.

  CSV and TSV data must have headers on the first row.

  JSON data must be an array of objects with the same keys, or a container object object where one of the keys is an array of objects.

  ## Credits

  The CSV and TSV parsing is performed using [Papa Parse](https://www.papaparse.com/), an MIT licensed JavaScript library that is bundled with this plugin.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:
  ```bash
  cd datasette-paste
  python3 -m venv venv
  source venv/bin/activate
  ```
  Now install the dependencies and test dependencies:
  ```bash
  pip install -e '.[test]'
  ```
  To run the tests:
  ```bash
  pytest
  ```

project_urls:
  {"CI": "https://github.com/datasette/datasette-paste/actions", "Changelog": "https://github.com/datasette/datasette-paste/releases", "Homepage": "https://github.com/datasette/datasette-paste", "Issues": "https://github.com/datasette/datasette-paste/issues"}


name: datasette-permissions-metadata
summary: Configure permissions for Datasette 0.x in metadata.json
classifiers: ["Framework :: Datasette", "License :: OSI Approved :: Apache Software License"]
author: Simon Willison
description_content_type: text/markdown
license: Apache-2.0
package_url: https://pypi.org/project/datasette-permissions-metadata/
project_url: https://pypi.org/project/datasette-permissions-metadata/
release_url: https://pypi.org/project/datasette-permissions-metadata/0.1/
requires_dist: ["datasette", "pytest; extra == \"test\"", "pytest-asyncio; extra == \"test\""]
requires_python: >=3.8
version: 0.1
description:
  # datasette-permissions-metadata

  [![PyPI](https://img.shields.io/pypi/v/datasette-permissions-metadata.svg)](https://pypi.org/project/datasette-permissions-metadata/)
  [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-permissions-metadata?include_prereleases&label=changelog)](https://github.com/datasette/datasette-permissions-metadata/releases)
  [![Tests](https://github.com/datasette/datasette-permissions-metadata/actions/workflows/test.yml/badge.svg)](https://github.com/datasette/datasette-permissions-metadata/actions/workflows/test.yml)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-permissions-metadata/blob/main/LICENSE)

  Configure permissions for Datasette 0.x in `metadata.json`

  ## Background

  Datasette 1.0 will introduce [a new way of configuring permissions](https://docs.datasette.io/en/latest/authentication.html#other-permissions-in-datasette-yaml) using YAML or JSON directly in the Datasette `datasette.yml` configuration file.

  This plugin makes a similar ability available to the Datasette 0.64+ series of releases, by allowing a `permissions` key to be added to the `metadata.json` or `metadata.yml` file used with that version of Datasette.

  ## Installation

  Install this plugin in the same environment as Datasette.
  ```bash
  datasette install datasette-permissions-metadata
  ```
  ## Configuration

  To grant the `upload-csvs` permission (used by the [datasette-upload-csvs](https://datasette.io/plugins/datasette-upload-csvs) plugin) to an user with an `id` of `simon`, add the following to `metadata.json`:

  ```json
  {
    "permissions": {
      "upload-csvs": {
        "id": "simon"
      }
    }
  }
  ```
  See [the documentation on allow blocks](https://docs.datasette.io/en/stable/authentication.html#defining-permissions-with-allow-blocks) for more details on this configuration format.

  ## Development

  To set up this plugin locally, first checkout the code. Then create a new virtual environment:
  ```bash
  cd datasette-permissions-metadata
  python3 -m venv venv
  source venv/bin/activate
  ```
  Now install the dependencies and test dependencies:
  ```bash
  pip install -e '.[test]'
  ```
  To run the tests:
  ```bash
  pytest
  ```

project_urls:
  {"CI": "https://github.com/datasette/datasette-permissions-metadata/actions", "Changelog": "https://github.com/datasette/datasette-permissions-metadata/releases", "Homepage": "https://github.com/datasette/datasette-permissions-metadata", "Issues": "https://github.com/datasette/datasette-permissions-metadata/issues"}


name: datasette-permissions-sql
summary: Datasette plugin for configuring permission checks using SQL queries
classifiers: []
author: Simon Willison
description_content_type: text/markdown
home_page: https://github.com/simonw/datasette-permissions-sql
license: Apache License, Version 2.0
package_url: https://pypi.org/project/datasette-permissions-sql/
project_url: https://pypi.org/project/datasette-permissions-sql/
project_urls: {"Homepage": "https://github.com/simonw/datasette-permissions-sql"}
release_url: https://pypi.org/project/datasette-permissions-sql/0.3a0/
version: 0.3a0
description:
  # datasette-permissions-sql

  [![PyPI](https://img.shields.io/pypi/v/datasette-permissions-sql.svg)](https://pypi.org/project/datasette-permissions-sql/)
  [![CircleCI](https://circleci.com/gh/simonw/datasette-permissions-sql.svg?style=svg)](https://circleci.com/gh/simonw/datasette-permissions-sql)
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-permissions-sql/blob/master/LICENSE)

  Datasette plugin for configuring permission checks using SQL queries

  ## Installation

  Install this plugin in the same environment as Datasette.

      $ pip install datasette-permissions-sql

  ## Usage

  First, read up on how Datasette's [authentication and permissions system](https://datasette.readthedocs.io/en/latest/authentication.html) works.

  This plugin lets you define rules containing SQL queries that are executed to see if the currently authenticated actor has permission to perform certain actions.

  Consider a canned query which authenticated users should only be able to execute if a row in the `users` table says that they are a member of staff.

  That `users` table in the `mydatabase.db` database could look like this:

  | id | username | is_staff |
  |--|--------|--------|
  | 1 | cleopaws | 0 |
  | 2 | simon | 1 |

  Authenticated users have an `actor` that looks like this:

  ```json
  {
      "id": 2,
      "username": "simon"
  }
  ```

  To configure the canned query to only be executable by staff users, add the following to your `metadata.json`:

  ```json
  {
      "plugins": {
          "datasette-permissions-sql": [
              {
                  "action": "view-query",
                  "resource": ["mydatabase", "promote_to_staff"],
                  "sql": "SELECT * FROM users WHERE is_staff = 1 AND id = :actor_id"
              }
          ]
      },
      "databases": {
          "mydatabase": {
              "queries": {
                  "promote_to_staff": {
                      "sql": "UPDATE users SET is is_staff=1 WHERE id=:id",
                      "write": true
                  }
              }
          }
      }
  }
  ```

  The `"datasette-permissions-sql"` key is a list of rules. Each of those rules has the following shape:

  ```json
  {
      "action": "name-of-action",
      "resource": ["resource identifier to run this on"],
      "sql": "SQL query to execute",
      "database": "mydatabase"
  }
  ```

  Both `"action"` and `"resource"` are optional. If present, the SQL query will only be executed on permission checks that match the action and, if present, the resource indicators.

  `"database"` is also optional: it specifies the named database that the query should be executed against. If it is not present the first connected database will be used.

  The Datasette documentation includes a [list of built-in permissions](https://datasette.readthedocs.io/en/stable/authentication.html#built-in-permissions) that you might want to use here.

  ### The SQL query

  If the SQL query returns any rows the action will be allowed. If it returns no rows, the plugin hook will return `False` and deny access to that action.

  The SQL query is called with a number of named parameters. You can use any of these as part of the query.

  The list of parameters is as follows:

  * `action` - the action, e.g. `"view-database"`
  * `resource_1` - the first component of the resource, if one was passed
  * `resource_2` - the second component of the resource, if available
  * `actor_*` - a parameter for every key on the actor. Usually `actor_id` is present.

  If any rows are returned, the permission check passes. If no rows are returned the check fails.

  Another example table, this time granting explicit access to individual tables. Consider a table called `table_access` that looks like this:

  | user_id | database | table |
  | - | - | - |
  | 1 | mydb | dogs |
  | 2 | mydb | dogs |
  | 1 | mydb | cats |

  The following SQL query would grant access to the `dogs` ttable in the `mydb.db` database to users 1 and 2 - but would forbid access for user 2 to the `cats` table:

  ```sql
  SELECT
      *
  FROM
      table_access
  WHERE
      user_id = :actor_id
      AND "database" = :resource_1
      AND "table" = :resource_2
  ```
  In a `metadata.yaml` configuration file that would look like this:

  ```yaml
  databases:
    mydb:
      allow_sql: {}
  plugins:
    datasette-permissions-sql:
    - action: view-table
      sql: |-
        SELECT
          *
        FROM
          table_access
        WHERE
          user_id = :actor_id
          AND "database" = :resource_1
          AND "table" = :resource_2
  ```
  We're using `allow_sql: {}` here to disable arbitrary SQL queries. This prevents users from running `select * from cats` directly to work around the permissions limits.

  ### Fallback mode

  The default behaviour of this plugin is to take full control of specified permissions. The SQL query will directly control if the user is allowed or denied access to the permission.

  This means that the default policy for each permission (which in Datasette core is "allow" for `view-database` and friends) will be ignored. It also means that any other `permission_allowed` plugins will not get their turn once this plugin has executed.

  You can change this on a per-rule basis using ``"fallback": true``:

  ```json
  {
      "action": "view-table",
      "resource": ["mydatabase", "mytable"],
      "sql": "select * from admins where user_id = :actor_id",
      "fallback": true
  }
  ```

  When running in fallback mode, a query result returning no rows will cause the plugin hook to return ``None`` - which means "I have no opinion on this permission, fall back to other plugins or the default".

  In this mode you can still return `False` (for "deny access") by returning a single row with a single value of `-1`. For example:

  ```json
  {
      "action": "view-table",
      "resource": ["mydatabase", "mytable"],
      "sql": "select -1 from banned where user_id = :actor_id",
      "fallback": true
  }
  ```

requires_dist:
  ["datasette (>=0.44)", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'", "httpx ; extra == 'test'", "sqlite-utils (~=2.0) ; extra == 'test'"]


