html_url,issue_url,id,node_id,user,created_at,updated_at,author_association,body,reactions,issue,performed_via_github_app
https://github.com/simonw/datasette/pull/2118#issuecomment-1652681136,https://api.github.com/repos/simonw/datasette/issues/2118,1652681136,IC_kwDOBm6k_c5igemw,9599,2023-07-26T23:30:44Z,2023-07-26T23:30:44Z,OWNER,"The `_shape=` stuff should use `json_renderer` instead - that's how the table view did it:
https://github.com/simonw/datasette/commit/d97e82df3c8a3f2e97038d7080167be9bb74a68d#diff-5c9ef29c33ed0fde413565b23fa258d60fc3a2bb205b016db9e915c9bd5ecfb3
https://github.com/simonw/datasette/blob/c3e3ecf590ca5fa61b00aba4c78599e33d370b60/datasette/views/table.py#L822-L850
Instead of:
https://github.com/simonw/datasette/blob/c3e3ecf590ca5fa61b00aba4c78599e33d370b60/datasette/views/database.py#L239-L288","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1823352380,
https://github.com/simonw/sqlite-utils/issues/581#issuecomment-1652496702,https://api.github.com/repos/simonw/sqlite-utils/issues/581,1652496702,IC_kwDOCGYnMM5ifxk-,9599,2023-07-26T21:07:45Z,2023-07-26T21:07:45Z,OWNER,Docs: https://sqlite-utils.datasette.io/en/latest/cli.html#using-the-debugger,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1823160748,
https://github.com/simonw/datasette/issues/2111#issuecomment-1652407208,https://api.github.com/repos/simonw/datasette/issues/2111,1652407208,IC_kwDOBm6k_c5ifbuo,9599,2023-07-26T19:54:20Z,2023-07-26T19:54:20Z,OWNER,"I implemented `/content` and `/content.json` but I left `/content?sql=...` as this:
https://github.com/simonw/datasette/blob/2e40a506ad45b44fd7642474f630a31ef18b5911/datasette/views/database.py#L220-L221","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1822937426,
https://github.com/simonw/datasette/issues/2111#issuecomment-1652354606,https://api.github.com/repos/simonw/datasette/issues/2111,1652354606,IC_kwDOBm6k_c5ifO4u,9599,2023-07-26T19:16:35Z,2023-07-26T19:17:45Z,OWNER,"I just noticed that this URL: https://latest.datasette.io/fixtures.csv
Returns a 500 error right now!
It's fine with a `?sql=` query: https://latest.datasette.io/fixtures.csv?sql=select+*+from+facetable","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1822937426,
https://github.com/simonw/datasette/issues/2110#issuecomment-1652330111,https://api.github.com/repos/simonw/datasette/issues/2110,1652330111,IC_kwDOBm6k_c5ifI5_,9599,2023-07-26T18:55:31Z,2023-07-26T18:55:31Z,OWNER,Changed my mind on this - I'm going to have the `query_view` mapped to `/db` but have the first code on there notice if `?sql=` is missing and return a `database_view()` function instead.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1822936521,
https://github.com/simonw/datasette/issues/2109#issuecomment-1652325193,https://api.github.com/repos/simonw/datasette/issues/2109,1652325193,IC_kwDOBm6k_c5ifHtJ,9599,2023-07-26T18:51:15Z,2023-07-26T18:51:15Z,OWNER,"Here's the code I'm going to be entirely replacing:
https://github.com/simonw/datasette/blob/18dd88ee4d78fe9d760e9da96028ae06d938a85c/datasette/views/database.py#L213-L530
Plus this weird class in `views/table.py`:
https://github.com/simonw/datasette/blob/18dd88ee4d78fe9d760e9da96028ae06d938a85c/datasette/views/table.py#L701-L749","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1822934563,
https://github.com/simonw/datasette/issues/2114#issuecomment-1652321419,https://api.github.com/repos/simonw/datasette/issues/2114,1652321419,IC_kwDOBm6k_c5ifGyL,9599,2023-07-26T18:48:03Z,2023-07-26T18:48:03Z,OWNER,"This is also where I'll bring back writable canned queries:
https://github.com/simonw/datasette/blob/18dd88ee4d78fe9d760e9da96028ae06d938a85c/datasette/views/database.py#L281-L334","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1822940263,
https://github.com/simonw/datasette/issues/2111#issuecomment-1652318269,https://api.github.com/repos/simonw/datasette/issues/2111,1652318269,IC_kwDOBm6k_c5ifGA9,9599,2023-07-26T18:45:23Z,2023-07-26T18:45:23Z,OWNER,"To avoid confusion I'll start by having `/content` return a HTML ""TODO: implement this"" message.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1822937426,
https://github.com/simonw/datasette/issues/2116#issuecomment-1652315926,https://api.github.com/repos/simonw/datasette/issues/2116,1652315926,IC_kwDOBm6k_c5ifFcW,9599,2023-07-26T18:43:17Z,2023-07-26T18:43:17Z,OWNER,"Tests pass, and manually tested like this too:
```bash
datasette -i pelicans.db
```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1822949756,
https://github.com/simonw/datasette/issues/2116#issuecomment-1652304289,https://api.github.com/repos/simonw/datasette/issues/2116,1652304289,IC_kwDOBm6k_c5ifCmh,9599,2023-07-26T18:33:07Z,2023-07-26T18:33:07Z,OWNER,"This runs three tests:
```bash
pytest -k test_database_download
```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1822949756,
https://github.com/simonw/datasette/issues/2116#issuecomment-1652303471,https://api.github.com/repos/simonw/datasette/issues/2116,1652303471,IC_kwDOBm6k_c5ifCZv,9599,2023-07-26T18:32:24Z,2023-07-26T18:32:24Z,OWNER,https://github.com/simonw/datasette/blob/dc5171eb1b1d9f1d55e367f8a4d93edb55a43351/datasette/views/database.py#L172-L212,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1822949756,
https://github.com/simonw/datasette/issues/2106#issuecomment-1652298879,https://api.github.com/repos/simonw/datasette/issues/2106,1652298879,IC_kwDOBm6k_c5ifBR_,9599,2023-07-26T18:28:33Z,2023-07-26T18:28:33Z,OWNER,"Applied the same fix as here:
- https://github.com/simonw/llm/issues/136","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1816857442,
https://github.com/simonw/datasette/pull/2053#issuecomment-1652296467,https://api.github.com/repos/simonw/datasette/issues/2053,1652296467,IC_kwDOBm6k_c5ifAsT,9599,2023-07-26T18:26:44Z,2023-07-26T18:26:44Z,OWNER,"I'm abandoning this branch in favour of a fresh attempt, described here:
- https://github.com/simonw/datasette/issues/2109
I'll copy bits and pieces of this branch across as-needed.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1656432059,
https://github.com/simonw/datasette/issues/2109#issuecomment-1652295866,https://api.github.com/repos/simonw/datasette/issues/2109,1652295866,IC_kwDOBm6k_c5ifAi6,9599,2023-07-26T18:26:18Z,2023-07-26T18:26:18Z,OWNER,"I'm going to do this work in a fresh branch, copying bits over from here as needed:
- https://github.com/simonw/datasette/pull/2053","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1822934563,
https://github.com/simonw/datasette/issues/2109#issuecomment-1652294920,https://api.github.com/repos/simonw/datasette/issues/2109,1652294920,IC_kwDOBm6k_c5ifAUI,9599,2023-07-26T18:25:34Z,2023-07-26T18:25:34Z,OWNER,"OK, these issues will do for the plan.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1822934563,
https://github.com/simonw/datasette/pull/2053#issuecomment-1651904060,https://api.github.com/repos/simonw/datasette/issues/2053,1651904060,IC_kwDOBm6k_c5idg48,9599,2023-07-26T14:19:00Z,2023-07-26T15:25:15Z,OWNER,"Worth noting that the `register_output_renderer()` is actually pretty easy to extend, because it returns a dictionary which could have more keys (like the required set of extras) added to it:
```python
@hookimpl
def register_output_renderer(datasette):
return {
""extension"": ""test"",
""render"": render_demo,
""can_render"": can_render_demo, # Optional
}
```
https://docs.datasette.io/en/0.64.3/plugin_hooks.html#register-output-renderer-datasette
One slight hiccup with that plugin hook is this:
> rows - list of `sqlite3.Row` objects
I could turn that into a Datasette defined object that behaves like a [sqlite3.Row](https://docs.python.org/3/library/sqlite3.html#sqlite3.Row) though, which would give me extra flexibility in the future.
A bit tricky though since it's implemented in C for performance: https://github.com/python/cpython/blob/b0202a4e5d6b629ba5acbc703e950f08ebaf07df/Modules/_sqlite/row.c
Pasted that into Claude for the following explanation:
> - pysqlite_Row is the structure defining the Row object. It contains the tuple of data (self->data) and description of columns (self->description).
> - pysqlite_row_new() is the constructor which creates a new Row object given a cursor and tuple of data.
> - pysqlite_row_dealloc() frees the memory when Row object is deleted.
> - pysqlite_row_keys() returns the column names of the row.
> - pysqlite_row_length() and pysqlite_row_subscript() implement sequence like behavior to access row elements by index.
> - pysqlite_row_subscript() also allows accessing by column name by doing a lookup in description.
> - pysqlite_row_hash() and pysqlite_row_richcompare() implement equality checks and hash function.
I could use protocols in Python to make my own `DatasetteRow` which can be used interchangeably with `sqlite3.Row` - https://docs.python.org/3/library/typing.html#typing.Protocol
Turned this into a TIL: https://til.simonwillison.net/python/protocols","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1656432059,
https://github.com/simonw/datasette/pull/2053#issuecomment-1651894668,https://api.github.com/repos/simonw/datasette/issues/2053,1651894668,IC_kwDOBm6k_c5idemM,9599,2023-07-26T14:14:34Z,2023-07-26T14:14:34Z,OWNER,"Another point of confusion is how `/content` sometimes serves the database index page (with a list of tables) and sometimes solves the results of a query.
I could resolve this by turning the information on the index page into extras, which can optionally be requested any time a query is run but default to being shown if there is no query.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1656432059,
https://github.com/simonw/datasette/pull/2053#issuecomment-1651883505,https://api.github.com/repos/simonw/datasette/issues/2053,1651883505,IC_kwDOBm6k_c5idb3x,9599,2023-07-26T14:08:20Z,2023-07-26T14:08:20Z,OWNER,"I think the hardest part of getting this working is dealing with the different formats.
Idea: refactor `.html` as a format (since it's by far the most complex) and tweak the plugin hook a bit as part of that, then use what I learn from that to get the other formats working.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1656432059,
https://github.com/simonw/datasette/pull/2053#issuecomment-1651874649,https://api.github.com/repos/simonw/datasette/issues/2053,1651874649,IC_kwDOBm6k_c5idZtZ,9599,2023-07-26T14:03:37Z,2023-07-26T14:03:37Z,OWNER,"Big chunk of commented-out code I just removed:
```python
import pdb
pdb.set_trace()
if isinstance(output, dict) and output.get(""ok"") is False:
# TODO: Other error codes?
response.status_code = 400
if datasette.cors:
add_cors_headers(response.headers)
return response
# registry = Registry(
# extra_count,
# extra_facet_results,
# extra_facets_timed_out,
# extra_suggested_facets,
# facet_instances,
# extra_human_description_en,
# extra_next_url,
# extra_columns,
# extra_primary_keys,
# run_display_columns_and_rows,
# extra_display_columns,
# extra_display_rows,
# extra_debug,
# extra_request,
# extra_query,
# extra_metadata,
# extra_extras,
# extra_database,
# extra_table,
# extra_database_color,
# extra_table_actions,
# extra_filters,
# extra_renderers,
# extra_custom_table_templates,
# extra_sorted_facet_results,
# extra_table_definition,
# extra_view_definition,
# extra_is_view,
# extra_private,
# extra_expandable_columns,
# extra_form_hidden_args,
# )
results = await registry.resolve_multi(
[""extra_{}"".format(extra) for extra in extras]
)
data = {
""ok"": True,
""next"": next_value and str(next_value) or None,
}
data.update(
{
key.replace(""extra_"", """"): value
for key, value in results.items()
if key.startswith(""extra_"") and key.replace(""extra_"", """") in extras
}
)
raw_sqlite_rows = rows[:page_size]
data[""rows""] = [dict(r) for r in raw_sqlite_rows]
private = False
if canned_query:
# Respect canned query permissions
visible, private = await datasette.check_visibility(
request.actor,
permissions=[
(""view-query"", (database, canned_query)),
(""view-database"", database),
""view-instance"",
],
)
if not visible:
raise Forbidden(""You do not have permission to view this query"")
else:
await datasette.ensure_permissions(request.actor, [(""execute-sql"", database)])
# If there's no sql, show the database index page
if not sql:
return await database_index_view(request, datasette, db)
validate_sql_select(sql)
# Extract any :named parameters
named_parameters = named_parameters or await derive_named_parameters(db, sql)
named_parameter_values = {
named_parameter: params.get(named_parameter) or """"
for named_parameter in named_parameters
if not named_parameter.startswith(""_"")
}
# Set to blank string if missing from params
for named_parameter in named_parameters:
if named_parameter not in params and not named_parameter.startswith(""_""):
params[named_parameter] = """"
extra_args = {}
if params.get(""_timelimit""):
extra_args[""custom_time_limit""] = int(params[""_timelimit""])
if _size:
extra_args[""page_size""] = _size
templates = [f""query-{to_css_class(database)}.html"", ""query.html""]
if canned_query:
templates.insert(
0,
f""query-{to_css_class(database)}-{to_css_class(canned_query)}.html"",
)
query_error = None
# Execute query - as write or as read
if write:
raise NotImplementedError(""Write queries not yet implemented"")
# if request.method == ""POST"":
# # If database is immutable, return an error
# if not db.is_mutable:
# raise Forbidden(""Database is immutable"")
# body = await request.post_body()
# body = body.decode(""utf-8"").strip()
# if body.startswith(""{"") and body.endswith(""}""):
# params = json.loads(body)
# # But we want key=value strings
# for key, value in params.items():
# params[key] = str(value)
# else:
# params = dict(parse_qsl(body, keep_blank_values=True))
# # Should we return JSON?
# should_return_json = (
# request.headers.get(""accept"") == ""application/json""
# or request.args.get(""_json"")
# or params.get(""_json"")
# )
# if canned_query:
# params_for_query = MagicParameters(params, request, self.ds)
# else:
# params_for_query = params
# ok = None
# try:
# cursor = await self.ds.databases[database].execute_write(
# sql, params_for_query
# )
# message = metadata.get(
# ""on_success_message""
# ) or ""Query executed, {} row{} affected"".format(
# cursor.rowcount, """" if cursor.rowcount == 1 else ""s""
# )
# message_type = self.ds.INFO
# redirect_url = metadata.get(""on_success_redirect"")
# ok = True
# except Exception as e:
# message = metadata.get(""on_error_message"") or str(e)
# message_type = self.ds.ERROR
# redirect_url = metadata.get(""on_error_redirect"")
# ok = False
# if should_return_json:
# return Response.json(
# {
# ""ok"": ok,
# ""message"": message,
# ""redirect"": redirect_url,
# }
# )
# else:
# self.ds.add_message(request, message, message_type)
# return self.redirect(request, redirect_url or request.path)
# else:
# async def extra_template():
# return {
# ""request"": request,
# ""db_is_immutable"": not db.is_mutable,
# ""path_with_added_args"": path_with_added_args,
# ""path_with_removed_args"": path_with_removed_args,
# ""named_parameter_values"": named_parameter_values,
# ""canned_query"": canned_query,
# ""success_message"": request.args.get(""_success"") or """",
# ""canned_write"": True,
# }
# return (
# {
# ""database"": database,
# ""rows"": [],
# ""truncated"": False,
# ""columns"": [],
# ""query"": {""sql"": sql, ""params"": params},
# ""private"": private,
# },
# extra_template,
# templates,
# )
# Not a write
rows = []
if canned_query:
params_for_query = MagicParameters(params, request, datasette)
else:
params_for_query = params
try:
results = await datasette.execute(
database, sql, params_for_query, truncate=True, **extra_args
)
columns = [r[0] for r in results.description]
rows = list(results.rows)
except sqlite3.DatabaseError as e:
query_error = e
results = None
columns = []
allow_execute_sql = await datasette.permission_allowed(
request.actor, ""execute-sql"", database
)
format_ = request.url_vars.get(""format"") or ""html""
if format_ == ""csv"":
raise NotImplementedError(""CSV format not yet implemented"")
elif format_ in datasette.renderers.keys():
# Dispatch request to the correct output format renderer
# (CSV is not handled here due to streaming)
result = call_with_supported_arguments(
datasette.renderers[format_][0],
datasette=datasette,
columns=columns,
rows=rows,
sql=sql,
query_name=None,
database=db.name,
table=None,
request=request,
view_name=""table"", # TODO: should this be ""query""?
# These will be deprecated in Datasette 1.0:
args=request.args,
data={
""rows"": rows,
}, # TODO what should this be?
)
result = await await_me_maybe(result)
if result is None:
raise NotFound(""No data"")
if isinstance(result, dict):
r = Response(
body=result.get(""body""),
status=result.get(""status_code"") or 200,
content_type=result.get(""content_type"", ""text/plain""),
headers=result.get(""headers""),
)
elif isinstance(result, Response):
r = result
# if status_code is not None:
# # Over-ride the status code
# r.status = status_code
else:
assert False, f""{result} should be dict or Response""
elif format_ == ""html"":
headers = {}
templates = [f""query-{to_css_class(database)}.html"", ""query.html""]
template = datasette.jinja_env.select_template(templates)
alternate_url_json = datasette.absolute_url(
request,
datasette.urls.path(path_with_format(request=request, format=""json"")),
)
headers.update(
{
""Link"": '{}; rel=""alternate""; type=""application/json+datasette""'.format(
alternate_url_json
)
}
)
r = Response.html(
await datasette.render_template(
template,
dict(
data,
append_querystring=append_querystring,
path_with_replaced_args=path_with_replaced_args,
fix_path=datasette.urls.path,
settings=datasette.settings_dict(),
# TODO: review up all of these hacks:
alternate_url_json=alternate_url_json,
datasette_allow_facet=(
""true"" if datasette.setting(""allow_facet"") else ""false""
),
is_sortable=any(c[""sortable""] for c in data[""display_columns""]),
allow_execute_sql=await datasette.permission_allowed(
request.actor, ""execute-sql"", resolved.db.name
),
query_ms=1.2,
select_templates=[
f""{'*' if template_name == template.name else ''}{template_name}""
for template_name in templates
],
),
request=request,
view_name=""table"",
),
headers=headers,
)
else:
assert False, ""Invalid format: {}"".format(format_)
# if next_url:
# r.headers[""link""] = f'<{next_url}>; rel=""next""'
return r
```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1656432059,