Skip to content

Commit

Permalink
datasette-secrets integration
Browse files Browse the repository at this point in the history
* datasette-secrets integration, closes #9

* Upgrade to datasette-enrichments>=0.4.1

Refs datasette/datasette-enrichments#49

* Test against Python 3.12 and both Datasette major versions

* Documentation for datasette-secrets configuration, refs #9
  • Loading branch information
simonw authored Apr 27, 2024
1 parent 1353a46 commit c20758d
Show file tree
Hide file tree
Showing 5 changed files with 28 additions and 72 deletions.
12 changes: 6 additions & 6 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
Expand All @@ -31,11 +31,11 @@ jobs:
runs-on: ubuntu-latest
needs: [test]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: "3.11"
python-version: "3.12"
cache: pip
cache-dependency-path: pyproject.toml
- name: Install dependencies
Expand Down
10 changes: 6 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,21 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
datasette-version: ["<1.0", ">=1.0a7"]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
cache-dependency-path: pyproject.toml
- name: Install dependencies
run: |
pip install '.[test]'
- name: Install specific version of Datasette
run: pip install "datasette${{ matrix.datasette-version }}"
- name: Run tests
run: |
pytest
18 changes: 6 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,13 @@ datasette install datasette-enrichments-gpt
```
## Configuration

This plugin needs an OpenAI API key. Configure that in `metadata.yml` like so
```yaml
plugins:
datasette-enrichments-gpt:
api_key: sk-..
```
Or to avoid that key being visible on `/-/metadata` set it as an environment variable and use this:
```yaml
plugins:
datasette-enrichments-gpt:
api_key:
$env: OPENAI_API_KEY
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

Expand Down
58 changes: 9 additions & 49 deletions datasette_enrichments_gpt/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations
from datasette_enrichments import Enrichment
from datasette_secrets import Secret
from datasette import hookimpl
from datasette.database import Database
import httpx
Expand All @@ -9,11 +10,9 @@
StringField,
TextAreaField,
BooleanField,
PasswordField,
SelectField,
)
from wtforms.validators import ValidationError, DataRequired
import secrets
import sqlite_utils


Expand All @@ -28,6 +27,12 @@ class GptEnrichment(Enrichment):
description = "Analyze data using OpenAI's GPT models"
runs_in_process = True
batch_size = 1
secret = Secret(
name="OPENAI_API_KEY",
description="OpenAI API key",
obtain_label="Get an OpenAI API key",
obtain_url="https://platform.openai.com/api-keys",
)

async def get_config_form(self, datasette, db, table):
columns = await db.table_columns(table)
Expand Down Expand Up @@ -90,30 +95,7 @@ def validate_prompt(self, field):
'The prompt or system prompt must contain the word "JSON" when JSON format is selected.'
)

def stash_api_key(form, field):
if not (field.data or "").startswith("sk-"):
raise ValidationError("API key must start with sk-")
if not hasattr(datasette, "_enrichments_gpt_stashed_keys"):
datasette._enrichments_gpt_stashed_keys = {}
key = secrets.token_urlsafe(16)
datasette._enrichments_gpt_stashed_keys[key] = field.data
field.data = key

class ConfigFormWithKey(ConfigForm):
api_key = PasswordField(
"API key",
description="Your OpenAI API key",
validators=[
DataRequired(message="API key is required."),
stash_api_key,
],
render_kw={"autocomplete": "off"},
)

plugin_config = datasette.plugin_config("datasette-enrichments-gpt") or {}
api_key = plugin_config.get("api_key")

return ConfigForm if api_key else ConfigFormWithKey
return ConfigForm

async def initialize(self, datasette, db, table, config):
# Ensure column exists
Expand Down Expand Up @@ -187,7 +169,7 @@ async def enrich_batch(
job_id: int,
) -> List[Optional[str]]:
# API key should be in plugin settings OR pointed to by config
api_key = resolve_api_key(datasette, config)
api_key = await self.get_secret(datasette, config)
if rows:
row = rows[0]
else:
Expand Down Expand Up @@ -220,25 +202,3 @@ async def enrich_batch(
),
[output] + list(row[pk] for pk in pks),
)


class ApiKeyError(Exception):
pass


def resolve_api_key(datasette, config):
plugin_config = datasette.plugin_config("datasette-enrichments-gpt") or {}
api_key = plugin_config.get("api_key")
if api_key:
return api_key
# Look for it in config
api_key_name = config.get("api_key")
if not api_key_name:
raise ApiKeyError("No API key reference found in config")
# Look it up in the stash
if not hasattr(datasette, "_enrichments_gpt_stashed_keys"):
raise ApiKeyError("No API key stash found")
stashed_keys = datasette._enrichments_gpt_stashed_keys
if api_key_name not in stashed_keys:
raise ApiKeyError("No API key found in stash for {}".format(api_key_name))
return stashed_keys[api_key_name]
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ classifiers=[
]
requires-python = ">=3.8"
dependencies = [
"datasette-enrichments>=0.2",
"datasette-enrichments>=0.4.1",
"sqlite-utils"
]

Expand Down

0 comments on commit c20758d

Please sign in to comment.