datasette-write 0.3.2__py3-none-any.whl → 0.4__py3-none-any.whl
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-write might be problematic. Click here for more details.
- datasette_write/__init__.py +134 -49
- datasette_write/templates/datasette_write.html +46 -14
- {datasette_write-0.3.2.dist-info → datasette_write-0.4.dist-info}/METADATA +11 -4
- datasette_write-0.4.dist-info/RECORD +7 -0
- {datasette_write-0.3.2.dist-info → datasette_write-0.4.dist-info}/WHEEL +1 -1
- datasette_write-0.3.2.dist-info/RECORD +0 -7
- {datasette_write-0.3.2.dist-info → datasette_write-0.4.dist-info}/entry_points.txt +0 -0
- {datasette_write-0.3.2.dist-info → datasette_write-0.4.dist-info}/top_level.txt +0 -0
datasette_write/__init__.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from datasette import hookimpl, Forbidden, Response
|
|
2
2
|
from datasette.utils import derive_named_parameters
|
|
3
|
+
import itsdangerous
|
|
3
4
|
from urllib.parse import urlencode
|
|
4
5
|
import re
|
|
5
6
|
|
|
@@ -9,41 +10,43 @@ async def write(request, datasette):
|
|
|
9
10
|
request.actor, "datasette-write", default=False
|
|
10
11
|
):
|
|
11
12
|
raise Forbidden("Permission denied for datasette-write")
|
|
12
|
-
|
|
13
|
-
db
|
|
14
|
-
for db in datasette.databases.values()
|
|
15
|
-
if db.is_mutable and db.name != "_internal"
|
|
16
|
-
]
|
|
13
|
+
database_name = request.url_vars["database"]
|
|
17
14
|
if request.method == "GET":
|
|
18
|
-
|
|
19
|
-
if not selected_database or selected_database == "_internal":
|
|
20
|
-
selected_database = databases[0].name
|
|
21
|
-
database = datasette.get_database(selected_database)
|
|
15
|
+
database = datasette.get_database(database_name)
|
|
22
16
|
tables = await database.table_names()
|
|
23
17
|
views = await database.view_names()
|
|
24
18
|
sql = request.args.get("sql") or ""
|
|
19
|
+
parameters = await derive_parameters(database, sql)
|
|
20
|
+
# Set values based on incoming request query string
|
|
21
|
+
for parameter in parameters:
|
|
22
|
+
parameter["value"] = request.args.get(parameter["name"]) or ""
|
|
23
|
+
custom_title = ""
|
|
24
|
+
try:
|
|
25
|
+
custom_title = datasette.unsign(
|
|
26
|
+
request.args.get("_title", ""), "query_title"
|
|
27
|
+
)
|
|
28
|
+
except itsdangerous.BadSignature:
|
|
29
|
+
pass
|
|
25
30
|
return Response.html(
|
|
26
31
|
await datasette.render_template(
|
|
27
32
|
"datasette_write.html",
|
|
28
33
|
{
|
|
29
|
-
"
|
|
34
|
+
"custom_title": custom_title,
|
|
30
35
|
"sql_from_args": sql,
|
|
31
|
-
"
|
|
32
|
-
"
|
|
36
|
+
"parameters": parameters,
|
|
37
|
+
"database_name": database_name,
|
|
33
38
|
"tables": tables,
|
|
34
39
|
"views": views,
|
|
40
|
+
"redirect_to": request.args.get("_redirect_to"),
|
|
41
|
+
"sql_textarea_height": max(10, int(1.4 * len(sql.split("\n")))),
|
|
35
42
|
},
|
|
36
43
|
request=request,
|
|
37
44
|
)
|
|
38
45
|
)
|
|
39
46
|
elif request.method == "POST":
|
|
40
47
|
formdata = await request.post_vars()
|
|
41
|
-
database_name = formdata["database"]
|
|
42
48
|
sql = formdata["sql"]
|
|
43
|
-
|
|
44
|
-
database = [db for db in databases if db.name == database_name][0]
|
|
45
|
-
except IndexError:
|
|
46
|
-
return Response.html("Database not found", status_code=404)
|
|
49
|
+
database = datasette.get_database(database_name)
|
|
47
50
|
|
|
48
51
|
result = None
|
|
49
52
|
message = None
|
|
@@ -79,31 +82,64 @@ async def write(request, datasette):
|
|
|
79
82
|
message,
|
|
80
83
|
type=datasette.INFO if result else datasette.ERROR,
|
|
81
84
|
)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
)
|
|
85
|
+
# Default redirect back to this page
|
|
86
|
+
redirect_to = datasette.urls.path("/-/write?") + urlencode(
|
|
87
|
+
{
|
|
88
|
+
"database": database.name,
|
|
89
|
+
"sql": sql,
|
|
90
|
+
}
|
|
90
91
|
)
|
|
92
|
+
try:
|
|
93
|
+
# Unless value and valid signature for _redirect_to=
|
|
94
|
+
redirect_to = datasette.unsign(formdata["_redirect_to"], "redirect_to")
|
|
95
|
+
except (itsdangerous.BadSignature, KeyError):
|
|
96
|
+
pass
|
|
97
|
+
return Response.redirect(redirect_to)
|
|
91
98
|
else:
|
|
92
99
|
return Response.html("Bad method", status_code=405)
|
|
93
100
|
|
|
94
101
|
|
|
102
|
+
async def write_redirect(request, datasette):
|
|
103
|
+
if not await datasette.permission_allowed(
|
|
104
|
+
request.actor, "datasette-write", default=False
|
|
105
|
+
):
|
|
106
|
+
raise Forbidden("Permission denied for datasette-write")
|
|
107
|
+
|
|
108
|
+
db = request.args.get("database") or ""
|
|
109
|
+
if not db:
|
|
110
|
+
db = datasette.get_database().name
|
|
111
|
+
|
|
112
|
+
# Preserve query string, except the database=
|
|
113
|
+
pairs = [
|
|
114
|
+
(key, request.args.getlist(key)) for key in request.args if key != "database"
|
|
115
|
+
]
|
|
116
|
+
query_string = ""
|
|
117
|
+
if pairs:
|
|
118
|
+
query_string = "?" + urlencode(pairs, doseq=True)
|
|
119
|
+
|
|
120
|
+
return Response.redirect(datasette.urls.database(db) + "/-/write" + query_string)
|
|
121
|
+
|
|
122
|
+
|
|
95
123
|
async def derive_parameters(db, sql):
|
|
96
124
|
parameters = await derive_named_parameters(db, sql)
|
|
125
|
+
|
|
126
|
+
def _type(parameter):
|
|
127
|
+
type = "text"
|
|
128
|
+
if parameter.endswith("_textarea"):
|
|
129
|
+
type = "textarea"
|
|
130
|
+
if parameter.endswith("_hidden"):
|
|
131
|
+
type = "hidden"
|
|
132
|
+
return type
|
|
133
|
+
|
|
134
|
+
def _label(parameter):
|
|
135
|
+
if parameter.endswith("_textarea"):
|
|
136
|
+
return parameter[: -len("_textarea")]
|
|
137
|
+
if parameter.endswith("_hidden"):
|
|
138
|
+
return parameter[: -len("_hidden")]
|
|
139
|
+
return parameter
|
|
140
|
+
|
|
97
141
|
return [
|
|
98
|
-
{
|
|
99
|
-
"name": parameter,
|
|
100
|
-
"type": "textarea" if parameter.endswith("textarea") else "text",
|
|
101
|
-
"label": (
|
|
102
|
-
parameter.replace("_textarea", "")
|
|
103
|
-
if parameter.endswith("textarea")
|
|
104
|
-
else parameter
|
|
105
|
-
),
|
|
106
|
-
}
|
|
142
|
+
{"name": parameter, "type": _type(parameter), "label": _label(parameter)}
|
|
107
143
|
for parameter in parameters
|
|
108
144
|
]
|
|
109
145
|
|
|
@@ -124,7 +160,8 @@ async def write_derive_parameters(datasette, request):
|
|
|
124
160
|
@hookimpl
|
|
125
161
|
def register_routes():
|
|
126
162
|
return [
|
|
127
|
-
(r"
|
|
163
|
+
(r"^/(?P<database>[^/]+)/-/write$", write),
|
|
164
|
+
(r"^/-/write$", write_redirect),
|
|
128
165
|
(r"^/-/write/derive-parameters$", write_derive_parameters),
|
|
129
166
|
]
|
|
130
167
|
|
|
@@ -136,13 +173,16 @@ def permission_allowed(actor, action):
|
|
|
136
173
|
|
|
137
174
|
|
|
138
175
|
@hookimpl
|
|
139
|
-
def
|
|
176
|
+
def database_actions(datasette, actor, database):
|
|
140
177
|
async def inner():
|
|
141
|
-
if await datasette.permission_allowed(
|
|
178
|
+
if database != "_internal" and await datasette.permission_allowed(
|
|
179
|
+
actor, "datasette-write", default=False
|
|
180
|
+
):
|
|
142
181
|
return [
|
|
143
182
|
{
|
|
144
|
-
"href": datasette.urls.
|
|
183
|
+
"href": datasette.urls.database(database) + "/-/write",
|
|
145
184
|
"label": "Execute SQL write",
|
|
185
|
+
"description": "Run queries like insert/update/delete against this database",
|
|
146
186
|
},
|
|
147
187
|
]
|
|
148
188
|
|
|
@@ -150,23 +190,68 @@ def menu_links(datasette, actor):
|
|
|
150
190
|
|
|
151
191
|
|
|
152
192
|
@hookimpl
|
|
153
|
-
def
|
|
193
|
+
def row_actions(datasette, actor, database, table, row, request):
|
|
154
194
|
async def inner():
|
|
155
195
|
if database != "_internal" and await datasette.permission_allowed(
|
|
156
196
|
actor, "datasette-write", default=False
|
|
157
197
|
):
|
|
198
|
+
db = datasette.get_database(database)
|
|
199
|
+
pks = []
|
|
200
|
+
columns = []
|
|
201
|
+
for details in await db.table_column_details(table):
|
|
202
|
+
if details.is_pk:
|
|
203
|
+
pks.append(details.name)
|
|
204
|
+
else:
|
|
205
|
+
columns.append(
|
|
206
|
+
{
|
|
207
|
+
"name": details.name,
|
|
208
|
+
"notnull": details.notnull,
|
|
209
|
+
}
|
|
210
|
+
)
|
|
211
|
+
row_dict = dict(row)
|
|
212
|
+
set_clause_bits = []
|
|
213
|
+
args = {
|
|
214
|
+
"database": database,
|
|
215
|
+
}
|
|
216
|
+
for column in columns:
|
|
217
|
+
column_name = column["name"]
|
|
218
|
+
field_name = column_name
|
|
219
|
+
if column_name in ("sql", "_redirect_to", "_title"):
|
|
220
|
+
field_name = "_{}".format(column_name)
|
|
221
|
+
current_value = str(row_dict.get(column_name) or "")
|
|
222
|
+
if "\n" in current_value:
|
|
223
|
+
field_name = field_name + "_textarea"
|
|
224
|
+
if column["notnull"]:
|
|
225
|
+
fragment = '"{}" = :{}'
|
|
226
|
+
else:
|
|
227
|
+
fragment = "\"{}\" = nullif(:{}, '')"
|
|
228
|
+
set_clause_bits.append(fragment.format(column["name"], field_name))
|
|
229
|
+
args[field_name] = current_value
|
|
230
|
+
set_clauses = ",\n ".join(set_clause_bits)
|
|
231
|
+
|
|
232
|
+
# Add the where clauses, with _hidden to prevent edits
|
|
233
|
+
where_clauses = " and ".join(
|
|
234
|
+
'"{}" = :{}_hidden'.format(pk, pk) for pk in pks
|
|
235
|
+
)
|
|
236
|
+
args.update([("{}_hidden".format(pk), row_dict[pk]) for pk in pks])
|
|
237
|
+
|
|
238
|
+
row_desc = ", ".join(
|
|
239
|
+
"{}={}".format(k, v) for k, v in row_dict.items() if k in pks
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
sql = 'update "{}" set\n {}\nwhere {}'.format(
|
|
243
|
+
table, set_clauses, where_clauses
|
|
244
|
+
)
|
|
245
|
+
args["sql"] = sql
|
|
246
|
+
args["_redirect_to"] = datasette.sign(request.path, "redirect_to")
|
|
247
|
+
args["_title"] = datasette.sign(
|
|
248
|
+
"Update {} where {}".format(table, row_desc), "query_title"
|
|
249
|
+
)
|
|
158
250
|
return [
|
|
159
251
|
{
|
|
160
|
-
"href": datasette.urls.path(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
{
|
|
164
|
-
"database": database,
|
|
165
|
-
}
|
|
166
|
-
)
|
|
167
|
-
),
|
|
168
|
-
"label": "Execute SQL write",
|
|
169
|
-
"description": "Run queries like insert/update/delete against this database",
|
|
252
|
+
"href": datasette.urls.path("/-/write") + "?" + urlencode(args),
|
|
253
|
+
"label": "Update using SQL",
|
|
254
|
+
"description": "Compose and execute a SQL query to update this row",
|
|
170
255
|
},
|
|
171
256
|
]
|
|
172
257
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{% extends "base.html" %}
|
|
2
2
|
|
|
3
|
-
{% block title %}Write with SQL{% endblock %}
|
|
3
|
+
{% block title %}{% if custom_title %}{{ custom_title }}{% else %}Write to {{ database_name }} with SQL{% endif %}{% endblock %}
|
|
4
4
|
|
|
5
5
|
{% block extra_head %}
|
|
6
6
|
<style>
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
box-sizing: border-box;
|
|
26
26
|
}
|
|
27
27
|
#query-parameters-area textarea {
|
|
28
|
-
height:
|
|
28
|
+
height: 20em;
|
|
29
29
|
}
|
|
30
30
|
.submit-container {
|
|
31
31
|
padding-top: 1em;
|
|
@@ -36,28 +36,34 @@
|
|
|
36
36
|
</style>
|
|
37
37
|
{% endblock %}
|
|
38
38
|
|
|
39
|
+
{% block crumbs %}
|
|
40
|
+
{{ crumbs.nav(request=request, database=database_name) }}
|
|
41
|
+
{% endblock %}
|
|
42
|
+
|
|
39
43
|
{% block content %}
|
|
40
|
-
<h1>Write to
|
|
44
|
+
<h1>{% if custom_title %}{{ custom_title }}{% else %}Write to {{ database_name }} with SQL{% endif %}</h1>
|
|
41
45
|
|
|
42
|
-
<form class="write-form" action="{{
|
|
46
|
+
<form class="write-form core" action="{{ request.path }}" method="post" style="margin-bottom: 1em">
|
|
43
47
|
<input type="hidden" name="csrftoken" value="{{ csrftoken() }}">
|
|
44
|
-
<
|
|
45
|
-
<
|
|
46
|
-
{%
|
|
47
|
-
<p><textarea name="sql" style="box-sizing: border-box; width: 100%; padding-right: 10px; max-width: 600px; height: 10em; padding: 6px;">{{ sql_from_args }}</textarea></p>
|
|
48
|
+
{% if custom_title %}<details style="margin-bottom: 1em"><summary>SQL query</summary>{% endif %}
|
|
49
|
+
<p><textarea name="sql" style="box-sizing: border-box; width: 100%; padding-right: 10px; max-width: 600px; height: {{ sql_textarea_height }}em; padding: 6px;">{{ sql_from_args }}</textarea></p>
|
|
50
|
+
{% if custom_title %}</details>{% endif %}
|
|
48
51
|
<div class="query-parameters">
|
|
49
52
|
<div id="query-parameters-area">
|
|
50
53
|
{% for parameter in parameters %}
|
|
51
54
|
<label for="qp_{{ parameter.name }}">{{ parameter.label }}</label>
|
|
52
55
|
{% if parameter.type == "text" %}
|
|
53
|
-
<input type="text" id="qp_{{ parameter.name }}" name="qp_{{ parameter.name }}" value="">
|
|
56
|
+
<input type="text" id="qp_{{ parameter.name }}" name="qp_{{ parameter.name }}" value="{{ parameter.value }}">
|
|
57
|
+
{% elif parameter.type == "hidden" %}
|
|
58
|
+
<input type="text" disabled id="qp_{{ parameter.name }}" name="qp_{{ parameter.name }}" value="{{ parameter.value }}">
|
|
54
59
|
{% elif parameter.type == "textarea" %}
|
|
55
|
-
<textarea id="qp_{{ parameter.name }}" name="qp_{{ parameter.name }}"
|
|
60
|
+
<textarea id="qp_{{ parameter.name }}" name="qp_{{ parameter.name }}">{{ parameter.value }}</textarea>
|
|
56
61
|
{% endif %}
|
|
57
62
|
{% endfor %}
|
|
58
63
|
</div>
|
|
59
64
|
</div>
|
|
60
65
|
<div class="submit-container">
|
|
66
|
+
<input type="hidden" name="_redirect_to" value="{{ redirect_to }}">
|
|
61
67
|
<input type="submit" value="Execute query">
|
|
62
68
|
</div>
|
|
63
69
|
</form>
|
|
@@ -65,7 +71,7 @@
|
|
|
65
71
|
{% if tables %}
|
|
66
72
|
<p><strong>Tables</strong>:
|
|
67
73
|
{% for table in tables %}
|
|
68
|
-
<a href="{{ urls.table(
|
|
74
|
+
<a href="{{ urls.table(database_name, table) }}">{{ table }}</a>{% if not loop.last %}, {% endif %}
|
|
69
75
|
{% endfor %}
|
|
70
76
|
</p>
|
|
71
77
|
{% endif %}
|
|
@@ -73,7 +79,7 @@
|
|
|
73
79
|
{% if views %}
|
|
74
80
|
<p><strong>Views</strong>:
|
|
75
81
|
{% for view in views %}
|
|
76
|
-
<a href="{{ urls.table(
|
|
82
|
+
<a href="{{ urls.table(database_name, view) }}">{{ view }}</a>{% if not loop.last %}, {% endif %}
|
|
77
83
|
{% endfor %}
|
|
78
84
|
</p>
|
|
79
85
|
{% endif %}
|
|
@@ -81,16 +87,42 @@
|
|
|
81
87
|
<script>
|
|
82
88
|
function buildQueryParametersHtml(parameters) {
|
|
83
89
|
let htmlString = '';
|
|
90
|
+
var qsParams = new URLSearchParams(location.search);
|
|
84
91
|
parameters.forEach(param => {
|
|
92
|
+
const value = escapeHtml(qsParams.get(param.name) || '');
|
|
85
93
|
if (param.type === 'text') {
|
|
86
|
-
htmlString +=
|
|
94
|
+
htmlString += `
|
|
95
|
+
<label for="qp_${param.name}">${param.label}</label>
|
|
96
|
+
<input type="text" id="qp_${param.name}" name="qp_${param.name}" value="${value}">
|
|
97
|
+
`;
|
|
87
98
|
} else if (param.type === 'textarea') {
|
|
88
|
-
htmlString +=
|
|
99
|
+
htmlString += `
|
|
100
|
+
<label for="qp_${param.name}">${param.label}</label>
|
|
101
|
+
<textarea id="qp_${param.name}" name="qp_${param.name}">${value}</textarea>
|
|
102
|
+
`;
|
|
103
|
+
} else if (param.type === 'hidden') {
|
|
104
|
+
htmlString += `
|
|
105
|
+
<span></span>
|
|
106
|
+
<input type="hidden" id="qp_${param.name}" name="qp_${param.name}" value="${value}">
|
|
107
|
+
`;
|
|
89
108
|
}
|
|
90
109
|
});
|
|
91
110
|
return htmlString;
|
|
92
111
|
}
|
|
93
112
|
|
|
113
|
+
function escapeHtml(str) {
|
|
114
|
+
return str.replace(/[&<>"']/g, function(match) {
|
|
115
|
+
switch (match) {
|
|
116
|
+
case '&': return '&';
|
|
117
|
+
case '<': return '<';
|
|
118
|
+
case '>': return '>';
|
|
119
|
+
case '"': return '"';
|
|
120
|
+
case "'": return ''';
|
|
121
|
+
default: return match;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
94
126
|
const sqlTextArea = document.querySelector('textarea[name="sql"]');
|
|
95
127
|
const queryParametersArea = document.getElementById('query-parameters-area');
|
|
96
128
|
let lastRequestTime = 0;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: datasette-write
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4
|
|
4
4
|
Summary: Datasette plugin providing a UI for writing to a database
|
|
5
5
|
Home-page: https://github.com/simonw/datasette-write
|
|
6
6
|
Author: Simon Willison
|
|
@@ -14,6 +14,7 @@ Provides-Extra: test
|
|
|
14
14
|
Requires-Dist: pytest ; extra == 'test'
|
|
15
15
|
Requires-Dist: pytest-asyncio ; extra == 'test'
|
|
16
16
|
Requires-Dist: httpx ; extra == 'test'
|
|
17
|
+
Requires-Dist: beautifulsoup4 ; extra == 'test'
|
|
17
18
|
|
|
18
19
|
# datasette-write
|
|
19
20
|
|
|
@@ -32,13 +33,13 @@ pip install datasette-write
|
|
|
32
33
|
```
|
|
33
34
|
## Usage
|
|
34
35
|
|
|
35
|
-
Having installed the plugin, visit
|
|
36
|
+
Having installed the plugin, visit `/db/-/write` on your Datasette instance to submit SQL queries that will be executed against a write connection to the specified database.
|
|
36
37
|
|
|
37
38
|
By default only the `root` user 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.
|
|
38
39
|
|
|
39
40
|
The `datasette-write` 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.
|
|
40
41
|
|
|
41
|
-
Pass `?sql=...` in the query string to pre-populate the SQL editor with a query.
|
|
42
|
+
Pass `?sql=...` in the query string to pre-populate the SQL editor with a query.
|
|
42
43
|
|
|
43
44
|
## Parameterized queries
|
|
44
45
|
|
|
@@ -47,10 +48,16 @@ SQL queries can include parameters like this:
|
|
|
47
48
|
insert into news (title, body)
|
|
48
49
|
values (:title, :body_textarea)
|
|
49
50
|
```
|
|
50
|
-
These will be converted into form fields on the
|
|
51
|
+
These will be converted into form fields on the `/db/-/write` page.
|
|
51
52
|
|
|
52
53
|
If a parameter name ends with `_textarea` it will be rendered as a multi-line textarea instead of a text input.
|
|
53
54
|
|
|
55
|
+
If a parameter name ends with `_hidden` it will be rendered as a hidden input.
|
|
56
|
+
|
|
57
|
+
## Updating rows with SQL
|
|
58
|
+
|
|
59
|
+
On Datasette 1.0a13 and higher a row actions menu item will be added to the row page linking to a SQL query for updating that row, for users with the `datasette-write` permission.
|
|
60
|
+
|
|
54
61
|
## Development
|
|
55
62
|
|
|
56
63
|
To set up this plugin locally, first checkout the code. Then create a new virtual environment:
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
datasette_write/__init__.py,sha256=Tq0t3Qa_S-GFA_PdZIUb4q1b1pzcM0u3i-udMyvLDQQ,10190
|
|
2
|
+
datasette_write/templates/datasette_write.html,sha256=SMcaO3eSw7h2nbMYu55pEq8yvvKtg6eOfGlvH4OeLic,6159
|
|
3
|
+
datasette_write-0.4.dist-info/METADATA,sha256=RagwdrxaSKYT_zKW7mGBqoGgz4DzCu0LNhu-VUnWcyM,3105
|
|
4
|
+
datasette_write-0.4.dist-info/WHEEL,sha256=uCRv0ZEik_232NlR4YDw4Pv3Ajt5bKvMH13NUU7hFuI,91
|
|
5
|
+
datasette_write-0.4.dist-info/entry_points.txt,sha256=q6Nsr_AVBF5D0y_ojz62qBHP8NQmxcpTzaD5O8ndsJ0,36
|
|
6
|
+
datasette_write-0.4.dist-info/top_level.txt,sha256=TqV9H_tIZKzfVqbxQQWAxuu9NrbmOOdziZgsBYuYods,16
|
|
7
|
+
datasette_write-0.4.dist-info/RECORD,,
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
datasette_write/__init__.py,sha256=cXmzHZ0_3e_PYGofZo4cCRBwdnotkDxW5uAZxUSdlSg,6693
|
|
2
|
-
datasette_write/templates/datasette_write.html,sha256=-7zwcfznocsei5aIZnRcgEsdCib6-H7Fa1TuxasetDM,4911
|
|
3
|
-
datasette_write-0.3.2.dist-info/METADATA,sha256=wp34mljBEpOYbVLWVW7T11QI0JZqqWIxxAz1LVUOMzQ,2834
|
|
4
|
-
datasette_write-0.3.2.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
5
|
-
datasette_write-0.3.2.dist-info/entry_points.txt,sha256=q6Nsr_AVBF5D0y_ojz62qBHP8NQmxcpTzaD5O8ndsJ0,36
|
|
6
|
-
datasette_write-0.3.2.dist-info/top_level.txt,sha256=TqV9H_tIZKzfVqbxQQWAxuu9NrbmOOdziZgsBYuYods,16
|
|
7
|
-
datasette_write-0.3.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|