datasette-secrets 0.1a1__tar.gz → 0.1a3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of datasette-secrets might be problematic. Click here for more details.
- {datasette_secrets-0.1a1 → datasette_secrets-0.1a3}/PKG-INFO +18 -3
- {datasette_secrets-0.1a1 → datasette_secrets-0.1a3}/README.md +17 -2
- {datasette_secrets-0.1a1 → datasette_secrets-0.1a3}/datasette_secrets/__init__.py +29 -19
- {datasette_secrets-0.1a1 → datasette_secrets-0.1a3}/datasette_secrets/templates/secrets_index.html +5 -2
- {datasette_secrets-0.1a1 → datasette_secrets-0.1a3}/datasette_secrets/templates/secrets_update.html +4 -2
- {datasette_secrets-0.1a1 → datasette_secrets-0.1a3}/datasette_secrets.egg-info/PKG-INFO +18 -3
- {datasette_secrets-0.1a1 → datasette_secrets-0.1a3}/pyproject.toml +1 -1
- {datasette_secrets-0.1a1 → datasette_secrets-0.1a3}/tests/test_secrets.py +123 -2
- {datasette_secrets-0.1a1 → datasette_secrets-0.1a3}/LICENSE +0 -0
- {datasette_secrets-0.1a1 → datasette_secrets-0.1a3}/datasette_secrets/hookspecs.py +0 -0
- {datasette_secrets-0.1a1 → datasette_secrets-0.1a3}/datasette_secrets.egg-info/SOURCES.txt +0 -0
- {datasette_secrets-0.1a1 → datasette_secrets-0.1a3}/datasette_secrets.egg-info/dependency_links.txt +0 -0
- {datasette_secrets-0.1a1 → datasette_secrets-0.1a3}/datasette_secrets.egg-info/entry_points.txt +0 -0
- {datasette_secrets-0.1a1 → datasette_secrets-0.1a3}/datasette_secrets.egg-info/requires.txt +0 -0
- {datasette_secrets-0.1a1 → datasette_secrets-0.1a3}/datasette_secrets.egg-info/top_level.txt +0 -0
- {datasette_secrets-0.1a1 → datasette_secrets-0.1a3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: datasette-secrets
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.1a3
|
|
4
4
|
Summary: Manage secrets such as API keys for use with other Datasette plugins
|
|
5
5
|
Author: Datasette
|
|
6
6
|
License: Apache-2.0
|
|
@@ -105,6 +105,8 @@ datasette data.db --internal internal.db \
|
|
|
105
105
|
|
|
106
106
|
users with the `manage-secrets` permission will see a new "Manage secrets" link in the Datasette navigation menu. This interface can also be accessed at `/-/secrets`.
|
|
107
107
|
|
|
108
|
+
The page with the list of secrets will show the user who last updated each secret. This will use the [actors_from_ids()](https://docs.datasette.io/en/latest/plugin_hooks.html#actors-from-ids-datasette-actor-ids) mechanism, displaying the actor's `username` if available, otherwise the `name`, otherwise the `id`.
|
|
109
|
+
|
|
108
110
|
## For plugin authors
|
|
109
111
|
|
|
110
112
|
Plugins can depend on this plugin if they want to implement secrets.
|
|
@@ -121,11 +123,24 @@ from datasette_secrets import Secret
|
|
|
121
123
|
def register_secrets():
|
|
122
124
|
return [
|
|
123
125
|
Secret(
|
|
124
|
-
"OPENAI_API_KEY",
|
|
125
|
-
|
|
126
|
+
name="OPENAI_API_KEY",
|
|
127
|
+
description="An OpenAI API key"
|
|
128
|
+
),
|
|
129
|
+
]
|
|
130
|
+
```
|
|
131
|
+
You can also provide optional `obtain_url` and `obtain_label` fields to link to a page where a user can obtain an API key:
|
|
132
|
+
```python
|
|
133
|
+
@hookimpl
|
|
134
|
+
def register_secrets():
|
|
135
|
+
return [
|
|
136
|
+
Secret(
|
|
137
|
+
name="OPENAI_API_KEY",
|
|
138
|
+
obtain_url="https://platform.openai.com/api-keys",
|
|
139
|
+
obtain_label="Get an OpenAI API key"
|
|
126
140
|
),
|
|
127
141
|
]
|
|
128
142
|
```
|
|
143
|
+
|
|
129
144
|
The hook can take an optional `datasette` argument. It can return a list or an `async def` function that, when awaited, returns a list.
|
|
130
145
|
|
|
131
146
|
The list should consist of `Secret()` instances, each with a name and an optional description. The description can contain HTML.
|
|
@@ -84,6 +84,8 @@ datasette data.db --internal internal.db \
|
|
|
84
84
|
|
|
85
85
|
users with the `manage-secrets` permission will see a new "Manage secrets" link in the Datasette navigation menu. This interface can also be accessed at `/-/secrets`.
|
|
86
86
|
|
|
87
|
+
The page with the list of secrets will show the user who last updated each secret. This will use the [actors_from_ids()](https://docs.datasette.io/en/latest/plugin_hooks.html#actors-from-ids-datasette-actor-ids) mechanism, displaying the actor's `username` if available, otherwise the `name`, otherwise the `id`.
|
|
88
|
+
|
|
87
89
|
## For plugin authors
|
|
88
90
|
|
|
89
91
|
Plugins can depend on this plugin if they want to implement secrets.
|
|
@@ -100,11 +102,24 @@ from datasette_secrets import Secret
|
|
|
100
102
|
def register_secrets():
|
|
101
103
|
return [
|
|
102
104
|
Secret(
|
|
103
|
-
"OPENAI_API_KEY",
|
|
104
|
-
|
|
105
|
+
name="OPENAI_API_KEY",
|
|
106
|
+
description="An OpenAI API key"
|
|
107
|
+
),
|
|
108
|
+
]
|
|
109
|
+
```
|
|
110
|
+
You can also provide optional `obtain_url` and `obtain_label` fields to link to a page where a user can obtain an API key:
|
|
111
|
+
```python
|
|
112
|
+
@hookimpl
|
|
113
|
+
def register_secrets():
|
|
114
|
+
return [
|
|
115
|
+
Secret(
|
|
116
|
+
name="OPENAI_API_KEY",
|
|
117
|
+
obtain_url="https://platform.openai.com/api-keys",
|
|
118
|
+
obtain_label="Get an OpenAI API key"
|
|
105
119
|
),
|
|
106
120
|
]
|
|
107
121
|
```
|
|
122
|
+
|
|
108
123
|
The hook can take an optional `datasette` argument. It can return a list or an `async def` function that, when awaited, returns a list.
|
|
109
124
|
|
|
110
125
|
The list should consist of `Secret()` instances, each with a name and an optional description. The description can contain HTML.
|
|
@@ -3,7 +3,7 @@ from cryptography.fernet import Fernet
|
|
|
3
3
|
import dataclasses
|
|
4
4
|
from datasette import hookimpl, Forbidden, Permission, Response
|
|
5
5
|
from datasette.plugins import pm
|
|
6
|
-
from datasette.utils import await_me_maybe
|
|
6
|
+
from datasette.utils import await_me_maybe, sqlite3
|
|
7
7
|
import os
|
|
8
8
|
from typing import Optional
|
|
9
9
|
from . import hookspecs
|
|
@@ -24,12 +24,15 @@ async def get_secret(datasette, secret_name, actor_id=None):
|
|
|
24
24
|
# Now look it up in the database
|
|
25
25
|
config = get_config(datasette)
|
|
26
26
|
db = get_database(datasette)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
try:
|
|
28
|
+
db_secret = (
|
|
29
|
+
await db.execute(
|
|
30
|
+
"select id, encrypted from datasette_secrets where name = ? order by version desc limit 1",
|
|
31
|
+
(secret_name,),
|
|
32
|
+
)
|
|
33
|
+
).first()
|
|
34
|
+
except sqlite3.OperationalError:
|
|
35
|
+
return None
|
|
33
36
|
if not db_secret:
|
|
34
37
|
return None
|
|
35
38
|
key = Fernet(config["encryption_key"].encode("utf-8"))
|
|
@@ -55,7 +58,9 @@ async def get_secret(datasette, secret_name, actor_id=None):
|
|
|
55
58
|
@dataclasses.dataclass
|
|
56
59
|
class Secret:
|
|
57
60
|
name: str
|
|
58
|
-
|
|
61
|
+
description: Optional[str] = None
|
|
62
|
+
obtain_url: Optional[str] = None
|
|
63
|
+
obtain_label: Optional[str] = None
|
|
59
64
|
|
|
60
65
|
|
|
61
66
|
SCHEMA = """
|
|
@@ -114,25 +119,20 @@ def register_permissions(datasette):
|
|
|
114
119
|
|
|
115
120
|
async def get_secrets(datasette):
|
|
116
121
|
secrets = []
|
|
122
|
+
seen = set()
|
|
117
123
|
for result in pm.hook.register_secrets(datasette=datasette):
|
|
118
124
|
result = await await_me_maybe(result)
|
|
119
|
-
|
|
125
|
+
for secret in result:
|
|
126
|
+
if secret.name in seen:
|
|
127
|
+
continue # Skip duplicates
|
|
128
|
+
seen.add(secret.name)
|
|
129
|
+
secrets.append(secret)
|
|
120
130
|
# if not secrets:
|
|
121
131
|
secrets.append(Secret("EXAMPLE_SECRET", "An example secret"))
|
|
122
132
|
|
|
123
133
|
return secrets
|
|
124
134
|
|
|
125
135
|
|
|
126
|
-
@hookimpl
|
|
127
|
-
def register_secrets():
|
|
128
|
-
return [
|
|
129
|
-
Secret(
|
|
130
|
-
"OPENAI_API_KEY",
|
|
131
|
-
'An OpenAI API key. Get them from <a href="https://platform.openai.com/api-keys">here</a>.',
|
|
132
|
-
),
|
|
133
|
-
]
|
|
134
|
-
|
|
135
|
-
|
|
136
136
|
@hookimpl
|
|
137
137
|
def register_commands(cli):
|
|
138
138
|
@cli.group()
|
|
@@ -183,6 +183,16 @@ async def secrets_index(datasette, request):
|
|
|
183
183
|
list(environment_secrets_names),
|
|
184
184
|
)
|
|
185
185
|
existing_secrets = {row["name"]: dict(row) for row in existing_secrets_result.rows}
|
|
186
|
+
# Try to turn updated_by into actors
|
|
187
|
+
actors = await datasette.actors_from_ids(
|
|
188
|
+
{row["updated_by"] for row in existing_secrets.values() if row["updated_by"]}
|
|
189
|
+
)
|
|
190
|
+
for secret in existing_secrets.values():
|
|
191
|
+
if secret["updated_by"]:
|
|
192
|
+
actor = actors.get(secret["updated_by"])
|
|
193
|
+
if actor:
|
|
194
|
+
display = actor.get("username") or actor.get("name") or actor.get("id")
|
|
195
|
+
secret["updated_by"] = display
|
|
186
196
|
unset_secrets = [
|
|
187
197
|
secret
|
|
188
198
|
for secret in all_secrets
|
{datasette_secrets-0.1a1 → datasette_secrets-0.1a3}/datasette_secrets/templates/secrets_index.html
RENAMED
|
@@ -25,7 +25,10 @@
|
|
|
25
25
|
<ul>
|
|
26
26
|
{% for secret in unset_secrets %}
|
|
27
27
|
<li><strong><a href="{{ urls.path("/-/secrets/") }}{{ secret.name }}">{{ secret.name }}</a></strong>
|
|
28
|
-
{% if secret.
|
|
28
|
+
{% if secret.description or secret.obtain_label %}
|
|
29
|
+
- {{ secret.description or "" }}{% if secret.description and secret.obtain_label %}, {% endif %}
|
|
30
|
+
{% if secret.obtain_label %}<a href="{{ secret.obtain_url }}">{{ secret.obtain_label }}</a>{% endif %}
|
|
31
|
+
{% endif %}</li>
|
|
29
32
|
{% endfor %}
|
|
30
33
|
</ul>
|
|
31
34
|
{% endif %}
|
|
@@ -34,7 +37,7 @@
|
|
|
34
37
|
<p style="margin-top: 2em">The following secret{% if environment_secrets|length == 1 %} is{% else %}s are{% endif %} set using environment variables:</p>
|
|
35
38
|
<ul>
|
|
36
39
|
{% for secret in environment_secrets %}
|
|
37
|
-
<li><strong>{{ secret.name }}</a></strong
|
|
40
|
+
<li><strong>{{ secret.name }}</a></strong>{% if secret.description %} - {{ secret.description }}{% endif %}<br>
|
|
38
41
|
<span style="font-size: 0.8 em">Set by <code>DATASETTE_SECRETS_{{ secret.name }}</code></span></li>
|
|
39
42
|
{% endfor %}
|
|
40
43
|
</ul>
|
{datasette_secrets-0.1a1 → datasette_secrets-0.1a3}/datasette_secrets/templates/secrets_update.html
RENAMED
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
{% block content %}
|
|
10
10
|
<h1>{% if current_secret %}Update{% else %}Add{% endif %} secret: {{ secret_name }}</h1>
|
|
11
11
|
|
|
12
|
-
{% if secret_details
|
|
13
|
-
<p>{{ secret_details.
|
|
12
|
+
{% if secret_details.description or secret_details.obtain_label %}
|
|
13
|
+
<p>{{ secret_details.description or "" }}{% if secret_details.description and secret_details.obtain_label %}. {% endif %}
|
|
14
|
+
{% if secret_details.obtain_label %}<a href="{{ secret_details.obtain_url }}">{{ secret_details.obtain_label }}</a>{% endif %}
|
|
15
|
+
</p>
|
|
14
16
|
{% endif %}
|
|
15
17
|
|
|
16
18
|
{% if error %}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: datasette-secrets
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.1a3
|
|
4
4
|
Summary: Manage secrets such as API keys for use with other Datasette plugins
|
|
5
5
|
Author: Datasette
|
|
6
6
|
License: Apache-2.0
|
|
@@ -105,6 +105,8 @@ datasette data.db --internal internal.db \
|
|
|
105
105
|
|
|
106
106
|
users with the `manage-secrets` permission will see a new "Manage secrets" link in the Datasette navigation menu. This interface can also be accessed at `/-/secrets`.
|
|
107
107
|
|
|
108
|
+
The page with the list of secrets will show the user who last updated each secret. This will use the [actors_from_ids()](https://docs.datasette.io/en/latest/plugin_hooks.html#actors-from-ids-datasette-actor-ids) mechanism, displaying the actor's `username` if available, otherwise the `name`, otherwise the `id`.
|
|
109
|
+
|
|
108
110
|
## For plugin authors
|
|
109
111
|
|
|
110
112
|
Plugins can depend on this plugin if they want to implement secrets.
|
|
@@ -121,11 +123,24 @@ from datasette_secrets import Secret
|
|
|
121
123
|
def register_secrets():
|
|
122
124
|
return [
|
|
123
125
|
Secret(
|
|
124
|
-
"OPENAI_API_KEY",
|
|
125
|
-
|
|
126
|
+
name="OPENAI_API_KEY",
|
|
127
|
+
description="An OpenAI API key"
|
|
128
|
+
),
|
|
129
|
+
]
|
|
130
|
+
```
|
|
131
|
+
You can also provide optional `obtain_url` and `obtain_label` fields to link to a page where a user can obtain an API key:
|
|
132
|
+
```python
|
|
133
|
+
@hookimpl
|
|
134
|
+
def register_secrets():
|
|
135
|
+
return [
|
|
136
|
+
Secret(
|
|
137
|
+
name="OPENAI_API_KEY",
|
|
138
|
+
obtain_url="https://platform.openai.com/api-keys",
|
|
139
|
+
obtain_label="Get an OpenAI API key"
|
|
126
140
|
),
|
|
127
141
|
]
|
|
128
142
|
```
|
|
143
|
+
|
|
129
144
|
The hook can take an optional `datasette` argument. It can return a list or an `async def` function that, when awaited, returns a list.
|
|
130
145
|
|
|
131
146
|
The list should consist of `Secret()` instances, each with a name and an optional description. The description can contain HTML.
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from click.testing import CliRunner
|
|
2
2
|
from cryptography.fernet import Fernet
|
|
3
|
+
from datasette import hookimpl
|
|
3
4
|
from datasette.app import Datasette
|
|
4
5
|
from datasette.cli import cli
|
|
5
|
-
from
|
|
6
|
+
from datasette.plugins import pm
|
|
7
|
+
from datasette_secrets import get_secret, Secret, startup
|
|
6
8
|
import pytest
|
|
7
9
|
from unittest.mock import ANY
|
|
8
10
|
|
|
@@ -21,6 +23,69 @@ def test_generate_command():
|
|
|
21
23
|
assert key.decrypt(key.encrypt(message)) == message
|
|
22
24
|
|
|
23
25
|
|
|
26
|
+
@pytest.fixture
|
|
27
|
+
def use_actors_plugin():
|
|
28
|
+
class ActorPlugin:
|
|
29
|
+
__name__ = "ActorPlugin"
|
|
30
|
+
|
|
31
|
+
@hookimpl
|
|
32
|
+
def actors_from_ids(self, actor_ids):
|
|
33
|
+
return {
|
|
34
|
+
id: {
|
|
35
|
+
"id": id,
|
|
36
|
+
"username": id.upper(),
|
|
37
|
+
}
|
|
38
|
+
for id in actor_ids
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
pm.register(ActorPlugin(), name="ActorPlugin")
|
|
42
|
+
yield
|
|
43
|
+
pm.unregister(name="ActorPlugin")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.fixture
|
|
47
|
+
def register_multiple_secrets():
|
|
48
|
+
class SecretOnePlugin:
|
|
49
|
+
__name__ = "SecretOnePlugin"
|
|
50
|
+
|
|
51
|
+
@hookimpl
|
|
52
|
+
def register_secrets(self):
|
|
53
|
+
return [
|
|
54
|
+
Secret(
|
|
55
|
+
name="OPENAI_API_KEY",
|
|
56
|
+
obtain_url="https://platform.openai.com/api-keys",
|
|
57
|
+
obtain_label="Get an OpenAI API key",
|
|
58
|
+
),
|
|
59
|
+
Secret(
|
|
60
|
+
name="ANTHROPIC_API_KEY", description="A key for Anthropic's API"
|
|
61
|
+
),
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
class SecretTwoPlugin:
|
|
65
|
+
__name__ = "SecretTwoPlugin"
|
|
66
|
+
|
|
67
|
+
@hookimpl
|
|
68
|
+
def register_secrets(self):
|
|
69
|
+
return [
|
|
70
|
+
Secret(
|
|
71
|
+
name="OPENAI_API_KEY",
|
|
72
|
+
description="Just a description but should be ignored",
|
|
73
|
+
),
|
|
74
|
+
Secret(
|
|
75
|
+
name="OPENCAGE_API_KEY",
|
|
76
|
+
description="The OpenCage Geocoder",
|
|
77
|
+
obtain_url="https://opencagedata.com/dashboard",
|
|
78
|
+
obtain_label="Get an OpenCage API key",
|
|
79
|
+
),
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
pm.register(SecretTwoPlugin(), name="SecretTwoPlugin")
|
|
83
|
+
pm.register(SecretOnePlugin(), name="SecretOnePlugin")
|
|
84
|
+
yield
|
|
85
|
+
pm.unregister(name="SecretOnePlugin")
|
|
86
|
+
pm.unregister(name="SecretTwoPlugin")
|
|
87
|
+
|
|
88
|
+
|
|
24
89
|
@pytest.fixture
|
|
25
90
|
def ds():
|
|
26
91
|
return Datasette(
|
|
@@ -65,7 +130,7 @@ async def test_permissions(ds, path, verb, data, user):
|
|
|
65
130
|
|
|
66
131
|
|
|
67
132
|
@pytest.mark.asyncio
|
|
68
|
-
async def test_set_secret(ds):
|
|
133
|
+
async def test_set_secret(ds, use_actors_plugin):
|
|
69
134
|
cookies = {"ds_actor": ds.client.actor_cookie({"id": "admin"})}
|
|
70
135
|
get_response = await ds.client.get("/-/secrets/EXAMPLE_SECRET", cookies=cookies)
|
|
71
136
|
csrftoken = get_response.cookies["ds_csrftoken"]
|
|
@@ -104,6 +169,13 @@ async def test_set_secret(ds):
|
|
|
104
169
|
decrypted = key.decrypt(encrypted)
|
|
105
170
|
assert decrypted == b"new-secret-value"
|
|
106
171
|
|
|
172
|
+
# Check that the listing is as expected, including showing the actor username
|
|
173
|
+
response = await ds.client.get("/-/secrets", cookies=cookies)
|
|
174
|
+
assert response.status_code == 200
|
|
175
|
+
assert "EXAMPLE_SECRET" in response.text
|
|
176
|
+
assert "new-note" in response.text
|
|
177
|
+
assert "<td>ADMIN</td>" in response.text
|
|
178
|
+
|
|
107
179
|
# Now let's edit it
|
|
108
180
|
post_response2 = await ds.client.post(
|
|
109
181
|
"/-/secrets/EXAMPLE_SECRET",
|
|
@@ -187,3 +259,52 @@ async def test_get_secret(ds, monkeypatch):
|
|
|
187
259
|
monkeypatch.setenv("DATASETTE_SECRETS_EXAMPLE_SECRET", "from env")
|
|
188
260
|
|
|
189
261
|
assert await get_secret(ds, "EXAMPLE_SECRET") == "from env"
|
|
262
|
+
|
|
263
|
+
# And check that it's shown that way on the /-/secrets page
|
|
264
|
+
response = await ds.client.get("/-/secrets", cookies=cookies)
|
|
265
|
+
assert response.status_code == 200
|
|
266
|
+
expected_html = """
|
|
267
|
+
<li><strong>EXAMPLE_SECRET</a></strong> - An example secret<br>
|
|
268
|
+
<span style="font-size: 0.8 em">Set by <code>DATASETTE_SECRETS_EXAMPLE_SECRET</code></span></li>
|
|
269
|
+
"""
|
|
270
|
+
assert remove_whitespace(expected_html) in remove_whitespace(response.text)
|
|
271
|
+
|
|
272
|
+
# Finally it should still work even if the datasette_secrets table is missing
|
|
273
|
+
await db.execute_write("drop table datasette_secrets")
|
|
274
|
+
monkeypatch.delenv("DATASETTE_SECRETS_EXAMPLE_SECRET")
|
|
275
|
+
assert await get_secret(ds, "EXAMPLE_SECRET") is None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@pytest.mark.asyncio
|
|
279
|
+
async def test_secret_index_page(ds, register_multiple_secrets):
|
|
280
|
+
response = await ds.client.get(
|
|
281
|
+
"/-/secrets",
|
|
282
|
+
cookies={
|
|
283
|
+
"ds_actor": ds.client.actor_cookie({"id": "admin"}),
|
|
284
|
+
},
|
|
285
|
+
)
|
|
286
|
+
assert response.status_code == 200
|
|
287
|
+
expected_html = """
|
|
288
|
+
<p style="margin-top: 2em">The following secrets have not been set:</p>
|
|
289
|
+
<ul>
|
|
290
|
+
<li><strong><a href="/-/secrets/OPENAI_API_KEY">OPENAI_API_KEY</a></strong>
|
|
291
|
+
-
|
|
292
|
+
<a href="https://platform.openai.com/api-keys">Get an OpenAI API key</a>
|
|
293
|
+
</li>
|
|
294
|
+
<li><strong><a href="/-/secrets/ANTHROPIC_API_KEY">ANTHROPIC_API_KEY</a></strong>
|
|
295
|
+
- A key for Anthropic's API
|
|
296
|
+
</li>
|
|
297
|
+
<li><strong><a href="/-/secrets/OPENCAGE_API_KEY">OPENCAGE_API_KEY</a></strong>
|
|
298
|
+
- The OpenCage Geocoder,
|
|
299
|
+
<a href="https://opencagedata.com/dashboard">Get an OpenCage API key</a>
|
|
300
|
+
</li>
|
|
301
|
+
<li><strong><a href="/-/secrets/EXAMPLE_SECRET">EXAMPLE_SECRET</a></strong>
|
|
302
|
+
- An example secret
|
|
303
|
+
</li>
|
|
304
|
+
</ul>
|
|
305
|
+
"""
|
|
306
|
+
assert remove_whitespace(expected_html) in remove_whitespace(response.text)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def remove_whitespace(s):
|
|
310
|
+
return " ".join(s.split())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datasette_secrets-0.1a1 → datasette_secrets-0.1a3}/datasette_secrets.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{datasette_secrets-0.1a1 → datasette_secrets-0.1a3}/datasette_secrets.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
{datasette_secrets-0.1a1 → datasette_secrets-0.1a3}/datasette_secrets.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|