New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Mechanism for specifying that an Enrichment subclass uses a secret #46
Comments
Here's how async def get_config_form(self, datasette, db, table):
class ConfigForm(Form):
# Select box to pick model from gpt-3.5-turbo or gpt-4-turbo
model = SelectField(
"Model",
choices=[
("gpt-3.5-turbo", "gpt-3.5-turbo"),
("gpt-4-turbo", "gpt-4-turbo"),
("gpt-4-vision", "gpt-4-turbo vision"),
],
default="gpt-3.5-turbo",
)
# ... more stuff like that
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 Then it accesses that stashed key here: 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] |
Since I'm encouraging integration with One potential design: class GptEnrichment(Enrichment):
name = "AI analysis with OpenAI GPT"
slug = "gpt"
description = "Analyze data using OpenAI's GPT models"
runs_in_process = True
batch_size = 1
needs_api_key = "OPENAI_API_KEY"
needs_api_key_label = "OpenAI API Key"
needs_api_key_obtain_url = "https://..." Or... since we'll be using |
I'm going to experiment with |
Maybe don't even tell plugins they have to call |
This depends on |
... which means the entire enrichments ecosystem is about to go alpha-only, which isn't ideal. |
Shipped |
OK, |
Writing the documentation first - documentation-driven development. |
Here's that initial documentation: https://github.com/datasette/datasette-enrichments/blob/5e06d06db0572880668e3dd8e6fbde8b04a9db73/docs/developing.md#enrichments-that-use-secrets-such-as-api-keys And the code example (which won't work yet): from datasette_enrichments import Enrichment
from datasette_secrets import Secret
# Then later in your enrichments class
class TrainEnthusiastsEnrichment(Enrichment):
name = "Train Enthusiasts"
slug = "train-enthusiasts"
description = "Enrich with extra data from the Train Enthusiasts API"
secret = Secret(
name="TRAIN_ENTHUSIASTS_API_KEY",
description="An API key from train-enthusiasts.doesnt.exist",
obtain_url="https://train-enthusiasts.doesnt.exist/api-keys",
obtain_label="Get an API key"
) |
The |
I'll use this pattern for that: class BaseClass:
subclasses = []
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls.subclasses.append(cls) |
I tested this and it works! The secrets show up in the Running test server like this at the moment: export KEY=$(datasette secrets generate-encryption-key)
datasette *.db -p 8026 --root \
-s plugins.datasette-secrets.encryption-key $KEY \
-s permissions.manage-secrets.id root \
--secret 1 I applied this change to my local copy of diff --git a/datasette_enrichments_gpt/__init__.py b/datasette_enrichments_gpt/__init__.py
index 84e7ac4..15501df 100644
--- a/datasette_enrichments_gpt/__init__.py
+++ b/datasette_enrichments_gpt/__init__.py
@@ -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
@@ -28,6 +29,7 @@ class GptEnrichment(Enrichment):
description = "Analyze data using OpenAI's GPT models"
runs_in_process = True
batch_size = 1
+ secret = Secret("OPENAI_API_KEY_FOR_ENRICHMENTS")
async def get_config_form(self, datasette, db, table):
columns = await db.table_columns(table) |
Next step: make it so this works - it should try |
Better: That's for: class GptEnrichment(Enrichment):
name = "AI analysis with OpenAI GPT"
slug = "gpt"
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",
) |
Next step is to figure out how to have an |
Everything is working now, needs tests before I land it. |
Split from:
The text was updated successfully, but these errors were encountered: