Skip to content

Commit

Permalink
description, obtain_url, obtain_label on Secret, closes #12
Browse files Browse the repository at this point in the history
  • Loading branch information
simonw committed Apr 24, 2024
1 parent bd1f257 commit 56640af
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 11 deletions.
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,24 @@ from datasette_secrets import Secret
def register_secrets():
return [
Secret(
"OPENAI_API_KEY",
'An OpenAI API key. Get them from <a href="https://platform.openai.com/api-keys">here</a>.',
name="OPENAI_API_KEY",
description="An OpenAI API key"
),
]
```
You can also provide optional `obtain_url` and `obtain_label` fields to link to a page where a user can obtain an API key:
```python
@hookimpl
def register_secrets():
return [
Secret(
name="OPENAI_API_KEY",
obtain_url="https://platform.openai.com/api-keys",
obtain_label="Get an OpenAI API key"
),
]
```

The hook can take an optional `datasette` argument. It can return a list or an `async def` function that, when awaited, returns a list.

The list should consist of `Secret()` instances, each with a name and an optional description. The description can contain HTML.
Expand Down
11 changes: 9 additions & 2 deletions datasette_secrets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ async def get_secret(datasette, secret_name, actor_id=None):
@dataclasses.dataclass
class Secret:
name: str
description_html: Optional[str] = None
description: Optional[str] = None
obtain_url: Optional[str] = None
obtain_label: Optional[str] = None


SCHEMA = """
Expand Down Expand Up @@ -114,9 +116,14 @@ def register_permissions(datasette):

async def get_secrets(datasette):
secrets = []
seen = set()
for result in pm.hook.register_secrets(datasette=datasette):
result = await await_me_maybe(result)
secrets.extend(result)
for secret in result:
if secret.name in seen:
continue # Skip duplicates
seen.add(secret.name)
secrets.append(secret)
# if not secrets:
secrets.append(Secret("EXAMPLE_SECRET", "An example secret"))

Expand Down
7 changes: 5 additions & 2 deletions datasette_secrets/templates/secrets_index.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ <h1>Manage secrets</h1>
<ul>
{% for secret in unset_secrets %}
<li><strong><a href="{{ urls.path("/-/secrets/") }}{{ secret.name }}">{{ secret.name }}</a></strong>
{% if secret.description_html %} - {{ secret.description_html|safe }}{% endif %}</li>
{% if secret.description or secret.obtain_label %}
- {{ secret.description or "" }}{% if secret.description and secret.obtain_label %}, {% endif %}
{% if secret.obtain_label %}<a href="{{ secret.obtain_url }}">{{ secret.obtain_label }}</a>{% endif %}
{% endif %}</li>
{% endfor %}
</ul>
{% endif %}
Expand All @@ -34,7 +37,7 @@ <h1>Manage secrets</h1>
<p style="margin-top: 2em">The following secret{% if environment_secrets|length == 1 %} is{% else %}s are{% endif %} set using environment variables:</p>
<ul>
{% for secret in environment_secrets %}
<li><strong>{{ secret.name }}</a></strong>- {{ secret.description_html|safe }}<br>
<li><strong>{{ secret.name }}</a></strong>{% if secret.description %} - {{ secret.description }}{% endif %}<br>
<span style="font-size: 0.8 em">Set by <code>DATASETTE_SECRETS_{{ secret.name }}</code></span></li>
{% endfor %}
</ul>
Expand Down
6 changes: 4 additions & 2 deletions datasette_secrets/templates/secrets_update.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
{% block content %}
<h1>{% if current_secret %}Update{% else %}Add{% endif %} secret: {{ secret_name }}</h1>

{% if secret_details and secret_details.description_html %}
<p>{{ secret_details.description_html|safe }}</p>
{% if secret_details.description or secret_details.obtain_label %}
<p>{{ secret_details.description or "" }}{% if secret_details.description and secret_details.obtain_label %}. {% endif %}
{% if secret_details.obtain_label %}<a href="{{ secret_details.obtain_url }}">{{ secret_details.obtain_label }}</a>{% endif %}
</p>
{% endif %}

{% if error %}
Expand Down
93 changes: 90 additions & 3 deletions tests/test_secrets.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from datasette.app import Datasette
from datasette.cli import cli
from datasette.plugins import pm
from datasette_secrets import get_secret
from datasette_secrets import get_secret, Secret
import pytest
from unittest.mock import ANY

Expand Down Expand Up @@ -38,9 +38,52 @@ def actors_from_ids(self, actor_ids):
for id in actor_ids
}

pm.register(ActorPlugin(), name="undo")
pm.register(ActorPlugin(), name="ActorPlugin")
yield
pm.unregister(name="undo")
pm.unregister(name="ActorPlugin")


@pytest.fixture
def register_multiple_secrets():
class SecretOnePlugin:
__name__ = "SecretOnePlugin"

@hookimpl
def register_secrets(self):
return [
Secret(
name="OPENAI_API_KEY",
obtain_url="https://platform.openai.com/api-keys",
obtain_label="Get an OpenAI API key",
),
Secret(
name="ANTHROPIC_API_KEY", description="A key for Anthropic's API"
),
]

class SecretTwoPlugin:
__name__ = "SecretTwoPlugin"

@hookimpl
def register_secrets(self):
return [
Secret(
name="OPENAI_API_KEY",
description="Just a description but should be ignored",
),
Secret(
name="OPENCAGE_API_KEY",
description="The OpenCage Geocoder",
obtain_url="https://opencagedata.com/dashboard",
obtain_label="Get an OpenCage API key",
),
]

pm.register(SecretTwoPlugin(), name="SecretTwoPlugin")
pm.register(SecretOnePlugin(), name="SecretOnePlugin")
yield
pm.unregister(name="SecretOnePlugin")
pm.unregister(name="SecretTwoPlugin")


@pytest.fixture
Expand Down Expand Up @@ -216,3 +259,47 @@ async def test_get_secret(ds, monkeypatch):
monkeypatch.setenv("DATASETTE_SECRETS_EXAMPLE_SECRET", "from env")

assert await get_secret(ds, "EXAMPLE_SECRET") == "from env"

# And check that it's shown that way on the /-/secrets page
response = await ds.client.get("/-/secrets", cookies=cookies)
assert response.status_code == 200
expected_html = """
<li><strong>EXAMPLE_SECRET</a></strong> - An example secret<br>
<span style="font-size: 0.8 em">Set by <code>DATASETTE_SECRETS_EXAMPLE_SECRET</code></span></li>
"""
assert remove_whitespace(expected_html) in remove_whitespace(response.text)


@pytest.mark.asyncio
async def test_secret_index_page(ds, register_multiple_secrets):
response = await ds.client.get(
"/-/secrets",
cookies={
"ds_actor": ds.client.actor_cookie({"id": "admin"}),
},
)
assert response.status_code == 200
expected_html = """
<p style="margin-top: 2em">The following secrets have not been set:</p>
<ul>
<li><strong><a href="/-/secrets/OPENAI_API_KEY">OPENAI_API_KEY</a></strong>
-
<a href="https://platform.openai.com/api-keys">Get an OpenAI API key</a>
</li>
<li><strong><a href="/-/secrets/ANTHROPIC_API_KEY">ANTHROPIC_API_KEY</a></strong>
- A key for Anthropic&#39;s API
</li>
<li><strong><a href="/-/secrets/OPENCAGE_API_KEY">OPENCAGE_API_KEY</a></strong>
- The OpenCage Geocoder,
<a href="https://opencagedata.com/dashboard">Get an OpenCage API key</a>
</li>
<li><strong><a href="/-/secrets/EXAMPLE_SECRET">EXAMPLE_SECRET</a></strong>
- An example secret
</li>
</ul>
"""
assert remove_whitespace(expected_html) in remove_whitespace(response.text)


def remove_whitespace(s):
return " ".join(s.split())

1 comment on commit 56640af

@simonw
Copy link
Contributor Author

@simonw simonw commented on 56640af Apr 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.