html_url,issue_url,id,node_id,user,user_label,created_at,updated_at,author_association,body,reactions,issue,issue_label,performed_via_github_app https://github.com/simonw/datasette/issues/2104#issuecomment-1641082395,https://api.github.com/repos/simonw/datasette/issues/2104,1641082395,IC_kwDOBm6k_c5h0O4b,15178711,asg017,2023-07-18T22:41:37Z,2023-07-18T22:41:37Z,CONTRIBUTOR,"For filtering virtual table's ""shadow tables"" (ex the FTS5 _content and most the spatialite tables), you can use `pragma_table_list` (first appeared in SQLite 3.37 (2021-11-27), which has a `type` column that calls out `type=""shadow""` tables https://www.sqlite.org/pragma.html#pragma_table_list","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1808215339,Tables starting with an underscore should be treated as hidden, https://github.com/simonw/datasette/issues/2104#issuecomment-1638552567,https://api.github.com/repos/simonw/datasette/issues/2104,1638552567,IC_kwDOBm6k_c5hqlP3,9599,simonw,2023-07-17T17:14:20Z,2023-07-17T17:14:20Z,OWNER,Relevant code: https://github.com/simonw/datasette/blob/0f7192b6154edb576c41b55bd3f2a3f53e5f436a/datasette/database.py#L391-L451,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1808215339,Tables starting with an underscore should be treated as hidden, https://github.com/simonw/datasette/issues/2102#issuecomment-1640064620,https://api.github.com/repos/simonw/datasette/issues/2102,1640064620,IC_kwDOBm6k_c5hwWZs,9599,simonw,2023-07-18T11:47:21Z,2023-07-18T11:47:21Z,OWNER,"I think I've figured out the problem here. The question being asked is ""can this actor access this resource, which is within this database within this instance"". The answer to this question needs to consider the full set of questions at once - yes they can access within this instance IF they have access to the specified table and that's the table being asked about. But the questions are currently being asked independently, which means the plugin hook acting on `view-instance` can't see that the answer here should be yes because it's actually about a table that the actor has explicit permission to view. So I think I may need to redesign the plugin hook to always see the full hierarchy of checks, not just a single check at a time. ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1805076818,API tokens with view-table but not view-database/view-instance cannot access the table, https://github.com/simonw/datasette/issues/2102#issuecomment-1638567228,https://api.github.com/repos/simonw/datasette/issues/2102,1638567228,IC_kwDOBm6k_c5hqo08,9599,simonw,2023-07-17T17:24:19Z,2023-07-17T17:25:12Z,OWNER,"Confirmed that this is an issue with regular Datasette signed tokens as well. I created one on https://latest.datasette.io/-/create-token with these details: ```json { ""_r"": { ""r"": { ""fixtures"": { ""sortable"": [ ""vt"" ] } } }, ""a"": ""root"", ""d"": 3600, ""t"": 1689614483 } ``` Run like this: ``` curl -H 'Authorization: Bearer dstok_eyJhIjoicm9vdCIsInQiOjE2ODk2MTQ0ODMsImQiOjM2MDAsIl9yIjp7InIiOnsiZml4dHVyZXMiOnsic29ydGFibGUiOlsidnQiXX19fX0.n-VGxxawz1Q0WK7sqLfhXUgcvY0' \ https://latest.datasette.io/fixtures/sortable.json ``` Returned an HTML Forbidden page: ```html Forbidden ... ``` Same token againts `/-/actor.json` returns: ```json { ""actor"": { ""id"": ""root"", ""token"": ""dstok"", ""_r"": { ""r"": { ""fixtures"": { ""sortable"": [ ""vt"" ] } } }, ""token_expires"": 1689618083 } } ``` Reminder - `""_r""` means restrict, `""r""` means resource.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1805076818,API tokens with view-table but not view-database/view-instance cannot access the table, https://github.com/simonw/datasette/issues/2102#issuecomment-1636093730,https://api.github.com/repos/simonw/datasette/issues/2102,1636093730,IC_kwDOBm6k_c5hhM8i,9599,simonw,2023-07-14T16:26:27Z,2023-07-14T16:32:49Z,OWNER,"Here's that crucial comment: > If _r is defined then we use those to further restrict the actor. > >Crucially, we only use this to say NO (return False) - we never use it to return YES (True) because that might over-ride other restrictions placed on this actor So that's why I implemented it like this. The goal here is to be able to issue a token which can't do anything _more_ than the actor it is associated with, but CAN be configured to do less. So I think the solution here is for the `_r` checking code to perhaps implement its own view cascade logic - it notices if you have `view-table` and consequently fails to block `view-table` and `view-instance`. I'm not sure that's going to work though - would that mean that granting `view-table` grants `view-database` in a surprising and harmful way? Maybe that's OK: if you have `view-database` but permission checks fail for individual tables and queries you shouldn't be able to see a thing that you shouldn't. Need to verify that though. Also, do `Permission` instances have enough information to implement this kind of cascade without hard-coding anything? ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1805076818,API tokens with view-table but not view-database/view-instance cannot access the table, https://github.com/simonw/datasette/issues/2102#issuecomment-1636053060,https://api.github.com/repos/simonw/datasette/issues/2102,1636053060,IC_kwDOBm6k_c5hhDBE,9599,simonw,2023-07-14T15:51:36Z,2023-07-14T16:14:05Z,OWNER,"This might only be an issue with the code that checks `_r` on actors. https://github.com/simonw/datasette/blob/0f7192b6154edb576c41b55bd3f2a3f53e5f436a/datasette/default_permissions.py#L185-L222 Added in https://github.com/simonw/datasette/commit/bcc781f4c50a8870e3389c4e60acb625c34b0317 - refs: - #1855 ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1805076818,API tokens with view-table but not view-database/view-instance cannot access the table, https://github.com/simonw/datasette/issues/2102#issuecomment-1636042066,https://api.github.com/repos/simonw/datasette/issues/2102,1636042066,IC_kwDOBm6k_c5hhAVS,9599,simonw,2023-07-14T15:41:54Z,2023-07-14T15:42:32Z,OWNER,"I tried some code spelunking and came across https://github.com/simonw/datasette/commit/d6e03b04302a0852e7133dc030eab50177c37be7 which says: > - If you have table permission but not database permission you can now view the table page Refs: - #832 Which suggests that my initial design decision wasn't what appears to be implemented today. Needs more investigation.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1805076818,API tokens with view-table but not view-database/view-instance cannot access the table, https://github.com/simonw/datasette/issues/2102#issuecomment-1636040164,https://api.github.com/repos/simonw/datasette/issues/2102,1636040164,IC_kwDOBm6k_c5hg_3k,9599,simonw,2023-07-14T15:40:21Z,2023-07-14T15:40:21Z,OWNER,"Relevant code: https://github.com/simonw/datasette/blob/0f7192b6154edb576c41b55bd3f2a3f53e5f436a/datasette/app.py#L822-L855","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1805076818,API tokens with view-table but not view-database/view-instance cannot access the table, https://github.com/simonw/datasette/issues/2102#issuecomment-1636036312,https://api.github.com/repos/simonw/datasette/issues/2102,1636036312,IC_kwDOBm6k_c5hg-7Y,9599,simonw,2023-07-14T15:37:14Z,2023-07-14T15:37:14Z,OWNER,"I think I made this decision because I was thinking about default deny: obviously if a user has been denied access to a database. It doesn't make sense that they could access tables within it. But now that I am spending more time with authentication tokens, which default to denying everything, except for the things that you have explicitly listed, this policy, no longer makes as much sense. ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1805076818,API tokens with view-table but not view-database/view-instance cannot access the table, https://github.com/simonw/datasette/issues/2101#issuecomment-1634443907,https://api.github.com/repos/simonw/datasette/issues/2101,1634443907,IC_kwDOBm6k_c5ha6KD,9599,simonw,2023-07-13T15:24:17Z,2023-07-13T15:24:17Z,OWNER,https://github.com/simonw/datasette/blob/0f7192b6154edb576c41b55bd3f2a3f53e5f436a/datasette/views/table.py#L486-L506,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1803264272,alter: true support for JSON write API, https://github.com/simonw/sqlite-utils/issues/567#issuecomment-1638926655,https://api.github.com/repos/simonw/sqlite-utils/issues/567,1638926655,IC_kwDOCGYnMM5hsAk_,9599,simonw,2023-07-17T21:42:37Z,2023-07-17T21:42:37Z,OWNER,"I really like this. I'm also interested in: - Plugins that make new custom SQL functions available - similar to this Datasette hook: https://docs.datasette.io/en/stable/plugin_hooks.html#prepare-connection-conn-database-datasette - Plugins that register functions that can be used as recipes for `sqlite-utils convert` https://sqlite-utils.datasette.io/en/stable/cli.html#sqlite-utils-convert-recipes The upload-data-to-Datasette problem is planned to be solved by a future version of https://github.com/simonw/dclient ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1801394744,Plugin system, https://github.com/simonw/sqlite-utils/issues/567#issuecomment-1638910473,https://api.github.com/repos/simonw/sqlite-utils/issues/567,1638910473,IC_kwDOCGYnMM5hr8oJ,15178711,asg017,2023-07-17T21:27:41Z,2023-07-17T21:27:41Z,NONE,"Another use-case: I want to make a `sqlite-utils` plugin that'll help me insert data into Datasette. ```bash sqlite-utils insert-datasette \ --token $DATASETTE_API_KEY \ https://latest.datasette.io/fixtures/my-table \ 'select ...' ``` This could also be a datasette plugin (ex `datasette upload-data ...`, but you can also think of `sqlite-utils` plugins that upload to S3, a postgres table, other DBMS's, etc.)","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1801394744,Plugin system, https://github.com/simonw/sqlite-utils/issues/566#issuecomment-1627598570,https://api.github.com/repos/simonw/sqlite-utils/issues/566,1627598570,IC_kwDOCGYnMM5hAy7q,9599,simonw,2023-07-09T04:13:34Z,2023-07-09T04:13:34Z,OWNER,On consulting https://pypi.org/project/tabulate/ it looks like most of those formats don't actually makes sense without headers - so the right thing here might be to raise an error if `--fmt` and `--no-headers` are used at the same time.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1795219865,`--no-headers` doesn't work on most formats, https://github.com/simonw/sqlite-utils/issues/566#issuecomment-1627597872,https://api.github.com/repos/simonw/sqlite-utils/issues/566,1627597872,IC_kwDOCGYnMM5hAyww,9599,simonw,2023-07-09T04:09:56Z,2023-07-09T04:09:56Z,OWNER,"Thanks, looks like a bug.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1795219865,`--no-headers` doesn't work on most formats, https://github.com/dogsheep/pocket-to-sqlite/issues/12#issuecomment-1627564127,https://api.github.com/repos/dogsheep/pocket-to-sqlite/issues/12,1627564127,IC_kwDODLZ_YM5hAqhf,9599,simonw,2023-07-09T01:19:42Z,2023-07-09T01:19:42Z,MEMBER,https://github.com/dogsheep/pocket-to-sqlite/tree/0.2.3 and https://pypi.org/project/pocket-to-sqlite/0.2.3/,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1795187493,Switch to pyproject.toml, https://github.com/dogsheep/pocket-to-sqlite/issues/12#issuecomment-1627563202,https://api.github.com/repos/dogsheep/pocket-to-sqlite/issues/12,1627563202,IC_kwDODLZ_YM5hAqTC,9599,simonw,2023-07-09T01:14:27Z,2023-07-09T01:14:27Z,MEMBER,I tested this locally with `python -m build` and then `pip install ...whl` in a fresh virtual environment.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1795187493,Switch to pyproject.toml, https://github.com/simonw/sqlite-utils/issues/565#issuecomment-1618380888,https://api.github.com/repos/simonw/sqlite-utils/issues/565,1618380888,IC_kwDOCGYnMM5gdohY,9599,simonw,2023-07-03T14:09:11Z,2023-07-03T14:09:31Z,OWNER,"For the CLI: ```bash sqlite-utils rename-table data.db old_table_name new_table_name ``` For the Python code, should it go on Table or on Database? ```python db[""foo""].rename_table(""bar"") db.rename_table(""foo"", ""bar"") ``` I think I like the second better, it's slightly more clear. Also need a design for an option for the `.transform()` method to indicate that the new table should be created with a new name without dropping the old one.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1786258502,Table renaming utilities, https://github.com/simonw/sqlite-utils/issues/563#issuecomment-1617395444,https://api.github.com/repos/simonw/sqlite-utils/issues/563,1617395444,IC_kwDOCGYnMM5gZ370,9599,simonw,2023-07-03T05:44:43Z,2023-07-03T05:44:43Z,OWNER,Documentation at the bottom of this section: https://sqlite-utils.datasette.io/en/latest/cli.html#inserting-csv-or-tsv-data,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1785360409,`--empty-null` option when importing CSV, https://github.com/simonw/sqlite-utils/issues/562#issuecomment-1616782404,https://api.github.com/repos/simonw/sqlite-utils/issues/562,1616782404,IC_kwDOCGYnMM5gXiRE,9599,simonw,2023-07-02T19:24:14Z,2023-07-02T19:26:39Z,OWNER,"[Dataclasses](https://docs.python.org/3/library/dataclasses.html) were added in Python 3.7 and `sqlite-utils` was originally written for Python 3.6 - but both 3.6 and 3.7 are EOL now. The thing that makes Dataclasses particularly interesting is the potential to use type annotations with them to help specify the types of the related SQLite columns. Example for https://datasette.io/content/users ```sql CREATE TABLE [users] ( [login] TEXT, [id] INTEGER PRIMARY KEY, [node_id] TEXT, [avatar_url] TEXT, [gravatar_id] TEXT, [html_url] TEXT, [type] TEXT, [site_admin] INTEGER, [name] TEXT ); ``` And the dataclass: ```python from dataclasses import dataclass @dataclass class User: id: int login: str node_id: str avatar_url: str gravatar_id: str html_url: str type: str site_admin: int name: str ``` ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1784794489,Explore the intersection between sqlite-utils and dataclasses, https://github.com/simonw/datasette/issues/2093#issuecomment-1616286848,https://api.github.com/repos/simonw/datasette/issues/2093,1616286848,IC_kwDOBm6k_c5gVpSA,15178711,asg017,2023-07-02T02:17:46Z,2023-07-02T02:17:46Z,CONTRIBUTOR,"Storing metadata in the database won't be required. I imagine there'll be many different ways to store metadata, including any possible `datasette_metadata` or sqlite-docs, or the older metadata.json way. The next question will be how precedence should work - i'd imagine metadata.json > plugins > datasette_metadata > sqlite-docs","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1781530343,"Proposal: Combine settings, metadata, static, etc. into a single `datasette.toml` File", https://github.com/simonw/datasette/issues/2093#issuecomment-1616195496,https://api.github.com/repos/simonw/datasette/issues/2093,1616195496,IC_kwDOBm6k_c5gVS-o,273509,terinjokes,2023-07-02T00:06:54Z,2023-07-02T00:07:17Z,NONE,"I'm not keen on requiring metadata to be within the database. I commonly have multiple DBs, from various sources, and having one config file to provide the metadata works out very well. I use Datasette with databases where I'm not the original source, needing to mutate them to add a metadata table or sqlite-docs makes me uncomfortable.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1781530343,"Proposal: Combine settings, metadata, static, etc. into a single `datasette.toml` File", https://github.com/simonw/datasette/issues/2093#issuecomment-1614652001,https://api.github.com/repos/simonw/datasette/issues/2093,1614652001,IC_kwDOBm6k_c5gPaJh,9599,simonw,2023-06-30T13:27:13Z,2023-06-30T13:27:13Z,OWNER,"I agree, settings in the DB doesn't make sense but metadata does. On the JSON v YAML v TOML issue I just spotted Caddy has a concept of config adapters which they use to resolve exactly that problem: https://caddyserver.com/docs/config-adapters","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1781530343,"Proposal: Combine settings, metadata, static, etc. into a single `datasette.toml` File", https://github.com/simonw/datasette/issues/2093#issuecomment-1613896210,https://api.github.com/repos/simonw/datasette/issues/2093,1613896210,IC_kwDOBm6k_c5gMhoS,15178711,asg017,2023-06-29T22:53:33Z,2023-06-29T22:53:33Z,CONTRIBUTOR,"Maybe we can have a separate issue for revamping `metadata.json`? A `datasette_metadata` table or the `sqlite-docs` extension seem like two reasonable additions that we can work through. Storing metadata inside a SQLite database makes sense, but I don't think storing `datasette.*` style config (ex ports, settings, etc.) inside a SQLite DB makes sense, since it's very environment-dependent","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1781530343,"Proposal: Combine settings, metadata, static, etc. into a single `datasette.toml` File", https://github.com/simonw/datasette/issues/2093#issuecomment-1613895188,https://api.github.com/repos/simonw/datasette/issues/2093,1613895188,IC_kwDOBm6k_c5gMhYU,15178711,asg017,2023-06-29T22:51:53Z,2023-06-29T22:51:53Z,CONTRIBUTOR,"I agree with not liking `metadata.json` stuff in a `datasette.*` config file. Editing description of a table/column in a file like `datasette.*` seems odd to me. Though since plugin configuration currently lives in `metadata.json`, I think it should be removed from there and placed in `datasette.*`, at least for top-level config like `datasette-auth-github`'s config. Keeping `metadata.json` strictly for documentation/licensing/column units makes sense to me, but anything plugin related should be in some config file, like `datasette.*`. And ya, supporting both `datasette.*` and CLI flags makes a lot of sense to me. Any `--setting` flag should override anything in `datasette.*` for easier debugging, with possibly a warning message so people don't get confused. Same with `--port` and a port defined in `datasette.*`","{""total_count"": 1, ""+1"": 1, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1781530343,"Proposal: Combine settings, metadata, static, etc. into a single `datasette.toml` File", https://github.com/simonw/datasette/issues/2093#issuecomment-1613889979,https://api.github.com/repos/simonw/datasette/issues/2093,1613889979,IC_kwDOBm6k_c5gMgG7,9599,simonw,2023-06-29T22:44:08Z,2023-06-30T13:25:39Z,OWNER,"I do like also being able to set options using command line options though - for things like SQL time limits I'd much rather be able to throw on `--setting sql_time_limit_ms 10000` than have to save a config file to disk. So I'd want to support both. Which maybe means also having a way to set plugin options with CLI options. `datasette publish` kind of has that ability already: ``` datasette publish heroku my_database.db \ --name my-heroku-app-demo \ --install=datasette-auth-github \ --plugin-secret datasette-auth-github client_id your_client_id \ --plugin-secret datasette-auth-github client_secret your_client_secret ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1781530343,"Proposal: Combine settings, metadata, static, etc. into a single `datasette.toml` File", https://github.com/simonw/datasette/issues/2093#issuecomment-1613887492,https://api.github.com/repos/simonw/datasette/issues/2093,1613887492,IC_kwDOBm6k_c5gMfgE,9599,simonw,2023-06-29T22:40:25Z,2023-06-29T22:40:25Z,OWNER,"I'm strongly in favour of combining settings, configuration and plugin configuration. I'm not keen on mixing in metadata as well - that feels like a different concept to me, and I'm unhappy with how that's already had things like plugin settings leak into it. I'm not yet sold on TOML - I actually find it less intuitive than YAML, surprisingly. They all have their warts I guess. Datasette already has the ability to consume JSON or YAML for metadata - maybe it could grow TOML support too? That way users could have a `datasette.json` or `datasette.yaml` or `datasette.toml` file depending on their preference. In terms of metadata: since that's means to be driven by a plugin hook anyway, maybe one of the potential sources of metadata is a `metadata` nested object in that `datasette.*` configuration file. Or you can have it in a separate `metadata.json` or bundled into the SQLite database or some other plugin-driven mechanism.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1781530343,"Proposal: Combine settings, metadata, static, etc. into a single `datasette.toml` File", https://github.com/simonw/datasette/issues/2092#issuecomment-1613381990,https://api.github.com/repos/simonw/datasette/issues/2092,1613381990,IC_kwDOBm6k_c5gKkFm,9599,simonw,2023-06-29T15:23:04Z,2023-06-29T15:26:28Z,OWNER,"Felt lazy: ```bash symbex test_homepage -f tests/test_api.py | \ llm -m 4 --system 'Change all of the == some integer tests in this code to isinstance(thing, int) instead' ``` Output: ```python # File: tests/test_api.py Line: 26 @pytest.mark.asyncio async def test_homepage(ds_client): response = await ds_client.get(""/.json"") assert response.status_code == 200 assert ""application/json; charset=utf-8"" == response.headers[""content-type""] data = response.json() assert isinstance(data.keys(), int) d = data[""fixtures""] assert d[""name""] == ""fixtures"" assert isinstance(d[""tables_count""], int) assert isinstance(len(d[""tables_and_views_truncated""]), int) assert d[""tables_and_views_more""] is True # 4 hidden FTS tables + no_primary_key (hidden in metadata) assert isinstance(d[""hidden_tables_count""], int) # 201 in no_primary_key, plus 6 in other hidden tables: assert isinstance(d[""hidden_table_rows_sum""], int), data assert isinstance(d[""views_count""], int) ``` I'll use most of that and delete the obsoleted comments.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1781047747,test_homepage intermittent failure, https://github.com/simonw/datasette/issues/2092#issuecomment-1613375407,https://api.github.com/repos/simonw/datasette/issues/2092,1613375407,IC_kwDOBm6k_c5gKiev,9599,simonw,2023-06-29T15:20:52Z,2023-06-29T15:21:05Z,OWNER,I'm going to remove this assertion entirely. The homepage JSON needs a refactor anyway.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1781047747,test_homepage intermittent failure, https://github.com/simonw/datasette/issues/2091#issuecomment-1613369355,https://api.github.com/repos/simonw/datasette/issues/2091,1613369355,IC_kwDOBm6k_c5gKhAL,9599,simonw,2023-06-29T15:18:34Z,2023-06-29T15:18:34Z,OWNER,Posted on the Glitch feedback forum about this here: https://support.glitch.com/t/upgrade-python-version-from-3-7-which-is-now-eol-to-something-more-recent/63011,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1781022369,Drop support for Python 3.7, https://github.com/simonw/datasette/issues/2091#issuecomment-1613360413,https://api.github.com/repos/simonw/datasette/issues/2091,1613360413,IC_kwDOBm6k_c5gKe0d,9599,simonw,2023-06-29T15:13:04Z,2023-06-29T15:13:04Z,OWNER,"One problem: https://glitch.com/ still provides 3.7: ``` $ python3 --version Python 3.7.10 ``` ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1781022369,Drop support for Python 3.7, https://github.com/simonw/datasette/issues/2090#issuecomment-1613346412,https://api.github.com/repos/simonw/datasette/issues/2090,1613346412,IC_kwDOBm6k_c5gKbZs,9599,simonw,2023-06-29T15:05:04Z,2023-06-29T15:05:04Z,OWNER,"Decided to fix just those ""Ambiguous variable name"" ones: ```bash ruff check . | grep E741 ``` Then iterated through and fixed them all.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1781005740,Adopt ruff for linting, https://github.com/simonw/datasette/issues/2090#issuecomment-1613339404,https://api.github.com/repos/simonw/datasette/issues/2090,1613339404,IC_kwDOBm6k_c5gKZsM,9599,simonw,2023-06-29T15:01:01Z,2023-06-29T15:01:20Z,OWNER,"I tried it just now and got some interesting results. I dropped in a `ruff.toml` file: ```toml line-length = 160 ``` Because the default line length limit of 88 was causing a lot of noisy errors. Then run: ```bash pip install ruff ruff check . ``` Plenty of warnings about unused imports - running `ruff check . --fix` fixed those automatically, but I think I still need to manually review them as some might be imports which are deliberate and should be in `__all__` to ensure they are visible from that module as well. Some lines in tests are longer than even 160 chars, e.g.: https://github.com/simonw/datasette/blob/99ba05118891db9dc30f1dca22ad6709775560de/tests/test_html.py#L673-L681 These can have ` # noqa: E501` added to the end of those lines to skip the check for them. That got it down to: ``` % ruff check . datasette/views/table.py:23:5: F811 Redefinition of unused `format_bytes` from line 19 run_tests.py:2:5: E401 Multiple imports on one line tests/test_api.py:591:40: F811 Redefinition of unused `app_client_no_files` from line 7 tests/test_api.py:629:35: F811 Redefinition of unused `app_client_no_files` from line 7 tests/test_api.py:635:54: F811 Redefinition of unused `app_client_with_dot` from line 8 tests/test_api.py:661:25: F811 Redefinition of unused `app_client_shorter_time_limit` from line 9 tests/test_api.py:759:25: F811 Redefinition of unused `app_client_two_attached_databases_one_immutable` from line 10 tests/test_api.py:892:28: F811 Redefinition of unused `app_client_larger_cache_size` from line 11 tests/test_api.py:928:5: F811 Redefinition of unused `app_client_with_cors` from line 12 tests/test_api.py:929:5: F811 Redefinition of unused `app_client_two_attached_databases_one_immutable` from line 10 tests/test_api.py:969:38: F811 Redefinition of unused `app_client_two_attached_databases` from line 13 tests/test_api.py:976:39: F811 Redefinition of unused `app_client_conflicting_database_names` from line 14 tests/test_api.py:987:38: F811 Redefinition of unused `app_client_immutable_and_inspect_file` from line 15 tests/test_api.py:1002:24: F811 Redefinition of unused `app_client` from line 6 tests/test_csv.py:67:33: F811 Redefinition of unused `app_client_with_cors` from line 6 tests/test_csv.py:157:21: F811 Redefinition of unused `app_client_csv_max_mb_one` from line 5 tests/test_csv.py:198:20: F811 Redefinition of unused `app_client_with_trace` from line 7 tests/test_csv.py:209:53: F811 Redefinition of unused `app_client_with_trace` from line 7 tests/test_csv.py:215:53: F811 Redefinition of unused `app_client_with_trace` from line 7 tests/test_filters.py:102:11: F811 Redefinition of unused `test_through_filters_from_request` from line 81 tests/test_html.py:19:19: F811 Redefinition of unused `app_client_two_attached_databases` from line 7 tests/test_html.py:175:25: F811 Redefinition of unused `app_client_shorter_time_limit` from line 6 tests/test_html.py:469:51: F811 Redefinition of unused `app_client` from line 4 tests/test_html.py:797:26: F811 Redefinition of unused `app_client_base_url_prefix` from line 5 tests/test_html.py:840:44: F811 Redefinition of unused `app_client_base_url_prefix` from line 5 tests/test_html.py:850:51: F811 Redefinition of unused `app_client_base_url_prefix` from line 5 tests/test_pagination.py:50:43: F821 Undefined name `parse_next` tests/test_pagination.py:82:7: F811 Redefinition of unused `KeysetPaginator` from line 36 tests/test_plugins.py:115:15: E741 Ambiguous variable name: `l` tests/test_plugins.py:482:161: E501 Line too long (170 > 160 characters) tests/test_plugins.py:543:29: E741 Ambiguous variable name: `l` tests/test_plugins.py:563:161: E501 Line too long (170 > 160 characters) tests/test_plugins.py:940:62: E741 Ambiguous variable name: `l` tests/test_table_api.py:739:5: F811 Redefinition of unused `app_client_returned_rows_matches_page_size` from line 6 tests/test_table_api.py:1066:45: F811 Redefinition of unused `app_client_with_trace` from line 5 tests/test_table_html.py:484:29: E741 Ambiguous variable name: `l` tests/test_table_html.py:524:29: E741 Ambiguous variable name: `l` tests/test_table_html.py:675:161: E501 Line too long (165 > 160 characters) tests/test_table_html.py:897:161: E501 Line too long (164 > 160 characters) tests/test_table_html.py:902:161: E501 Line too long (164 > 160 characters) tests/test_utils.py:141:161: E501 Line too long (176 > 160 characters) Found 41 errors. ``` Those ""Redefinition of unused `app_client_two_attached_databases`"" lines are caused because of the fixtures pattern I'm using here: https://github.com/simonw/datasette/blob/99ba05118891db9dc30f1dca22ad6709775560de/tests/test_html.py#L3-L20 I could fix that by getting rid of `fixtures.py` and moving those into `conftest.py`.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1781005740,Adopt ruff for linting, https://github.com/simonw/datasette/issues/2089#issuecomment-1613316722,https://api.github.com/repos/simonw/datasette/issues/2089,1613316722,IC_kwDOBm6k_c5gKUJy,9599,simonw,2023-06-29T14:48:10Z,2023-06-29T14:48:10Z,OWNER,Spell check is passing now.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1780973290,codespell test failure, https://github.com/simonw/datasette/issues/2089#issuecomment-1613315851,https://api.github.com/repos/simonw/datasette/issues/2089,1613315851,IC_kwDOBm6k_c5gKT8L,9599,simonw,2023-06-29T14:47:38Z,2023-06-29T14:47:38Z,OWNER,"Confirmed, this was a 2.2.5 change: https://github.com/codespell-project/codespell/releases/tag/v2.2.5 > - Add displaing->displaying by [@peternewman](https://github.com/peternewman) in [#2808](https://github.com/codespell-project/codespell/pull/2808)","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1780973290,codespell test failure, https://github.com/simonw/datasette/issues/2089#issuecomment-1613307716,https://api.github.com/repos/simonw/datasette/issues/2089,1613307716,IC_kwDOBm6k_c5gKR9E,9599,simonw,2023-06-29T14:42:23Z,2023-06-29T14:42:23Z,OWNER,"Yes, upgrading locally got me the correct version and the test failure: ``` % pip install -U codespell Requirement already satisfied: codespell in /Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages (2.2.2) Collecting codespell Downloading codespell-2.2.5-py3-none-any.whl (242 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 242.7/242.7 kB 4.9 MB/s eta 0:00:00 Installing collected packages: codespell Attempting uninstall: codespell Found existing installation: codespell 2.2.2 Uninstalling codespell-2.2.2: Successfully uninstalled codespell-2.2.2 Successfully installed codespell-2.2.5 % codespell docs/metadata.rst docs/metadata.rst:192: displaing ==> displaying ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1780973290,codespell test failure, https://github.com/simonw/datasette/issues/2089#issuecomment-1613306787,https://api.github.com/repos/simonw/datasette/issues/2089,1613306787,IC_kwDOBm6k_c5gKRuj,9599,simonw,2023-06-29T14:41:47Z,2023-06-29T14:41:47Z,OWNER,"Looks like in CI it's running 2.2.5: ``` Collecting codespell (from datasette==1.0a2) Downloading codespell-2.2.5-py3-none-any.whl (242 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 242.7/242.7 kB 31.1 MB/s eta 0:00:00 ``` But on my laptop it's 2.2.2: ``` % codespell --version 2.2.2 ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1780973290,codespell test failure, https://github.com/simonw/datasette/issues/2089#issuecomment-1613305070,https://api.github.com/repos/simonw/datasette/issues/2089,1613305070,IC_kwDOBm6k_c5gKRTu,9599,simonw,2023-06-29T14:40:44Z,2023-06-29T14:40:44Z,OWNER,"I'm not sure why I can't duplicate this failure in my local development environment: ``` % codespell docs/metadata.rst ``` It finds no errors.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1780973290,codespell test failure, https://github.com/simonw/sqlite-utils/issues/561#issuecomment-1610040517,https://api.github.com/repos/simonw/sqlite-utils/issues/561,1610040517,IC_kwDOCGYnMM5f90TF,9599,simonw,2023-06-27T18:44:31Z,2023-06-27T18:44:38Z,OWNER,"Got this working: ```bash sqlite-utils insert /tmp/playground.db Playground_Submission_Data \ ~/Downloads/Playground_Submission_Data.csv --csv --stop-after 2000 ``` ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1777548699,`--stop-after` option for `insert` and `upsert` commands, https://github.com/simonw/sqlite-utils/pull/560#issuecomment-1606315321,https://api.github.com/repos/simonw/sqlite-utils/issues/560,1606315321,IC_kwDOCGYnMM5fvm05,9599,simonw,2023-06-25T23:18:33Z,2023-06-25T23:18:33Z,OWNER,Documentation preview: https://sqlite-utils--560.org.readthedocs.build/en/560/installation.html#alternatives-to-sqlite3,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1773458985,Use sqlean if available in environment, https://github.com/simonw/sqlite-utils/pull/560#issuecomment-1606310630,https://api.github.com/repos/simonw/sqlite-utils/issues/560,1606310630,IC_kwDOCGYnMM5fvlrm,9599,simonw,2023-06-25T23:06:07Z,2023-06-25T23:06:07Z,OWNER,"Filed an issue about the above with `pysqlite3` (which `sqlean.py` is based on) here: - https://github.com/coleifer/pysqlite3/issues/58","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1773458985,Use sqlean if available in environment, https://github.com/simonw/sqlite-utils/pull/560#issuecomment-1606297356,https://api.github.com/repos/simonw/sqlite-utils/issues/560,1606297356,IC_kwDOCGYnMM5fvicM,9599,simonw,2023-06-25T22:42:41Z,2023-06-25T22:42:41Z,OWNER,"Yes that does seem to do the trick: ```pycon >>> import sqlean >>> db = sqlean.connect(""/tmp/4.db"") >>> db.execute('PRAGMA journal_mode;').fetchall() [('delete',)] >>> db.isolation_level '' >>> db.execute('PRAGMA journal_mode=wal;') Traceback (most recent call last): File """", line 1, in sqlean.dbapi2.OperationalError: cannot change into wal mode from within a transaction >>> db.isolation_level = None >>> db.isolation_level >>> db.execute('PRAGMA journal_mode=wal;') ``` Weird how `isolation_level` of empty string causes the error, but setting that to `None` fixes the error.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1773458985,Use sqlean if available in environment, https://github.com/simonw/sqlite-utils/pull/560#issuecomment-1606294627,https://api.github.com/repos/simonw/sqlite-utils/issues/560,1606294627,IC_kwDOCGYnMM5fvhxj,9599,simonw,2023-06-25T22:40:10Z,2023-06-25T22:40:10Z,OWNER,I suspect this has something to do with `autocommit` mode in `sqlite3` - which I may be able to turn off by setting `con.isolation_level = None`.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1773458985,Use sqlean if available in environment, https://github.com/simonw/sqlite-utils/pull/560#issuecomment-1606293382,https://api.github.com/repos/simonw/sqlite-utils/issues/560,1606293382,IC_kwDOCGYnMM5fvheG,9599,simonw,2023-06-25T22:34:47Z,2023-06-25T22:34:47Z,OWNER,"```pycon >>> import sqlite3 >>> db = sqlite3.connect(""/tmp/1.db"") >>> db.execute('PRAGMA journal_mode=wal;') >>> import sqlean >>> db2 = sqlean.connect(""/tmp/2.db"") >>> db2.execute('PRAGMA journal_mode=wal;') Traceback (most recent call last): File """", line 1, in sqlean.dbapi2.OperationalError: cannot change into wal mode from within a transaction ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1773458985,Use sqlean if available in environment, https://github.com/simonw/sqlite-utils/pull/560#issuecomment-1606290917,https://api.github.com/repos/simonw/sqlite-utils/issues/560,1606290917,IC_kwDOCGYnMM5fvg3l,9599,simonw,2023-06-25T22:32:28Z,2023-06-25T22:32:28Z,OWNER,"I've fixed most of the test failures, but I still need to fix this one: > cannot change into wal mode from within a transaction","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1773458985,Use sqlean if available in environment, https://github.com/simonw/sqlite-utils/pull/560#issuecomment-1606273005,https://api.github.com/repos/simonw/sqlite-utils/issues/560,1606273005,IC_kwDOCGYnMM5fvcft,9599,simonw,2023-06-25T21:47:47Z,2023-06-25T21:47:47Z,OWNER,I can use https://github.com/simonw/sqlite-dump as an optional dependency to handle the missing `.iterdump()` method.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1773458985,Use sqlean if available in environment, https://github.com/simonw/sqlite-utils/pull/560#issuecomment-1606270887,https://api.github.com/repos/simonw/sqlite-utils/issues/560,1606270887,IC_kwDOCGYnMM5fvb-n,9599,simonw,2023-06-25T21:37:12Z,2023-06-26T08:21:00Z,OWNER,"On my own laptop I got a crash running the tests - details here: - https://github.com/nalgeon/sqlean.py/issues/3","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1773458985,Use sqlean if available in environment, https://github.com/simonw/sqlite-utils/pull/560#issuecomment-1606270055,https://api.github.com/repos/simonw/sqlite-utils/issues/560,1606270055,IC_kwDOCGYnMM5fvbxn,9599,simonw,2023-06-25T21:31:56Z,2023-06-25T21:31:56Z,OWNER,"Lots of failures now that I'm trying to run the tests against `sqlean.py` on macOS and Python 3.10: https://github.com/simonw/sqlite-utils/actions/runs/5371800108/jobs/9744802953 A bunch of these, because `pysqlite3` chooses not to implement `.iterdump()`: ``` @pytest.fixture def db_to_analyze_path(db_to_analyze, tmpdir): path = str(tmpdir / ""test.db"") db = sqlite3.connect(path) > db.executescript(""\n"".join(db_to_analyze.conn.iterdump())) E AttributeError: 'sqlean.dbapi2.Connection' object has no attribute 'iterdump' ``` Also some of these: ``` def test_analyze_whole_database(db): assert set(db.table_names()) == {""one_index"", ""two_indexes""} db.analyze() > assert set(db.table_names()) == {""one_index"", ""two_indexes"", ""sqlite_stat1""} E AssertionError: assert {'one_index',...'two_indexes'} == {'one_index',...'two_indexes'} E Extra items in the left set: E 'sqlite_stat4' E Full diff: E - {'two_indexes', 'sqlite_stat1', 'one_index'} E + {'two_indexes', 'sqlite_stat1', 'sqlite_stat4', 'one_index'} E ? ++++++++++++++++ ``` Apparently `sqlean.py` adds a `sqlite_stat4` table that the tests are not expecting. Plus some errors that look like this: ``` def test_enable_wal(): runner = CliRunner() dbs = [""test.db"", ""test2.db""] with runner.isolated_filesystem(): for dbname in dbs: db = Database(dbname) db[""t""].create({""pk"": int}, pk=""pk"") assert db.journal_mode == ""delete"" result = runner.invoke(cli.cli, [""enable-wal""] + dbs) > assert 0 == result.exit_code E AssertionError: assert 0 == 1 E + where 1 = .exit_code ``` Test summary: ``` ============ 13 failed, 909 passed, 16 skipped, 2 errors in 19.29s ============= ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1773458985,Use sqlean if available in environment, https://github.com/simonw/sqlite-utils/pull/560#issuecomment-1606237836,https://api.github.com/repos/simonw/sqlite-utils/issues/560,1606237836,IC_kwDOCGYnMM5fvT6M,22429695,codecov[bot],2023-06-25T19:49:45Z,2023-06-26T08:20:59Z,NONE,"## [Codecov](https://app.codecov.io/gh/simonw/sqlite-utils/pull/560?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) Report Patch coverage: **`80.55`**% and project coverage change: **`-0.15`** :warning: > Comparison is base [(`2747257`)](https://app.codecov.io/gh/simonw/sqlite-utils/commit/2747257a3334d55e890b40ec58fada57ae8cfbfd?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) 96.36% compared to head [(`5e7d27e`)](https://app.codecov.io/gh/simonw/sqlite-utils/pull/560?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) 96.22%.
Additional details and impacted files ```diff @@ Coverage Diff @@ ## main #560 +/- ## ========================================== - Coverage 96.36% 96.22% -0.15% ========================================== Files 6 6 Lines 2726 2752 +26 ========================================== + Hits 2627 2648 +21 - Misses 99 104 +5 ``` | [Impacted Files](https://app.codecov.io/gh/simonw/sqlite-utils/pull/560?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) | Coverage Δ | | |---|---|---| | [sqlite\_utils/utils.py](https://app.codecov.io/gh/simonw/sqlite-utils/pull/560?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-c3FsaXRlX3V0aWxzL3V0aWxzLnB5) | `94.56% <63.63%> (-0.62%)` | :arrow_down: | | [sqlite\_utils/db.py](https://app.codecov.io/gh/simonw/sqlite-utils/pull/560?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-c3FsaXRlX3V0aWxzL2RiLnB5) | `97.33% <86.36%> (-0.20%)` | :arrow_down: | | [sqlite\_utils/cli.py](https://app.codecov.io/gh/simonw/sqlite-utils/pull/560?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-c3FsaXRlX3V0aWxzL2NsaS5weQ==) | `95.22% <100.00%> (ø)` | |
[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/simonw/sqlite-utils/pull/560?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). :loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1773458985,Use sqlean if available in environment, https://github.com/simonw/datasette/issues/2087#issuecomment-1636134091,https://api.github.com/repos/simonw/datasette/issues/2087,1636134091,IC_kwDOBm6k_c5hhWzL,653549,adarshp,2023-07-14T17:02:03Z,2023-07-14T17:02:03Z,NONE,"@asg017 - the docs say that the autodetection only occurs in configuration directory mode. I for one would also be interested in the `--settings settings.json` feature. For context, I am developing a large database for use with Datasette, but the database lives in a different network volume than my source code, since the volume in which my source code lives is aggressively backed up, while the location where the database lives is meant for temporary files and is not as aggressively backed up (since the backups would get unreasonably large).","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1765870617,`--settings settings.json` option, https://github.com/simonw/datasette/issues/2087#issuecomment-1616853644,https://api.github.com/repos/simonw/datasette/issues/2087,1616853644,IC_kwDOBm6k_c5gXzqM,15178711,asg017,2023-07-02T22:00:48Z,2023-07-02T22:00:48Z,CONTRIBUTOR,"I just saw in the docs that Dasette auto-detects `settings.json`: > settings.json - settings that would normally be passed using --setting - here they should be stored as a JSON object of key/value pairs > [*Source*](https://docs.datasette.io/en/stable/settings.html#:~:text=settings.json%20%2D%20settings%20that%20would%20normally%20be%20passed%20using%20%2D%2Dsetting%20%2D%20here%20they%20should%20be%20stored%20as%20a%20JSON%20object%20of%20key/value%20pairs)","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1765870617,`--settings settings.json` option, https://github.com/simonw/sqlite-utils/issues/557#issuecomment-1590531892,https://api.github.com/repos/simonw/sqlite-utils/issues/557,1590531892,IC_kwDOCGYnMM5ezZc0,7908073,chapmanjacobd,2023-06-14T06:09:21Z,2023-06-14T06:09:21Z,CONTRIBUTOR,"I put together a [simple script](https://github.com/chapmanjacobd/library/blob/42129c5ebe15f9d74653c0f5ca4ed0c991d383e0/xklb/scripts/dedupe_db.py) to upsert and remove duplicate rows based on business keys. If anyone has similar problems with above this might help ``` CREATE TABLE my_table ( id INTEGER PRIMARY KEY, column1 TEXT, column2 TEXT, column3 TEXT ); INSERT INTO my_table (column1, column2, column3) VALUES ('Value 1', 'Duplicate 1', 'Duplicate A'), ('Value 2', 'Duplicate 2', 'Duplicate B'), ('Value 3', 'Duplicate 2', 'Duplicate C'), ('Value 4', 'Duplicate 3', 'Duplicate D'), ('Value 5', 'Duplicate 3', 'Duplicate E'), ('Value 6', 'Duplicate 3', 'Duplicate F'); ``` ``` library dedupe-db test.db my_table --bk column2 ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1740150327,Aliased ROWID option for tables created from alter=True commands, https://github.com/simonw/sqlite-utils/issues/557#issuecomment-1577355134,https://api.github.com/repos/simonw/sqlite-utils/issues/557,1577355134,IC_kwDOCGYnMM5eBId-,7908073,chapmanjacobd,2023-06-05T19:26:26Z,2023-06-05T19:26:26Z,CONTRIBUTOR,"this isn't really actionable... I'm just being a whiny baby. I have tasted the milk of being able to use `upsert_all`, `insert_all`, etc without having to write DDL to create tables. The meat of the issue is that SQLITE doesn't make rowid stable between vacuums so it is not possible to take shortcuts","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1740150327,Aliased ROWID option for tables created from alter=True commands, https://github.com/simonw/sqlite-utils/issues/556#issuecomment-1575310378,https://api.github.com/repos/simonw/sqlite-utils/issues/556,1575310378,IC_kwDOCGYnMM5d5VQq,601708,mcint,2023-06-04T01:21:15Z,2023-06-04T01:21:15Z,CONTRIBUTOR,"I've resolved my use, with the line-buffered output and while read loop for line buffered input, but I leave this here so the incremental saving or line-buffered use-case can be explicitly handled or rejected (or deferred).","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1740026046,Support storing incrementally piped values, https://github.com/simonw/sqlite-utils/issues/555#issuecomment-1592047502,https://api.github.com/repos/simonw/sqlite-utils/issues/555,1592047502,IC_kwDOCGYnMM5e5LeO,7908073,chapmanjacobd,2023-06-14T22:00:10Z,2023-06-14T22:01:57Z,CONTRIBUTOR,"You may want to try doing a performance comparison between this and just selecting all the ids with few constraints and then doing the filtering within python. That might seem like a lazy-programmer, inefficient way but queries with large resultsets are a different profile than what databases like SQLITE are designed for. That is not to say that SQLITE is slow or that python is always faster but when you start reading >20% of an index there is an equilibrium that is reached. Especially when adding in writing extra temp tables and stuff to memory/disk. And especially given the `NOT IN` style of query... You may also try chunking like this: ```py def chunks(lst, n) -> Generator: for i in range(0, len(lst), n): yield lst[i : i + n] SQLITE_PARAM_LIMIT = 32765 data = [] chunked = chunks(video_ids, consts.SQLITE_PARAM_LIMIT) for ids in chunked: data.expand( list( db.query( f""""""SELECT * from videos WHERE id in ("""""" + "","".join([""?""] * len(ids)) + "")"", (*ids,), ) ) ) ``` but that actually won't work with your `NOT IN` requirements. You need to query the full resultset to check any row. Since you are doing stuff with files/videos in SQLITE you might be interested in my side project: https://github.com/chapmanjacobd/library","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1733198948,Filter table by a large bunch of ids, https://github.com/simonw/datasette/pull/2080#issuecomment-1563650990,https://api.github.com/repos/simonw/datasette/issues/2080,1563650990,IC_kwDOBm6k_c5dM2uu,9599,simonw,2023-05-26T00:08:59Z,2023-05-26T00:08:59Z,OWNER,"I'm not going to document this yet, I want to let it bake for a bit longer first.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1726603778,New View base class, https://github.com/simonw/datasette/pull/2080#issuecomment-1563629348,https://api.github.com/repos/simonw/datasette/issues/2080,1563629348,IC_kwDOBm6k_c5dMxck,22429695,codecov[bot],2023-05-25T23:31:10Z,2023-05-26T00:07:34Z,NONE,"## [Codecov](https://app.codecov.io/gh/simonw/datasette/pull/2080?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) Report Patch coverage: **`95.45`**% and project coverage change: **`+0.01`** :tada: > Comparison is base [(`b49fa44`)](https://app.codecov.io/gh/simonw/datasette/commit/b49fa446d683ddcaf6faf2944dacc0d866bf2d70?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) 92.40% compared to head [(`df5fd73`)](https://app.codecov.io/gh/simonw/datasette/pull/2080?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) 92.41%. > :exclamation: Current head df5fd73 differs from pull request most recent head e990fbc. Consider uploading reports for the commit e990fbc to get more accurate results
Additional details and impacted files ```diff @@ Coverage Diff @@ ## main #2080 +/- ## ========================================== + Coverage 92.40% 92.41% +0.01% ========================================== Files 39 39 Lines 5768 5790 +22 ========================================== + Hits 5330 5351 +21 - Misses 438 439 +1 ``` | [Impacted Files](https://app.codecov.io/gh/simonw/datasette/pull/2080?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) | Coverage Δ | | |---|---|---| | [datasette/views/base.py](https://app.codecov.io/gh/simonw/datasette/pull/2080?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-ZGF0YXNldHRlL3ZpZXdzL2Jhc2UucHk=) | `92.97% <95.45%> (+0.18%)` | :arrow_up: |
[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/simonw/datasette/pull/2080?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). :loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1726603778,New View base class, https://github.com/simonw/datasette/pull/2080#issuecomment-1563626231,https://api.github.com/repos/simonw/datasette/issues/2080,1563626231,IC_kwDOBm6k_c5dMwr3,9599,simonw,2023-05-25T23:25:17Z,2023-05-25T23:25:17Z,OWNER,I'm going to try using this for the `/-/patterns` page.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1726603778,New View base class, https://github.com/simonw/datasette/issues/2079#issuecomment-1563607291,https://api.github.com/repos/simonw/datasette/issues/2079,1563607291,IC_kwDOBm6k_c5dMsD7,9599,simonw,2023-05-25T22:56:28Z,2023-05-25T22:56:28Z,OWNER,Wrote this up as a TIL: https://til.simonwillison.net/http/testing-cors-max-age,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1726531350,Datasette should serve Access-Control-Max-Age, https://github.com/simonw/datasette/issues/2079#issuecomment-1563597589,https://api.github.com/repos/simonw/datasette/issues/2079,1563597589,IC_kwDOBm6k_c5dMpsV,9599,simonw,2023-05-25T22:42:07Z,2023-05-25T22:42:07Z,OWNER,"Mystery solved as to why I wasn't seeing this work: I had ""Disable Cache"" checked! I ran this experiment after un-checking that box: ```javascript fetch('https://latest.datasette.io/ephemeral/foo/1/-/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ test: 'test' }) }); // And run it again fetch('https://latest.datasette.io/ephemeral/foo/1/-/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ test: 'test' }) }); // Now try a thing that doesn't serve that max-age header yet: fetch('https://latest-with-plugins.datasette.io/ephemeral/foo/1/-/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ test: 'test' }) }); // And a second time but within 5s fetch('https://latest-with-plugins.datasette.io/ephemeral/foo/1/-/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ test: 'test' }) }); // Third time after waiting longer than 5s fetch('https://latest-with-plugins.datasette.io/ephemeral/foo/1/-/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ test: 'test' }) }); // Try that original one again - still within the 1hr cache time fetch('https://latest.datasette.io/ephemeral/foo/1/-/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ test: 'test' }) }); ``` The results show that the cache of 1hr was being obeyed for `latest.datasette.io` while the `latest-with-plugins.datasette.io` default cache of 5s was being obeyed too. ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1726531350,Datasette should serve Access-Control-Max-Age, https://github.com/simonw/datasette/issues/2079#issuecomment-1563588199,https://api.github.com/repos/simonw/datasette/issues/2079,1563588199,IC_kwDOBm6k_c5dMnZn,9599,simonw,2023-05-25T22:29:47Z,2023-05-25T22:30:12Z,OWNER,"https://fetch.spec.whatwg.org/#http-access-control-max-age says: > Indicates the number of seconds (5 by default) the information provided by the [Access-Control-Allow-Methods](https://fetch.spec.whatwg.org/#http-access-control-allow-methods) and [Access-Control-Allow-Headers](https://fetch.spec.whatwg.org/#http-access-control-allow-headers) [headers](https://fetch.spec.whatwg.org/#concept-header) can be cached. So there was already a 5s cache anyway.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1726531350,Datasette should serve Access-Control-Max-Age, https://github.com/simonw/datasette/issues/2079#issuecomment-1563587230,https://api.github.com/repos/simonw/datasette/issues/2079,1563587230,IC_kwDOBm6k_c5dMnKe,9599,simonw,2023-05-25T22:28:20Z,2023-05-25T22:28:20Z,OWNER,"Weird... after the deploy went out: But the request did indeed get the new header: So I'm not sure why it's making multiple `POST` requests like that. Maybe it's because the attempted `POST` failed with a 404?","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1726531350,Datasette should serve Access-Control-Max-Age, https://github.com/simonw/datasette/issues/2079#issuecomment-1563565407,https://api.github.com/repos/simonw/datasette/issues/2079,1563565407,IC_kwDOBm6k_c5dMh1f,9599,simonw,2023-05-25T22:09:53Z,2023-05-25T22:09:53Z,OWNER,Updated docs: https://docs.datasette.io/en/latest/json_api.html#enabling-cors,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1726531350,Datasette should serve Access-Control-Max-Age, https://github.com/simonw/datasette/issues/2079#issuecomment-1563563438,https://api.github.com/repos/simonw/datasette/issues/2079,1563563438,IC_kwDOBm6k_c5dMhWu,9599,simonw,2023-05-25T22:08:28Z,2023-05-25T22:08:28Z,OWNER,"I ran this on https://www.example.com/ twice using the console: ```javascript fetch( `https://latest.datasette.io/ephemeral/foo/1/-/update`, { method: ""POST"", mode: ""cors"", headers: { Authorization: `Bearer tok`, ""Content-Type"": ""application/json"", }, body: JSON.stringify({update: {blah: 1}}), } ) .then((r) => r.json()) .then((data) => { console.log(data); }); ``` And got this in the network pane: ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1726531350,Datasette should serve Access-Control-Max-Age, https://github.com/simonw/datasette/issues/2079#issuecomment-1563558915,https://api.github.com/repos/simonw/datasette/issues/2079,1563558915,IC_kwDOBm6k_c5dMgQD,9599,simonw,2023-05-25T22:04:41Z,2023-05-25T22:04:41Z,OWNER,I'm going with 3600 for 1 hour instead of 600 for 10 minutes.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1726531350,Datasette should serve Access-Control-Max-Age, https://github.com/simonw/datasette/issues/2079#issuecomment-1563547097,https://api.github.com/repos/simonw/datasette/issues/2079,1563547097,IC_kwDOBm6k_c5dMdXZ,9599,simonw,2023-05-25T21:51:38Z,2023-05-25T21:51:38Z,OWNER,"Also need to update this documentation: https://github.com/simonw/datasette/blob/9584879534ff0556e04e4c420262972884cac87b/docs/json_api.rst?plain=1#L453-L465 Or maybe make that automated via `cog`.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1726531350,Datasette should serve Access-Control-Max-Age, https://github.com/simonw/datasette/issues/2078#issuecomment-1563625093,https://api.github.com/repos/simonw/datasette/issues/2078,1563625093,IC_kwDOBm6k_c5dMwaF,9599,simonw,2023-05-25T23:23:15Z,2023-05-25T23:23:15Z,OWNER,"Rest of the work on this will happen in the PR: - #2080","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1726236847,Resolve the difference between `wrap_view()` and `BaseView`, https://github.com/simonw/datasette/issues/2078#issuecomment-1563522011,https://api.github.com/repos/simonw/datasette/issues/2078,1563522011,IC_kwDOBm6k_c5dMXPb,9599,simonw,2023-05-25T21:22:30Z,2023-05-25T21:22:30Z,OWNER,"This is bad: ```python async def __call__(self, request, datasette): try: handler = getattr(self, request.method.lower()) return await handler(request, datasette) except AttributeError: return await self.method_not_allowed(request) ``` Because it hides any `AttributeError` exceptions that might occur in the view code.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1726236847,Resolve the difference between `wrap_view()` and `BaseView`, https://github.com/simonw/datasette/issues/2078#issuecomment-1563511171,https://api.github.com/repos/simonw/datasette/issues/2078,1563511171,IC_kwDOBm6k_c5dMUmD,9599,simonw,2023-05-25T21:11:20Z,2023-05-25T21:13:05Z,OWNER,I'm going to call this `VerbView` for the moment. Might even rename it to `View` later.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1726236847,Resolve the difference between `wrap_view()` and `BaseView`, https://github.com/simonw/datasette/issues/2078#issuecomment-1563498048,https://api.github.com/repos/simonw/datasette/issues/2078,1563498048,IC_kwDOBm6k_c5dMRZA,9599,simonw,2023-05-25T20:57:52Z,2023-05-25T20:58:13Z,OWNER,"Here's a new `BaseView` class that automatically populates `OPTIONS` based on available methods: ```python class BaseView: async def head(self, *args, **kwargs): try: response = await self.get(*args, **kwargs) response.body = b"""" return response except AttributeError: raise async def method_not_allowed(self, request): if ( request.path.endswith("".json"") or request.headers.get(""content-type"") == ""application/json"" ): response = Response.json( {""ok"": False, ""error"": ""Method not allowed""}, status=405 ) else: response = Response.text(""Method not allowed"", status=405) return response async def options(self, request, *args, **kwargs): response = Response.text(""ok"") response.headers[""allow""] = "", "".join( method.upper() for method in (""head"", ""get"", ""post"", ""put"", ""patch"", ""delete"") if hasattr(self, method) ) return response async def __call__(self, request, datasette): try: handler = getattr(self, request.method.lower()) return await handler(request, datasette) except AttributeError: return await self.method_not_allowed(request) class DemoView(BaseView): async def get(self, datasette, request): return Response.text(""Hello there! {} - {}"".format(datasette, request)) post = get ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1726236847,Resolve the difference between `wrap_view()` and `BaseView`, https://github.com/simonw/datasette/issues/2078#issuecomment-1563488929,https://api.github.com/repos/simonw/datasette/issues/2078,1563488929,IC_kwDOBm6k_c5dMPKh,9599,simonw,2023-05-25T20:48:12Z,2023-05-25T20:48:39Z,OWNER,"Actually no need for that extra level of parameter detection: `BaseView.__call__` should _always_ take `datasette, request` - `scope` and `receive` are both available on `request`, and `send` is only needed if you're not planning on returning a `Response` object. So the `get` and `post` and suchlike methods should take `datasette` and `request` too.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1726236847,Resolve the difference between `wrap_view()` and `BaseView`, https://github.com/simonw/datasette/issues/2078#issuecomment-1563444296,https://api.github.com/repos/simonw/datasette/issues/2078,1563444296,IC_kwDOBm6k_c5dMERI,9599,simonw,2023-05-25T20:06:08Z,2023-05-25T20:06:08Z,OWNER,"This prototype seems to work well: ```diff diff --git a/datasette/app.py b/datasette/app.py index d7dace67..ed0edf28 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -17,6 +17,7 @@ import secrets import sys import threading import time +import types import urllib.parse from concurrent import futures from pathlib import Path @@ -1266,6 +1267,8 @@ class Datasette: # TODO: /favicon.ico and /-/static/ deserve far-future cache expires add_route(favicon, ""/favicon.ico"") + add_route(wrap_view(DemoView, self), '/demo') + add_route( asgi_static(app_root / ""datasette"" / ""static""), r""/-/static/(?P.*)$"" ) @@ -1673,8 +1676,46 @@ def _cleaner_task_str(task): return _cleaner_task_str_re.sub("""", s) -def wrap_view(view_fn, datasette): - @functools.wraps(view_fn) +class DemoView: + async def __call__(self, datasette, request): + return Response.text(""Hello there! {} - {}"".format(datasette, request)) + +def wrap_view(view_fn_or_class, datasette): + is_function = isinstance(view_fn_or_class, types.FunctionType) + if is_function: + return wrap_view_function(view_fn_or_class, datasette) + else: + if not isinstance(view_fn_or_class, type): + raise ValueError(""view_fn_or_class must be a function or a class"") + return wrap_view_class(view_fn_or_class, datasette) + + +def wrap_view_class(view_class, datasette): + async def async_view_for_class(request, send): + instance = view_class() + if inspect.iscoroutinefunction(instance.__call__): + return await async_call_with_supported_arguments( + instance.__call__, + scope=request.scope, + receive=request.receive, + send=send, + request=request, + datasette=datasette, + ) + else: + return call_with_supported_arguments( + instance.__call__, + scope=request.scope, + receive=request.receive, + send=send, + request=request, + datasette=datasette, + ) + + return async_view_for_class + + +def wrap_view_function(view_fn, datasette): async def async_view_fn(request, send): if inspect.iscoroutinefunction(view_fn): response = await async_call_with_supported_arguments( ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1726236847,Resolve the difference between `wrap_view()` and `BaseView`, https://github.com/simonw/datasette/issues/2078#issuecomment-1563419066,https://api.github.com/repos/simonw/datasette/issues/2078,1563419066,IC_kwDOBm6k_c5dL-G6,9599,simonw,2023-05-25T19:42:16Z,2023-05-25T19:43:08Z,OWNER,"Maybe what I want here is the ability to register classes with the router - and have the router know that if it's a class it should instantiate it via its constructor and then await `__call__` it. The neat thing about it is that it can reduce the risk of having a class instance that accidentally shares state between requests. It also encourages that each class only responds based on the `datasette, request, ...` objects that are passed to its methods.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1726236847,Resolve the difference between `wrap_view()` and `BaseView`, https://github.com/simonw/datasette/issues/2078#issuecomment-1563359114,https://api.github.com/repos/simonw/datasette/issues/2078,1563359114,IC_kwDOBm6k_c5dLveK,9599,simonw,2023-05-25T18:47:57Z,2023-05-25T18:47:57Z,OWNER,"Oops, that broke everything: ``` @documented async def await_me_maybe(value: typing.Any) -> typing.Any: ""If value is callable, call it. If awaitable, await it. Otherwise return it."" > if callable(value): E TypeError: 'module' object is not callable ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1726236847,Resolve the difference between `wrap_view()` and `BaseView`, https://github.com/simonw/datasette/issues/2078#issuecomment-1563329245,https://api.github.com/repos/simonw/datasette/issues/2078,1563329245,IC_kwDOBm6k_c5dLoLd,9599,simonw,2023-05-25T18:26:47Z,2023-05-25T18:28:08Z,OWNER,"With type hints and a namedtuple: ```python import asyncio import types from typing import NamedTuple, Any class CallableStatus(NamedTuple): is_callable: bool is_async_callable: bool def check_callable(obj: Any) -> CallableStatus: if not callable(obj): return CallableStatus(False, False) if isinstance(obj, type): # It's a class return CallableStatus(True, False) if isinstance(obj, types.FunctionType): return CallableStatus(True, asyncio.iscoroutinefunction(obj)) if hasattr(obj, ""__call__""): return CallableStatus(True, asyncio.iscoroutinefunction(obj.__call__)) assert False, ""obj {} is somehow callable with no __call__ method"".format(repr(obj)) ``` ```python for thing in ( async_func, non_async_func, AsyncClass(), NotAsyncClass(), ClassNoCall(), AsyncClass, NotAsyncClass, ClassNoCall, ): print(thing, check_callable(thing)) ``` ``` CallableStatus(is_callable=True, is_async_callable=True) CallableStatus(is_callable=True, is_async_callable=False) <__main__.AsyncClass object at 0x106ba7490> CallableStatus(is_callable=True, is_async_callable=True) <__main__.NotAsyncClass object at 0x106740150> CallableStatus(is_callable=True, is_async_callable=False) <__main__.ClassNoCall object at 0x10676d910> CallableStatus(is_callable=False, is_async_callable=False) CallableStatus(is_callable=True, is_async_callable=False) CallableStatus(is_callable=True, is_async_callable=False) CallableStatus(is_callable=True, is_async_callable=False) ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1726236847,Resolve the difference between `wrap_view()` and `BaseView`, https://github.com/simonw/datasette/issues/2078#issuecomment-1563326000,https://api.github.com/repos/simonw/datasette/issues/2078,1563326000,IC_kwDOBm6k_c5dLnYw,9599,simonw,2023-05-25T18:23:38Z,2023-05-25T18:23:38Z,OWNER,I don't like that `is_callable()` implies a single boolean result but actually returns a pair. I'll call it `check_callable(obj)` instead.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1726236847,Resolve the difference between `wrap_view()` and `BaseView`, https://github.com/simonw/datasette/issues/2078#issuecomment-1563318598,https://api.github.com/repos/simonw/datasette/issues/2078,1563318598,IC_kwDOBm6k_c5dLllG,9599,simonw,2023-05-25T18:17:03Z,2023-05-25T18:21:25Z,OWNER,"I think I want that to return `(is_callable, is_async)` - so I can both test if the thing can be called AND if it should be awaited in the same operation (without any exceptions). I tried this: ```python def is_callable(obj): ""Returns (is_callable, is_async_callable)"" if not callable(obj): return False, False if isinstance(obj, types.FunctionType): return True, asyncio.iscoroutinefunction(obj) if hasattr(obj, '__call__'): return True, asyncio.iscoroutinefunction(obj.__call__) return False, False ``` ```python for thing in ( async_func, non_async_func, AsyncClass(), NotAsyncClass(), ClassNoCall(), AsyncClass, NotAsyncClass, ClassNoCall ): print(thing, is_callable(thing)) ``` And got: ``` (True, True) (True, False) <__main__.AsyncClass object at 0x106cce490> (True, True) <__main__.NotAsyncClass object at 0x106ccf710> (True, False) <__main__.ClassNoCall object at 0x106ccc810> (False, False) (True, True) (True, False) (True, False) ``` Which is almost right, but I don't like that `AsyncClass` is shown as callable (which it is, since it's a class) and awaitable (which it is not - the `__call__` method may be async but calling the class constructor is not). So I'm going to detect classes using `isinstance(obj, type)`. ```python def is_callable(obj): ""Returns (is_callable, is_async_callable)"" if not callable(obj): return False, False if isinstance(obj, type): # It's a class return True, False if isinstance(obj, types.FunctionType): return True, asyncio.iscoroutinefunction(obj) if hasattr(obj, '__call__'): return True, asyncio.iscoroutinefunction(obj.__call__) assert False, ""obj {} somehow is callable with no __call__ method"".format(obj) ``` I am reasonably confident the `AssertionError` can never be raised. And now: ``` (True, True) (True, False) <__main__.AsyncClass object at 0x106ccfa50> (True, True) <__main__.NotAsyncClass object at 0x106ccc8d0> (True, False) <__main__.ClassNoCall object at 0x106cd7690> (False, False) (True, False) (True, False) (True, False) ``` Which is what I wanted.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1726236847,Resolve the difference between `wrap_view()` and `BaseView`, https://github.com/simonw/datasette/issues/2078#issuecomment-1563308919,https://api.github.com/repos/simonw/datasette/issues/2078,1563308919,IC_kwDOBm6k_c5dLjN3,9599,simonw,2023-05-25T18:08:34Z,2023-05-25T18:08:34Z,OWNER,"After much fiddling this seems to work: ```python import asyncio, types def is_async_callable(obj): if not callable(obj): raise ValueError(""Object is not callable"") if isinstance(obj, types.FunctionType): return asyncio.iscoroutinefunction(obj) if hasattr(obj, '__call__'): return asyncio.iscoroutinefunction(obj.__call__) raise ValueError(""Not a function and has no __call__ attribute"") ``` Tested like so: ```python class AsyncClass: async def __call__(self): pass class NotAsyncClass: def __call__(self): pass class ClassNoCall: pass async def async_func(): pass def non_async_func(): pass for thing in (AsyncClass(), NotAsyncClass(), ClassNoCall(), async_func, non_async_func): try: print(thing, is_async_callable(thing)) except Exception as ex: print(thing, ex) ``` ``` <__main__.AsyncClass object at 0x106c32150> True <__main__.NotAsyncClass object at 0x106c32390> False <__main__.ClassNoCall object at 0x106c32750> Object is not callable True False ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1726236847,Resolve the difference between `wrap_view()` and `BaseView`, https://github.com/simonw/datasette/issues/2078#issuecomment-1563294669,https://api.github.com/repos/simonw/datasette/issues/2078,1563294669,IC_kwDOBm6k_c5dLfvN,9599,simonw,2023-05-25T17:57:06Z,2023-05-25T17:57:06Z,OWNER,"I may need to be able to detect if a class instance has an `async def __call__` method - I think I can do that like so: ```python def iscoroutinefunction(obj): if inspect.iscoroutinefunction(obj): return True if hasattr(obj, '__call__') and inspect.iscoroutinefunction(obj.__call__): return True return False ``` From https://github.com/encode/starlette/issues/886#issuecomment-606585152","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1726236847,Resolve the difference between `wrap_view()` and `BaseView`, https://github.com/simonw/datasette/issues/2078#issuecomment-1563292373,https://api.github.com/repos/simonw/datasette/issues/2078,1563292373,IC_kwDOBm6k_c5dLfLV,9599,simonw,2023-05-25T17:55:12Z,2023-05-25T17:55:30Z,OWNER,"So I think subclasses of `BaseView` need to offer a callable which accepts all five of the DI arguments - `datasette`, `request`, `scope`, `send`, `receive` - and then makes a decision based on the HTTP verb as to which method of the class to call. Those methods themselves can accept a subset of those parameters and will only be sent on to them. Having two layers of parameter detection feels a little bit untidy, but I think it will work.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1726236847,Resolve the difference between `wrap_view()` and `BaseView`, https://github.com/simonw/datasette/issues/2078#issuecomment-1563283939,https://api.github.com/repos/simonw/datasette/issues/2078,1563283939,IC_kwDOBm6k_c5dLdHj,9599,simonw,2023-05-25T17:47:38Z,2023-05-25T17:47:38Z,OWNER,"The idea behind `wrap_view()` is dependency injection - it's mainly used by plugins: https://docs.datasette.io/en/0.64.3/plugin_hooks.html#register-routes-datasette But I like the pattern so I started using it for some of Datasette's own features. I should use it for _all_ of Datasette's own features. But I still like the way `BaseView` helps with running different code for GET/POST/etc verbs.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1726236847,Resolve the difference between `wrap_view()` and `BaseView`, https://github.com/simonw/datasette/issues/2078#issuecomment-1563282327,https://api.github.com/repos/simonw/datasette/issues/2078,1563282327,IC_kwDOBm6k_c5dLcuX,9599,simonw,2023-05-25T17:46:05Z,2023-05-25T17:46:05Z,OWNER,"Here's what `wrap_view()` does: https://github.com/simonw/datasette/blob/49184c569cd70efbda4f3f062afef3a34401d8d5/datasette/app.py#L1676-L1700 It's used e.g. here: https://github.com/simonw/datasette/blob/49184c569cd70efbda4f3f062afef3a34401d8d5/datasette/app.py#L1371-L1375 The `BaseView` thing meanwhile works like this: https://github.com/simonw/datasette/blob/d97e82df3c8a3f2e97038d7080167be9bb74a68d/datasette/views/base.py#L56-L157","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1726236847,Resolve the difference between `wrap_view()` and `BaseView`, https://github.com/simonw/sqlite-utils/issues/554#issuecomment-1557607516,https://api.github.com/repos/simonw/sqlite-utils/issues/554,1557607516,IC_kwDOCGYnMM5c1zRc,1231935,xavdid,2023-05-22T17:18:33Z,2023-05-22T17:18:33Z,NONE,"Oh and for context - this goes away if I use `.upsert` instead of `insert(..., ignore=True)`, but I don't want to update the value if it's written, just do an insert if it's new. The code is basically: ```py def save_items(table, items): db[""users""].insert(build_user(items[0]), pk=""id"",ignore=True) db[table].insert_all(items) if comments := fetch_comments(): save_items('comments', comments) if posts := fetch_posts(): save_items('posts', posts) ``` So either `comments` or `post` could create the relevant user if those items exist. In cases where they _both_ exist, I get this error. I need the `pk` because either call could create the table.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1720096994,"`IndexError` when doing `.insert(..., pk='id')` after `insert_all`", https://github.com/simonw/datasette/pull/2077#issuecomment-1613290899,https://api.github.com/repos/simonw/datasette/issues/2077,1613290899,IC_kwDOBm6k_c5gKN2T,9599,simonw,2023-06-29T14:32:16Z,2023-06-29T14:32:16Z,OWNER,@dependabot recreate,"{""total_count"": 1, ""+1"": 1, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1719759468,Bump furo from 2023.3.27 to 2023.5.20, https://github.com/simonw/datasette/pull/2077#issuecomment-1557289070,https://api.github.com/repos/simonw/datasette/issues/2077,1557289070,IC_kwDOBm6k_c5c0lhu,22429695,codecov[bot],2023-05-22T14:08:33Z,2023-06-29T14:40:35Z,NONE,"## [Codecov](https://app.codecov.io/gh/simonw/datasette/pull/2077?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) Report Patch and project coverage have no change. > Comparison is base [(`ede6203`)](https://app.codecov.io/gh/simonw/datasette/commit/ede62036180993dbd9d4e5d280fc21c183cda1c3?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) 92.40% compared to head [(`9785c4f`)](https://app.codecov.io/gh/simonw/datasette/pull/2077?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) 92.40%.
Additional details and impacted files ```diff @@ Coverage Diff @@ ## main #2077 +/- ## ======================================= Coverage 92.40% 92.40% ======================================= Files 39 39 Lines 5803 5803 ======================================= Hits 5362 5362 Misses 441 441 ```
[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/simonw/datasette/pull/2077?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). :loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1719759468,Bump furo from 2023.3.27 to 2023.5.20, https://github.com/simonw/sqlite-utils/pull/553#issuecomment-1556288300,https://api.github.com/repos/simonw/sqlite-utils/issues/553,1556288300,IC_kwDOCGYnMM5cwxMs,9599,simonw,2023-05-21T20:48:01Z,2023-05-21T20:48:01Z,OWNER,If https://sqlite-utils--553.org.readthedocs.build/en/553/cli.html#running-sql-queries looks good I can merge this.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1718635018,Reformatted CLI examples in docs, https://github.com/simonw/sqlite-utils/pull/553#issuecomment-1556287870,https://api.github.com/repos/simonw/sqlite-utils/issues/553,1556287870,IC_kwDOCGYnMM5cwxF-,22429695,codecov[bot],2023-05-21T20:45:58Z,2023-05-21T20:57:08Z,NONE,"## [Codecov](https://app.codecov.io/gh/simonw/sqlite-utils/pull/553?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) Report Patch and project coverage have no change. > Comparison is base [(`e240133`)](https://app.codecov.io/gh/simonw/sqlite-utils/commit/e240133b11588d31dc22c632f7a7ca636c72978d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) 96.36% compared to head [(`0b81794`)](https://app.codecov.io/gh/simonw/sqlite-utils/pull/553?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) 96.36%. > :exclamation: Current head 0b81794 differs from pull request most recent head 21036a5. Consider uploading reports for the commit 21036a5 to get more accurate results
Additional details and impacted files ```diff @@ Coverage Diff @@ ## main #553 +/- ## ======================================= Coverage 96.36% 96.36% ======================================= Files 6 6 Lines 2726 2726 ======================================= Hits 2627 2627 Misses 99 99 ```
[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/simonw/sqlite-utils/pull/553?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). :loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1718635018,Reformatted CLI examples in docs, https://github.com/simonw/sqlite-utils/issues/552#issuecomment-1556292204,https://api.github.com/repos/simonw/sqlite-utils/issues/552,1556292204,IC_kwDOCGYnMM5cwyJs,9599,simonw,2023-05-21T21:05:15Z,2023-05-21T21:05:15Z,OWNER,Now live at https://sqlite-utils.datasette.io/en/latest/installation.html#setting-up-shell-completion,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1718612569,Document how to setup shell auto-completion, https://github.com/simonw/sqlite-utils/issues/551#issuecomment-1556291915,https://api.github.com/repos/simonw/sqlite-utils/issues/551,1556291915,IC_kwDOCGYnMM5cwyFL,9599,simonw,2023-05-21T21:04:03Z,2023-05-21T21:04:03Z,OWNER,Now live at https://sqlite-utils.datasette.io/en/latest/cli.html,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1718607907,Make as many examples in the CLI docs as possible copy-and-pastable, https://github.com/simonw/sqlite-utils/issues/551#issuecomment-1556288270,https://api.github.com/repos/simonw/sqlite-utils/issues/551,1556288270,IC_kwDOCGYnMM5cwxMO,9599,simonw,2023-05-21T20:47:51Z,2023-05-21T20:47:51Z,OWNER,This page has all of the changes: https://sqlite-utils--553.org.readthedocs.build/en/553/cli.html#running-sql-queries,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1718607907,Make as many examples in the CLI docs as possible copy-and-pastable, https://github.com/simonw/sqlite-utils/issues/551#issuecomment-1556287599,https://api.github.com/repos/simonw/sqlite-utils/issues/551,1556287599,IC_kwDOCGYnMM5cwxBv,9599,simonw,2023-05-21T20:44:55Z,2023-05-21T20:44:55Z,OWNER,"Put this in a PR so I can preview it: - #553 ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1718607907,Make as many examples in the CLI docs as possible copy-and-pastable, https://github.com/simonw/sqlite-utils/issues/551#issuecomment-1556265772,https://api.github.com/repos/simonw/sqlite-utils/issues/551,1556265772,IC_kwDOCGYnMM5cwrss,9599,simonw,2023-05-21T19:16:15Z,2023-05-21T19:16:15Z,OWNER,"Another option: That's using this markup: ``` Newline-delimited JSON ~~~~~~~~~~~~~~~~~~~~~~ Use ``--nl`` to get back newline-delimited JSON objects: .. code-block:: bash sqlite-utils dogs.db ""select * from dogs"" --nl .. code-block:: output {""id"": 1, ""age"": 4, ""name"": ""Cleo""} {""id"": 2, ""age"": 2, ""name"": ""Pancakes""} ``` And this extra CSS: ```css .highlight-output .highlight { border-left: 9px solid #30c94f; } ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1718607907,Make as many examples in the CLI docs as possible copy-and-pastable, https://github.com/simonw/sqlite-utils/issues/551#issuecomment-1556263182,https://api.github.com/repos/simonw/sqlite-utils/issues/551,1556263182,IC_kwDOCGYnMM5cwrEO,9599,simonw,2023-05-21T19:06:48Z,2023-05-21T19:06:48Z,OWNER,"I could split them up into two blocks like this: I do miss the visual indication that one of these is the command and one is the output though.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1718607907,Make as many examples in the CLI docs as possible copy-and-pastable, https://github.com/simonw/sqlite-utils/issues/551#issuecomment-1556262574,https://api.github.com/repos/simonw/sqlite-utils/issues/551,1556262574,IC_kwDOCGYnMM5cwq6u,9599,simonw,2023-05-21T19:04:59Z,2023-05-21T19:04:59Z,OWNER,"I wrote the docs like this because early examples include both the command and its output: https://sqlite-utils.datasette.io/en/stable/cli.html#returning-json ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1718607907,Make as many examples in the CLI docs as possible copy-and-pastable, https://github.com/simonw/sqlite-utils/issues/550#issuecomment-1556255309,https://api.github.com/repos/simonw/sqlite-utils/issues/550,1556255309,IC_kwDOCGYnMM5cwpJN,9599,simonw,2023-05-21T18:42:25Z,2023-05-21T18:42:25Z,OWNER,Tests passed here: https://github.com/simonw/sqlite-utils/actions/runs/5039119716,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1718595700,AttributeError: 'EntryPoints' object has no attribute 'get' for flake8 on Python 3.7, https://github.com/simonw/sqlite-utils/issues/550#issuecomment-1556250236,https://api.github.com/repos/simonw/sqlite-utils/issues/550,1556250236,IC_kwDOCGYnMM5cwn58,9599,simonw,2023-05-21T18:25:26Z,2023-05-21T18:25:26Z,OWNER,"Relevant issues: - https://github.com/python/importlib_metadata/issues/406 - https://github.com/PyCQA/flake8/issues/1701 It looks to me like this is only a problem for `flake8` on Python 3.7 - 3.8 and higher work OK.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1718595700,AttributeError: 'EntryPoints' object has no attribute 'get' for flake8 on Python 3.7, https://github.com/simonw/sqlite-utils/issues/550#issuecomment-1556249984,https://api.github.com/repos/simonw/sqlite-utils/issues/550,1556249984,IC_kwDOCGYnMM5cwn2A,9599,simonw,2023-05-21T18:24:48Z,2023-05-21T18:24:48Z,OWNER,"This is blocking: - #549","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1718595700,AttributeError: 'EntryPoints' object has no attribute 'get' for flake8 on Python 3.7, https://github.com/simonw/sqlite-utils/pull/549#issuecomment-1556242262,https://api.github.com/repos/simonw/sqlite-utils/issues/549,1556242262,IC_kwDOCGYnMM5cwl9W,9599,simonw,2023-05-21T18:00:05Z,2023-05-21T18:00:05Z,OWNER,"Failing `mypy` test: https://github.com/simonw/sqlite-utils/actions/runs/5038983349/jobs/9036828465 ``` sqlite_utils/cli.py:37: error: Skipping analyzing ""trogon"": module is installed, but missing library stubs or py.typed marker [import] sqlite_utils/cli.py:37: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports Found 1 error in 1 file (checked 52 source files) ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1718586377,TUI powered by Trogon, https://github.com/simonw/sqlite-utils/pull/549#issuecomment-1556241812,https://api.github.com/repos/simonw/sqlite-utils/issues/549,1556241812,IC_kwDOCGYnMM5cwl2U,9599,simonw,2023-05-21T17:58:25Z,2023-05-21T17:58:25Z,OWNER,Documentation: https://sqlite-utils--549.org.readthedocs.build/en/549/cli.html#cli-tui,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1718586377,TUI powered by Trogon, https://github.com/simonw/sqlite-utils/pull/549#issuecomment-1556241555,https://api.github.com/repos/simonw/sqlite-utils/issues/549,1556241555,IC_kwDOCGYnMM5cwlyT,22429695,codecov[bot],2023-05-21T17:57:24Z,2023-05-21T18:28:44Z,NONE,"## [Codecov](https://app.codecov.io/gh/simonw/sqlite-utils/pull/549?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) Report Patch coverage: **`83.33`**% and project coverage change: **`+0.06`** :tada: > Comparison is base [(`b3b100d`)](https://app.codecov.io/gh/simonw/sqlite-utils/commit/b3b100d7f5b2a76ccd4bfe8b0301a29e321d0375?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) 96.30% compared to head [(`948692a`)](https://app.codecov.io/gh/simonw/sqlite-utils/pull/549?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) 96.36%.
Additional details and impacted files ```diff @@ Coverage Diff @@ ## main #549 +/- ## ========================================== + Coverage 96.30% 96.36% +0.06% ========================================== Files 6 6 Lines 2707 2726 +19 ========================================== + Hits 2607 2627 +20 + Misses 100 99 -1 ``` | [Impacted Files](https://app.codecov.io/gh/simonw/sqlite-utils/pull/549?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) | Coverage Δ | | |---|---|---| | [sqlite\_utils/cli.py](https://app.codecov.io/gh/simonw/sqlite-utils/pull/549?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-c3FsaXRlX3V0aWxzL2NsaS5weQ==) | `95.22% <83.33%> (-0.03%)` | :arrow_down: | ... and [1 file with indirect coverage changes](https://app.codecov.io/gh/simonw/sqlite-utils/pull/549/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison)
[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/simonw/sqlite-utils/pull/549?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). :loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1718586377,TUI powered by Trogon, https://github.com/simonw/sqlite-utils/issues/548#issuecomment-1556231832,https://api.github.com/repos/simonw/sqlite-utils/issues/548,1556231832,IC_kwDOCGYnMM5cwjaY,9599,simonw,2023-05-21T17:24:13Z,2023-05-21T17:24:13Z,OWNER,"Oh, I see why that is now: https://github.com/simonw/sqlite-utils/blob/6027f3ea6939a399aeef2578fca17efec0e539df/sqlite_utils/cli.py#L2670-L2679 This is because of the following command: sqlite-utils analyze-tables table1 table2 --column x Since you can pass multiple tables AND multiple columns, the tool currently assumes that the column(s) you specify may be available on a subset of the provided tables. I'm going to change this so if the column is not on ANY of those tables you get an error.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1718576761,analyze-tables should validate provide --column names, https://github.com/simonw/sqlite-utils/issues/547#issuecomment-1556228395,https://api.github.com/repos/simonw/sqlite-utils/issues/547,1556228395,IC_kwDOCGYnMM5cwikr,9599,simonw,2023-05-21T17:11:15Z,2023-05-21T17:11:15Z,OWNER,"This will be a cosmetic change to the CLI output only - the options to save data to the database and the Python API function will continue to return `[(None, 158)]`. I can add an optimization though to avoid running the SQL count query if we know that it's all `null`.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1718572201,No need to show common values if everything is null, https://github.com/simonw/sqlite-utils/pull/546#issuecomment-1556213396,https://api.github.com/repos/simonw/sqlite-utils/issues/546,1556213396,IC_kwDOCGYnMM5cwe6U,9599,simonw,2023-05-21T15:58:12Z,2023-05-21T16:18:46Z,OWNER,"Documentation preview: - https://sqlite-utils--546.org.readthedocs.build/en/546/cli.html#cli-analyze-tables - https://sqlite-utils--546.org.readthedocs.build/en/546/cli-reference.html#analyze-tables - https://sqlite-utils--546.org.readthedocs.build/en/546/python-api.html#analyzing-a-column - https://sqlite-utils--546.org.readthedocs.build/en/546/reference.html#sqlite_utils.db.Table.analyze_column","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1718550688,"Analyze tables options: --common-limit, --no-most, --no-least", https://github.com/simonw/sqlite-utils/pull/546#issuecomment-1556213031,https://api.github.com/repos/simonw/sqlite-utils/issues/546,1556213031,IC_kwDOCGYnMM5cwe0n,22429695,codecov[bot],2023-05-21T15:56:05Z,2023-05-21T16:18:03Z,NONE,"## [Codecov](https://app.codecov.io/gh/simonw/sqlite-utils/pull/546?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) Report Patch coverage: **`93.75`**% and no project coverage change. > Comparison is base [(`b3b100d`)](https://app.codecov.io/gh/simonw/sqlite-utils/commit/b3b100d7f5b2a76ccd4bfe8b0301a29e321d0375?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) 96.30% compared to head [(`9f23e68`)](https://app.codecov.io/gh/simonw/sqlite-utils/pull/546?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) 96.31%. > :exclamation: Current head 9f23e68 differs from pull request most recent head 2eca17d. Consider uploading reports for the commit 2eca17d to get more accurate results
Additional details and impacted files ```diff @@ Coverage Diff @@ ## main #546 +/- ## ======================================= Coverage 96.30% 96.31% ======================================= Files 6 6 Lines 2707 2712 +5 ======================================= + Hits 2607 2612 +5 Misses 100 100 ``` | [Impacted Files](https://app.codecov.io/gh/simonw/sqlite-utils/pull/546?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) | Coverage Δ | | |---|---|---| | [sqlite\_utils/db.py](https://app.codecov.io/gh/simonw/sqlite-utils/pull/546?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-c3FsaXRlX3V0aWxzL2RiLnB5) | `97.37% <90.90%> (+<0.01%)` | :arrow_up: | | [sqlite\_utils/cli.py](https://app.codecov.io/gh/simonw/sqlite-utils/pull/546?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-c3FsaXRlX3V0aWxzL2NsaS5weQ==) | `95.26% <100.00%> (+0.01%)` | :arrow_up: |
[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/simonw/sqlite-utils/pull/546?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). :loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1718550688,"Analyze tables options: --common-limit, --no-most, --no-least", https://github.com/simonw/sqlite-utils/issues/545#issuecomment-1556269616,https://api.github.com/repos/simonw/sqlite-utils/issues/545,1556269616,IC_kwDOCGYnMM5cwsow,9599,simonw,2023-05-21T19:33:13Z,2023-05-21T19:33:13Z,OWNER,Now released: https://sqlite-utils.datasette.io/en/stable/changelog.html#v3-32,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1718517882,Try out Trogon for a tui interface, https://github.com/simonw/sqlite-utils/issues/545#issuecomment-1556247818,https://api.github.com/repos/simonw/sqlite-utils/issues/545,1556247818,IC_kwDOCGYnMM5cwnUK,9599,simonw,2023-05-21T18:17:46Z,2023-05-21T18:17:46Z,OWNER,Draft documentation: https://sqlite-utils--549.org.readthedocs.build/en/549/cli.html#cli-tui,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1718517882,Try out Trogon for a tui interface, https://github.com/simonw/sqlite-utils/issues/545#issuecomment-1556210844,https://api.github.com/repos/simonw/sqlite-utils/issues/545,1556210844,IC_kwDOCGYnMM5cweSc,9599,simonw,2023-05-21T15:44:10Z,2023-05-21T15:44:10Z,OWNER,"It looks like `nargs=-1` on a positional argument isn't yet supported - opened an issue here: - https://github.com/Textualize/trogon/issues/4","{""total_count"": 1, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 1, ""rocket"": 0, ""eyes"": 0}",1718517882,Try out Trogon for a tui interface, https://github.com/simonw/sqlite-utils/issues/545#issuecomment-1556191894,https://api.github.com/repos/simonw/sqlite-utils/issues/545,1556191894,IC_kwDOCGYnMM5cwZqW,9599,simonw,2023-05-21T14:20:14Z,2023-05-21T14:20:14Z,OWNER,"Opened a feature request for customizing the help and command name: - https://github.com/Textualize/trogon/issues/2","{""total_count"": 2, ""+1"": 1, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 1, ""rocket"": 0, ""eyes"": 0}",1718517882,Try out Trogon for a tui interface, https://github.com/simonw/sqlite-utils/issues/545#issuecomment-1556190531,https://api.github.com/repos/simonw/sqlite-utils/issues/545,1556190531,IC_kwDOCGYnMM5cwZVD,9599,simonw,2023-05-21T14:13:43Z,2023-05-21T14:13:43Z,OWNER,"OK, this works! ![trogon](https://github.com/simonw/sqlite-utils/assets/9599/2ae194c5-ec82-471a-9d1b-b01b3f2632f3) To try it out, install that branch from GitHub: pip install https://github.com/simonw/sqlite-utils/archive/refs/heads/trogon.zip Then run this: sqlite-utils install trogon And this: sqlite-utils tui ","{""total_count"": 5, ""+1"": 2, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 3, ""eyes"": 0}",1718517882,Try out Trogon for a tui interface, https://github.com/simonw/sqlite-utils/issues/545#issuecomment-1556189823,https://api.github.com/repos/simonw/sqlite-utils/issues/545,1556189823,IC_kwDOCGYnMM5cwZJ_,9599,simonw,2023-05-21T14:09:59Z,2023-05-21T14:09:59Z,OWNER,"I don't want to add `trogon` as a default dependency because it's a little heavy - it pulls in all of Rich and Textual as well. People who use `sqlite-utils` just for its Python API won't benefit from this - it's a CLI feature only. But I have a `sqlite-utils install ...` command for helping people to install packages into the same virtual environment as `sqlite-utils` no matter how they installed that tool: https://sqlite-utils.datasette.io/en/stable/cli.html#cli-install So I can treat Trogon as an optional dependency and add the `sqlite-utils tui` command only if that package is also installed.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1718517882,Try out Trogon for a tui interface, https://github.com/simonw/sqlite-utils/issues/544#issuecomment-1556225788,https://api.github.com/repos/simonw/sqlite-utils/issues/544,1556225788,IC_kwDOCGYnMM5cwh78,9599,simonw,2023-05-21T17:02:05Z,2023-05-21T17:02:05Z,OWNER,"New docs: - https://sqlite-utils.datasette.io/en/latest/cli.html#cli-analyze-tables - https://sqlite-utils.datasette.io/en/latest/cli-reference.html#analyze-tables - https://sqlite-utils.datasette.io/en/latest/python-api.html#analyzing-a-column - https://sqlite-utils.datasette.io/en/latest/reference.html#sqlite_utils.db.Table.analyze_column New help output: ``` % sqlite-utils analyze-tables --help Usage: sqlite-utils analyze-tables [OPTIONS] PATH [TABLES]... Analyze the columns in one or more tables Example: sqlite-utils analyze-tables data.db trees Options: -c, --column TEXT Specific columns to analyze --save Save results to _analyze_tables table --common-limit INTEGER How many common values --no-most Skip most common values --no-least Skip least common values --load-extension TEXT Path to SQLite extension, with optional :entrypoint -h, --help Show this message and exit. ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1718515590,New options for analyze-tables --common-limit --no-most and --no-least, https://github.com/simonw/sqlite-utils/issues/544#issuecomment-1556211643,https://api.github.com/repos/simonw/sqlite-utils/issues/544,1556211643,IC_kwDOCGYnMM5cwee7,9599,simonw,2023-05-21T15:48:17Z,2023-05-21T15:48:17Z,OWNER,I generated the commit message in https://github.com/simonw/sqlite-utils/commit/1c1991b447a1ddd3d61d9d4a8a1d6a9da47ced20 using `git diff | llm --system 'describe this change'`.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1718515590,New options for analyze-tables --common-limit --no-most and --no-least, https://github.com/simonw/datasette/pull/2075#issuecomment-1547944971,https://api.github.com/repos/simonw/datasette/issues/2075,1547944971,IC_kwDOBm6k_c5cQ8QL,22429695,codecov[bot],2023-05-15T14:12:20Z,2023-05-15T14:12:20Z,NONE,"## [Codecov](https://app.codecov.io/gh/simonw/datasette/pull/2075?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) Report Patch and project coverage have no change. > Comparison is base [(`49184c5`)](https://app.codecov.io/gh/simonw/datasette/commit/49184c569cd70efbda4f3f062afef3a34401d8d5?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) 92.40% compared to head [(`b99e1d3`)](https://app.codecov.io/gh/simonw/datasette/pull/2075?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) 92.40%.
Additional details and impacted files ```diff @@ Coverage Diff @@ ## main #2075 +/- ## ======================================= Coverage 92.40% 92.40% ======================================= Files 38 38 Lines 5751 5751 ======================================= Hits 5314 5314 Misses 437 437 ```
[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/simonw/datasette/pull/2075?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). :loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1710164693,Bump sphinx from 6.1.3 to 7.0.1, https://github.com/simonw/datasette/issues/2073#issuecomment-1546119773,https://api.github.com/repos/simonw/datasette/issues/2073,1546119773,IC_kwDOBm6k_c5cJ-pd,9599,simonw,2023-05-12T18:24:07Z,2023-05-12T18:24:07Z,OWNER,"Here's a demo of this breaking in Datasette Lite: https://lite.datasette.io/?sql=https://gist.github.com/simonw/261564c0ca01567df6eeb9b222b8be84&json=https%3A%2F%2Fcdn.jsdelivr.net%2Fnpm%2Fweb-features%2Findex.json#/data/baseline?_filter_column_1=is_baseline&_filter_op_1=exact&_filter_value_1=1&_filter_column_2=&_filter_op_2=notnull__1&_filter_value_2=1&_filter_column=&_filter_op=exact&_filter_value=&_sort=&_facet=is_baseline Here's a SQL query that demonstrates the underlying issue: ```sql select 'working', count(*) from baseline where is_baseline = 1 union all select 'broken', count(*) from baseline where is_baseline = '1' ``` https://lite.datasette.io/?sql=https://gist.github.com/simonw/261564c0ca01567df6eeb9b222b8be84&json=https%3A%2F%2Fcdn.jsdelivr.net%2Fnpm%2Fweb-features%2Findex.json#/data?sql=select+%27working%27%2C+count%28*%29+from+baseline+where+is_baseline+%3D+1%0Aunion+all%0Aselect+%27broken%27%2C+count%28*%29+from+baseline+where+is_baseline+%3D+%271%27 ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1708030220,Faceting doesn't work against integer columns in views, https://github.com/simonw/datasette/issues/2073#issuecomment-1546117538,https://api.github.com/repos/simonw/datasette/issues/2073,1546117538,IC_kwDOBm6k_c5cJ-Gi,9599,simonw,2023-05-12T18:21:38Z,2023-05-12T18:21:38Z,OWNER,"https://latest.datasette.io/fixtures doesn't currently have a view with any integer columns in it, making this bug harder to demonstrate there. I can't replicate the bug using https://datasette.io/content/plugins?_facet=stargazers_count&stargazers_count=3 - I would expect that not to work correctly.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1708030220,Faceting doesn't work against integer columns in views, https://github.com/simonw/datasette/issues/2070#issuecomment-1540494121,https://api.github.com/repos/simonw/datasette/issues/2070,1540494121,IC_kwDOBm6k_c5b0hMp,9599,simonw,2023-05-09T16:25:00Z,2023-05-09T16:25:00Z,OWNER,Can now be used here: https://github.com/simonw/datasette/actions/workflows/deploy-branch-preview.yml,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1702354223,Mechanism for deploying a preview of a branch using Vercel, https://github.com/simonw/datasette/issues/2070#issuecomment-1540491751,https://api.github.com/repos/simonw/datasette/issues/2070,1540491751,IC_kwDOBm6k_c5b0gnn,9599,simonw,2023-05-09T16:23:12Z,2023-05-09T16:23:12Z,OWNER,Added a actions `BRANCH_PREVIEW_VERCEL_TOKEN` secret to this repository.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1702354223,Mechanism for deploying a preview of a branch using Vercel, https://github.com/simonw/sqlite-utils/issues/542#issuecomment-1539052467,https://api.github.com/repos/simonw/sqlite-utils/issues/542,1539052467,IC_kwDOCGYnMM5bvBOz,9599,simonw,2023-05-08T21:07:41Z,2023-05-08T21:07:41Z,OWNER,"Relevant commits (will mostly revert these): - https://github.com/simonw/sqlite-utils/commit/455c35b512895c19bf922c2b804d750d27cb8dbd - https://github.com/simonw/sqlite-utils/commit/e0ec4c345129996011951e400388fd74865f65a2","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1700936245,Remove `skip_false=True` and `--no-skip-false` in `sqlite-utils` 4.0, https://github.com/simonw/sqlite-utils/issues/541#issuecomment-1538963959,https://api.github.com/repos/simonw/sqlite-utils/issues/541,1538963959,IC_kwDOCGYnMM5burn3,9599,simonw,2023-05-08T19:59:34Z,2023-05-08T19:59:34Z,OWNER,"There are 8 failing tests left: ``` ==== short test summary info ==== FAILED tests/test_cli_memory.py::test_memory_csv[False-test] - pytest.PytestUnraisableExceptionWarning: Exception ignored in: <_io.FileIO [closed]> FAILED tests/test_cli_memory.py::test_memory_csv[False-t] - pytest.PytestUnraisableExceptionWarning: Exception ignored in: <_io.FileIO [closed]> FAILED tests/test_cli_memory.py::test_memory_csv[False-t1] - pytest.PytestUnraisableExceptionWarning: Exception ignored in: <_io.FileIO [closed]> FAILED tests/test_cli_memory.py::test_memory_tsv[False] - pytest.PytestUnraisableExceptionWarning: Exception ignored in: <_io.FileIO [closed]> FAILED tests/test_cli_memory.py::test_memory_dump[extra_args0] - pytest.PytestUnraisableExceptionWarning: Exception ignored in: <_io.FileIO [closed]> FAILED tests/test_cli_memory.py::test_memory_two_files_with_same_stem - pytest.PytestUnraisableExceptionWarning: Exception ignored in: <_io.FileIO [closed]> FAILED tests/test_recipes.py::test_dateparse_errors[None-parsedate] - pytest.PytestUnraisableExceptionWarning: Exception ignored in: .convert_value at 0x106bcaca0> FAILED tests/test_recipes.py::test_dateparse_errors[None-parsedatetime] - pytest.PytestUnraisableExceptionWarning: Exception ignored in: .convert_value at 0x106bc9620> ERROR tests/test_cli.py::test_upsert_analyze - pytest.PytestUnraisableExceptionWarning: Exception ignored in: <_io.FileIO [closed]> ==== 8 failed, 894 passed, 4 skipped, 1 error in 6.27s ==== ``` Full traceback here: https://gist.github.com/simonw/b40b3e814729d6c02a0302a84ce54d9e","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1700840265,Get tests to pass with `pytest -Werror`, https://github.com/simonw/sqlite-utils/issues/540#issuecomment-1537744000,https://api.github.com/repos/simonw/sqlite-utils/issues/540,1537744000,IC_kwDOCGYnMM5bqByA,42327,pquentin,2023-05-08T04:56:12Z,2023-05-08T04:56:12Z,NONE,"Hey @simonw, urllib3 maintainer here :wave: Sorry for breaking your CI. I understand you may prefer to pin the Python version, but note that specifying just `python: ""3""` will get you the latest. We use that in urllib3: https://github.com/urllib3/urllib3/blob/main/.readthedocs.yml I can open PRs to sqlite-utils / datasette if you're interested","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1699184583,sphinx.builders.linkcheck build error, https://github.com/simonw/sqlite-utils/issues/540#issuecomment-1537514069,https://api.github.com/repos/simonw/sqlite-utils/issues/540,1537514069,IC_kwDOCGYnMM5bpJpV,9599,simonw,2023-05-07T18:40:18Z,2023-05-07T18:40:18Z,OWNER,"https://docs.readthedocs.io/en/stable/config-file/v2.html suggests: ```yaml build: os: ubuntu-22.04 tools: python: ""3.11"" ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1699184583,sphinx.builders.linkcheck build error, https://github.com/simonw/sqlite-utils/issues/540#issuecomment-1537513912,https://api.github.com/repos/simonw/sqlite-utils/issues/540,1537513912,IC_kwDOCGYnMM5bpJm4,9599,simonw,2023-05-07T18:39:29Z,2023-05-07T18:39:29Z,OWNER,"https://readthedocs.org/projects/sqlite-utils/builds/20513034/ said: > Problem in your project's configuration. Invalid ""python.version"": expected one of (2, 2.7, 3, 3.5, 3.6, 3.7, 3.8, pypy3.5), got 3.11","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1699184583,sphinx.builders.linkcheck build error, https://github.com/simonw/sqlite-utils/issues/540#issuecomment-1537513653,https://api.github.com/repos/simonw/sqlite-utils/issues/540,1537513653,IC_kwDOCGYnMM5bpJi1,9599,simonw,2023-05-07T18:37:59Z,2023-05-07T18:38:19Z,OWNER,"Useful comment here: - https://github.com/urllib3/urllib3/issues/2168#issuecomment-1537360928 > I faced the same issue. I switched to a different Python kernel (3.11.2) and it worked.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1699184583,sphinx.builders.linkcheck build error, https://github.com/simonw/sqlite-utils/issues/539#issuecomment-1537514610,https://api.github.com/repos/simonw/sqlite-utils/issues/539,1537514610,IC_kwDOCGYnMM5bpJxy,9599,simonw,2023-05-07T18:43:24Z,2023-05-07T18:43:24Z,OWNER,"Documentation: - https://sqlite-utils.datasette.io/en/latest/cli.html#returning-raw-data-such-as-binary-content - https://sqlite-utils.datasette.io/en/latest/cli-reference.html#query - https://sqlite-utils.datasette.io/en/latest/cli-reference.html#memory","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1699174055,"`--raw-lines` option, like `--raw` for multiple lines", https://github.com/simonw/sqlite-utils/issues/539#issuecomment-1537507676,https://api.github.com/repos/simonw/sqlite-utils/issues/539,1537507676,IC_kwDOCGYnMM5bpIFc,9599,simonw,2023-05-07T18:09:43Z,2023-05-07T18:09:54Z,OWNER,"This worked: ```bash sqlite-utils memory /tmp/books3.json:nl \ 'select name from books3' --raw-lines > titles.txt ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1699174055,"`--raw-lines` option, like `--raw` for multiple lines", https://github.com/simonw/sqlite-utils/issues/539#issuecomment-1537507525,https://api.github.com/repos/simonw/sqlite-utils/issues/539,1537507525,IC_kwDOCGYnMM5bpIDF,9599,simonw,2023-05-07T18:09:09Z,2023-05-07T18:09:09Z,OWNER,"I'm tempted to upgrade `--raw` to do this instead, but that would be a breaking change.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1699174055,"`--raw-lines` option, like `--raw` for multiple lines", https://github.com/simonw/sqlite-utils/issues/539#issuecomment-1537507394,https://api.github.com/repos/simonw/sqlite-utils/issues/539,1537507394,IC_kwDOCGYnMM5bpIBC,9599,simonw,2023-05-07T18:08:44Z,2023-05-07T18:08:44Z,OWNER,"Prototype: ```diff diff --git a/docs/cli-reference.rst b/docs/cli-reference.rst index 153e5f9..c830518 100644 --- a/docs/cli-reference.rst +++ b/docs/cli-reference.rst @@ -124,6 +124,7 @@ See :ref:`cli_query`. --json-cols Detect JSON cols and output them as JSON, not escaped strings -r, --raw Raw output, first column of first row + --raw-lines Raw output, first column of each row -p, --param ... Named :parameters for SQL query --functions TEXT Python code defining one or more custom SQL functions @@ -192,6 +193,7 @@ See :ref:`cli_memory`. --json-cols Detect JSON cols and output them as JSON, not escaped strings -r, --raw Raw output, first column of first row + --raw-lines Raw output, first column of each row -p, --param ... Named :parameters for SQL query --encoding TEXT Character encoding for CSV input, defaults to utf-8 diff --git a/sqlite_utils/cli.py b/sqlite_utils/cli.py index d25b1df..da0e4b6 100644 --- a/sqlite_utils/cli.py +++ b/sqlite_utils/cli.py @@ -1653,6 +1653,7 @@ def drop_view(path, view, ignore, load_extension): ) @output_options @click.option(""-r"", ""--raw"", is_flag=True, help=""Raw output, first column of first row"") +@click.option(""--raw-lines"", is_flag=True, help=""Raw output, first column of each row"") @click.option( ""-p"", ""--param"", @@ -1677,6 +1678,7 @@ def query( fmt, json_cols, raw, + raw_lines, param, load_extension, functions, @@ -1700,7 +1702,19 @@ def query( _register_functions(db, functions) _execute_query( - db, sql, param, raw, table, csv, tsv, no_headers, fmt, nl, arrays, json_cols + db, + sql, + param, + raw, + raw_lines, + table, + csv, + tsv, + no_headers, + fmt, + nl, + arrays, + json_cols, ) @@ -1728,6 +1742,7 @@ def query( ) @output_options @click.option(""-r"", ""--raw"", is_flag=True, help=""Raw output, first column of first row"") +@click.option(""--raw-lines"", is_flag=True, help=""Raw output, first column of each row"") @click.option( ""-p"", ""--param"", @@ -1773,6 +1788,7 @@ def memory( fmt, json_cols, raw, + raw_lines, param, encoding, no_detect_types, @@ -1879,12 +1895,36 @@ def memory( _register_functions(db, functions) _execute_query( - db, sql, param, raw, table, csv, tsv, no_headers, fmt, nl, arrays, json_cols + db, + sql, + param, + raw, + raw_lines, + table, + csv, + tsv, + no_headers, + fmt, + nl, + arrays, + json_cols, ) def _execute_query( - db, sql, param, raw, table, csv, tsv, no_headers, fmt, nl, arrays, json_cols + db, + sql, + param, + raw, + raw_lines, + table, + csv, + tsv, + no_headers, + fmt, + nl, + arrays, + json_cols, ): with db.conn: try: @@ -1903,6 +1943,13 @@ def _execute_query( sys.stdout.buffer.write(data) else: sys.stdout.write(str(data)) + elif raw_lines: + for row in cursor: + data = row[0] + if isinstance(data, bytes): + sys.stdout.buffer.write(data + b""\n"") + else: + sys.stdout.write(str(data) + ""\n"") elif fmt or table: print( tabulate.tabulate( ``` Needs tests and more documentation.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1699174055,"`--raw-lines` option, like `--raw` for multiple lines", https://github.com/simonw/datasette/issues/2069#issuecomment-1537277919,https://api.github.com/repos/simonw/datasette/issues/2069,1537277919,IC_kwDOBm6k_c5boP_f,31861128,yqlbu,2023-05-07T03:17:35Z,2023-05-07T03:17:35Z,NONE,"Some updates: I notice that there is an option in the CLI where we can explicitly set `immutable` mode when spinning up the server ```console Options: -i, --immutable PATH Database files to open in immutable mode ``` Then, the question is - how can I disable immutable mode in the deployed instance on Vercel?","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1698865182,[BUG] Cannot insert new data to deployed instance, https://github.com/simonw/sqlite-utils/issues/538#issuecomment-1538975545,https://api.github.com/repos/simonw/sqlite-utils/issues/538,1538975545,IC_kwDOCGYnMM5buuc5,1231935,xavdid,2023-05-08T20:06:35Z,2023-05-08T20:06:35Z,NONE,"perfect, thank you!","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1695428235,`table.upsert_all` fails to write rows when `not_null` is present, https://github.com/simonw/sqlite-utils/issues/538#issuecomment-1538921774,https://api.github.com/repos/simonw/sqlite-utils/issues/538,1538921774,IC_kwDOCGYnMM5buhUu,9599,simonw,2023-05-08T19:24:41Z,2023-05-08T19:24:41Z,OWNER,That fix seems to work!,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1695428235,`table.upsert_all` fails to write rows when `not_null` is present, https://github.com/simonw/sqlite-utils/issues/538#issuecomment-1538910894,https://api.github.com/repos/simonw/sqlite-utils/issues/538,1538910894,IC_kwDOCGYnMM5buequ,9599,simonw,2023-05-08T19:16:52Z,2023-05-08T19:17:00Z,OWNER,"How about if I had logic which checked that all not-null columns were provided in the call to `upsert_all()` - and if they were, modified the `INSERT OR IGNORE INTO` to include a placeholder value for those columns that would then be fixed by the later `UPDATE`? Something like this: ```python [ ('INSERT OR IGNORE INTO [comments]([id], name) VALUES(?, ?);', [1, '']), ('UPDATE [comments] SET [name] = ? WHERE [id] = ?', ['Cleo', 1]) ] ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1695428235,`table.upsert_all` fails to write rows when `not_null` is present, https://github.com/simonw/sqlite-utils/issues/538#issuecomment-1538903556,https://api.github.com/repos/simonw/sqlite-utils/issues/538,1538903556,IC_kwDOCGYnMM5buc4E,9599,simonw,2023-05-08T19:11:24Z,2023-05-08T19:13:23Z,OWNER,"I could detect if this happens using `cursor.rowcount` - not sure how I would recover from it though. This would also require some major re-engineering, since currently it all works by generating a list of SQL queries in advance and applying them inside a loop in `.insert_chunk()`: https://github.com/simonw/sqlite-utils/blob/80763edaa2bdaf1113717378b8d62075c4dcbcfb/sqlite_utils/db.py#L2839-L2878 ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1695428235,`table.upsert_all` fails to write rows when `not_null` is present, https://github.com/simonw/sqlite-utils/issues/538#issuecomment-1538893329,https://api.github.com/repos/simonw/sqlite-utils/issues/538,1538893329,IC_kwDOCGYnMM5buaYR,9599,simonw,2023-05-08T19:04:47Z,2023-05-08T19:04:47Z,OWNER,"This feels like a fundamental flaw in the way upserts are implemented by `sqlite-utils`. One fix would be to switch to using the `UPSERT` feature in SQLite: https://www.sqlite.org/lang_UPSERT.html But... > UPSERT syntax was added to SQLite with version 3.24.0 (2018-06-04). I still want to support SQLite versions earlier than that.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1695428235,`table.upsert_all` fails to write rows when `not_null` is present, https://github.com/simonw/sqlite-utils/issues/538#issuecomment-1538889482,https://api.github.com/repos/simonw/sqlite-utils/issues/538,1538889482,IC_kwDOCGYnMM5buZcK,9599,simonw,2023-05-08T19:02:38Z,2023-05-08T19:02:38Z,OWNER,"Here's the code at fault: https://github.com/simonw/sqlite-utils/blob/80763edaa2bdaf1113717378b8d62075c4dcbcfb/sqlite_utils/db.py#L2774-L2788","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1695428235,`table.upsert_all` fails to write rows when `not_null` is present, https://github.com/simonw/sqlite-utils/issues/538#issuecomment-1538887361,https://api.github.com/repos/simonw/sqlite-utils/issues/538,1538887361,IC_kwDOCGYnMM5buY7B,9599,simonw,2023-05-08T19:01:20Z,2023-05-08T19:01:20Z,OWNER,"Here's the problem: ```python import sqlite3 db = sqlite3.connect("":memory:"") db.execute('create table foo (id integer primary key, name not null)') db.execute('insert into foo (id) values (1)') ``` Produces: ``` IntegrityError: NOT NULL constraint failed: foo.name ``` But this: ```python db.execute('insert or ignore into foo (id) values (1)') ``` Completes without an exception.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1695428235,`table.upsert_all` fails to write rows when `not_null` is present, https://github.com/simonw/sqlite-utils/issues/538#issuecomment-1538801855,https://api.github.com/repos/simonw/sqlite-utils/issues/538,1538801855,IC_kwDOCGYnMM5buEC_,9599,simonw,2023-05-08T18:00:17Z,2023-05-08T18:00:17Z,OWNER,"From time in the debugger, after creating the table it ends up doing this: ``` (Pdb) queries_and_params [ ('INSERT OR IGNORE INTO [comments]([id]) VALUES(?);', [1]), ('UPDATE [comments] SET [name] = ? WHERE [id] = ?', ['Cleo', 1]) ] ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1695428235,`table.upsert_all` fails to write rows when `not_null` is present, https://github.com/simonw/sqlite-utils/issues/538#issuecomment-1538793817,https://api.github.com/repos/simonw/sqlite-utils/issues/538,1538793817,IC_kwDOCGYnMM5buCFZ,9599,simonw,2023-05-08T17:55:10Z,2023-05-08T17:55:10Z,OWNER,"Confirmed - I added this test and it fails: ```python def test_upsert_all_not_null(fresh_db): # https://github.com/simonw/sqlite-utils/issues/538 fresh_db[""comments""].upsert_all( [{""id"": 1, ""name"": ""Cleo""}], pk=""id"", not_null=[""name""], ) assert list(fresh_db[""comments""].rows) == [{""id"": 1, ""name"": ""Cleo""}] ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1695428235,`table.upsert_all` fails to write rows when `not_null` is present, https://github.com/simonw/datasette/pull/2068#issuecomment-1547911570,https://api.github.com/repos/simonw/datasette/issues/2068,1547911570,IC_kwDOBm6k_c5cQ0GS,49699333,dependabot[bot],2023-05-15T13:59:35Z,2023-05-15T13:59:35Z,CONTRIBUTOR,Superseded by #2075.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1690842199,Bump sphinx from 6.1.3 to 7.0.0, https://github.com/simonw/datasette/issues/2067#issuecomment-1532304714,https://api.github.com/repos/simonw/datasette/issues/2067,1532304714,IC_kwDOBm6k_c5bVR1K,39538958,justmars,2023-05-03T00:16:03Z,2023-05-03T00:16:03Z,NONE,"Curiously, after running commands on the database that was litestream-restored, datasette starts to work again, e.g. ```sh litestream restore -o data/db.sqlite s3://mytestbucketxx/db datasette data/db.sqlite # fails (OperationalError described above) ``` ```sh litestream restore -o data/db.sqlite s3://mytestbucketxx/db sqlite-utils enable-wal data/db.sqlite datasette data/db.sqlite # works ``` ```sh litestream restore -o data/db.sqlite s3://mytestbucketxx/db sqlite-utils optimize data/db.sqlite datasette data/db.sqlite # works ``` ```sh litestream restore -o data/db.sqlite s3://mytestbucketxx/db sqlite3 data/db.sqlite "".clone test.db"" datasette test.db # works ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1690765434,Litestream-restored db: errors on 3.11 and 3.10.8; but works on py3.10.7 and 3.10.6, https://github.com/simonw/datasette/issues/2066#issuecomment-1524680160,https://api.github.com/repos/simonw/datasette/issues/2066,1524680160,IC_kwDOBm6k_c5a4MXg,9599,simonw,2023-04-27T04:27:50Z,2023-04-27T04:27:50Z,OWNER,That fixed it.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1686042269,Failing test: httpx.InvalidURL: URL too long, https://github.com/simonw/datasette/issues/2066#issuecomment-1524675817,https://api.github.com/repos/simonw/datasette/issues/2066,1524675817,IC_kwDOBm6k_c5a4LTp,9599,simonw,2023-04-27T04:21:03Z,2023-04-27T04:21:03Z,OWNER,I went with this to generate the long string: https://github.com/simonw/datasette/blob/0b0c5cd7a94fe3f151a3e10261b5c84ee64f2f18/tests/test_csv.py#L157-L176,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1686042269,Failing test: httpx.InvalidURL: URL too long, https://github.com/simonw/datasette/issues/2066#issuecomment-1524669124,https://api.github.com/repos/simonw/datasette/issues/2066,1524669124,IC_kwDOBm6k_c5a4JrE,9599,simonw,2023-04-27T04:10:44Z,2023-04-27T04:10:52Z,OWNER,"I need an alternative way of generating a long string with a shorter URL. ```sql select group_concat('abcabcabc', '') from json_each(json_array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)) ``` https://latest.datasette.io/_memory?sql=select+group_concat%28%27abcabcabc%27%2C+%27%27%29+from+json_each%28json_array%281%2C+2%2C+3%2C+4%2C+5%2C+6%2C+7%2C+8%2C+9%2C+10%29%29","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1686042269,Failing test: httpx.InvalidURL: URL too long, https://github.com/simonw/datasette/issues/2066#issuecomment-1524666049,https://api.github.com/repos/simonw/datasette/issues/2066,1524666049,IC_kwDOBm6k_c5a4I7B,9599,simonw,2023-04-27T04:06:18Z,2023-04-27T04:06:18Z,OWNER,"Most recent `httpx` release is 0.24 a couple of weeks ago. Here's what changed in that version: https://github.com/encode/httpx/compare/0.23.3...0.24.0 It looks like that URL limit is new: https://github.com/encode/httpx/commit/57daabf673705954afa94686c0002801c93d31f3#diff-78d8d93b5dd4c77d99c3e2b46b7286ba71a8fd60e92d8bd4eee5fb200b4f87bfR149-R155 ```python def urlparse(url: str = """", **kwargs: typing.Optional[str]) -> ParseResult: # Initial basic checks on allowable URLs. # --------------------------------------- # Hard limit the maximum allowable URL length. if len(url) > MAX_URL_LENGTH: raise InvalidURL(""URL too long"") ``` https://github.com/encode/httpx/blob/32e25497a36e6222cc64a758c98275b450dac28d/httpx/_urlparse.py#L153-L155","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1686042269,Failing test: httpx.InvalidURL: URL too long, https://github.com/simonw/datasette/issues/2066#issuecomment-1524660603,https://api.github.com/repos/simonw/datasette/issues/2066,1524660603,IC_kwDOBm6k_c5a4Hl7,9599,simonw,2023-04-27T04:02:55Z,2023-04-27T04:02:55Z,OWNER,"In the debugger: ``` (Pdb) MAX_URL_LENGTH 65536 ``` Weird this only seems to be a problem with `httpx` on Python 3.7 though.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1686042269,Failing test: httpx.InvalidURL: URL too long, https://github.com/simonw/datasette/issues/2066#issuecomment-1524659084,https://api.github.com/repos/simonw/datasette/issues/2066,1524659084,IC_kwDOBm6k_c5a4HOM,9599,simonw,2023-04-27T04:02:07Z,2023-04-27T04:02:07Z,OWNER,"This is the failing test: https://github.com/simonw/datasette/blob/249fcf8e3e2a90e763f41b080c1b9ec8017f5005/tests/test_csv.py#L156-L167","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1686042269,Failing test: httpx.InvalidURL: URL too long, https://github.com/simonw/datasette/issues/2066#issuecomment-1524655203,https://api.github.com/repos/simonw/datasette/issues/2066,1524655203,IC_kwDOBm6k_c5a4GRj,9599,simonw,2023-04-27T03:59:56Z,2023-04-27T03:59:56Z,OWNER,"In a fresh Datasette checkout I ran: pipenv shell --python /Users/simon/.pyenv/versions/3.7.16/bin/python That gave me a virtual environment with 3.7.16 Python. Then I ran: pip install -e '.[test]' Weirdly that gave me a `which pytest` of `/opt/homebrew/bin/pytest` which ran the tests on 3.11. I figured out the location of the virtual environment with `which python` and then ran this: ``` % /Users/simon/.local/share/virtualenvs/datasette-cZYvnUqY/bin/pytest tests/test_csv.py ============================================================================================== test session starts =============================================================================================== platform darwin -- Python 3.7.16, pytest-7.3.1, pluggy-1.0.0 SQLite: 3.39.5 rootdir: /private/tmp/datasette configfile: pytest.ini plugins: asyncio-0.21.0, timeout-2.1.0, xdist-3.2.1, anyio-3.6.2 asyncio: mode=strict collected 15 items tests/test_csv.py ..........F.... [100%] ==================================================================================================== FAILURES ==================================================================================================== ________________________________________________________________________________________________ test_max_csv_mb _________________________________________________________________________________________________ app_client_csv_max_mb_one = def test_max_csv_mb(app_client_csv_max_mb_one): response = app_client_csv_max_mb_one.get( ( ""/fixtures.csv?sql=select+'{}'+"" ""from+compound_three_primary_keys&_stream=1&_size=max"" > ).format(""abcdefg"" * 10000) ) /private/tmp/datasette/tests/test_csv.py:161: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ /Users/simon/.local/share/virtualenvs/datasette-cZYvnUqY/lib/python3.7/site-packages/asgiref/sync.py:240: in __call__ return call_result.result() /Users/simon/.pyenv/versions/3.7.16/lib/python3.7/concurrent/futures/_base.py:428: in result return self.__get_result() /Users/simon/.pyenv/versions/3.7.16/lib/python3.7/concurrent/futures/_base.py:384: in __get_result raise self._exception /Users/simon/.local/share/virtualenvs/datasette-cZYvnUqY/lib/python3.7/site-packages/asgiref/sync.py:306: in main_wrap result = await self.awaitable(*args, **kwargs) /private/tmp/datasette/datasette/utils/testing.py:76: in get headers=headers, /private/tmp/datasette/datasette/utils/testing.py:167: in _request content=post_body, /private/tmp/datasette/datasette/app.py:1787: in request method, self._fix(path, avoid_path_rewrites), **kwargs /Users/simon/.local/share/virtualenvs/datasette-cZYvnUqY/lib/python3.7/site-packages/httpx/_client.py:1528: in request extensions=extensions, /Users/simon/.local/share/virtualenvs/datasette-cZYvnUqY/lib/python3.7/site-packages/httpx/_client.py:346: in build_request url = self._merge_url(url) /Users/simon/.local/share/virtualenvs/datasette-cZYvnUqY/lib/python3.7/site-packages/httpx/_client.py:376: in _merge_url merge_url = URL(url) /Users/simon/.local/share/virtualenvs/datasette-cZYvnUqY/lib/python3.7/site-packages/httpx/_urls.py:113: in __init__ self._uri_reference = urlparse(url, **kwargs) _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ url = ""http://localhost/fixtures.csv?sql=select+'abcdefgabcdefgabcdefgabcdefgabcdefgabcdefgabcdefgabcdefgabcdefgabcdefgabcde...gabcdefgabcdefgabcdefgabcdefgabcdefgabcdefgabcdefgabcdefgabcdefg'+from+compound_three_primary_keys&_stream=1&_size=max"" kwargs = {} def urlparse(url: str = """", **kwargs: typing.Optional[str]) -> ParseResult: # Initial basic checks on allowable URLs. # --------------------------------------- # Hard limit the maximum allowable URL length. if len(url) > MAX_URL_LENGTH: > raise InvalidURL(""URL too long"") E httpx.InvalidURL: URL too long /Users/simon/.local/share/virtualenvs/datasette-cZYvnUqY/lib/python3.7/site-packages/httpx/_urlparse.py:155: InvalidURL ============================================================================================ short test summary info ============================================================================================= FAILED tests/test_csv.py::test_max_csv_mb - httpx.InvalidURL: URL too long ========================================================================================== 1 failed, 14 passed in 1.83s ========================================================================================== (datasette) simon@Simons-MacBook-Pro datasette % ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1686042269,Failing test: httpx.InvalidURL: URL too long, https://github.com/simonw/datasette/issues/2066#issuecomment-1524648995,https://api.github.com/repos/simonw/datasette/issues/2066,1524648995,IC_kwDOBm6k_c5a4Ewj,9599,simonw,2023-04-27T03:56:42Z,2023-04-27T03:57:11Z,OWNER,"I don't have 3.7 locally. Trying to install it with `pyenv`. brew install pyenv Then: pyenv install --list | grep 3.7 Installing: pyenv install 3.7.16 Output: Installed Python-3.7.16 to /Users/simon/.pyenv/versions/3.7.16 So the executable is `/Users/simon/.pyenv/versions/3.7.16/bin/python`.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1686042269,Failing test: httpx.InvalidURL: URL too long, https://github.com/simonw/datasette/issues/2066#issuecomment-1524638233,https://api.github.com/repos/simonw/datasette/issues/2066,1524638233,IC_kwDOBm6k_c5a4CIZ,9599,simonw,2023-04-27T03:50:51Z,2023-04-27T03:50:51Z,OWNER,Failure was on 3.7. I'll try that.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1686042269,Failing test: httpx.InvalidURL: URL too long, https://github.com/simonw/datasette/issues/2066#issuecomment-1524637376,https://api.github.com/repos/simonw/datasette/issues/2066,1524637376,IC_kwDOBm6k_c5a4B7A,9599,simonw,2023-04-27T03:50:19Z,2023-04-27T03:50:19Z,OWNER,"Having trouble replicating this on my laptop. I tried a fresh virtual environment with fresh packages (in case this is a `httpx` change) but this passed: pytest tests/test_csv.py","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1686042269,Failing test: httpx.InvalidURL: URL too long, https://github.com/simonw/datasette/issues/2065#issuecomment-1524709988,https://api.github.com/repos/simonw/datasette/issues/2065,1524709988,IC_kwDOBm6k_c5a4Tpk,9599,simonw,2023-04-27T05:09:36Z,2023-04-27T05:09:36Z,OWNER,"That fixed it - after installing `main.zip` again I ran this and it worked: ~/.rye/tools/main-zip/bin/datasette install datasette-graphql ``` % ~/.rye/tools/main-zip/bin/datasette plugins [ { ""name"": ""datasette-graphql"", ""static"": true, ""templates"": true, ""version"": ""2.2"", ""hooks"": [ ""database_actions"", ""extra_template_vars"", ""menu_links"", ""register_routes"", ""startup"", ""table_actions"" ] } ] ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1686033652,Datasette cannot be installed with Rye, https://github.com/simonw/datasette/issues/2065#issuecomment-1524707628,https://api.github.com/repos/simonw/datasette/issues/2065,1524707628,IC_kwDOBm6k_c5a4TEs,9599,simonw,2023-04-27T05:06:44Z,2023-04-27T05:06:44Z,OWNER,"I need `pip` as a dependency too: ``` % ~/.rye/tools/main-zip/bin/datasette install datasette-graphql Traceback (most recent call last): File ""/Users/simon/.rye/tools/main-zip/bin/datasette"", line 8, in sys.exit(cli()) ^^^^^ File ""/Users/simon/.rye/tools/main-zip/lib/python3.11/site-packages/click/core.py"", line 1130, in __call__ return self.main(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^ File ""/Users/simon/.rye/tools/main-zip/lib/python3.11/site-packages/click/core.py"", line 1055, in main rv = self.invoke(ctx) ^^^^^^^^^^^^^^^^ File ""/Users/simon/.rye/tools/main-zip/lib/python3.11/site-packages/click/core.py"", line 1657, in invoke return _process_result(sub_ctx.command.invoke(sub_ctx)) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File ""/Users/simon/.rye/tools/main-zip/lib/python3.11/site-packages/click/core.py"", line 1404, in invoke return ctx.invoke(self.callback, **ctx.params) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File ""/Users/simon/.rye/tools/main-zip/lib/python3.11/site-packages/click/core.py"", line 760, in invoke return __callback(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File ""/Users/simon/.rye/tools/main-zip/lib/python3.11/site-packages/datasette/cli.py"", line 365, in install run_module(""pip"", run_name=""__main__"") File """", line 222, in run_module File """", line 142, in _get_module_details ImportError: No module named pip ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1686033652,Datasette cannot be installed with Rye, https://github.com/simonw/datasette/issues/2065#issuecomment-1524699863,https://api.github.com/repos/simonw/datasette/issues/2065,1524699863,IC_kwDOBm6k_c5a4RLX,9599,simonw,2023-04-27T04:56:22Z,2023-04-27T04:56:22Z,OWNER,Turned this into a TIL: https://til.simonwillison.net/python/rye,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1686033652,Datasette cannot be installed with Rye, https://github.com/simonw/datasette/issues/2065#issuecomment-1524616740,https://api.github.com/repos/simonw/datasette/issues/2065,1524616740,IC_kwDOBm6k_c5a384k,9599,simonw,2023-04-27T03:38:21Z,2023-04-27T03:38:21Z,OWNER,"Tried this: rye install https://github.com/simonw/datasette/archive/refs/heads/main.zip But got this error: Error: Expected one of `@`, `(`, `<`, `=`, `>`, `~`, `!`, `;`, found `:` I instead downloaded that file and ran: rye install main.zip This worked! And now: ``` ~/.rye/tools/main-zip/bin/datasette --version ``` ``` datasette, version 1.0a2 ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1686033652,Datasette cannot be installed with Rye, https://github.com/simonw/datasette/pull/2064#issuecomment-1529737426,https://api.github.com/repos/simonw/datasette/issues/2064,1529737426,IC_kwDOBm6k_c5bLfDS,49699333,dependabot[bot],2023-05-01T13:58:50Z,2023-05-01T13:58:50Z,CONTRIBUTOR,Superseded by #2068.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1683229834,Bump sphinx from 6.1.3 to 6.2.1, https://github.com/simonw/datasette/pull/2063#issuecomment-1521837780,https://api.github.com/repos/simonw/datasette/issues/2063,1521837780,IC_kwDOBm6k_c5atWbU,49699333,dependabot[bot],2023-04-25T13:57:52Z,2023-04-25T13:57:52Z,CONTRIBUTOR,Superseded by #2064.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1681339696,Bump sphinx from 6.1.3 to 6.2.0, https://github.com/simonw/sqlite-utils/pull/537#issuecomment-1539157643,https://api.github.com/repos/simonw/sqlite-utils/issues/537,1539157643,IC_kwDOCGYnMM5bva6L,9599,simonw,2023-05-08T22:45:09Z,2023-05-08T22:45:21Z,OWNER,"Here's an example from the new tests: https://github.com/simonw/sqlite-utils/blob/a75abeb61b91a28650d3b9933e7ec80ad0d92529/tests/test_create.py#L291-L307","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1665200812,Support self-referencing FKs in `Table.create`, https://github.com/simonw/sqlite-utils/pull/537#issuecomment-1539055393,https://api.github.com/repos/simonw/sqlite-utils/issues/537,1539055393,IC_kwDOCGYnMM5bvB8h,9599,simonw,2023-05-08T21:10:06Z,2023-05-08T21:10:06Z,OWNER,Thanks!,"{""total_count"": 1, ""+1"": 1, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1665200812,Support self-referencing FKs in `Table.create`, https://github.com/simonw/sqlite-utils/pull/537#issuecomment-1506200813,https://api.github.com/repos/simonw/sqlite-utils/issues/537,1506200813,IC_kwDOCGYnMM5Zxszt,22429695,codecov[bot],2023-04-13T01:45:22Z,2023-04-13T01:45:22Z,NONE,"## [Codecov](https://codecov.io/gh/simonw/sqlite-utils/pull/537?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) Report Patch coverage: **`100.00`**% and no project coverage change. > Comparison is base [(`c0251cc`)](https://codecov.io/gh/simonw/sqlite-utils/commit/c0251cc9271260de73b4227859a51fab9b4cb745?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) 96.25% compared to head [(`a75abeb`)](https://codecov.io/gh/simonw/sqlite-utils/pull/537?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) 96.25%.
Additional details and impacted files ```diff @@ Coverage Diff @@ ## main #537 +/- ## ======================================= Coverage 96.25% 96.25% ======================================= Files 6 6 Lines 2671 2673 +2 ======================================= + Hits 2571 2573 +2 Misses 100 100 ``` | [Impacted Files](https://codecov.io/gh/simonw/sqlite-utils/pull/537?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) | Coverage Δ | | |---|---|---| | [sqlite\_utils/db.py](https://codecov.io/gh/simonw/sqlite-utils/pull/537?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-c3FsaXRlX3V0aWxzL2RiLnB5) | `97.33% <100.00%> (+<0.01%)` | :arrow_up: | Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison)
[:umbrella: View full report in Codecov by Sentry](https://codecov.io/gh/simonw/sqlite-utils/pull/537?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). :loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1665200812,Support self-referencing FKs in `Table.create`, https://github.com/simonw/datasette/issues/2059#issuecomment-1506174353,https://api.github.com/repos/simonw/datasette/issues/2059,1506174353,IC_kwDOBm6k_c5ZxmWR,9599,simonw,2023-04-13T01:13:00Z,2023-04-13T01:13:00Z,OWNER,"Can you provide a URL to an example, and/or a screenshot of this? Is it a browser warning or is it a warning from Heroku itself?","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1665053646,"""Deceptive site ahead"" alert on Heroku deployment", https://github.com/simonw/datasette/issues/2058#issuecomment-1507264934,https://api.github.com/repos/simonw/datasette/issues/2058,1507264934,IC_kwDOBm6k_c5Z1wmm,1138559,esagara,2023-04-13T16:35:21Z,2023-04-13T16:35:21Z,NONE,"I tried deploying the fix you submitted, but still getting the same errors. I can past the logs here if needed, but I really don't see anything new in them.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1663399821,"500 ""attempt to write a readonly database"" error caused by ""PRAGMA schema_version""", https://github.com/simonw/datasette/issues/2058#issuecomment-1506203550,https://api.github.com/repos/simonw/datasette/issues/2058,1506203550,IC_kwDOBm6k_c5Zxtee,547438,cephillips,2023-04-13T01:48:21Z,2023-04-13T01:48:21Z,NONE,Really interesting how you are using ChatGPT in this.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1663399821,"500 ""attempt to write a readonly database"" error caused by ""PRAGMA schema_version""", https://github.com/simonw/datasette/issues/2058#issuecomment-1504426792,https://api.github.com/repos/simonw/datasette/issues/2058,1504426792,IC_kwDOBm6k_c5Zq7so,9599,simonw,2023-04-12T02:02:42Z,2023-04-12T02:02:42Z,OWNER,"I tightened up the benchmark (it was measuring the time taken to create the tables too) and got this: ![image](https://user-images.githubusercontent.com/9599/231328328-85ca35ac-a11b-46f4-b132-dae367103570.png) ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1663399821,"500 ""attempt to write a readonly database"" error caused by ""PRAGMA schema_version""", https://github.com/simonw/datasette/issues/2058#issuecomment-1504328395,https://api.github.com/repos/simonw/datasette/issues/2058,1504328395,IC_kwDOBm6k_c5ZqjrL,9599,simonw,2023-04-12T00:28:38Z,2023-04-12T00:28:38Z,OWNER,"Here's a much better chart, which shows that MD5 performance unsurprisingly gets worse as the number of tables increases while `schema_version` remains constant: ![image](https://user-images.githubusercontent.com/9599/231316778-513bd99f-5ea4-495c-b86d-c572a7106369.png) ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1663399821,"500 ""attempt to write a readonly database"" error caused by ""PRAGMA schema_version""", https://github.com/simonw/datasette/issues/2058#issuecomment-1504315697,https://api.github.com/repos/simonw/datasette/issues/2058,1504315697,IC_kwDOBm6k_c5Zqgkx,9599,simonw,2023-04-12T00:16:22Z,2023-04-12T00:27:12Z,OWNER,"I got ChatGPT (code execution alpha) to run a micro-benchmark for me. This was the conclusion: > The benchmark using `PRAGMA schema_version` is approximately 1.36 times faster than the benchmark using `hashlib.md5` for the case with 100 tables. For the case with 200 tables, the benchmark using `PRAGMA schema_version` is approximately 2.33 times faster than the benchmark using `hashlib.md5`. Here's the chart it drew me: ![image](https://user-images.githubusercontent.com/9599/231315366-3a12b6d3-08d7-419d-a1fd-36eb24da0d85.png) (It's a pretty rubbish chart though, it only took measurements at 100 and 200 and drew a line between the two, I should have told it to measure every 10 and plot that) And the full transcript: https://gist.github.com/simonw/2fc46effbfbe49e6de0bcfdc9e31b235 The benchmark looks good enough on first glance that I don't feel the need to be more thorough with it. `PRAGMA schema_version` is faster, but not so fast that I feel like the MD5 hack is worth worrying about too much. I'm tempted to add something to the `/-/versions` page that tries to identify if this is a problem or not though.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1663399821,"500 ""attempt to write a readonly database"" error caused by ""PRAGMA schema_version""", https://github.com/simonw/datasette/issues/2058#issuecomment-1504298448,https://api.github.com/repos/simonw/datasette/issues/2058,1504298448,IC_kwDOBm6k_c5ZqcXQ,9599,simonw,2023-04-12T00:04:01Z,2023-04-12T00:04:01Z,OWNER,"Here's a potential workaround: when I store the schema versions, I could also score an MD5 hash of the full schema (`select group_concat(sql) from sqlite_master`). When I read the schema version with `PRAGMA schema_version` I could catch that exception and, if I see it, I could calculate that MD5 hash again as a fallback and use that to determine if the schema has changed instead. The performance overhead of this needs investigating - how much more expensive is `md5(... that SQL query result)` compared to just `PRAGMA schema_version`, especially on a database with a lot of tables?","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1663399821,"500 ""attempt to write a readonly database"" error caused by ""PRAGMA schema_version""", https://github.com/simonw/datasette/issues/2058#issuecomment-1504295345,https://api.github.com/repos/simonw/datasette/issues/2058,1504295345,IC_kwDOBm6k_c5Zqbmx,9599,simonw,2023-04-12T00:01:42Z,2023-04-12T00:02:26Z,OWNER,"Here's the relevant code: https://github.com/simonw/datasette/blob/5890a20c374fb0812d88c9b0ef26a838bfa06c76/datasette/app.py#L421-L437 This function is called on almost every request (everything that subclasses `BaseView` at least - need to remember that for the refactor in #2053 etc). https://github.com/simonw/datasette/blob/5890a20c374fb0812d88c9b0ef26a838bfa06c76/datasette/views/base.py#L101-L103 It uses `PRAGMA schema_version` as a cheap way to determine if the schema has changed, in which case it needs to refresh the internal schema tables. This was already the cause of a subtle bug here: - #1231 ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1663399821,"500 ""attempt to write a readonly database"" error caused by ""PRAGMA schema_version""", https://github.com/simonw/datasette/issues/2058#issuecomment-1504292145,https://api.github.com/repos/simonw/datasette/issues/2058,1504292145,IC_kwDOBm6k_c5Zqa0x,9599,simonw,2023-04-11T23:58:59Z,2023-04-11T23:58:59Z,OWNER,Asked on the SQLite Forum if anyone has seen this before: https://sqlite.org/forum/forumpost/793a2ed75b,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1663399821,"500 ""attempt to write a readonly database"" error caused by ""PRAGMA schema_version""", https://github.com/simonw/datasette/issues/2058#issuecomment-1504291892,https://api.github.com/repos/simonw/datasette/issues/2058,1504291892,IC_kwDOBm6k_c5Zqaw0,9599,simonw,2023-04-11T23:58:45Z,2023-04-11T23:58:45Z,OWNER,"I thought it might relate to the ""defensive mode"" issue described here: - https://github.com/simonw/sqlite-utils/issues/235 But I have since determined that the Datasette official Docker image does NOT run anything in defensive mode, so I don't think it's related to that.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1663399821,"500 ""attempt to write a readonly database"" error caused by ""PRAGMA schema_version""", https://github.com/simonw/datasette/issues/2057#issuecomment-1503838640,https://api.github.com/repos/simonw/datasette/issues/2057,1503838640,IC_kwDOBm6k_c5ZosGw,9599,simonw,2023-04-11T17:48:23Z,2023-04-11T17:48:23Z,OWNER,"> This looks wrong to me - I would expect something like `is_directory()` not `is_file()` for telling if `static/` is a directory. I was right about that: ```pycon >>> importlib.resources.files('datasette_graphql') PosixPath('/Users/simon/.local/share/virtualenvs/datasette-big-local-6Yn-280V/lib/python3.11/site-packages/datasette_graphql') >>> importlib.resources.files('datasette_graphql').joinpath(""static"") PosixPath('/Users/simon/.local/share/virtualenvs/datasette-big-local-6Yn-280V/lib/python3.11/site-packages/datasette_graphql/static') >>> p = importlib.resources.files('datasette_graphql').joinpath(""static"") >>> p PosixPath('/Users/simon/.local/share/virtualenvs/datasette-big-local-6Yn-280V/lib/python3.11/site-packages/datasette_graphql/static') >>> p.is_ p.is_absolute() p.is_char_device() p.is_fifo() p.is_mount() p.is_reserved() p.is_symlink() p.is_block_device() p.is_dir() p.is_file() p.is_relative_to( p.is_socket() >>> p.is_dir() True >>> p.is_file() False ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1662951875,DeprecationWarning: pkg_resources is deprecated as an API, https://github.com/simonw/datasette/issues/2057#issuecomment-1503833906,https://api.github.com/repos/simonw/datasette/issues/2057,1503833906,IC_kwDOBm6k_c5Zoq8y,9599,simonw,2023-04-11T17:44:16Z,2023-04-11T17:45:45Z,OWNER,"Another prompt: > How to fix this: > > `pkg_resources.get_distribution(package).version` Response: ```python import importlib.metadata # Get the version number of the specified package package_version = importlib.metadata.version(package) ``` That seems to work: ```pycon >>> import importlib.metadata >>> importlib.metadata.version(""datasette"") '0.64.2' >>> importlib.metadata.version(""pluggy"") '1.0.0' >>> importlib.metadata.version(""not-a-package"") ... importlib.metadata.PackageNotFoundError: No package metadata was found for not-a-package ``` ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1662951875,DeprecationWarning: pkg_resources is deprecated as an API, https://github.com/simonw/datasette/issues/2057#issuecomment-1503832422,https://api.github.com/repos/simonw/datasette/issues/2057,1503832422,IC_kwDOBm6k_c5Zoqlm,9599,simonw,2023-04-11T17:42:57Z,2023-04-11T17:46:42Z,OWNER,"I ran this prompt against ChatGPT with the Browsing alpha: > ```python > if pkg_resources.resource_isdir(plugin.__name__, ""static""): > static_path = pkg_resources.resource_filename( > plugin.__name__, ""static"" > ) > if pkg_resources.resource_isdir(plugin.__name__, ""templates""): > templates_path = pkg_resources.resource_filename( > plugin.__name__, ""templates"" > ) > ``` > This code gives a deprecation warning in Python 3.11 - fix it It looked up the fix for me: And suggested: ```python import importlib.resources # Replace pkg_resources.resource_isdir with importlib.resources.files().is_file() if importlib.resources.files(plugin.__name__).joinpath(""static"").is_file(): static_path = importlib.resources.as_file( importlib.resources.files(plugin.__name__).joinpath(""static"") ) if importlib.resources.files(plugin.__name__).joinpath(""templates"").is_file(): templates_path = importlib.resources.as_file( importlib.resources.files(plugin.__name__).joinpath(""templates"") ) ``` This looks wrong to me - I would expect something like `is_directory()` not `is_file()` for telling if `static/` is a directory.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1662951875,DeprecationWarning: pkg_resources is deprecated as an API, https://github.com/simonw/datasette/pull/2056#issuecomment-1509852821,https://api.github.com/repos/simonw/datasette/issues/2056,1509852821,IC_kwDOBm6k_c5Z_oaV,3709715,cclauss,2023-04-15T14:24:45Z,2023-04-15T14:24:45Z,NONE,Status?,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1661860507,GitHub Action to lint Python code with ruff, https://github.com/simonw/datasette/pull/2056#issuecomment-1506485287,https://api.github.com/repos/simonw/datasette/issues/2056,1506485287,IC_kwDOBm6k_c5ZyyQn,3709715,cclauss,2023-04-13T07:29:38Z,2023-04-13T07:41:55Z,NONE,"Ruff (written in Rust, not Python) is a 23MB executable so the time to download and pip install it dwarfs its runtime. Let's run ruff with and without GitHub Actions pip cache side-by-side to see the relative performance. Once you approve the workflows below, `ruff_with_cache` should echo `cache-hit = false` but if you rerun that job hopefully it should echo `cache-hit = true`. That will be the execution time that we are interested to compare. There are two great problems in computer science: ;-) 1. Naming things 2. Cache invalidation 3. Off-by-one errors For 2., https://github.com/actions/setup-python#caching-packages-dependencies is vital reading. Only _exactly pinned requirements_ can be cached. Currently in `setup.py` the only pinned dependencies are: 1. Sphinx==6.1.3 2. furo==2023.3.27 3. black==23.3.0 4. blacken-docs==1.13.0 # but unpinned elsewhere in `setup.py` This means that there will be very few cache hits in the current actions. See the link below to print out cache hits: https://github.com/actions/setup-python/blob/main/docs/advanced-usage.md#cache-hit > PyPI uses Fastly's CDN to quickly serve content to end-users, allowing us to minimize our hosting infrastructure and obscure possible downtime. -- https://pypi.org/sponsors I would be shocked if Fastly does not have beefy CDN nodes in the same datacenters where GitHub Actions run so GHA requests to download `ruff` probably never hit a PyPI server. ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1661860507,GitHub Action to lint Python code with ruff, https://github.com/simonw/datasette/pull/2056#issuecomment-1506179555,https://api.github.com/repos/simonw/datasette/issues/2056,1506179555,IC_kwDOBm6k_c5Zxnnj,9599,simonw,2023-04-13T01:21:05Z,2023-04-13T01:22:08Z,OWNER,"OK, I'm sold - this is a really neat improvement. One thing to change in the PR: right now it runs `pip install --user ruff` on every commit, which hits PyPI to install the package. I prefer to avoid hitting PyPI every time, so I like to use the GitHub Actions cache. My usual pattern for that looks like this: https://github.com/simonw/datasette/blob/5890a20c374fb0812d88c9b0ef26a838bfa06c76/.github/workflows/test-pyodide.yml#L16-L20 Then a separate command that runs `pip install ...` will benefit from that cache. Are you OK to make that change?","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1661860507,GitHub Action to lint Python code with ruff, https://github.com/simonw/datasette/pull/2056#issuecomment-1506177857,https://api.github.com/repos/simonw/datasette/issues/2056,1506177857,IC_kwDOBm6k_c5ZxnNB,9599,simonw,2023-04-13T01:18:18Z,2023-04-13T01:18:18Z,OWNER,"Cool - and now https://github.com/simonw/datasette/pull/2056/files is showing me those inline annotations: ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1661860507,GitHub Action to lint Python code with ruff, https://github.com/simonw/datasette/pull/2056#issuecomment-1506177115,https://api.github.com/repos/simonw/datasette/issues/2056,1506177115,IC_kwDOBm6k_c5ZxnBb,9599,simonw,2023-04-13T01:17:16Z,2023-04-13T01:17:16Z,OWNER,"Here are the failures: https://github.com/simonw/datasette/actions/runs/4684460653/jobs/8300630794?pr=2056 ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1661860507,GitHub Action to lint Python code with ruff, https://github.com/simonw/datasette/pull/2056#issuecomment-1506175208,https://api.github.com/repos/simonw/datasette/issues/2056,1506175208,IC_kwDOBm6k_c5Zxmjo,9599,simonw,2023-04-13T01:14:13Z,2023-04-13T01:14:13Z,OWNER,https://github.com/simonw/datasette/actions/runs/4664796647/jobs/8300596121?pr=2056 it's pretty fast - that finished in 9s.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1661860507,GitHub Action to lint Python code with ruff, https://github.com/simonw/sqlite-utils/pull/536#issuecomment-1500893216,https://api.github.com/repos/simonw/sqlite-utils/issues/536,1500893216,IC_kwDOCGYnMM5ZddAg,22429695,codecov[bot],2023-04-08T13:35:42Z,2023-04-08T13:35:42Z,NONE,"## [Codecov](https://codecov.io/gh/simonw/sqlite-utils/pull/536?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) Report Patch and project coverage have no change. > Comparison is base [(`c0251cc`)](https://codecov.io/gh/simonw/sqlite-utils/commit/c0251cc9271260de73b4227859a51fab9b4cb745?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) 96.25% compared to head [(`cea05dc`)](https://codecov.io/gh/simonw/sqlite-utils/pull/536?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) 96.25%.
Additional details and impacted files ```diff @@ Coverage Diff @@ ## main #536 +/- ## ======================================= Coverage 96.25% 96.25% ======================================= Files 6 6 Lines 2671 2671 ======================================= Hits 2571 2571 Misses 100 100 ``` | [Impacted Files](https://codecov.io/gh/simonw/sqlite-utils/pull/536?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) | Coverage Δ | | |---|---|---| | [sqlite\_utils/utils.py](https://codecov.io/gh/simonw/sqlite-utils/pull/536?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-c3FsaXRlX3V0aWxzL3V0aWxzLnB5) | `95.13% <ø> (ø)` | | Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison)
[:umbrella: View full report in Codecov by Sentry](https://codecov.io/gh/simonw/sqlite-utils/pull/536?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). :loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1659525418,Add paths for homebrew on Apple silicon, https://github.com/simonw/datasette/issues/2054#issuecomment-1500608101,https://api.github.com/repos/simonw/datasette/issues/2054,1500608101,IC_kwDOBm6k_c5ZcXZl,9599,simonw,2023-04-07T20:14:38Z,2023-04-07T20:14:38Z,OWNER,"Ooh that one's really interesting - very different from the others: ```ruby # app.rb require ""roda"" class App < Roda route do |r| r.root do ""Home page"" end r.on ""pages"" do r.get "":slug"" do |slug| ""Page: #{slug}"" end end r.on ""news"" do r.get "":yyyy/:mm/:dd"" do |yyyy, mm, dd| ""News for #{yyyy}/#{mm}/#{dd}"" end end end end # config.ru require_relative ""app"" run App.freeze.app ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1657861026,"Make detailed notes on how table, query and row views work right now", https://github.com/simonw/datasette/issues/2054#issuecomment-1499797384,https://api.github.com/repos/simonw/datasette/issues/2054,1499797384,IC_kwDOBm6k_c5ZZReI,6213,dsisnero,2023-04-07T00:46:50Z,2023-04-07T00:46:50Z,NONE,you should have a look at Roda written in ruby . ,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1657861026,"Make detailed notes on how table, query and row views work right now", https://github.com/simonw/datasette/issues/2054#issuecomment-1499604822,https://api.github.com/repos/simonw/datasette/issues/2054,1499604822,IC_kwDOBm6k_c5ZYidW,9599,simonw,2023-04-06T20:48:19Z,2023-04-06T20:48:55Z,OWNER,"I actually quite like that. I could even use `@classmethod` and have utility methods defined on that class that both `get()` and `post()` could call. The crucial rule here is NO INSTANCE STATE - that's what makes routing to classes particularly damaging, and encourages code that's hard to maintain.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1657861026,"Make detailed notes on how table, query and row views work right now", https://github.com/simonw/datasette/issues/2054#issuecomment-1499604066,https://api.github.com/repos/simonw/datasette/issues/2054,1499604066,IC_kwDOBm6k_c5ZYiRi,9599,simonw,2023-04-06T20:47:30Z,2023-04-06T20:47:30Z,OWNER,"I'm contemplating a new approach: using a class with static methods. Something like this: ```python class TableView(MethodRouter): @staticmethod async def get(request): return Response.text(""GET"") @staticmethod async def post(request): return Response.text(""POST"") ``` So effectively the class is just there to bundle together verb implementations, and to provide a `route(request)` method which knows how to dispatch them to the right place. It can offer default HEAD and OPTIONS methods too.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1657861026,"Make detailed notes on how table, query and row views work right now", https://github.com/simonw/datasette/issues/2054#issuecomment-1499596941,https://api.github.com/repos/simonw/datasette/issues/2054,1499596941,IC_kwDOBm6k_c5ZYgiN,9599,simonw,2023-04-06T20:39:35Z,2023-04-06T20:39:35Z,OWNER,"I think there are two main approaches: - Map path to a function, that function then behaves differently on GET/POST/etc - Map method AND path to a function - the `app.get('/example')` v.s. `app.post('/example')` ones The decorator approach is calling out to me a bit now.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1657861026,"Make detailed notes on how table, query and row views work right now", https://github.com/simonw/datasette/issues/2054#issuecomment-1499530921,https://api.github.com/repos/simonw/datasette/issues/2054,1499530921,IC_kwDOBm6k_c5ZYQap,9599,simonw,2023-04-06T19:41:52Z,2023-04-06T19:42:09Z,OWNER,Turned this into a TIL: https://til.simonwillison.net/gpt3/gpt4-api-design,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1657861026,"Make detailed notes on how table, query and row views work right now", https://github.com/simonw/datasette/issues/2054#issuecomment-1499491258,https://api.github.com/repos/simonw/datasette/issues/2054,1499491258,IC_kwDOBm6k_c5ZYGu6,9599,simonw,2023-04-06T19:00:16Z,2023-04-06T20:40:16Z,OWNER,"I asked GPT-4 to compare URL routing based on path and verb approaches in different languages and frameworks and spit out some examples. Full transcript here: https://gist.github.com/simonw/fa2379b97420404a81b0fcdb4db79657 Initial prompt: > Show me code examples of different web frameworks in Python and JavaScript and Go illustrating how HTTP routing works - in particular the problem of mapping an incoming HTTP request to some code based on both the URL path and the HTTP verb > > Don't show installation instructions, just get straight to the code snippets Flask: ```python from flask import Flask app = Flask(__name__) @app.route('/example', methods=['GET']) def get_example(): return ""This is a GET request."" @app.route('/example', methods=['POST']) def post_example(): return ""This is a POST request."" if __name__ == '__main__': app.run() ``` Express: ```javascript const express = require('express'); const app = express(); app.get('/example', (req, res) => { res.send('This is a GET request.'); }); app.post('/example', (req, res) => { res.send('This is a POST request.'); }); app.listen(3000, () => { console.log('Server is running on port 3000'); }); ``` Gin: ```go package main import ( ""github.com/gin-gonic/gin"" ) func main() { r := gin.Default() r.GET(""/example"", func(c *gin.Context) { c.String(200, ""This is a GET request."") }) r.POST(""/example"", func(c *gin.Context) { c.String(200, ""This is a POST request."") }) r.Run() } ``` Actix-web: ```rust use actix_web::{web, App, HttpResponse, HttpServer, Responder}; async fn get_example() -> impl Responder { HttpResponse::Ok().body(""This is a GET request."") } async fn post_example() -> impl Responder { HttpResponse::Ok().body(""This is a POST request."") } #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() .route(""/example"", web::get().to(get_example)) .route(""/example"", web::post().to(post_example)) }) .bind(""127.0.0.1:8080"")? .run() .await } ``` Koa: ```javascript const Koa = require('koa'); const Router = require('koa-router'); const app = new Koa(); const router = new Router(); router.get('/example', (ctx) => { ctx.body = 'This is a GET request.'; }); router.post('/example', (ctx) => { ctx.body = 'This is a POST request.'; }); app.use(router.routes()); app.use(router.allowedMethods()); app.listen(3000, () => { console.log('Server is running on port 3000'); }); ``` Django: ```python from django.http import HttpResponse from django.views import View class ExampleView(View): def get(self, request): return HttpResponse(""This is a GET request."") def post(self, request): return HttpResponse(""This is a POST request."") ``` ```python from django.urls import path from .views import ExampleView urlpatterns = [ path('example/', ExampleView.as_view(), name='example'), ] ``` Rocket: ```rust #![feature(proc_macro_hygiene, decl_macro)] #[macro_use] extern crate rocket; use rocket::response::content; #[get(""/example"")] fn get_example() -> content::Plain<&'static str> { content::Plain(""This is a GET request."") } #[post(""/example"")] fn post_example() -> content::Plain<&'static str> { content::Plain(""This is a POST request."") } #[launch] fn rocket() -> _ { rocket::build().mount(""/"", routes![get_example, post_example]) } ``` Elixir/Phoenix: ```elixir defmodule ExampleAppWeb.Router do use ExampleAppWeb, :router pipeline :api do plug :accepts, [""json""] end scope ""/api"", ExampleAppWeb do pipe_through :api get ""/example"", ExampleController, :get_example post ""/example"", ExampleController, :post_example end end ``` Bottle: ```python from bottle import Bottle, run app = Bottle() @app.route('/example', method='GET') def get_example(): return ""This is a GET request."" @app.route('/example', method='POST') def post_example(): return ""This is a POST request."" if __name__ == '__main__': run(app, host='localhost', port=8080) ``` FastAPI: ```python from fastapi import FastAPI app = FastAPI() @app.get(""/example"") async def get_example(): return {""message"": ""This is a GET request.""} @app.post(""/example"") async def post_example(): return {""message"": ""This is a POST request.""} if __name__ == ""__main__"": import uvicorn uvicorn.run(app, host=""127.0.0.1"", port=8000) ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1657861026,"Make detailed notes on how table, query and row views work right now", https://github.com/simonw/datasette/issues/2054#issuecomment-1499467703,https://api.github.com/repos/simonw/datasette/issues/2054,1499467703,IC_kwDOBm6k_c5ZYA-3,9599,simonw,2023-04-06T18:37:22Z,2023-04-06T18:57:22Z,OWNER,"I've been hoping to move entirely away from class-based views, but now I'm wondering if that's the right decision given the need to support HTTP verbs. It is cleaner to have a class that has methods for each verb, or to have a single function that can behave differently depending on the verb? Or should I have a mechanism for dispatching to separate functions based on the verbs that isn't implemented in a base class? The trouble with using base classes is that I've already shown that they tempt me to put logic in weird places, which makes code harder to modify later on. The thing here is that the ONLY thing I want to use the base class for is as an HTTP verb switcher - which makes me doubt that I should risk taking on the other temptations of having a base class.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1657861026,"Make detailed notes on how table, query and row views work right now", https://github.com/simonw/datasette/issues/2054#issuecomment-1499465648,https://api.github.com/repos/simonw/datasette/issues/2054,1499465648,IC_kwDOBm6k_c5ZYAew,9599,simonw,2023-04-06T18:35:03Z,2023-04-06T18:35:03Z,OWNER,"There are actually five classes that subclass `DataView`: https://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/row.py#L16 https://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/database.py#L34 https://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/database.py#L172 https://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/database.py#L215 https://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/table.py#L72 I don't think `DatabaseView` and `DatabaseDownload` should have subclassed that at all, since they don't return a table of data.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1657861026,"Make detailed notes on how table, query and row views work right now", https://github.com/simonw/datasette/issues/2054#issuecomment-1499462324,https://api.github.com/repos/simonw/datasette/issues/2054,1499462324,IC_kwDOBm6k_c5ZX_q0,9599,simonw,2023-04-06T18:31:56Z,2023-04-06T18:31:56Z,OWNER,"The `DataView` class does a LOT of work - mostly involving CSV responses. https://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/base.py#L160-L544 It has a `redirect()` method with some complex logic and CORS handling: https://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/base.py#L163-L172 It uses this method a lot, which has to be over-ridden in the `TableView` and `RowView` and `QueryView` subclasses: https://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/base.py#L174-L175 This method: https://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/base.py#L180 Is the bulk of the complexity, because it knows how to both turn a list of SQLite rows into a CSV file but also knows how to call `.data()` repeatedly with different pagination arguments in order to stream CSV back for a large table. The `async def get()` method for GET requests is also very complicated. It mainly handles format stuff - knowing how to render HTML v.s. JSON v.s. CSV v.s. other formats specified using this plugin hook: https://docs.datasette.io/en/1.0a2/plugin_hooks.html#register-output-renderer-datasette Plus it catches interrupted queries and returns a special error page for those (and other error messages too): https://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/base.py#L381-L408 It adds the time taken to execute the queries: https://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/base.py#L410-L411","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1657861026,"Make detailed notes on how table, query and row views work right now", https://github.com/simonw/datasette/issues/2054#issuecomment-1499457291,https://api.github.com/repos/simonw/datasette/issues/2054,1499457291,IC_kwDOBm6k_c5ZX-cL,9599,simonw,2023-04-06T18:26:45Z,2023-04-06T18:26:45Z,OWNER," Here's `BaseView`: https://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/base.py#L56-L145 It has methods for the `options`, `get`, `post`, `delete`, `put`, `patch` and `head` HTTP verbs, most defaulting to returinng a 405 Method not allowed message in plain text or JSON, depending on this check: https://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/base.py#L71-L81 Also adds CORS headers to anything if CORS mode is on: https://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/base.py#L106-L107 And adds the `database_color` (weirdly) and the `select_templates` variables to the template context: https://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/base.py#L112-L122 And has special code for setting the `Link: ...; rel=""alternate""` HTTP header: https://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/base.py#L124-L136","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1657861026,"Make detailed notes on how table, query and row views work right now", https://github.com/simonw/datasette/issues/2054#issuecomment-1499457201,https://api.github.com/repos/simonw/datasette/issues/2054,1499457201,IC_kwDOBm6k_c5ZX-ax,9599,simonw,2023-04-06T18:26:39Z,2023-04-06T18:26:39Z,OWNER,These classes - `TableView` and `RowView` and `QueryView` - all subclass `DataView` which subclasses` BaseView`.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1657861026,"Make detailed notes on how table, query and row views work right now", https://github.com/simonw/datasette/issues/2054#issuecomment-1499452122,https://api.github.com/repos/simonw/datasette/issues/2054,1499452122,IC_kwDOBm6k_c5ZX9La,9599,simonw,2023-04-06T18:21:51Z,2023-04-06T18:21:51Z,OWNER,"I'm going to make notes against the code in the most recent alpha release, ignoring the recent work I did to refactor `TableView`. https://github.com/simonw/datasette/tree/1.0a2/datasette/views","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1657861026,"Make detailed notes on how table, query and row views work right now", https://github.com/simonw/datasette/pull/2053#issuecomment-1565058994,https://api.github.com/repos/simonw/datasette/issues/2053,1565058994,IC_kwDOBm6k_c5dSOey,9599,simonw,2023-05-26T23:13:02Z,2023-05-26T23:13:02Z,OWNER,"I should have an extra called `extra_html_context` which bundles together all of the weird extra stuff needed by the HTML template, and is then passed as the root context when the template is rendered (with the other stuff from extras patched into it).","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1656432059,WIP new JSON for queries, https://github.com/simonw/datasette/pull/2053#issuecomment-1563793781,https://api.github.com/repos/simonw/datasette/issues/2053,1563793781,IC_kwDOBm6k_c5dNZl1,9599,simonw,2023-05-26T04:27:55Z,2023-05-26T04:27:55Z,OWNER,"I should split out a `canned_query.html` template too, as something that extends the `query.html` template.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1656432059,WIP new JSON for queries, https://github.com/simonw/datasette/pull/2053#issuecomment-1563667574,https://api.github.com/repos/simonw/datasette/issues/2053,1563667574,IC_kwDOBm6k_c5dM6x2,9599,simonw,2023-05-26T00:40:22Z,2023-05-26T00:40:22Z,OWNER,"Or maybe... - `BaseQueryView(View)` - knows how to render the results of a SQL query - `QueryView(BaseQueryView)` - renders from `?sql=` - `CannedQueryView(BaseQueryView)` - renders for a named canned query And then later perhaps: - `RowQueryView(BaseQueryView)` - renders the `select * from t where pk = ?` - `TableQueryView(BaseQueryView)` - replaces the super complex existing `TableView`","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1656432059,WIP new JSON for queries, https://github.com/simonw/datasette/pull/2053#issuecomment-1563663925,https://api.github.com/repos/simonw/datasette/issues/2053,1563663925,IC_kwDOBm6k_c5dM541,9599,simonw,2023-05-26T00:32:47Z,2023-05-26T00:35:47Z,OWNER,"I'm going to entirely split canned queries off from `?sql=` queries - they share a bunch of code right now which is just making everything much harder to follow. I'll refactor their shared bits into functions that they both call. Or _maybe_ I'll try having `CannedQueryView` as a subclass of `QueryView`.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1656432059,WIP new JSON for queries, https://github.com/simonw/datasette/pull/2053#issuecomment-1563663616,https://api.github.com/repos/simonw/datasette/issues/2053,1563663616,IC_kwDOBm6k_c5dM50A,9599,simonw,2023-05-26T00:32:08Z,2023-05-26T00:32:08Z,OWNER,"Now that I have the new `View` subclass from #2078 I want to use it to simplify this code. Challenge: there are several things to consider here: - The `/db` page without `?sql=` displays a list of tables in that database - With `?sql=` it shows the query results for that query (or an error) - If it's a `/db/name-of-canned-query` it works a bit like the query page, but executes a canned query instead of the `?sql=` query - POST `/db/name-of-canned-query` is support for writable canned queries","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1656432059,WIP new JSON for queries, https://github.com/simonw/datasette/pull/2053#issuecomment-1563285150,https://api.github.com/repos/simonw/datasette/issues/2053,1563285150,IC_kwDOBm6k_c5dLdae,9599,simonw,2023-05-25T17:48:50Z,2023-05-25T17:49:52Z,OWNER,"Uncommitted experimental code: ```diff diff --git a/datasette/views/database.py b/datasette/views/database.py index 455ebd1f..85775433 100644 --- a/datasette/views/database.py +++ b/datasette/views/database.py @@ -909,12 +909,13 @@ async def query_view( elif format_ in datasette.renderers.keys(): # Dispatch request to the correct output format renderer # (CSV is not handled here due to streaming) + print(data) result = call_with_supported_arguments( datasette.renderers[format_][0], datasette=datasette, - columns=columns, - rows=rows, - sql=sql, + columns=data[""rows""][0].keys(), + rows=data[""rows""], + sql='', query_name=None, database=db.name, table=None, @@ -923,7 +924,7 @@ async def query_view( # These will be deprecated in Datasette 1.0: args=request.args, data={ - ""rows"": rows, + ""rows"": data[""rows""], }, # TODO what should this be? ) result = await await_me_maybe(result) diff --git a/docs/index.rst b/docs/index.rst index 5a9cc7ed..254ed3da 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -57,6 +57,7 @@ Contents settings introspection custom_templates + template_context plugins writing_plugins plugin_hooks ``` Where `docs/template_context.rst` looked like this: ```rst .. _template_context: Template context ================ .. currentmodule:: datasette.context This page describes the variables made available to templates used by Datasette to render different pages of the application. .. autoclass:: QueryContext :members: ``` And `datasette/context.py` had this: ```python from dataclasses import dataclass @dataclass class QueryContext: """""" Used by the ``/database`` page when showing the results of a SQL query """""" id: int ""Id is a thing"" rows: list[dict] ""Name is another thing"" ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1656432059,WIP new JSON for queries, https://github.com/simonw/datasette/pull/2053#issuecomment-1498279469,https://api.github.com/repos/simonw/datasette/issues/2053,1498279469,IC_kwDOBm6k_c5ZTe4t,9599,simonw,2023-04-05T23:28:53Z,2023-04-05T23:28:53Z,OWNER,"Table errors page currently does this: ```json { ""ok"": false, ""error"": ""no such column: blah"", ""status"": 400, ""title"": ""Invalid SQL"" } ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1656432059,WIP new JSON for queries, https://github.com/simonw/sqlite-utils/issues/535#issuecomment-1592617271,https://api.github.com/repos/simonw/sqlite-utils/issues/535,1592617271,IC_kwDOCGYnMM5e7Wk3,13780613,erlend-aasland,2023-06-15T08:39:49Z,2023-06-15T08:39:49Z,NONE,"> piping to `jq` is good enough usually ... or `python -m json.tool`[^1], if you don't have `jq` installed. [^1]: no fancy colouring, like `jq`; only pretty-printing","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1655860104,rows: --transpose or psql extended view-like functionality, https://github.com/simonw/sqlite-utils/issues/535#issuecomment-1592052320,https://api.github.com/repos/simonw/sqlite-utils/issues/535,1592052320,IC_kwDOCGYnMM5e5Mpg,7908073,chapmanjacobd,2023-06-14T22:05:28Z,2023-06-14T22:05:28Z,CONTRIBUTOR,piping to `jq` is good enough usually,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1655860104,rows: --transpose or psql extended view-like functionality, https://github.com/simonw/datasette/pull/2052#issuecomment-1632867333,https://api.github.com/repos/simonw/datasette/issues/2052,1632867333,IC_kwDOBm6k_c5hU5QF,22429695,codecov[bot],2023-07-12T16:38:27Z,2023-07-12T16:38:27Z,NONE,"## [Codecov](https://app.codecov.io/gh/simonw/datasette/pull/2052?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) Report Patch coverage has no change and project coverage change: **`-0.02`** :warning: > Comparison is base [(`3feed1f`)](https://app.codecov.io/gh/simonw/datasette/commit/3feed1f66e2b746f349ee56970a62246a18bb164?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) 92.46% compared to head [(`cf5a9df`)](https://app.codecov.io/gh/simonw/datasette/pull/2052?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) 92.45%.
Additional details and impacted files ```diff @@ Coverage Diff @@ ## main #2052 +/- ## ========================================== - Coverage 92.46% 92.45% -0.02% ========================================== Files 38 39 +1 Lines 5750 5802 +52 ========================================== + Hits 5317 5364 +47 - Misses 433 438 +5 ``` [see 6 files with indirect coverage changes](https://app.codecov.io/gh/simonw/datasette/pull/2052/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison)
[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/simonw/datasette/pull/2052?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). :loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1651082214,"feat: Javascript Plugin API (Custom panels, column menu items with JS actions)", https://github.com/simonw/datasette/pull/2052#issuecomment-1630776144,https://api.github.com/repos/simonw/datasette/issues/2052,1630776144,IC_kwDOBm6k_c5hM6tQ,9020979,hydrosquall,2023-07-11T12:54:03Z,2023-07-11T12:54:03Z,NONE,"Thanks for the review and the code pointers @simonw - I've made the suggested edits, fixed the renamed variable, and confirmed that the panels still render on the `table` and `database` views. ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1651082214,"feat: Javascript Plugin API (Custom panels, column menu items with JS actions)", https://github.com/simonw/datasette/pull/2052#issuecomment-1629337927,https://api.github.com/repos/simonw/datasette/issues/2052,1629337927,IC_kwDOBm6k_c5hHblH,9599,simonw,2023-07-10T16:43:38Z,2023-07-10T16:44:23Z,OWNER,"I tried running this locally just now. I made one edit: ```diff diff --git a/demos/plugins/example_js_manager_plugins.py b/demos/plugins/example_js_manager_plugins.py index 7bdb9f3f..f9dfa8e6 100644 --- a/demos/plugins/example_js_manager_plugins.py +++ b/demos/plugins/example_js_manager_plugins.py @@ -15,6 +15,6 @@ def extra_js_urls(view_name): if view_name in PERMITTED_VIEWS: return [ { - ""url"": f""/-/demos/plugins/static/table-example-plugins.js"", + ""url"": f""/static/table-example-plugins.js"", } ] ``` And then started it running like this: ```bash wget https://datasette.io/content.db ``` ```bash datasette content.db \ --plugins-dir demos/plugins \ --static static:datasette/demos/plugins/static ``` It didn't quite work for me - I got this error on a table page: And this error on a query page: ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1651082214,"feat: Javascript Plugin API (Custom panels, column menu items with JS actions)", https://github.com/simonw/datasette/pull/2052#issuecomment-1616095810,https://api.github.com/repos/simonw/datasette/issues/2052,1616095810,IC_kwDOBm6k_c5gU6pC,15178711,asg017,2023-07-01T20:31:31Z,2023-07-01T20:31:31Z,CONTRIBUTOR,"> Just curious, is there a query that can be used to compile this programmatically, or did you identify these through memory? I just did a github search for `user:simonw ""def extra_js_urls(""` ! Though I'm sure other plugins made by people other than Simon also exist out there https://github.com/search?q=user%3Asimonw+%22def+extra_js_urls%28%22&type=code","{""total_count"": 1, ""+1"": 1, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1651082214,"feat: Javascript Plugin API (Custom panels, column menu items with JS actions)", https://github.com/simonw/datasette/pull/2052#issuecomment-1615997736,https://api.github.com/repos/simonw/datasette/issues/2052,1615997736,IC_kwDOBm6k_c5gUiso,9020979,hydrosquall,2023-07-01T16:55:24Z,2023-07-01T16:55:24Z,NONE,"> Ok @hydrosquall a couple things before this PR should be good to go: Thank you @asg017 ! I've pushed both suggested changes onto this branch. > Not sure how difficult it'll be to inject it server-side If we are OK with having a build system, it would free me up to do do many things! We could make datasette-manager.js a server-side rendered file as a ""template"" instead of having it as a static JS file, but I'm not sure it's worth the extra jump in complexity / loss of syntax highlighting in the JS file. In the short-term, I could see an intermediary solution where a unit test in the preferred language was able to read both `version.py` and `datasette-manager.js`, and make sure that the strings versions are in sync. (This assumes that we want the manager and datasette's versions to be synced, and not decoupled). Since the version is not changing very often, a ""manual sync"" might be good enough. > In terms of how to integrate this into Datasette, a few options I can see working: This sounds good to me. I'm not sure how to add a settings flag, but will be interested to see the PR that adds support for it. > I'm also curious to see how ""plugins for a plugin' would work I'm comfortable to wait until we have a realistic usecase for this. In the short term, I think we could give plugins a way to grant access to a ""public API of other plugins"", and also ask to be notified when plugins with other names have loaded, but don't picture the datasette manager getting more involved than that. > here's a list of Simon's Datasette plugins that use ""extra_js_urls()"" Neat, thanks for compiling this list! Just curious, is there a query that can be used to compile this programmatically, or did you identify these through memory? > I want to make a javascript plugin on top of the code-mirror editor to make a few things nicer (function auto-complete, table/column descriptions, etc.) I look forward to trying this out 👍 ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1651082214,"feat: Javascript Plugin API (Custom panels, column menu items with JS actions)", https://github.com/simonw/datasette/pull/2052#issuecomment-1613778296,https://api.github.com/repos/simonw/datasette/issues/2052,1613778296,IC_kwDOBm6k_c5gME14,15178711,asg017,2023-06-29T20:36:09Z,2023-06-29T20:36:09Z,CONTRIBUTOR,"Ok @hydrosquall a couple things before this PR should be good to go: - Can we move `datasette/static/table-example-plugins.js` into `demos/plugins/static`? - For `datasetteManager.VERSION`, can we fill that in or just comment it out for now? Not sure how difficult it'll be to inject it server-side. I imagine we could also have a small build process with esbuild/rollup that just injects a version string into `manager.js` directly, so we don't have to worry about server-rendering (but that can be a future PR) In terms of how to integrate this into Datasette, a few options I can see working: - Push this as-is and figure it out before the next release - Hide this feature behind a settings flag (`--setting unstable-js-plugins on`) and use that setting to hide/show `` in `base.html` I'll let @simonw decide which one to work with. I kindof like the idea of having an ""unstable"" opt-in process to enable JS plugins, to give us time to try it out with a wide variety of plugins until we feel its ready. I'm also curious to see how ""plugins for a plugin' would work, like #1542. For example, if the leaflet plugin showed default markers, but also included its own hook for other plugins to add more markers/styling. I'm imagine that the individual plugin would re-create their own plugin system compared to this, since handling ""plugins of plugins"" at the top with Datasette seems really convoluted. Also for posterity, here's a list of Simon's Datasette plugins that use ""extra_js_urls()"", which probably means they can be ported/re-written to use this new plugin system: - [`datasette-vega`](https://github.com/simonw/datasette-vega/blob/00de059ab1ef77394ba9f9547abfacf966c479c4/datasette_vega/__init__.py#L25) - [`datasette-cluster-map`](https://github.com/simonw/datasette-cluster-map/blob/795d25ad9ff6cba0307191f44fecc8f8070bef5c/datasette_cluster_map/__init__.py#L14) - [`datasette-leaflet-geojson`](https://github.com/simonw/datasette-leaflet-geojson/blob/64713aa497750400b9ac2c12e8bb6ffab8eb77f3/datasette_leaflet_geojson/__init__.py#L47) - [`datasette-pretty-traces`](https://github.com/simonw/datasette-pretty-traces/blob/5219d65eca3d7d7a73bb9d3120df42fe046a1315/datasette_pretty_traces/__init__.py#L5) - [`datasette-youtube-embed`](https://github.com/simonw/datasette-youtube-embed/blob/4b4a0d7e58ebe15f47e9baf68beb9908c1d899da/datasette_youtube_embed/__init__.py#L55) - [`datasette-leaflet-freedraw`](https://github.com/simonw/datasette-leaflet-freedraw/blob/8f28c2c2080ec9d29f18386cc6a2573a1c8fbde7/datasette_leaflet_freedraw/__init__.py#L66) - [`datasette-hovercards`](https://github.com/simonw/datasette-hovercards/blob/9439ba46b7140fb03223faff0d21aeba5615a287/datasette_hovercards/__init__.py#L5) - [`datasette-mp3-audio`](https://github.com/simonw/datasette-mp3-audio/blob/4402168792f452a46ab7b488e40ec49cd4b12185/datasette_mp3_audio/__init__.py#L6) - [`datasette-geojson-map`](https://github.com/simonw/datasette-geojson-map/blob/32af5f1fd1a07278bbf8071fbb20a61e0f613246/datasette_geojson_map/__init__.py#L30)","{""total_count"": 1, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 1}",1651082214,"feat: Javascript Plugin API (Custom panels, column menu items with JS actions)", https://github.com/simonw/datasette/pull/2052#issuecomment-1606352600,https://api.github.com/repos/simonw/datasette/issues/2052,1606352600,IC_kwDOBm6k_c5fvv7Y,15178711,asg017,2023-06-26T00:17:04Z,2023-06-26T00:17:04Z,CONTRIBUTOR,":wave: would love to see this get merged soon! I want to make a javascript plugin on top of the code-mirror editor to make a few things nicer (function auto-complete, table/column descriptions, etc.), and this would help out a bunch","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1651082214,"feat: Javascript Plugin API (Custom panels, column menu items with JS actions)", https://github.com/simonw/datasette/pull/2052#issuecomment-1585149909,https://api.github.com/repos/simonw/datasette/issues/2052,1585149909,IC_kwDOBm6k_c5ee3fV,9020979,hydrosquall,2023-06-09T21:35:00Z,2023-06-09T21:35:00Z,NONE,"Thanks @cldellow for the thoughtful comments! These are all things that I'll keep in mind as we figure out how/if this API is actually used by plugin authors once it's actually out in the world. > Yes, this would work - but it requires me to continue to communicate the column names out of band (in order to fetch the facet data per-column before registering my plugin), vs being able to re-use them from the plugin implementation. Ah, I understand now! Thanks for explaining. ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1651082214,"feat: Javascript Plugin API (Custom panels, column menu items with JS actions)", https://github.com/simonw/datasette/pull/2052#issuecomment-1548617257,https://api.github.com/repos/simonw/datasette/issues/2052,1548617257,IC_kwDOBm6k_c5cTgYp,193185,cldellow,2023-05-15T21:32:20Z,2023-05-15T21:32:20Z,CONTRIBUTOR,"> Were you picturing that the whole plugin config object could be returned as a promise, or that the individual hooks (like makeColumnActions or makeAboveTablePanelConfigs supported returning a promise of arrays instead only returning plain arrays? The latter - that you could return a promise of arrays, so it parallels the [""await me maybe"" pattern in Datasette](https://simonwillison.net/2020/Sep/2/await-me-maybe/), where you can return either a value, a callable or an awaitable. > I have a hunch that what you're describing might be achievable without adding Promises to the API with something Oops, I did a poor job explaining. Yes, this would work - but it requires me to continue to communicate the column names out of band (in order to fetch the facet data per-column before registering my plugin), vs being able to re-use them from the plugin implementation. This isn't that big of a deal - it'd be a nice ergonomic improvement, but nowhere near as a big of an improvement as having an officially sanctioned way to add stuff to the column menus in the first place. This could also be layered on in a future commit without breaking v1 users, too, so it's not at all urgent. > especially if those lines are encapsulated by a function we provide (maybe something that's available on the window provided by Datasette as an inline script tag Ah, this is maybe the the key point. Since it's all hosted inside Datasette, Datasette can provide some arbitrary sugar to make it easier to work with. My experience with async scripts in JS is that people sometimes don't understand the race conditions inherent to them. If they copy/paste from a tutorial, it does just work. But then they'll delete half the code, and by chance it still works on their machine/Datasette templates, and now someone's headed for an annoying debugging session -- maybe them, maybe someone else who tries to re-use their plugin. Again, a fairly minor thing, though.","{""total_count"": 1, ""+1"": 1, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1651082214,"feat: Javascript Plugin API (Custom panels, column menu items with JS actions)", https://github.com/simonw/datasette/pull/2052#issuecomment-1546362374,https://api.github.com/repos/simonw/datasette/issues/2052,1546362374,IC_kwDOBm6k_c5cK54G,9020979,hydrosquall,2023-05-12T22:09:03Z,2023-05-12T22:09:03Z,NONE,"Hey @cldellow , thanks for the thoughtful feedback and describing the ""lazy facets"" feature! It sounds like the [postTask](https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/postTask) API might be relevant for the types of network request scheduling you have in mind. Addressing your points inline below: > It might also be nice if the plugins could return Promises. Were you picturing that the whole plugin config object could be returned as a promise, or that the individual hooks (like `makeColumnActions` or `makeAboveTablePanelConfigs` supported returning a promise of arrays instead only returning plain arrays? I think what you're describing can be achievable, but I want to make sure I do so in a way that addresses your need / keeps the complexity of the plugin core system at a level this is approachable . I have a hunch that what you're describing might be achievable without adding Promises to the API with something like ``` fetch('/api/with-custom-facets').then(myFacets => { // reusing the go() idiom go(manager, myFacets); }) ``` but I'd like to confirm if that's the case before investigating adding support. > bulletproof plugin registration code that is robust against the order in which the script tags load Yes, I think what you wrote looks right to me! While it looks a little bit verbose compared to the second example, I'm hoping we can mitigate the cost of that during this API incubation phase by making it an easy-to-copy paste code snippet. I haven't heard of the GA queing pattern before, thanks for the example. I won't have time to implement of proof of concept in the next few weeks, but I took some time to think through the pros/cons to decide whether we may want to add this in a future release: I can see that this approach brings advantages - Plugin developers don't need to know the name of the datasette initialization event to start their plugin - Pushing a function to an array probably is easier (definitely more concise) than adding a document event listener - One less event listener sitting in memory It also has some minor costs - A malicious plugin could choose to (or accidentally) mess with the order of the queue if multiple scripts are lined up - Some risk in encouraging people to mutate global state - (not a cost, more a moot point): changing this API may not make a meaningful difference if we're discussing whether people enter 2 vs 5 lines of code, especially if those lines are encapsulated by a function we provide (maybe something that's available on the `window` provided by Datasette as an inline script tag). ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1651082214,"feat: Javascript Plugin API (Custom panels, column menu items with JS actions)", https://github.com/simonw/datasette/pull/2052#issuecomment-1530822437,https://api.github.com/repos/simonw/datasette/issues/2052,1530822437,IC_kwDOBm6k_c5bPn8l,193185,cldellow,2023-05-02T03:35:30Z,2023-05-02T16:02:38Z,CONTRIBUTOR,"Also, just checking - is this how I'd write bulletproof plugin registration code that is robust against the order in which the script tags load (eg if both my code and the Datasette code are loaded via a `\n\n\n\n\n', ...} == {'1+1': 2, 'c... 0xXXX>', ...} FAILED tests/test_permissions.py::test_permissions_checked[/fixtures/simple_primary_key-permissions3] - assert 500 in (200, 403) FAILED tests/test_table_html.py::test_table_csv_json_export_interface - assert 500 == 200 FAILED tests/test_table_html.py::test_table_metadata - assert 500 == 200 FAILED tests/test_html.py::test_css_classes_on_body[/fixtures/simple_primary_key-expected_classes3] - assert 500 == 200 FAILED tests/test_html.py::test_templates_considered[/fixtures/simple_primary_key-table-fixtures-simple_primary_key.html, *table.html] - assert 500 == 200 FAILED tests/test_plugins.py::test_hook_table_actions[simple_view] - AssertionError: assert [] == [{'href': '/'...simple_view'}] ERROR tests/test_internals_database.py::test_execute_write_fn_connection_exception - Failed: Timeout >1.0s ERROR tests/test_internals_database.py::test_mtime_ns - Failed: Timeout >1.0s ERROR tests/test_internals_database.py::test_mtime_ns_is_none_for_memory - Failed: Timeout >1.0s ERROR tests/test_internals_database.py::test_is_mutable - Failed: Timeout >1.0s ERROR tests/test_internals_database.py::test_database_memory_name - Failed: Timeout >1.0s ERROR tests/test_internals_database.py::test_in_memory_databases_forbid_writes - Failed: Timeout >1.0s =============== 21 failed, 1275 passed, 2 skipped, 1 xfailed, 6 errors in 59.18s ================ ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1480163485,https://api.github.com/repos/simonw/datasette/issues/1999,1480163485,IC_kwDOBm6k_c5YOYCd,9599,simonw,2023-03-22T19:48:00Z,2023-03-22T19:48:00Z,OWNER,"Getting close now! Only 13 failures left, mostly relating to CSV. ``` FAILED tests/test_csv.py::test_table_csv - assert 500 == 200 FAILED tests/test_csv.py::test_table_csv_cors_headers - assert 500 == 200 FAILED tests/test_csv.py::test_table_csv_no_header - assert 500 == 200 FAILED tests/test_csv.py::test_table_csv_with_labels - assert 500 == 200 FAILED tests/test_csv.py::test_table_csv_with_nullable_labels - assert 500 == 200 FAILED tests/test_csv.py::test_table_csv_blob_columns - assert 500 == 200 FAILED tests/test_csv.py::test_table_csv_download - assert 500 == 200 FAILED tests/test_csv.py::test_table_csv_stream - assert 1 == 101 FAILED tests/test_plugins.py::test_hook_extra_css_urls[/fixtures/sortable-expected_decoded_object2] - AssertionError: assert {'added': 15,...ortable', ...} == {'added': 15,...ortable', ...} FAILED tests/test_plugins.py::test_hook_register_facet_classes - KeyError: 'suggested_facets' FAILED tests/test_csv.py::test_csv_trace - AttributeError: 'NoneType' object has no attribute 'text' FAILED tests/test_plugins.py::test_hook_extra_body_script[/fixtures/sortable-expected_extra_body_script2] - AssertionError: assert {'added': 15,...ixtures', ...} == {'added': 15,...ixtures', ...} FAILED tests/test_plugins.py::test_hook_register_output_renderer_all_parameters - assert {'1+1': 2, 'c... 0xXXX>', ...} == {'1+1': 2, 'c... 0xXXX>', ...} =============== 13 failed, 1287 passed, 2 skipped, 1 xfailed in 61.57s (0:01:01) ================ ``` ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1480154453,https://api.github.com/repos/simonw/datasette/issues/1999,1480154453,IC_kwDOBm6k_c5YOV1V,9599,simonw,2023-03-22T19:41:39Z,2023-03-22T19:43:04Z,OWNER,"To replace this code: https://github.com/simonw/datasette/blob/56b0758a5fbf85d01ff80a40c9b028469d7bb65f/datasette/views/base.py#L110-L122 Maybe `datasette.render_template()` should optionally accept a list of templates. https://docs.datasette.io/en/stable/internals.html#await-render-template-template-context-none-request-none - turns out it does already: > If this is a list of template file names then the first one that exists will be loaded and rendered. It doesn't have an easy way to populate that `select_templates` debug template variable though.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1477082852,https://api.github.com/repos/simonw/datasette/issues/1999,1477082852,IC_kwDOBm6k_c5YCn7k,9599,simonw,2023-03-20T23:27:25Z,2023-03-20T23:27:25Z,OWNER,"Urgh getting CSV to work is going to be _so hard_, because the logic for that currently lives in a huge chunk of code in `BaseView` which depends on the old design of the `data()` method: https://github.com/simonw/datasette/blob/4bb49848697e40b8b9a1557be42b8e59eac965b3/datasette/views/base.py#L177-L343","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1476898261,https://api.github.com/repos/simonw/datasette/issues/1999,1476898261,IC_kwDOBm6k_c5YB63V,9599,simonw,2023-03-20T20:37:52Z,2023-03-20T20:37:52Z,OWNER,"Manual testing spotted a bug. `/content/repos.json?owner=9599&_facet_array=topics` - does not return a `facet_results` key. `/content/repos.json?owner=9599&_facet_array=topics&_facet=owner` does.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1476860334,https://api.github.com/repos/simonw/datasette/issues/1999,1476860334,IC_kwDOBm6k_c5YBxmu,9599,simonw,2023-03-20T20:06:56Z,2023-03-20T22:09:03Z,OWNER,"`pytest -n 8` locally says: ``` FAILED tests/test_canned_queries.py::test_canned_query_form_csrf_hidden_field[add_name_specify_id-True] - assert '\n\n\n Error 500\n details.open = false);\n});\n\n\n\n Error 500\n details.open = false);\n});\n' in '\n\n\n Error 500\n details.open = false);\... FAILED tests/test_canned_queries.py::test_insert - KeyError: 'ds_csrftoken' FAILED tests/test_canned_queries.py::test_insert_error - KeyError: 'ds_csrftoken' FAILED tests/test_canned_queries.py::test_magic_parameters_csrf_json[False-True] - KeyError: 'ds_csrftoken' FAILED tests/test_canned_queries.py::test_magic_parameters_csrf_json[True-True] - KeyError: 'ds_csrftoken' FAILED tests/test_canned_queries.py::test_vary_header - KeyError: 'vary' FAILED tests/test_csv.py::test_csv_trace - AssertionError: assert 'application/...charset=utf-8' == 'text/html; charset=utf-8' FAILED tests/test_csv.py::test_table_csv - AssertionError: assert 'application/...charset=utf-8' == 'text/plain; charset=utf-8' FAILED tests/test_csv.py::test_table_csv_blob_columns - AssertionError: assert 'application/...charset=utf-8' == 'text/plain; charset=utf-8' FAILED tests/test_csv.py::test_table_csv_download - AssertionError: assert 'application/...charset=utf-8' == 'text/csv; charset=utf-8' FAILED tests/test_csv.py::test_table_csv_no_header - AssertionError: assert 'application/...charset=utf-8' == 'text/plain; charset=utf-8' FAILED tests/test_csv.py::test_table_csv_stream - assert 1 == 101 FAILED tests/test_csv.py::test_table_csv_stream_does_not_calculate_counts - AttributeError: 'NoneType' object has no attribute 'text' FAILED tests/test_csv.py::test_table_csv_stream_does_not_calculate_facets - AttributeError: 'NoneType' object has no attribute 'text' FAILED tests/test_csv.py::test_table_csv_with_labels - AssertionError: assert 'application/...charset=utf-8' == 'text/plain; charset=utf-8' FAILED tests/test_csv.py::test_table_csv_with_nullable_labels - AssertionError: assert 'application/...charset=utf-8' == 'text/plain; charset=utf-8' FAILED tests/test_facets.py::test_array_facet_handle_duplicate_tags - KeyError: 'facet_results' FAILED tests/test_facets.py::test_conflicting_facet_names_json - AssertionError: assert {'results', 'timed_out'} == {'created', '...gs', 'tags_2'} FAILED tests/test_facets.py::test_facet_size - KeyError: 'suggested_facets' FAILED tests/test_facets.py::test_json_array_with_blanks_and_nulls - KeyError: 'suggested_facets' FAILED tests/test_facets.py::test_other_types_of_facet_in_metadata - assert 'created (date)\n' in '\n\n\n fixtures: facetable: 15 rows\n \n\n\n\n' in '\n\n\n fixtures: simple_primary_key: 5 rows\n ' in '\n\n\n fixtures: table/with/slashes.csv: 1 row\n ', ...} FAILED tests/test_plugins.py::test_hook_register_output_renderer_no_parameters - assert b'Hello' == b'{""ok"": true... ""n"": null}]}' FAILED tests/test_plugins.py::test_hook_register_output_renderer_returning_broken_value - assert 200 == 500 FAILED tests/test_plugins.py::test_hook_register_output_renderer_returning_response - assert {'next': None...', ...}, ...]} == {'this_is': 'json'} === 39 failed, 1259 passed, 2 skipped, 1 xfailed in 58.07s === ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1476854645,https://api.github.com/repos/simonw/datasette/issues/1999,1476854645,IC_kwDOBm6k_c5YBwN1,9599,simonw,2023-03-20T20:02:24Z,2023-03-20T20:02:24Z,OWNER,"``` def test_routes(routes, path, expected_class, expected_matches): match, view = resolve_routes(routes, path) if expected_class is None: assert match is None else: > assert view.view_class.__name__ == expected_class E AttributeError: 'function' object has no attribute 'view_class' ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1476851525,https://api.github.com/repos/simonw/datasette/issues/1999,1476851525,IC_kwDOBm6k_c5YBvdF,9599,simonw,2023-03-20T19:59:51Z,2023-03-20T20:01:40Z,OWNER,"Three failures in `test_html.py`: ``` FAILED tests/test_html.py::test_templates_considered[/fixtures/simple_primary_key-table-fixtures-simple_primary_key.html, *table.html] - assert '' in '\n\n\n fixtures: simple_primary_key: 5 rows\n ' in '\n\n\n fixtures: table/with/slashes.csv: 1 row\n \xa0']] == [['\xa0']] FAILED tests/test_table_html.py::test_table_html_disable_foreign_key_links_with_labels - assert [['b']] == [['b']] FAILED tests/test_table_html.py::test_table_html_foreign_key_custom_label_column - assert [['']] == [['']] FAILED tests/test_table_html.py::test_table_html_filter_form_column_options[/fixtures/infinity-expected_column_options0] - AssertionError: assert ['- column -'...wid', 'value'] == ['- column -'] FAILED tests/test_table_html.py::test_table_html_filter_form_column_options[/fixtures/primary_key_multiple_columns-expected_column_options1] - AssertionError: assert ['- column -'...', 'content2'] == ['- column -'] FAILED tests/test_table_html.py::test_table_html_filter_form_column_options[/fixtures/compound_primary_key-expected_column_options2] - AssertionError: assert ['- column -'...2', 'content'] == ['- column -'] FAILED tests/test_table_html.py::test_table_html_filter_form_still_shows_nocol_columns - AssertionError: assert ['- column -'] == ['- column -'...nulls_2', ...] FAILED tests/test_table_html.py::test_compound_primary_key_with_foreign_key_references - assert [['']] == [['']] FAILED tests/test_table_html.py::test_view_html - assert None is not None FAILED tests/test_table_html.py::test_extra_where_clauses - assert [('_where', ""...'_city_id=1')] == [] FAILED tests/test_table_html.py::test_other_hidden_form_fields[/fixtures/facetable?_size=10-expected_hidden0] - AssertionError: assert [] == [('_size', '10')] FAILED tests/test_table_html.py::test_other_hidden_form_fields[/fixtures/facetable?_size=10&_ignore=1&_ignore=2-expected_hidden1] - AssertionError: assert [] == [('_size', '1...ignore', '2')] FAILED tests/test_table_html.py::test_search_and_sort_fields_not_duplicated[/fixtures/searchable?_sort=text2&_where=1-expected_hidden3] - AssertionError: assert [] == [('_where', '1')] FAILED tests/test_table_html.py::test_binary_data_display_in_table - assert [['\xa0']] == [['\xa0']] FAILED tests/test_table_html.py::test_metadata_sort - AttributeError: 'NoneType' object has no attribute 'string' FAILED tests/test_table_html.py::test_metadata_sort_desc - AttributeError: 'NoneType' object has no attribute 'string' FAILED tests/test_table_html.py::test_facet_more_links[5-/fixtures/facetable?_facet=_neighborhood-2-True-/fixtures/facetable?_facet=_neighborhood&_facet_size=max] - assert 0 == 2 FAILED tests/test_table_html.py::test_facet_more_links[5-/fixtures/facetable?_facet=_neighborhood&_facet_size=50-5-True-/fixtures/facetable?_facet=_neighborhood&_facet_size=max] - assert 0 == 5 FAILED tests/test_table_html.py::test_facet_total - assert 500 == 200 ``` Deduped that's 30 tests: ``` FAILED tests/test_table_html.py::test_add_filter_redirects FAILED tests/test_table_html.py::test_binary_data_display_in_table FAILED tests/test_table_html.py::test_compound_primary_key_with_foreign_key_references FAILED tests/test_table_html.py::test_csv_json_export_links_include_labels_if_foreign_keys FAILED tests/test_table_html.py::test_empty_search_parameter_gets_removed FAILED tests/test_table_html.py::test_existing_filter_redirects FAILED tests/test_table_html.py::test_extra_where_clauses FAILED tests/test_table_html.py::test_facet_more_links FAILED tests/test_table_html.py::test_facet_total FAILED tests/test_table_html.py::test_facets_persist_through_filter_form FAILED tests/test_table_html.py::test_metadata_sort FAILED tests/test_table_html.py::test_metadata_sort_desc FAILED tests/test_table_html.py::test_next_does_not_persist_in_hidden_field FAILED tests/test_table_html.py::test_other_hidden_form_fields FAILED tests/test_table_html.py::test_reflected_hidden_form_fields FAILED tests/test_table_html.py::test_rowid_sortable_no_primary_key FAILED tests/test_table_html.py::test_search_and_sort_fields_not_duplicated FAILED tests/test_table_html.py::test_searchable_view_persists_fts_table FAILED tests/test_table_html.py::test_sort_by_desc_redirects FAILED tests/test_table_html.py::test_sort_links FAILED tests/test_table_html.py::test_table_csv_json_export_interface FAILED tests/test_table_html.py::test_table_html_compound_primary_key FAILED tests/test_table_html.py::test_table_html_disable_foreign_key_links_with_labels FAILED tests/test_table_html.py::test_table_html_filter_form_column_options FAILED tests/test_table_html.py::test_table_html_filter_form_still_shows_nocol_columns FAILED tests/test_table_html.py::test_table_html_foreign_key_custom_label_column FAILED tests/test_table_html.py::test_table_html_foreign_key_links FAILED tests/test_table_html.py::test_table_html_no_primary_key FAILED tests/test_table_html.py::test_table_html_simple_primary_key FAILED tests/test_table_html.py::test_view_html ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1475074025,https://api.github.com/repos/simonw/datasette/issues/1999,1475074025,IC_kwDOBm6k_c5X69fp,9599,simonw,2023-03-19T02:14:28Z,2023-03-19T02:14:51Z,OWNER,"I had to replicate quite a bit of this logic from `base.py`: https://github.com/simonw/datasette/blob/56b0758a5fbf85d01ff80a40c9b028469d7bb65f/datasette/views/base.py#L526-L544 I should refactor this when I move the canned / arbitrary query views away from that base class too.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1475016834,https://api.github.com/repos/simonw/datasette/issues/1999,1475016834,IC_kwDOBm6k_c5X6viC,9599,simonw,2023-03-18T22:30:31Z,2023-03-18T22:30:31Z,OWNER,"`test_paginate_using_link_header` will be tricky. The reason the tests are failing is that `json_renderer()` attempts to populate the `link` header using `data[""next_url""]` - but that's not present unless `?_extra=next_url` has been passed: https://github.com/simonw/datasette/blob/2f38479dcc81f11a4362f4e28511fa88afc34e61/datasette/renderer.py#L101-L102 But I can only rely on `next` being present, not `next_url`. I thought I could maybe assemble the `link` header using `next`, by turning that into a `next_url` link - but there's some custom logic which kicks in for pagination against SQL views (offset/limit pagination) to calculate the `next_url` which isn't easily replicable at the `json_renderer()` layer, in the `_next_value_and_url()` utility function: https://github.com/simonw/datasette/blob/2f38479dcc81f11a4362f4e28511fa88afc34e61/datasette/views/table.py#L2275-L2282 ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1475003292,https://api.github.com/repos/simonw/datasette/issues/1999,1475003292,IC_kwDOBm6k_c5X6sOc,9599,simonw,2023-03-18T21:46:20Z,2023-03-18T21:46:20Z,OWNER,"Now 25 failures in `test_table_api.py`: ``` FAILED tests/test_table_api.py::test_expand_labels - assert {'2': {'pk': 2, 'created': '2019-01-14 08:00:00', 'planet_int': 1, 'on_earth': 1, 'state': 'CA', '_city_id': 1, '_neighborhood': 'Dogpatch', 'tags': '[""tag1"", ""tag3""]', 'complex_array'... FAILED tests/test_table_api.py::test_expand_label - AssertionError: assert {'1': {'pk': '1', 'foreign_key_with_label': '1', 'foreign_key_with_blank_label': '3', 'foreign_key_with_no_label': '1', 'foreign_key_compound_pk1': 'a', 'foreign_key_co... FAILED tests/test_table_api.py::test_ttl_parameter[/fixtures/facetable.json-max-age=5] - KeyError: 'Cache-Control' FAILED tests/test_table_api.py::test_ttl_parameter[/fixtures/facetable.json?_ttl=invalid-max-age=5] - KeyError: 'Cache-Control' FAILED tests/test_table_api.py::test_ttl_parameter[/fixtures/facetable.json?_ttl=10-max-age=10] - KeyError: 'Cache-Control' FAILED tests/test_table_api.py::test_ttl_parameter[/fixtures/facetable.json?_ttl=0-no-cache] - KeyError: 'Cache-Control' FAILED tests/test_table_api.py::test_infinity_returned_as_null - AssertionError: assert [{'rowid': 1, 'value': inf}, {'rowid': 2, 'value': -inf}, {'rowid': 3, 'value': 1.5}] == [{'rowid': 1, 'value': None}, {'rowid': 2, 'value': None}, {'rowid': 3, 'value'... FAILED tests/test_table_api.py::test_null_and_compound_foreign_keys_are_not_expanded - AssertionError: assert [{'pk': '1', 'foreign_key_with_label': '1', 'foreign_key_with_blank_label': '3', 'foreign_key_with_no_label': '1', 'foreign_key_compound_pk1': 'a', 'foreign_key_compoun... FAILED tests/test_table_api.py::test_binary_data_in_json[/fixtures/binary_data.json?_shape=array-expected_json0-None] - assert [{'rowid': 1, 'data': ""b'\\x15\\x1c\\x02\\xc7\\xad\\x05\\xfe'""}, {'rowid': 2, 'data': ""b'\\x15\\x1c\\x03\\xc7\\xad\\x05\\xfe'""}, {'rowid': 3, 'data': None}] == [{'rowid': 1, 'data': {'... FAILED tests/test_table_api.py::test_binary_data_in_json[/fixtures/binary_data.json?_shape=array&_nl=on-None-{""rowid"": 1, ""data"": {""$base64"": true, ""encoded"": ""FRwCx60F/g==""}}\n{""rowid"": 2, ""data"": {""$base64"": true, ""encoded"": ""FRwDx60F/g==""}}\n{""rowid"": 3, ""data"": null}] - assert '{""ok"": false, ""error"": ""Object of type bytes is not JSON serializable"", ""status"": 500, ""title"": null}' == '{""rowid"": 1, ""data"": {""$base64"": true, ""encoded"": ""FRwCx60F/g==""}}\n{""rowid""... FAILED tests/test_table_api.py::test_paginate_using_link_header[] - assert 1 == 21 FAILED tests/test_table_api.py::test_paginate_using_link_header[?_shape=arrays] - assert 1 == 21 FAILED tests/test_table_api.py::test_paginate_using_link_header[?_shape=arrayfirst] - assert 400 == 200 FAILED tests/test_table_api.py::test_paginate_using_link_header[?_shape=object] - assert 1 == 21 FAILED tests/test_table_api.py::test_paginate_using_link_header[?_shape=objects] - assert 1 == 21 FAILED tests/test_table_api.py::test_paginate_using_link_header[?_shape=array] - assert 1 == 21 FAILED tests/test_table_api.py::test_paginate_using_link_header[?_shape=array&_nl=on] - assert 1 == 21 FAILED tests/test_table_api.py::test_col_nocol[/fixtures/facetable.json?_col=created-expected_columns0] - KeyError: 'columns' FAILED tests/test_table_api.py::test_col_nocol[/fixtures/facetable.json?_nocol=created-expected_columns1] - KeyError: 'columns' FAILED tests/test_table_api.py::test_col_nocol[/fixtures/facetable.json?_col=state&_col=created-expected_columns2] - KeyError: 'columns' FAILED tests/test_table_api.py::test_col_nocol[/fixtures/facetable.json?_col=state&_col=state-expected_columns3] - KeyError: 'columns' FAILED tests/test_table_api.py::test_col_nocol[/fixtures/facetable.json?_col=state&_col=created&_nocol=created-expected_columns4] - KeyError: 'columns' FAILED tests/test_table_api.py::test_col_nocol[/fixtures/facetable.json?_nocol=state&_facet=state-expected_columns5] - KeyError: 'columns' FAILED tests/test_table_api.py::test_col_nocol[/fixtures/simple_view.json?_nocol=content-expected_columns6] - KeyError: 'columns' FAILED tests/test_table_api.py::test_col_nocol[/fixtures/simple_view.json?_col=content-expected_columns7] - KeyError: 'columns' ============================================================================= 25 failed, 86 passed, 1 xfailed in 7.18s ============================================================================= ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1474704790,https://api.github.com/repos/simonw/datasette/issues/1999,1474704790,IC_kwDOBm6k_c5X5jWW,9599,simonw,2023-03-18T04:52:59Z,2023-03-18T04:52:59Z,OWNER,"Here are the next set of tests to get passing: ``` % pytest tests/test_table_api.py ``` ``` FAILED tests/test_table_api.py::test_facets[/fixtures/facetable.json?_facet=state&_facet=_city_id-expected_facet_results0] - KeyError: 'name' FAILED tests/test_table_api.py::test_facets[/fixtures/facetable.json?_facet=state&_facet=_city_id&state=MI-expected_facet_results1] - KeyError: 'name' FAILED tests/test_table_api.py::test_facets[/fixtures/facetable.json?_facet=planet_int-expected_facet_results2] - KeyError: 'name' FAILED tests/test_table_api.py::test_facets[/fixtures/facetable.json?_facet=planet_int&planet_int=1-expected_facet_results3] - KeyError: 'name' FAILED tests/test_table_api.py::test_suggested_facets - KeyError: 'suggested_facets' FAILED tests/test_table_api.py::test_allow_facet_off - KeyError: 'suggested_facets' FAILED tests/test_table_api.py::test_suggest_facets_off - KeyError: 'suggested_facets' FAILED tests/test_table_api.py::test_nofacet[True] - KeyError: 'suggested_facets' FAILED tests/test_table_api.py::test_nofacet[False] - KeyError: 'suggested_facets' FAILED tests/test_table_api.py::test_nosuggest[True] - KeyError: 'suggested_facets' FAILED tests/test_table_api.py::test_nosuggest[False] - KeyError: 'suggested_facets' FAILED tests/test_table_api.py::test_nocount[True-None] - KeyError: 'count' FAILED tests/test_table_api.py::test_nocount[False-15] - KeyError: 'count' FAILED tests/test_table_api.py::test_expand_labels - AssertionError: assert {'13': {'_cit...:00:00', ...}} == {'13': {'_cit...:00:00', ...}} FAILED tests/test_table_api.py::test_expand_label - AssertionError: assert {'1': {'forei...l': '1', ...}} == {'1': {'forei...': '1'}, ...}} FAILED tests/test_table_api.py::test_ttl_parameter[/fixtures/facetable.json-max-age=5] - KeyError: 'Cache-Control' FAILED tests/test_table_api.py::test_ttl_parameter[/fixtures/facetable.json?_ttl=invalid-max-age=5] - KeyError: 'Cache-Control' FAILED tests/test_table_api.py::test_ttl_parameter[/fixtures/facetable.json?_ttl=10-max-age=10] - KeyError: 'Cache-Control' FAILED tests/test_table_api.py::test_ttl_parameter[/fixtures/facetable.json?_ttl=0-no-cache] - KeyError: 'Cache-Control' FAILED tests/test_table_api.py::test_infinity_returned_as_null - AssertionError: assert [{'rowid': 1,...'value': 1.5}] == [{'rowid': 1,...'value': 1.5}] FAILED tests/test_table_api.py::test_null_and_compound_foreign_keys_are_not_expanded - AssertionError: assert [{'foreign_ke...': None, ...}] == [{'foreign_ke...': None, ...}] FAILED tests/test_table_api.py::test_binary_data_in_json[/fixtures/binary_data.json?_shape=array-expected_json0-None] - assert [{'data': ""b'..., 'rowid': 3}] == [{'data': {'$..., 'rowid': 3}] FAILED tests/test_table_api.py::test_binary_data_in_json[/fixtures/binary_data.json?_shape=array&_nl=on-None-{""rowid"": 1, ""data"": {""$base64"": true, ""encoded"": ""FRwCx60F/g==""}}\n{""rowid"": 2, ""data"": {""$base64"": true, ""encoded"": ""FRwDx60F/g==""}}\n{""rowid"": 3, ""data"": null}] - assert '{""ok"": false...title"": null}' == '{""rowid"": 1,...""data"": null}' FAILED tests/test_table_api.py::test_paginate_using_link_header[] - assert 1 == 21 FAILED tests/test_table_api.py::test_paginate_using_link_header[?_shape=arrays] - assert 1 == 21 FAILED tests/test_table_api.py::test_paginate_using_link_header[?_shape=arrayfirst] - assert 400 == 200 FAILED tests/test_table_api.py::test_paginate_using_link_header[?_shape=object] - assert 1 == 21 FAILED tests/test_table_api.py::test_paginate_using_link_header[?_shape=objects] - assert 1 == 21 FAILED tests/test_table_api.py::test_paginate_using_link_header[?_shape=array] - assert 1 == 21 FAILED tests/test_table_api.py::test_paginate_using_link_header[?_shape=array&_nl=on] - assert 1 == 21 FAILED tests/test_table_api.py::test_col_nocol[/fixtures/facetable.json?_col=created-expected_columns0] - KeyError: 'columns' FAILED tests/test_table_api.py::test_col_nocol[/fixtures/facetable.json?_nocol=created-expected_columns1] - KeyError: 'columns' FAILED tests/test_table_api.py::test_col_nocol[/fixtures/facetable.json?_col=state&_col=created-expected_columns2] - KeyError: 'columns' FAILED tests/test_table_api.py::test_col_nocol[/fixtures/facetable.json?_col=state&_col=state-expected_columns3] - KeyError: 'columns' FAILED tests/test_table_api.py::test_col_nocol[/fixtures/facetable.json?_col=state&_col=created&_nocol=created-expected_columns4] - KeyError: 'columns' FAILED tests/test_table_api.py::test_col_nocol[/fixtures/facetable.json?_nocol=state&_facet=state-expected_columns5] - KeyError: 'columns' FAILED tests/test_table_api.py::test_col_nocol[/fixtures/simple_view.json?_nocol=content-expected_columns6] - KeyError: 'columns' FAILED tests/test_table_api.py::test_col_nocol[/fixtures/simple_view.json?_col=content-expected_columns7] - KeyError: 'columns' ============================================================================= 38 failed, 73 passed, 1 xfailed in 7.25s ============================================================================= ``` ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1463113856,https://api.github.com/repos/simonw/datasette/issues/1999,1463113856,IC_kwDOBm6k_c5XNViA,9599,simonw,2023-03-10T02:13:15Z,2023-03-10T02:13:15Z,OWNER,Idea for if this change ends up making a bunch of breaking changes to the templates (which I think it should) - I can generate a GitHub diff link between the old and new templates and include that link in the 1.0 upgrade documentation to help people who wrote custom templates see what they might need to change - with minimal effort from myself to document those changes.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1463024951,https://api.github.com/repos/simonw/datasette/issues/1999,1463024951,IC_kwDOBm6k_c5XM_03,9599,simonw,2023-03-10T00:17:58Z,2023-03-10T00:17:58Z,OWNER,"Renderers have an impact on three different pages: query results, table page and row page. The row page feature is incomplete though: https://congress-legislators.datasettes.com/legislators/social_media/A000055 Why is there no `.csv` link there?","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1463023674,https://api.github.com/repos/simonw/datasette/issues/1999,1463023674,IC_kwDOBm6k_c5XM_g6,9599,simonw,2023-03-10T00:16:03Z,2023-03-10T00:16:03Z,OWNER,"I also need to figure out the `renderers` stuff, so I can link to the right URLs for CSV and JSON and other formats: https://github.com/simonw/datasette/blob/6d07a7da1531cd749844fc6827d9a1e57009b2ea/datasette/views/base.py#L474-L518","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1463022397,https://api.github.com/repos/simonw/datasette/issues/1999,1463022397,IC_kwDOBm6k_c5XM_M9,9599,simonw,2023-03-10T00:14:21Z,2023-03-10T00:14:21Z,OWNER,"Ironically the thing I most need right now is comprehensive documentation of what variables are being passed to the templates! One big challenge is that I need to untangle the template context that happens in `BaseView` - I'm hacking that together at the moment, but I need a real answer for how that should work in a world in which view functions aren't using that base class. https://github.com/simonw/datasette/blob/56b0758a5fbf85d01ff80a40c9b028469d7bb65f/datasette/views/base.py#L110-L145","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1463021383,https://api.github.com/repos/simonw/datasette/issues/1999,1463021383,IC_kwDOBm6k_c5XM-9H,9599,simonw,2023-03-10T00:12:50Z,2023-03-10T00:12:50Z,OWNER,"Now at 34 failed, 34 passed.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1463005744,https://api.github.com/repos/simonw/datasette/issues/1999,1463005744,IC_kwDOBm6k_c5XM7Iw,9599,simonw,2023-03-09T23:52:15Z,2023-03-09T23:52:23Z,OWNER,"I need to figure out what to do about `extra_context_from_filters` - which was previously passed straight to the HTML context. https://github.com/simonw/datasette/blob/11f7feb7a3f7166c71389786880863d60ed3d165/datasette/views/table.py#L406-L422","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1462997800,https://api.github.com/repos/simonw/datasette/issues/1999,1462997800,IC_kwDOBm6k_c5XM5Mo,9599,simonw,2023-03-09T23:39:47Z,2023-03-09T23:39:47Z,OWNER,"Found a neat trick: ```diff diff --git a/datasette/app.py b/datasette/app.py index 186f192d..40416713 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -23,7 +23,13 @@ from pathlib import Path from markupsafe import Markup, escape from itsdangerous import URLSafeSerializer -from jinja2 import ChoiceLoader, Environment, FileSystemLoader, PrefixLoader +from jinja2 import ( + ChoiceLoader, + Environment, + FileSystemLoader, + PrefixLoader, + StrictUndefined, +) from jinja2.environment import Template from jinja2.exceptions import TemplateNotFound @@ -394,7 +400,10 @@ class Datasette: ] ) self.jinja_env = Environment( - loader=template_loader, autoescape=True, enable_async=True + loader=template_loader, + autoescape=True, + enable_async=True, + undefined=StrictUndefined, ) self.jinja_env.filters[""escape_css_string""] = escape_css_string self.jinja_env.filters[""quote_plus""] = urllib.parse.quote_plus ``` This causes Jinja to raise a hard error if there are any variables referenced in the template that are not available in the context. It's helping me spot things that are still missing, rather than just relying on failed unit tests.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1461161256,https://api.github.com/repos/simonw/datasette/issues/1999,1461161256,IC_kwDOBm6k_c5XF40o,9599,simonw,2023-03-09T02:10:07Z,2023-03-09T02:10:07Z,OWNER,"Just ran into a `no such table: columns` error - which I think is because my new view code sometimes bypasses calling this method (currently done in `BaseView.dispatch_request()`): https://github.com/simonw/datasette/blob/96e94f9b7b2db53865e61390bcce6761727f26d8/datasette/views/base.py#L101-L103","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1461148579,https://api.github.com/repos/simonw/datasette/issues/1999,1461148579,IC_kwDOBm6k_c5XF1uj,9599,simonw,2023-03-09T01:54:10Z,2023-03-09T01:55:33Z,OWNER,Or... I could temporarily build a quick additional `CannedQueryView` subclass that just does the necessary bits to get the existing code to work. I'm going to try that.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1461148254,https://api.github.com/repos/simonw/datasette/issues/1999,1461148254,IC_kwDOBm6k_c5XF1pe,9599,simonw,2023-03-09T01:53:41Z,2023-03-09T01:53:41Z,OWNER,"Solving this is proving difficult: https://github.com/simonw/datasette/blob/96e94f9b7b2db53865e61390bcce6761727f26d8/datasette/views/table.py#L1500-L1503 The problem is that calling `.data()` on `QueryView` only works here because we expect to ourselves be inside a `.data()` method, with all of the existing magic that knows how to render things that are returned by that. So I may need to substantially re-engineer how `QueryView` works in order to get this to work.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1461075648,https://api.github.com/repos/simonw/datasette/issues/1999,1461075648,IC_kwDOBm6k_c5XFj7A,9599,simonw,2023-03-09T00:24:22Z,2023-03-09T00:24:22Z,OWNER,`127.0.0.1:8001/fixtures/neighborhood_search` fails because the forwarding to a canned query does not yet work.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1461074526,https://api.github.com/repos/simonw/datasette/issues/1999,1461074526,IC_kwDOBm6k_c5XFjpe,9599,simonw,2023-03-09T00:23:06Z,2023-03-09T00:23:06Z,OWNER," pytest tests/test_table_html.py Currently 44 failed, 24 passed in 7.53s Failures here: https://gist.github.com/simonw/df0a52cd7d820b776dc3dfc50e7cb778","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1461070937,https://api.github.com/repos/simonw/datasette/issues/1999,1461070937,IC_kwDOBm6k_c5XFixZ,9599,simonw,2023-03-09T00:18:52Z,2023-03-09T00:19:36Z,OWNER,"I managed to get HTML view working! I did it by continuing to add more things to the extras and the `_html` bundle until the page loaded for me: ```diff async def extra_extras(): ""Available ?_extra= blocks"" return { @@ -1981,6 +2053,14 @@ async def extra_extras(): ""query"", ""display_columns"", ""display_rows"", + ""database"", + ""table"", + ""database_color"", + ""table_actions"", + ""filters"", + ""renderers"", + ""custom_table_templates"", + ""sorted_facet_results"", ] } @@ -2006,6 +2086,14 @@ async def extra_extras(): 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, ) ``` I'll probably refactor this into something cleaner, and maybe but a bunch of them in a `""html""` dictionary and update the templates to use `{{ html.filters }}` or similar. Will look at that once the tests are passing. ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1461047607,https://api.github.com/repos/simonw/datasette/issues/1999,1461047607,IC_kwDOBm6k_c5XFdE3,9599,simonw,2023-03-08T23:51:46Z,2023-03-08T23:51:46Z,OWNER,"This feels quite nice: ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1461044477,https://api.github.com/repos/simonw/datasette/issues/1999,1461044477,IC_kwDOBm6k_c5XFcT9,9599,simonw,2023-03-08T23:47:26Z,2023-03-08T23:47:26Z,OWNER,"I want to package together all of the extras that are needed for the HTML format. A few options for doing that: - Introduce `?_extra=_html` where the leading underscore indicates that this is a ""bundle"" of extras, then define a bundle that's everything needed for the HTML renderer - Have some other mechanism whereby different renderers can request a bundle of extras. I'm leaning towards the first option. I'll try that and see what it looks like.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1461023559,https://api.github.com/repos/simonw/datasette/issues/1999,1461023559,IC_kwDOBm6k_c5XFXNH,9599,simonw,2023-03-08T23:23:02Z,2023-03-08T23:23:02Z,OWNER,"To get this unblocked, I'm going to allow myself to pass non-JSON-serializable objects to the HTML template version of things. If I can get that working (and get the existing tests to pass) I can consider a later change that makes those JSON serializable - or admit that it's OK for the templates to have non-JSON data passed to them and figure out how best to document those variables independently from the JSON documentation.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1461002039,https://api.github.com/repos/simonw/datasette/issues/1999,1461002039,IC_kwDOBm6k_c5XFR83,9599,simonw,2023-03-08T22:58:16Z,2023-03-08T23:02:09Z,OWNER,"The reason for that `Row` thing is that it allows custom templates that do things like this: https://docs.datasette.io/en/stable/changelog.html#easier-custom-templates-for-table-rows ```html+jinja {% for row in display_rows %}

{{ row[""title""] }}

{{ row[""description""] }}

Category: {{ row.display(""category_id"") }}

{% endfor %} ``` Is that a good design? the `.display()` thing feels weird - I wonder if anyone has ever actually used that. It's documented here: https://docs.datasette.io/en/0.64.2/custom_templates.html#custom-templates > If you want to output the rendered HTML version of a column, including any links to foreign keys, you can use `{{ row.display(""column_name"") }}`. I can't see any examples of anyone using it in this code search: https://cs.github.com/?scopeName=All+repos&scope=&q=datasette+row.display It is however useful to have some kind of abstraction layer here that insulates the SQLite `Row` object, since having an extra layer will help if Datasette ever grows support for alternative database backends such as DuckDB or PostgreSQL.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1460988975,https://api.github.com/repos/simonw/datasette/issues/1999,1460988975,IC_kwDOBm6k_c5XFOwv,9599,simonw,2023-03-08T22:42:57Z,2023-03-08T22:42:57Z,OWNER,"Aside idea: it might be interesting if there were ""lazy"" template variables available in the context: things that are not actually executed unless a template author requests them. Imagine if `metadata` was a lazy template reference, such that custom templates that don't display any metadata don't trigger it to be resolved (which might involve additional database queries some day).","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1460986533,https://api.github.com/repos/simonw/datasette/issues/1999,1460986533,IC_kwDOBm6k_c5XFOKl,9599,simonw,2023-03-08T22:40:28Z,2023-03-08T22:40:28Z,OWNER,"Figuring out what to do with `display_columns_and_rows()` is hard. That returns rows as this special kind of object, which is designed to be accessed from the HTML templates: https://github.com/simonw/datasette/blob/96e94f9b7b2db53865e61390bcce6761727f26d8/datasette/views/table.py#L45-L71","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1460970807,https://api.github.com/repos/simonw/datasette/issues/1999,1460970807,IC_kwDOBm6k_c5XFKU3,9599,simonw,2023-03-08T22:31:49Z,2023-03-08T22:33:03Z,OWNER,"For the HTML version, I need to decide where all of the stuff that happens in `async def extra_template()` is going to live. I think it's another one of those extra functions, triggered for `?_extra=context`. https://github.com/simonw/datasette/blob/96e94f9b7b2db53865e61390bcce6761727f26d8/datasette/views/table.py#L813-L912","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1460943097,https://api.github.com/repos/simonw/datasette/issues/1999,1460943097,IC_kwDOBm6k_c5XFDj5,9599,simonw,2023-03-08T22:09:24Z,2023-03-08T22:09:47Z,OWNER,"The ease with which I added that `?_extra=query` feature in https://github.com/simonw/datasette/pull/1999/commits/96e94f9b7b2db53865e61390bcce6761727f26d8 made me feel really confident that this architecture is going in the right direction. ```diff diff --git a/datasette/views/table.py b/datasette/views/table.py index 8d3bb2c930..3e1db9c85f 100644 --- a/datasette/views/table.py +++ b/datasette/views/table.py @@ -1913,6 +1913,13 @@ async def extra_request(): ""args"": request.args._data, } + async def extra_query(): + ""Details of the underlying SQL query"" + return { + ""sql"": sql, + ""params"": params, + } + async def extra_extras(): ""Available ?_extra= blocks"" return { @@ -1938,6 +1945,7 @@ async def extra_extras(): extra_primary_keys, extra_debug, extra_request, + extra_query, extra_extras, ) ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1460916405,https://api.github.com/repos/simonw/datasette/issues/1999,1460916405,IC_kwDOBm6k_c5XE9C1,9599,simonw,2023-03-08T21:43:27Z,2023-03-08T21:43:27Z,OWNER,"Just noticed that `_json=colname` is not working, and that's because it's handled by the renderer here: https://github.com/simonw/datasette/blob/56b0758a5fbf85d01ff80a40c9b028469d7bb65f/datasette/renderer.py#L29-L40 But that's not currently being called by my new code.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1460907148,https://api.github.com/repos/simonw/datasette/issues/1999,1460907148,IC_kwDOBm6k_c5XE6yM,9599,simonw,2023-03-08T21:34:30Z,2023-03-08T21:34:30Z,OWNER,"I'm going to hold off on that refactor until later, when I have tests to show me if the refactor works or not.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1460906741,https://api.github.com/repos/simonw/datasette/issues/1999,1460906741,IC_kwDOBm6k_c5XE6r1,9599,simonw,2023-03-08T21:34:08Z,2023-03-08T21:34:08Z,OWNER,"So maybe I can refactor it to look a bit more like this: https://github.com/simonw/datasette/blob/db1a88f4e17a1f50bdaa681e8beddb2276503e7c/datasette/views/table.py#L1602-L1604 One thing that's useful here is that `is_view` is handled early, like this: https://github.com/simonw/datasette/blob/db1a88f4e17a1f50bdaa681e8beddb2276503e7c/datasette/views/table.py#L466-L472 So if I omit the `is_view` bit from the extracted function I can simplify more.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1460905469,https://api.github.com/repos/simonw/datasette/issues/1999,1460905469,IC_kwDOBm6k_c5XE6X9,9599,simonw,2023-03-08T21:32:56Z,2023-03-08T21:32:56Z,OWNER,"I'd really like to extract this ugly logic out into a helper function: https://github.com/simonw/datasette/blob/db1a88f4e17a1f50bdaa681e8beddb2276503e7c/datasette/views/table.py#L1617-L1684 I copied it in and asked ChatGPT to ""List all of the variable that are either modified or assigned to by the above code"": > Modified variables: > > offset > where_clauses > params > order_by > > Assigned variables: > > sort_value > next_by_pk_clauses > order_by (when sort or sort_desc is not None) Then I asked which variables were used as inputs, and argued with it a bit about whether it should be counting functions. Eventually got to this: > My apologies for the oversight. Here are the variables needed as input by the above code, excluding any functions: > > _next > is_view > sort > sort_desc > use_rowid > pks > order_by Note that `use_rowid` is actually defined earlier in terms of two of those other variables: https://github.com/simonw/datasette/blob/db1a88f4e17a1f50bdaa681e8beddb2276503e7c/datasette/views/table.py#L1540","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1460760116,https://api.github.com/repos/simonw/datasette/issues/1999,1460760116,IC_kwDOBm6k_c5XEW40,9599,simonw,2023-03-08T19:48:52Z,2023-03-08T19:48:52Z,OWNER,"I'm trying to get `http://127.0.0.1:8001/fixtures/compound_three_primary_keys?_next=a,d,v` to return the correct results.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1460759358,https://api.github.com/repos/simonw/datasette/issues/1999,1460759358,IC_kwDOBm6k_c5XEWs-,9599,simonw,2023-03-08T19:48:13Z,2023-03-20T18:47:12Z,OWNER,"Breaking this down into smaller steps: - [x] Get `?_next=` working - [x] Implement extensions - so `.json` is needed again for the JSON version, and anything without an extension is passed through a new code path for HTML - [ ] That HTML view should only access JSON data, which can be seen by using `.context` - this will require a lot of updates to templates (it may be necessary to still provide access to some helper functions though). This will form the basis of the ambition to fully document the template context. - [ ] Get a bunch of the existing table HTML and JSON tests to pass - [ ] Use those tests to refactor the nasty `_next` code, see https://github.com/simonw/datasette/pull/1999#issuecomment-1460905469 - [ ] Figure out how the [register_output_renderer(datasette)](https://docs.datasette.io/en/stable/plugin_hooks.html#register-output-renderer-datasette) plugin hook should work","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1421988953,https://api.github.com/repos/simonw/datasette/issues/1999,1421988953,IC_kwDOBm6k_c5UwdRZ,9599,simonw,2023-02-08T04:35:44Z,2023-02-08T05:27:48Z,OWNER,"Next step: get `?_next=...` working (it is ignored at the moment, even though the returned JSON includes the `""next""` key). Then... figure out how to render HTML and other requested formats. Then get the tests to pass!","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1399343659,https://api.github.com/repos/simonw/datasette/issues/1999,1399343659,IC_kwDOBm6k_c5TaEor,9599,simonw,2023-01-21T22:19:20Z,2023-02-06T23:02:12Z,OWNER,"HTML mode needs a list of renderers so it can show links to `.geojson` etc - can do that as a hidden extra (maybe called `renderers`), repeating this code: https://github.com/simonw/datasette/blob/e4ebef082de90db4e1b8527abc0d582b7ae0bc9d/datasette/views/base.py#L477-L497","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1399341658,https://api.github.com/repos/simonw/datasette/issues/1999,1399341658,IC_kwDOBm6k_c5TaEJa,9599,simonw,2023-01-21T22:06:29Z,2023-01-21T22:07:30Z,OWNER,"Relevant: - #1101 - #1672 - #1062","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/pull/1999#issuecomment-1399341151,https://api.github.com/repos/simonw/datasette/issues/1999,1399341151,IC_kwDOBm6k_c5TaEBf,9599,simonw,2023-01-21T22:03:20Z,2023-01-21T22:03:20Z,OWNER,"I think I'm going to have to write a new view function from scratch which completely ignores the existing BaseView/DataView/TableView hierarchy. Here's what I get on the incoming request: ``` (Pdb) request.url, request.full_path, request.host, request.url_vars ('http://127.0.0.1:8001/content/repos.json', '/content/repos.json', '127.0.0.1:8001', {'database': 'content', 'table': 'repos', 'format': 'json'}) ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551694938,?_extra= support (draft), https://github.com/simonw/datasette/issues/1998#issuecomment-1398768399,https://api.github.com/repos/simonw/datasette/issues/1998,1398768399,IC_kwDOBm6k_c5TX4MP,9599,simonw,2023-01-20T18:19:06Z,2023-01-20T18:19:06Z,OWNER,"Simplest solution would be to ditch the `version_option` decorator and roll a custom option based on it instead, imitating what this code does: https://github.com/pallets/click/blob/7586834cab38c5592d9d6de3ee0ebe75d4353bfb/src/click/decorators.py#L413-L524","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551113681,`datasette --version` should also show the SQLite version, https://github.com/simonw/datasette/issues/1998#issuecomment-1398767813,https://api.github.com/repos/simonw/datasette/issues/1998,1398767813,IC_kwDOBm6k_c5TX4DF,9599,simonw,2023-01-20T18:18:27Z,2023-01-20T18:18:27Z,OWNER,"Fell down a bit of a rabbit hole trying to figure out how to get Click's `version_option()` to evaluate a custom message. Got this far: ```python class _VersionMessage(UserString): @property def data(self): return ""%(prog)s, version %(version)s (SQLite {})"".format( sqlite3.connect("":memory:"").execute(""select sqlite_version()"").fetchone()[0] ) @data.setter def data(self, value): pass @click.group(cls=DefaultGroup, default=""serve"", default_if_no_args=True) @click.version_option(version=__version__, message=_VersionMessage("""")) def cli(): """""" Datasette is an open source multi-tool for exploring and publishing data \b About Datasette: https://datasette.io/ Full documentation: https://docs.datasette.io/ """""" ``` But now: ``` % datasette --version %(prog)s, version %(version)s (SQLite 3.40.1) ``` I was trying to avoid running that `select sqlite_version()` thing unless the `--version` option was used.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1551113681,`datasette --version` should also show the SQLite version, https://github.com/simonw/datasette/pull/1996#issuecomment-1396933348,https://api.github.com/repos/simonw/datasette/issues/1996,1396933348,IC_kwDOBm6k_c5TQ4Lk,22429695,codecov[bot],2023-01-19T12:55:57Z,2023-01-19T12:55:57Z,NONE,"# [Codecov](https://codecov.io/gh/simonw/datasette/pull/1996?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) Report Base: **92.11**% // Head: **92.11**% // Increases project coverage by **`+0.00%`** :tada: > Coverage data is based on head [(`96b8b7f`)](https://codecov.io/gh/simonw/datasette/pull/1996?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) compared to base [(`6a352e9`)](https://codecov.io/gh/simonw/datasette/commit/6a352e99ab988dbf8fd22a100049caa6ad33f1ec?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). > Patch coverage: 100.00% of modified lines in pull request are covered.
Additional details and impacted files ```diff @@ Coverage Diff @@ ## main #1996 +/- ## ======================================= Coverage 92.11% 92.11% ======================================= Files 38 38 Lines 5555 5556 +1 ======================================= + Hits 5117 5118 +1 Misses 438 438 ``` | [Impacted Files](https://codecov.io/gh/simonw/datasette/pull/1996?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) | Coverage Δ | | |---|---|---| | [datasette/utils/\_\_init\_\_.py](https://codecov.io/gh/simonw/datasette/pull/1996?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-ZGF0YXNldHRlL3V0aWxzL19faW5pdF9fLnB5) | `94.87% <100.00%> (+<0.01%)` | :arrow_up: | Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison)
[:umbrella: View full report at Codecov](https://codecov.io/gh/simonw/datasette/pull/1996?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). :loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1538342965,Document custom json encoder, https://github.com/simonw/datasette/issues/1994#issuecomment-1399957897,https://api.github.com/repos/simonw/datasette/issues/1994,1399957897,IC_kwDOBm6k_c5TcamJ,201897,julienma,2023-01-23T08:21:08Z,2023-01-23T08:21:08Z,NONE,"Me too, on a M1. Not sure if it's compatible?","{""total_count"": 1, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 1, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1536851861,Stuck on loading screen, https://github.com/simonw/datasette/pull/1992#issuecomment-1384039611,https://api.github.com/repos/simonw/datasette/issues/1992,1384039611,IC_kwDOBm6k_c5SfsS7,22429695,codecov[bot],2023-01-16T13:12:32Z,2023-03-29T06:10:23Z,NONE,"## [Codecov](https://codecov.io/gh/simonw/datasette/pull/1992?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) Report Patch coverage has no change and project coverage change: **`-0.31`** :warning: > Comparison is base [(`c025b01`)](https://codecov.io/gh/simonw/datasette/commit/c025b0180fe344e4f28177ecd7cf388d492d3064?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) 92.41% compared to head [(`bfaa59b`)](https://codecov.io/gh/simonw/datasette/pull/1992?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) 92.11%. > :exclamation: Current head bfaa59b differs from pull request most recent head 64dd698. Consider uploading reports for the commit 64dd698 to get more accurate results
Additional details and impacted files ```diff @@ Coverage Diff @@ ## main #1992 +/- ## ========================================== - Coverage 92.41% 92.11% -0.31% ========================================== Files 38 38 Lines 5750 5555 -195 ========================================== - Hits 5314 5117 -197 - Misses 436 438 +2 ``` [see 9 files with indirect coverage changes](https://codecov.io/gh/simonw/datasette/pull/1992/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison)
[:umbrella: View full report in Codecov by Sentry](https://codecov.io/gh/simonw/datasette/pull/1992?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). :loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1534904478,Bump blacken-docs from 1.12.1 to 1.13.0, https://github.com/simonw/datasette/issues/1989#issuecomment-1491357104,https://api.github.com/repos/simonw/datasette/issues/1989,1491357104,IC_kwDOBm6k_c5Y5E2w,1231935,xavdid,2023-03-31T06:17:23Z,2023-03-31T06:18:05Z,NONE,"I'm running into a similar use case as pax above- I made a `nice` view that just has the data I'm interested in (which doesn't include the `id`, since it's not important in this case). But, by excluding `id` from the view, I can't do fts queries against it because the view has no `id` field to tie to `rowid`: ``` ERROR: conn=, sql = 'select time, text, permalink, num_children from nice where id in (select rowid from items_fts where items_fts match :search) limit 101', params = {'search': 'whatever'}: no such column: id ``` It works fine when I include `id` in my view, but now my `nice` view is cluttered up. Would be great to hide it permanently in the `config.json`.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1531991339,Suggestion: Hiding columns, https://github.com/simonw/datasette/issues/1989#issuecomment-1402347667,https://api.github.com/repos/simonw/datasette/issues/1989,1402347667,IC_kwDOBm6k_c5TliCT,116795,pax,2023-01-24T17:48:59Z,2023-01-24T17:48:59Z,NONE,"The problem (in my particular use case) with using a VIEW is that I'd need one of the columns to be searchable – but that ([enable-fts](https://github.com/simonw/datasette-search-all)) doesn't work with views :/ __ side-suggestion: I don't know how feasible this might be, but when one column (or table) would be marked as hidden, could the _Download SQLite DB_ link take that into account? 🧐","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1531991339,Suggestion: Hiding columns, https://github.com/simonw/datasette/issues/1989#issuecomment-1397734424,https://api.github.com/repos/simonw/datasette/issues/1989,1397734424,IC_kwDOBm6k_c5TT7wY,9599,simonw,2023-01-19T23:18:45Z,2023-01-19T23:18:45Z,OWNER,"There's a `?_nocol=x` argument you can use to hide a column when you link to a table, but that won't help you if you need to hide the column for privacy reasons: https://docs.datasette.io/en/latest/json_api.html#special-table-arguments One solution right now is to define a SQL view for the things that you DO want people to be able to see, and then use Datasette's permission system to hide the tables (`'""allow"": false` in metadata for each table) but show the views. If you want to redact specific columns there's a plugin for doing that: https://datasette.io/plugins/datasette-mask-columns This does make sense as more of a core Datasette feature though - tagging it as a feature suggestion.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1531991339,Suggestion: Hiding columns, https://github.com/simonw/datasette/issues/1988#issuecomment-1379502161,https://api.github.com/repos/simonw/datasette/issues/1988,1379502161,IC_kwDOBm6k_c5SOYhR,9599,simonw,2023-01-11T21:24:24Z,2023-01-11T21:25:05Z,OWNER,Renaming `sql()` to `template_sql()` in https://github.com/simonw/datasette-template-sql and shipping a 2.0 release of that plugin (it is currently at 1.0.2) would be a sensible short-term fix too.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1529707837,Reconsider pattern where plugins could break existing template context, https://github.com/simonw/datasette/issues/1988#issuecomment-1379500131,https://api.github.com/repos/simonw/datasette/issues/1988,1379500131,IC_kwDOBm6k_c5SOYBj,9599,simonw,2023-01-11T21:22:31Z,2023-01-11T21:22:31Z,OWNER,"Relevant code: https://github.com/simonw/datasette/blob/6a352e99ab988dbf8fd22a100049caa6ad33f1ec/datasette/app.py#L1138-L1149 https://github.com/simonw/datasette/blob/6a352e99ab988dbf8fd22a100049caa6ad33f1ec/datasette/app.py#L1193-L1195","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1529707837,Reconsider pattern where plugins could break existing template context, https://github.com/simonw/datasette/issues/1988#issuecomment-1379495504,https://api.github.com/repos/simonw/datasette/issues/1988,1379495504,IC_kwDOBm6k_c5SOW5Q,9599,simonw,2023-01-11T21:18:00Z,2023-01-11T21:18:25Z,OWNER,"A few options: - Ensure that the explicit template context overrides anything that plugins might do - so in this case the `sql(...)` function would not be available on that page. This would break people who have custom templates that use that function though. - Encourage naming conventions where functions and variables from plugins are less likely to interfere with existing functionality. - Completely change how templates work, so you never have a variable called `{{ sql }}` - it is always accessed via some parent object instead, such as `{{ page.sql }}`. That last option actually fits quite well with my efforts to unify template rendering with JSON (and JSON extras) so it might be the best way to address this.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1529707837,Reconsider pattern where plugins could break existing template context, https://github.com/simonw/datasette/issues/1988#issuecomment-1379492499,https://api.github.com/repos/simonw/datasette/issues/1988,1379492499,IC_kwDOBm6k_c5SOWKT,9599,simonw,2023-01-11T21:15:01Z,2023-01-11T21:20:14Z,OWNER,"This is tricky. `datasette-template-sql` registers a custom `sql()` function, but @cldellow spotted a situation where this would interfere with an existing `{{ sql }}` variable in a template context! https://github.com/simonw/datasette-template-sql/blob/70483fd94bb0349761830e37c9ab928e8966841a/datasette_template_sql/__init__.py ```python @hookimpl def extra_template_vars(datasette): async def execute_sql(sql, args=None, database=None): db = datasette.get_database(database) return (await db.execute(sql, args)).rows return {""sql"": execute_sql} ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1529707837,Reconsider pattern where plugins could break existing template context, https://github.com/simonw/datasette/issues/1987#issuecomment-1379313564,https://api.github.com/repos/simonw/datasette/issues/1987,1379313564,IC_kwDOBm6k_c5SNqec,9599,simonw,2023-01-11T18:29:25Z,2023-01-11T18:29:25Z,OWNER,"Fixed: - https://docs.datasette.io/en/latest/installation.html#using-pip - https://docs.datasette.io/en/stable/installation.html#using-pip - https://docs.datasette.io/en/0.64.1/installation.html#using-pip","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1529452371,installpython3.com is now a spam website, https://github.com/simonw/datasette/issues/1987#issuecomment-1379311498,https://api.github.com/repos/simonw/datasette/issues/1987,1379311498,IC_kwDOBm6k_c5SNp-K,9599,simonw,2023-01-11T18:27:30Z,2023-01-11T18:27:46Z,OWNER,"I'm going to leave the bad link in the old versions, because I can't figure out a way to update them using ReadTheDocs - shipping 0.64.1 will at least mean that both `/latest/` and `/stable/` have a good link.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1529452371,installpython3.com is now a spam website, https://github.com/simonw/datasette/issues/1987#issuecomment-1379282756,https://api.github.com/repos/simonw/datasette/issues/1987,1379282756,IC_kwDOBm6k_c5SNi9E,9599,simonw,2023-01-11T18:04:22Z,2023-01-11T18:04:22Z,OWNER,"That change is now live on https://docs.datasette.io/en/latest/installation.html#using-pip I may need to ship a new stable dot-release to update the `/en/stable/` page though.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1529452371,installpython3.com is now a spam website, https://github.com/simonw/datasette/issues/1987#issuecomment-1379276484,https://api.github.com/repos/simonw/datasette/issues/1987,1379276484,IC_kwDOBm6k_c5SNhbE,9599,simonw,2023-01-11T18:00:35Z,2023-01-11T18:00:35Z,OWNER,"Next challenge: how to best deploy this. Ideally I'd like to fix all of the older versions of the docs too - pages like https://docs.datasette.io/en/0.59.4/installation.html#using-pip Not sure if ReadTheDocs has a mechanism for that.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1529452371,installpython3.com is now a spam website, https://github.com/simonw/datasette/issues/1985#issuecomment-1378253346,https://api.github.com/repos/simonw/datasette/issues/1985,1378253346,IC_kwDOBm6k_c5SJnoi,9599,simonw,2023-01-11T05:18:05Z,2023-01-11T05:18:05Z,OWNER,`Datasette(path)` should either work or it should return an easily debugged error message.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1528448642,Don't let Datasette(path) without a list cause weird errors, https://github.com/simonw/datasette/pull/1984#issuecomment-1376619691,https://api.github.com/repos/simonw/datasette/issues/1984,1376619691,IC_kwDOBm6k_c5SDYyr,9599,simonw,2023-01-10T02:02:16Z,2023-01-10T02:02:16Z,OWNER,Preview at https://datasette--1984.org.readthedocs.build/en/1984/ looks good to me.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1526635374,Upgrade Sphinx, https://github.com/simonw/datasette/issues/1983#issuecomment-1375810027,https://api.github.com/repos/simonw/datasette/issues/1983,1375810027,IC_kwDOBm6k_c5SATHr,25778,eyeseast,2023-01-09T15:35:58Z,2023-01-09T15:35:58Z,CONTRIBUTOR,"Yes please, and thank you. I realized I was maybe getting myself in trouble using that, but I think it's a good way to standardize JSON handling.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1525815985,Make CustomJSONEncoder a documented public API, https://github.com/simonw/datasette/issues/1983#issuecomment-1375797856,https://api.github.com/repos/simonw/datasette/issues/1983,1375797856,IC_kwDOBm6k_c5SAQJg,9599,simonw,2023-01-09T15:29:34Z,2023-01-09T15:29:34Z,OWNER,"I had forgotten this had special handling for cursors! The default representation of a row as a tuple rather than a dictionary might not be the right thing to do based on: - #782 ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1525815985,Make CustomJSONEncoder a documented public API, https://github.com/simonw/datasette/issues/1983#issuecomment-1375795441,https://api.github.com/repos/simonw/datasette/issues/1983,1375795441,IC_kwDOBm6k_c5SAPjx,9599,simonw,2023-01-09T15:27:53Z,2023-01-09T15:27:53Z,OWNER,https://github.com/simonw/datasette/blob/7b48664d751e59222c01c97a2739d53d6363bdda/datasette/utils/__init__.py#L163-L178,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1525815985,Make CustomJSONEncoder a documented public API, https://github.com/simonw/datasette/pull/1982#issuecomment-1376620851,https://api.github.com/repos/simonw/datasette/issues/1982,1376620851,IC_kwDOBm6k_c5SDZEz,49699333,dependabot[bot],2023-01-10T02:03:18Z,2023-01-10T02:03:18Z,CONTRIBUTOR,"Looks like sphinx is up-to-date now, so this is no longer needed.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1525560504,Bump sphinx from 5.3.0 to 6.1.2, https://github.com/simonw/datasette/issues/1981#issuecomment-1375142205,https://api.github.com/repos/simonw/datasette/issues/1981,1375142205,IC_kwDOBm6k_c5R9wE9,9599,simonw,2023-01-09T06:05:44Z,2023-01-09T06:05:44Z,OWNER,"I want the labels to take as much space as they need, but line up with each other. If they are more than half the width of the page then the form fields should break to another line.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1524983536,Canned query field labels truncated, https://github.com/simonw/datasette/issues/1980#issuecomment-1375062405,https://api.github.com/repos/simonw/datasette/issues/1980,1375062405,IC_kwDOBm6k_c5R9cmF,9599,simonw,2023-01-09T03:23:53Z,2023-01-09T03:23:53Z,OWNER,"Some potential solutions: - Make it so the primary key is _always_ sortable - not a bad idea, the `sortable_columns` thing was designed to protect against sorting GIANT tables by a non-indexed column, and the primary key always has an index - Make sure that ` ``` But `id` is not a valid sort option any more.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1524867951,"""Cannot sort table by id"" when sortable_columns is used", https://github.com/simonw/datasette/issues/1979#issuecomment-1374659874,https://api.github.com/repos/simonw/datasette/issues/1979,1374659874,IC_kwDOBm6k_c5R76Ui,9599,simonw,2023-01-07T23:59:11Z,2023-01-07T23:59:11Z,OWNER,I back-ported this fix to `0.63.x` as well.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1524076587,More useful error message if enable_load_extension is not available, https://github.com/simonw/datasette/issues/1979#issuecomment-1374659233,https://api.github.com/repos/simonw/datasette/issues/1979,1374659233,IC_kwDOBm6k_c5R76Kh,9599,simonw,2023-01-07T23:56:25Z,2023-01-07T23:56:25Z,OWNER,"``` % datasette --load-extension foo Error: Your Python installation does not have the ability to load SQLite extensions. More information: https://datasette.io/help/extensions ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1524076587,More useful error message if enable_load_extension is not available, https://github.com/simonw/datasette/issues/1979#issuecomment-1374658468,https://api.github.com/repos/simonw/datasette/issues/1979,1374658468,IC_kwDOBm6k_c5R75-k,9599,simonw,2023-01-07T23:53:41Z,2023-01-07T23:53:41Z,OWNER,https://datasette.io/help/extensions now redirects to this new section of documentation: https://docs.datasette.io/en/latest/installation.html#a-note-about-extensions,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1524076587,More useful error message if enable_load_extension is not available, https://github.com/simonw/datasette/issues/1979#issuecomment-1374657204,https://api.github.com/repos/simonw/datasette/issues/1979,1374657204,IC_kwDOBm6k_c5R75q0,9599,simonw,2023-01-07T23:48:42Z,2023-01-07T23:48:42Z,OWNER,Once I ship the next release I should change that `/help/extensions` link to go to `/stable/` and not `/latest/`.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1524076587,More useful error message if enable_load_extension is not available, https://github.com/simonw/datasette/issues/1979#issuecomment-1374582375,https://api.github.com/repos/simonw/datasette/issues/1979,1374582375,IC_kwDOBm6k_c5R7nZn,9599,simonw,2023-01-07T19:22:39Z,2023-01-07T19:22:39Z,OWNER,"This helps: ```diff diff --git a/datasette/cli.py b/datasette/cli.py index 2b61292b..ea98879c 100644 --- a/datasette/cli.py +++ b/datasette/cli.py @@ -4,13 +4,15 @@ import click from click import formatting from click.types import CompositeParamType from click_default_group import DefaultGroup +import functools import json import os import pathlib import shutil from subprocess import call -import sys from runpy import run_module +import sys +import textwrap import webbrowser from .app import ( OBSOLETE_SETTINGS, @@ -126,7 +128,7 @@ class Setting(CompositeParamType): def sqlite_extensions(fn): - return click.option( + fn = click.option( ""sqlite_extensions"", ""--load-extension"", type=LoadExtension(), @@ -134,6 +136,25 @@ def sqlite_extensions(fn): multiple=True, help=""Path to a SQLite extension to load, and optional entrypoint"", )(fn) + # Wrap it in a custom error handler + @functools.wraps(fn) + def wrapped(*args, **kwargs): + try: + return fn(*args, **kwargs) + except AttributeError as e: + if ""enable_load_extension"" in str(e): + raise click.ClickException( + textwrap.dedent( + """""" + Your Python installation does not have the ability to load SQLite extensions. + + More information: https://docs.datasette.io/en/stable/installation.html#extensions + """""" + ).strip() + ) + raise + + return wrapped @click.group(cls=DefaultGroup, default=""serve"", default_if_no_args=True) ``` Need to write help for that to link to.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1524076587,More useful error message if enable_load_extension is not available, https://github.com/simonw/datasette/issues/1978#issuecomment-1375708725,https://api.github.com/repos/simonw/datasette/issues/1978,1375708725,IC_kwDOBm6k_c5R_6Y1,25778,eyeseast,2023-01-09T14:30:00Z,2023-01-09T14:30:00Z,CONTRIBUTOR,Totally missed that issue. I can close this as a duplicate.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1522778923,Document datasette.urls.row and row_blob, https://github.com/simonw/datasette/issues/1978#issuecomment-1374916593,https://api.github.com/repos/simonw/datasette/issues/1978,1374916593,IC_kwDOBm6k_c5R84_x,9599,simonw,2023-01-08T20:06:16Z,2023-01-08T20:06:16Z,OWNER,"Related issue - I'm not 100% settled on the design for these yet: - https://github.com/simonw/datasette/issues/1048#issuecomment-1179757912","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1522778923,Document datasette.urls.row and row_blob, https://github.com/simonw/datasette/pull/1977#issuecomment-1375596856,https://api.github.com/repos/simonw/datasette/issues/1977,1375596856,IC_kwDOBm6k_c5R_fE4,49699333,dependabot[bot],2023-01-09T13:06:14Z,2023-01-09T13:06:14Z,CONTRIBUTOR,Superseded by #1982.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1522552817,Bump sphinx from 5.3.0 to 6.1.1, https://github.com/simonw/datasette/pull/1977#issuecomment-1373600402,https://api.github.com/repos/simonw/datasette/issues/1977,1373600402,IC_kwDOBm6k_c5R33qS,22429695,codecov[bot],2023-01-06T13:09:51Z,2023-01-06T13:09:51Z,NONE,"# [Codecov](https://codecov.io/gh/simonw/datasette/pull/1977?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) Report Base: **92.15**% // Head: **92.15**% // No change to project coverage :thumbsup: > Coverage data is based on head [(`a4eebee`)](https://codecov.io/gh/simonw/datasette/pull/1977?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) compared to base [(`c41278b`)](https://codecov.io/gh/simonw/datasette/commit/c41278b46f7936b4b1a8a14bf285bed82c81c609?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). > Patch has no changes to coverable lines.
Additional details and impacted files ```diff @@ Coverage Diff @@ ## main #1977 +/- ## ======================================= Coverage 92.15% 92.15% ======================================= Files 38 38 Lines 5542 5542 ======================================= Hits 5107 5107 Misses 435 435 ``` Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison)
[:umbrella: View full report at Codecov](https://codecov.io/gh/simonw/datasette/pull/1977?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). :loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1522552817,Bump sphinx from 5.3.0 to 6.1.1, https://github.com/simonw/datasette/pull/1976#issuecomment-1373592231,https://api.github.com/repos/simonw/datasette/issues/1976,1373592231,IC_kwDOBm6k_c5R31qn,49699333,dependabot[bot],2023-01-06T13:02:15Z,2023-01-06T13:02:15Z,CONTRIBUTOR,Superseded by #1977.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1520712722,Bump sphinx from 5.3.0 to 6.1.0, https://github.com/simonw/datasette/pull/1976#issuecomment-1372203113,https://api.github.com/repos/simonw/datasette/issues/1976,1372203113,IC_kwDOBm6k_c5Ryihp,22429695,codecov[bot],2023-01-05T13:15:04Z,2023-01-05T13:15:04Z,NONE,"# [Codecov](https://codecov.io/gh/simonw/datasette/pull/1976?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) Report Base: **92.15**% // Head: **92.15**% // No change to project coverage :thumbsup: > Coverage data is based on head [(`af76abc`)](https://codecov.io/gh/simonw/datasette/pull/1976?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) compared to base [(`c41278b`)](https://codecov.io/gh/simonw/datasette/commit/c41278b46f7936b4b1a8a14bf285bed82c81c609?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). > Patch has no changes to coverable lines.
Additional details and impacted files ```diff @@ Coverage Diff @@ ## main #1976 +/- ## ======================================= Coverage 92.15% 92.15% ======================================= Files 38 38 Lines 5542 5542 ======================================= Hits 5107 5107 Misses 435 435 ``` Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison)
[:umbrella: View full report at Codecov](https://codecov.io/gh/simonw/datasette/pull/1976?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). :loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1520712722,Bump sphinx from 5.3.0 to 6.1.0, https://github.com/simonw/sqlite-utils/issues/520#issuecomment-1539109587,https://api.github.com/repos/simonw/sqlite-utils/issues/520,1539109587,IC_kwDOCGYnMM5bvPLT,9599,simonw,2023-05-08T22:00:46Z,2023-05-08T22:00:46Z,OWNER,"> Hey, isn't this essentially the same issue as #448 ? Yes it is, good catch!","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1516644980,rows_from_file() raises confusing error if file-like object is not in binary mode, https://github.com/simonw/sqlite-utils/issues/520#issuecomment-1421571810,https://api.github.com/repos/simonw/sqlite-utils/issues/520,1421571810,IC_kwDOCGYnMM5Uu3bi,167893,mcarpenter,2023-02-07T22:43:09Z,2023-02-07T22:43:09Z,CONTRIBUTOR,"Hey, isn't this essentially the same issue as #448 ?","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1516644980,rows_from_file() raises confusing error if file-like object is not in binary mode, https://github.com/simonw/sqlite-utils/issues/520#issuecomment-1369333759,https://api.github.com/repos/simonw/sqlite-utils/issues/520,1369333759,IC_kwDOCGYnMM5Rnl__,9599,simonw,2023-01-03T02:23:43Z,2023-01-03T02:23:43Z,OWNER,"The documentation here does at least say the following: https://sqlite-utils.datasette.io/en/3.30/python-api.html#reading-rows-from-a-file > - **fp** (*BinaryIO*) -- a file-like object containing binary data","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1516644980,rows_from_file() raises confusing error if file-like object is not in binary mode, https://github.com/simonw/datasette/pull/1974#issuecomment-1372188571,https://api.github.com/repos/simonw/datasette/issues/1974,1372188571,IC_kwDOBm6k_c5Rye-b,49699333,dependabot[bot],2023-01-05T13:02:40Z,2023-01-05T13:02:40Z,CONTRIBUTOR,Superseded by #1976.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1516376583,Bump sphinx from 5.3.0 to 6.0.0, https://github.com/simonw/datasette/pull/1974#issuecomment-1368935738,https://api.github.com/repos/simonw/datasette/issues/1974,1368935738,IC_kwDOBm6k_c5RmE06,22429695,codecov[bot],2023-01-02T13:12:30Z,2023-01-02T13:12:30Z,NONE,"# [Codecov](https://codecov.io/gh/simonw/datasette/pull/1974?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) Report Base: **92.10**% // Head: **92.10**% // No change to project coverage :thumbsup: > Coverage data is based on head [(`1e213a3`)](https://codecov.io/gh/simonw/datasette/pull/1974?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) compared to base [(`572bdb5`)](https://codecov.io/gh/simonw/datasette/commit/572bdb5b8095bbdc5ec2aa95400055e49ea028cb?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). > Patch has no changes to coverable lines.
Additional details and impacted files ```diff @@ Coverage Diff @@ ## main #1974 +/- ## ======================================= Coverage 92.10% 92.10% ======================================= Files 38 38 Lines 5538 5538 ======================================= Hits 5101 5101 Misses 437 437 ``` Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison)
[:umbrella: View full report at Codecov](https://codecov.io/gh/simonw/datasette/pull/1974?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). :loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1516376583,Bump sphinx from 5.3.0 to 6.0.0, https://github.com/dogsheep/healthkit-to-sqlite/issues/24#issuecomment-1464796494,https://api.github.com/repos/dogsheep/healthkit-to-sqlite/issues/24,1464796494,IC_kwDOC8tyDs5XTwVO,956433,Mjboothaus,2023-03-11T02:23:42Z,2023-03-11T02:23:42Z,NONE,@simonw - maybe put in some error handling to trap for poorly formed XML (from Apple engineers) so that it suggests that there are problems with export.zip rather than odd looking Python errors :),"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1515883470,DOC: xml.etree.ElementTree.ParseError due to healthkit version 12 , https://github.com/dogsheep/healthkit-to-sqlite/issues/24#issuecomment-1464786643,https://api.github.com/repos/dogsheep/healthkit-to-sqlite/issues/24,1464786643,IC_kwDOC8tyDs5XTt7T,956433,Mjboothaus,2023-03-11T02:01:27Z,2023-03-11T02:01:27Z,NONE,Thanks for reporting this and providing a solution -- I was puzzled by this error when I revisited my walking data and experienced this issues. I haven't tried the fix yet.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1515883470,DOC: xml.etree.ElementTree.ParseError due to healthkit version 12 , https://github.com/simonw/datasette/issues/1973#issuecomment-1407523547,https://api.github.com/repos/simonw/datasette/issues/1973,1407523547,IC_kwDOBm6k_c5T5Rrb,193185,cldellow,2023-01-29T00:40:31Z,2023-01-29T00:40:31Z,CONTRIBUTOR,"A +1 for switching to `CustomRow`: I think you currently only get a `CustomRow` if the result set had a column that was an fkey ([this code](https://github.com/simonw/datasette/blob/3c352b7132ef09b829abb69a0da0ad00be5edef9/datasette/views/table.py#L667-L682)) Otherwise you get vanilla `sqlite3.Row`s, which will fail if you try to access `.columns` or lookup the cell by name, which surprised me recently","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1515815014,render_cell plugin hook's row object is not a sqlite.Row, https://github.com/simonw/datasette/issues/1973#issuecomment-1369044959,https://api.github.com/repos/simonw/datasette/issues/1973,1369044959,IC_kwDOBm6k_c5Rmfff,193185,cldellow,2023-01-02T15:41:40Z,2023-01-02T15:41:40Z,CONTRIBUTOR,"Thanks for the response! Yes, it does seem like a pretty nice developer experience--both the automagical labelling of fkeys, and the ability to index the row by column name in addition to column index.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1515815014,render_cell plugin hook's row object is not a sqlite.Row, https://github.com/simonw/datasette/issues/1973#issuecomment-1368936146,https://api.github.com/repos/simonw/datasette/issues/1973,1368936146,IC_kwDOBm6k_c5RmE7S,9599,simonw,2023-01-02T13:13:03Z,2023-01-02T13:13:03Z,OWNER,It's quite handy that the label for foreign keys is made available to the custom render function here - if that was properly documented (and maybe had a slightly more intuitive API) I think it might be a good solution here.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1515815014,render_cell plugin hook's row object is not a sqlite.Row, https://github.com/simonw/datasette/issues/1973#issuecomment-1368935320,https://api.github.com/repos/simonw/datasette/issues/1973,1368935320,IC_kwDOBm6k_c5RmEuY,9599,simonw,2023-01-02T13:12:00Z,2023-01-02T13:12:00Z,OWNER,"Thanks for catching this! Definitely a bug. This might be a good opportunity to reconsider how his should work though. It's possible that Datasette will eventually support alternative database backends - PostgreSQL, DuckDB and ClickHouse are all on my radar here. If that happens, this plugin hook returning a SQLite Row wouldn't make sense. Some kind of thing abstraction layer - maybe CustomRow, maybe something a little bit better - might be a good idea.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1515815014,render_cell plugin hook's row object is not a sqlite.Row, https://github.com/simonw/datasette/issues/1971#issuecomment-1368266904,https://api.github.com/repos/simonw/datasette/issues/1971,1368266904,IC_kwDOBm6k_c5RjhiY,9599,simonw,2022-12-31T19:09:48Z,2022-12-31T19:09:48Z,OWNER,"For the moment I'm going to pin to Sphinx 5.3.0 Note that I got this warning there: ``` [sphinx-autobuild] > sphinx-build -b html /Users/simon/Dropbox/Development/datasette/docs /Users/simon/Dropbox/Development/datasette/docs/_build Running Sphinx v5.3.0 loading pickled environment... done WARNING: extlinks: Sphinx-6.0 will require a caption string to contain exactly one '%s' and all other '%' need to be escaped as '%%'. ``` So I should probably fix that in advance of the 6.0 upgrade.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1515185383,Upgrade for Sphinx 6.0 (once Furo has support for it), https://github.com/simonw/datasette/issues/1971#issuecomment-1368266658,https://api.github.com/repos/simonw/datasette/issues/1971,1368266658,IC_kwDOBm6k_c5Rjhei,9599,simonw,2022-12-31T19:07:19Z,2022-12-31T19:07:19Z,OWNER,"So it looks like Furo doesn't work with Sphinx 6.0 yet - there' s a fix on `main` but it hasn't been released: - https://github.com/pradyunsg/furo/discussions/569","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1515185383,Upgrade for Sphinx 6.0 (once Furo has support for it), https://github.com/simonw/datasette/issues/1971#issuecomment-1368266457,https://api.github.com/repos/simonw/datasette/issues/1971,1368266457,IC_kwDOBm6k_c5RjhbZ,9599,simonw,2022-12-31T19:05:26Z,2022-12-31T19:05:26Z,OWNER,"Ran this locally: pip install -U Sphinx Got these warnings: ``` Installing collected packages: Sphinx Attempting uninstall: Sphinx Found existing installation: Sphinx 5.1.1 Uninstalling Sphinx-5.1.1: Successfully uninstalled Sphinx-5.1.1 ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts. sphinx-basic-ng 0.0.1a12 requires sphinx<6.0,>=4.0, but you have sphinx 6.0.0 which is incompatible. furo 2022.6.21 requires sphinx<6.0,>=4.0, but you have sphinx 6.0.0 which is incompatible. ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1515185383,Upgrade for Sphinx 6.0 (once Furo has support for it), https://github.com/simonw/datasette/issues/1970#issuecomment-1368268233,https://api.github.com/repos/simonw/datasette/issues/1970,1368268233,IC_kwDOBm6k_c5Rjh3J,9599,simonw,2022-12-31T19:22:58Z,2022-12-31T19:22:58Z,OWNER,"https://latest.datasette.io/_internal/databases shows the fix now: ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1515182998,"Path ""None"" in _internal database table", https://github.com/simonw/datasette/issues/1970#issuecomment-1368264882,https://api.github.com/repos/simonw/datasette/issues/1970,1368264882,IC_kwDOBm6k_c5RjhCy,9599,simonw,2022-12-31T18:52:01Z,2022-12-31T18:52:01Z,OWNER,Relevant code: https://github.com/simonw/datasette/blob/e03aed00026cc2e59c09ca41f69a247e1a85cc89/datasette/app.py#L418-L424,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1515182998,"Path ""None"" in _internal database table", https://github.com/simonw/sqlite-utils/pull/519#issuecomment-1539058795,https://api.github.com/repos/simonw/sqlite-utils/issues/519,1539058795,IC_kwDOCGYnMM5bvCxr,9599,simonw,2023-05-08T21:12:52Z,2023-05-08T21:12:52Z,OWNER,"This is a really neat fix, thank you.","{""total_count"": 1, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 1, ""rocket"": 0, ""eyes"": 0}",1505568103,Fixes breaking DEFAULT values, https://github.com/simonw/datasette/pull/1967#issuecomment-1368267484,https://api.github.com/repos/simonw/datasette/issues/1967,1368267484,IC_kwDOBm6k_c5Rjhrc,9599,simonw,2022-12-31T19:15:50Z,2022-12-31T19:15:50Z,OWNER,"My Firefox tab before: And after: ","{""total_count"": 1, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 1, ""rocket"": 0, ""eyes"": 0}",1503010009,Add favicon to documentation, https://github.com/simonw/datasette/pull/1967#issuecomment-1368265354,https://api.github.com/repos/simonw/datasette/issues/1967,1368265354,IC_kwDOBm6k_c5RjhKK,9599,simonw,2022-12-31T18:56:35Z,2022-12-31T18:56:35Z,OWNER,"Thanks for this! Datasette has a favicon in this file: https://github.com/simonw/datasette/blob/2b4a8aa0833068da25072b6247bf349949ca00ee/datasette/static/favicon.png - so I'll copy that over and use it here as well.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1503010009,Add favicon to documentation, https://github.com/simonw/datasette/issues/1966#issuecomment-1368267406,https://api.github.com/repos/simonw/datasette/issues/1966,1368267406,IC_kwDOBm6k_c5RjhqO,9599,simonw,2022-12-31T19:15:10Z,2022-12-31T19:15:10Z,OWNER,"It's working again now - not sure what happened there, I was paying for Heroku for that already.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1501900064,Broken link to live demo in Getting started docs, https://github.com/simonw/datasette/pull/1965#issuecomment-1356827218,https://api.github.com/repos/simonw/datasette/issues/1965,1356827218,IC_kwDOBm6k_c5Q34pS,9599,simonw,2022-12-18T16:01:36Z,2022-12-18T16:01:36Z,OWNER,Will link to this from my TIL shortly.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1501843596,Detect server start/stop more reliably., https://github.com/simonw/datasette/pull/1965#issuecomment-1356827167,https://api.github.com/repos/simonw/datasette/issues/1965,1356827167,IC_kwDOBm6k_c5Q34of,9599,simonw,2022-12-18T16:01:22Z,2022-12-18T16:01:22Z,OWNER,"This is great, thank you!","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1501843596,Detect server start/stop more reliably., https://github.com/simonw/datasette/issues/1964#issuecomment-1356697705,https://api.github.com/repos/simonw/datasette/issues/1964,1356697705,IC_kwDOBm6k_c5Q3ZBp,9599,simonw,2022-12-18T06:37:23Z,2022-12-18T06:37:23Z,OWNER,"I'm certain the two other cog menus (the app menu on the right of the nav bar and the column action menus) have the same problem. Would be great to figure out the right ARIA attributes for these too.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1501778647,Cog menu is not keyboard accessible (also no ARIA), https://github.com/simonw/datasette/issues/1963#issuecomment-1356652057,https://api.github.com/repos/simonw/datasette/issues/1963,1356652057,IC_kwDOBm6k_c5Q3N4Z,9599,simonw,2022-12-18T03:23:22Z,2022-12-18T03:23:22Z,OWNER,https://pypi.org/project/datasette/0.63.3/ is released.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1501713288,0.63.3 bugfix release, https://github.com/simonw/datasette/issues/1963#issuecomment-1356651943,https://api.github.com/repos/simonw/datasette/issues/1963,1356651943,IC_kwDOBm6k_c5Q3N2n,9599,simonw,2022-12-18T03:23:03Z,2022-12-18T03:23:03Z,OWNER,"Oh that's annoying... every step in publish succeeded except the static docs one: https://github.com/simonw/datasette/actions/runs/3723015082/jobs/6314292722 This means the documentation database used to update the search engine on https://datasette.io/ won't reflect the very latest changelog. I'm OK with that - I'll fix this workflow so that next time I publish a release this will work correctly.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1501713288,0.63.3 bugfix release, https://github.com/simonw/datasette/issues/1962#issuecomment-1355691595,https://api.github.com/repos/simonw/datasette/issues/1962,1355691595,IC_kwDOBm6k_c5QzjZL,9599,simonw,2022-12-16T21:53:45Z,2022-12-16T21:55:29Z,OWNER,"Also need an alternative mechanism to this convenience for getting CSRF tokens before a POST: https://github.com/simonw/datasette/blob/5ee954e34b6eb762ccecbdb2be0791d0166fd19c/datasette/utils/testing.py#L90-L103 One option would be adding that mechanism to `datasette.client.post(...)` - as a `_csrf_from=` parameter (with an underscore because it's mainly intended for use in tests, though perhaps that's a weird convention that I shouldn't introduce).","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1500636982,"Alternative, async-friendly pattern for `make_app_client()` and similar - fully retire `TestClient`", https://github.com/simonw/datasette/pull/1960#issuecomment-1356478575,https://api.github.com/repos/simonw/datasette/issues/1960,1356478575,IC_kwDOBm6k_c5Q2jhv,9599,simonw,2022-12-17T21:47:48Z,2022-12-17T21:47:48Z,OWNER,"Stick a twig in it, this will do for the moment.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499150951,Port as many tests as possible to async def tests against ds_client, https://github.com/simonw/datasette/pull/1960#issuecomment-1356476886,https://api.github.com/repos/simonw/datasette/issues/1960,1356476886,IC_kwDOBm6k_c5Q2jHW,9599,simonw,2022-12-17T21:37:05Z,2022-12-17T21:37:05Z,OWNER,"I think this test may be to blame: https://github.com/simonw/datasette/blob/5ee954e34b6eb762ccecbdb2be0791d0166fd19c/tests/test_plugins.py#L950-L972 It's over-riding `_metadata_local` and then failing to set it back to original in a `finally:` block at the end.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499150951,Port as many tests as possible to async def tests against ds_client, https://github.com/simonw/datasette/pull/1960#issuecomment-1356476583,https://api.github.com/repos/simonw/datasette/issues/1960,1356476583,IC_kwDOBm6k_c5Q2jCn,9599,simonw,2022-12-17T21:34:51Z,2022-12-17T21:34:51Z,OWNER,"These are all the places that tests touch `_metadata_local` at the moment: ``` (venv) root@76a81d2417f5:/tmp/datasette/tests# rg _metadata_local test_facets.py 596: ds._metadata_local = { 605: ds._metadata_local[""databases""][""test_facet_size""][""tables""][""neighbourhoods""][ test_permissions.py 62: padlock_client.ds._metadata_local[""allow""] = allow 77: del padlock_client.ds._metadata_local[""allow""] 522: cascade_app_client.ds._metadata_local = updated_metadata 533: cascade_app_client.ds._metadata_local = previous_metadata 549: previous_metadata = cascade_app_client.ds._metadata_local 551: cascade_app_client.ds._metadata_local = metadata 566: cascade_app_client.ds._metadata_local = previous_metadata 842: perms_ds._metadata_local = updated_metadata 849: perms_ds._metadata_local = previous_metadata test_html.py 1114: orig = ds_client.ds._metadata_local 1115: ds_client.ds._metadata_local = metadata 1123: ds_client.ds._metadata_local = orig test_plugins.py 1034: ds_client.ds._metadata_local = { ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499150951,Port as many tests as possible to async def tests against ds_client, https://github.com/simonw/datasette/pull/1960#issuecomment-1356056018,https://api.github.com/repos/simonw/datasette/issues/1960,1356056018,IC_kwDOBm6k_c5Q08XS,9599,simonw,2022-12-17T05:33:26Z,2022-12-17T05:33:26Z,OWNER,"I think I've found the problem. The failing test is this one: `paginated_view.json-201-9` That's this example: https://github.com/simonw/datasette/blob/cede1efeedbc3d928397d53d5a1611eecc598fde/tests/test_table_api.py#L179-L180 Why is it expected to take 9 pages and not 5, when the definition of that view is this: ```sql CREATE VIEW paginated_view AS SELECT content, '- ' || content || ' -' AS content_extra FROM no_primary_key; ``` Because `paginated_view` has extra configuration in `metadata.json`: https://github.com/simonw/datasette/blob/5ee954e34b6eb762ccecbdb2be0791d0166fd19c/tests/fixtures.py#L357 So this test is showing that `metadata` can be used to set an alternative page size for a view. ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499150951,Port as many tests as possible to async def tests against ds_client, https://github.com/simonw/datasette/pull/1960#issuecomment-1356038242,https://api.github.com/repos/simonw/datasette/issues/1960,1356038242,IC_kwDOBm6k_c5Q04Bi,9599,simonw,2022-12-17T04:56:22Z,2022-12-17T04:57:04Z,OWNER,"May have spotted the problem with that `test_paginate_tables_and_views` test: ``` (Pdb) path, expected_rows, expected_pages (None, 201, 9) ``` `path` should not be `None` here: https://github.com/simonw/datasette/blob/cede1efeedbc3d928397d53d5a1611eecc598fde/tests/test_table_api.py#L175-L212 No that's not it either - `path` is reassigned on purpose.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499150951,Port as many tests as possible to async def tests against ds_client, https://github.com/simonw/datasette/pull/1960#issuecomment-1355823260,https://api.github.com/repos/simonw/datasette/issues/1960,1355823260,IC_kwDOBm6k_c5Q0Dic,9599,simonw,2022-12-16T23:36:07Z,2022-12-16T23:36:07Z,OWNER,"I ran `pytest --pdb -x` to drop into the debugger on the first failing test. ``` assert expected_rows == len(fetched) > assert expected_pages == count E assert 9 == 5 ``` That's in `test_paginate_tables_and_views()`. On a hunch, I checked the current settings: ``` (Pdb) ds_client.ds._settings {'default_page_size': 50, 'max_returned_rows': 100, 'max_insert_rows': 100, 'num_sql_threads': 1, 'sql_time_limit_ms': 200, 'default_facet_size': 30, 'facet_time_limit_ms': 200, 'facet_suggest_time_limit_ms': 50, 'allow_facet': True, 'allow_download': True, 'allow_signed_tokens': True, 'max_signed_tokens_ttl': 0, 'suggest_facets': True, 'default_cache_ttl': 5, 'cache_size_kb': 0, 'allow_csv_stream': True, 'max_csv_mb': 100, 'truncate_cells_html': 2048, 'force_https_urls': False, 'template_debug': False, 'trace_debug': False, 'base_url': '/'} ``` Looks like something changed `default_page_size` to 50 and forgot to change it back!","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499150951,Port as many tests as possible to async def tests against ds_client, https://github.com/simonw/datasette/pull/1960#issuecomment-1355685828,https://api.github.com/repos/simonw/datasette/issues/1960,1355685828,IC_kwDOBm6k_c5Qzh_E,9599,simonw,2022-12-16T21:50:01Z,2022-12-16T21:50:01Z,OWNER,"Looks like that `@pytest.mark.ds_client` mark I've been using isn't necessary - I added that so I could easily run `pytest -m ds_client` to execute all tests that I had ported to the new feature, but actually this achieves the same thing: pytest -k ds_client So I'm going to remove the mark.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499150951,Port as many tests as possible to async def tests against ds_client, https://github.com/simonw/datasette/pull/1960#issuecomment-1355478743,https://api.github.com/repos/simonw/datasette/issues/1960,1355478743,IC_kwDOBm6k_c5QyvbX,9599,simonw,2022-12-16T19:27:12Z,2022-12-16T19:27:12Z,OWNER,"Bad news: they're definitely caused by tests that are subtly affected by other tests. This passes without errors: pytest -k test_paginate_tables_and_views","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499150951,Port as many tests as possible to async def tests against ds_client, https://github.com/simonw/datasette/pull/1960#issuecomment-1355475671,https://api.github.com/repos/simonw/datasette/issues/1960,1355475671,IC_kwDOBm6k_c5QyurX,9599,simonw,2022-12-16T19:26:08Z,2022-12-16T19:26:08Z,OWNER,"Great news! The test failures I got running on my laptop (with that fresh Ubuntu Docker image) look like they match the failures I saw in CI: ``` ======== short test summary info ======== FAILED tests/test_table_api.py::test_paginate_tables_and_views[/fixtures/paginated_view.json-201-9] - assert 9 == 5 FAILED tests/test_table_api.py::test_sortable_columns_metadata - KeyError: 'error' FAILED tests/test_table_api.py::test_searchable_views[/fixtures/searchable_view_configured_by_metadata.json?_search=weasel-expected_rows0] - AssertionError: assert [[2, 'terry d...sel', 'puma']] == [[1, 'barry c...sel', 'puma']] FAILED tests/test_table_api.py::test_unit_filters - KeyError: 'distance' FAILED tests/test_table_api.py::test_custom_query_with_unicode_characters - AssertionError: assert {'error': 'Ta...'title': None} == [{'id': 1, 'n...n Francisco'}] FAILED tests/test_table_html.py::test_sort_links - AssertionError: assert [{'a_href': N...', ...}}, ...] == [{'a_href': N...', ...}}, ...] FAILED tests/test_table_html.py::test_table_html_no_primary_key - AttributeError: 'NoneType' object has no attribute 'strip' FAILED tests/test_table_html.py::test_table_html_foreign_key_custom_label_column - assert [['']] == [['1']] FAILED tests/test_table_html.py::test_view_html - assert upper_content is None FAILED tests/test_table_html.py::test_table_metadata - AssertionError: assert 'This <em&...t; is escaped' == 'simple_primary_key' FAILED tests/test_table_html.py::test_metadata_sort - AssertionError: assert ['id', 'name\xa0▼'] == ['id\xa0▼', 'name'] FAILED tests/test_table_html.py::test_metadata_sort_desc - AssertionError: assert ['pk\xa0▲', 'name'] == ['pk\xa0▼', 'name'] FAILED tests/test_table_html.py::test_column_metadata - AttributeError: 'NoneType' object has no attribute 'findAll' ======== 13 failed, 1279 passed, 3 skipped, 57 warnings in 572.40s (0:09:32) ======== ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499150951,Port as many tests as possible to async def tests against ds_client, https://github.com/simonw/datasette/pull/1960#issuecomment-1355471341,https://api.github.com/repos/simonw/datasette/issues/1960,1355471341,IC_kwDOBm6k_c5Qytnt,9599,simonw,2022-12-16T19:23:33Z,2022-12-16T19:23:33Z,OWNER,"Trying this instead: ``` docker run -it ubuntu:22.04 /bin/bash ``` Then in that shell: ``` apt-get update apt-get install python3.11 python3.11-venv git -y cd /tmp git clone https://github.com/simonw/datasette cd datasette git checkout async-tests python3.11 -m venv venv pip install -e '.[test]' pytest ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499150951,Port as many tests as possible to async def tests against ds_client, https://github.com/simonw/datasette/pull/1960#issuecomment-1355445710,https://api.github.com/repos/simonw/datasette/issues/1960,1355445710,IC_kwDOBm6k_c5QynXO,9599,simonw,2022-12-16T19:06:49Z,2022-12-16T19:09:35Z,OWNER,"This would be much easier to debug if I could use Docker to run the GitHub Actions image directly on my own laptop. https://github.com/actions/runner-images/blob/releases/ubuntu22/20221212/images/linux/Ubuntu2204-Readme.md is the README for their most recent image. Not sure if there's an easy way to run it in Docker though. https://github.com/actions/runner-images/blob/main/docs/create-image-and-azure-resources.md is instructions for building them locally - looks fiddly though, involves https://www.packer.io/","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499150951,Port as many tests as possible to async def tests against ds_client, https://github.com/simonw/datasette/pull/1960#issuecomment-1355325426,https://api.github.com/repos/simonw/datasette/issues/1960,1355325426,IC_kwDOBm6k_c5QyJ_y,9599,simonw,2022-12-16T18:00:40Z,2022-12-16T18:00:40Z,OWNER,Many of the failing tests pass on my laptop but fail in CI.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499150951,Port as many tests as possible to async def tests against ds_client, https://github.com/simonw/datasette/pull/1960#issuecomment-1355319541,https://api.github.com/repos/simonw/datasette/issues/1960,1355319541,IC_kwDOBm6k_c5QyIj1,9599,simonw,2022-12-16T17:58:24Z,2022-12-16T17:58:46Z,OWNER,"> I tried adding `invoke_startup()` to the `ds_client()` fixture to see if that would fix this. It did not: I'm still seeing those same failures. Frustrating: https://github.com/simonw/datasette/actions/runs/3715317653/jobs/6300336884 ====== 11 failed, 1252 passed, 1 skipped, 1 warning in 185.77s (0:03:05) =======","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499150951,Port as many tests as possible to async def tests against ds_client, https://github.com/simonw/datasette/pull/1960#issuecomment-1355317369,https://api.github.com/repos/simonw/datasette/issues/1960,1355317369,IC_kwDOBm6k_c5QyIB5,9599,simonw,2022-12-16T17:57:14Z,2022-12-16T17:57:14Z,OWNER,"Opened a follow-up issue here: - #1962","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499150951,Port as many tests as possible to async def tests against ds_client, https://github.com/simonw/datasette/pull/1960#issuecomment-1355313058,https://api.github.com/repos/simonw/datasette/issues/1960,1355313058,IC_kwDOBm6k_c5QyG-i,9599,simonw,2022-12-16T17:53:17Z,2022-12-16T17:53:17Z,OWNER,"Got some surprising test failures here: https://github.com/simonw/datasette/actions/runs/3715317653/jobs/6300336626 ``` > assert response.json() == [{""id"": 1, ""name"": ""San Francisco""}] E AssertionError: assert {'error': 'Ta...'title': None} == [{'id': 1, 'n...n Francisco'}] E Full diff: E - [{'id': 1, 'name': 'San Francisco'}] E + {'error': 'Table not found: 𝐜𝐢𝐭𝐢𝐞𝐬', 'ok': False, 'status': 404, 'title': None} ``` A hunch: this failure suggests that maybe the fixtures tables were not correctly created when this test run. Maybe that can happen when `python -n auto` runs a bunch of separate processes and hence one of the tests randomly gets run in a fresh process and executes before the in-memory fixtures database has been fully populated. I tried adding `invoke_startup()` to the `ds_client()` fixture to see if that would fix this.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499150951,Port as many tests as possible to async def tests against ds_client, https://github.com/simonw/datasette/pull/1960#issuecomment-1355300217,https://api.github.com/repos/simonw/datasette/issues/1960,1355300217,IC_kwDOBm6k_c5QyD15,9599,simonw,2022-12-16T17:44:55Z,2022-12-16T17:44:55Z,OWNER,That's enough for this round. I'll get the tests passing and land this.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499150951,Port as many tests as possible to async def tests against ds_client, https://github.com/simonw/datasette/pull/1960#issuecomment-1354269873,https://api.github.com/repos/simonw/datasette/issues/1960,1354269873,IC_kwDOBm6k_c5QuISx,9599,simonw,2022-12-16T06:11:43Z,2022-12-16T06:11:43Z,OWNER,"This is quite fast: ``` % pytest -m ds_client -n auto ================================================================================== test session starts ================================================================================== platform darwin -- Python 3.10.3, pytest-7.1.3, pluggy-1.0.0 SQLite: 3.39.4 rootdir: /Users/simon/Dropbox/Development/datasette, configfile: pytest.ini plugins: anyio-3.6.1, xdist-2.5.0, forked-1.4.0, asyncio-0.19.0, timeout-2.1.0, profiling-1.7.0 asyncio: mode=strict gw0 [291] / gw1 [291] / gw2 [291] / gw3 [291] / gw4 [291] / gw5 [291] ................................................................................................................................................................................. [ 60%] .................................................................................................................. [100%] ================================================================================== 291 passed in 6.30s ================================================================================== ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499150951,Port as many tests as possible to async def tests against ds_client, https://github.com/simonw/datasette/pull/1960#issuecomment-1354148139,https://api.github.com/repos/simonw/datasette/issues/1960,1354148139,IC_kwDOBm6k_c5Qtqkr,9599,simonw,2022-12-16T03:32:52Z,2022-12-16T03:32:52Z,OWNER,"Got that done to: 68 passed in 14.92s By implementing my own global variable - since `pytest` won't use a global fixture for me, I decided to do it for myself.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499150951,Port as many tests as possible to async def tests against ds_client, https://github.com/simonw/datasette/pull/1960#issuecomment-1354072344,https://api.github.com/repos/simonw/datasette/issues/1960,1354072344,IC_kwDOBm6k_c5QtYEY,9599,simonw,2022-12-16T02:00:07Z,2022-12-16T02:00:07Z,OWNER,"It did NOT speed it up: 68 passed in 26.26s","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499150951,Port as many tests as possible to async def tests against ds_client, https://github.com/simonw/datasette/pull/1960#issuecomment-1354062939,https://api.github.com/repos/simonw/datasette/issues/1960,1354062939,IC_kwDOBm6k_c5QtVxb,22429695,codecov[bot],2022-12-16T01:48:06Z,2022-12-17T21:40:43Z,NONE,"# [Codecov](https://codecov.io/gh/simonw/datasette/pull/1960?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) Report Base: **92.19**% // Head: **92.05**% // Decreases project coverage by **`-0.13%`** :warning: > Coverage data is based on head [(`770879a`)](https://codecov.io/gh/simonw/datasette/pull/1960?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) compared to base [(`0b68996`)](https://codecov.io/gh/simonw/datasette/commit/0b68996cc511b3a801f0cd0157bd66332d75f46f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). > Patch coverage: 100.00% of modified lines in pull request are covered. > :exclamation: Current head 770879a differs from pull request most recent head f42bca8. Consider uploading reports for the commit f42bca8 to get more accurate results
Additional details and impacted files ```diff @@ Coverage Diff @@ ## main #1960 +/- ## ========================================== - Coverage 92.19% 92.05% -0.14% ========================================== Files 38 38 Lines 5521 5527 +6 ========================================== - Hits 5090 5088 -2 - Misses 431 439 +8 ``` | [Impacted Files](https://codecov.io/gh/simonw/datasette/pull/1960?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) | Coverage Δ | | |---|---|---| | [datasette/app.py](https://codecov.io/gh/simonw/datasette/pull/1960/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-ZGF0YXNldHRlL2FwcC5weQ==) | `94.13% <100.00%> (-0.57%)` | :arrow_down: | | [datasette/utils/testing.py](https://codecov.io/gh/simonw/datasette/pull/1960/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-ZGF0YXNldHRlL3V0aWxzL3Rlc3RpbmcucHk=) | `95.83% <100.00%> (+0.24%)` | :arrow_up: | | [datasette/views/index.py](https://codecov.io/gh/simonw/datasette/pull/1960/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-ZGF0YXNldHRlL3ZpZXdzL2luZGV4LnB5) | `96.49% <0.00%> (-1.76%)` | :arrow_down: | | [datasette/database.py](https://codecov.io/gh/simonw/datasette/pull/1960/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-ZGF0YXNldHRlL2RhdGFiYXNlLnB5) | `94.57% <0.00%> (-0.61%)` | :arrow_down: | Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison)
[:umbrella: View full report at Codecov](https://codecov.io/gh/simonw/datasette/pull/1960?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). :loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499150951,Port as many tests as possible to async def tests against ds_client, https://github.com/simonw/datasette/pull/1960#issuecomment-1354061440,https://api.github.com/repos/simonw/datasette/issues/1960,1354061440,IC_kwDOBm6k_c5QtVaA,9599,simonw,2022-12-16T01:45:38Z,2022-12-16T01:45:38Z,OWNER,"I'm going to do `test_table_html.py` next. Currently: 68 passed in 17.20s Will this speed it up?","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499150951,Port as many tests as possible to async def tests against ds_client, https://github.com/simonw/datasette/pull/1960#issuecomment-1354053151,https://api.github.com/repos/simonw/datasette/issues/1960,1354053151,IC_kwDOBm6k_c5QtTYf,9599,simonw,2022-12-16T01:33:22Z,2022-12-16T01:33:22Z,OWNER,"The thing with `Datasette(memory=False)` is tripping me up. The problem is that the tests written against `app_client` - which I want to replace - all assume that there is no `_memory` database, because when you start Datasette with at least one database file it doesn't enable `_memory` unless you explicitly tell it to. But the new `ds_client` fixture works by creating a named in-memory database called `fixtures`, which it does with a call to `ds.add_memory_database(""fixtures"")` after the object has been instantiated. This results in a datasette instance that DOES have a `_memory` database, when we didn't want one. My initial solution attempt was a huge hack - I decided that if you pass `memory=False` to the `Datasette` constructor it should mean ""don't add a `_memory` database even though I didn't pass any files"". I set a the default `memory` argument to `None`. This is weird and surprising (`memory=False` no does something different from `memory=None`?) and I found other tests that it broke, like this one: ```python def test_sql_errors_logged_to_stderr(): runner = CliRunner(mix_stderr=False) result = runner.invoke(cli, [""--get"", ""/_memory.json?sql=select+blah""]) assert result.exit_code == 1 assert ""sql = 'select blah', params = {}: no such column: blah\n"" in result.stderr ``` It ended up with no `_memory` database because it turns out `datasette serve ...` passes `memory=False` without me realizing it. So I'm going to undo that hack and teach the fixture to do this instead: ```python db = ds.add_memory_database(""fixtures"") ds.remove_database(""_memory"") ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499150951,Port as many tests as possible to async def tests against ds_client, https://github.com/simonw/datasette/pull/1960#issuecomment-1354046627,https://api.github.com/repos/simonw/datasette/issues/1960,1354046627,IC_kwDOBm6k_c5QtRyj,9599,simonw,2022-12-16T01:23:21Z,2022-12-16T01:23:21Z,OWNER,"This does seem to help: ```diff diff --git a/tests/conftest.py b/tests/conftest.py index 1306c407..af9c7696 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -25,12 +25,7 @@ UNDOCUMENTED_PERMISSIONS = { } -@pytest.fixture(scope=""session"") -def event_loop(): - return asyncio.get_event_loop() - - -@pytest_asyncio.fixture(scope=""session"") +@pytest_asyncio.fixture async def ds_client(): from datasette.app import Datasette from .fixtures import METADATA, PLUGINS_DIR @@ -53,10 +48,11 @@ async def ds_client(): db = ds.add_memory_database(""fixtures"") def prepare(conn): - conn.executescript(TABLES) - for sql, params in TABLE_PARAMETERIZED_SQL: - with conn: - conn.execute(sql, params) + if not conn.execute(""select count(*) from sqlite_master"").fetchone()[0]: + conn.executescript(TABLES) + for sql, params in TABLE_PARAMETERIZED_SQL: + with conn: + conn.execute(sql, params) await db.execute_write_fn(prepare) return ds.client diff --git a/tests/plugins/my_plugin_2.py b/tests/plugins/my_plugin_2.py index 4f7bf08c..d588342c 100644 --- a/tests/plugins/my_plugin_2.py +++ b/tests/plugins/my_plugin_2.py @@ -117,7 +117,12 @@ def actor_from_request(datasette, request): def permission_allowed(datasette, actor, action): # Testing asyncio version of permission_allowed async def inner(): - assert 2 == (await datasette.get_database().execute(""select 1 + 1"")).first()[0] + assert ( + 2 + == ( + await datasette.get_database(""_internal"").execute(""select 1 + 1"") + ).first()[0] + ) if action == ""this_is_allowed_async"": return True elif action == ""this_is_denied_async"": ``` `pytest -m ds_client` now passes 134 tests. Need to get `pytest -n auto` passing too.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499150951,Port as many tests as possible to async def tests against ds_client, https://github.com/simonw/datasette/pull/1960#issuecomment-1354036967,https://api.github.com/repos/simonw/datasette/issues/1960,1354036967,IC_kwDOBm6k_c5QtPbn,9599,simonw,2022-12-16T01:10:12Z,2022-12-16T01:10:12Z,OWNER,"If it does turn out that I can't use `scope=""session""` on this fixture it might not actually be a showstopper: I can take advantage of the fact that `memory_name=""...""` databases stay present in memory for the duration of the process, so I could have it such that each test that uses the `ds_client` fixture DOES construct a fresh `Datasette` instance, but doesn't need to populate the database since they can re-use the in-memory database from the previous object.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499150951,Port as many tests as possible to async def tests against ds_client, https://github.com/simonw/datasette/pull/1960#issuecomment-1353812913,https://api.github.com/repos/simonw/datasette/issues/1960,1353812913,IC_kwDOBm6k_c5QsYux,9599,simonw,2022-12-15T22:48:54Z,2022-12-15T22:48:54Z,OWNER,"This is all very broken: ``` % pytest -x --pdb ================================================================================== test session starts ================================================================================== platform darwin -- Python 3.10.3, pytest-7.1.3, pluggy-1.0.0 SQLite: 3.39.4 rootdir: /Users/simon/Dropbox/Development/datasette, configfile: pytest.ini plugins: anyio-3.6.1, xdist-2.5.0, forked-1.4.0, asyncio-0.19.0, timeout-2.1.0, profiling-1.7.0 asyncio: mode=strict collected 1295 items tests/test_package.py .. [ 0%] tests/test_cli.py . [ 0%] tests/test_cli_serve_get.py .. [ 0%] tests/test_cli.py . [ 0%] tests/test_black.py . [ 0%] tests/test_api.py E >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> fixturedef = , request = > @pytest.hookimpl(hookwrapper=True) def pytest_fixture_setup( fixturedef: FixtureDef, request: SubRequest ) -> Optional[object]: """"""Adjust the event loop policy when an event loop is produced."""""" if fixturedef.argname == ""event_loop"": outcome = yield > loop = outcome.get_result() /Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages/pytest_asyncio/plugin.py:377: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ /Users/simon/Dropbox/Development/datasette/tests/conftest.py:30: in event_loop return asyncio.get_event_loop() _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = def get_event_loop(self): """"""Get the event loop for the current context. Returns an instance of EventLoop or raises an exception. """""" if (self._local._loop is None and not self._local._set_called and threading.current_thread() is threading.main_thread()): self.set_event_loop(self.new_event_loop()) if self._local._loop is None: > raise RuntimeError('There is no current event loop in thread %r.' % threading.current_thread().name) E RuntimeError: There is no current event loop in thread 'MainThread'. /Users/simon/.pyenv/versions/3.10.3/lib/python3.10/asyncio/events.py:656: RuntimeError >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> PDB post_mortem (IO-capturing turned off) >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> > /Users/simon/.pyenv/versions/3.10.3/lib/python3.10/asyncio/events.py(656)get_event_loop() -> raise RuntimeError('There is no current event loop in thread %r.' (Pdb) q =================================================================================== warnings summary ==================================================================================== tests/test_cli.py::test_inspect_cli_writes_to_file tests/test_cli.py::test_inspect_cli /Users/simon/Dropbox/Development/datasette/datasette/cli.py:163: DeprecationWarning: There is no current event loop loop = asyncio.get_event_loop() tests/test_cli_serve_get.py::test_serve_with_get_exit_code_for_error tests/test_cli_serve_get.py::test_serve_with_get /Users/simon/Dropbox/Development/datasette/datasette/cli.py:596: DeprecationWarning: There is no current event loop asyncio.get_event_loop().run_until_complete(ds.invoke_startup()) tests/test_cli_serve_get.py::test_serve_with_get_exit_code_for_error tests/test_cli_serve_get.py::test_serve_with_get /Users/simon/Dropbox/Development/datasette/datasette/cli.py:599: DeprecationWarning: There is no current event loop asyncio.get_event_loop().run_until_complete(check_databases(ds)) tests/test_api.py::test_homepage /Users/simon/Dropbox/Development/datasette/tests/conftest.py:30: DeprecationWarning: There is no current event loop return asyncio.get_event_loop() -- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html ================================================================================ short test summary info ================================================================================ ERROR tests/test_api.py::test_homepage - RuntimeError: There is no current event loop in thread 'MainThread'. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! _pytest.outcomes.Exit: Quitting debugger !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ======================================================================== 7 passed, 7 warnings, 1 error in 19.15s ======================================================================== (datasette) datasette % ``` The problem looks to be caused by this: https://github.com/simonw/datasette/blob/87737aa1ace82fa7b54c60c41471ec9a661f5299/tests/conftest.py#L28-L30 Which I found necessary in order to have `async def` fixtures that could be shared on the `scope=""session""` basis. Can I work around this, or is `scope=""session""` for async fixtures incompatible with my test suite for some reason? ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499150951,Port as many tests as possible to async def tests against ds_client, https://github.com/simonw/datasette/pull/1960#issuecomment-1353805839,https://api.github.com/repos/simonw/datasette/issues/1960,1353805839,IC_kwDOBm6k_c5QsXAP,9599,simonw,2022-12-15T22:38:37Z,2022-12-15T22:38:37Z,OWNER,"I'm going to make `.status_code` work on `TestClient` response too, so I don't have to worry about using both `status` or `status_code` depending on which kind of object I am using.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499150951,Port as many tests as possible to async def tests against ds_client, https://github.com/simonw/datasette/pull/1960#issuecomment-1353765125,https://api.github.com/repos/simonw/datasette/issues/1960,1353765125,IC_kwDOBm6k_c5QsNEF,9599,simonw,2022-12-15T22:00:04Z,2022-12-15T22:00:04Z,OWNER,I'm going to punt on that for the moment and continue to use `app_client` for tests that use that mechanism.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499150951,Port as many tests as possible to async def tests against ds_client, https://github.com/simonw/datasette/pull/1960#issuecomment-1353763837,https://api.github.com/repos/simonw/datasette/issues/1960,1353763837,IC_kwDOBm6k_c5QsMv9,9599,simonw,2022-12-15T21:59:05Z,2022-12-15T21:59:05Z,OWNER,"Here's an annoying error: ``` > response4 = await ds_client.post( ""/-/logout"", csrftoken_from=True, cookies={""ds_actor"": ds_client.actor_cookie({""id"": ""test""})}, ) tests/test_auth.py:88: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = , path = '/-/logout' kwargs = {'cookies': {'ds_actor': 'eyJhIjp7ImlkIjoidGVzdCJ9fQ.fuFCTJG5XE-RNnUM7dcnXx9sPvE'}, 'csrftoken_from': True}, client = async def post(self, path, **kwargs): await self.ds.invoke_startup() async with httpx.AsyncClient(app=self.app) as client: > return await client.post(self._fix(path), **kwargs) E TypeError: AsyncClient.post() got an unexpected keyword argument 'csrftoken_from' ``` I need an alternative to the `csrftoken_from` mechanism I built for `TestClient`: https://github.com/simonw/datasette/blob/0b68996cc511b3a801f0cd0157bd66332d75f46f/datasette/utils/testing.py#L77-L103","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499150951,Port as many tests as possible to async def tests against ds_client, https://github.com/simonw/datasette/pull/1960#issuecomment-1353749401,https://api.github.com/repos/simonw/datasette/issues/1960,1353749401,IC_kwDOBm6k_c5QsJOZ,9599,simonw,2022-12-15T21:47:27Z,2022-12-15T21:47:27Z,OWNER,"I'm using this new mark: ```python @pytest.mark.ds_client ``` Purely so I can run all of the tests that I've refactored using: ``` pytest -m ds_client ``` I'll likely remove this once the test refactoring project is complete.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499150951,Port as many tests as possible to async def tests against ds_client, https://github.com/simonw/datasette/issues/1959#issuecomment-1356478792,https://api.github.com/repos/simonw/datasette/issues/1959,1356478792,IC_kwDOBm6k_c5Q2jlI,9599,simonw,2022-12-17T21:49:36Z,2022-12-17T21:49:36Z,OWNER,"Made a really good start on this in the just-merged PR: - #1960 The follow-up work will happen in: - #1962","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499081664,Refactor test suite to use mostly `async def` tests, https://github.com/simonw/datasette/issues/1959#issuecomment-1355317782,https://api.github.com/repos/simonw/datasette/issues/1959,1355317782,IC_kwDOBm6k_c5QyIIW,9599,simonw,2022-12-16T17:57:25Z,2022-12-16T17:57:25Z,OWNER,"Opened a follow-up issue here: - #1962","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499081664,Refactor test suite to use mostly `async def` tests, https://github.com/simonw/datasette/issues/1959#issuecomment-1353747370,https://api.github.com/repos/simonw/datasette/issues/1959,1353747370,IC_kwDOBm6k_c5QsIuq,9599,simonw,2022-12-15T21:45:14Z,2022-12-15T21:45:14Z,OWNER,I'm going to do this in a PR.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499081664,Refactor test suite to use mostly `async def` tests, https://github.com/simonw/datasette/issues/1959#issuecomment-1353738075,https://api.github.com/repos/simonw/datasette/issues/1959,1353738075,IC_kwDOBm6k_c5QsGdb,9599,simonw,2022-12-15T21:35:56Z,2022-12-15T21:35:56Z,OWNER,"I built that `OldResponse` class: ```diff diff --git a/tests/utils.py b/tests/utils.py index 191ead9b..f39ac434 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -30,3 +30,25 @@ def inner_html(soup): def has_load_extension(): conn = sqlite3.connect("":memory:"") return hasattr(conn, ""enable_load_extension"") + + +class OldResponse: + ""Transform an HTTPX response to simulate the older TestClient responses"" + # https://github.com/simonw/datasette/issues/1959#issuecomment-1353721091 + def __init__(self, response): + self.response = response + self._json = None + + @property + def headers(self): + return self.response.headers + + @property + def status(self): + return self.response.status_code + + @property + def json(self): + if self._json is None: + self._json = self.response.json() + return self._json ``` I can use it in tests like this: ```python @pytest.mark.asyncio async def test_homepage(ds_client): response = OldResponse(await ds_client.get(""/.json"")) assert response.status == 200 assert ""application/json; charset=utf-8"" == response.headers[""content-type""] assert response.json.keys() == {""fixtures"": 0}.keys() d = response.json[""fixtures""] assert d[""name""] == ""fixtures"" assert d[""tables_count""] == 24 assert len(d[""tables_and_views_truncated""]) == 5 assert d[""tables_and_views_more""] is True # 4 hidden FTS tables + no_primary_key (hidden in metadata) assert d[""hidden_tables_count""] == 6 # 201 in no_primary_key, plus 6 in other hidden tables: assert d[""hidden_table_rows_sum""] == 207, response.json assert d[""views_count""] == 4 ``` But as I work through the tests I'm finding it's actually not too hard to port them over, so I likely won't use it after all.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499081664,Refactor test suite to use mostly `async def` tests, https://github.com/simonw/datasette/issues/1959#issuecomment-1353728682,https://api.github.com/repos/simonw/datasette/issues/1959,1353728682,IC_kwDOBm6k_c5QsEKq,9599,simonw,2022-12-15T21:28:35Z,2022-12-15T21:28:35Z,OWNER,"Got this error trying to have two tests use the same `ds_client` async fixture when I added `scope=""session""` to that fixture: - https://github.com/tortoise/tortoise-orm/issues/638 Adding this to `conftest.py` (as suggested in that issue thread) seemed to fix it: ```python @pytest.fixture(scope=""session"") def event_loop(): return asyncio.get_event_loop() ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499081664,Refactor test suite to use mostly `async def` tests, https://github.com/simonw/datasette/issues/1959#issuecomment-1353721091,https://api.github.com/repos/simonw/datasette/issues/1959,1353721091,IC_kwDOBm6k_c5QsCUD,9599,simonw,2022-12-15T21:20:32Z,2022-12-15T21:20:32Z,OWNER,Rather than tediously rewriting every single test to the new shape I'm going to try a wrapper for that HTTPX response that transforms it into an imitation of the one returned by the existing `TestClient` class.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499081664,Refactor test suite to use mostly `async def` tests, https://github.com/simonw/datasette/issues/1959#issuecomment-1353720559,https://api.github.com/repos/simonw/datasette/issues/1959,1353720559,IC_kwDOBm6k_c5QsCLv,9599,simonw,2022-12-15T21:19:56Z,2022-12-15T21:19:56Z,OWNER,"Here's a port of the first `def ...(app_client)` test. Note that the TestClient object works slightly differently from the HTTPX response returned by `await datasette.client.get(...)`: ```diff diff --git a/datasette/app.py b/datasette/app.py index f3cb8876..b770b469 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -281,7 +281,7 @@ class Datasette: raise self.crossdb = crossdb self.nolock = nolock - if memory or crossdb or not self.files: + if memory or crossdb or (not self.files and memory is not False): self.add_database( Database(self, is_mutable=False, is_memory=True), name=""_memory"" ) diff --git a/pytest.ini b/pytest.ini index 559e518c..0bcb0d1e 100644 --- a/pytest.ini +++ b/pytest.ini @@ -8,4 +8,5 @@ filterwarnings= ignore:.*current_task.*:PendingDeprecationWarning markers = serial: tests to avoid using with pytest-xdist + ds_client: tests using the ds_client fixture asyncio_mode = strict diff --git a/tests/conftest.py b/tests/conftest.py index cd735e12..648423ba 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,7 @@ import httpx import os import pathlib import pytest +import pytest_asyncio import re import subprocess import tempfile @@ -23,6 +24,22 @@ UNDOCUMENTED_PERMISSIONS = { } +@pytest_asyncio.fixture +async def ds_client(): + from datasette.app import Datasette + from .fixtures import METADATA, PLUGINS_DIR + ds = Datasette(memory=False, metadata=METADATA, plugins_dir=PLUGINS_DIR) + from .fixtures import TABLES, TABLE_PARAMETERIZED_SQL + db = ds.add_memory_database(""fixtures"") + def prepare(conn): + conn.executescript(TABLES) + for sql, params in TABLE_PARAMETERIZED_SQL: + with conn: + conn.execute(sql, params) + await db.execute_write_fn(prepare) + return ds.client + + def pytest_report_header(config): return ""SQLite: {}"".format( sqlite3.connect("":memory:"").execute(""select sqlite_version()"").fetchone()[0] diff --git a/tests/test_api.py b/tests/test_api.py index 5f2a6ea6..ddf4219c 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -23,12 +23,15 @@ import sys import urllib -def test_homepage(app_client): - response = app_client.get(""/.json"") - assert response.status == 200 +@pytest.mark.ds_client +@pytest.mark.asyncio +async def test_homepage(ds_client): + response = await ds_client.get(""/.json"") + assert response.status_code == 200 assert ""application/json; charset=utf-8"" == response.headers[""content-type""] - assert response.json.keys() == {""fixtures"": 0}.keys() - d = response.json[""fixtures""] + data = response.json() + assert data.keys() == {""fixtures"": 0}.keys() + d = data[""fixtures""] assert d[""name""] == ""fixtures"" assert d[""tables_count""] == 24 assert len(d[""tables_and_views_truncated""]) == 5 @@ -36,7 +39,7 @@ def test_homepage(app_client): # 4 hidden FTS tables + no_primary_key (hidden in metadata) assert d[""hidden_tables_count""] == 6 # 201 in no_primary_key, plus 6 in other hidden tables: - assert d[""hidden_table_rows_sum""] == 207, response.json + assert d[""hidden_table_rows_sum""] == 207, data assert d[""views_count""] == 4 ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499081664,Refactor test suite to use mostly `async def` tests, https://github.com/simonw/datasette/issues/1959#issuecomment-1353707828,https://api.github.com/repos/simonw/datasette/issues/1959,1353707828,IC_kwDOBm6k_c5Qr_E0,9599,simonw,2022-12-15T21:06:29Z,2022-12-15T21:06:29Z,OWNER,"Previous, abandoned attempt at this work (for #1843): ```diff diff --git a/datasette/app.py b/datasette/app.py index 7e682498..cf35c3a2 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -228,7 +228,7 @@ class Datasette: template_dir=None, plugins_dir=None, static_mounts=None, - memory=False, + memory=None, settings=None, secret=None, version_note=None, @@ -238,6 +238,7 @@ class Datasette: nolock=False, ): self._startup_invoked = False + self._extra_on_startup = [] assert config_dir is None or isinstance( config_dir, Path ), ""config_dir= should be a pathlib.Path"" @@ -278,7 +279,7 @@ class Datasette: raise self.crossdb = crossdb self.nolock = nolock - if memory or crossdb or not self.files: + if memory or crossdb or (not self.files and memory is not False): self.add_database( Database(self, is_mutable=False, is_memory=True), name=""_memory"" ) @@ -391,6 +392,9 @@ class Datasette: self._root_token = secrets.token_hex(32) self.client = DatasetteClient(self) + def _add_on_startup(self, fn): + self._extra_on_startup.append(fn) + async def refresh_schemas(self): if self._refresh_schemas_lock.locked(): return @@ -431,6 +435,8 @@ class Datasette: # This must be called for Datasette to be in a usable state if self._startup_invoked: return + for fn in self._extra_on_startup: + await fn() # Register permissions, but watch out for duplicate name/abbr names = {} abbrs = {} @@ -1431,9 +1437,9 @@ class Datasette: ) if self.setting(""trace_debug""): asgi = AsgiTracer(asgi) - asgi = AsgiRunOnFirstRequest(asgi, on_startup=[setup_db, self.invoke_startup]) for wrapper in pm.hook.asgi_wrapper(datasette=self): asgi = wrapper(asgi) + asgi = AsgiRunOnFirstRequest(asgi, on_startup=[setup_db, self.invoke_startup]) return asgi diff --git a/datasette/utils/asgi.py b/datasette/utils/asgi.py index 56690251..986755cb 100644 --- a/datasette/utils/asgi.py +++ b/datasette/utils/asgi.py @@ -423,9 +423,9 @@ class AsgiFileDownload: class AsgiRunOnFirstRequest: - def __init__(self, asgi, on_startup): + def __init__(self, app, on_startup): assert isinstance(on_startup, list) - self.asgi = asgi + self.app = app self.on_startup = on_startup self._started = False @@ -434,4 +434,4 @@ class AsgiRunOnFirstRequest: self._started = True for hook in self.on_startup: await hook() - return await self.asgi(scope, receive, send) + return await self.app(scope, receive, send) diff --git a/tests/conftest.py b/tests/conftest.py index cd735e12..d1301943 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,6 +23,15 @@ UNDOCUMENTED_PERMISSIONS = { } +# @pytest.fixture(autouse=True) +# def log_name_of_test_before_test(request): +# # To help identify tests that are hanging +# name = str(request.node) +# with open(""/tmp/test.log"", ""a"") as f: +# f.write(name + ""\n"") +# yield + + def pytest_report_header(config): return ""SQLite: {}"".format( sqlite3.connect("":memory:"").execute(""select sqlite_version()"").fetchone()[0] diff --git a/tests/fixtures.py b/tests/fixtures.py index a6700239..18d3f1b7 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -101,6 +101,19 @@ EXPECTED_PLUGINS = [ ] +def _populate_connection(conn): + # Drop any tables and views that exist + to_drop = conn.execute( + ""SELECT name, type FROM sqlite_master where type in ('table', 'view')"" + ).fetchall() + for name, type in to_drop: + conn.execute(f""DROP {type} IF EXISTS [{name}]"") + conn.executescript(TABLES) + for sql, params in TABLE_PARAMETERIZED_SQL: + with conn: + conn.execute(sql, params) + + @contextlib.contextmanager def make_app_client( sql_time_limit_ms=None, @@ -117,45 +130,22 @@ def make_app_client( metadata=None, crossdb=False, ): - with tempfile.TemporaryDirectory() as tmpdir: - filepath = os.path.join(tmpdir, filename) - if is_immutable: - files = [] - immutables = [filepath] - else: - files = [filepath] - immutables = [] - conn = sqlite3.connect(filepath) - conn.executescript(TABLES) - for sql, params in TABLE_PARAMETERIZED_SQL: - with conn: - conn.execute(sql, params) - # Close the connection to avoid ""too many open files"" errors - conn.close() - if extra_databases is not None: - for extra_filename, extra_sql in extra_databases.items(): - extra_filepath = os.path.join(tmpdir, extra_filename) - c2 = sqlite3.connect(extra_filepath) - c2.executescript(extra_sql) - c2.close() - # Insert at start to help test /-/databases ordering: - files.insert(0, extra_filepath) - os.chdir(os.path.dirname(filepath)) - settings = settings or {} - for key, value in { - ""default_page_size"": 50, - ""max_returned_rows"": max_returned_rows or 100, - ""sql_time_limit_ms"": sql_time_limit_ms or 200, - # Default is 3 but this results in ""too many open files"" - # errors when running the full test suite: - ""num_sql_threads"": 1, - }.items(): - if key not in settings: - settings[key] = value + settings = settings or {} + for key, value in { + ""default_page_size"": 50, + ""max_returned_rows"": max_returned_rows or 100, + ""sql_time_limit_ms"": sql_time_limit_ms or 200, + # Default is 3 but this results in ""too many open files"" + # errors when running the full test suite: + ""num_sql_threads"": 1, + }.items(): + if key not in settings: + settings[key] = value + # We can use an in-memory database, but only if we're not doing anything + # with is_immutable or extra_databases and filename is the default + if not is_immutable and not extra_databases and filename == ""fixtures.db"": ds = Datasette( - files, - immutables=immutables, - memory=memory, + memory=memory or False, cors=cors, metadata=metadata or METADATA, plugins_dir=PLUGINS_DIR, @@ -165,12 +155,57 @@ def make_app_client( template_dir=template_dir, crossdb=crossdb, ) + db = ds.add_memory_database(""fixtures"") + + async def populate_fixtures(): + print(""Here we go... populating fixtures"") + await db.execute_write_fn(_populate_connection) + + ds._add_on_startup(populate_fixtures) yield TestClient(ds) - # Close as many database connections as possible - # to try and avoid too many open files error - for db in ds.databases.values(): - if not db.is_memory: - db.close() + else: + with tempfile.TemporaryDirectory() as tmpdir: + filepath = os.path.join(tmpdir, filename) + if is_immutable: + files = [] + immutables = [filepath] + else: + files = [filepath] + immutables = [] + + conn = sqlite3.connect(filepath) + _populate_connection(conn) + # Close the connection to reduce ""too many open files"" errors + conn.close() + + if extra_databases is not None: + for extra_filename, extra_sql in extra_databases.items(): + extra_filepath = os.path.join(tmpdir, extra_filename) + c2 = sqlite3.connect(extra_filepath) + c2.executescript(extra_sql) + c2.close() + # Insert at start to help test /-/databases ordering: + files.insert(0, extra_filepath) + os.chdir(os.path.dirname(filepath)) + ds = Datasette( + files, + immutables=immutables, + memory=memory, + cors=cors, + metadata=metadata or METADATA, + plugins_dir=PLUGINS_DIR, + settings=settings, + inspect_data=inspect_data, + static_mounts=static_mounts, + template_dir=template_dir, + crossdb=crossdb, + ) + yield TestClient(ds) + # Close as many database connections as possible + # to try and avoid too many open files error + for db in ds.databases.values(): + if not db.is_memory: + db.close() @pytest.fixture(scope=""session"") diff --git a/tests/test_cli.py b/tests/test_cli.py index d3e015fa..d9e4e457 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,5 +1,6 @@ from .fixtures import ( app_client, + app_client_with_cors, make_app_client, TestClient as _TestClient, EXPECTED_PLUGINS, @@ -38,7 +39,7 @@ def test_inspect_cli(app_client): assert expected_count == database[""tables""][table_name][""count""] -def test_inspect_cli_writes_to_file(app_client): +def test_inspect_cli_writes_to_file(app_client_with_cors): runner = CliRunner() result = runner.invoke( cli, [""inspect"", ""fixtures.db"", ""--inspect-file"", ""foo.json""] ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499081664,Refactor test suite to use mostly `async def` tests, https://github.com/simonw/datasette/issues/1959#issuecomment-1353705072,https://api.github.com/repos/simonw/datasette/issues/1959,1353705072,IC_kwDOBm6k_c5Qr-Zw,9599,simonw,2022-12-15T21:04:07Z,2022-12-15T21:04:07Z,OWNER,I'm going to start by getting every test that uses the raw `(app_client)` fixture and nothing else (194 at the moment) to switch to `async def` using a shared Datasette instance and `datasette.client.get()`.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1499081664,Refactor test suite to use mostly `async def` tests, https://github.com/simonw/datasette/issues/1958#issuecomment-1354025319,https://api.github.com/repos/simonw/datasette/issues/1958,1354025319,IC_kwDOBm6k_c5QtMln,11729897,davidhaley,2022-12-16T00:59:12Z,2022-12-16T00:59:12Z,NONE,Awesome. Thank you @simonw.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1497909798,datasette --root running in Docker doesn't reliably show the magic URL, https://github.com/simonw/datasette/issues/1958#issuecomment-1354023960,https://api.github.com/repos/simonw/datasette/issues/1958,1354023960,IC_kwDOBm6k_c5QtMQY,9599,simonw,2022-12-16T00:58:14Z,2022-12-16T00:58:19Z,OWNER,"This is in the `0.63.x` branch now, ready to go out in a bug fix release.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1497909798,datasette --root running in Docker doesn't reliably show the magic URL, https://github.com/simonw/datasette/issues/1958#issuecomment-1354019543,https://api.github.com/repos/simonw/datasette/issues/1958,1354019543,IC_kwDOBm6k_c5QtLLX,9599,simonw,2022-12-16T00:54:11Z,2022-12-16T00:54:11Z,OWNER,"To test the fix, I did a fresh checkout of `simonw/datasette` and edited `Dockerfile` to look like this: ```dockerfile FROM python:3.11.0-slim-bullseye as build RUN apt-get update && \ apt-get install -y --no-install-recommends libsqlite3-mod-spatialite && \ apt clean && \ rm -rf /var/lib/apt && \ rm -rf /var/lib/dpkg/info/* RUN pip install https://github.com/simonw/datasette/archive/refs/heads/main.zip && \ find /usr/local/lib -name '__pycache__' | xargs rm -r && \ rm -rf /root/.cache/pip EXPOSE 8001 CMD [""datasette""] ``` Then I built it like this: ``` docker build -f Dockerfile -t datasette-pre-click . ``` And ran like this: ``` docker run -p 8001:8001 -v $(pwd):/mnt datasette-pre-click datasette --root -p 8001 -h 0.0.0.0 ``` This exhibited the same problem. Then I pushed a changed to branch and changed the line to: ``` RUN pip install https://github.com/simonw/datasette/archive/refs/heads/click-echo-root.zip && \ ``` Ran this: ``` docker build -f Dockerfile -t datasette-post-click . ``` And this: ``` docker run -p 8001:8001 -v $(pwd):/mnt datasette-post-click datasette --root -p 8001 -h 0.0.0.0 ``` It fixed the problem! ``` datasette % docker run -p 8001:8001 -v $(pwd):/mnt datasette-post-click datasette --root -p 8001 -h 0.0.0.0 http://0.0.0.0:8001/-/auth-token?token=6542dcf5c8f34f8d13f4af9ce728359c602469efb54029098562bd06c87ad26d INFO: Started server process [1] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://0.0.0.0:8001 (Press CTRL+C to quit) ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1497909798,datasette --root running in Docker doesn't reliably show the magic URL, https://github.com/simonw/datasette/issues/1958#issuecomment-1354008688,https://api.github.com/repos/simonw/datasette/issues/1958,1354008688,IC_kwDOBm6k_c5QtIhw,9599,simonw,2022-12-16T00:45:40Z,2022-12-16T00:45:40Z,OWNER,The fix may just be to switch to `click.echo()`. https://click.palletsprojects.com/en/8.1.x/api/#click.echo,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1497909798,datasette --root running in Docker doesn't reliably show the magic URL, https://github.com/simonw/datasette/issues/1958#issuecomment-1353977605,https://api.github.com/repos/simonw/datasette/issues/1958,1353977605,IC_kwDOBm6k_c5QtA8F,9599,simonw,2022-12-16T00:38:23Z,2022-12-16T00:38:23Z,OWNER,"Confirmed, I just got the same result: ``` - % docker run datasetteproject/datasette pip install datasette-upload-csvs ~ % docker commit $(docker ps -lq) datasette-with-plugins sha256:8cde4a6357b9221d6f9e15887a314f2b4d9d1b87b517764d207ccbaec7c0a69f ~ % docker run -p 8001:8001 -v $(pwd):/mnt datasette-with-plugins datasette --root -p 8001 -h 0.0.0.0 INFO: Started server process [1] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://0.0.0.0:8001 (Press CTRL+C to quit) ^CINFO: Shutting down INFO: Waiting for application shutdown. INFO: Application shutdown complete. INFO: Finished server process [1] http://0.0.0.0:8001/-/auth-token?token=4bd70fdbca215ea55c874eaf889adf8c09f2a00231f7e5e6d0470f3176407a98 ``` Note how the auth-token URL is only displayed after you hit `Ctrl+C`!","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1497909798,datasette --root running in Docker doesn't reliably show the magic URL, https://github.com/simonw/datasette/issues/1958#issuecomment-1352644281,https://api.github.com/repos/simonw/datasette/issues/1958,1352644281,IC_kwDOBm6k_c5Qn7a5,9599,simonw,2022-12-15T07:08:14Z,2022-12-15T07:08:14Z,OWNER,Thanks for the details write-up! This looks like a bug in Datasette itself when run with Docker. Moving this issue there.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1497909798,datasette --root running in Docker doesn't reliably show the magic URL, https://github.com/simonw/datasette/issues/1958#issuecomment-1352644276,https://api.github.com/repos/simonw/datasette/issues/1958,1352644276,IC_kwDOBm6k_c5Qn7a0,11729897,davidhaley,2022-12-14T14:53:53Z,2022-12-14T14:53:53Z,NONE,"I don't have much experience with Python; however, I wonder if this print statement needs `flush=True`? https://github.com/simonw/datasette/blob/fdf7c27b5438f02153c3a7f8ad1b320e4b29e4f4/datasette/cli.py#L621","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1497909798,datasette --root running in Docker doesn't reliably show the magic URL, https://github.com/simonw/datasette/issues/1958#issuecomment-1352644274,https://api.github.com/repos/simonw/datasette/issues/1958,1352644274,IC_kwDOBm6k_c5Qn7ay,11729897,davidhaley,2022-12-14T14:19:24Z,2022-12-14T14:19:24Z,NONE,"Hmm, it appears after I kill the process with `Ctrl+c`: ![image](https://user-images.githubusercontent.com/11729897/207619736-a49b2e97-c5d0-4e54-ad3c-3c3c279927c7.png) ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1497909798,datasette --root running in Docker doesn't reliably show the magic URL, https://github.com/simonw/datasette/issues/1958#issuecomment-1352644270,https://api.github.com/repos/simonw/datasette/issues/1958,1352644270,IC_kwDOBm6k_c5Qn7au,11729897,davidhaley,2022-12-14T00:05:16Z,2022-12-14T00:05:16Z,NONE,"FYI @simonw, I don't see that message. ![image](https://user-images.githubusercontent.com/11729897/207471893-1ec5d4d0-81db-4eaa-ba40-adedcc7c9a32.png) ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1497909798,datasette --root running in Docker doesn't reliably show the magic URL, https://github.com/simonw/datasette/issues/1958#issuecomment-1352644267,https://api.github.com/repos/simonw/datasette/issues/1958,1352644267,IC_kwDOBm6k_c5Qn7ar,9599,simonw,2022-12-13T18:33:32Z,2022-12-13T18:33:32Z,OWNER,"When you run `--root` you need to follow the special link that gets output to the console: ``` % datasette --root http://127.0.0.1:8001/-/auth-token?token=036d8055cc8000e9667f21c1dd08722a9358c066463873ad9566d23d88765c52 INFO: Started server process [53934] INFO: Waiting for application startup. INFO: Application startup complete. ``` That `/-/auth-token?...` link is the one that sets the cookie and lets you in.","{""total_count"": 1, ""+1"": 1, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1497909798,datasette --root running in Docker doesn't reliably show the magic URL, https://github.com/simonw/datasette/issues/1958#issuecomment-1352644262,https://api.github.com/repos/simonw/datasette/issues/1958,1352644262,IC_kwDOBm6k_c5Qn7am,11729897,davidhaley,2022-12-13T16:49:31Z,2022-12-13T16:49:52Z,NONE,"I may have found the reason. I don't think the `--root` option is taking effect. Visited: http://127.0.0.1:8001/-/permissions ![image](https://user-images.githubusercontent.com/11729897/207394127-59f8320f-3410-4ff8-869d-b19ab955d67c.png)","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1497909798,datasette --root running in Docker doesn't reliably show the magic URL, https://github.com/simonw/datasette/issues/1957#issuecomment-1352371019,https://api.github.com/repos/simonw/datasette/issues/1957,1352371019,IC_kwDOBm6k_c5Qm4tL,9599,simonw,2022-12-14T23:50:50Z,2022-12-14T23:50:50Z,OWNER,"One option: if any rows were truncated, show a button you can click to run the query again with truncation disabled - maybe with `?_truncate=off` in the URL or similar. But... still want to truncate if the user runs a query that would return multiple MBs of HTML (assuming the other Datasette query limits don't stop that from happening).","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1497577017,Reconsider row value truncation on query page, https://github.com/simonw/datasette/issues/1956#issuecomment-1352075845,https://api.github.com/repos/simonw/datasette/issues/1956,1352075845,IC_kwDOBm6k_c5QlwpF,9599,simonw,2022-12-14T19:57:17Z,2022-12-14T19:58:22Z,OWNER,"I'm going to test this using calls to `ds.permission_allowed()` with an actor with `_r` block. I can add extra tests to https://github.com/simonw/datasette/blob/1a3dcf494376e32f7cff110c86a88e5b0a3f3924/tests/test_permissions.py#L605-L616","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1497288666,Handle abbreviations properly in permission_allowed_actor_restrictions, https://github.com/simonw/datasette/issues/1956#issuecomment-1352070655,https://api.github.com/repos/simonw/datasette/issues/1956,1352070655,IC_kwDOBm6k_c5QlvX_,9599,simonw,2022-12-14T19:54:36Z,2022-12-14T19:54:36Z,OWNER,Also this code should work with non-abbreviations too.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1497288666,Handle abbreviations properly in permission_allowed_actor_restrictions, https://github.com/simonw/datasette/issues/1955#issuecomment-1357084279,https://api.github.com/repos/simonw/datasette/issues/1955,1357084279,IC_kwDOBm6k_c5Q43Z3,178162,andrewdotn,2022-12-19T04:34:16Z,2022-12-19T04:34:16Z,NONE,"You were super-close on the python version of the test here, changing `http` to `https` on 8b73fc6b47dffd8836f5c58aae1e57c1f66a5754 is enough to pass the test: ```diff diff --git a/tests/conftest.py b/tests/conftest.py index 69dee68b4a3f..ba07a11d37f6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -207,7 +207,7 @@ def ds_localhost_https_server(tmp_path_factory): stderr=subprocess.STDOUT, cwd=tempfile.gettempdir(), ) - wait_until_responds(""http://localhost:8042/"", verify=client_cert) + wait_until_responds(""https://localhost:8042/"", verify=client_cert) # Check it started successfully assert not ds_proc.poll(), ds_proc.stdout.read().decode(""utf-8"") yield ds_proc, client_cert ``` My speculation about what was happening here: when `wait_until_responds()` would time out due to SSL connection problems, because `.terminate()` isn’t in a `finally`, the datasette process wouldn’t get killed. That could (1) hang CI and (2) cause all your future local test runs to mysteriously fail because they’d be secretly talking to that old datasette process still hanging around from a past test run with an old temporary server certificate, and that old server cert wouldn’t validate against your newly-created ca cert. A `finally` for `.terminate()` would help; a fancier version could be a context manager for running the external `datasette` process that could: - ensure the process always exited when no longer needed - if you want to be fancy, call `terminate()`, `wait()` for a short timeout for the process to exit, then try `kill()` and `wait()` again; raise an exception complaining about the seemingly-unkillable process if all that fails - raise an error if the process exited with a non-zero error code; here it’s likely that some `datasette` executions were secretly failing with `[Errno 48] error while attempting to bind on address ('127.0.0.1', 8042): address already in use`","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1356640463,https://api.github.com/repos/simonw/datasette/issues/1955,1356640463,IC_kwDOBm6k_c5Q3LDP,9599,simonw,2022-12-18T02:45:18Z,2022-12-18T02:45:18Z,OWNER,"... and with this change, the following now works correctly: ``` % datasette install datasette-gunicorn % datasette gunicorn fixtures.db -p 8855 [2022-12-17 18:44:29 -0800] [7651] [INFO] Starting gunicorn 20.1.0 [2022-12-17 18:44:29 -0800] [7651] [INFO] Listening at: http://127.0.0.1:8855 (7651) [2022-12-17 18:44:29 -0800] [7651] [INFO] Using worker: uvicorn.workers.UvicornWorker [2022-12-17 18:44:29 -0800] [7653] [INFO] Booting worker with pid: 7653 [2022-12-17 18:44:29 -0800] [7653] [INFO] Started server process [7653] [2022-12-17 18:44:29 -0800] [7653] [INFO] Waiting for application startup. [2022-12-17 18:44:29 -0800] [7653] [INFO] Application startup complete. ``` So this issue is now fixed!","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1356640266,https://api.github.com/repos/simonw/datasette/issues/1955,1356640266,IC_kwDOBm6k_c5Q3LAK,9599,simonw,2022-12-18T02:43:00Z,2022-12-18T02:43:00Z,OWNER,"https://github.com/simonw/datasette/actions/runs/3722908296/jobs/6314093163 shows that new test passing in CI: ``` Generated a certificate for 'localhost', '127.0.0.1', '::1' Configure your server to use the following files: cert=/home/runner/work/datasette/datasette/server.pem key=/home/runner/work/datasette/datasette/server.key Configure your client to use the following files: cert=/home/runner/work/datasette/datasette/client.pem INFO: Started server process [4036] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on https://127.0.0.1:8152/ (Press CTRL+C to quit) % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed INFO: 127.0.0.1:56726 - ""GET /_memory.json HTTP/1.1"" 200 OK 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 100 213 0 213 0 0 11542 0 --:--:-- --:--:-- --:--:-- 11833 INFO: Shutting down INFO: Waiting for application shutdown. INFO: Application shutdown complete. INFO: Finished server process [4036] {""database"": ""_memory"", ""private"": false, ""path"": ""/_memory"", ""size"": 0, ""tables"": [], ""hidden_count"": 0, ""views"": [], ""queries"": [], ""allow_execute_sql"": true, ""table_columns"": {}, ""query_ms"": 1.4545189999921604}0 ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1356633937,https://api.github.com/repos/simonw/datasette/issues/1955,1356633937,IC_kwDOBm6k_c5Q3JdR,9599,simonw,2022-12-18T02:28:22Z,2022-12-18T02:28:22Z,OWNER,"This `bash` script does the job: ```bash #!/bin/bash # Generate certificates python -m trustme # This creates server.pem, server.key, client.pem # Start the server in the background datasette --memory \ --ssl-keyfile=server.key \ --ssl-certfile=server.pem \ -p 8152 & # Store the background process ID in a variable server_pid=$! # Wait for the server to start sleep 2 # Make a test request using curl curl -f --cacert client.pem 'https://localhost:8152/_memory.json' # Save curl's exit code (-f option causes it to return one on HTTP errors) curl_exit_code=$? # Shut down the server kill $server_pid sleep 1 # Clean up the certificates rm server.pem server.key client.pem echo $curl_exit_code exit $curl_exit_code ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1356630092,https://api.github.com/repos/simonw/datasette/issues/1955,1356630092,IC_kwDOBm6k_c5Q3IhM,9599,simonw,2022-12-18T02:20:01Z,2022-12-18T02:20:01Z,OWNER,"Asked ChatGPT: > Write a bash script which starts a server in the background using ""datasette -p 8002"", then uses curl to make a test request against it, then shuts the server down again at the end It gave me: ```bash #!/bin/bash # Start the server in the background datasette -p 8002 & # Store the background process ID in a variable server_pid=$! # Make a test request using curl curl http://localhost:8002 # Shut down the server kill $server_pid ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1356629783,https://api.github.com/repos/simonw/datasette/issues/1955,1356629783,IC_kwDOBm6k_c5Q3IcX,9599,simonw,2022-12-18T02:18:43Z,2022-12-18T02:18:43Z,OWNER,"Various attempts at a fix which didn't work: ```diff diff --git a/tests/conftest.py b/tests/conftest.py index 69dee68b..899d36fd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,3 @@ -import asyncio import httpx import os import pathlib @@ -6,6 +5,7 @@ import pytest import pytest_asyncio import re import subprocess +import sys import tempfile import time import trustme @@ -27,13 +27,23 @@ UNDOCUMENTED_PERMISSIONS = { _ds_client = None -def wait_until_responds(url, timeout=5.0, client=httpx, **kwargs): +def wait_until_responds(url, timeout=5.0, client=None, **kwargs): + client = client or httpx.Client(**kwargs) start = time.time() while time.time() - start < timeout: try: - client.get(url, **kwargs) + if ""verify"" in kwargs: + print(kwargs[""verify""]) + print( + ""Contents of verify file: {}"".format( + open(kwargs.get(""verify"")).read() + ) + ) + print(""client = {}, kwargs = {}"".format(client, kwargs)) + client.get(url) return - except httpx.ConnectError: + except (httpx.ConnectError, httpx.RemoteProtocolError) as ex: + print(ex) time.sleep(0.1) raise AssertionError(""Timed out waiting for {} to respond"".format(url)) @@ -166,7 +176,7 @@ def check_permission_actions_are_documented(): @pytest.fixture(scope=""session"") def ds_localhost_http_server(): ds_proc = subprocess.Popen( - [""datasette"", ""--memory"", ""-p"", ""8041""], + [sys.executable, ""-m"", ""datasette"", ""--memory"", ""-p"", ""8041""], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, # Avoid FileNotFoundError: [Errno 2] No such file or directory: @@ -180,7 +190,7 @@ def ds_localhost_http_server(): ds_proc.terminate() -@pytest.fixture(scope=""session"") +@pytest.fixture def ds_localhost_https_server(tmp_path_factory): cert_directory = tmp_path_factory.mktemp(""certs"") ca = trustme.CA() @@ -194,6 +204,8 @@ def ds_localhost_https_server(tmp_path_factory): ca.cert_pem.write_to_path(path=client_cert) ds_proc = subprocess.Popen( [ + sys.executable, + ""-m"", ""datasette"", ""--memory"", ""-p"", @@ -207,7 +219,11 @@ def ds_localhost_https_server(tmp_path_factory): stderr=subprocess.STDOUT, cwd=tempfile.gettempdir(), ) - wait_until_responds(""http://localhost:8042/"", verify=client_cert) + wait_until_responds( + ""http://localhost:8042/_memory.json"", + verify=client_cert, + headers={""Connection"": ""close""}, + ) # Check it started successfully assert not ds_proc.poll(), ds_proc.stdout.read().decode(""utf-8"") yield ds_proc, client_cert diff --git a/tests/test_cli_serve_server.py b/tests/test_cli_serve_server.py index 1c31e2a3..9320b623 100644 --- a/tests/test_cli_serve_server.py +++ b/tests/test_cli_serve_server.py @@ -16,7 +16,11 @@ def test_serve_localhost_http(ds_localhost_http_server): @pytest.mark.serial def test_serve_localhost_https(ds_localhost_https_server): _, client_cert = ds_localhost_https_server - response = httpx.get(""https://localhost:8042/_memory.json"", verify=client_cert) + response = httpx.get( + ""https://localhost:8042/_memory.json"", + verify=client_cert, + headers={""Connection"": ""close""}, + ) assert { ""database"": ""_memory"", ""path"": ""/_memory"", ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1356627931,https://api.github.com/repos/simonw/datasette/issues/1955,1356627931,IC_kwDOBm6k_c5Q3H_b,9599,simonw,2022-12-18T02:13:01Z,2022-12-18T02:13:01Z,OWNER,"Rather than continue to bang my head against this, I'm tempted to rewrite this test to happen outside of Python world - in a bash script run by GitHub Actions, for example.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1356627331,https://api.github.com/repos/simonw/datasette/issues/1955,1356627331,IC_kwDOBm6k_c5Q3H2D,9599,simonw,2022-12-18T02:11:17Z,2022-12-18T02:11:17Z,OWNER,"This issue might be relevant, but I tried the suggested fix in there (`Connection: close` on the incoming requests) and it didn't fix my problem: - https://github.com/encode/httpx/discussions/2056","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1356626334,https://api.github.com/repos/simonw/datasette/issues/1955,1356626334,IC_kwDOBm6k_c5Q3Hme,9599,simonw,2022-12-18T02:04:01Z,2022-12-18T02:04:07Z,OWNER,"I used the steps to test manually from this comment: https://github.com/simonw/datasette/issues/1221#issuecomment-777901052 In one terminal: ``` cd /tmp python -m trustme datasette --memory --ssl-keyfile=/tmp/server.key --ssl-certfile=/tmp/server.pem -p 8003 ``` Then in another terminal: ``` curl --cacert /tmp/client.pem 'https://localhost:8003/_memory.json' ``` This worked correctly, outputting the expected JSON. So the feature still works, it's just the test that is broken for some reason.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1356625642,https://api.github.com/repos/simonw/datasette/issues/1955,1356625642,IC_kwDOBm6k_c5Q3Hbq,9599,simonw,2022-12-18T02:00:57Z,2022-12-18T02:00:57Z,OWNER,"I added the TLS support here: - https://github.com/simonw/datasette/issues/1221","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1356625556,https://api.github.com/repos/simonw/datasette/issues/1955,1356625556,IC_kwDOBm6k_c5Q3HaU,9599,simonw,2022-12-18T02:00:18Z,2022-12-18T02:00:18Z,OWNER,Maybe the reason the ASGI lifespan stuff broke was this line: https://github.com/simonw/datasette/blob/8b73fc6b47dffd8836f5c58aae1e57c1f66a5754/datasette/cli.py#L630-L632,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1356620233,https://api.github.com/repos/simonw/datasette/issues/1955,1356620233,IC_kwDOBm6k_c5Q3GHJ,9599,simonw,2022-12-18T01:31:10Z,2022-12-18T01:31:10Z,OWNER,"During the polling loop it constantly raises: `httpx.RemoteProtocolError`: Server disconnected without sending a response","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1356618913,https://api.github.com/repos/simonw/datasette/issues/1955,1356618913,IC_kwDOBm6k_c5Q3Fyh,9599,simonw,2022-12-18T01:29:05Z,2022-12-18T01:29:05Z,OWNER,"Now the only failure is in the `https` test - which fails like this (in CI and on my laptop): ``` message = str(exc) > raise mapped_exc(message) from exc E httpx.RemoteProtocolError: Server disconnected without sending a response. /opt/hostedtoolcache/Python/3.11.1/x64/lib/python3.11/site-packages/httpx/_transports/default.py:77: RemoteProtocolError =========================== short test summary info ============================ ERROR tests/test_cli_serve_server.py::test_serve_localhost_https - httpx.RemoteProtocolError: Server disconnected without sending a response. ================= 30 passed, 1264 deselected, 1 error in 6.15s ================= ``` That's this test: https://github.com/simonw/datasette/blob/63fb750f39cac6f49b451387fdff659ecd9edc5c/tests/test_cli_serve_server.py#L16-L24 And this fixture: https://github.com/simonw/datasette/blob/63fb750f39cac6f49b451387fdff659ecd9edc5c/tests/conftest.py#L178-L215 ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1356610089,https://api.github.com/repos/simonw/datasette/issues/1955,1356610089,IC_kwDOBm6k_c5Q3Dop,9599,simonw,2022-12-18T01:12:39Z,2022-12-18T01:12:39Z,OWNER,"... and it turns out those tests saved me. Because I forgot to check if `datasette` would actually start a server correctly! ``` % datasette fixtures.db -p 8852 INFO: Started server process [3538] INFO: Waiting for application startup. ERROR: Exception in 'lifespan' protocol Traceback (most recent call last): File ""/Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages/uvicorn/lifespan/on.py"", line 86, in main await app(scope, self.receive, self.send) File ""/Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py"", line 78, in __call__ return await self.app(scope, receive, send) File ""/Users/simon/Dropbox/Development/datasette/datasette/utils/asgi.py"", line 437, in __call__ return await self.asgi(scope, receive, send) File ""/Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages/asgi_csrf.py"", line 39, in app_wrapped_with_csrf await app(scope, receive, send) File ""/Users/simon/Dropbox/Development/datasette/datasette/app.py"", line 1457, in __call__ path = scope[""path""] KeyError: 'path' ERROR: Application startup failed. Exiting. ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1356609095,https://api.github.com/repos/simonw/datasette/issues/1955,1356609095,IC_kwDOBm6k_c5Q3DZH,9599,simonw,2022-12-18T01:10:43Z,2022-12-18T01:10:43Z,OWNER,"Improved version of that fixture: ```diff diff --git a/tests/conftest.py b/tests/conftest.py index 44c44f87..69dee68b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -27,6 +27,17 @@ UNDOCUMENTED_PERMISSIONS = { _ds_client = None +def wait_until_responds(url, timeout=5.0, client=httpx, **kwargs): + start = time.time() + while time.time() - start < timeout: + try: + client.get(url, **kwargs) + return + except httpx.ConnectError: + time.sleep(0.1) + raise AssertionError(""Timed out waiting for {} to respond"".format(url)) + + @pytest_asyncio.fixture async def ds_client(): from datasette.app import Datasette @@ -161,13 +172,7 @@ def ds_localhost_http_server(): # Avoid FileNotFoundError: [Errno 2] No such file or directory: cwd=tempfile.gettempdir(), ) - # Loop until port 8041 serves traffic - while True: - try: - httpx.get(""http://localhost:8041/"") - break - except httpx.ConnectError: - time.sleep(0.1) + wait_until_responds(""http://localhost:8041/"") # Check it started successfully assert not ds_proc.poll(), ds_proc.stdout.read().decode(""utf-8"") yield ds_proc @@ -202,12 +207,7 @@ def ds_localhost_https_server(tmp_path_factory): stderr=subprocess.STDOUT, cwd=tempfile.gettempdir(), ) - while True: - try: - httpx.get(""https://localhost:8042/"", verify=client_cert) - break - except httpx.ConnectError: - time.sleep(0.1) + wait_until_responds(""http://localhost:8042/"", verify=client_cert) # Check it started successfully assert not ds_proc.poll(), ds_proc.stdout.read().decode(""utf-8"") yield ds_proc, client_cert @@ -231,12 +231,7 @@ def ds_unix_domain_socket_server(tmp_path_factory): # Poll until available transport = httpx.HTTPTransport(uds=uds) client = httpx.Client(transport=transport) - while True: - try: - client.get(""http://localhost/_memory.json"") - break - except httpx.ConnectError: - time.sleep(0.1) + wait_until_responds(""http://localhost/_memory.json"", client=client) # Check it started successfully assert not ds_proc.poll(), ds_proc.stdout.read().decode(""utf-8"") yield ds_proc, uds ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1356600917,https://api.github.com/repos/simonw/datasette/issues/1955,1356600917,IC_kwDOBm6k_c5Q3BZV,9599,simonw,2022-12-18T01:02:26Z,2022-12-18T01:02:26Z,OWNER,"This bit here looks like it could hang! ```python # Loop until port 8041 serves traffic while True: try: httpx.get(""http://localhost:8041/"") break except httpx.ConnectError: time.sleep(0.1) ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1356599930,https://api.github.com/repos/simonw/datasette/issues/1955,1356599930,IC_kwDOBm6k_c5Q3BJ6,9599,simonw,2022-12-18T01:01:47Z,2022-12-18T01:01:47Z,OWNER,"I think that's this test: https://github.com/simonw/datasette/blob/63fb750f39cac6f49b451387fdff659ecd9edc5c/tests/test_cli_serve_server.py#L6-L13 Using this fixture: https://github.com/simonw/datasette/blob/63fb750f39cac6f49b451387fdff659ecd9edc5c/tests/conftest.py#L155-L175","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1356596740,https://api.github.com/repos/simonw/datasette/issues/1955,1356596740,IC_kwDOBm6k_c5Q3AYE,9599,simonw,2022-12-18T00:59:47Z,2022-12-18T00:59:47Z,OWNER,"Hitting `Ctrl+C` while using `--full-trace` gave me more clues: ``` % pytest -m serial tests/test_cli_serve_server.py --full-trace ======================================================= test session starts ======================================================== platform darwin -- Python 3.10.3, pytest-7.1.3, pluggy-1.0.0 SQLite: 3.39.4 rootdir: /Users/simon/Dropbox/Development/datasette, configfile: pytest.ini plugins: anyio-3.6.1, xdist-2.5.0, forked-1.4.0, asyncio-0.19.0, timeout-2.1.0, profiling-1.7.0 asyncio: mode=strict collected 3 items tests/test_cli_serve_server.py ^C^C ====================================================== no tests ran in 3.49s ======================================================= Traceback (most recent call last): File ""/Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages/httpcore/_exceptions.py"", line 8, in map_exceptions yield File ""/Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages/httpcore/backends/sync.py"", line 86, in connect_tcp sock = socket.create_connection( File ""/Users/simon/.pyenv/versions/3.10.3/lib/python3.10/socket.py"", line 845, in create_connection raise err File ""/Users/simon/.pyenv/versions/3.10.3/lib/python3.10/socket.py"", line 833, in create_connection sock.connect(sa) ConnectionRefusedError: [Errno 61] Connection refused [...] ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1356595665,https://api.github.com/repos/simonw/datasette/issues/1955,1356595665,IC_kwDOBm6k_c5Q3AHR,9599,simonw,2022-12-18T00:58:16Z,2022-12-18T00:58:16Z,OWNER,"`pytest -m serial` on my Mac laptop also freezes: ``` (datasette) datasette % pytest -m serial ======================================================= test session starts ======================================================== platform darwin -- Python 3.10.3, pytest-7.1.3, pluggy-1.0.0 SQLite: 3.39.4 rootdir: /Users/simon/Dropbox/Development/datasette, configfile: pytest.ini plugins: anyio-3.6.1, xdist-2.5.0, forked-1.4.0, asyncio-0.19.0, timeout-2.1.0, profiling-1.7.0 asyncio: mode=strict collected 1295 items / 1264 deselected / 31 selected tests/test_package.py . [ 3%] tests/test_cli_serve_server.py ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1356489200,https://api.github.com/repos/simonw/datasette/issues/1955,1356489200,IC_kwDOBm6k_c5Q2mHw,9599,simonw,2022-12-17T22:29:51Z,2022-12-17T22:29:51Z,OWNER,"No, it still causes the tests to hang (I let them run for 12 minutes): Interesting that the regular tests passed an then the `pytest -m serial` ones seem to have failed.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1356487139,https://api.github.com/repos/simonw/datasette/issues/1955,1356487139,IC_kwDOBm6k_c5Q2lnj,9599,simonw,2022-12-17T22:16:52Z,2022-12-17T22:16:52Z,OWNER,"I'm trying this fix again, after a bunch of work on the test suite in: - #1959","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1353701674,https://api.github.com/repos/simonw/datasette/issues/1955,1353701674,IC_kwDOBm6k_c5Qr9kq,9599,simonw,2022-12-15T21:00:51Z,2022-12-15T21:00:51Z,OWNER,"OK, I've broken the test suite here. I'm going to revert these two commits: - https://github.com/simonw/datasette/commit/dc18f62089e5672d03176f217d7840cdafa5c447 - https://github.com/simonw/datasette/commit/51ee8caa4a697fa3f4120e93b1c205b714a6cdc7 Then I'll do a bunch of work making the test suite more robust before I try this again.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1353694582,https://api.github.com/repos/simonw/datasette/issues/1955,1353694582,IC_kwDOBm6k_c5Qr712,9599,simonw,2022-12-15T20:52:46Z,2022-12-15T20:52:46Z,OWNER,"Just noticed this: https://github.com/simonw/datasette/actions/runs/3706504228/jobs/6281796135 This suggests that the regular tests passed in CI fine, but the non-serial ones failed. I'm going to try running everything using `pytest -n auto` without splitting serial and non-serial tests. Maybe the serial thing isn't needed any more?","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1353683238,https://api.github.com/repos/simonw/datasette/issues/1955,1353683238,IC_kwDOBm6k_c5Qr5Em,9599,simonw,2022-12-15T20:42:18Z,2022-12-15T20:42:18Z,OWNER,"Possibly related issue: - https://github.com/pytest-dev/pytest-xdist/issues/60","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1353680261,https://api.github.com/repos/simonw/datasette/issues/1955,1353680261,IC_kwDOBm6k_c5Qr4WF,9599,simonw,2022-12-15T20:39:19Z,2022-12-15T20:39:19Z,OWNER,"When I hit `Ctr+C` here's the traceback I get: ``` ^C^CException ignored in: Traceback (most recent call last): File ""/Users/simon/.pyenv/versions/3.10.3/lib/python3.10/threading.py"", line 1530, in _shutdown !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! KeyboardInterrupt !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! /Users/simon/.pyenv/versions/3.10.3/lib/python3.10/threading.py:324: KeyboardInterrupt (to show a full traceback on KeyboardInterrupt use --full-trace) Traceback (most recent call last): File ""/Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/bin/pytest"", line 8, in atexit_call() File ""/Users/simon/.pyenv/versions/3.10.3/lib/python3.10/concurrent/futures/thread.py"", line 31, in _python_exit sys.exit(console_main()) File ""/Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages/_pytest/config/__init__.py"", line 187, in console_main t.join() File ""/Users/simon/.pyenv/versions/3.10.3/lib/python3.10/threading.py"", line 1089, in join self._wait_for_tstate_lock() File ""/Users/simon/.pyenv/versions/3.10.3/lib/python3.10/threading.py"", line 1109, in _wait_for_tstate_lock if lock.acquire(block, timeout): KeyboardInterrupt: code = main() File ""/Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages/_pytest/config/__init__.py"", line 164, in main ret: Union[ExitCode, int] = config.hook.pytest_cmdline_main( File ""/Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages/pluggy/_hooks.py"", line 265, in __call__ return self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult) File ""/Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages/pluggy/_manager.py"", line 80, in _hookexec return self._inner_hookexec(hook_name, methods, kwargs, firstresult) File ""/Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages/pluggy/_callers.py"", line 60, in _multicall return outcome.get_result() File ""/Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages/pluggy/_result.py"", line 60, in get_result raise ex[1].with_traceback(ex[2]) File ""/Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages/pluggy/_callers.py"", line 39, in _multicall res = hook_impl.function(*args) File ""/Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages/_pytest/main.py"", line 315, in pytest_cmdline_main return wrap_session(config, _main) File ""/Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages/_pytest/main.py"", line 303, in wrap_session config.hook.pytest_sessionfinish( File ""/Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages/pluggy/_hooks.py"", line 265, in __call__ return self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult) File ""/Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages/pluggy/_manager.py"", line 80, in _hookexec return self._inner_hookexec(hook_name, methods, kwargs, firstresult) File ""/Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages/pluggy/_callers.py"", line 55, in _multicall gen.send(outcome) File ""/Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages/_pytest/terminal.py"", line 798, in pytest_sessionfinish outcome.get_result() File ""/Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages/pluggy/_result.py"", line 60, in get_result raise ex[1].with_traceback(ex[2]) File ""/Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages/pluggy/_callers.py"", line 39, in _multicall res = hook_impl.function(*args) File ""/Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages/xdist/dsession.py"", line 88, in pytest_sessionfinish nm.teardown_nodes() File ""/Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages/xdist/workermanage.py"", line 79, in teardown_nodes self.group.terminate(self.EXIT_TIMEOUT) File ""/Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages/execnet/multi.py"", line 215, in terminate safe_terminate( File ""/Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages/execnet/multi.py"", line 311, in safe_terminate reply.get() File ""/Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages/execnet/gateway_base.py"", line 206, in get self.waitfinish(timeout) File ""/Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages/execnet/gateway_base.py"", line 213, in waitfinish if not self._result_ready.wait(timeout): File ""/Users/simon/.pyenv/versions/3.10.3/lib/python3.10/threading.py"", line 600, in wait signaled = self._cond.wait(timeout) File ""/Users/simon/.pyenv/versions/3.10.3/lib/python3.10/threading.py"", line 320, in wait waiter.acquire() KeyboardInterrupt ``` It looks to me like this relates to `pytest-xdist` istelf - it's waiting on some locks but `site-packages/xdist/workermanage.py` shows up in that track.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1353516572,https://api.github.com/repos/simonw/datasette/issues/1955,1353516572,IC_kwDOBm6k_c5QrQYc,9599,simonw,2022-12-15T18:15:28Z,2022-12-15T18:15:28Z,OWNER,"I added `return` to the first line of that test to disable it, then ran again - and now it's hanging at about the same progress point through the tests but in a different test: ![Image](https://user-images.githubusercontent.com/9599/207936587-30ebf780-c0da-4e62-b20b-e274e0adaa19.png) So this time it was hanging at `test_urlsafe_components()`. So it's clearly not the individual tests themselves that are the problem - something about running the entire test suite in one go is incompatible with this change for some reason.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1353512099,https://api.github.com/repos/simonw/datasette/issues/1955,1353512099,IC_kwDOBm6k_c5QrPSj,9599,simonw,2022-12-15T18:11:27Z,2022-12-15T18:11:27Z,OWNER,"This is surprising! ![Image](https://user-images.githubusercontent.com/9599/207935885-e1f51983-0621-4490-86a6-fafd4c876f41.png) The logs suggest that the test suite hung running this test here: https://github.com/simonw/datasette/blob/dc18f62089e5672d03176f217d7840cdafa5c447/tests/test_utils.py#L55-L58 I find that very hard to believe.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1353509776,https://api.github.com/repos/simonw/datasette/issues/1955,1353509776,IC_kwDOBm6k_c5QrOuQ,9599,simonw,2022-12-15T18:09:26Z,2022-12-15T18:09:26Z,OWNER,"I added this to `conftest.py`: ```python @pytest.fixture(autouse=True) def log_name_of_test_before_test(request): # To help identify tests that are hanging name = str(request.node) with open(""/tmp/test.log"", ""a"") as f: f.write(name + ""\n"") yield ``` This logs out the name of each test to `/tmp/test.log` before running the test - so I can wait until it hangs and see which test it was that caused that.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1353473571,https://api.github.com/repos/simonw/datasette/issues/1955,1353473571,IC_kwDOBm6k_c5QrF4j,9599,simonw,2022-12-15T17:43:28Z,2022-12-15T17:43:48Z,OWNER,"Running: pytest -n auto -x -v On may laptop to see if I can replicate.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1353473086,https://api.github.com/repos/simonw/datasette/issues/1955,1353473086,IC_kwDOBm6k_c5QrFw-,9599,simonw,2022-12-15T17:43:08Z,2022-12-15T17:43:08Z,OWNER,It looks like that fix _almost_ works... except it seems to push the tests into an infinite loop or similar? They're not finishing their runs from what I can see.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1353448095,https://api.github.com/repos/simonw/datasette/issues/1955,1353448095,IC_kwDOBm6k_c5Qq_qf,9599,simonw,2022-12-15T17:25:05Z,2022-12-15T17:25:05Z,OWNER,"So actually that `setup_db()` function I wrote back in 2019 has not been executing for most of Datasette's tests. Which seems bad. I'm inclined to ditch `AsgiLifespan` entirely in favour of the mechanism I described above, where `invoke_startup()` is called for every request on the first request processed by the server.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1353443718,https://api.github.com/repos/simonw/datasette/issues/1955,1353443718,IC_kwDOBm6k_c5Qq-mG,9599,simonw,2022-12-15T17:23:12Z,2022-12-15T17:23:55Z,OWNER,"That may not be the best fix here. It turns out this pattern: ```python async def get(self, path, **kwargs): async with httpx.AsyncClient(app=self.app) as client: return await client.get(self._fix(path), **kwargs) ``` Doesn't trigger that `AsgiLifespan` class. I wrote about that previously in this TIL: https://til.simonwillison.net/asgi/lifespan-test-httpx","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1353423584,https://api.github.com/repos/simonw/datasette/issues/1955,1353423584,IC_kwDOBm6k_c5Qq5rg,9599,simonw,2022-12-15T17:13:18Z,2022-12-15T17:22:59Z,OWNER,"Wow, just spotted this in the code - it turns out I solved this problem a different (and better) way long before i introduced `invoke_startup()`! https://github.com/simonw/datasette/blob/e054704fb64d1f23154ec43b81b6c9481ff8202f/datasette/app.py#L1416-L1440","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1352674924,https://api.github.com/repos/simonw/datasette/issues/1955,1352674924,IC_kwDOBm6k_c5QoC5s,9599,simonw,2022-12-15T07:46:36Z,2022-12-15T07:46:36Z,OWNER,"It's possible the fix for this might be for the first incoming HTTP request to trigger `invoke_startup()` if it hasn't been called yet - similar to the hack I put in place for `datasette.client.get()` in tests: https://github.com/simonw/datasette/blob/e054704fb64d1f23154ec43b81b6c9481ff8202f/datasette/app.py#L1728-L1731 This would be a much more elegant fix, I could remove those multiple `invoke_startup()` calls entirely - and remove this tip from the documentation too: https://docs.datasette.io/en/0.63.2/testing_plugins.html#setting-up-a-datasette-test-instance","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1352643333,https://api.github.com/repos/simonw/datasette/issues/1955,1352643333,IC_kwDOBm6k_c5Qn7MF,9599,simonw,2022-12-15T07:07:29Z,2022-12-15T07:07:29Z,OWNER,"Datasette 0.63 is the release that broke this, thanks to this issue: - https://github.com/simonw/datasette/issues/1809","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1955#issuecomment-1352643049,https://api.github.com/repos/simonw/datasette/issues/1955,1352643049,IC_kwDOBm6k_c5Qn7Hp,9599,simonw,2022-12-15T07:07:10Z,2022-12-15T07:07:10Z,OWNER,"This is definitely a regression: Datasette is meant to work in those environments, and I didn't think to test them when I added the `invoke_startup()` hook. Coincidentally I actually built a plugin for running Datasette with Gunicorn just a couple of months ago: https://datasette.io/plugins/datasette-gunicorn And I just tested and it has the same bug you describe here! Filed: - https://github.com/simonw/datasette-gunicorn/issues/5 ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1496652622,"invoke_startup() is not run in some conditions, e.g. gunicorn/uvicorn workers, breaking lots of things", https://github.com/simonw/datasette/issues/1953#issuecomment-1352459146,https://api.github.com/repos/simonw/datasette/issues/1953,1352459146,IC_kwDOBm6k_c5QnOOK,9599,simonw,2022-12-15T02:02:15Z,2022-12-15T02:02:15Z,OWNER,"``` The third Datasette 1.0 alpha release adds upsert support to the JSON API, plus the ability to specify finely grained permissions when creating an API token. - New `/db/table/-/upsert` API, [documented here](https://docs.datasette.io/en/latest/json_api.html#tableupsertview). upsert is an update-or-replace: existing rows will have specified keys updated, but if no row matches the incoming primary key a brand new row will be inserted instead. ([#1878](https://github.com/simonw/datasette/issues/1878)) - New [register_permissions(datasette)](https://docs.datasette.io/en/latest/plugin_hooks.html#plugin-register-permissions) plugin hook. Plugins can now register named permissions, which will then be listed in various interfaces that show available permissions. ([#1940](https://github.com/simonw/datasette/issues/1940)) - The `/db/-/create` API for [creating a table](https://docs.datasette.io/en/latest/json_api.html#tablecreateview) now accepts `""ignore"": true` and `""replace"": true` options when called with the `""rows""` property that creates a new table based on an example set of rows. This means the API can be called multiple times with different rows, setting rules for what should happen if a primary key collides with an existing row. ([#1927](https://github.com/simonw/datasette/issues/1927)) - Arbitrary permissions can now be configured at the instance, database and resource (table, SQL view or canned query) level in Datasette's [Metadata](https://docs.datasette.io/en/latest/metadata.html#metadata) JSON and YAML files. The new `""permissions""` key can be used to specify which actors should have which permissions. See [Other permissions in metadata](https://docs.datasette.io/en/latest/authentication.html#authentication-permissions-other) for details. ([#1636](https://github.com/simonw/datasette/issues/1636)) - The `/-/create-token` page can now be used to create API tokens which are restricted to just a subset of actions, including against specific databases or resources. See [API Tokens](https://docs.datasette.io/en/latest/authentication.html#createtokenview) for details. ([#1947](https://github.com/simonw/datasette/issues/1947)) - Likewise, the `datasette create-token` CLI command can now create tokens with [a subset of permissions](https://docs.datasette.io/en/latest/authentication.html#authentication-cli-create-token-restrict). ([#1855](https://github.com/simonw/datasette/issues/1855)) - New datasette.create_token() API method ` for programmatically creating signed API tokens. ([#1951](https://github.com/simonw/datasette/issues/1951)) - `/db/-/create` API now requires actor to have `insert-row` permission in order to use the `""row""` or `""rows""` properties. ([#1937](https://github.com/simonw/datasette/issues/1937)) ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1495821607,Release notes for Datasette 1.0a2, https://github.com/simonw/datasette/issues/1953#issuecomment-1352410078,https://api.github.com/repos/simonw/datasette/issues/1953,1352410078,IC_kwDOBm6k_c5QnCPe,9599,simonw,2022-12-15T00:44:56Z,2022-12-15T00:44:56Z,OWNER,"Highlights: - `/db/table/-/upsert` - ignore and replace for `/db/-/create` - `register_permissions()` plugin hook - `datasette create-token` can create restricted tokens - `/-/create-token` can too - `datasette --get --token` option - `datasette.create_token()` API method Plus some smaller things.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1495821607,Release notes for Datasette 1.0a2, https://github.com/simonw/datasette/issues/1952#issuecomment-1350415644,https://api.github.com/repos/simonw/datasette/issues/1952,1350415644,IC_kwDOBm6k_c5QfbUc,9599,simonw,2022-12-14T05:22:59Z,2022-12-14T05:22:59Z,OWNER,"Non-memory named databases shouldn't show write actions, since those won't persist.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1495716243,Improvements to /-/create-token restrictions interface, https://github.com/simonw/datasette/issues/1951#issuecomment-1350293098,https://api.github.com/repos/simonw/datasette/issues/1951,1350293098,IC_kwDOBm6k_c5Qe9Zq,9599,simonw,2022-12-14T02:43:44Z,2022-12-14T02:43:44Z,OWNER,Documentation for the new method: https://docs.datasette.io/en/latest/internals.html#create-token-actor-id-expires-after-none-restrict-all-none-restrict-database-none-restrict-resource-none,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1495431932,`datasette.create_token(...)` method for creating signed API tokens, https://github.com/simonw/datasette/issues/1951#issuecomment-1350231654,https://api.github.com/repos/simonw/datasette/issues/1951,1350231654,IC_kwDOBm6k_c5QeuZm,9599,simonw,2022-12-14T01:48:50Z,2022-12-14T01:48:57Z,OWNER,I like that the word `restrict` reflects the `_r` in the actor/token.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1495431932,`datasette.create_token(...)` method for creating signed API tokens, https://github.com/simonw/datasette/issues/1951#issuecomment-1350222701,https://api.github.com/repos/simonw/datasette/issues/1951,1350222701,IC_kwDOBm6k_c5QesNt,9599,simonw,2022-12-14T01:35:05Z,2022-12-14T01:35:22Z,OWNER,"Maybe this: ```python datasette.create_token(""root"", expires_after=3600, restrict_all=(""view-query"", ""view-table"")) token = datasette.create_token(""root"", expires_after=3600, restrict_database={ ""fixtures"": (""view-query"",) }) token = datasette.create_token(""root"", expires_after=3600, restrict_resource={ ""fixtures"": { ""facetable"": (""insert-row"", ""update-row"") } }) ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1495431932,`datasette.create_token(...)` method for creating signed API tokens, https://github.com/simonw/datasette/issues/1951#issuecomment-1350220579,https://api.github.com/repos/simonw/datasette/issues/1951,1350220579,IC_kwDOBm6k_c5Qersj,9599,simonw,2022-12-14T01:31:38Z,2022-12-14T01:31:38Z,OWNER,"The problem with `all=(..)` is it feels misleading - it's actually restricting the permissions made available to the token. Likewise, `databases=` being a dict of restricted permissions isn't completely obvious. And the nested `tables=` dictionary feels a bit odd too.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1495431932,`datasette.create_token(...)` method for creating signed API tokens, https://github.com/simonw/datasette/issues/1951#issuecomment-1350218177,https://api.github.com/repos/simonw/datasette/issues/1951,1350218177,IC_kwDOBm6k_c5QerHB,9599,simonw,2022-12-14T01:27:45Z,2022-12-14T01:30:41Z,OWNER,"Some sketches: ```python # Token for root user token = datasette.create_token(""root"") # Expiring in an hour token = datasette.create_token(""root"", expires_after=3600) ``` More complicated is when you want to restrict to specific permissions: ```python # Limited to view-query and view-table token = datasette.create_token(""root"", expires_after=3600, all=(""view-query"", ""view-table"")) # I'm not sure about that all= name # Limits within a specific database: token = datasette.create_token(""root"", expires_after=3600, databases={ ""fixtures"": (""view-query"",) }) # And specific tables: token = datasette.create_token(""root"", expires_after=3600, tables={ ""fixtures"": { ""facetable"": (""insert-row"", ""update-row"") } }) ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1495431932,`datasette.create_token(...)` method for creating signed API tokens, https://github.com/simonw/datasette/issues/1951#issuecomment-1350217380,https://api.github.com/repos/simonw/datasette/issues/1951,1350217380,IC_kwDOBm6k_c5Qeq6k,9599,simonw,2022-12-14T01:26:22Z,2022-12-14T01:26:22Z,OWNER,"It's going to look very similar to the CLI tool, at least in terms of capabilities: ``` Usage: datasette create-token [OPTIONS] ID Create a signed API token for the specified actor ID Example: datasette create-token root --secret mysecret To allow only ""view-database-download"" for all databases: datasette create-token root --secret mysecret \ --all view-database-download To allow ""create-table"" against a specific database: datasette create-token root --secret mysecret \ --database mydb create-table To allow ""insert-row"" against a specific table: datasette create-token root --secret myscret \ --resource mydb mytable insert-row Restricted actions can be specified multiple times using multiple --all, --database, and --resource options. Add --debug to see a decoded version of the token. ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1495431932,`datasette.create_token(...)` method for creating signed API tokens, https://github.com/simonw/datasette/issues/1950#issuecomment-1349864950,https://api.github.com/repos/simonw/datasette/issues/1950,1349864950,IC_kwDOBm6k_c5QdU32,9599,simonw,2022-12-13T22:11:15Z,2022-12-13T22:11:15Z,OWNER,"Most places I use that exception at the moment set their own non-500 status error: ``` datasette % rg DatasetteError datasette/handle_exception.py 7:from .views.base import DatasetteError 33: elif isinstance(exception, DatasetteError): datasette/filters.py 2:from datasette.views.base import DatasetteError 22: raise DatasetteError(""_where= is not allowed"", status=403) 141: raise DatasetteError( datasette/views/table.py 34:from .base import BaseView, DataView, DatasetteError, ureg, _error 178: raise DatasetteError( 192: raise DatasetteError( 390: raise DatasetteError(""Cannot use _sort and _sort_desc at the same time"") 394: raise DatasetteError(f""Cannot sort table by {sort}"") 400: raise DatasetteError(f""Cannot sort table by {sort_desc}"") datasette/views/base.py 39:class DatasetteError(Exception): 219: raise DatasetteError(str(e), title=""Invalid SQL"", status=400) 222: raise DatasetteError(str(e)) 224: except DatasetteError: 382: raise DatasetteError( 402: raise DatasetteError(str(e), title=""Invalid SQL"", status=400) 405: raise DatasetteError(str(e)) 407: except DatasetteError: datasette/views/table2.py 28:from .base import DataView, DatasetteError, ureg 296: raise DatasetteError( 310: raise DatasetteError( 472: raise DatasetteError(""Cannot use _sort and _sort_desc at the same time"") 476: raise DatasetteError(f""Cannot sort table by {sort}"") 482: raise DatasetteError(f""Cannot sort table by {sort_desc}"") datasette/views/database.py 31:from .base import BaseView, DatasetteError, DataView, _error 188: raise DatasetteError(""Invalid database"", status=404) 190: raise DatasetteError(""Cannot download in-memory databases"", status=404) 194: raise DatasetteError(""Cannot download database"", status=404) ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1495241162,"Bad ?_sort returns a 500 error, should be a 400", https://github.com/simonw/datasette/issues/1950#issuecomment-1349855620,https://api.github.com/repos/simonw/datasette/issues/1950,1349855620,IC_kwDOBm6k_c5QdSmE,9599,simonw,2022-12-13T22:08:50Z,2022-12-13T22:08:50Z,OWNER,https://github.com/simonw/datasette/blob/d4b98d3924dec625a99236e65b1b169ff957381f/datasette/views/table.py#L392-L400,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1495241162,"Bad ?_sort returns a 500 error, should be a 400", https://github.com/simonw/datasette/issues/1949#issuecomment-1352411327,https://api.github.com/repos/simonw/datasette/issues/1949,1352411327,IC_kwDOBm6k_c5QnCi_,9599,simonw,2022-12-15T00:46:27Z,2022-12-15T00:46:27Z,OWNER,"I got this far: ```diff diff --git a/datasette/handle_exception.py b/datasette/handle_exception.py index 8b7e83e3..31d41e00 100644 --- a/datasette/handle_exception.py +++ b/datasette/handle_exception.py @@ -54,7 +54,17 @@ def handle_exception(datasette, request, exception): headers = {} if datasette.cors: add_cors_headers(headers) - if request.path.split(""?"")[0].endswith("".json""): + # Return JSON error under certain conditions + should_return_json = ( + # URL ends in .json + request.path.split(""?"")[0].endswith("".json"") + or + # Hints from incoming request headers + request.headers.get(""content-type"") == ""application/json"" + or ""application/json"" in request.headers.get(""accept"", """") + ) + breakpoint() + if should_return_json: return Response.json(info, status=status, headers=headers) else: template = datasette.jinja_env.select_template(templates) diff --git a/tests/test_api_write.py b/tests/test_api_write.py index f27d143f..982543a6 100644 --- a/tests/test_api_write.py +++ b/tests/test_api_write.py @@ -1140,6 +1140,38 @@ async def test_create_table_permissions( assert data[""errors""] == expected_errors +@pytest.mark.asyncio +@pytest.mark.parametrize( + ""headers,expect_json"", + ( + ({}, False), + ({""Accept"": ""text/html""}, True), + ({""Accept"": ""application/json""}, True), + ({""Content-Type"": ""application/json""}, True), + ({""Accept"": ""application/json, text/plain, */*""}, True), + ({""Content-Type"": ""application/json""}, True), + ({""accept"": ""application/json, text/plain, */*""}, True), + ({""content-type"": ""application/json""}, True), + ), +) +async def test_permission_errors_html_and_json(ds_write, headers, expect_json): + request_headers = {""Authorization"": ""Bearer bad_token""} + request_headers.update(headers) + response = await ds_write.client.post( + ""/data/-/create"", + json={}, + headers=request_headers, + ) + assert response.status_code == 403 + if expect_json: + data = response.json() + assert data[""ok""] is False + assert data[""errors""] == [""Permission denied""] + else: + assert response.headers[""Content-Type""] == ""text/html; charset=utf-8"" + assert ""Permission denied"" in response.text + + @pytest.mark.asyncio @pytest.mark.parametrize( ""input,expected_rows_after"", ``` Then decided I would punt this until the next milestone.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493471221,`.json` errors should be returned as JSON, https://github.com/simonw/datasette/issues/1949#issuecomment-1352378370,https://api.github.com/repos/simonw/datasette/issues/1949,1352378370,IC_kwDOBm6k_c5Qm6gC,9599,simonw,2022-12-15T00:02:08Z,2022-12-15T00:04:54Z,OWNER,"I fixed this issue to help research this further: - https://github.com/simonw/datasette-ripgrep/issues/26 Now this search works: I wish I had this feature! - https://github.com/simonw/datasette-ripgrep/issues/24 Looks like I have both `_error()` and `_errors()` functions in there! ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493471221,`.json` errors should be returned as JSON, https://github.com/simonw/datasette/issues/1949#issuecomment-1352356356,https://api.github.com/repos/simonw/datasette/issues/1949,1352356356,IC_kwDOBm6k_c5Qm1IE,9599,simonw,2022-12-14T23:27:25Z,2022-12-14T23:28:16Z,OWNER,"Also weird: errors returned by that mechanism look like this: ```json { ""ok"": false, ""errors"": [""list of error messages""] } ``` While errors returned by the rest of Datasette look like this: https://latest.datasette.io/fixtures/no_table.json ```json { ""ok"": false, ""error"": ""Table not found: no_table"", ""status"": 404, ""title"": null } ``` Related: - #1875","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493471221,`.json` errors should be returned as JSON, https://github.com/simonw/datasette/issues/1949#issuecomment-1352354927,https://api.github.com/repos/simonw/datasette/issues/1949,1352354927,IC_kwDOBm6k_c5Qm0xv,9599,simonw,2022-12-14T23:25:06Z,2022-12-14T23:25:14Z,OWNER,"Looks like the code I've written for permission checking on `TableCreateView` and friends doesn't use the regular `raise Forbidden` or `raise DatasetteError` mechanisms - it does its own thing here: https://github.com/simonw/datasette/blob/9ad76d279e2c3874ca5070626a25458ce129f126/datasette/views/database.py#L580-L584 Which uses this: https://github.com/simonw/datasette/blob/9ad76d279e2c3874ca5070626a25458ce129f126/datasette/views/base.py#L547-L548 Having two different patterns to return errors is bad, I should fix that.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493471221,`.json` errors should be returned as JSON, https://github.com/simonw/datasette/issues/1949#issuecomment-1352340518,https://api.github.com/repos/simonw/datasette/issues/1949,1352340518,IC_kwDOBm6k_c5QmxQm,9599,simonw,2022-12-14T23:07:01Z,2022-12-14T23:07:01Z,OWNER,"Easiest fix would be to look for `accept: application/json` and/or `content-type: application/json` headers. Not bullet-proof, so people might occasionally make JSON requests and get back an HTML error - but the documentation can tell people that they need to send those headers if they want to reliably get back JSON error messages. I'm happy with this as a solution.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493471221,`.json` errors should be returned as JSON, https://github.com/simonw/datasette/issues/1949#issuecomment-1352338620,https://api.github.com/repos/simonw/datasette/issues/1949,1352338620,IC_kwDOBm6k_c5Qmwy8,9599,simonw,2022-12-14T23:05:17Z,2022-12-14T23:05:17Z,OWNER,"Sniffing for a `{` is a little bit tricky though, as the post body is lazily loaded on request here: https://github.com/simonw/datasette/blob/9ad76d279e2c3874ca5070626a25458ce129f126/datasette/utils/asgi.py#L127-L135","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493471221,`.json` errors should be returned as JSON, https://github.com/simonw/datasette/issues/1949#issuecomment-1352335503,https://api.github.com/repos/simonw/datasette/issues/1949,1352335503,IC_kwDOBm6k_c5QmwCP,9599,simonw,2022-12-14T23:03:28Z,2022-12-14T23:03:28Z,OWNER,"This raises a more complicated issue At some point I'm likely to want to add an HTML interface for creating tables and inserting and updating rows. The obvious URLs for that are the same as for the JSON API: `/db/table/-/insert` and suchlike. Those endpoints are currently POST only - and can return JSON all the time. If they start accepting form POSTs too they'll need to be able to accept form-encoded data and return HTML instead. That's OK - they can detect incoming JSON thanks to the `content-type` header an the fact that the request body starts with `{` - but the `should_return_json` fix described above could intefere with how errors are returned if I'm not careful. I think it can still work though: I'll only set `should_return_json = True` if the endpoint gets a POST with a body starting `{`, or a content-type JSON header.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493471221,`.json` errors should be returned as JSON, https://github.com/simonw/datasette/issues/1949#issuecomment-1352331314,https://api.github.com/repos/simonw/datasette/issues/1949,1352331314,IC_kwDOBm6k_c5QmvAy,9599,simonw,2022-12-14T22:59:36Z,2022-12-14T22:59:36Z,OWNER,I'm going to prototype that up to see what it looks like.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493471221,`.json` errors should be returned as JSON, https://github.com/simonw/datasette/issues/1949#issuecomment-1352330825,https://api.github.com/repos/simonw/datasette/issues/1949,1352330825,IC_kwDOBm6k_c5Qmu5J,9599,simonw,2022-12-14T22:58:51Z,2022-12-14T22:59:27Z,OWNER,"I need a way for those JSON endpoints to communicate back to the `handle_exception` handler that they are returning JSON, so it knows to behave differently. Since it gets the `request` object, one way could be to have view code set `request.should_return_json = True` so that the handler knows to do something different. It's a bit of a cludge though!","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493471221,`.json` errors should be returned as JSON, https://github.com/simonw/datasette/issues/1949#issuecomment-1352329027,https://api.github.com/repos/simonw/datasette/issues/1949,1352329027,IC_kwDOBm6k_c5QmudD,9599,simonw,2022-12-14T22:56:24Z,2022-12-14T22:57:19Z,OWNER,"Most `.json` errors DO return as JSON, thanks to this: https://github.com/simonw/datasette/blob/c094dde3ff2bae030f261e6440d4fb082eb860a9/datasette/handle_exception.py#L19-L24 https://github.com/simonw/datasette/blob/c094dde3ff2bae030f261e6440d4fb082eb860a9/datasette/handle_exception.py#L57-L58 But that code triggers when the URL ends with `.json` - and none of the JSON write API endpoints (things like `/db/-/create` and `/db/table/-/insert`) follow that convention.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493471221,`.json` errors should be returned as JSON, https://github.com/simonw/datasette/issues/1948#issuecomment-1347766530,https://api.github.com/repos/simonw/datasette/issues/1948,1347766530,IC_kwDOBm6k_c5QVUkC,9599,simonw,2022-12-13T05:22:19Z,2022-12-13T05:22:19Z,OWNER,"I tested: ``` {""id"": ""root"", ""_r"": {""a"": ""view-table""}} ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493404423,500 error on permission debug page when testing actors with _r, https://github.com/simonw/datasette/issues/1947#issuecomment-1350414961,https://api.github.com/repos/simonw/datasette/issues/1947,1350414961,IC_kwDOBm6k_c5QfbJx,9599,simonw,2022-12-14T05:22:00Z,2022-12-14T05:22:00Z,OWNER,I think the next big step for this feature is for me to actually use it to build a few things.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493390939,UI to create reduced scope tokens from the `/-/create-token` page, https://github.com/simonw/datasette/issues/1947#issuecomment-1350414402,https://api.github.com/repos/simonw/datasette/issues/1947,1350414402,IC_kwDOBm6k_c5QfbBC,9599,simonw,2022-12-14T05:21:07Z,2022-12-14T05:21:07Z,OWNER,"It would be neat not to show write permissions against immutable databases too - and not hard from a performance perspective since it doesn't involve hundreds more permission checks. That will need permissions to grow a flag for if they need a mutable database though, which is a bigger job.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493390939,UI to create reduced scope tokens from the `/-/create-token` page, https://github.com/simonw/datasette/issues/1947#issuecomment-1350413555,https://api.github.com/repos/simonw/datasette/issues/1947,1350413555,IC_kwDOBm6k_c5Qfazz,9599,simonw,2022-12-14T05:19:52Z,2022-12-14T05:19:52Z,OWNER,"Maybe I should have kept `_memory` listed for instances that are running with `--crossdb` enabled? Yeah I think I should.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493390939,UI to create reduced scope tokens from the `/-/create-token` page, https://github.com/simonw/datasette/issues/1947#issuecomment-1350409537,https://api.github.com/repos/simonw/datasette/issues/1947,1350409537,IC_kwDOBm6k_c5QfZ1B,9599,simonw,2022-12-14T05:14:16Z,2022-12-14T05:14:16Z,OWNER,"New interface now live at https://latest.datasette.io/-/create-token It shouldn't be showing `_memory` though.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493390939,UI to create reduced scope tokens from the `/-/create-token` page, https://github.com/simonw/datasette/issues/1947#issuecomment-1350402667,https://api.github.com/repos/simonw/datasette/issues/1947,1350402667,IC_kwDOBm6k_c5QfYJr,9599,simonw,2022-12-14T05:05:10Z,2022-12-14T05:05:10Z,OWNER,"Tests can go here: https://github.com/simonw/datasette/blob/d98a8effb10ce8fe04a03eae42baa8a9cb0ca3f7/tests/test_auth.py#L143-L160","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493390939,UI to create reduced scope tokens from the `/-/create-token` page, https://github.com/simonw/datasette/issues/1947#issuecomment-1350401651,https://api.github.com/repos/simonw/datasette/issues/1947,1350401651,IC_kwDOBm6k_c5QfX5z,9599,simonw,2022-12-14T05:03:59Z,2022-12-14T05:03:59Z,OWNER,I shipped a working interface. Could still do with some extra tests.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493390939,UI to create reduced scope tokens from the `/-/create-token` page, https://github.com/simonw/datasette/issues/1947#issuecomment-1350215936,https://api.github.com/repos/simonw/datasette/issues/1947,1350215936,IC_kwDOBm6k_c5QeqkA,9599,simonw,2022-12-14T01:23:42Z,2022-12-14T01:23:42Z,OWNER,"With tilde-encoding for database and table names the HTML looks like this: ```html ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493390939,UI to create reduced scope tokens from the `/-/create-token` page, https://github.com/simonw/datasette/issues/1947#issuecomment-1350148192,https://api.github.com/repos/simonw/datasette/issues/1947,1350148192,IC_kwDOBm6k_c5QeaBg,9599,simonw,2022-12-14T00:19:06Z,2022-12-14T00:19:06Z,OWNER,"Another option: I could set a time limit - say 200ms - on how long I'm willing to spend calculating permissions before displaying this form First calculate view permissions for tables and databases (and maybe views and canned queries too). Then see if I can check every permission that I'm going to show as a checkbox on this page. If I get that done within the time limit use that to show the options. If I run out of time show all options and maybe include a note saying that some of them may not actually be available.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493390939,UI to create reduced scope tokens from the `/-/create-token` page, https://github.com/simonw/datasette/issues/1947#issuecomment-1350125018,https://api.github.com/repos/simonw/datasette/issues/1947,1350125018,IC_kwDOBm6k_c5QeUXa,9599,simonw,2022-12-14T00:08:09Z,2022-12-14T00:08:09Z,OWNER,Also: don't show hidden tables.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493390939,UI to create reduced scope tokens from the `/-/create-token` page, https://github.com/simonw/datasette/issues/1947#issuecomment-1350124381,https://api.github.com/repos/simonw/datasette/issues/1947,1350124381,IC_kwDOBm6k_c5QeUNd,9599,simonw,2022-12-14T00:07:51Z,2022-12-14T00:07:51Z,OWNER,"Another thing to consider in the future: once Datasette can support thousands of tables (see #417) the list on this page will turn into multiple MBs of HTML, which may cause all kinds of problems - not to mention the overhead of all of those table visibility permission checks. Hopefully by then I'll have a good fix for the permission listings problem: - #1152 And I can apply the same mechanism here.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493390939,UI to create reduced scope tokens from the `/-/create-token` page, https://github.com/simonw/datasette/issues/1947#issuecomment-1350037572,https://api.github.com/repos/simonw/datasette/issues/1947,1350037572,IC_kwDOBm6k_c5Qd_BE,9599,simonw,2022-12-13T23:27:32Z,2022-12-13T23:27:32Z,OWNER,"I'm going to ignore the permissions issue for the moment - I'll allow people to select any permissions they like in any of the databases or tables that are visible to them (don't want to leak the existence of databases/tables to users who shouldn't be able to see them). I think the value of getting this working outweights any potential confusion from not using finely grained permission checks to decide if the user should be able to apply a permission or not. The tokens themselves won't be able to perform `insert-row` or similar if the user doesn't have the ability to do that, even if they selected that checkbox.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493390939,UI to create reduced scope tokens from the `/-/create-token` page, https://github.com/simonw/datasette/issues/1947#issuecomment-1350019528,https://api.github.com/repos/simonw/datasette/issues/1947,1350019528,IC_kwDOBm6k_c5Qd6nI,9599,simonw,2022-12-13T23:19:16Z,2022-12-13T23:19:16Z,OWNER,"Here's the checkbox prototype: ```diff diff --git a/datasette/templates/create_token.html b/datasette/templates/create_token.html index a94881ed..1795ebaf 100644 --- a/datasette/templates/create_token.html +++ b/datasette/templates/create_token.html @@ -2,11 +2,20 @@ {% block title %}Create an API token{% endblock %} +{% block extra_head %} + +{% endblock %} + {% block content %}

Create an API token

-

This token will allow API access with the same abilities as your current user.

+

This token will allow API access with the same abilities as your current user, {{ request.actor.id }}

{% if errors %} {% for error in errors %} @@ -27,8 +36,39 @@ - + +
+ Restrict actions that can be performed using this token +

All databases and tables

+
    + {% for permission in all_permissions %} +
  • + {% endfor %} +
+ + {% for database in databases %} +

All tables in database: {{ database }}

+
    + {% for permission in database_permissions %} +
  • + {% endfor %} +
+ {% endfor %} +

Specific tables

+ {% for dbt in database_with_tables %} + {% for table in dbt.tables %} +

{{ dbt.database }}: {{ table }}

+
    + {% for permission in table_permissions %} +
  • + {% endfor %} +
+ {% endfor %} + {% endfor %} +
+ + {% if token %}
diff --git a/datasette/views/special.py b/datasette/views/special.py index 30345d14..48357f87 100644 --- a/datasette/views/special.py +++ b/datasette/views/special.py @@ -231,12 +231,37 @@ class CreateTokenView(BaseView): return await self.render( [""create_token.html""], request, - {""actor"": request.actor}, + { + ""actor"": request.actor, + ""all_permissions"": self.ds.permissions.keys(), + ""database_permissions"": [ + key + for key, value in self.ds.permissions.items() + if value.takes_database + ], + ""table_permissions"": [ + key + for key, value in self.ds.permissions.items() + if value.takes_resource + ], + ""databases"": [k for k in self.ds.databases.keys() if k != ""_internal""], + ""database_with_tables"": [ + { + ""database"": db.name, + ""tables"": await db.table_names(), + } + for db in self.ds.databases.values() + if db.name != ""_internal"" + ], + }, ) async def post(self, request): self.check_permission(request) post = await request.post_vars() + from pprint import pprint + + pprint(post) errors = [] duration = None if post.get(""expire_type""): ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493390939,UI to create reduced scope tokens from the `/-/create-token` page, https://github.com/simonw/datasette/issues/1947#issuecomment-1350013016,https://api.github.com/repos/simonw/datasette/issues/1947,1350013016,IC_kwDOBm6k_c5Qd5BY,9599,simonw,2022-12-13T23:16:24Z,2022-12-13T23:17:17Z,OWNER,"Slightly tricky thing here is that it should only show permissions that the user themselves has - on databases and tables that they have permission to access. I have a nasty feeling this may require looping through _everything_ and running every permission check, which could get very expensive if there are plugins involved that do their own storage check to resolve a permission. It's that classic permission system problem: how to efficiently iterate through everything the user has permission to do in one go? Might be that I have to punt on that, and show the user a list of permissions to select that they might not actually have ability for.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493390939,UI to create reduced scope tokens from the `/-/create-token` page, https://github.com/simonw/datasette/issues/1947#issuecomment-1350008636,https://api.github.com/repos/simonw/datasette/issues/1947,1350008636,IC_kwDOBm6k_c5Qd388,9599,simonw,2022-12-13T23:14:33Z,2022-12-13T23:14:33Z,OWNER,"Checkbox interface looks like this. It's not beautiful but it's good enough for the moment: ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493390939,UI to create reduced scope tokens from the `/-/create-token` page, https://github.com/simonw/datasette/issues/1947#issuecomment-1350002434,https://api.github.com/repos/simonw/datasette/issues/1947,1350002434,IC_kwDOBm6k_c5Qd2cC,9599,simonw,2022-12-13T23:11:50Z,2022-12-13T23:11:59Z,OWNER,"I think checkboxes will work well. Here's the data I get back from them (as `post_vars()`): ``` {'all:debug-menu': 'on', 'all:insert-row': 'on', 'expire_duration': '', 'expire_type': '', 'table:fixtures:delete-row': 'on', 'table:fixtures:drop-table': 'on', 'table:fixtures:view-query': 'on'} ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493390939,UI to create reduced scope tokens from the `/-/create-token` page, https://github.com/simonw/datasette/issues/1947#issuecomment-1349975255,https://api.github.com/repos/simonw/datasette/issues/1947,1349975255,IC_kwDOBm6k_c5QdvzX,9599,simonw,2022-12-13T23:00:11Z,2022-12-13T23:00:11Z,OWNER,"My `
+ +
+ Restrict actions that can be performed using this token +

Restrict actions that can be performed using this token:

+

All databases and tables:

+

+
+ {% if token %} diff --git a/datasette/views/special.py b/datasette/views/special.py index 30345d14..9d0fcd31 100644 --- a/datasette/views/special.py +++ b/datasette/views/special.py @@ -231,7 +231,17 @@ class CreateTokenView(BaseView): return await self.render( [""create_token.html""], request, - {""actor"": request.actor}, + { + ""actor"": request.actor, + ""all_permissions"": self.ds.permissions.keys(), + ""database_permissions"": [key for key, value in self.ds.permissions.items() if value.takes_database], + ""table_permissions"": [key for key, value in self.ds.permissions.items() if value.takes_resource], + ""databases"": self.ds.databases.keys(), + ""database_with_tables"": [{ + ""database"": db.name, + ""tables"": await db.table_names(), + } for db in self.ds.databases.values()], + }, ) async def post(self, request): ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493390939,UI to create reduced scope tokens from the `/-/create-token` page, https://github.com/simonw/datasette/issues/1947#issuecomment-1349974287,https://api.github.com/repos/simonw/datasette/issues/1947,1349974287,IC_kwDOBm6k_c5QdvkP,9599,simonw,2022-12-13T22:59:44Z,2022-12-13T22:59:44Z,OWNER,"Got an option group thing working: But... it strikes me that any time you're considering a `` for this. The usability for keyboards is still pretty awful, but it's a niche enough feature that maybe that's OK for the moment? ```javascript var select = document.querySelector('select'); var selected = Array.from(temp0.options).filter(o => o.selected).map(o => o.value) ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493390939,UI to create reduced scope tokens from the `/-/create-token` page, https://github.com/simonw/datasette/issues/1947#issuecomment-1347775760,https://api.github.com/repos/simonw/datasette/issues/1947,1347775760,IC_kwDOBm6k_c5QVW0Q,9599,simonw,2022-12-13T05:38:47Z,2022-12-13T05:38:47Z,OWNER,I'm going to hide the options for reducing the scope of the token inside a details/summary element.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493390939,UI to create reduced scope tokens from the `/-/create-token` page, https://github.com/simonw/datasette/issues/1947#issuecomment-1347768549,https://api.github.com/repos/simonw/datasette/issues/1947,1347768549,IC_kwDOBm6k_c5QVVDl,9599,simonw,2022-12-13T05:25:56Z,2022-12-13T22:29:12Z,OWNER,- [x] I should add a `--database` example to that help text.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493390939,UI to create reduced scope tokens from the `/-/create-token` page, https://github.com/simonw/datasette/issues/1947#issuecomment-1347768328,https://api.github.com/repos/simonw/datasette/issues/1947,1347768328,IC_kwDOBm6k_c5QVVAI,9599,simonw,2022-12-13T05:25:31Z,2022-12-13T22:25:46Z,OWNER,"https://latest.datasette.io/-/create-token currently looks like this: ![Image](https://user-images.githubusercontent.com/9599/207458002-7c46940b-22c0-45d3-a668-ec7f1082588c.png) As a reminder, the CLI options that this needs to provide an alternative to are: https://github.com/simonw/datasette/blob/d4b98d3924dec625a99236e65b1b169ff957381f/docs/cli-reference.rst#L619-L638","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493390939,UI to create reduced scope tokens from the `/-/create-token` page, https://github.com/simonw/datasette/issues/1947#issuecomment-1347760109,https://api.github.com/repos/simonw/datasette/issues/1947,1347760109,IC_kwDOBm6k_c5QVS_t,9599,simonw,2022-12-13T05:12:00Z,2022-12-13T05:12:00Z,OWNER,"For the UI: I think I'm going to dump a whole bunch of form elements on the page (so you can set up to 3 of each category of limit without any JavaScript), then add JavaScript that hides all but one of the options and gives you a ""add another"" widget that adds multiple more.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493390939,UI to create reduced scope tokens from the `/-/create-token` page, https://github.com/simonw/datasette/issues/1946#issuecomment-1347733217,https://api.github.com/repos/simonw/datasette/issues/1946,1347733217,IC_kwDOBm6k_c5QVMbh,9599,simonw,2022-12-13T04:28:45Z,2022-12-13T04:28:45Z,OWNER,"Demo of the new feature: ``` % datasette create-token --secret s root dstok_eyJhIjoicm9vdCIsInRva2VuIjoiZHN0b2siLCJ0IjoxNjcwOTA1NjgwfQ.pqSWOwCSNp678hEWl9l5o7m1GaM % datasette --get /-/actor.json {""actor"": null} % DATASETTE_SECRET=s datasette --get /-/actor.json --token dstok_eyJhIjoicm9vdCIsInRva2VuIjoiZHN0b2siLCJ0IjoxNjcwOTA1NjgwfQ.pqSWOwCSNp678hEWl9l5o7m1GaM {""actor"": {""id"": ""root"", ""token"": ""dstok""}} ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493339206,`datasette --get` mechanism for sending tokens, https://github.com/simonw/datasette/issues/1946#issuecomment-1347732039,https://api.github.com/repos/simonw/datasette/issues/1946,1347732039,IC_kwDOBm6k_c5QVMJH,9599,simonw,2022-12-13T04:26:20Z,2022-12-13T04:26:20Z,OWNER,"Two options: - `--header ""Authorization: Bearer XXX""` which can be used to send any headers - `--token XXX` to specify the token, which is then sent using that header I like the second option more, simply because there are currently no other headers that affect how Datasette works. `--token` feels obvious and easy to use.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1493339206,`datasette --get` mechanism for sending tokens, https://github.com/simonw/datasette/issues/1943#issuecomment-1347645615,https://api.github.com/repos/simonw/datasette/issues/1943,1347645615,IC_kwDOBm6k_c5QU3Cv,9599,simonw,2022-12-13T02:06:47Z,2022-12-13T02:06:47Z,OWNER,This URL is already used for the https://latest.datasette.io/-/permissions tool - but it could include a block on that page that tells you what permissions are available.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1490576818,`/-/permissions` should list available permissions, https://github.com/simonw/sqlite-utils/issues/517#issuecomment-1344965367,https://api.github.com/repos/simonw/sqlite-utils/issues/517,1344965367,IC_kwDOCGYnMM5QKor3,9599,simonw,2022-12-10T01:26:31Z,2022-12-10T01:26:31Z,OWNER,At some point I should drop it from all of these other projects too: https://cs.github.com/?scopeName=All+repos&scope=&q=user%3Asimonw+%223.6%22+path%3A.github%2Fworkflows%2F*,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1487757143,Drop support for Python 3.6, https://github.com/simonw/datasette/issues/1942#issuecomment-1344959032,https://api.github.com/repos/simonw/datasette/issues/1942,1344959032,IC_kwDOBm6k_c5QKnI4,9599,simonw,2022-12-10T01:11:26Z,2022-12-10T01:11:26Z,OWNER,"One way this could work: if plugins request it, a block like this is added to the page: ```html ``` Then a function could be provided which extracts and parses that data: ```javascript var data = await datasette.jsonData(); ``` Why an `await`? Because then I could have it work the exact same way if the data is NOT available on the page - it could trigger a `fetch()` call for the same stuff. So loading it on the page becomes an optional performance optimization.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1487738738,Option for plugins to request that JSON be served on the page, https://github.com/simonw/datasette/issues/1941#issuecomment-1343869900,https://api.github.com/repos/simonw/datasette/issues/1941,1343869900,IC_kwDOBm6k_c5QGdPM,9599,simonw,2022-12-09T05:25:19Z,2022-12-09T05:25:19Z,OWNER,"I don't plan to implement this for Datasette 1.0, but it should be something that can be added later on in a minor version bump.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1486036269,Mechanism for supporting key rotation for DATASETTE_SECRET, https://github.com/simonw/datasette/pull/1940#issuecomment-1347640542,https://api.github.com/repos/simonw/datasette/issues/1940,1347640542,IC_kwDOBm6k_c5QU1ze,9599,simonw,2022-12-13T02:02:10Z,2022-12-13T02:02:10Z,OWNER,"This PR ended up bundling part of the implementation of: - #1636 I'm going to be bad an NOT untangle that from this before I merge it.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1486011362,register_permissions() plugin hook, https://github.com/simonw/datasette/pull/1940#issuecomment-1347634128,https://api.github.com/repos/simonw/datasette/issues/1940,1347634128,IC_kwDOBm6k_c5QU0PQ,9599,simonw,2022-12-13T01:51:56Z,2022-12-13T01:51:56Z,OWNER,Actually one last thing: I said that the error would only occur if the permissions differed in some way.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1486011362,register_permissions() plugin hook, https://github.com/simonw/datasette/pull/1940#issuecomment-1347632350,https://api.github.com/repos/simonw/datasette/issues/1940,1347632350,IC_kwDOBm6k_c5QUzze,22429695,codecov[bot],2022-12-13T01:48:40Z,2022-12-13T02:00:52Z,NONE,"# [Codecov](https://codecov.io/gh/simonw/datasette/pull/1940?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) Report Base: **92.00**% // Head: **92.03**% // Increases project coverage by **`+0.02%`** :tada: > Coverage data is based on head [(`a1317ab`)](https://codecov.io/gh/simonw/datasette/pull/1940?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) compared to base [(`e539c1c`)](https://codecov.io/gh/simonw/datasette/commit/e539c1c024bc62d88df91d9107cbe37e7f0fe55f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). > Patch coverage: 100.00% of modified lines in pull request are covered. > :exclamation: Current head a1317ab differs from pull request most recent head 94e5c75. Consider uploading reports for the commit 94e5c75 to get more accurate results
Additional details and impacted files ```diff @@ Coverage Diff @@ ## main #1940 +/- ## ========================================== + Coverage 92.00% 92.03% +0.02% ========================================== Files 38 38 Lines 5378 5396 +18 ========================================== + Hits 4948 4966 +18 Misses 430 430 ``` | [Impacted Files](https://codecov.io/gh/simonw/datasette/pull/1940?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) | Coverage Δ | | |---|---|---| | [datasette/permissions.py](https://codecov.io/gh/simonw/datasette/pull/1940/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-ZGF0YXNldHRlL3Blcm1pc3Npb25zLnB5) | `100.00% <ø> (ø)` | | | [datasette/views/database.py](https://codecov.io/gh/simonw/datasette/pull/1940/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-ZGF0YXNldHRlL3ZpZXdzL2RhdGFiYXNlLnB5) | `96.26% <ø> (ø)` | | | [datasette/views/index.py](https://codecov.io/gh/simonw/datasette/pull/1940/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-ZGF0YXNldHRlL3ZpZXdzL2luZGV4LnB5) | `96.49% <ø> (ø)` | | | [datasette/views/special.py](https://codecov.io/gh/simonw/datasette/pull/1940/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-ZGF0YXNldHRlL3ZpZXdzL3NwZWNpYWwucHk=) | `79.20% <ø> (-0.21%)` | :arrow_down: | | [datasette/views/table.py](https://codecov.io/gh/simonw/datasette/pull/1940/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-ZGF0YXNldHRlL3ZpZXdzL3RhYmxlLnB5) | `92.57% <ø> (ø)` | | | [datasette/\_\_init\_\_.py](https://codecov.io/gh/simonw/datasette/pull/1940/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-ZGF0YXNldHRlL19faW5pdF9fLnB5) | `100.00% <100.00%> (ø)` | | | [datasette/app.py](https://codecov.io/gh/simonw/datasette/pull/1940/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-ZGF0YXNldHRlL2FwcC5weQ==) | `94.47% <100.00%> (+0.04%)` | :arrow_up: | | [datasette/default\_permissions.py](https://codecov.io/gh/simonw/datasette/pull/1940/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-ZGF0YXNldHRlL2RlZmF1bHRfcGVybWlzc2lvbnMucHk=) | `95.20% <100.00%> (+0.39%)` | :arrow_up: | | [datasette/hookspecs.py](https://codecov.io/gh/simonw/datasette/pull/1940/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-ZGF0YXNldHRlL2hvb2tzcGVjcy5weQ==) | `100.00% <100.00%> (ø)` | | Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison)
[:umbrella: View full report at Codecov](https://codecov.io/gh/simonw/datasette/pull/1940?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). :loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1486011362,register_permissions() plugin hook, https://github.com/simonw/datasette/pull/1940#issuecomment-1347620733,https://api.github.com/repos/simonw/datasette/issues/1940,1347620733,IC_kwDOBm6k_c5QUw99,9599,simonw,2022-12-13T01:33:06Z,2022-12-13T01:33:06Z,OWNER,"It's this change which triggers the failures: ```diff diff --git a/datasette/app.py b/datasette/app.py index 760063d5..defa9688 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -707,9 +707,12 @@ class Datasette: ) return crumbs - async def permission_allowed(self, actor, action, resource=None, default=False): + async def permission_allowed(self, actor, action, resource=None, default=None): """"""Check permissions using the permissions_allowed plugin hook"""""" result = None + # Use default from registered permission, if available + if default is None and action in self.permissions: + default = self.permissions[action].default for check in pm.hook.permission_allowed( datasette=self, actor=actor, ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1486011362,register_permissions() plugin hook, https://github.com/simonw/datasette/pull/1940#issuecomment-1347616055,https://api.github.com/repos/simonw/datasette/issues/1940,1347616055,IC_kwDOBm6k_c5QUv03,9599,simonw,2022-12-13T01:27:03Z,2022-12-13T01:27:03Z,OWNER,"I'm going to revert that last commit, see if I can get the tests running again and then apply the changes a line at a time to figure out which ones broke things.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1486011362,register_permissions() plugin hook, https://github.com/simonw/datasette/pull/1940#issuecomment-1345701316,https://api.github.com/repos/simonw/datasette/issues/1940,1345701316,IC_kwDOBm6k_c5QNcXE,9599,simonw,2022-12-12T00:10:59Z,2022-12-12T00:10:59Z,OWNER,"Here's my first test failure: ``` tests/test_permissions.py .......F >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> allow = {}, expected_anon = 403, expected_auth = 403, path = '/fixtures/compound_three_primary_keys' padlock_client = @pytest.mark.parametrize( ""allow,expected_anon,expected_auth"", [ (None, 200, 200), ({}, 403, 403), ({""id"": ""root""}, 403, 200), ], ) @pytest.mark.parametrize( ""path"", ( ""/"", ""/fixtures"", ""/fixtures/compound_three_primary_keys"", ""/fixtures/compound_three_primary_keys/a,a,a"", ""/fixtures/two"", # Query ), ) def test_view_padlock(allow, expected_anon, expected_auth, path, padlock_client): padlock_client.ds._metadata_local[""allow""] = allow fragment = ""🔒"" anon_response = padlock_client.get(path) > assert expected_anon == anon_response.status E assert 403 == 200 E + where 200 = .status /Users/simon/Dropbox/Development/datasette/tests/test_permissions.py:61: AssertionError >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> PDB post_mortem (IO-capturing turned off) >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> > /Users/simon/Dropbox/Development/datasette/tests/test_permissions.py(61)test_view_padlock() -> assert expected_anon == anon_response.status (Pdb) anon_response (Pdb) anon_response.status 200 (Pdb) path '/fixtures/compound_three_primary_keys' (Pdb) padlock_client.ds._metadata_ *** AttributeError: 'Datasette' object has no attribute '_metadata_' (Pdb) padlock_client.ds._metadata_local {'databases': {'fixtures': {'queries': {'two': {'sql': 'select 1 + 1', 'name': 'two'}, 'from_async_hook': {'sql': 'select 2', 'name': 'from_async_hook'}, 'from_hook': {'sql': ""select 1, 'null' as actor_id"", 'name': 'from_hook'}}, 'source': None, 'source_url': None, 'license': None, 'license_url': None, 'about': None, 'about_url': None}}, 'allow': {}} (Pdb) allow {} ``` It looks like I've broken the `allow` logic that notices that if there's an `""allow"": {}` on the root then anonymous users should not be allowed to view any pages.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1486011362,register_permissions() plugin hook, https://github.com/simonw/datasette/issues/1939#issuecomment-1347646516,https://api.github.com/repos/simonw/datasette/issues/1939,1347646516,IC_kwDOBm6k_c5QU3Q0,9599,simonw,2022-12-13T02:07:50Z,2022-12-13T02:07:50Z,OWNER,Documentation for the new hook: https://docs.datasette.io/en/latest/plugin_hooks.html#register-permissions-datasette,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1485757511,register_permissions(datasette) plugin hook, https://github.com/simonw/datasette/issues/1939#issuecomment-1345691103,https://api.github.com/repos/simonw/datasette/issues/1939,1345691103,IC_kwDOBm6k_c5QNZ3f,9599,simonw,2022-12-11T23:37:49Z,2022-12-11T23:37:49Z,OWNER,"Idea: a `/-/permissions` introspection endpoint for listing registered permissions ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1485757511,register_permissions(datasette) plugin hook, https://github.com/simonw/datasette/issues/1939#issuecomment-1343872168,https://api.github.com/repos/simonw/datasette/issues/1939,1343872168,IC_kwDOBm6k_c5QGdyo,9599,simonw,2022-12-09T05:29:53Z,2022-12-09T05:29:53Z,OWNER,"I'm going to address those ideas for changes to the `permission_allowed()` in a separate issue. What would it take for the `register_permissions()` hook to be something I'm comfortable landing? I think it's mainly that the list of permissions it provides should Do More Stuff: - Participate in unit tests, in particular this one: https://github.com/simonw/datasette/blob/e539c1c024bc62d88df91d9107cbe37e7f0fe55f/tests/conftest.py#L79-L102 - That new `default` option should be respected - maybe if you omit `default=` from a call to `permission_allowed()` it could fall back on the default from there? - Log a warning if you attempt to check a permission that wasn't registered Then I can use the permissions - in particular their metadata - to help implement his: - #1636","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1485757511,register_permissions(datasette) plugin hook, https://github.com/simonw/datasette/issues/1939#issuecomment-1343858998,https://api.github.com/repos/simonw/datasette/issues/1939,1343858998,IC_kwDOBm6k_c5QGak2,9599,simonw,2022-12-09T05:12:17Z,2022-12-09T05:12:17Z,OWNER,Draft docs for the new plugin hook: https://datasette--1940.org.readthedocs.build/en/1940/plugin_hooks.html#register-permissions-datasette,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1485757511,register_permissions(datasette) plugin hook, https://github.com/simonw/datasette/issues/1939#issuecomment-1343856781,https://api.github.com/repos/simonw/datasette/issues/1939,1343856781,IC_kwDOBm6k_c5QGaCN,9599,simonw,2022-12-09T05:10:00Z,2022-12-09T05:10:00Z,OWNER,Made a draft PR so ReadTheDocs would deploy my new documentation somewhere.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1485757511,register_permissions(datasette) plugin hook, https://github.com/simonw/datasette/issues/1939#issuecomment-1343844555,https://api.github.com/repos/simonw/datasette/issues/1939,1343844555,IC_kwDOBm6k_c5QGXDL,9599,simonw,2022-12-09T04:48:28Z,2022-12-09T04:48:28Z,OWNER,"I'm going to try a spike in a branch with `datasette.action_allowed(...)` and a `register_permissions()` plugin hook, to see what they look like.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1485757511,register_permissions(datasette) plugin hook, https://github.com/simonw/datasette/issues/1939#issuecomment-1343844112,https://api.github.com/repos/simonw/datasette/issues/1939,1343844112,IC_kwDOBm6k_c5QGW8Q,9599,simonw,2022-12-09T04:47:28Z,2022-12-09T04:47:28Z,OWNER,"I think `action_allowed` is my favourite, even though there's a little bit of concept overlap with `table_actions` and `database_actions`. I never really liked those plugin hook names much to be honest, especially since they are inconsistent with `menu_links`: https://github.com/simonw/datasette/blob/d67f812b7327c7075732688f3df728807503dc58/datasette/hookspecs.py#L123-L135","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1485757511,register_permissions(datasette) plugin hook, https://github.com/simonw/datasette/issues/1939#issuecomment-1343843352,https://api.github.com/repos/simonw/datasette/issues/1939,1343843352,IC_kwDOBm6k_c5QGWwY,9599,simonw,2022-12-09T04:45:50Z,2022-12-09T04:45:50Z,OWNER,"Another option: ```python if await datasette.actor_can(actor, ""insert-data""...) ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1485757511,register_permissions(datasette) plugin hook, https://github.com/simonw/datasette/issues/1939#issuecomment-1343842362,https://api.github.com/repos/simonw/datasette/issues/1939,1343842362,IC_kwDOBm6k_c5QGWg6,9599,simonw,2022-12-09T04:43:38Z,2022-12-09T04:43:38Z,OWNER,"Asked ChatGPT for some alternative names, I didn't like any of them: is_permission_granted has_permission check_permission is_action_allowed check_access_permission permission_check validate_permission check_actor_permission verify_permission check_authorization ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1485757511,register_permissions(datasette) plugin hook, https://github.com/simonw/datasette/issues/1939#issuecomment-1343753386,https://api.github.com/repos/simonw/datasette/issues/1939,1343753386,IC_kwDOBm6k_c5QGAyq,9599,simonw,2022-12-09T02:20:20Z,2022-12-09T02:21:01Z,OWNER,"It's also referenced in this plugin hook: ```python @hookspec def permission_allowed(datasette, actor, action, resource): """"""Check if actor is allowed to perform this action - return True, False or None"""""" ``` But more importantly, in these ones: ```python @hookspec def table_actions(datasette, actor, database, table, request): """"""Links for the table actions menu"""""" @hookspec def database_actions(datasette, actor, database, request): """"""Links for the database actions menu"""""" ``` So the word ""action"" is already used within Datasette to refer to those things - which are _almost_ but not quite the same as actions-as-permissions: many of the things that show up in those menus relate to permissions the user has, but not necessarily all of them.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1485757511,register_permissions(datasette) plugin hook, https://github.com/simonw/datasette/issues/1939#issuecomment-1343751860,https://api.github.com/repos/simonw/datasette/issues/1939,1343751860,IC_kwDOBm6k_c5QGAa0,9599,simonw,2022-12-09T02:18:11Z,2022-12-09T02:19:23Z,OWNER,"Should I rename ""permission"" to ""action"" elsewhere too? Maybe have a `register_actions(...)` plugin hook instead of adding `register_permissions(...)`? What else could the word ""action"" mean? Currently it's used in the codebase to refer to GitHub Actions, and for code like this: ```python if await self.permission_allowed( actor=actor, action=""view-instance"", default=True ): ``` Which is already revealing the confusion between ""permission"" and ""action"".","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1485757511,register_permissions(datasette) plugin hook, https://github.com/simonw/datasette/issues/1939#issuecomment-1343751261,https://api.github.com/repos/simonw/datasette/issues/1939,1343751261,IC_kwDOBm6k_c5QGARd,9599,simonw,2022-12-09T02:17:14Z,2022-12-09T02:17:14Z,OWNER,"One option: ```python async def action_allowed(self, actor, action, database=None, resource=None): ``` `action_allowed` fixes the `permission` v.s. `action` thing a bit, and is a new name that doesn't clash with the existing method. I dropped `default` because that's now a property of the permission itself. `table` is now called `resource` and `database` is a separate parameter.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1485757511,register_permissions(datasette) plugin hook, https://github.com/simonw/datasette/issues/1939#issuecomment-1343749617,https://api.github.com/repos/simonw/datasette/issues/1939,1343749617,IC_kwDOBm6k_c5QF_3x,9599,simonw,2022-12-09T02:15:54Z,2022-12-09T02:15:54Z,OWNER,"What if I came up with a new method name for this, which could co-exist with the old one while that old one was deprecated?","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1485757511,register_permissions(datasette) plugin hook, https://github.com/simonw/datasette/issues/1939#issuecomment-1343744338,https://api.github.com/repos/simonw/datasette/issues/1939,1343744338,IC_kwDOBm6k_c5QF-lS,9599,simonw,2022-12-09T02:08:42Z,2022-12-09T02:08:42Z,OWNER,Extracted a TIL: https://til.simonwillison.net/github/github-code-search-api-uses,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1485757511,register_permissions(datasette) plugin hook, https://github.com/simonw/datasette/issues/1939#issuecomment-1343734812,https://api.github.com/repos/simonw/datasette/issues/1939,1343734812,IC_kwDOBm6k_c5QF8Qc,9599,simonw,2022-12-09T01:57:07Z,2022-12-09T01:57:07Z,OWNER,"This search is better: datasette permission_allowed -user:simonw -path:datasette/** -path:docs/** -path:tests/** language:python That returns 11 results: https://cs.github.com/?scopeName=All+repos&scope=&q=datasette+permission_allowed+-user%3Asimonw+-path%3Adatasette%2F**+-path%3Adocs%2F**+-path%3Atests%2F**+language%3Apython 3 are forks of my repos. The rest are all by four users: - [20after4/ddd](https://github.com/20after4/ddd) - [emg110/datasette-graphql](https://github.com/emg110/datasette-graphql) - [next-LI/datasette-csv-importer](https://github.com/next-LI/datasette-csv-importer) - [next-LI/datasette-demo](https://github.com/next-LI/datasette-demo) - [next-LI/datasette-live-config](https://github.com/next-LI/datasette-live-config) - [next-LI/datasette-live-permissions](https://github.com/next-LI/datasette-live-permissions) - [next-LI/datasette-search-all](https://github.com/next-LI/datasette-search-all) - [next-LI/datasette-surveys](https://github.com/next-LI/datasette-surveys) - [next-LI/datasette-write](https://github.com/next-LI/datasette-write) - [rclement/datasette-dashboards](https://github.com/rclement/datasette-dashboards) ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1485757511,register_permissions(datasette) plugin hook, https://github.com/simonw/datasette/issues/1939#issuecomment-1343728929,https://api.github.com/repos/simonw/datasette/issues/1939,1343728929,IC_kwDOBm6k_c5QF60h,9599,simonw,2022-12-09T01:48:11Z,2022-12-09T01:52:33Z,OWNER,"This code search shows a bunch of repos I don't know about that would be affected by this change: https://cs.github.com/?scopeName=All+repos&scope=&q=datasette+permission_allowed+-user%3Asimonw# These (and likely more): Repositories - [20after4/ddd](https://github.com/20after4/ddd) - [next-LI/datasette-csv-importer](https://github.com/next-LI/datasette-csv-importer) - [digital-land/datasette](https://github.com/digital-land/datasette) - [mroswell/datasette](https://github.com/mroswell/datasette) - [next-LI/datasette-live-config](https://github.com/next-LI/datasette-live-config) - [keladhruv/datasette](https://github.com/keladhruv/datasette) - [RhetTbull/datasette](https://github.com/RhetTbull/datasette) - [chriswedgwood/datasette](https://github.com/chriswedgwood/datasette) - [boan-anbo/datasette](https://github.com/boan-anbo/datasette) - [MattTriano/datasette](https://github.com/MattTriano/datasette) - [incadenza/datasette](https://github.com/incadenza/datasette) - [robdyke/datasette](https://github.com/robdyke/datasette) - [ctb/datasette](https://github.com/ctb/datasette) - [eyeseast/datasette](https://github.com/eyeseast/datasette) - [symbol-management/api-match-audit](https://github.com/symbol-management/api-match-audit) Actually a lot of those are forks of Datasette itself - so maybe this is manageable? Would be nice if I could come up with a GitHub search that excluded any repos with ""datasette"" as their exact name. https://docs.github.com/en/search-github/github-code-search/understanding-github-code-search-syntax#using-qualifiers says: > **Note:** The new code search beta does not currently support regular expressions or partial matching for repository names, so you will have to type the entire repository name (including the user prefix) for the `repo:` qualifier to work.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1485757511,register_permissions(datasette) plugin hook, https://github.com/simonw/datasette/issues/1939#issuecomment-1343727184,https://api.github.com/repos/simonw/datasette/issues/1939,1343727184,IC_kwDOBm6k_c5QF6ZQ,9599,simonw,2022-12-09T01:45:15Z,2022-12-09T01:45:15Z,OWNER,"Moving the concept of the default for the permission into this registry warrants a redesign of this method anyway: https://github.com/simonw/datasette/blob/e539c1c024bc62d88df91d9107cbe37e7f0fe55f/datasette/app.py#L706","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1485757511,register_permissions(datasette) plugin hook, https://github.com/simonw/datasette/issues/1939#issuecomment-1343724732,https://api.github.com/repos/simonw/datasette/issues/1939,1343724732,IC_kwDOBm6k_c5QF5y8,9599,simonw,2022-12-09T01:40:44Z,2022-12-09T01:43:25Z,OWNER,"```python Permission = collections.namedtuple( ""Permission"", (""name"", ""abbr"", ""takes_database"", ""takes_table"", ""default"") ) ``` I don`t think that design is quite right. - Elsewhere in the code the concept is called an ""action"" rather than a ""permission"" - I think I can stick with the `Permission` name here though, it's pretty clear - `takes_database` - is `takes_` the right verb here? - `takes_table` can also refer to a SQL view or a canned named query A question that was raised by the work in #1938 is whether you should be able to grant a permission like `insert-row` at the instance or database level - and if so, what does that look like? I think you should be able to do that, it doesn't make sense to have to grant it explicitly for every single table. So maybe `takes_table` and `takes_database` are the right names here? But `table` is still bad because it doesn't reflect views and canned queries. One thought is to use `resource` - but that will require a bunch of breaking changes to the existing APIs which treat resource as a tuple. Now's the best time to do that though before Datasette 1.0.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1485757511,register_permissions(datasette) plugin hook, https://github.com/simonw/datasette/issues/1939#issuecomment-1343722020,https://api.github.com/repos/simonw/datasette/issues/1939,1343722020,IC_kwDOBm6k_c5QF5Ik,9599,simonw,2022-12-09T01:36:05Z,2022-12-09T01:36:16Z,OWNER,"I originally added `permissions.py` for the permission debug tool in https://github.com/simonw/datasette/commit/c51d9246b996a2831c9bd6a1e205f6cb48b9a5f3 - I don't think anything else uses it yet. - #1881","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1485757511,register_permissions(datasette) plugin hook, https://github.com/simonw/datasette/issues/1939#issuecomment-1343721522,https://api.github.com/repos/simonw/datasette/issues/1939,1343721522,IC_kwDOBm6k_c5QF5Ay,9599,simonw,2022-12-09T01:35:15Z,2022-12-09T01:35:15Z,OWNER,"One concern I have about this: there are a bunch of existing plugins that do stuff with permissions that won't currently be using this hook. Do I break those plugins, forcing new releases of them for compatibility with Datasette 1.0? Or maybe I keep them working, but until they've upgraded to register their permissions there are things about them that won't work - e.g. you won't be able to configure their permissions in `metadata.yml` until they release something that does this hook. Best thing is probably for me to get this working in core first and then evaluate the impact it would have on existing plugins once I have some running code.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1485757511,register_permissions(datasette) plugin hook, https://github.com/simonw/datasette/pull/1938#issuecomment-1347767048,https://api.github.com/repos/simonw/datasette/issues/1938,1347767048,IC_kwDOBm6k_c5QVUsI,9599,simonw,2022-12-13T05:23:18Z,2022-12-13T05:23:18Z,OWNER,"I landed this already: - #1636 ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1485488236,"""permissions"" blocks in metadata.json/yaml", https://github.com/simonw/datasette/pull/1938#issuecomment-1343449918,https://api.github.com/repos/simonw/datasette/issues/1938,1343449918,IC_kwDOBm6k_c5QE2s-,22429695,codecov[bot],2022-12-08T22:20:10Z,2022-12-08T22:54:08Z,NONE,"# [Codecov](https://codecov.io/gh/simonw/datasette/pull/1938?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) Report Base: **92.00**% // Head: **92.01**% // Increases project coverage by **`+0.01%`** :tada: > Coverage data is based on head [(`6e35a6b`)](https://codecov.io/gh/simonw/datasette/pull/1938?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) compared to base [(`e539c1c`)](https://codecov.io/gh/simonw/datasette/commit/e539c1c024bc62d88df91d9107cbe37e7f0fe55f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). > Patch coverage: 100.00% of modified lines in pull request are covered.
Additional details and impacted files ```diff @@ Coverage Diff @@ ## main #1938 +/- ## ========================================== + Coverage 92.00% 92.01% +0.01% ========================================== Files 38 38 Lines 5378 5386 +8 ========================================== + Hits 4948 4956 +8 Misses 430 430 ``` | [Impacted Files](https://codecov.io/gh/simonw/datasette/pull/1938?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) | Coverage Δ | | |---|---|---| | [datasette/default\_permissions.py](https://codecov.io/gh/simonw/datasette/pull/1938/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-ZGF0YXNldHRlL2RlZmF1bHRfcGVybWlzc2lvbnMucHk=) | `95.10% <100.00%> (+0.29%)` | :arrow_up: | Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison)
[:umbrella: View full report at Codecov](https://codecov.io/gh/simonw/datasette/pull/1938?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). :loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1485488236,"""permissions"" blocks in metadata.json/yaml", https://github.com/simonw/datasette/pull/1938#issuecomment-1343445885,https://api.github.com/repos/simonw/datasette/issues/1938,1343445885,IC_kwDOBm6k_c5QE1t9,9599,simonw,2022-12-08T22:16:03Z,2022-12-08T22:16:03Z,OWNER,Docs: https://datasette--1938.org.readthedocs.build/en/1938/authentication.html#other-permissions-in-metadata,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1485488236,"""permissions"" blocks in metadata.json/yaml", https://github.com/simonw/datasette/issues/1937#issuecomment-1347770871,https://api.github.com/repos/simonw/datasette/issues/1937,1347770871,IC_kwDOBm6k_c5QVVn3,9599,simonw,2022-12-13T05:30:43Z,2022-12-13T05:30:43Z,OWNER,"Also you should need `update-row` permission to use the `""replace"": true` option - I should add that rule to `/-/insert` add well.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1483320357,/db/-/create API should require insert-rows permission to use row: or rows: option, https://github.com/simonw/datasette/issues/1937#issuecomment-1341848525,https://api.github.com/repos/simonw/datasette/issues/1937,1341848525,IC_kwDOBm6k_c5P-vvN,9599,simonw,2022-12-08T01:34:03Z,2022-12-08T01:34:03Z,OWNER,Check should go somewhere around here: https://github.com/simonw/datasette/blob/dee18ed8ce7af2ab8699bcb5a51a99f48301bc42/datasette/views/database.py#L625-L631,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1483320357,/db/-/create API should require insert-rows permission to use row: or rows: option, https://github.com/simonw/datasette/issues/1936#issuecomment-1341849735,https://api.github.com/repos/simonw/datasette/issues/1936,1341849735,IC_kwDOBm6k_c5P-wCH,9599,simonw,2022-12-08T01:35:54Z,2022-12-08T01:35:54Z,OWNER,"Running that twice produced this: ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1483250004,Fix /db/table/-/upsert in the API explorer, https://github.com/simonw/datasette/issues/1936#issuecomment-1341849496,https://api.github.com/repos/simonw/datasette/issues/1936,1341849496,IC_kwDOBm6k_c5P-v-Y,9599,simonw,2022-12-08T01:35:35Z,2022-12-08T01:35:35Z,OWNER,"Related bug: you can send `""id"": null` and it works (it should throw an error): ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1483250004,Fix /db/table/-/upsert in the API explorer, https://github.com/simonw/datasette/pull/1935#issuecomment-1340950566,https://api.github.com/repos/simonw/datasette/issues/1935,1340950566,IC_kwDOBm6k_c5P7Ugm,22429695,codecov[bot],2022-12-07T13:14:41Z,2022-12-07T13:14:41Z,NONE,"# [Codecov](https://codecov.io/gh/simonw/datasette/pull/1935?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) Report Base: **91.73**% // Head: **91.49**% // Decreases project coverage by **`-0.24%`** :warning: > Coverage data is based on head [(`e8ae41e`)](https://codecov.io/gh/simonw/datasette/pull/1935?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) compared to base [(`93ababe`)](https://codecov.io/gh/simonw/datasette/commit/93ababe6f7150454d2cf278dae08569e505d2a5b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). > Patch has no changes to coverable lines.
Additional details and impacted files ```diff @@ Coverage Diff @@ ## main #1935 +/- ## ========================================== - Coverage 91.73% 91.49% -0.25% ========================================== Files 36 37 +1 Lines 4987 5031 +44 ========================================== + Hits 4575 4603 +28 - Misses 412 428 +16 ``` | [Impacted Files](https://codecov.io/gh/simonw/datasette/pull/1935?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) | Coverage Δ | | |---|---|---| | [datasette/utils/shutil\_backport.py](https://codecov.io/gh/simonw/datasette/pull/1935/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-ZGF0YXNldHRlL3V0aWxzL3NodXRpbF9iYWNrcG9ydC5weQ==) | `9.09% <0.00%> (ø)` | | | [datasette/app.py](https://codecov.io/gh/simonw/datasette/pull/1935/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-ZGF0YXNldHRlL2FwcC5weQ==) | `94.78% <0.00%> (+0.36%)` | :arrow_up: | | [datasette/plugins.py](https://codecov.io/gh/simonw/datasette/pull/1935/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-ZGF0YXNldHRlL3BsdWdpbnMucHk=) | `85.29% <0.00%> (+2.94%)` | :arrow_up: | | [datasette/utils/asgi.py](https://codecov.io/gh/simonw/datasette/pull/1935/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-ZGF0YXNldHRlL3V0aWxzL2FzZ2kucHk=) | `93.60% <0.00%> (+3.59%)` | :arrow_up: | | [datasette/cli.py](https://codecov.io/gh/simonw/datasette/pull/1935/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-ZGF0YXNldHRlL2NsaS5weQ==) | `82.18% <0.00%> (+4.00%)` | :arrow_up: | Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison)
[:umbrella: View full report at Codecov](https://codecov.io/gh/simonw/datasette/pull/1935?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). :loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1481875485,Bump furo from 2022.9.29 to 2022.12.7, https://github.com/simonw/sqlite-utils/issues/516#issuecomment-1339844639,https://api.github.com/repos/simonw/sqlite-utils/issues/516,1339844639,IC_kwDOCGYnMM5P3Ggf,122043,briandorsey,2022-12-06T19:08:13Z,2022-12-06T19:08:13Z,NONE,"Reference: tqdm (https://tqdm.github.io/) shows a progress bar when total is known, and falls back to counting units of work done for streams. File input vs. stdin seems like a similar situation. ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1479914599,Feature request: output number of ignored/replaced rows for insert command, https://github.com/simonw/sqlite-utils/issues/516#issuecomment-1339839767,https://api.github.com/repos/simonw/sqlite-utils/issues/516,1339839767,IC_kwDOCGYnMM5P3FUX,122043,briandorsey,2022-12-06T19:04:17Z,2022-12-06T19:04:17Z,NONE,"Current behavior is different when importing via stdin vs. a file. Imports from a file give a progress bar. For this new request, I'd love to see total imported and total ignored/replaced in both cases. ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1479914599,Feature request: output number of ignored/replaced rows for insert command, https://github.com/simonw/sqlite-utils/issues/516#issuecomment-1339837520,https://api.github.com/repos/simonw/sqlite-utils/issues/516,1339837520,IC_kwDOCGYnMM5P3ExQ,122043,briandorsey,2022-12-06T19:02:30Z,2022-12-06T19:02:30Z,NONE,"`--verbose` or `--verbosity=ABC` were the flags I looked for. Expected to see them at a global level near `--version`. But only sharing because that's where I looked first, I don't have a strong opinion on the exact wording/location. ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1479914599,Feature request: output number of ignored/replaced rows for insert command, https://github.com/simonw/sqlite-utils/issues/516#issuecomment-1339834918,https://api.github.com/repos/simonw/sqlite-utils/issues/516,1339834918,IC_kwDOCGYnMM5P3EIm,9599,simonw,2022-12-06T19:00:18Z,2022-12-06T19:00:35Z,OWNER,"Right now the command produces no output at all. Maybe a `--verbose` mode that writes these numbers to standard error (or even standard output since it's an option)? Is there a better name than `--verbose` for this? `--summary` perhaps?","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1479914599,Feature request: output number of ignored/replaced rows for insert command, https://github.com/simonw/datasette/pull/1931#issuecomment-1341825314,https://api.github.com/repos/simonw/datasette/issues/1931,1341825314,IC_kwDOBm6k_c5P-qEi,9599,simonw,2022-12-08T01:03:18Z,2022-12-08T01:03:18Z,OWNER,"I broke this test: ``` ds_write = @pytest.mark.asyncio async def test_insert_row(ds_write): token = write_token(ds_write) response = await ds_write.client.post( ""/data/docs/-/insert"", json={""row"": {""title"": ""Test"", ""score"": 1.2, ""age"": 5}}, headers={ ""Authorization"": ""***"".format(token), ""Content-Type"": ""application/json"", }, ) expected_row = {""id"": 1, ""title"": ""Test"", ""score"": 1.2, ""age"": 5} > assert response.status_code == 201 E assert 500 == 201 E + where 500 = .status_code /home/runner/work/datasette/datasette/tests/test_api_write.py:43: AssertionError ----------------------------- Captured stderr call ----------------------------- Traceback (most recent call last): File ""/home/runner/work/datasette/datasette/datasette/app.py"", line 1447, in route_path response = await view(request, send) File ""/home/runner/work/datasette/datasette/datasette/views/base.py"", line 151, in view return await self.dispatch_request(request) File ""/home/runner/work/datasette/datasette/datasette/views/base.py"", line 105, in dispatch_request response = await handler(request) File ""/home/runner/work/datasette/datasette/datasette/views/table.py"", line 1228, in post row_pk_values_for_later = [tuple(row[pk] for pk in pks) for row in rows] File ""/home/runner/work/datasette/datasette/datasette/views/table.py"", line 1228, in row_pk_values_for_later = [tuple(row[pk] for pk in pks) for row in rows] File ""/home/runner/work/datasette/datasette/datasette/views/table.py"", line 1228, in row_pk_values_for_later = [tuple(row[pk] for pk in pks) for row in rows] KeyError: 'id' ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1473814539,/db/table/-/upsert, https://github.com/simonw/datasette/pull/1931#issuecomment-1341821213,https://api.github.com/repos/simonw/datasette/issues/1931,1341821213,IC_kwDOBm6k_c5P-pEd,9599,simonw,2022-12-08T00:58:21Z,2022-12-08T00:58:21Z,OWNER,"In the interests of shipping, I'm going to punt the API explorer to a later issue.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1473814539,/db/table/-/upsert, https://github.com/simonw/datasette/pull/1931#issuecomment-1339968514,https://api.github.com/repos/simonw/datasette/issues/1931,1339968514,IC_kwDOBm6k_c5P3kwC,9599,simonw,2022-12-06T20:28:47Z,2022-12-06T20:28:47Z,OWNER,"Should the `""return"": true` mode reflect the order in which the rows were provided when the API was called? I think it should. Since this is small enough to happily fit in Python memory (thanks to the `max_insert_rows` setting) I can load the fresh data from the database and then sort it in Python space before returning it.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1473814539,/db/table/-/upsert, https://github.com/simonw/datasette/pull/1931#issuecomment-1339916064,https://api.github.com/repos/simonw/datasette/issues/1931,1339916064,IC_kwDOBm6k_c5P3X8g,3556,davidbgk,2022-12-06T19:42:45Z,2022-12-06T19:42:45Z,CONTRIBUTOR,"The `""return"": true` option is really nice!","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1473814539,/db/table/-/upsert, https://github.com/simonw/datasette/pull/1931#issuecomment-1339911152,https://api.github.com/repos/simonw/datasette/issues/1931,1339911152,IC_kwDOBm6k_c5P3Wvw,9599,simonw,2022-12-06T19:38:12Z,2022-12-06T19:38:12Z,OWNER,Documentation: https://datasette--1931.org.readthedocs.build/en/1931/json_api.html#upserting-rows,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1473814539,/db/table/-/upsert, https://github.com/simonw/datasette/pull/1931#issuecomment-1339906241,https://api.github.com/repos/simonw/datasette/issues/1931,1339906241,IC_kwDOBm6k_c5P3VjB,22429695,codecov[bot],2022-12-06T19:33:32Z,2022-12-08T01:04:56Z,NONE,"# [Codecov](https://codecov.io/gh/simonw/datasette/pull/1931?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) Report Base: **90.42**% // Head: **91.77**% // Increases project coverage by **`+1.34%`** :tada: > Coverage data is based on head [(`645be0f`)](https://codecov.io/gh/simonw/datasette/pull/1931?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) compared to base [(`cab5b60`)](https://codecov.io/gh/simonw/datasette/commit/cab5b60e09e94aca820dbec5308446a88c99ea3d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). > Patch coverage: 95.55% of modified lines in pull request are covered. > :exclamation: Current head 645be0f differs from pull request most recent head 7cd6fd9. Consider uploading reports for the commit 7cd6fd9 to get more accurate results
Additional details and impacted files ```diff @@ Coverage Diff @@ ## main #1931 +/- ## ========================================== + Coverage 90.42% 91.77% +1.34% ========================================== Files 36 36 Lines 5057 5019 -38 ========================================== + Hits 4573 4606 +33 + Misses 484 413 -71 ``` | [Impacted Files](https://codecov.io/gh/simonw/datasette/pull/1931?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) | Coverage Δ | | |---|---|---| | [datasette/views/special.py](https://codecov.io/gh/simonw/datasette/pull/1931/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-ZGF0YXNldHRlL3ZpZXdzL3NwZWNpYWwucHk=) | `79.41% <0.00%> (ø)` | | | [datasette/views/table.py](https://codecov.io/gh/simonw/datasette/pull/1931/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-ZGF0YXNldHRlL3ZpZXdzL3RhYmxlLnB5) | `92.44% <97.43%> (+0.20%)` | :arrow_up: | | [datasette/app.py](https://codecov.io/gh/simonw/datasette/pull/1931/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-ZGF0YXNldHRlL2FwcC5weQ==) | `94.42% <100.00%> (+<0.01%)` | :arrow_up: | | [datasette/default\_permissions.py](https://codecov.io/gh/simonw/datasette/pull/1931/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-ZGF0YXNldHRlL2RlZmF1bHRfcGVybWlzc2lvbnMucHk=) | `94.81% <100.00%> (+0.07%)` | :arrow_up: | | [datasette/views/database.py](https://codecov.io/gh/simonw/datasette/pull/1931/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-ZGF0YXNldHRlL3ZpZXdzL2RhdGFiYXNlLnB5) | `95.83% <0.00%> (+17.01%)` | :arrow_up: | Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison)
[:umbrella: View full report at Codecov](https://codecov.io/gh/simonw/datasette/pull/1931?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). :loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1473814539,/db/table/-/upsert, https://github.com/simonw/datasette/pull/1931#issuecomment-1339784569,https://api.github.com/repos/simonw/datasette/issues/1931,1339784569,IC_kwDOBm6k_c5P2315,9599,simonw,2022-12-06T18:16:15Z,2022-12-06T18:17:56Z,OWNER,"Just noticed the insert API returns `{}` when it should return `{""ok"": true}` - will fix that here too. UPDATE: no it did that already, it was just the documentation that was wrong.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1473814539,/db/table/-/upsert, https://github.com/simonw/datasette/pull/1931#issuecomment-1339768422,https://api.github.com/repos/simonw/datasette/issues/1931,1339768422,IC_kwDOBm6k_c5P2z5m,9599,simonw,2022-12-06T18:04:59Z,2022-12-06T18:04:59Z,OWNER,"I realized this API should require both the `insert-row` AND the `update-row` permissions, since calls to it could do either one.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1473814539,/db/table/-/upsert, https://github.com/simonw/datasette/pull/1930#issuecomment-1343360006,https://api.github.com/repos/simonw/datasette/issues/1930,1343360006,IC_kwDOBm6k_c5QEgwG,9599,simonw,2022-12-08T21:12:28Z,2022-12-08T21:12:28Z,OWNER,Thanks!,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1473664029,Typo in JSON API `Updating a row` documentation, https://github.com/simonw/datasette/pull/1930#issuecomment-1336017976,https://api.github.com/repos/simonw/datasette/issues/1930,1336017976,IC_kwDOBm6k_c5PogQ4,22429695,codecov[bot],2022-12-03T02:30:21Z,2022-12-03T02:30:21Z,NONE,"# [Codecov](https://codecov.io/gh/simonw/datasette/pull/1930?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) Report Base: **90.42**% // Head: **90.42**% // No change to project coverage :thumbsup: > Coverage data is based on head [(`9928ff1`)](https://codecov.io/gh/simonw/datasette/pull/1930?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) compared to base [(`cab5b60`)](https://codecov.io/gh/simonw/datasette/commit/cab5b60e09e94aca820dbec5308446a88c99ea3d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). > Patch has no changes to coverable lines.
Additional details and impacted files ```diff @@ Coverage Diff @@ ## main #1930 +/- ## ======================================= Coverage 90.42% 90.42% ======================================= Files 36 36 Lines 5057 5057 ======================================= Hits 4573 4573 Misses 484 484 ``` Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison)
[:umbrella: View full report at Codecov](https://codecov.io/gh/simonw/datasette/pull/1930?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). :loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1473664029,Typo in JSON API `Updating a row` documentation, https://github.com/simonw/datasette/issues/1929#issuecomment-1339909159,https://api.github.com/repos/simonw/datasette/issues/1929,1339909159,IC_kwDOBm6k_c5P3WQn,9599,simonw,2022-12-06T19:36:23Z,2022-12-06T19:36:23Z,OWNER,https://docs.datasette.io/en/1.0a1/json_api.html 👍 ,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1473659191,Incorrect link from the API explorer to the JSON API documentation, https://github.com/simonw/datasette/issues/1929#issuecomment-1339906969,https://api.github.com/repos/simonw/datasette/issues/1929,1339906969,IC_kwDOBm6k_c5P3VuZ,3556,davidbgk,2022-12-06T19:34:20Z,2022-12-06T19:34:20Z,CONTRIBUTOR,I confirm that it works 👍 ,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1473659191,Incorrect link from the API explorer to the JSON API documentation, https://github.com/simonw/datasette/issues/1929#issuecomment-1339871933,https://api.github.com/repos/simonw/datasette/issues/1929,1339871933,IC_kwDOBm6k_c5P3NK9,9599,simonw,2022-12-06T19:23:48Z,2022-12-06T19:24:17Z,OWNER,"I can do that on this page: https://readthedocs.org/projects/datasette/versions/?version_filter=1.0 I'm making them both active and hidden: ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1473659191,Incorrect link from the API explorer to the JSON API documentation, https://github.com/simonw/datasette/issues/1929#issuecomment-1339867570,https://api.github.com/repos/simonw/datasette/issues/1929,1339867570,IC_kwDOBm6k_c5P3MGy,9599,simonw,2022-12-06T19:22:47Z,2022-12-06T19:22:47Z,OWNER,"Yeah I should deploy the docs for the alpha versions (the intention is that the docs exactly match the release you are using), thanks for spotting that.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1473659191,Incorrect link from the API explorer to the JSON API documentation, https://github.com/simonw/datasette/issues/1928#issuecomment-1335966329,https://api.github.com/repos/simonw/datasette/issues/1928,1335966329,IC_kwDOBm6k_c5PoTp5,9599,simonw,2022-12-02T23:47:11Z,2022-12-02T23:47:11Z,OWNER,Wrote about this extensively here: https://simonwillison.net/2022/Dec/2/datasette-write-api/,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1473481262,Hacker News Datasette write demo, https://github.com/simonw/datasette/issues/1928#issuecomment-1335870889,https://api.github.com/repos/simonw/datasette/issues/1928,1335870889,IC_kwDOBm6k_c5Pn8Wp,9599,simonw,2022-12-02T21:41:09Z,2022-12-02T21:41:09Z,OWNER,"Got it working! https://simon.datasette.cloud/data/hacker_news_posts https://github.com/simonw/scrape-hacker-news-by-domain/blob/main/submit-to-datasette-cloud.sh","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1473481262,Hacker News Datasette write demo, https://github.com/simonw/datasette/issues/1928#issuecomment-1335870887,https://api.github.com/repos/simonw/datasette/issues/1928,1335870887,IC_kwDOBm6k_c5Pn8Wn,9599,simonw,2022-12-02T21:25:16Z,2022-12-02T21:25:16Z,OWNER,"Here's the change that should submit data to Datasette Cloud: https://github.com/simonw/scrape-hacker-news-by-domain/commit/848bb7e835a9fb87cd656362591835179cd1dc1b I ran `delete from hacker_news_posts` against my instance so https://simon.datasette.cloud/data/hacker_news_posts is now empty.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1473481262,Hacker News Datasette write demo, https://github.com/simonw/datasette/issues/1928#issuecomment-1335870884,https://api.github.com/repos/simonw/datasette/issues/1928,1335870884,IC_kwDOBm6k_c5Pn8Wk,9599,simonw,2022-12-02T21:19:58Z,2022-12-02T21:19:58Z,OWNER,"But until I fix this issue: - https://github.com/simonw/datasette/issues/1927 I need to insert freshly scraped data like this: ```bash export ROWS=$( jq -n --argjson rows ""$(cat simonwillison-net.json)"" \ '{ ""rows"": $rows, ""replace"": true }' ) curl -X POST \ https://simon.datasette.cloud/data/hacker_news_posts/-/insert \ -H ""Content-Type: application/json"" \ -H ""Authorization: Bearer $DS_TOKEN"" \ -d $ROWS ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1473481262,Hacker News Datasette write demo, https://github.com/simonw/datasette/issues/1928#issuecomment-1335870883,https://api.github.com/repos/simonw/datasette/issues/1928,1335870883,IC_kwDOBm6k_c5Pn8Wj,9599,simonw,2022-12-02T21:19:10Z,2022-12-02T21:19:10Z,OWNER,"I created the `hacker_news_posts` table like this: ```bash export ROWS=$( jq -n --argjson rows ""$(cat simonwillison-net.json)"" \ '{ ""table"": ""hacker_news_posts"", ""rows"": $rows, ""pk"": ""id"", ""replace"": true }' ) # Use curl to POST some JSON to a URL curl -X POST \ https://simon.datasette.cloud/data/-/create \ -H ""Content-Type: application/json"" \ -H ""Authorization: Bearer $DS_TOKEN"" \ -d $ROWS ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1473481262,Hacker News Datasette write demo, https://github.com/simonw/datasette/issues/1928#issuecomment-1335870879,https://api.github.com/repos/simonw/datasette/issues/1928,1335870879,IC_kwDOBm6k_c5Pn8Wf,9599,simonw,2022-12-02T21:18:25Z,2022-12-02T21:18:25Z,OWNER,"This is the SQL view for the atom feed: ```sql CREATE VIEW hacker_news_posts_atom as select id as atom_id, title as atom_title, url, commentsUrl as atom_link, dt || 'Z' as atom_updated, 'Submitter: ' || submitter || ' - ' || points || ' points, ' || numComments || ' comments' as atom_content from hacker_news_posts order by dt desc limit 100; ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1473481262,Hacker News Datasette write demo, https://github.com/simonw/datasette/issues/1928#issuecomment-1335870877,https://api.github.com/repos/simonw/datasette/issues/1928,1335870877,IC_kwDOBm6k_c5Pn8Wd,9599,simonw,2022-12-02T21:17:50Z,2022-12-02T21:17:50Z,OWNER,https://simon.datasette.cloud/data/hacker_news_posts_atom.atom is working now.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1473481262,Hacker News Datasette write demo, https://github.com/simonw/datasette/issues/1927#issuecomment-1339910494,https://api.github.com/repos/simonw/datasette/issues/1927,1339910494,IC_kwDOBm6k_c5P3Wle,9599,simonw,2022-12-06T19:37:39Z,2022-12-06T19:37:39Z,OWNER,"I'll finish this after I land: - #1931 ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1473411197,ignore:true/replace:true options for /db/-/create API, https://github.com/simonw/datasette/issues/1927#issuecomment-1336099588,https://api.github.com/repos/simonw/datasette/issues/1927,1336099588,IC_kwDOBm6k_c5Po0ME,9599,simonw,2022-12-03T06:58:14Z,2022-12-03T06:58:14Z,OWNER,I have not yet documented the new `insert` and `replace` options.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1473411197,ignore:true/replace:true options for /db/-/create API, https://github.com/simonw/datasette/issues/1927#issuecomment-1336099533,https://api.github.com/repos/simonw/datasette/issues/1927,1336099533,IC_kwDOBm6k_c5Po0LN,9599,simonw,2022-12-03T06:57:52Z,2022-12-03T06:57:52Z,OWNER,I'm going to push what I have anyway. I'll keep this issue open while I think through the above comment.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1473411197,ignore:true/replace:true options for /db/-/create API, https://github.com/simonw/datasette/issues/1927#issuecomment-1336099368,https://api.github.com/repos/simonw/datasette/issues/1927,1336099368,IC_kwDOBm6k_c5Po0Io,9599,simonw,2022-12-03T06:56:36Z,2022-12-03T06:56:36Z,OWNER,"Neither of these options make sense if you didn't pass a `""pk""`. My initial implementation spotted if the `pk` was missing and looked it up from the table, but actually I don't think that makes sense - if you know the table exists and hence don't pass the `pk` you should be using `/-/insert` or `/-/upsert` instead. So maybe this work should expanded to include validation that checks if the table exists already - and if it does, confirms that the primary key (and maybe even the columns) are the same as for that existing table. Of course if you only send `row` or `rows` then checking `columns` doesn't completely make sense - but we could check that the rows you have sent are equal to or a subset of the columns in the table. We could even check the column types as well, as seen in: - #1910 ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1473411197,ignore:true/replace:true options for /db/-/create API, https://github.com/simonw/datasette/issues/1927#issuecomment-1335984268,https://api.github.com/repos/simonw/datasette/issues/1927,1335984268,IC_kwDOBm6k_c5PoYCM,9599,simonw,2022-12-03T00:26:26Z,2022-12-03T00:26:26Z,OWNER,"Also: the documentation should clarify that you can call this API multiple times when using the `rows` option. (It will probably grow `""alter"": true` soon too).","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1473411197,ignore:true/replace:true options for /db/-/create API, https://github.com/simonw/datasette/issues/1926#issuecomment-1334508062,https://api.github.com/repos/simonw/datasette/issues/1926,1334508062,IC_kwDOBm6k_c5Pivoe,9599,simonw,2022-12-01T22:06:12Z,2022-12-01T22:06:12Z,OWNER,Released: https://pypi.org/project/datasette/1.0a1/,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1471969984,Release notes for 1.0a1 (and release it), https://github.com/simonw/datasette/issues/1924#issuecomment-1334165225,https://api.github.com/repos/simonw/datasette/issues/1924,1334165225,IC_kwDOBm6k_c5Phb7p,9599,simonw,2022-12-01T18:15:15Z,2022-12-01T18:15:15Z,OWNER,Updated docs at the bottom of this section: https://docs.datasette.io/en/latest/json_api.html#inserting-rows,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1470509936,Docs for replace:true and ignore:true options for insert API, https://github.com/simonw/datasette/issues/1924#issuecomment-1333042785,https://api.github.com/repos/simonw/datasette/issues/1924,1333042785,IC_kwDOBm6k_c5PdJ5h,9599,simonw,2022-12-01T02:00:06Z,2022-12-01T02:00:06Z,OWNER,"Looks like I did implement these already: https://github.com/simonw/datasette/blob/9a1536b52a07e32da5900652da1bd7894c58fa9f/tests/test_api_write.py#L270-L273 But forgot to document them!","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1470509936,Docs for replace:true and ignore:true options for insert API, https://github.com/simonw/datasette/issues/1924#issuecomment-1333025076,https://api.github.com/repos/simonw/datasette/issues/1924,1333025076,IC_kwDOBm6k_c5PdFk0,9599,simonw,2022-12-01T01:37:09Z,2022-12-01T01:37:09Z,OWNER,"Related question: what happens if you attempt to insert rows without either of these settings and 1:10 of them is an integrity error due to an existing primary key? Does the entire batch fail to be inserted? Should it? This may be the point that I need to think hard about how to use transactions here. ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1470509936,Docs for replace:true and ignore:true options for insert API, https://github.com/simonw/datasette/issues/1924#issuecomment-1333023273,https://api.github.com/repos/simonw/datasette/issues/1924,1333023273,IC_kwDOBm6k_c5PdFIp,9599,simonw,2022-12-01T01:34:48Z,2022-12-01T01:34:48Z,OWNER,"This will enable some very cool Datasette write API demos - for example, Git scrapers that insert-replace the most recent copy of the scraped data to a table somewhere (which can then produce an atom feed with https://datasette.io/plugins/datasette-atom)","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1470509936,Docs for replace:true and ignore:true options for insert API, https://github.com/simonw/datasette/issues/1923#issuecomment-1332851215,https://api.github.com/repos/simonw/datasette/issues/1923,1332851215,IC_kwDOBm6k_c5PcbIP,9599,simonw,2022-11-30T23:04:56Z,2022-11-30T23:04:56Z,OWNER,That fixed it.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1470320227,latest.datasette.io Cloud Run deploys failing, https://github.com/simonw/datasette/issues/1923#issuecomment-1332842435,https://api.github.com/repos/simonw/datasette/issues/1923,1332842435,IC_kwDOBm6k_c5PcY_D,9599,simonw,2022-11-30T22:58:33Z,2022-11-30T22:58:33Z,OWNER,https://stackoverflow.com/questions/74490465/github-actions-failing-for-setup-gcloud/74562740#74562740 suggests trying Python 3.9.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1470320227,latest.datasette.io Cloud Run deploys failing, https://github.com/simonw/datasette/issues/1923#issuecomment-1332835664,https://api.github.com/repos/simonw/datasette/issues/1923,1332835664,IC_kwDOBm6k_c5PcXVQ,9599,simonw,2022-11-30T22:50:10Z,2022-11-30T22:51:25Z,OWNER,https://stackoverflow.com/questions/74490465/github-actions-failing-for-setup-gcloud/74562526#74562526 suggests setting `version: '318.0.0'` of `google-github-actions/setup-gcloud`.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1470320227,latest.datasette.io Cloud Run deploys failing, https://github.com/simonw/datasette/issues/1922#issuecomment-1332903011,https://api.github.com/repos/simonw/datasette/issues/1922,1332903011,IC_kwDOBm6k_c5Pcnxj,9599,simonw,2022-11-30T23:45:29Z,2022-11-30T23:45:29Z,OWNER,"That worked for the preflight request - got this now: So it looks like error responses (in this case for permission denied) are missing CORS headers.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1469973742,Make sure CORS works for write APIs, https://github.com/simonw/datasette/issues/1922#issuecomment-1332855687,https://api.github.com/repos/simonw/datasette/issues/1922,1332855687,IC_kwDOBm6k_c5PccOH,9599,simonw,2022-11-30T23:09:31Z,2022-11-30T23:09:31Z,OWNER,"Still getting a CORS header. I tried it in Chrome, which outputs helpful messages to the console: ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1469973742,Make sure CORS works for write APIs, https://github.com/simonw/datasette/issues/1922#issuecomment-1332698636,https://api.github.com/repos/simonw/datasette/issues/1922,1332698636,IC_kwDOBm6k_c5Pb14M,9599,simonw,2022-11-30T20:25:50Z,2022-11-30T20:25:50Z,OWNER,"I just shipped this: Access-Control-Allow-Methods: GET, POST, HEAD, OPTIONS I'll try this out on `latest.datasette.io` - but I need to research more to check if this is a safe thing to do or not.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1469973742,Make sure CORS works for write APIs, https://github.com/simonw/datasette/issues/1922#issuecomment-1332689547,https://api.github.com/repos/simonw/datasette/issues/1922,1332689547,IC_kwDOBm6k_c5PbzqL,9599,simonw,2022-11-30T20:16:21Z,2022-11-30T20:16:46Z,OWNER,"That notebook: ```javascript viewof token = Inputs.text({ label: ""Your API token"" }) ``` ```javascript viewof createResponse = Inputs.button(""Create table"", { value: null, reduce: async () => { const response = await fetch( ""https://latest.datasette.io/ephemeral/-/create"", { method: ""POST"", mode: ""cors"", headers: { Authorization: `Bearer {$token}`, ""Content-Type"": ""application/json"" }, body: JSON.stringify({ table: ""my_new_table"", row: { task: ""Demonstrate a JSON creation from another domain"" } }) } ); return await response.json(); } }) ``` Based on this tip: https://talk.observablehq.com/t/best-pattern-for-click-here-to-submit-your-results-to-an-api-backend/7353/3","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1469973742,Make sure CORS works for write APIs, https://github.com/simonw/datasette/issues/1922#issuecomment-1332688245,https://api.github.com/repos/simonw/datasette/issues/1922,1332688245,IC_kwDOBm6k_c5PbzV1,9599,simonw,2022-11-30T20:15:08Z,2022-11-30T20:15:08Z,OWNER,"Still getting a CORS error: My hunch is this is because I'm not sending `Access-Control-Allow-Methods: GET,HEAD,POST`.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1469973742,Make sure CORS works for write APIs, https://github.com/simonw/datasette/issues/1922#issuecomment-1332585861,https://api.github.com/repos/simonw/datasette/issues/1922,1332585861,IC_kwDOBm6k_c5PbaWF,9599,simonw,2022-11-30T18:43:46Z,2022-11-30T18:43:46Z,OWNER,"Here's what Django Rest Framework does: https://github.com/encode/django-rest-framework/blob/1ae812ea209392ad76cc5d2f35f9f7fb337f63e4/rest_framework/views.py#L514-L521 ```python def options(self, request, *args, **kwargs): """""" Handler method for HTTP 'OPTIONS' request. """""" if self.metadata_class is None: return self.http_method_not_allowed(request, *args, **kwargs) data = self.metadata_class().determine_metadata(request, self) return Response(data, status=status.HTTP_200_OK) ``` That default `determine_metadata` method looks like this: https://github.com/encode/django-rest-framework/blob/1ae812ea209392ad76cc5d2f35f9f7fb337f63e4/rest_framework/metadata.py#L61-L71 ```python def determine_metadata(self, request, view): metadata = OrderedDict() metadata['name'] = view.get_view_name() metadata['description'] = view.get_view_description() metadata['renders'] = [renderer.media_type for renderer in view.renderer_classes] metadata['parses'] = [parser.media_type for parser in view.parser_classes] if hasattr(view, 'get_serializer'): actions = self.determine_actions(request, view) if actions: metadata['actions'] = actions return metadata ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1469973742,Make sure CORS works for write APIs, https://github.com/simonw/datasette/issues/1922#issuecomment-1332580395,https://api.github.com/repos/simonw/datasette/issues/1922,1332580395,IC_kwDOBm6k_c5PbZAr,9599,simonw,2022-11-30T18:38:22Z,2022-11-30T18:38:22Z,OWNER,"> [@simon](https://fedi.simonwillison.net/@simon) IMO, it should always be a 2XX series response, typically with no content & an extra `Allow` header with a list of HTTP verbs it responds to. https://mastodon.social/@daniellindsley/109434186252099323","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1469973742,Make sure CORS works for write APIs, https://github.com/simonw/datasette/issues/1922#issuecomment-1332572453,https://api.github.com/repos/simonw/datasette/issues/1922,1332572453,IC_kwDOBm6k_c5PbXEl,9599,simonw,2022-11-30T18:30:38Z,2022-11-30T18:30:54Z,OWNER,Started a conversation about how OPTIONS should work on Mastodon: https://fedi.simonwillison.net/@simon/109434148676475291,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1469973742,Make sure CORS works for write APIs, https://github.com/simonw/datasette/issues/1922#issuecomment-1332561813,https://api.github.com/repos/simonw/datasette/issues/1922,1332561813,IC_kwDOBm6k_c5PbUeV,9599,simonw,2022-11-30T18:20:05Z,2022-11-30T18:20:05Z,OWNER,"Weird, GitHub reply with a 404! ``` ~ % curl -X OPTIONS https://github.com/ -i HTTP/2 404 server: GitHub.com date: Wed, 30 Nov 2022 18:19:39 GMT content-type: text/html; charset=utf-8 content-length: 0 strict-transport-security: max-age=31536000; includeSubdomains; preload x-frame-options: deny x-content-type-options: nosniff x-xss-protection: 0 referrer-policy: origin-when-cross-origin, strict-origin-when-cross-origin content-security-policy: default-src 'none'; base-uri 'self'; block-all-mixed-content; child-src github.com/assets-cdn/worker/ gist.github.com/assets-cdn/worker/; connect-src 'self' uploads.github.com objects-origin.githubusercontent.com www.githubstatus.com collector.github.com raw.githubusercontent.com api.github.com github-cloud.s3.amazonaws.com github-production-repository-file-5c1aeb.s3.amazonaws.com github-production-upload-manifest-file-7fdce7.s3.amazonaws.com github-production-user-asset-6210df.s3.amazonaws.com cdn.optimizely.com logx.optimizely.com/v1/events; font-src github.githubassets.com; form-action 'self' github.com gist.github.com objects-origin.githubusercontent.com; frame-ancestors 'none'; frame-src viewscreen.githubusercontent.com notebooks.githubusercontent.com; img-src 'self' data: github.githubassets.com media.githubusercontent.com camo.githubusercontent.com identicons.github.com avatars.githubusercontent.com github-cloud.s3.amazonaws.com objects.githubusercontent.com objects-origin.githubusercontent.com secured-user-images.githubusercontent.com/ opengraph.githubassets.com github-production-user-asset-6210df.s3.amazonaws.com customer-stories-feed.github.com spotlights-feed.github.com; manifest-src 'self'; media-src github.com user-images.githubusercontent.com/ secured-user-images.githubusercontent.com/; script-src github.githubassets.com; style-src 'unsafe-inline' github.githubassets.com; worker-src github.com/assets-cdn/worker/ gist.github.com/assets-cdn/worker/ vary: Accept-Encoding, Accept, X-Requested-With x-github-request-id: DD6B:5ACA:102E8A6:1164A99:63879EBB ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1469973742,Make sure CORS works for write APIs, https://github.com/simonw/datasette/issues/1922#issuecomment-1332561059,https://api.github.com/repos/simonw/datasette/issues/1922,1332561059,IC_kwDOBm6k_c5PbUSj,9599,simonw,2022-11-30T18:19:20Z,2022-11-30T18:19:20Z,OWNER,"Two test failures: ``` ____________________________ test_homepage_options _____________________________ [gw0] linux -- Python 3.11.0 /opt/hostedtoolcache/Python/3.11.0/x64/bin/python app_client = def test_homepage_options(app_client): response = app_client.get(""/"", method=""OPTIONS"") > assert response.status == 405 E assert 200 == 405 E + where 200 = .status /home/runner/work/datasette/datasette/tests/test_html.py:58: AssertionError ______________________ test_client_methods[options-/-405] ______________________ [gw1] linux -- Python 3.11.0 /opt/hostedtoolcache/Python/3.11.0/x64/bin/python datasette = method = 'options', path = '/', expected_status = 405 @pytest.mark.asyncio @pytest.mark.parametrize( ""method,path,expected_status"", [ (""get"", ""/"", 200), (""options"", ""/"", 405), (""head"", ""/"", 200), (""put"", ""/"", 405), (""patch"", ""/"", 405), (""delete"", ""/"", 405), ], ) async def test_client_methods(datasette, method, path, expected_status): client_method = getattr(datasette.client, method) response = await client_method(path) assert isinstance(response, httpx.Response) > assert response.status_code == expected_status E assert 200 == 405 E + where 200 = .status_code /home/runner/work/datasette/datasette/tests/test_internals_datasette_client.py:29: AssertionError =============================== warnings summary =============================== tests/test_cli.py::test_inspect_cli_writes_to_file tests/test_cli.py::test_inspect_cli /home/runner/work/datasette/datasette/datasette/cli.py:163: DeprecationWarning: There is no current event loop loop = asyncio.get_event_loop() tests/test_cli_serve_get.py: 2 warnings tests/test_cli.py: 12 warnings tests/test_crossdb.py: 1 warning /home/runner/work/datasette/datasette/datasette/cli.py:591: DeprecationWarning: There is no current event loop asyncio.get_event_loop().run_until_complete(ds.invoke_startup()) tests/test_cli_serve_get.py: 2 warnings tests/test_cli.py: 12 warnings tests/test_crossdb.py: 1 warning /home/runner/work/datasette/datasette/datasette/cli.py:594: DeprecationWarning: There is no current event loop asyncio.get_event_loop().run_until_complete(check_databases(ds)) -- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html =========================== short test summary info ============================ FAILED tests/test_html.py::test_homepage_options - assert 200 == 405 + where 200 = .status FAILED tests/test_internals_datasette_client.py::test_client_methods[options-/-405] - assert 200 == 405 + where 200 = .status_code ====== 2 failed, 1195 passed, 1 skipped, 32 warnings in 191.06s (0:03:11) ====== Error: Process completed with exit code 1. ``` On reading https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS I feel like I should be a bit more thoughtful about how I treat OPTIONS - maybe it should work for every URL on the site, but return a `204 No Content` header? Comparing a few different sites: ``` ~ % curl -X OPTIONS https://www.google.com/ -i HTTP/2 405 allow: GET, HEAD date: Wed, 30 Nov 2022 18:18:15 GMT content-type: text/html; charset=UTF-8 server: gws content-length: 1592 x-xss-protection: 0 x-frame-options: SAMEORIGIN alt-svc: h3="":443""; ma=2592000,h3-29="":443""; ma=2592000,h3-Q050="":443""; ma=2592000,h3-Q046="":443""; ma=2592000,h3-Q043="":443""; ma=2592000,quic="":443""; ma=2592000; v=""46,43"" Error 405 (Method Not Allowed)!!1

405. That’s an error.

The request method OPTIONS is inappropriate for the URL /. That’s all we know. ~ % curl -X OPTIONS https://www.mozilla.org/ -i HTTP/2 405 content-type: text/html; charset=utf-8 content-length: 0 server: meinheld/1.0.2 date: Wed, 30 Nov 2022 18:18:38 GMT allow: GET, HEAD x-frame-options: DENY content-security-policy: child-src 'self' *.mozilla.net *.mozilla.org *.mozilla.com www.googletagmanager.com www.google-analytics.com www.youtube-nocookie.com trackertest.org www.surveygizmo.com accounts.firefox.com accounts.firefox.com.cn www.youtube.com; connect-src 'self' *.mozilla.net *.mozilla.org *.mozilla.com www.googletagmanager.com www.google-analytics.com region1.google-analytics.com logs.convertexperiments.com 1003350.metrics.convertexperiments.com 1003343.metrics.convertexperiments.com sentry.prod.mozaws.net o1069899.sentry.io o1069899.ingest.sentry.io https://accounts.firefox.com/ stage.cjms.nonprod.cloudops.mozgcp.net cjms.services.mozilla.com; frame-src 'self' *.mozilla.net *.mozilla.org *.mozilla.com www.googletagmanager.com www.google-analytics.com www.youtube-nocookie.com trackertest.org www.surveygizmo.com accounts.firefox.com accounts.firefox.com.cn www.youtube.com; script-src 'self' *.mozilla.net *.mozilla.org *.mozilla.com 'unsafe-inline' 'unsafe-eval' www.googletagmanager.com www.google-analytics.com tagmanager.google.com www.youtube.com s.ytimg.com cdn-3.convertexperiments.com app.convert.com data.track.convertexperiments.com 1003350.track.convertexperiments.com 1003343.track.convertexperiments.com; img-src 'self' *.mozilla.net *.mozilla.org *.mozilla.com data: mozilla.org www.googletagmanager.com www.google-analytics.com adservice.google.com adservice.google.de adservice.google.dk creativecommons.org cdn-3.convertexperiments.com logs.convertexperiments.com images.ctfassets.net ad.doubleclick.net; style-src 'self' *.mozilla.net *.mozilla.org *.mozilla.com 'unsafe-inline' app.convert.com; default-src 'self' *.mozilla.net *.mozilla.org *.mozilla.com; font-src 'self' cache-control: max-age=600 expires: Wed, 30 Nov 2022 18:28:38 GMT x-backend-server: bedrock-prod-web-b95bc569d-grd25.iowa-a strict-transport-security: max-age=31536000 x-content-type-options: nosniff x-xss-protection: 1; mode=block referrer-policy: strict-origin-when-cross-origin via: 1.1 google, 1.1 6c90b631453c435bd0022caa657b67e8.cloudfront.net (CloudFront) x-cache: Error from cloudfront x-amz-cf-pop: SFO5-P2 x-amz-cf-id: A6-9mLztaE2tz840CbV9bXYiBMZRKEamDj6jGGEl1U7sg8egWfsDqg== ~ % curl -X OPTIONS https://example.com -i HTTP/2 200 allow: OPTIONS, GET, HEAD, POST cache-control: max-age=604800 content-type: text/html; charset=UTF-8 date: Wed, 30 Nov 2022 18:18:59 GMT expires: Wed, 07 Dec 2022 18:18:59 GMT server: EOS (vny/0451) content-length: 0 ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1469973742,Make sure CORS works for write APIs, https://github.com/simonw/datasette/issues/1922#issuecomment-1332504654,https://api.github.com/repos/simonw/datasette/issues/1922,1332504654,IC_kwDOBm6k_c5PbGhO,9599,simonw,2022-11-30T17:27:39Z,2022-11-30T17:27:39Z,OWNER,I'll test this once it's deployed to https://latest.datasette.io/,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1469973742,Make sure CORS works for write APIs, https://github.com/simonw/datasette/issues/1922#issuecomment-1332493004,https://api.github.com/repos/simonw/datasette/issues/1922,1332493004,IC_kwDOBm6k_c5PbDrM,9599,simonw,2022-11-30T17:18:10Z,2022-11-30T17:18:10Z,OWNER,"Here's why: https://github.com/simonw/datasette/blob/4ddd77e51254bda3bac990ea662bac2e6b29c5e0/datasette/views/base.py#L71-L79 That's code in `BaseView` - but it turns out the code that adds CORS headers is in the `DataView` subclass of that (which the various write API endpoints do not use). https://github.com/simonw/datasette/blob/4ddd77e51254bda3bac990ea662bac2e6b29c5e0/datasette/views/base.py#L158-L162","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1469973742,Make sure CORS works for write APIs, https://github.com/simonw/datasette/issues/1922#issuecomment-1332492092,https://api.github.com/repos/simonw/datasette/issues/1922,1332492092,IC_kwDOBm6k_c5PbDc8,9599,simonw,2022-11-30T17:17:21Z,2022-11-30T17:17:21Z,OWNER,I tried running `fetch()` with a POST from a separate domain and got a browser error because it did a GET against the `/db/-/create` endpoint and the 405 method not supported response did not include the CORS headers.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1469973742,Make sure CORS works for write APIs, https://github.com/simonw/datasette/issues/1919#issuecomment-1331657404,https://api.github.com/repos/simonw/datasette/issues/1919,1331657404,IC_kwDOBm6k_c5PX3q8,9599,simonw,2022-11-30T05:19:43Z,2022-11-30T05:19:43Z,OWNER,"This is the test: https://github.com/simonw/datasette/blob/8404b21556d133c89eda4bd1bf5335ed9a0785d6/tests/test_api_write.py#L342-L401 I'm suspicious that there's a timing error of some sort but I can't think what it might be.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1469062686,Intermittent `test_delete_row` test failure , https://github.com/simonw/datasette/issues/1918#issuecomment-1331658629,https://api.github.com/repos/simonw/datasette/issues/1918,1331658629,IC_kwDOBm6k_c5PX3-F,9599,simonw,2022-11-30T05:21:51Z,2022-11-30T05:21:51Z,OWNER,"Much better: ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1469044738,API explorer should list mutable databases first, https://github.com/simonw/datasette/issues/1917#issuecomment-1331644751,https://api.github.com/repos/simonw/datasette/issues/1917,1331644751,IC_kwDOBm6k_c5PX0lP,9599,simonw,2022-11-30T04:59:22Z,2022-11-30T04:59:22Z,OWNER,"Yeah it looks like I introduced this bug here: https://github.com/simonw/datasette/commit/fb7e70d5e72a951efe4b29ad999d8915c032d021","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1469043836,Don't allow writable API to edit the `_memory` database, https://github.com/simonw/datasette/issues/1917#issuecomment-1331644078,https://api.github.com/repos/simonw/datasette/issues/1917,1331644078,IC_kwDOBm6k_c5PX0au,9599,simonw,2022-11-30T04:58:06Z,2022-11-30T04:58:06Z,OWNER,"The problem might actually be here: https://github.com/simonw/datasette/blob/9f5321ff1eca58c469a45cc406d7eb5ad05accbd/datasette/app.py#L280-L281 `is_mutable` defaults to `True`, so this line should probably be: ```python self.add_database(Database(self, is_mutable=False, is_memory=True), name=""_memory"") ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1469043836,Don't allow writable API to edit the `_memory` database, https://github.com/simonw/datasette/issues/1916#issuecomment-1331651721,https://api.github.com/repos/simonw/datasette/issues/1916,1331651721,IC_kwDOBm6k_c5PX2SJ,9599,simonw,2022-11-30T05:10:27Z,2022-11-30T05:10:27Z,OWNER,"They should return 405 method not allowed with an `{""ok"":false, ""error"": ""Method not allowed""}` body.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1469015001,GET requests against POST endpoints should not 500 error, https://github.com/simonw/datasette/issues/1915#issuecomment-1331479606,https://api.github.com/repos/simonw/datasette/issues/1915,1331479606,IC_kwDOBm6k_c5PXMQ2,9599,simonw,2022-11-30T00:09:06Z,2022-11-30T00:09:06Z,OWNER,One last feature: I want to show an indication on the table page that the table has X seconds left to live.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1468709531,Interactive demo of Datasette 1.0 write APIs, https://github.com/simonw/datasette/issues/1915#issuecomment-1331479328,https://api.github.com/repos/simonw/datasette/issues/1915,1331479328,IC_kwDOBm6k_c5PXMMg,9599,simonw,2022-11-30T00:08:41Z,2022-11-30T00:08:41Z,OWNER,Five minute has now passed and https://latest.datasette.io/ephemeral/new_table is gone.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1468709531,Interactive demo of Datasette 1.0 write APIs, https://github.com/simonw/datasette/issues/1915#issuecomment-1331478611,https://api.github.com/repos/simonw/datasette/issues/1915,1331478611,IC_kwDOBm6k_c5PXMBT,9599,simonw,2022-11-30T00:07:37Z,2022-11-30T00:07:37Z,OWNER,"Then I created an API token at https://latest.datasette.io/-/create-token and ran this: ``` curl -XPOST 'https://latest.datasette.io/ephemeral/new_table/-/insert' \ -H 'Authorization: Bearer xxx' \ -H 'Content-Type: application/json' \ -d '{""row"": {""name"": ""NAME""}}' ``` And it inserted a row into https://latest.datasette.io/ephemeral/new_table","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1468709531,Interactive demo of Datasette 1.0 write APIs, https://github.com/simonw/datasette/issues/1915#issuecomment-1331476246,https://api.github.com/repos/simonw/datasette/issues/1915,1331476246,IC_kwDOBm6k_c5PXLcW,9599,simonw,2022-11-30T00:04:35Z,2022-11-30T00:08:24Z,OWNER,"The new https://github.com/simonw/datasette-ephemeral-tables plugin is live now: https://latest.datasette.io/ephemeral - you have to navigate through https://latest.datasette.io/login-as-root first It work! I created a table using https://latest.datasette.io/-/api#path=%2Fephemeral%2F-%2Fcreate&json=%7B%0A++%22table%22%3A+%22new_table%22%2C%0A++%22columns%22%3A+%5B%0A++++%7B%0A++++++%22name%22%3A+%22id%22%2C%0A++++++%22type%22%3A+%22integer%22%0A++++%7D%2C%0A++++%7B%0A++++++%22name%22%3A+%22name%22%2C%0A++++++%22type%22%3A+%22text%22%0A++++%7D%0A++%5D%2C%0A++%22pk%22%3A+%22id%22%0A%7D&method=POST The table should vanish in a few minutes.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1468709531,Interactive demo of Datasette 1.0 write APIs, https://github.com/simonw/datasette/issues/1915#issuecomment-1331432223,https://api.github.com/repos/simonw/datasette/issues/1915,1331432223,IC_kwDOBm6k_c5PXAsf,9599,simonw,2022-11-29T23:06:17Z,2022-11-29T23:06:17Z,OWNER,To (slightly) discourage abuse I'm going to make the demo database only visible to the root user - so people can't create tables with rude names and have them show to the public on https://latest.datasette.io/,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1468709531,Interactive demo of Datasette 1.0 write APIs, https://github.com/simonw/datasette/issues/1915#issuecomment-1331331082,https://api.github.com/repos/simonw/datasette/issues/1915,1331331082,IC_kwDOBm6k_c5PWoAK,9599,simonw,2022-11-29T21:24:59Z,2022-11-29T21:34:53Z,OWNER,Maybe a plugin called `datasette-temporary-tables` or `datasette-demo-tables` or `datasette-demo-database`.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1468709531,Interactive demo of Datasette 1.0 write APIs, https://github.com/simonw/datasette/issues/1914#issuecomment-1347801679,https://api.github.com/repos/simonw/datasette/issues/1914,1347801679,IC_kwDOBm6k_c5QVdJP,9599,simonw,2022-12-13T06:15:54Z,2022-12-13T06:15:54Z,OWNER,"Should make sure that every API that returns an object as the top level (that's almost all of them) includes `""ok"": true` to indicate no errors.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1468689139,Finalize design of JSON for Datasette 1.0, https://github.com/simonw/datasette/issues/1913#issuecomment-1331238029,https://api.github.com/repos/simonw/datasette/issues/1913,1331238029,IC_kwDOBm6k_c5PWRSN,9599,simonw,2022-11-29T20:10:35Z,2022-11-29T20:10:35Z,OWNER,"Released: - https://pypi.org/project/datasette/1.0a0/ - https://docs.datasette.io/en/latest/changelog.html#a0-2022-11-29","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1468603401,Release Datasette 1.0a0, https://github.com/simonw/datasette/issues/1913#issuecomment-1331226346,https://api.github.com/repos/simonw/datasette/issues/1913,1331226346,IC_kwDOBm6k_c5PWObq,9599,simonw,2022-11-29T20:00:16Z,2022-11-29T20:00:36Z,OWNER,"Looks like a fix is coming: https://github.com/pypa/twine/issues/940#issuecomment-1331225509 > > Note that `must_decode` was defined in `pkg_info/_compat.py`, and was thus never an API: before 1.9.0, it was only imported and used in `pkginfo/distribution.py'. > > Nevertheless, I will push out a 1.9.1 release of `pkginfo` which restores a deprecated compatibility alias.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1468603401,Release Datasette 1.0a0, https://github.com/simonw/datasette/issues/1913#issuecomment-1331225277,https://api.github.com/repos/simonw/datasette/issues/1913,1331225277,IC_kwDOBm6k_c5PWOK9,9599,simonw,2022-11-29T19:59:14Z,2022-11-29T19:59:34Z,OWNER,I deleted the tag and tried creating a new release. Now running here: https://github.com/simonw/datasette/actions/runs/3577554546,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1468603401,Release Datasette 1.0a0, https://github.com/simonw/datasette/issues/1913#issuecomment-1331216652,https://api.github.com/repos/simonw/datasette/issues/1913,1331216652,IC_kwDOBm6k_c5PWMEM,9599,simonw,2022-11-29T19:54:22Z,2022-11-29T19:54:22Z,OWNER,Filed a bug report here: https://bugs.launchpad.net/pkginfo/+bug/1998249,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1468603401,Release Datasette 1.0a0, https://github.com/simonw/datasette/issues/1913#issuecomment-1331208206,https://api.github.com/repos/simonw/datasette/issues/1913,1331208206,IC_kwDOBm6k_c5PWKAO,9599,simonw,2022-11-29T19:51:31Z,2022-11-29T19:51:31Z,OWNER,https://pypi.org/project/pkginfo/#history - 1.9.0 came out 39 minutes ago!,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1468603401,Release Datasette 1.0a0, https://github.com/simonw/datasette/issues/1913#issuecomment-1331207334,https://api.github.com/repos/simonw/datasette/issues/1913,1331207334,IC_kwDOBm6k_c5PWJym,9599,simonw,2022-11-29T19:50:37Z,2022-11-29T19:50:37Z,OWNER,"https://pypi.org/project/setuptools/65.6.3/ came out most recently - 23rd November (wheel and twine are older). No search results at all for that error message. This is very weird, I would have expected it to have been reported by now.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1468603401,Release Datasette 1.0a0, https://github.com/simonw/datasette/issues/1913#issuecomment-1331205613,https://api.github.com/repos/simonw/datasette/issues/1913,1331205613,IC_kwDOBm6k_c5PWJXt,9599,simonw,2022-11-29T19:48:52Z,2022-11-29T19:48:52Z,OWNER,https://github.com/simonw/datasette/blob/07aad511769da9242260c850e8d975cbd8c29552/.github/workflows/publish.yml#L52-L61,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1468603401,Release Datasette 1.0a0, https://github.com/simonw/datasette/issues/1913#issuecomment-1331204360,https://api.github.com/repos/simonw/datasette/issues/1913,1331204360,IC_kwDOBm6k_c5PWJEI,9599,simonw,2022-11-29T19:47:40Z,2022-11-29T19:47:40Z,OWNER,"... but the last step of the deploy failed, when it was meant to push to PyPI! ``` Uploading distributions to https://upload.pypi.org/legacy/ Traceback (most recent call last): File ""/opt/hostedtoolcache/Python/3.11.0/x64/bin/twine"", line 8, in sys.exit(main()) ^^^^^^ File ""/opt/hostedtoolcache/Python/3.11.0/x64/lib/python3.11/site-packages/twine/__main__.py"", line 33, in main error = cli.dispatch(sys.argv[1:]) ^^^^^^^^^^^^^^^^^^^^^^^^^^ File ""/opt/hostedtoolcache/Python/3.11.0/x64/lib/python3.11/site-packages/twine/cli.py"", line 123, in dispatch return main(args.args) ^^^^^^^^^^^^^^^ File ""/opt/hostedtoolcache/Python/3.11.0/x64/lib/python3.11/site-packages/twine/commands/upload.py"", line 198, in main return upload(upload_settings, parsed_args.dists) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File ""/opt/hostedtoolcache/Python/3.11.0/x64/lib/python3.11/site-packages/twine/commands/upload.py"", line 123, in upload packages_to_upload = [ ^ File ""/opt/hostedtoolcache/Python/3.11.0/x64/lib/python3.11/site-packages/twine/commands/upload.py"", line 124, in _make_package(filename, signatures, upload_settings) for filename in uploads ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File ""/opt/hostedtoolcache/Python/3.11.0/x64/lib/python3.11/site-packages/twine/commands/upload.py"", line 77, in _make_package package = package_file.PackageFile.from_filename(filename, upload_settings.comment) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File ""/opt/hostedtoolcache/Python/3.11.0/x64/lib/python3.11/site-packages/twine/package.py"", line 96, in from_filename meta = DIST_TYPES[dtype](filename) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File ""/opt/hostedtoolcache/Python/3.11.0/x64/lib/python3.11/site-packages/twine/wheel.py"", line 42, in __init__ self.extractMetadata() File ""/opt/hostedtoolcache/Python/3.11.0/x64/lib/python3.11/site-packages/pkginfo/distribution.py"", line 121, in extractMetadata self.parse(data) File ""/opt/hostedtoolcache/Python/3.11.0/x64/lib/python3.11/site-packages/twine/wheel.py"", line 89, in parse fp = io.StringIO(distribution.must_decode(data)) ^^^^^^^^^^^^^^^^^^^^^^^^ AttributeError: module 'pkginfo.distribution' has no attribute 'must_decode'. Did you mean: '_must_decode'? Error: Process completed with exit code 1. ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1468603401,Release Datasette 1.0a0, https://github.com/simonw/datasette/issues/1913#issuecomment-1331203997,https://api.github.com/repos/simonw/datasette/issues/1913,1331203997,IC_kwDOBm6k_c5PWI-d,9599,simonw,2022-11-29T19:47:13Z,2022-11-29T19:47:13Z,OWNER,"Weird, retrying the tests DID get them to pass. https://github.com/simonw/datasette/actions/runs/3577355358/jobs/6016518244","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1468603401,Release Datasette 1.0a0, https://github.com/simonw/datasette/pull/1912#issuecomment-1331196531,https://api.github.com/repos/simonw/datasette/issues/1912,1331196531,IC_kwDOBm6k_c5PWHJz,9599,simonw,2022-11-29T19:39:10Z,2022-11-29T19:39:10Z,OWNER,"Annoyingly it looks like I can't rebase this one, and I don't want to squash-merge and lose the commits, so I'm going to do a regular merge instead.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1468592292,Merge 1.0-dev (with initial write API) back into main, https://github.com/simonw/datasette/issues/1911#issuecomment-1331135709,https://api.github.com/repos/simonw/datasette/issues/1911,1331135709,IC_kwDOBm6k_c5PV4Td,9599,simonw,2022-11-29T18:50:58Z,2022-11-29T18:50:58Z,OWNER,Updated docs: https://docs.datasette.io/en/1.0-dev/json_api.html#creating-a-table,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1468519699,`/db/-/create` should support creating tables with compound primary keys, https://github.com/simonw/datasette/issues/1911#issuecomment-1331120055,https://api.github.com/repos/simonw/datasette/issues/1911,1331120055,IC_kwDOBm6k_c5PV0e3,9599,simonw,2022-11-29T18:36:01Z,2022-11-29T18:36:01Z,OWNER,"Current API design: ``` POST //-/create ``` ```json { ""table"": ""name_of_new_table"", ""columns"": [ { ""name"": ""id"", ""type"": ""integer"" }, { ""name"": ""title"", ""type"": ""text"" } ], ""pk"": ""id"" } ``` I'm going to add a new `""pks""` key which is a list, and can be used in place of `""pk""`.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1468519699,`/db/-/create` should support creating tables with compound primary keys, https://github.com/simonw/datasette/issues/1909#issuecomment-1329624931,https://api.github.com/repos/simonw/datasette/issues/1909,1329624931,IC_kwDOBm6k_c5PQHdj,9599,simonw,2022-11-28T19:19:26Z,2022-11-28T19:19:26Z,OWNER,The list of states here is a good example of somewhere this might be useful: https://congress-legislators.datasettes.com/legislators/legislator_terms?_facet=state&_facet_size=max,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1466952626,Option to sort facets alphabetically, https://github.com/simonw/sqlite-utils/pull/515#issuecomment-1539077777,https://api.github.com/repos/simonw/sqlite-utils/issues/515,1539077777,IC_kwDOCGYnMM5bvHaR,9599,simonw,2023-05-08T21:27:10Z,2023-05-08T21:27:10Z,OWNER,I should have spotted this PR before I shipped my own fix! https://github.com/simonw/sqlite-utils/commit/2376c452a56b0c3e75e7ca698273434e32945304,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1465194930,"upsert new rows with constraints, fixes #514", https://github.com/simonw/sqlite-utils/issues/514#issuecomment-1539100300,https://api.github.com/repos/simonw/sqlite-utils/issues/514,1539100300,IC_kwDOCGYnMM5bvM6M,9599,simonw,2023-05-08T21:50:51Z,2023-05-08T21:50:51Z,OWNER,Seeing as `sqlite-utils` doesn't currently provide mechanisms for adding `check` constraints like this I'm going to leave this - I'm happy with the fix I put in for the `not null` constraints.,"{""total_count"": 1, ""+1"": 1, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1465194249,upsert of new row with check constraints fails, https://github.com/simonw/sqlite-utils/issues/514#issuecomment-1539099703,https://api.github.com/repos/simonw/sqlite-utils/issues/514,1539099703,IC_kwDOCGYnMM5bvMw3,9599,simonw,2023-05-08T21:50:06Z,2023-05-08T21:50:06Z,OWNER,"Applying the fix from the PR here doesn't fix the above problem either: - https://github.com/simonw/sqlite-utils/pull/515 So it looks like these kinds of `check` constraints currently aren't compatible with how `upsert()` works.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1465194249,upsert of new row with check constraints fails, https://github.com/simonw/sqlite-utils/issues/514#issuecomment-1539094287,https://api.github.com/repos/simonw/sqlite-utils/issues/514,1539094287,IC_kwDOCGYnMM5bvLcP,9599,simonw,2023-05-08T21:44:11Z,2023-05-08T21:44:11Z,OWNER,"OK, this fails silently: ```python import sqlite_utils db = sqlite_utils.Database(memory=True) db.execute('''CREATE TABLE employees ( id INTEGER PRIMARY KEY, name TEXT, age INTEGER, salary REAL, CHECK (salary is not null and salary > 0) );''') db[""employees""].upsert({""id"": 1, ""name"": ""Bob""}, pk=""id"") list(db[""employees""].rows) ```` It outputs: ```python [] ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1465194249,upsert of new row with check constraints fails, https://github.com/simonw/sqlite-utils/issues/514#issuecomment-1539079507,https://api.github.com/repos/simonw/sqlite-utils/issues/514,1539079507,IC_kwDOCGYnMM5bvH1T,9599,simonw,2023-05-08T21:28:37Z,2023-05-08T21:28:37Z,OWNER,"> This means that a table with NON NULL (or other constraint) columns that aren't part of the pkey can't have new rows upserted. Huh... on that basis, it's possible my fix in https://github.com/simonw/sqlite-utils/commit/2376c452a56b0c3e75e7ca698273434e32945304 is incomplete. I only covered the 'not null' case.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1465194249,upsert of new row with check constraints fails, https://github.com/simonw/sqlite-utils/issues/514#issuecomment-1539078429,https://api.github.com/repos/simonw/sqlite-utils/issues/514,1539078429,IC_kwDOCGYnMM5bvHkd,9599,simonw,2023-05-08T21:27:40Z,2023-05-08T21:27:40Z,OWNER,"Dupe of: - #538","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1465194249,upsert of new row with check constraints fails, https://github.com/simonw/datasette/issues/1905#issuecomment-1320721241,https://api.github.com/repos/simonw/datasette/issues/1905,1320721241,IC_kwDOBm6k_c5OuJtZ,9599,simonw,2022-11-19T01:12:05Z,2022-11-19T01:12:05Z,OWNER,Used it to deploy this: https://fivethirtyeight.datasettes.com/-/versions,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1456012874,`publish heroku` failing due to old Python version, https://github.com/simonw/datasette/issues/1905#issuecomment-1320706596,https://api.github.com/repos/simonw/datasette/issues/1905,1320706596,IC_kwDOBm6k_c5OuGIk,9599,simonw,2022-11-19T00:41:44Z,2022-11-19T00:41:44Z,OWNER,"Tested a deploy: ``` % datasette publish heroku fixtures.db -n datasette-issue-1905 › Warning: heroku update available from 7.63.0 to 7.66.4. › Warning: heroku update available from 7.63.0 to 7.66.4. › Warning: heroku update available from 7.63.0 to 7.66.4. Creating datasette-issue-1905... done › Warning: heroku update available from 7.63.0 to 7.66.4. ▸ Couldn't detect GNU tar. Builds could fail due to decompression errors ▸ See https://devcenter.heroku.com/articles/platform-api-deploying-slugs#create-slug-archive ▸ Please install it, or specify the '--tar' option ▸ Falling back to node's built-in compressor -----> Building on the Heroku-22 stack -----> Determining which buildpack to use for this app -----> Python app detected -----> Using Python version specified in runtime.txt -----> Installing python-3.11.0 -----> Installing pip 22.3.1, setuptools 63.4.3 and wheel 0.37.1 -----> Installing SQLite3 -----> Installing requirements with pip Collecting datasette Downloading datasette-0.63.1-py3-none-any.whl (231 kB) ... -----> Running post-compile hook -----> Discovering process types Procfile declares types -> web -----> Compressing... Done: 28M -----> Launching... Released v3 https://datasette-issue-1905.herokuapp.com/ deployed to Heroku Starting November 28th, 2022, free Heroku Dynos, free Heroku Postgres, and free Heroku Data for Redis® will no longer be available. If you have apps using any of these resources, you must upgrade to paid plans by this date to ensure your apps continue to run and to retain your data. For students, we will announce a new program by the end of September. Learn more at https://blog.heroku.com/next-chapter ``` I had to then pay for the dino because I'd run out of free hours. https://datasette-issue-1905.herokuapp.com/-/versions shows: ```json { ""python"": { ""version"": ""3.11.0"", ""full"": ""3.11.0 (main, Oct 24 2022, 21:34:02) [GCC 11.2.0]"" }, ""datasette"": { ""version"": ""0.63.1"" } ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1456012874,`publish heroku` failing due to old Python version, https://github.com/simonw/datasette/issues/1905#issuecomment-1320689643,https://api.github.com/repos/simonw/datasette/issues/1905,1320689643,IC_kwDOBm6k_c5OuB_r,9599,simonw,2022-11-19T00:17:19Z,2022-11-19T00:41:54Z,OWNER,"The tests don't cover this bit at the moment. Would be easier to write tests if there was a `--generate-dir` option as seen in https://datasette.io/plugins/datasette-publish-vercel","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1456012874,`publish heroku` failing due to old Python version, https://github.com/simonw/datasette/issues/1905#issuecomment-1320678715,https://api.github.com/repos/simonw/datasette/issues/1905,1320678715,IC_kwDOBm6k_c5Ot_U7,9599,simonw,2022-11-19T00:02:28Z,2022-11-19T00:02:28Z,OWNER,This is a strong argument for extracting the Heroku support out to a plugin - it would allow this to be fixed with a plugin release without needing to push a full release of Datasette itself.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1456012874,`publish heroku` failing due to old Python version, https://github.com/simonw/datasette/issues/1903#issuecomment-1321262142,https://api.github.com/repos/simonw/datasette/issues/1903,1321262142,IC_kwDOBm6k_c5OwNw-,9599,simonw,2022-11-20T22:35:01Z,2022-11-20T22:35:01Z,OWNER,A want to call this `datasette/exceptions.py` inspired by Takahē: https://github.com/andrewgodwin/takahe/blob/f491fdb56e2de9200e14b855b5576009ca99dfa5/core/exceptions.py,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1455928469,Refactor all error classes into a datasette.exceptions module, https://github.com/simonw/datasette/issues/1903#issuecomment-1320614541,https://api.github.com/repos/simonw/datasette/issues/1903,1320614541,IC_kwDOBm6k_c5OtvqN,9599,simonw,2022-11-18T22:47:41Z,2022-11-18T22:47:41Z,OWNER,"When I do this it's important to update the documentation for `resolve_database()` and the like: https://github.com/simonw/datasette/blob/ee64130fa8a5ff4a24791916c696e10cf2375102/docs/internals.rst#L594","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1455928469,Refactor all error classes into a datasette.exceptions module, https://github.com/simonw/datasette/issues/1901#issuecomment-1319627012,https://api.github.com/repos/simonw/datasette/issues/1901,1319627012,IC_kwDOBm6k_c5Op-kE,9599,simonw,2022-11-18T07:07:03Z,2022-11-18T07:07:03Z,OWNER,"Here's the full list of 10 plugin releases for this issue: * [datasette-search-all 1.1.1](https://github.com/simonw/datasette-search-all/releases/tag/1.1.1) * [datasette-ripgrep 0.7.1](https://github.com/simonw/datasette-ripgrep/releases/tag/0.7.1) * [datasette-socrata 0.3.1](https://github.com/simonw/datasette-socrata/releases/tag/0.3.1) * [datasette-configure-fts 1.1.1](https://github.com/simonw/datasette-configure-fts/releases/tag/1.1.1) * [datasette-edit-templates 0.2](https://github.com/simonw/datasette-edit-templates/releases/tag/0.2) * [datasette-copyable 0.3.2](https://github.com/simonw/datasette-copyable/releases/tag/0.3.2) * [datasette-public 0.2.1](https://github.com/simonw/datasette-public/releases/tag/0.2.1) * [datasette-import-table 0.3.1](https://github.com/simonw/datasette-import-table/releases/tag/0.3.1) * [datasette-indieauth 1.2.2](https://github.com/simonw/datasette-indieauth/releases/tag/1.2.2) * [datasette-edit-schema 0.5.2](https://github.com/simonw/datasette-edit-schema/releases/tag/0.5.2)","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1453813400,"Some plugins show ""home"" breadcrumbs twice in the top left", https://github.com/simonw/datasette/issues/1901#issuecomment-1319623911,https://api.github.com/repos/simonw/datasette/issues/1901,1319623911,IC_kwDOBm6k_c5Op9zn,9599,simonw,2022-11-18T07:02:56Z,2022-11-18T07:02:56Z,OWNER,That's all of them!,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1453813400,"Some plugins show ""home"" breadcrumbs twice in the top left", https://github.com/simonw/datasette/issues/1901#issuecomment-1319588163,https://api.github.com/repos/simonw/datasette/issues/1901,1319588163,IC_kwDOBm6k_c5Op1FD,9599,simonw,2022-11-18T06:05:11Z,2022-11-18T06:05:11Z,OWNER,"For `datasette-copyable` I want to show breadcrumbs that take database/instance permissions into account, so I'm removing `{% block nav %}` entirely and replacing it with this: ```html+jinja {% block crumbs %} {{ crumbs.nav(request=request, database=database, table=table) }} {% endblock %} ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1453813400,"Some plugins show ""home"" breadcrumbs twice in the top left", https://github.com/simonw/datasette/issues/1901#issuecomment-1319528359,https://api.github.com/repos/simonw/datasette/issues/1901,1319528359,IC_kwDOBm6k_c5Opmen,9599,simonw,2022-11-18T04:27:00Z,2022-11-18T04:27:00Z,OWNER,Also `datasette-indieauth` https://github.com/simonw/datasette-indieauth/blob/a08ce67ddad6098b1240adbeff37d040e4df53b1/datasette_indieauth/templates/indieauth.html#L5-L10,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1453813400,"Some plugins show ""home"" breadcrumbs twice in the top left", https://github.com/simonw/datasette/issues/1901#issuecomment-1319525520,https://api.github.com/repos/simonw/datasette/issues/1901,1319525520,IC_kwDOBm6k_c5OplyQ,9599,simonw,2022-11-18T04:21:14Z,2022-11-18T07:22:37Z,OWNER,This search helps too: [https://ripgrep.datasette.io/-/ripgrep?pattern=%7B%25+block+nav&literal=on&ignore=on&glob=%21datasette%2F**](https://ripgrep.datasette.io/-/ripgrep?pattern=%7B%25+block+nav&literal=on&ignore=on&glob=%21datasette%2F**),"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1453813400,"Some plugins show ""home"" breadcrumbs twice in the top left", https://github.com/simonw/datasette/issues/1901#issuecomment-1319493475,https://api.github.com/repos/simonw/datasette/issues/1901,1319493475,IC_kwDOBm6k_c5Opd9j,9599,simonw,2022-11-18T03:19:25Z,2022-11-18T07:03:03Z,OWNER,"Other plugins this looks like it will affect: - [x] `datasette-ripgrep` https://github.com/simonw/datasette-ripgrep/blob/03446464420130368582022eeb5944993f64ec8f/datasette_ripgrep/templates/ripgrep.html#L37-L42 - [x] `datasette-socrata` https://github.com/simonw/datasette-socrata/blob/32fb256a461bf0e790eca10bdc7dd9d96c20f7c4/datasette_socrata/templates/datasette_socrata_error.html#L5-L10 - [x] `datasette-configure-fts` https://github.com/simonw/datasette-configure-fts/blob/eca742e5d4b9190fc22d68bc0a406c575e6d09a0/datasette_configure_fts/templates/configure_fts_database.html#L9-L14 - [x] `datasette-edit-templates` https://github.com/simonw/datasette-edit-templates/blob/f772aff4a2a4080c949746668a8ec6302dbeb0d9/datasette_edit_templates/templates/edit_template.html#L17-L23 - [x] `datasette-copyable` https://github.com/simonw/datasette-copyable/blob/204d5c912a8d48c49155c67fba7339d4bb26ab9a/datasette_copyable/templates/copyable.html#L36-L43 - [x] `datasette-public` https://github.com/simonw/datasette-public/blob/32b6a0ba53bd5714b6b41eddd8705b213c105efc/datasette_public/templates/public_table_change_privacy.html#L5-L11 - [x] `datasette-import-table` https://github.com/simonw/datasettecloud-datasette/blob/37d0fe525c6649c1aec3d1ee8bc35a684570e87f/templates/import_data.html#L5-L10 - [x] `datasette-edit-schema` (three places) - [x] `datasette-indieauth` https://github.com/simonw/datasette-indieauth/blob/a08ce67ddad6098b1240adbeff37d040e4df53b1/datasette_indieauth/templates/indieauth.html#L5-L10","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1453813400,"Some plugins show ""home"" breadcrumbs twice in the top left", https://github.com/simonw/datasette/issues/1901#issuecomment-1319483555,https://api.github.com/repos/simonw/datasette/issues/1901,1319483555,IC_kwDOBm6k_c5Opbij,9599,simonw,2022-11-18T03:02:35Z,2022-11-18T03:02:35Z,OWNER,Looks like this issue could affect a bunch of other plugins too: https://cs.github.com/?scopeName=All+repos&scope=&q=%3Cp+class%3D%22crumbs%22%3E+user%3Asimonw,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1453813400,"Some plugins show ""home"" breadcrumbs twice in the top left", https://github.com/simonw/datasette/issues/1901#issuecomment-1319482791,https://api.github.com/repos/simonw/datasette/issues/1901,1319482791,IC_kwDOBm6k_c5OpbWn,9599,simonw,2022-11-18T03:01:36Z,2022-11-18T03:01:36Z,OWNER,"Good catch. Looks like that bug was introduced by this change: https://github.com/simonw/datasette/commit/1a5e5f2aa951e5bd731067a49819efba68fbe8ef From: - https://github.com/simonw/datasette/issues/1831 The search all plugin includes this code which interacts poorly with that refactor: https://github.com/simonw/datasette-search-all/blob/847b55c368a285e4567627029624d7872ee75cac/datasette_search_all/templates/search_all.html#L31-L36 ```html+jinja {% block nav %}

home

{{ super() }} {% endblock %} ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1453813400,"Some plugins show ""home"" breadcrumbs twice in the top left", https://github.com/simonw/datasette/issues/1900#issuecomment-1319664697,https://api.github.com/repos/simonw/datasette/issues/1900,1319664697,IC_kwDOBm6k_c5OqHw5,419145,rdmurphy,2022-11-18T07:59:36Z,2022-11-18T08:00:38Z,NONE,"Okay, my final observations for the night! I've been pushing and pulling the various levers in `utils/__init__.py` to see what makes this work without hard-coding in something for `arm64` and it seems that if I change `/usr/lib/x86_64-linux-gnu/mod_spatialite.so` [here](https://github.com/simonw/datasette/blob/3ecd131e57add427d847b614c920c9624bb2e66b/datasette/utils/__init__.py#L407) to just `mod_spatialite` it's happy. Unfortunately cannot audit that for `x86_64`, but maybe that's a solution that'd be cross-arch compatible? It seems like it's the hard-coding of that path that's tripping it up. (It was actually [this comment from back in 2018 in an entirely unrelated repo](https://github.com/pelias/docker/pull/28#issuecomment-433168462) that nudged me to try this, ha.)","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1452572348,datasette package --spatialite throws error during build, https://github.com/simonw/datasette/issues/1900#issuecomment-1319641636,https://api.github.com/repos/simonw/datasette/issues/1900,1319641636,IC_kwDOBm6k_c5OqCIk,419145,rdmurphy,2022-11-18T07:27:26Z,2022-11-18T07:27:26Z,NONE,"Can confirm that my `uname -a` returns something different at the end: ``` root:xnu-8792.41.9~2/RELEASE_ARM64_T6000 arm64 ``` I'm in `arm64` land, you're in `x86_64`. I am admittedly very fuzzy on how this factors into Docker these days. Honestly thought this was one of the things Docker was suppose to help address. 🤔","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1452572348,datasette package --spatialite throws error during build, https://github.com/simonw/datasette/issues/1900#issuecomment-1319639462,https://api.github.com/repos/simonw/datasette/issues/1900,1319639462,IC_kwDOBm6k_c5OqBmm,419145,rdmurphy,2022-11-18T07:24:19Z,2022-11-18T07:24:19Z,NONE,"Is it, uh, possible we are on different architectures? 😅 I'm using an Apple M1 Pro. I jumped into a bash shell of an unmodified `python:3.11.0-slim-bullseye` container and manually ran `apt-get update` and installed `libsqlite3-mod-spatialite`. I don't end up with with `mod_spatialite.so` in `/usr/lib/x86_64-linux-gnu/` — _mine_ is in `/usr/lib/aarch64-linux-gnu/`. [I swapped that directory in here](https://github.com/simonw/datasette/blob/3db37e9a21f774d6c387fd04bf1e4c870554209e/datasette/utils/__init__.py#L407) in a local copy of `datasette` and poof — it worked!","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1452572348,datasette package --spatialite throws error during build, https://github.com/simonw/datasette/issues/1900#issuecomment-1319631421,https://api.github.com/repos/simonw/datasette/issues/1900,1319631421,IC_kwDOBm6k_c5Op_o9,9599,simonw,2022-11-18T07:13:00Z,2022-11-18T07:13:00Z,OWNER,"You get: ``` => [internal] load metadata for docker.io/library/python:3.11.0-slim-bullseye 0.9s => [internal] load build context 2.3s => => transferring context: 72.38MB 2.3s => CACHED [1/6] FROM docker.io/library/python:3.11.0-slim-bullseye@sha256:1cd45c5dad845af18d71745c017325725dc979571c1bbe625b67e6051533716c 0.0s ``` I get: ``` => [internal] load metadata for docker.io/library/python:3.11.0-slim-bullseye 1.0s => [internal] load build context 0.0s => => transferring context: 705B 0.0s => CACHED [1/6] FROM docker.io/library/python:3.11.0-slim-bullseye@sha256:1cd45c5dad845af18d71745c017325725dc979571c1bbe625b67e6051533716c 0.0s ``` Both the image name and the hash are _exactly_ the same. So why are you getting an error while mine works OK? For my machine: ``` ~ % docker --version Docker version 20.10.12, build e91ed57 ~ % uname -a Darwin Simons-MacBook-Pro-2.local 22.1.0 Darwin Kernel Version 22.1.0: Sun Oct 9 20:14:54 PDT 2022; root:xnu-8792.41.9~2/RELEASE_X86_64 x86_64 ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1452572348,datasette package --spatialite throws error during build, https://github.com/simonw/datasette/issues/1900#issuecomment-1319629469,https://api.github.com/repos/simonw/datasette/issues/1900,1319629469,IC_kwDOBm6k_c5Op_Kd,9599,simonw,2022-11-18T07:10:17Z,2022-11-18T07:10:17Z,OWNER,This is so weird! What version of Datasette do you get from `datasette --version` there - and what's your Docker version / operating system version?,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1452572348,datasette package --spatialite throws error during build, https://github.com/simonw/datasette/issues/1900#issuecomment-1319596087,https://api.github.com/repos/simonw/datasette/issues/1900,1319596087,IC_kwDOBm6k_c5Op3A3,419145,rdmurphy,2022-11-18T06:16:33Z,2022-11-18T06:16:33Z,NONE,"Interesting! So I tried this locally using your copy of `nps-spatialite.db` and I got the same error. 🤔 ``` ❯ datasette package nps-spatialite.db --spatialite [+] Building 27.5s (10/10) FINISHED => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 622B 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 2B 0.0s => [internal] load metadata for docker.io/library/python:3.11.0-slim-bullseye 0.9s => [internal] load build context 2.3s => => transferring context: 72.38MB 2.3s => CACHED [1/6] FROM docker.io/library/python:3.11.0-slim-bullseye@sha256:1cd45c5dad845af18d71745c017325725dc979571c1bbe625b67e6051533716c 0.0s => [2/6] COPY . /app 0.1s => [3/6] WORKDIR /app 0.0s => [4/6] RUN apt-get update && apt-get install -y python3-dev gcc libsqlite3-mod-spatialite && rm -rf /var/lib/apt/lists/* 18.5s => [5/6] RUN pip install -U datasette 4.9s => ERROR [6/6] RUN datasette inspect nps-spatialite.db --inspect-file inspect-data.json 0.7s ------ > [6/6] RUN datasette inspect nps-spatialite.db --inspect-file inspect-data.json: #10 0.681 Traceback (most recent call last): #10 0.681 File ""/usr/local/bin/datasette"", line 8, in #10 0.681 sys.exit(cli()) #10 0.681 ^^^^^ #10 0.681 File ""/usr/local/lib/python3.11/site-packages/click/core.py"", line 1130, in __call__ #10 0.682 return self.main(*args, **kwargs) #10 0.682 ^^^^^^^^^^^^^^^^^^^^^^^^^^ #10 0.682 File ""/usr/local/lib/python3.11/site-packages/click/core.py"", line 1055, in main #10 0.682 rv = self.invoke(ctx) #10 0.682 ^^^^^^^^^^^^^^^^ #10 0.682 File ""/usr/local/lib/python3.11/site-packages/click/core.py"", line 1657, in invoke #10 0.682 return _process_result(sub_ctx.command.invoke(sub_ctx)) #10 0.682 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #10 0.682 File ""/usr/local/lib/python3.11/site-packages/click/core.py"", line 1404, in invoke #10 0.682 return ctx.invoke(self.callback, **ctx.params) #10 0.682 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #10 0.682 File ""/usr/local/lib/python3.11/site-packages/click/core.py"", line 760, in invoke #10 0.682 return __callback(*args, **kwargs) #10 0.682 ^^^^^^^^^^^^^^^^^^^^^^^^^^^ #10 0.683 File ""/usr/local/lib/python3.11/site-packages/datasette/cli.py"", line 164, in inspect #10 0.683 inspect_data = loop.run_until_complete(inspect_(files, sqlite_extensions)) #10 0.683 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #10 0.683 File ""/usr/local/lib/python3.11/asyncio/base_events.py"", line 650, in run_until_complete #10 0.683 return future.result() #10 0.683 ^^^^^^^^^^^^^^^ #10 0.683 File ""/usr/local/lib/python3.11/site-packages/datasette/cli.py"", line 179, in inspect_ #10 0.683 counts = await database.table_counts(limit=3600 * 1000) #10 0.683 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #10 0.683 File ""/usr/local/lib/python3.11/site-packages/datasette/database.py"", line 304, in table_counts #10 0.683 for table in await self.table_names(): #10 0.683 ^^^^^^^^^^^^^^^^^^^^^^^^ #10 0.683 File ""/usr/local/lib/python3.11/site-packages/datasette/database.py"", line 342, in table_names #10 0.683 results = await self.execute( #10 0.683 ^^^^^^^^^^^^^^^^^^^ #10 0.683 File ""/usr/local/lib/python3.11/site-packages/datasette/database.py"", line 267, in execute #10 0.683 results = await self.execute_fn(sql_operation_in_thread) #10 0.683 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #10 0.683 File ""/usr/local/lib/python3.11/site-packages/datasette/database.py"", line 213, in execute_fn #10 0.683 return await asyncio.get_event_loop().run_in_executor( #10 0.683 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #10 0.683 File ""/usr/local/lib/python3.11/concurrent/futures/thread.py"", line 58, in run #10 0.683 result = self.fn(*self.args, **self.kwargs) #10 0.683 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #10 0.683 File ""/usr/local/lib/python3.11/site-packages/datasette/database.py"", line 209, in in_thread #10 0.683 self.ds._prepare_connection(conn, self.name) #10 0.683 File ""/usr/local/lib/python3.11/site-packages/datasette/app.py"", line 593, in _prepare_connection #10 0.683 conn.execute(""SELECT load_extension(?)"", [extension]) #10 0.683 sqlite3.OperationalError: /usr/lib/x86_64-linux-gnu/mod_spatialite.so.so: cannot open shared object file: No such file or directory ------ executor failed running [/bin/sh -c datasette inspect nps-spatialite.db --inspect-file inspect-data.json]: exit code: 1 ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1452572348,datasette package --spatialite throws error during build, https://github.com/simonw/datasette/issues/1900#issuecomment-1319583703,https://api.github.com/repos/simonw/datasette/issues/1900,1319583703,IC_kwDOBm6k_c5Opz_X,9599,simonw,2022-11-18T05:58:31Z,2022-11-18T05:58:31Z,OWNER,Could you provide full steps to reproduce plus a SpatiaLite database file that triggered this for you? I'm not able to recreate the problem.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1452572348,datasette package --spatialite throws error during build, https://github.com/simonw/datasette/issues/1900#issuecomment-1319583281,https://api.github.com/repos/simonw/datasette/issues/1900,1319583281,IC_kwDOBm6k_c5Opz4x,9599,simonw,2022-11-18T05:57:44Z,2022-11-18T05:57:44Z,OWNER,"Did you use the `--spatialite` option? I just tried this: datasette package nps-spatialite.db It built the image OK (I didn't see the error you reported), but running the container failed with an error: ``` /tmp % docker run -p 8001:8001 7298e8e6bbfb Usage: datasette serve [OPTIONS] [FILES]... Try 'datasette serve --help' for help. Error: It looks like you're trying to load a SpatiaLite database without first loading the SpatiaLite module. Read more: https://docs.datasette.io/en/stable/spatialite.html ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1452572348,datasette package --spatialite throws error during build, https://github.com/simonw/datasette/issues/1900#issuecomment-1319582239,https://api.github.com/repos/simonw/datasette/issues/1900,1319582239,IC_kwDOBm6k_c5Opzof,9599,simonw,2022-11-18T05:55:38Z,2022-11-18T05:55:38Z,OWNER,"Trying this out locally with this 69MB SpatiaLite file I happened to have lying around (from testing `shapefile-to-sqlite` a while ago): https://static.simonwillison.net/static/2022/nps-spatialite.db ``` % datasette package nps-spatialite.db --spatialite ... => [2/6] COPY . /app 0.4s => [3/6] WORKDIR /app 0.0s => [4/6] RUN apt-get update && apt-get install -y python3-dev gcc libsqlite3-mod-spatialite && rm -rf /var/lib/apt/lists/* 29.6s => [5/6] RUN pip install -U datasette 12.0s => [6/6] RUN datasette inspect nps-spatialite.db --inspect-file inspect-data.json 2.6s => exporting to image 3.0s => => exporting layers 3.0s => => writing image sha256:4dfef1c373c5c057ef7ac22344f834d522acef24313a1b25d2eba9e500066b8f 0.0s ``` And then: docker run -p 8001:8001 4dfef1c373c5 This worked fine for me. I ran `datasette package` using Datasette 0.63.1.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1452572348,datasette package --spatialite throws error during build, https://github.com/simonw/datasette/issues/1900#issuecomment-1319574972,https://api.github.com/repos/simonw/datasette/issues/1900,1319574972,IC_kwDOBm6k_c5Opx28,9599,simonw,2022-11-18T05:41:28Z,2022-11-18T05:41:28Z,OWNER,Oh this is with `datasette package`? That should work. Will investigate.,"{""total_count"": 1, ""+1"": 1, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1452572348,datasette package --spatialite throws error during build, https://github.com/simonw/datasette/issues/1900#issuecomment-1319571220,https://api.github.com/repos/simonw/datasette/issues/1900,1319571220,IC_kwDOBm6k_c5Opw8U,9599,simonw,2022-11-18T05:34:35Z,2022-11-18T05:34:35Z,OWNER,Which Docker image are you using here? It looks like it's missing SpatiaLite from the image.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1452572348,datasette package --spatialite throws error during build, https://github.com/simonw/datasette/issues/1899#issuecomment-1319642338,https://api.github.com/repos/simonw/datasette/issues/1899,1319642338,IC_kwDOBm6k_c5OqCTi,9599,simonw,2022-11-18T07:28:28Z,2022-11-18T07:28:28Z,OWNER,Demo: https://latest.datasette.io/fixtures,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1452495049,Clicking within the CodeMirror area below the SQL (i.e. when there's only a single line) doesn't cause the editor to get focused , https://github.com/simonw/datasette/issues/1899#issuecomment-1319584553,https://api.github.com/repos/simonw/datasette/issues/1899,1319584553,IC_kwDOBm6k_c5Op0Mp,9599,simonw,2022-11-18T06:00:10Z,2022-11-18T06:01:50Z,OWNER,"I can't actually remember where that `min-height: 70px` came from. I just tried without it and it seems fine - especially since any time you add a newline in the editor it increases its height to fit. I ran this in the DevTools console: ```javascript document.querySelector('.cm-editor').style.minHeight = 'none'; ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1452495049,Clicking within the CodeMirror area below the SQL (i.e. when there's only a single line) doesn't cause the editor to get focused , https://github.com/simonw/datasette/issues/1899#issuecomment-1318897922,https://api.github.com/repos/simonw/datasette/issues/1899,1318897922,IC_kwDOBm6k_c5OnMkC,95570,bgrins,2022-11-17T16:32:42Z,2022-11-17T16:32:42Z,CONTRIBUTOR,Another idea would be to just not set a min-height and allow the 1 line input to be 1 line heigh,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1452495049,Clicking within the CodeMirror area below the SQL (i.e. when there's only a single line) doesn't cause the editor to get focused , https://github.com/simonw/datasette/issues/1899#issuecomment-1317873458,https://api.github.com/repos/simonw/datasette/issues/1899,1317873458,IC_kwDOBm6k_c5OjScy,95570,bgrins,2022-11-17T00:31:07Z,2022-11-17T00:31:07Z,CONTRIBUTOR,"This is one way to fix it ```patch r.html diff --git a/datasette/static/cm-editor-6.0.1.js b/datasette/static/cm-editor-6.0.1.js index c1fd2ab..68cf398 100644 --- a/datasette/static/cm-editor-6.0.1.js +++ b/datasette/static/cm-editor-6.0.1.js @@ -22,7 +22,14 @@ export function editorFromTextArea(textarea, conf = {}) { // https://github.com/codemirror/lang-sql#user-content-sqlconfig.tables let view = new EditorView({ doc: textarea.value, + extensions: [ + EditorView.theme({ + "".cm-content"": { + // Height on cm-content ensures the editor is focusable by clicking beyond the height of the text + minHeight: ""70px"", + }, + }), keymap.of([ { key: ""Shift-Enter"", diff --git a/datasette/templates/_codemirror.html b/datasette/templates/_codemirror.html index dea4710..c4629ae 100644 --- a/datasette/templates/_codemirror.html +++ b/datasette/templates/_codemirror.html @@ -4,7 +4,6 @@ .cm-editor { resize: both; overflow: hidden; - min-height: 70px; width: 80%; border: 1px solid #ddd; } ``` I don't love it but it seems to work for the default case. You can still retrigger the bug by resizing the editor to be > 70px high. The other approach would be to listen for a click on that empty region and move focus to the editor, or something","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1452495049,Clicking within the CodeMirror area below the SQL (i.e. when there's only a single line) doesn't cause the editor to get focused , https://github.com/simonw/datasette/pull/1898#issuecomment-1319642535,https://api.github.com/repos/simonw/datasette/issues/1898,1319642535,IC_kwDOBm6k_c5OqCWn,9599,simonw,2022-11-18T07:28:45Z,2022-11-18T07:28:45Z,OWNER,Thanks!,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1452485922,Use DOMContentLoaded instead of load event for CodeMirror initialization, https://github.com/simonw/datasette/pull/1898#issuecomment-1317870550,https://api.github.com/repos/simonw/datasette/issues/1898,1317870550,IC_kwDOBm6k_c5OjRvW,22429695,codecov[bot],2022-11-17T00:27:55Z,2022-11-17T00:27:55Z,NONE,"# [Codecov](https://codecov.io/gh/simonw/datasette/pull/1898?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) Report Base: **92.55**% // Head: **92.55**% // No change to project coverage :thumbsup: > Coverage data is based on head [(`5eb4ea4`)](https://codecov.io/gh/simonw/datasette/pull/1898?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) compared to base [(`00e233d`)](https://codecov.io/gh/simonw/datasette/commit/00e233d7a7f6443cb95fb5227c23580c48551cad?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). > Patch has no changes to coverable lines.
Additional details and impacted files ```diff @@ Coverage Diff @@ ## main #1898 +/- ## ======================================= Coverage 92.55% 92.55% ======================================= Files 35 35 Lines 4432 4432 ======================================= Hits 4102 4102 Misses 330 330 ``` Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison)
[:umbrella: View full report at Codecov](https://codecov.io/gh/simonw/datasette/pull/1898?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). :loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1452485922,Use DOMContentLoaded instead of load event for CodeMirror initialization, https://github.com/simonw/datasette/issues/1897#issuecomment-1319570586,https://api.github.com/repos/simonw/datasette/issues/1897,1319570586,IC_kwDOBm6k_c5Opwya,9599,simonw,2022-11-18T05:33:20Z,2022-11-18T05:33:20Z,OWNER,"One of the big changes still left to do for Datasette 1.0 is to unify the JSON representation with the context psssed to the templates (via an `?_extra=` mechanism to add extra context needed by the HTML templates), because a goal for 1.0 is for the template context to be a documented API contract such that custom templates won't break with future releases. As such I expect to do quite a bit of refactoring and cleanup on how the template context works later on.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1452457263,Serve schema JSON to the SQL editor to enable autocomplete, https://github.com/simonw/datasette/issues/1897#issuecomment-1319533445,https://api.github.com/repos/simonw/datasette/issues/1897,1319533445,IC_kwDOBm6k_c5OpnuF,95570,bgrins,2022-11-18T04:38:03Z,2022-11-18T04:38:03Z,CONTRIBUTOR,Are you tracking the change to send the JSON over to the frontend separately or was that part of this? Something like this is probably pretty close https://github.com/bgrins/datasette/commit/8431c98850c7a552dbcde2a4dd0c3dc942a97d25#diff-0c93232bfd5477eeac96382e52769108b41433d960d5277ffcccf2f464e60abdR9,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1452457263,Serve schema JSON to the SQL editor to enable autocomplete, https://github.com/simonw/datasette/issues/1897#issuecomment-1319478811,https://api.github.com/repos/simonw/datasette/issues/1897,1319478811,IC_kwDOBm6k_c5OpaYb,9599,simonw,2022-11-18T02:53:57Z,2022-11-18T02:53:57Z,OWNER,"I decided to just go for the view names, not their columns.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1452457263,Serve schema JSON to the SQL editor to enable autocomplete, https://github.com/simonw/datasette/issues/1897#issuecomment-1319477721,https://api.github.com/repos/simonw/datasette/issues/1897,1319477721,IC_kwDOBm6k_c5OpaHZ,9599,simonw,2022-11-18T02:51:40Z,2022-11-18T02:51:40Z,OWNER,Views aren't currently available in the `_internal` schema.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1452457263,Serve schema JSON to the SQL editor to enable autocomplete, https://github.com/simonw/datasette/issues/1897#issuecomment-1319435374,https://api.github.com/repos/simonw/datasette/issues/1897,1319435374,IC_kwDOBm6k_c5OpPxu,9599,simonw,2022-11-18T01:33:30Z,2022-11-18T01:33:30Z,OWNER,"Just noticed that this isn't including views, which it should.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1452457263,Serve schema JSON to the SQL editor to enable autocomplete, https://github.com/simonw/datasette/issues/1897#issuecomment-1319401843,https://api.github.com/repos/simonw/datasette/issues/1897,1319401843,IC_kwDOBm6k_c5OpHlz,9599,simonw,2022-11-18T00:42:03Z,2022-11-18T00:42:23Z,OWNER,"This function works even if the SQLite JSON functions are not available: ```python async def _table_columns(datasette, database_name): internal = datasette.get_database(""_internal"") result = await internal.execute( ""select table_name, name from columns where database_name = ?"", [database_name], ) table_columns = {} for row in result.rows: table_columns.setdefault(row[""table_name""], []).append(row[""name""]) return table_columns ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1452457263,Serve schema JSON to the SQL editor to enable autocomplete, https://github.com/simonw/datasette/issues/1897#issuecomment-1317840727,https://api.github.com/repos/simonw/datasette/issues/1897,1317840727,IC_kwDOBm6k_c5OjKdX,9599,simonw,2022-11-16T23:57:52Z,2022-11-16T23:57:52Z,OWNER,In terms of permissions: if you have `execute-sql` permission for a database then it's OK for you to see the table columns for that database.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1452457263,Serve schema JSON to the SQL editor to enable autocomplete, https://github.com/simonw/datasette/issues/1897#issuecomment-1317839781,https://api.github.com/repos/simonw/datasette/issues/1897,1317839781,IC_kwDOBm6k_c5OjKOl,9599,simonw,2022-11-16T23:56:47Z,2022-11-16T23:56:47Z,OWNER,I'm going to call this `table_columns` in the template context (because `schema` might mean `CREATE TABLE ...`.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1452457263,Serve schema JSON to the SQL editor to enable autocomplete, https://github.com/simonw/datasette/issues/1897#issuecomment-1317838892,https://api.github.com/repos/simonw/datasette/issues/1897,1317838892,IC_kwDOBm6k_c5OjKAs,9599,simonw,2022-11-16T23:55:42Z,2022-11-16T23:55:42Z,OWNER,"Here's where the schema is hard-coded at the moment: https://github.com/simonw/datasette/blob/00e233d7a7f6443cb95fb5227c23580c48551cad/datasette/templates/_codemirror_foot.html#L2-L7 I figured out how to extract that data from the `_internal` table in this comment: https://github.com/simonw/datasette/pull/1893#issuecomment-1317475720 Although that used JSON functions which may (in a real edge-case) not be available in the version of SQLite that Datasette is running on, so probably going to use a regular SQL query and then assemble the JSON separately.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1452457263,Serve schema JSON to the SQL editor to enable autocomplete, https://github.com/simonw/datasette/issues/1896#issuecomment-1320616559,https://api.github.com/repos/simonw/datasette/issues/1896,1320616559,IC_kwDOBm6k_c5OtwJv,9599,simonw,2022-11-18T22:51:14Z,2022-11-18T22:51:14Z,OWNER,New methods are documented here: https://docs.datasette.io/en/1.0-dev/internals.html#resolve-database-request,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1452364777,Extract logic for resolving a URL to a database / table / row, https://github.com/simonw/datasette/issues/1896#issuecomment-1320588299,https://api.github.com/repos/simonw/datasette/issues/1896,1320588299,IC_kwDOBm6k_c5OtpQL,9599,simonw,2022-11-18T22:16:59Z,2022-11-18T22:17:06Z,OWNER,"Found myself needing an `await db.view_exists()` method for this, similar to the existing `await db.table_exists()` one.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1452364777,Extract logic for resolving a URL to a database / table / row, https://github.com/simonw/datasette/issues/1896#issuecomment-1317757949,https://api.github.com/repos/simonw/datasette/issues/1896,1317757949,IC_kwDOBm6k_c5Oi2P9,9599,simonw,2022-11-16T22:27:47Z,2022-11-18T21:48:29Z,OWNER,"Open question: should `resolve_table()` know how to identify named canned queries too? I think not, at least for the moment. Feels a bit too specialist to expose in a documented API.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1452364777,Extract logic for resolving a URL to a database / table / row, https://github.com/simonw/datasette/issues/1896#issuecomment-1317757112,https://api.github.com/repos/simonw/datasette/issues/1896,1317757112,IC_kwDOBm6k_c5Oi2C4,9599,simonw,2022-11-16T22:26:52Z,2022-11-16T22:33:13Z,OWNER,"Some ideas from walking the dog: Challenge: standard URL routing of request to database/table/row Standardize on the named components of the URL patterns - `database`, `table`, `pks` Async function that takes the request and the Datasette instance and returns a Resolved instance with: ``` .level - database or table or row (better name?) .database - the name of the database .db - the database object .table - the name of the table (or view) .is_view perhaps? .pk_values if it's a row ``` Should this attempt to resolve names queries too? ``` .where_sql - the where fragment you use .where_params - accompanying dictionary await datasette.resolve_request(request) ``` Or even better three methods: ```python datasette.resolve_database(request) datasette.resolve_table(request) datasette.resolve_row(request) ``` These can be typed correctly Methods raise `NotFound` if not found","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1452364777,Extract logic for resolving a URL to a database / table / row, https://github.com/simonw/sqlite-utils/issues/512#issuecomment-1316530539,https://api.github.com/repos/simonw/sqlite-utils/issues/512,1316530539,IC_kwDOCGYnMM5OeKlr,9599,simonw,2022-11-16T07:49:50Z,2022-11-16T07:49:50Z,OWNER,Tests passed.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450952393,mypy failures in CI, https://github.com/simonw/sqlite-utils/issues/512#issuecomment-1316447182,https://api.github.com/repos/simonw/sqlite-utils/issues/512,1316447182,IC_kwDOCGYnMM5Od2PO,9599,simonw,2022-11-16T06:32:31Z,2022-11-16T06:32:31Z,OWNER,"Test failed again: https://github.com/simonw/sqlite-utils/actions/runs/3476950474/jobs/5812663096 `E: Failed to fetch http://azure.archive.ubuntu.com/ubuntu/pool/universe/s/spatialite/libsqlite3-mod-spatialite_4.3.0a-6build1_amd64.deb Unable to connect to azure.archive.ubuntu.com:http:` That looks like an intermittent error. I'll try running it again in the morning.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450952393,mypy failures in CI, https://github.com/simonw/sqlite-utils/issues/512#issuecomment-1316437748,https://api.github.com/repos/simonw/sqlite-utils/issues/512,1316437748,IC_kwDOCGYnMM5Odz70,9599,simonw,2022-11-16T06:24:31Z,2022-11-16T06:24:31Z,OWNER,"``` sqlite-utils % pipx run no_implicit_optional . Calculating full-repo metadata... Executing codemod... 11.43s 98% complete, 0.24s estimated for 5 files to go... ``` Then: ``` Finished codemodding 239 files! - Transformed 239 files successfully. - Skipped 0 files. - Failed to codemod 0 files. - 0 warnings were generated. ``` Here's the diff: ```diff diff --git a/sqlite_utils/db.py b/sqlite_utils/db.py index a06f4b7..e819d17 100644 --- a/sqlite_utils/db.py +++ b/sqlite_utils/db.py @@ -297,12 +297,12 @@ class Database: def __init__( self, - filename_or_conn: Union[str, pathlib.Path, sqlite3.Connection] = None, + filename_or_conn: Optional[Union[str, pathlib.Path, sqlite3.Connection]] = None, memory: bool = False, - memory_name: str = None, + memory_name: Optional[str] = None, recreate: bool = False, recursive_triggers: bool = True, - tracer: Callable = None, + tracer: Optional[Callable] = None, use_counts_table: bool = False, ): assert (filename_or_conn is not None and (not memory and not memory_name)) or ( @@ -341,7 +341,7 @@ class Database: self.conn.close() @contextlib.contextmanager - def tracer(self, tracer: Callable = None): + def tracer(self, tracer: Optional[Callable] = None): """""" Context manager to temporarily set a tracer function - all executed SQL queries will be passed to this. @@ -378,7 +378,7 @@ class Database: def register_function( self, - fn: Callable = None, + fn: Optional[Callable] = None, deterministic: bool = False, replace: bool = False, name: Optional[str] = None, @@ -879,7 +879,7 @@ class Database: pk: Optional[Any] = None, foreign_keys: Optional[ForeignKeysType] = None, column_order: Optional[List[str]] = None, - not_null: Iterable[str] = None, + not_null: Optional[Iterable[str]] = None, defaults: Optional[Dict[str, Any]] = None, hash_id: Optional[str] = None, hash_id_columns: Optional[Iterable[str]] = None, @@ -1129,7 +1129,7 @@ class Database: sql += "" [{}]"".format(name) self.execute(sql) - def init_spatialite(self, path: str = None) -> bool: + def init_spatialite(self, path: Optional[str] = None) -> bool: """""" The ``init_spatialite`` method will load and initialize the SpatiaLite extension. The ``path`` argument should be an absolute path to the compiled extension, which @@ -1182,7 +1182,7 @@ class Queryable: def count_where( self, - where: str = None, + where: Optional[str] = None, where_args: Optional[Union[Iterable, dict]] = None, ) -> int: """""" @@ -1213,12 +1213,12 @@ class Queryable: def rows_where( self, - where: str = None, + where: Optional[str] = None, where_args: Optional[Union[Iterable, dict]] = None, - order_by: str = None, + order_by: Optional[str] = None, select: str = ""*"", - limit: int = None, - offset: int = None, + limit: Optional[int] = None, + offset: Optional[int] = None, ) -> Generator[dict, None, None]: """""" Iterate over every row in this table or view that matches the specified where clause. @@ -1251,11 +1251,11 @@ class Queryable: def pks_and_rows_where( self, - where: str = None, + where: Optional[str] = None, where_args: Optional[Union[Iterable, dict]] = None, - order_by: str = None, - limit: int = None, - offset: int = None, + order_by: Optional[str] = None, + limit: Optional[int] = None, + offset: Optional[int] = None, ) -> Generator[Tuple[Any, Dict], None, None]: """""" Like ``.rows_where()`` but returns ``(pk, row)`` pairs - ``pk`` can be a single value or tuple. @@ -1345,7 +1345,7 @@ class Table(Queryable): pk: Optional[Any] = None, foreign_keys: Optional[ForeignKeysType] = None, column_order: Optional[List[str]] = None, - not_null: Iterable[str] = None, + not_null: Optional[Iterable[str]] = None, defaults: Optional[Dict[str, Any]] = None, batch_size: int = 100, hash_id: Optional[str] = None, @@ -1545,7 +1545,7 @@ class Table(Queryable): pk: Optional[Any] = None, foreign_keys: Optional[ForeignKeysType] = None, column_order: Optional[List[str]] = None, - not_null: Iterable[str] = None, + not_null: Optional[Iterable[str]] = None, defaults: Optional[Dict[str, Any]] = None, hash_id: Optional[str] = None, hash_id_columns: Optional[Iterable[str]] = None, @@ -2464,7 +2464,7 @@ class Table(Queryable): columns: Optional[Iterable[str]] = None, limit: Optional[int] = None, offset: Optional[int] = None, - where: str = None, + where: Optional[str] = None, where_args: Optional[Union[Iterable, dict]] = None, quote: bool = False, ) -> Generator[dict, None, None]: @@ -2527,7 +2527,7 @@ class Table(Queryable): def delete_where( self, - where: str = None, + where: Optional[str] = None, where_args: Optional[Union[Iterable, dict]] = None, analyze: bool = False, ) -> ""Table"": ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450952393,mypy failures in CI, https://github.com/simonw/datasette/pull/1893#issuecomment-1317837416,https://api.github.com/repos/simonw/datasette/issues/1893,1317837416,IC_kwDOBm6k_c5OjJpo,9599,simonw,2022-11-16T23:54:02Z,2022-11-16T23:54:02Z,OWNER,"I'm going to tackle #1897 in the next few minutes. Tests failed due to Prettier check, just pushed a fix so it would ignore `.bundle.js` too.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1317834838,https://api.github.com/repos/simonw/datasette/issues/1893,1317834838,IC_kwDOBm6k_c5OjJBW,95570,bgrins,2022-11-16T23:50:58Z,2022-11-16T23:50:58Z,CONTRIBUTOR,"Should we empty out the fixture schema to avoid fixture autocomplete showing up on live databases in the interim, or are you planning to tackle #1897 shortly?","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1317831555,https://api.github.com/repos/simonw/datasette/issues/1893,1317831555,IC_kwDOBm6k_c5OjIOD,9599,simonw,2022-11-16T23:47:13Z,2022-11-16T23:47:13Z,OWNER,I'll open a follow-up issue to fix the schema.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1317831425,https://api.github.com/repos/simonw/datasette/issues/1893,1317831425,IC_kwDOBm6k_c5OjIMB,9599,simonw,2022-11-16T23:47:05Z,2022-11-16T23:47:05Z,OWNER,"OK, let's do it! Thanks so much for this.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1317829214,https://api.github.com/repos/simonw/datasette/issues/1893,1317829214,IC_kwDOBm6k_c5OjHpe,9599,simonw,2022-11-16T23:44:36Z,2022-11-16T23:44:36Z,OWNER,Deployed that to https://datasette-pr-1893.vercel.app/fixtures - looks good to me!,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1317805482,https://api.github.com/repos/simonw/datasette/issues/1893,1317805482,IC_kwDOBm6k_c5OjB2q,95570,bgrins,2022-11-16T23:18:17Z,2022-11-16T23:18:17Z,CONTRIBUTOR,Alright with https://github.com/simonw/datasette/pull/1893/commits/f254be4b38936e95e7a7f25866e7c6b0520db96f we should be getting autocomplete on fixture data. Give that a test and see what you think,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1317797044,https://api.github.com/repos/simonw/datasette/issues/1893,1317797044,IC_kwDOBm6k_c5Oi_y0,9599,simonw,2022-11-16T23:08:34Z,2022-11-16T23:08:34Z,OWNER,"> I can push up a commit that uses the static fixtures schema for testing, but given that the query used to generate it is authed we would still need some work to make that work on live data, right? Yeah, push that up. I'm happy to wire in the query right after we land this.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1317789308,https://api.github.com/repos/simonw/datasette/issues/1893,1317789308,IC_kwDOBm6k_c5Oi958,95570,bgrins,2022-11-16T22:59:57Z,2022-11-16T22:59:57Z,CONTRIBUTOR,"I can push up a commit that uses the static fixtures schema for testing, but given that the query used to generate it is authed we would still need some work to make that work on live data, right? Ideally it could come down to db and query views directly to avoid waiting on an extra xhr and managing that state change.On Nov 16, 2022, at 2:16 PM, Simon Willison ***@***.***> wrote: Honestly I'm not too bothered if table names with weird characters don't work correctly here - I care about those in the Datasette fixtures.db database because Datasette aims to support ANY valid SQLite database, so I need stuff in the test suite that includes weird edge cases like this. But I would hope very few people actually create tables with spaces in their names, so it's not a huge concern to me if autocompletion doesn't work properly for those. —Reply to this email directly, view it on GitHub, or unsubscribe.You are receiving this because you authored the thread.Message ID: ***@***.***>","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1317746206,https://api.github.com/repos/simonw/datasette/issues/1893,1317746206,IC_kwDOBm6k_c5OizYe,9599,simonw,2022-11-16T22:17:24Z,2022-11-16T22:17:24Z,OWNER,Deployed 0a649e8f78c23e8db6869442eeb0dfe36a5443da: https://datasette-pr-1893.vercel.app/fixtures,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1317744563,https://api.github.com/repos/simonw/datasette/issues/1893,1317744563,IC_kwDOBm6k_c5Oiy-z,9599,simonw,2022-11-16T22:16:03Z,2022-11-16T22:16:03Z,OWNER,"Honestly I'm not too bothered if table names with weird characters don't work correctly here - I care about those in the Datasette `fixtures.db` database because Datasette aims to support ANY valid SQLite database, so I need stuff in the test suite that includes weird edge cases like this. But I would hope very few people actually create tables with spaces in their names, so it's not a huge concern to me if autocompletion doesn't work properly for those.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1317715580,https://api.github.com/repos/simonw/datasette/issues/1893,1317715580,IC_kwDOBm6k_c5Oir58,95570,bgrins,2022-11-16T21:49:51Z,2022-11-16T21:49:51Z,CONTRIBUTOR,"I think the table completion still has some quirks to work out. Something like ``` schema: { ""[123_starts_with_digits]"": [""content""], } ``` Seems to work alright, although it will append it after any other numbers you've started typing - so you end up with `select * from 12[123_starts_with_digits]` if you typed ""12"" to get the completion to appear. This might just be an issue with numeric names, I haven't tested it in a lot of detail. You can do ``` searchable: [ { label: ""name with . and spaces"", apply: ""[name with . and spaces]"", }, ""pk"", ""text1"", ""text2"", ], ``` Which is pretty neat and will show the non-escaped string but complete to the escaped one. You can't easily do that with the table names themselves (you can pass a `tables` array like so https://github.com/codemirror/lang-sql/blob/ebf115fffdbe07f91465ccbd82868c587f8182bc/src/sql.ts#L121 but it will overwrite the columns from the schema ). It's buggy enough (bad output for these unusual table names) that I'd suggest that work gets moved into a follow up to the upgrade to 6. That would give space to sort out how to deliver that to the view directly, figure out where name escaping should happen, and have overall testing to uncover bugs and fix papercuts before enabling it.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1317681193,https://api.github.com/repos/simonw/datasette/issues/1893,1317681193,IC_kwDOBm6k_c5Oijgp,95570,bgrins,2022-11-16T21:19:13Z,2022-11-16T21:19:13Z,CONTRIBUTOR,"Alright, added Cmd+Enter to submit (Ctrl+Enter on Windows as well bc of using Meta-Enter on codemirror). We can make that MacOS only by changing the combo to Cmd+Enter specifically but I think it's probably fine to have both.","{""total_count"": 1, ""+1"": 1, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1317522323,https://api.github.com/repos/simonw/datasette/issues/1893,1317522323,IC_kwDOBm6k_c5Oh8uT,95570,bgrins,2022-11-16T18:59:49Z,2022-11-16T18:59:49Z,CONTRIBUTOR,Or I guess you could return only the escaped table name and then we could derive the unescaped from the client side (removing the outer `[]` when present),"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1317520304,https://api.github.com/repos/simonw/datasette/issues/1893,1317520304,IC_kwDOBm6k_c5Oh8Ow,95570,bgrins,2022-11-16T18:58:43Z,2022-11-16T18:58:43Z,CONTRIBUTOR,Nice. And is it possible to include another field which is an escaped table name (only when necessary) - i.e. `[123_starts_with_digits]`. Or is that easy enough to derive on the client? I'm thinking we'd map those to Completion objects so that CM would show the non escaped text but complete to escaped.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1317475720,https://api.github.com/repos/simonw/datasette/issues/1893,1317475720,IC_kwDOBm6k_c5OhxWI,9599,simonw,2022-11-16T18:25:16Z,2022-11-16T18:25:16Z,OWNER,"Here's a query that returns the exact JSON we need to pass to the schema: https://latest.datasette.io/_internal?sql=with+inner+as+%28%0D%0A++select%0D%0A++++table_name%2C%0D%0A++++json_group_array%28name%29+as+table_columns%0D%0A++from%0D%0A++++columns%0D%0A++where%0D%0A++++database_name+%3D+%3Adatabase%0D%0A++group+by%0D%0A++++table_name%0D%0A%29%0D%0Aselect%0D%0A++json_group_object%28table_name%2C+table_columns%29%0D%0Afrom%0D%0A++inner&database=fixtures ```sql with inner as ( select table_name, json_group_array(name) as table_columns from columns where database_name = :database group by table_name ) select json_group_object(table_name, table_columns) from inner ``` Returns (after pretty-printing): ```json { ""123_starts_with_digits"": [ ""content"" ], ""Table With Space In Name"": [ ""content"", ""pk"" ], ""attraction_characteristic"": [ ""name"", ""pk"" ], ""binary_data"": [ ""data"" ], ""complex_foreign_keys"": [ ""f1"", ""f2"", ""f3"", ""pk"" ], ""compound_primary_key"": [ ""content"", ""pk1"", ""pk2"" ], ""compound_three_primary_keys"": [ ""content"", ""pk1"", ""pk2"", ""pk3"" ], ""custom_foreign_key_label"": [ ""foreign_key_with_custom_label"", ""pk"" ], ""facet_cities"": [ ""id"", ""name"" ], ""facetable"": [ ""_city_id"", ""_neighborhood"", ""complex_array"", ""created"", ""distinct_some_null"", ""n"", ""on_earth"", ""pk"", ""planet_int"", ""state"", ""tags"" ], ""foreign_key_references"": [ ""foreign_key_compound_pk1"", ""foreign_key_compound_pk2"", ""foreign_key_with_blank_label"", ""foreign_key_with_label"", ""foreign_key_with_no_label"", ""pk"" ], ""infinity"": [ ""value"" ], ""no_primary_key"": [ ""a"", ""b"", ""c"", ""content"" ], ""primary_key_multiple_columns"": [ ""content"", ""content2"", ""id"" ], ""primary_key_multiple_columns_explicit_label"": [ ""content"", ""content2"", ""id"" ], ""roadside_attraction_characteristics"": [ ""attraction_id"", ""characteristic_id"" ], ""roadside_attractions"": [ ""address"", ""latitude"", ""longitude"", ""name"", ""pk"", ""url"" ], ""searchable"": [ ""name with . and spaces"", ""pk"", ""text1"", ""text2"" ], ""searchable_fts"": [ ""__langid"", ""docid"", ""name with . and spaces"", ""searchable_fts"", ""text1"", ""text2"" ], ""searchable_fts_docsize"": [ ""docid"", ""size"" ], ""searchable_fts_segdir"": [ ""end_block"", ""idx"", ""leaves_end_block"", ""level"", ""root"", ""start_block"" ], ""searchable_fts_segments"": [ ""block"", ""blockid"" ], ""searchable_fts_stat"": [ ""id"", ""value"" ], ""searchable_tags"": [ ""searchable_id"", ""tag"" ], ""select"": [ ""and"", ""group"", ""having"", ""json"" ], ""simple_primary_key"": [ ""content"", ""id"" ], ""sortable"": [ ""content"", ""pk1"", ""pk2"", ""sortable"", ""sortable_with_nulls"", ""sortable_with_nulls_2"", ""text"" ], ""table/with/slashes.csv"": [ ""content"", ""pk"" ], ""tags"": [ ""tag"" ], ""units"": [ ""distance"", ""frequency"", ""pk"" ] } ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1317465874,https://api.github.com/repos/simonw/datasette/issues/1893,1317465874,IC_kwDOBm6k_c5Ohu8S,9599,simonw,2022-11-16T18:21:17Z,2022-11-16T18:21:54Z,OWNER,"I was worrying about the server-side overhead of collecting together all of the tables and column names for databases that might have hundreds of tables... but then I remember that I built the `_internal` table precisely for this kind of thing - so gathering all of that data should still only be a single SQL query against an in-memory database. https://latest.datasette.io/login-as-root and then visit this page for an example query: https://latest.datasette.io/_internal?sql=select%0D%0A++database_name%2C%0D%0A++table_name%2C%0D%0A++json_group_array%28name%29%0D%0Afrom%0D%0A++columns%0D%0Awhere%0D%0A++database_name+%21%3D+%27_internal%27%0D%0Agroup+by%0D%0A++database_name%2C%0D%0A++table_name ```sql select database_name, table_name, json_group_array(name) from columns where database_name != '_internal' group by database_name, table_name ``` database_name | table_name | json_group_array(name) -- | -- | -- extra_database | searchable | [""pk"",""text1"",""text2""] extra_database | searchable_fts | [""__langid"",""content"",""docid"",""searchable_fts"",""text1"",""text2""] extra_database | searchable_fts_content | [""c0text1"",""c1text2"",""c2content"",""docid""] extra_database | searchable_fts_segdir | [""end_block"",""idx"",""leaves_end_block"",""level"",""root"",""start_block""] extra_database | searchable_fts_segments | [""block"",""blockid""] fixtures | 123_starts_with_digits | [""content""] fixtures | Table With Space In Name | [""content"",""pk""] fixtures | attraction_characteristic | [""name"",""pk""] fixtures | binary_data | [""data""] fixtures | complex_foreign_keys | [""f1"",""f2"",""f3"",""pk""] fixtures | compound_primary_key | [""content"",""pk1"",""pk2""] ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1317456909,https://api.github.com/repos/simonw/datasette/issues/1893,1317456909,IC_kwDOBm6k_c5OhswN,9599,simonw,2022-11-16T18:17:39Z,2022-11-16T18:17:39Z,OWNER,"Tiny feature request (since you're in this code already) - I keep hitting Command+Enter on my macOS keyboard to submit the query, but the correct shortcut is Shift+Enter. Would be great if both worked!","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1317452541,https://api.github.com/repos/simonw/datasette/issues/1893,1317452541,IC_kwDOBm6k_c5Ohrr9,9599,simonw,2022-11-16T18:15:52Z,2022-11-16T18:15:52Z,OWNER,"Deployed latest copy with: ``` datasette publish vercel fixtures.db \ --project datasette-pr-1893 \ --about 'PR 1893' \ --about_url https://github.com/simonw/datasette/pull/1893 \ --scope datasette \ --branch eccb1c6c781d69d8ec3c542ef65c78a4a0927a7c ``` https://datasette-pr-1893.vercel.app/fixtures ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1317449610,https://api.github.com/repos/simonw/datasette/issues/1893,1317449610,IC_kwDOBm6k_c5Ohq-K,9599,simonw,2022-11-16T18:14:28Z,2022-11-16T18:14:28Z,OWNER,"> I'm thinking of also adding `count` to the list since that's a common thing people would want to autocomplete. I notice BQ console highlights `count` in the same manner as other keywords like `select` as well. Huh, yeah we should definitely have `count` - surprised it's not on the list on https://www.sqlite.org/lang_keywords.html which is why we didn't get it from the GPT-3 generated schema.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1317329157,https://api.github.com/repos/simonw/datasette/issues/1893,1317329157,IC_kwDOBm6k_c5OhNkF,95570,bgrins,2022-11-16T16:46:52Z,2022-11-16T16:46:52Z,CONTRIBUTOR,"> > > UI issue I see on the autocomplete popup with overlapping icon & text. Screenshot's from Firefox, it seems even a little more pronounced on Safari I checked and if I empty out app.css the bug goes away, so there's some kind of inheritance issue there. It's hard to debug bc the autocomplete popup goes away on blur (i.e. when trying to inspect it in devtools), but at least it's narrowed down a bit.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1317326406,https://api.github.com/repos/simonw/datasette/issues/1893,1317326406,IC_kwDOBm6k_c5OhM5G,95570,bgrins,2022-11-16T16:45:09Z,2022-11-16T16:45:09Z,CONTRIBUTOR,"For escaped table names it looks like we could pass a Completion object (https://codemirror.net/docs/ref/#autocomplete) instead of a string which would allow the non escaped name to be a label and then the escaped name to actually complete in the editor, which might help with some of the funkiness I was seeing w/ completion","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1317314064,https://api.github.com/repos/simonw/datasette/issues/1893,1317314064,IC_kwDOBm6k_c5OhJ4Q,95570,bgrins,2022-11-16T16:36:46Z,2022-11-16T16:36:46Z,CONTRIBUTOR,"With ```patch diff --git a/datasette/templates/_codemirror_foot.html b/datasette/templates/_codemirror_foot.html index ed709b3..74fe18e 100644 --- a/datasette/templates/_codemirror_foot.html +++ b/datasette/templates/_codemirror_foot.html @@ -7,7 +7,11 @@ sqlFormat.hidden = false; } if (sqlInput) { - var editor = (window.editor = cm.editorFromTextArea(sqlInput)); + var editor = (window.editor = cm.editorFromTextArea(sqlInput, { + schema: { + compound_three_primary_keys: [""pk1"", ""pk2"", ""pk3"", ""content""], + }, + })); ``` we get table autocompletion and column completion if you name the table in the query (see screencast). I do see bugs with escaped table names like `""'123_starts_with_digits'"": [""col1"", ""col2""]` or `""[123_starts_with_digits]"": [""col1"", ""col2""]` where it doesn't seem to pick up the column names though. I think it needs some further testing and debugging. https://user-images.githubusercontent.com/95570/202238521-e613b4e2-ba92-4418-9068-fc022edaee93.mp4 ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1317281292,https://api.github.com/repos/simonw/datasette/issues/1893,1317281292,IC_kwDOBm6k_c5OhB4M,95570,bgrins,2022-11-16T16:19:16Z,2022-11-16T16:19:16Z,CONTRIBUTOR,"Ha, nice idea! Updating the dialect with that list. I'm thinking of also adding `count` to the list since that's a common thing people would want to autocomplete. I notice BQ console highlights `count` in the same manner as other keywords like `select` as well.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1316412234,https://api.github.com/repos/simonw/datasette/issues/1893,1316412234,IC_kwDOBm6k_c5OdttK,9599,simonw,2022-11-16T06:00:39Z,2022-11-16T06:01:36Z,OWNER,"Should note though that this is a classic example of GPT-3 making stuff up in places. > current: Returns the current date, time, or timestamp `select current` throws an error for me: https://latest.datasette.io/_memory?sql=select+current `select current_date, current_time, current_timestamp` works though: https://latest.datasette.io/_memory?sql=select+current_date%2C+current_time%2C+current_timestamp So let's drop `current` from the list. I'm OK with it though, I think it's likely good enough for the first attempt at this. We should drop `temp` and `temporary` too.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1316401895,https://api.github.com/repos/simonw/datasette/issues/1893,1316401895,IC_kwDOBm6k_c5OdrLn,9599,simonw,2022-11-16T05:50:40Z,2022-11-16T05:50:40Z,OWNER,"So I think our dialect (at least to start with) should be: ``` keywords: ""and as asc between by case cast current current_date current_time current_timestamp desc distinct each else escape except exists explain filter first for from full generated group having if in index inner intersect into isnull join last left like limit not null or order outer over pragma primary query raise range regexp right rollback row select set table temp temporary then to union unique using values view virtual when where"", // https://www.sqlite.org/datatype3.html types: ""null integer real text blob"", builtin: """" ``` I left `builtin` blank here because I don't think we need any of those things at all.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1316400688,https://api.github.com/repos/simonw/datasette/issues/1893,1316400688,IC_kwDOBm6k_c5Odq4w,9599,simonw,2022-11-16T05:49:31Z,2022-11-16T05:49:31Z,OWNER,"OK, I decided to play around with GPT-3 for this. https://beta.openai.com/playground I prompted: ``` For each keyword say it it can be used in a SQLite select statement: and as asc between by case cast cross current current_date current_time current_timestamp desc detach distinct each else end escape except exclude exclusive exists explain fail filter first following for foreign from full generated glob group groups having if ignore immediate in index indexed initially inner insert instead intersect into is isnull join key last left like limit match materialized natural no not nothing notnull null nulls of offset on or order others outer over partition plan pragma preceding primary query raise range recursive references regexp reindex release rename replace restrict returning right rollback row rows savepoint select set table temp temporary then ties to transaction trigger unbounded union unique update using vacuum values view virtual when where window with without - and: yes - as: yes - asc: yes - between: yes - by: yes - case: yes - cast: yes - current: yes - current_date: yes - current_time: yes - current_timestamp: yes - desc: yes - detach: no - distinct: yes - each: yes - else: yes - end: yes - escape: yes - except: no - exclude: yes - exclusive: no ``` Having given it these examples I ran it a few times: ``` - exists: yes - explain: no - fail: no - filter: yes - first: yes - following: no - for: yes - foreign: no - from: yes - full: no - generated: no - glob: yes - group: yes - groups: no - having: yes - if: yes - ignore: no - immediate: no - in: yes - index: yes - indexed: no - initially: no - inner: no - insert: no - instead: no - intersect: no - into: yes - is: yes - isnull: yes - join: yes - key: no - last: yes - left: yes - like: yes - limit: yes - match: no - materialized: no -natural: yes - no: no - not: no - nothing: no - notnull: yes - null: yes - nulls: yes - of: no - offset: yes - on: yes - or: yes - order: yes - others: yes - outer: no - over: no - partition: no - pragma: no - preceding: no - primary: no - query: no - raise: no - range: no - recursive: no - references: no - regexp: yes - reindex: no - release: no - rename: no - replace: no - restrict: no - returning: yes - right: yes - rollback: no - row: yes - rows: no - savepoint: no - select: yes - set: no - table: yes - temp: yes - temporary: yes - then: yes - ties: no - to: yes - transaction: no - trigger: no - unbounded: no - union: yes - unique: yes - update: no - using: yes - vacuum: no - values: yes - view: yes - virtual: no - when: yes - where: yes - window: no - with: yes - without: no ``` I added this: ``` A space separated list of all of the keywords for which the answer was yes: ``` It replied: ``` and as asc between by case cast current current_date current_time current_timestamp desc distinct each else escape except exists explain filter first for from full generated group having if in index inner intersect into isnull join last left like limit not null or order outer over pragma primary query raise range regexp right rollback row select set table temp temporary then to union unique using values view virtual when where ``` Then I tried doing this: ``` Produce a short summary of how each of those are used: - and: ``` This kind of worked too! It ended up defining every keyword, not just the ones for which the ""is it used in select"" was ""yes"", but it gave me this list: > - and: Used to combine multiple conditions in a WHERE clause > - as: Rename a column or table in the output of a query > - asc: Sort results in ascending order > - between: Used in a WHERE clause to specify a range > - by: Specifies how to sort the results of a query > - case: Used to create different outputs based on conditions > - cast: Converts a value from one data type to another > - current: Returns the current date, time, or timestamp > - current_date: Returns the current date > - current_time: Returns the current time > - current_timestamp: Returns the current timestamp > - desc: Sort results in descending order > - distinct: Selects only unique values > - each: Used with aggregate functions > - else: Used with the CASE statement > - escape: Used to escape special characters in a string > - except: Returns all rows from the first query that are not in the second query > - exists: Used with subqueries > - explain: Analyzes a query and returns information about it > - fail: Used with the ON CONFLICT clause > - filter: Used to specify conditions for aggregate functions > - first: Used to specify conditions for aggregate functions > - following: Used with window functions > - for: Used to specify conditions for aggregate functions > - foreign: Creates a foreign key > - from: Specifies the table(s) to select data from > - full: Used with outer joins > - generated: Used with generated columns > - glob: Used to match a string with a pattern > - group: Groups together rows that have the same values > - groups: Used with aggregate functions > - having: Specifies conditions for groups > - if: Used with the ON CONFLICT clause > - ignore: Used with the ON CONFLICT clause > - immediate: Used with the ON CONFLICT clause > - in: Used to check if a value is in a list > - index: Creates an index on a column or columns > - indexed: Used with the ON CONFLICT clause > - initially: Used with the ON CONFLICT clause > - inner: Creates an inner join > - insert: Inserts new data into a table > - instead: Used with triggers > - intersect: Returns all rows that are in both queries > - into: Specifies the table to insert data into > - is: Used to compare values > - isnull: Checks if a value is null > - join: Joins two or more tables together > - key: Creates a primary key or unique key > - last: Used to specify conditions for aggregate functions > - left: Creates a left join > - like: Checks if a value matches a pattern > - limit: Specifies the maximum number of rows to return > - not: Negates a condition > - null: Specifies a null value > - nulls: Used with the ORDER BY clause > - of: Used with window functions > - offset: Specifies the number of rows to skip > - on: Specifies conditions for a join > - or: Checks if either condition is true > - order: Specifies how to sort the results of a query > - others: Used with the CASE statement > - outer: Creates an outer join > - over: Used with window functions > - partition: Used with window functions > - pragma: Used to query or change various internal settings > - preceding: Used with window functions > - primary: Creates a primary key > - query: Used with the ON CONFLICT clause > - raise: Used with the ON CONFLICT clause > - range: Used with window functions > - recursive: Used with common table expressions > - references: Creates a foreign key > - regexp: Checks if a value matches a regular expression > - reindex: Recreates an index > - release: Used with savepoints > - rename: Changes the name of a table or column > - replace: Inserts new data into a table or updates existing data > - restrict: Used with savepoints > - returning: Returns data from inserted or updated rows > - right: Creates a right join > - rollback: Rolls back a transaction > - row: Used with aggregate functions > - select: Selects data from a table > - set: Changes the value of a column > - table: Specifies the table(s) to select data from > - temp: Creates a temporary table > - temporary: Creates a temporary table > - then: Used with the CASE statement > - ties: Used with the ORDER BY clause > - to: Used with common table expressions > - transaction: Starts a transaction > - trigger: Creates a trigger > - unbounded: Used with window functions > - union: Returns all rows from both queries > - unique: Creates a unique constraint > - update: Updates existing data in a table > - using: Specifies the table(s) to join > - vacuum: Optimizes a database > - values: Specifies the values to insert into a table > - view: Creates a view > - virtual: Creates a virtual table > - when: Used with the CASE statement > - where: Specifies conditions for a query > - window: Used with window functions > - with: Specifies common table expressions used in a query > - without: Used with indexes","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1316387382,https://api.github.com/repos/simonw/datasette/issues/1893,1316387382,IC_kwDOBm6k_c5Odno2,95570,bgrins,2022-11-16T05:33:55Z,2022-11-16T05:33:55Z,CONTRIBUTOR,I added a commit to make our own dialect at https://github.com/simonw/datasette/pull/1893/commits/e273fc8ed5341bdf0b622e722d761bd2acc30a90. Pulled in the full list of keywords from https://www.sqlite.org/lang_keywords.html but haven't gone through and pruned it to only include common select keywords. @simonw you'll have better knowledge than me on that - do you want to take a first shot at narrowing that down to the set that people will be using in the editor?,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1316340865,https://api.github.com/repos/simonw/datasette/issues/1893,1316340865,IC_kwDOBm6k_c5OdcSB,9599,simonw,2022-11-16T04:49:30Z,2022-11-16T04:49:43Z,OWNER,"> The main issue is that we don't pass the relevant table data down to QueryView. If you can come up with a static example JSON data structure example that does the right thing, I'm happy to refactor QueryView to make that available to the template - or even have a separate `fetch()` that grabs just the data needed for the autocomplete as a separate hit when the page loads (whichever has better performance implications). I'm working a fair amount in the view classes at the moment so adding this to that work would make sense. ","{""total_count"": 1, ""+1"": 1, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1316339035,https://api.github.com/repos/simonw/datasette/issues/1893,1316339035,IC_kwDOBm6k_c5Odb1b,95570,bgrins,2022-11-16T04:47:11Z,2022-11-16T04:47:11Z,CONTRIBUTOR,"> Have you ever seen CodeMirror correctly auto-completing columns? I'm not entirely sure I believe that the feature works anywhere else. I was thinking of the BigQuery console, like But they must be doing something pretty custom & appears to be using Monaco anyway. I suspect some kind of lower level autocomplete integration could make this work, but if the table completion is a good-enough starting point I think it's not too hard. The main issue is that we don't pass the relevant table data down to QueryView.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1316320521,https://api.github.com/repos/simonw/datasette/issues/1893,1316320521,IC_kwDOBm6k_c5OdXUJ,95570,bgrins,2022-11-16T04:29:23Z,2022-11-16T04:29:23Z,CONTRIBUTOR," UI issue I see on the autocomplete popup with overlapping icon & text. Screenshot's from Firefox, it seems even a little more pronounced on Safari","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1316318961,https://api.github.com/repos/simonw/datasette/issues/1893,1316318961,IC_kwDOBm6k_c5OdW7x,95570,bgrins,2022-11-16T04:27:51Z,2022-11-16T04:27:51Z,CONTRIBUTOR,"> The resize handle doesn't appear on Mobile Safari on iPhone - I don't think that particularly matters though. > > The textarea does get a weird border around it when focused on iPhone though. The default focus styles appear to be ``` .c1.cm-editor.cm-focused { outline: 1px dotted #212121; } ``` Which I also see on desktop. Would be nice to changed to whatever the default UA textarea styles are to blend in better but I wouldn't recommend removing it entirely - just to keep the visual indication that the element is focused. Maybe followup material to have a theming pass","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1316297347,https://api.github.com/repos/simonw/datasette/issues/1893,1316297347,IC_kwDOBm6k_c5OdRqD,22429695,codecov[bot],2022-11-16T04:05:12Z,2022-11-16T23:27:45Z,NONE,"# [Codecov](https://codecov.io/gh/simonw/datasette/pull/1893?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) Report Base: **92.55**% // Head: **92.55**% // No change to project coverage :thumbsup: > Coverage data is based on head [(`f254be4`)](https://codecov.io/gh/simonw/datasette/pull/1893?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) compared to base [(`6f610e1`)](https://codecov.io/gh/simonw/datasette/commit/6f610e1d94b7b8ec605b5b7fcb01537f6adf9c5b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). > Patch has no changes to coverable lines.
Additional details and impacted files ```diff @@ Coverage Diff @@ ## main #1893 +/- ## ======================================= Coverage 92.55% 92.55% ======================================= Files 35 35 Lines 4432 4432 ======================================= Hits 4102 4102 Misses 330 330 ``` Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison)
[:umbrella: View full report at Codecov](https://codecov.io/gh/simonw/datasette/pull/1893?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). :loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1316294156,https://api.github.com/repos/simonw/datasette/issues/1893,1316294156,IC_kwDOBm6k_c5OdQ4M,9599,simonw,2022-11-16T04:00:12Z,2022-11-16T04:00:12Z,OWNER,Have you ever seen CodeMirror correctly auto-completing columns? I'm not entirely sure I believe that the feature works anywhere else.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1316293353,https://api.github.com/repos/simonw/datasette/issues/1893,1316293353,IC_kwDOBm6k_c5OdQrp,9599,simonw,2022-11-16T03:59:03Z,2022-11-16T03:59:03Z,OWNER,"Deployed a fresh copy: ``` datasette publish vercel fixtures.db \ --branch b7b2942b13f9ea09cfa9f8c73e2869b9bd2349ae \ --project datasette-pr-1893 \ --about 'PR 1893' \ --about_url https://github.com/simonw/datasette/pull/1893 \ --scope datasette ``` https://datasette-pr-1893.vercel.app/fixtures","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1316256386,https://api.github.com/repos/simonw/datasette/issues/1893,1316256386,IC_kwDOBm6k_c5OdHqC,95570,bgrins,2022-11-16T03:18:06Z,2022-11-16T03:18:06Z,CONTRIBUTOR,"> If you can get a version of this working with table and column autocompletion just using a static JavaScript object in the source code with the right tables and columns, I'm happy to take on the work of turning that static object into something that Datasette includes in the page itself with all of the correct values. This version ""sort of"" works when on the main database page where the template passes the relevant data https://github.com/bgrins/datasette/commit/8431c98850c7a552dbcde2a4dd0c3dc942a97d25 by doing this and passing that into the `schema` object: ``` let TABLES_DATA = []; {% if tables is defined %} TABLES_DATA = {{ tables | tojson(indent=2) }}; {% endif %} // Turn into an object, shaped like https://github.com/codemirror/lang-sql/blob/ebf115fffdbe07f91465ccbd82868c587f8182bc/test/test-complete.ts#L27. const TABLES_SCHEMA = Object.fromEntries( new Map( TABLES_DATA.map((table) => { return [table.name, table.columns]; }) ).entries() ); ``` But there are a number of papercuts with it - it's not escaping table names with spaces (likely be fixable from the data being passed into the view) but mainly it doesn't seem to autocomplete columns. I think it might only want to do it when you first type the table name from my read of https://github.com/codemirror/lang-sql/blob/ebf115fffdbe07f91465ccbd82868c587f8182bc/test/test-complete.ts#L37. It's possible I'm just passing something wrong, but it may end up being something that needs feature work upstream. ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1316253186,https://api.github.com/repos/simonw/datasette/issues/1893,1316253186,IC_kwDOBm6k_c5OdG4C,9599,simonw,2022-11-16T03:16:36Z,2022-11-16T03:16:36Z,OWNER,Yeah I haven't written this down anywhere but Datasette definitely has an undocumented preference for lower-case SQL.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1316243602,https://api.github.com/repos/simonw/datasette/issues/1893,1316243602,IC_kwDOBm6k_c5OdEiS,95570,bgrins,2022-11-16T03:11:46Z,2022-11-16T03:11:46Z,CONTRIBUTOR,Was just reviewing the SQL options and there's an [upperCaseKeywords](https://github.com/codemirror/lang-sql#user-content-sqlconfig.uppercasekeywords) if we'd rather have SELECT vs select. Datasette seems to prefer lowercase so probably best to keep it as-is,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1316236448,https://api.github.com/repos/simonw/datasette/issues/1893,1316236448,IC_kwDOBm6k_c5OdCyg,9599,simonw,2022-11-16T03:04:57Z,2022-11-16T03:04:57Z,OWNER,If you rebase from `main` you should get the fix for that test failure.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1316232588,https://api.github.com/repos/simonw/datasette/issues/1893,1316232588,IC_kwDOBm6k_c5OdB2M,9599,simonw,2022-11-16T03:00:04Z,2022-11-16T03:00:04Z,OWNER,"Oops, the tests are failing because of a test failure I introduced here: - #1890","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1316231560,https://api.github.com/repos/simonw/datasette/issues/1893,1316231560,IC_kwDOBm6k_c5OdBmI,9599,simonw,2022-11-16T02:59:00Z,2022-11-16T02:59:00Z,OWNER,"The resize handle doesn't appear on Mobile Safari on iPhone - I don't think that particularly matters though. The textarea does get a weird border around it when focused on iPhone though. Focused: ![BF34E8FB-E35C-4CAB-9BFB-8EEF7E29B16C_1_201_a](https://user-images.githubusercontent.com/9599/202072748-c85bab94-a039-4ed6-8185-3cac25c78ed3.jpeg) Not focused: ![31A5CF38-D540-4A1A-8A7D-E29453D150F4_1_201_a](https://user-images.githubusercontent.com/9599/202072744-d9f0ea62-13b7-46ff-afe1-6d88d7fb8b53.jpeg) ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1316227073,https://api.github.com/repos/simonw/datasette/issues/1893,1316227073,IC_kwDOBm6k_c5OdAgB,9599,simonw,2022-11-16T02:54:22Z,2022-11-16T02:54:32Z,OWNER,"If you can get a version of this working with table and column autocompletion just using a static JavaScript object in the source code with the right tables and columns, I'm happy to take on the work of turning that static object into something that Datasette includes in the page itself with all of the correct values.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1316141764,https://api.github.com/repos/simonw/datasette/issues/1893,1316141764,IC_kwDOBm6k_c5OcrrE,9599,simonw,2022-11-16T01:26:59Z,2022-11-16T01:26:59Z,OWNER,"Resizing works great for me - and the page automatically sizes the editor to fit an existing query, e.g. on https://datasette-pr-1893.vercel.app/fixtures?sql=select+id%2C+content%2C+content2%0D%0A++from+primary_key_multiple_columns_explicit_label%0D%0A++order+by+id%0D%0A++limit+101","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1316137982,https://api.github.com/repos/simonw/datasette/issues/1893,1316137982,IC_kwDOBm6k_c5Ocqv-,9599,simonw,2022-11-16T01:23:47Z,2022-11-16T01:23:47Z,OWNER,"Autocomplete here looks promising (I've wanted that to work for years!), but it does currently show a whole bunch of suggestions which aren't part of the SQLite SQL dialect: ![autocomplete](https://user-images.githubusercontent.com/9599/202060211-51ec9f45-bc52-459a-a729-27fc2faadff9.gif) ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1316135244,https://api.github.com/repos/simonw/datasette/issues/1893,1316135244,IC_kwDOBm6k_c5OcqFM,9599,simonw,2022-11-16T01:21:41Z,2022-11-16T01:21:41Z,OWNER,"I just deployed a demo instance like this (using the commit hash from this PR): ```bash datasette publish vercel fixtures.db \ --branch 544f7025900b78f63c34b9985522271ba5fd9c0f \ --project datasette-pr-1893 \ --scope datasette \ --about 'PR 1893' \ --about_url https://github.com/simonw/datasette/pull/1893 ``` Here's the result: https://datasette-pr-1893.vercel.app/fixtures","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1316041828,https://api.github.com/repos/simonw/datasette/issues/1893,1316041828,IC_kwDOBm6k_c5OcTRk,95570,bgrins,2022-11-15T23:51:35Z,2022-11-15T23:51:35Z,CONTRIBUTOR,"I experimented with autocompleting the actual schema in https://github.com/bgrins/datasette/commit/8431c98850c7a552dbcde2a4dd0c3dc942a97d25, but it would need some work (current problems with it listed in the commit message there)","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1315869946,https://api.github.com/repos/simonw/datasette/issues/1893,1315869946,IC_kwDOBm6k_c5ObpT6,95570,bgrins,2022-11-15T21:12:38Z,2022-11-15T21:12:38Z,CONTRIBUTOR,https://github.com/Sphinxxxx/cm-resize isn't compatible with 6. There's a suggestion to try using CSS resize in https://discuss.codemirror.net/t/resizing-codemirror-6/3265/2,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1315869040,https://api.github.com/repos/simonw/datasette/issues/1893,1315869040,IC_kwDOBm6k_c5ObpFw,95570,bgrins,2022-11-15T21:11:42Z,2022-11-15T21:11:42Z,CONTRIBUTOR,"extraKeys is done - Shift+Enter is added in the helper function, and it appears that the Tab behavior now defaults to what the `Tab: false` setting was doing (allowing it to escape to the form)","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/pull/1893#issuecomment-1315853097,https://api.github.com/repos/simonw/datasette/issues/1893,1315853097,IC_kwDOBm6k_c5OblMp,95570,bgrins,2022-11-15T20:55:40Z,2022-11-15T20:55:40Z,CONTRIBUTOR,Should also minify the bundled output,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450363982,"Upgrade to CodeMirror 6, add SQL autocomplete", https://github.com/simonw/datasette/issues/1892#issuecomment-1315814786,https://api.github.com/repos/simonw/datasette/issues/1892,1315814786,IC_kwDOBm6k_c5Obb2C,4399499,ocdtrekkie,2022-11-15T20:14:38Z,2022-11-15T20:14:38Z,NONE,"I have no particular point, I just want to say being around for the 1.0 release of Datasette seems historic and legendary to witness.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450312343,Merge 1.0-dev branch back to main, https://github.com/simonw/datasette/issues/1892#issuecomment-1315805498,https://api.github.com/repos/simonw/datasette/issues/1892,1315805498,IC_kwDOBm6k_c5ObZk6,9599,simonw,2022-11-15T20:05:30Z,2022-11-15T20:05:30Z,OWNER,"One slight concern: https://latest.datasette.io/ will increasingly reflect a version that isn't the most recent production release. I might setup https://stable.datasette.io/ as a demo instance of the most recent non-alpha release to compensate for that.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450312343,Merge 1.0-dev branch back to main, https://github.com/simonw/datasette/issues/1892#issuecomment-1315804535,https://api.github.com/repos/simonw/datasette/issues/1892,1315804535,IC_kwDOBm6k_c5ObZV3,9599,simonw,2022-11-15T20:04:38Z,2022-11-15T20:04:38Z,OWNER,"I'll do this after the 1.0a0 release: - #1708","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450312343,Merge 1.0-dev branch back to main, https://github.com/simonw/datasette/issues/1891#issuecomment-1331181922,https://api.github.com/repos/simonw/datasette/issues/1891,1331181922,IC_kwDOBm6k_c5PWDli,9599,simonw,2022-11-29T19:23:41Z,2022-11-29T19:23:41Z,OWNER,https://github.com/simonw/datasette/blob/4d49a5a39739476e1ada43f70a0029abcef07977/docs/changelog.rst#10a0-2022-11-29,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450303205,1.0a0 release notes, https://github.com/simonw/datasette/issues/1891#issuecomment-1331143292,https://api.github.com/repos/simonw/datasette/issues/1891,1331143292,IC_kwDOBm6k_c5PV6J8,9599,simonw,2022-11-29T18:57:40Z,2022-11-29T18:57:40Z,OWNER,I'm going to keep these short - they'll mostly be links to the documentation for the new features.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450303205,1.0a0 release notes, https://github.com/simonw/datasette/issues/1891#issuecomment-1331140747,https://api.github.com/repos/simonw/datasette/issues/1891,1331140747,IC_kwDOBm6k_c5PV5iL,9599,simonw,2022-11-29T18:55:42Z,2022-11-29T18:55:42Z,OWNER,"All features for the alpha are complete now. Release notes should be based on these commits: https://github.com/simonw/datasette/compare/0.63.2...6bda2257868a2cbd70b84b7a86a5bcb47dcc4874","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450303205,1.0a0 release notes, https://github.com/simonw/datasette/issues/1891#issuecomment-1320625260,https://api.github.com/repos/simonw/datasette/issues/1891,1320625260,IC_kwDOBm6k_c5OtyRs,9599,simonw,2022-11-18T23:01:03Z,2022-11-18T23:01:48Z,OWNER,I think this actually needs to include a whole section of the documentation about the road to 1.0 - what to expect (planned breaking changes) etc. I can add that to the https://docs.datasette.io/en/stable/contributing.html page perhaps - or even create a Roadmap page.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1450303205,1.0a0 release notes, https://github.com/simonw/datasette/issues/1890#issuecomment-1317889323,https://api.github.com/repos/simonw/datasette/issues/1890,1317889323,IC_kwDOBm6k_c5OjWUr,536941,fgregg,2022-11-17T00:47:36Z,2022-11-17T00:47:36Z,CONTRIBUTOR,amazing! thanks @simonw ,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1448143294,Autocomplete text entry for filter values that correspond to facets, https://github.com/simonw/datasette/issues/1890#issuecomment-1316262169,https://api.github.com/repos/simonw/datasette/issues/1890,1316262169,IC_kwDOBm6k_c5OdJEZ,9599,simonw,2022-11-16T03:22:40Z,2022-11-16T03:22:40Z,OWNER,"Actually this works as it should in desktop Safari: ![autocomplete-safari](https://user-images.githubusercontent.com/9599/202075764-fbc4b4c8-c92f-4f69-81fd-84002de5aea7.gif) I'm going to just put up with the weird behaviour in Mobile Safari.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1448143294,Autocomplete text entry for filter values that correspond to facets, https://github.com/simonw/datasette/issues/1890#issuecomment-1316242752,https://api.github.com/repos/simonw/datasette/issues/1890,1316242752,IC_kwDOBm6k_c5OdEVA,9599,simonw,2022-11-16T03:10:52Z,2022-11-16T03:12:47Z,OWNER,"https://bugs.webkit.org/show_bug.cgi?id=201768 - "" Datalist option's label not used"" - marked as RESOLVED FIXED on March 31st 2020. The commit: https://trac.webkit.org/changeset/259330/webkit And here's the test mirrored on GitHub: https://cs.github.com/qtwebkit/webkit-mirror/blob/cc3fcd0b4bad1f7cf77c26e34aa01d16618d6d5e/LayoutTests/fast/forms/datalist/datalist-option-labels.html?q=datalist-option-labels.html","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1448143294,Autocomplete text entry for filter values that correspond to facets, https://github.com/simonw/datasette/issues/1890#issuecomment-1316240839,https://api.github.com/repos/simonw/datasette/issues/1890,1316240839,IC_kwDOBm6k_c5OdD3H,9599,simonw,2022-11-16T03:09:11Z,2022-11-16T03:09:11Z,OWNER,"Here's a polyfill for ``: https://github.com/mfranzke/datalist-polyfill It shouldn't be necessary now that Safari has shipped support (apparently added in https://developer.apple.com/documentation/safari-release-notes/safari-12_1-release-notes#3130314 Safari 12.1 in March 2019). But it does look like Safari doesn't support differing `label` and `value` attributes, though documentation about this is hard to come by.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1448143294,Autocomplete text entry for filter values that correspond to facets, https://github.com/simonw/datasette/issues/1890#issuecomment-1316233532,https://api.github.com/repos/simonw/datasette/issues/1890,1316233532,IC_kwDOBm6k_c5OdCE8,9599,simonw,2022-11-16T03:00:58Z,2022-11-16T03:00:58Z,OWNER,"Oops, introduced a test failure: ``` def test_table_html_foreign_key_facets(app_client): response = app_client.get( ""/fixtures/foreign_key_references?_facet=foreign_key_with_blank_label"" ) assert response.status == 200 > assert ( '
  • ' ""- 1
  • "" ) in response.text E assert '
  • - 1
  • ' in '\n\n\n fixtures: foreign_key_references: 2 rows\n \n\n\n\n\n' E + where '\n\n\n fixtures: foreign_key_references: 2 rows\n \n\n\n\n\n' = .text ``` Need to fix this test: https://github.com/simonw/datasette/blob/eac028d3f77aa5473a5fcf59240635a1bca80f7d/tests/test_table_html.py#L616-L624","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1448143294,Autocomplete text entry for filter values that correspond to facets, https://github.com/simonw/datasette/issues/1890#issuecomment-1314891228,https://api.github.com/repos/simonw/datasette/issues/1890,1314891228,IC_kwDOBm6k_c5OX6Xc,9599,simonw,2022-11-15T07:23:01Z,2022-11-15T07:23:01Z,OWNER,"Annoying: Mobile Safari doesn't seem to support separate labels and values. I should probably disable this feature on that browser, at least for foreign key facets (for the moment).","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1448143294,Autocomplete text entry for filter values that correspond to facets, https://github.com/simonw/datasette/issues/1890#issuecomment-1314856513,https://api.github.com/repos/simonw/datasette/issues/1890,1314856513,IC_kwDOBm6k_c5OXx5B,9599,simonw,2022-11-15T06:56:29Z,2022-11-15T06:56:29Z,OWNER,"Looks like I can fix that like so: ```html ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1448143294,Autocomplete text entry for filter values that correspond to facets, https://github.com/simonw/datasette/issues/1890#issuecomment-1314850524,https://api.github.com/repos/simonw/datasette/issues/1890,1314850524,IC_kwDOBm6k_c5OXwbc,9599,simonw,2022-11-15T06:48:37Z,2022-11-15T06:48:37Z,OWNER,"Spotted a bug with this on https://latest.datasette.io/fixtures/facetable?_facet=_city_id - the `_city_id` column is a foreign key, so you need to type `1` or `2` - but the autocomplete list shows the full text names for the cities.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1448143294,Autocomplete text entry for filter values that correspond to facets, https://github.com/simonw/datasette/issues/1890#issuecomment-1314849867,https://api.github.com/repos/simonw/datasette/issues/1890,1314849867,IC_kwDOBm6k_c5OXwRL,9599,simonw,2022-11-15T06:47:51Z,2022-11-15T06:47:51Z,OWNER,Demo now live here: https://congress-legislators.datasettes.com/legislators/legislator_terms?_facet=party - select `party` and start typing.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1448143294,Autocomplete text entry for filter values that correspond to facets, https://github.com/simonw/datasette/issues/1890#issuecomment-1314848432,https://api.github.com/repos/simonw/datasette/issues/1890,1314848432,IC_kwDOBm6k_c5OXv6w,9599,simonw,2022-11-15T06:46:08Z,2022-11-15T06:46:08Z,OWNER,Wrote a TIL about ``: https://til.simonwillison.net/html/datalist,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1448143294,Autocomplete text entry for filter values that correspond to facets, https://github.com/simonw/datasette/issues/1890#issuecomment-1314835740,https://api.github.com/repos/simonw/datasette/issues/1890,1314835740,IC_kwDOBm6k_c5OXs0c,9599,simonw,2022-11-15T06:30:26Z,2022-11-15T06:30:26Z,OWNER,That prototype actually works really well! I'm going to add that to `table.js`.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1448143294,Autocomplete text entry for filter values that correspond to facets, https://github.com/simonw/datasette/issues/1890#issuecomment-1314833881,https://api.github.com/repos/simonw/datasette/issues/1890,1314833881,IC_kwDOBm6k_c5OXsXZ,9599,simonw,2022-11-15T06:27:21Z,2022-11-15T06:27:21Z,OWNER,"Here's a prototype: ```javascript function createDataLists() { var facetResults = document.querySelectorAll("".facet-results [data-column]""); Array.from(facetResults).forEach(function (facetResult) { // Use link text from all links in the facet result var linkTexts = Array.from( facetResult.querySelectorAll(""li:not(.facet-truncated) a"") ).map(function (link) { return link.textContent; }); // Create a datalist element var datalist = document.createElement(""datalist""); datalist.id = ""datalist-"" + facetResult.dataset.column; // Create an option element for each link text linkTexts.forEach(function (linkText) { var option = document.createElement(""option""); option.value = linkText; datalist.appendChild(option); }); // Add the datalist to the facet result facetResult.appendChild(datalist); }); } createDataLists(); // When any select with name=_filter_column changes, update the datalist document.body.addEventListener(""change"", function (event) { if (event.target.name === ""_filter_column"") { event.target .closest("".filter-row"") .querySelector("".filter-value"") .setAttribute(""list"", ""datalist-"" + event.target.value); } }); ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1448143294,Autocomplete text entry for filter values that correspond to facets, https://github.com/simonw/datasette/issues/1890#issuecomment-1314829751,https://api.github.com/repos/simonw/datasette/issues/1890,1314829751,IC_kwDOBm6k_c5OXrW3,9599,simonw,2022-11-15T06:20:50Z,2022-11-15T06:20:50Z,OWNER,"This finds the right links on the page: document.querySelectorAll('.facet-results [data-column] li:not(.facet-truncated) a')","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1448143294,Autocomplete text entry for filter values that correspond to facets, https://github.com/simonw/datasette/issues/1890#issuecomment-1314825019,https://api.github.com/repos/simonw/datasette/issues/1890,1314825019,IC_kwDOBm6k_c5OXqM7,9599,simonw,2022-11-15T06:13:36Z,2022-11-15T06:13:36Z,OWNER,"This could start out as a purely JavaScript enhancement for pages that already figured out the available values through faceting, like you suggested.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1448143294,Autocomplete text entry for filter values that correspond to facets, https://github.com/simonw/datasette/issues/1890#issuecomment-1314823752,https://api.github.com/repos/simonw/datasette/issues/1890,1314823752,IC_kwDOBm6k_c5OXp5I,9599,simonw,2022-11-15T06:11:49Z,2022-11-15T06:11:49Z,OWNER,"I tried this out on https://congress-legislators.datasettes.com/legislators/legislator_terms for the `party` column - here's the demo: ![datalist](https://user-images.githubusercontent.com/9599/201839812-db887ce0-c4b9-432c-8620-5ac73f222a63.gif) I made this work by dropping the following HTML into the page in the browser DevTools: ```html ``` And then adding `list=""party""` to the input element in the filter form.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1448143294,Autocomplete text entry for filter values that correspond to facets, https://github.com/simonw/datasette/issues/1890#issuecomment-1314821337,https://api.github.com/repos/simonw/datasette/issues/1890,1314821337,IC_kwDOBm6k_c5OXpTZ,9599,simonw,2022-11-15T06:08:19Z,2022-11-15T06:08:19Z,OWNER,"Oh interesting... this doesn't even need to be attached to the visible faceting feature, necessarily: Datasette could try to detect when a column has a limited number of options (which the faceting code handles already) and could turn those into an auto-complete interface. There's actually a native HTML element for this these days: the `` https://developer.mozilla.org/en-US/docs/Web/HTML/Element/datalist","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1448143294,Autocomplete text entry for filter values that correspond to facets, https://github.com/simonw/datasette/issues/1888#issuecomment-1313139657,https://api.github.com/repos/simonw/datasette/issues/1888,1313139657,IC_kwDOBm6k_c5OROvJ,9599,simonw,2022-11-14T06:04:48Z,2022-11-14T06:04:48Z,OWNER,Demo: https://latest-1-0-dev.datasette.io/-/api,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1447439985,API explorer should take immutability into account, https://github.com/simonw/datasette/issues/1887#issuecomment-1313113642,https://api.github.com/repos/simonw/datasette/issues/1887,1313113642,IC_kwDOBm6k_c5ORIYq,9599,simonw,2022-11-14T05:18:51Z,2022-11-14T05:18:51Z,OWNER,Updated docs: https://docs.datasette.io/en/1.0-dev/json_api.html#dropping-tables,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1447388809,Add a confirm step to the drop table API, https://github.com/simonw/datasette/issues/1887#issuecomment-1313097713,https://api.github.com/repos/simonw/datasette/issues/1887,1313097713,IC_kwDOBm6k_c5OREfx,9599,simonw,2022-11-14T05:00:54Z,2022-11-14T05:00:54Z,OWNER,"I'm going to add a `""confirm"": true` option to the API. Without that, it returns a note about how many rows will be deleted.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1447388809,Add a confirm step to the drop table API, https://github.com/simonw/datasette/issues/1886#issuecomment-1356842576,https://api.github.com/repos/simonw/datasette/issues/1886,1356842576,IC_kwDOBm6k_c5Q38ZQ,18738650,stevecrawshaw,2022-12-18T17:34:20Z,2022-12-18T17:34:20Z,NONE,"A bit late to this, but I have made an app to publish air quality data in Bristol, UK. [air quality data in Bristol, UK.](https://brisaq-wfzqhmj43q-ew.a.run.app/) Next step to see if I can make a streamlit app based on this to produce some nice charts.","{""total_count"": 1, ""+1"": 1, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1447050738,"Call for birthday presents: if you're using Datasette, let us know how you're using it here", https://github.com/simonw/datasette/issues/1886#issuecomment-1321241426,https://api.github.com/repos/simonw/datasette/issues/1886,1321241426,IC_kwDOBm6k_c5OwItS,536941,fgregg,2022-11-20T20:58:54Z,2022-11-20T20:58:54Z,CONTRIBUTOR,i wrote up a blog post of how i'm using it! https://bunkum.us/2022/11/20/mgdo-stack.html,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1447050738,"Call for birthday presents: if you're using Datasette, let us know how you're using it here", https://github.com/simonw/datasette/issues/1886#issuecomment-1321003094,https://api.github.com/repos/simonw/datasette/issues/1886,1321003094,IC_kwDOBm6k_c5OvOhW,9020979,hydrosquall,2022-11-20T00:52:05Z,2022-11-20T00:52:05Z,NONE,"Happy birthday to datasette and thank you Simon for your continued effort on this project! I use datasette (python) as a fast layer on top of search for github projects using https://github.com/dogsheep/github-to-sqlite , and use the JSON API it provides to serve sample data to make Vega-Lite graphing workshop examples that don't require authentication/API keys. It's awesome to have a full SQL API support working without needing to develop any custom API middleware for both filtering and grouping. I've also enjoyed using it as a teaching tool for working with public dataset in [civic data workshops](https://2022.open-data.nyc/event/low-code-visual-data-exploration-with-nyc-public-data/) and as a platform for making visualization [plugins](https://github.com/hydrosquall/datasette-nteract-data-explorer) . I I'm especially excited about datasette-lite, as it will let people participate in future editions of this workshop without having to install anything to make use of their own tables :)","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1447050738,"Call for birthday presents: if you're using Datasette, let us know how you're using it here", https://github.com/simonw/datasette/issues/1886#issuecomment-1316289392,https://api.github.com/repos/simonw/datasette/issues/1886,1316289392,IC_kwDOBm6k_c5OdPtw,45195,rtanglao,2022-11-16T03:54:17Z,2022-11-16T03:58:56Z,NONE,"Happy Birthday Datasette! Thanks Simon!! I use datasette on everything most notably [my flickr metadata SQLite DB](https://www.dropbox.com/s/6j10e2vohp2j5kf/roland2019-2020.db?dl=0) to make art. Datasette lite on my 2019 flickr metadata is super helpful too: https://lite.datasette.io/?csv=https%3A%2F%2Fraw.githubusercontent.com%2Frtanglao%2Frt-flickr-sqlite-csv%2Fmain%2F2019-roland-flickr-metadata.csv Even better datasette lite on all firefox support questions from 2021: https://lite.datasette.io/?url=https%3A%2F%2Fraw.githubusercontent.com%2Frtanglao%2Frt-kits-api3%2Fmain%2FYEARLY_CSV_FILES%2F2021-firefox-sumo-questions.db Thanks again Simon! So great! What a gift to the world!!!!!! ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1447050738,"Call for birthday presents: if you're using Datasette, let us know how you're using it here", https://github.com/simonw/datasette/issues/1886#issuecomment-1314627077,https://api.github.com/repos/simonw/datasette/issues/1886,1314627077,IC_kwDOBm6k_c5OW54F,11788561,jrdmb,2022-11-15T01:19:54Z,2022-11-15T01:19:54Z,NONE,"Datasette usage comments for its 5th anniversary celebration: I use Datasette and related tools for a Cosmology Researcher Talks database app project, which is [described in the github Readme](https://github.com/jrdmb/cosmotalks-datasette#readme) The app hosted on the Google Cloud Run service also uses other Datasette-related tools developed by Simon - datasette-render-markdown, csvs-to-sqlite, datasette-template-sql, and datasette-block-robots. This is one of two apps used for querying the talks database, each has it pros/cons as described in the github Readme. At present, over 170 different sites that host cosmology talks are scraped to collect new talks for import into the sqlite database. The shot-scraper and sqlite-utils tools are a major help for this. I also use the Mastodon API to get my favorites, toots, and boosts into a local database so I can do searches on the data. This was done on Twitter and was then extended to the Mastodon data. Again, sqlite-utils is an important tool for this. ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1447050738,"Call for birthday presents: if you're using Datasette, let us know how you're using it here", https://github.com/simonw/datasette/issues/1886#issuecomment-1314455003,https://api.github.com/repos/simonw/datasette/issues/1886,1314455003,IC_kwDOBm6k_c5OWP3b,17053189,sachaj,2022-11-14T21:51:11Z,2022-11-14T21:51:11Z,NONE,"Happy Birthday Datasette! I am a librarian at the Université du Québec à Montréal (UQAM) and I've been using Datasette to publish excerpts of our library data. There are several use cases I'm working with as a proof of concept : 1. New titles list : based on reports of recent acquisitions by subject, discipline, etc. 2. List of all UQAM theses and dissertations : based on an extract of bibliographic records 3. List of all publications by UQAM Authors : based on an extract of bibliographic records See our prototype under construction here : https://datasette-bib.uqam.ca/ (some bits and pieces have been translated into French) Datasette is amazing, there is so much potential here for libraries. Thanks to Simon and all the contributors for this outstanding effort. Also sqlite-utils deserves special mention as incredibly handy and useful.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1447050738,"Call for birthday presents: if you're using Datasette, let us know how you're using it here", https://github.com/simonw/datasette/issues/1886#issuecomment-1314241058,https://api.github.com/repos/simonw/datasette/issues/1886,1314241058,IC_kwDOBm6k_c5OVboi,25778,eyeseast,2022-11-14T19:06:35Z,2022-11-14T19:06:35Z,CONTRIBUTOR,"This probably counts as a case study: https://github.com/eyeseast/spatial-data-cooking-show. Even has video. Seriously, though, this workflow has become integral to my work with reporters and editors across USA TODAY Network. Very often, I get sent a folder of data in mixed formats, with a vague ask of how we should communicate some part of it to users. Datasette and its constellation of tools makes it easy to get a quick look at that data, run exploratory queries, map it and ask questions to figure out what's important to show. And then I export a version of the data that's exactly what I need for display. ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1447050738,"Call for birthday presents: if you're using Datasette, let us know how you're using it here", https://github.com/simonw/datasette/issues/1886#issuecomment-1314223118,https://api.github.com/repos/simonw/datasette/issues/1886,1314223118,IC_kwDOBm6k_c5OVXQO,639730,virtadpt,2022-11-14T18:51:20Z,2022-11-14T18:51:20Z,NONE,I use Datasette to analyze blocklists by using csv-to-sqlite to pull their contents into a database and Datasette to look around through them. I also use its REST API to query said database as part of filtering out garbage from domains found in those blocklists.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1447050738,"Call for birthday presents: if you're using Datasette, let us know how you're using it here", https://github.com/simonw/datasette/issues/1886#issuecomment-1313271719,https://api.github.com/repos/simonw/datasette/issues/1886,1313271719,IC_kwDOBm6k_c5ORu-n,124274,lucapette,2022-11-14T08:25:12Z,2022-11-14T08:25:12Z,NONE,"Nothing spectacular yet but I think this falls under ""cool/cute application of datasette"": [improving fakedata performance for fun](https://lucapette.me/writing/improving-fakedata-performance-for-fun/). tl;dr I used datasette to visualize benchmarking data.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1447050738,"Call for birthday presents: if you're using Datasette, let us know how you're using it here", https://github.com/simonw/datasette/issues/1886#issuecomment-1313252879,https://api.github.com/repos/simonw/datasette/issues/1886,1313252879,IC_kwDOBm6k_c5ORqYP,883348,adipasquale,2022-11-14T08:10:23Z,2022-11-14T08:10:23Z,CONTRIBUTOR,"Hi @simonw and thanks for the great tools you're publishing, your dedication is inspiring! I work for the French Ministry of Culture on a surveying tool for objects protected for their historical value. It is part of a program building modern public services called [beta.gouv.fr](https://beta.gouv.fr/). In that context I'm using data published by the Ministry that I have ingested into datasette and published on a free Fly instance : https://collectif-objets-datasette.fly.dev . I have also ingested another data set with infos about french cities on this instance so that I can perform joined queries. The surveying tool synchronizes its data regularly from this datasette instance, and I also use it to perform queries when asked generic questions about the distribution of objects. (The data is not very accessible as it's undocumented and for internal usage mostly)","{""total_count"": 3, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 3, ""rocket"": 0, ""eyes"": 0}",1447050738,"Call for birthday presents: if you're using Datasette, let us know how you're using it here", https://github.com/simonw/datasette/issues/1886#issuecomment-1313052863,https://api.github.com/repos/simonw/datasette/issues/1886,1313052863,IC_kwDOBm6k_c5OQ5i_,9599,simonw,2022-11-14T03:40:50Z,2022-11-14T03:40:50Z,OWNER,"Tim Sherratt on Twitter: https://twitter.com/wragge/status/1591930345469153282 > Where do I start? The [#GLAMWorkbench](https://twitter.com/hashtag/GLAMWorkbench?src=hashtag_click) now includes a number of examples where GLAM data is harvested, processed, and then made available for exploration via Datasette. > > https://glam-workbench.net/ > > For example the GLAM Name Index Search brings together 10+ million entries from 240 indexes and provides an aggregated search using the Datasette search-all plugin: > > https://glam-workbench.net/name-search/ > > Most recently I converted PDFs of the Tasmanian Postal Directories to a big Datasette instance: https://updates.timsherratt.org/2022/09/15/from-pdfs-to.html the process is documented and reusable.","{""total_count"": 1, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 1, ""rocket"": 0, ""eyes"": 0}",1447050738,"Call for birthday presents: if you're using Datasette, let us know how you're using it here", https://github.com/simonw/datasette/issues/1886#issuecomment-1312898318,https://api.github.com/repos/simonw/datasette/issues/1886,1312898318,IC_kwDOBm6k_c5OQT0O,19851673,eigenfoo,2022-11-14T00:52:16Z,2022-11-14T00:52:16Z,NONE,"I'm a cryptic crossword enthusiast and have spent a lot of time scraping and parsing cryptic crossword clues from various blogs, forums and publications. The result is over **half a million clues from cryptic crosswords over the past twelve years**, including the clue, answer, puzzle date, puzzle name and a link to the original source. This is all hosted using Datasette, which has been a delight to use: https://cryptics.georgeho.org/ This dataset is a significant work of crossword archivism and scholarship, as acquiring historical crosswords and structuring their contents require focused effort and tedious cleaning that few are willing to do for such trivial data - for example, according to [this 2004 selection guide](https://cryptics.georgeho.org/static/documents/Selection_AppendixE_v2.pdf), the Library of Congress explicitly does not collect crossword puzzles. Anecdotally, I know that many constructors/setters of cryptic crosswords use this dataset as a resource, and some even simply call it ""the database"" - this is probably one of the most impactful data projects I've worked on!","{""total_count"": 1, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 1, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1447050738,"Call for birthday presents: if you're using Datasette, let us know how you're using it here", https://github.com/simonw/datasette/issues/1886#issuecomment-1312814245,https://api.github.com/repos/simonw/datasette/issues/1886,1312814245,IC_kwDOBm6k_c5OP_Sl,2090382,noslouch,2022-11-13T20:28:26Z,2022-11-13T20:28:26Z,NONE,"I work at The Wall Street Journal as a computational journalist and serve as our self-appointed Datasette evangelist. They say that to a hammer everything looks like a nail, but the reality is newsrooms find themselves in a sea of nails! I've only got a couple public projects that I can share, but happy to offer you a look at some of the internal projects. More often than not the internal projects stay internal because the reporting doesn't lead anywhere or I can't convince an editor to greenlight it. But imho that's the beauty of datasette: a (relatively) painless mechanism to see if there's any there there. - [WSJ Inflation Tracker](wsj.com/inflationtracker) - I scraped the oscars website and turned it into a datasette instance and ran the numbers on [best actress/best picture overlap ](https://www.wsj.com/livecoverage/oscars-academy-awards-2022/card/the-best-actress-nominees-aren-t-in-any-best-pictures-contenders-when-is-the-last-time-that-happened--mDxvbLug3rq84pxLE8gY) ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1447050738,"Call for birthday presents: if you're using Datasette, let us know how you're using it here", https://github.com/simonw/datasette/issues/1884#issuecomment-1321460293,https://api.github.com/repos/simonw/datasette/issues/1884,1321460293,IC_kwDOBm6k_c5Ow-JF,15178711,asg017,2022-11-21T04:40:55Z,2022-11-21T04:40:55Z,CONTRIBUTOR,"Counting any virtual tables can be pretty tricky. On one hand, counting a [CSV virtual table](https://www.sqlite.org/csv.html) would return the number of rows in the CSV, which is helpful (but can be I/O intensive). Counting a [FTS5 virtual table](https://www.sqlite.org/fts5.html) would return the number of entries in the FTS index, which is kindof helpful, but can be misleading in some cases. On the other hand, arbitrarily running `COUNT(*)` on some virtual tables can be incredibly expensive. SQLite offers new shortcuts/pushdowns on `COUNT(*)` queries for virtual tables, and instead calls the underlying vtab implementation and iterates through all rows in the table without discretion. For example, a virtual table that's backed by a Postgres table would call `select * from pg_table`, which would use up a lot of network and CPU calls. Or a virtual table backed by a [google sheet](https://github.com/0x6b/libgsqlite) would make network/API requests to get all the rows from the sheet just to make a count. The [`pragma_table_list`](https://www.sqlite.org/pragma.html#pragma_table_list) pragma tells you when a table is a regular table or virtual (in the `type` column), but was only added in version 3.37.0 (2021-11-27). Personally, I wouldnt try to `COUNT(*)` virtual tables - it depends on how the virtual table is implemented, it requires that the connection has the proper extensions loaded, and it may accientally cause perf issues for new-age extensions. A few extensions that I'm writing have virtual tables that wouldn't benefit much from `COUNT(*)`, and the fact that SQLite iterates through all rows in a table to count just makes things worse. ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1439009231,Exclude virtual tables from datasette inspect, https://github.com/simonw/datasette/issues/1884#issuecomment-1314066229,https://api.github.com/repos/simonw/datasette/issues/1884,1314066229,IC_kwDOBm6k_c5OUw81,25778,eyeseast,2022-11-14T16:48:35Z,2022-11-14T16:48:35Z,CONTRIBUTOR,"I'm realizing I don't know if a virtual table will ever return a count. Maybe it depends on the implementation. For these three, just checking now, it'll always return zero. That said, I'm not sure there's any downside to having them return zero and caching that. (They're hidden, too.) ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1439009231,Exclude virtual tables from datasette inspect, https://github.com/simonw/datasette/issues/1884#issuecomment-1314054300,https://api.github.com/repos/simonw/datasette/issues/1884,1314054300,IC_kwDOBm6k_c5OUuCc,9599,simonw,2022-11-14T16:40:06Z,2022-11-14T16:40:06Z,OWNER,"I wonder if there are any reasons that inspect SHOULD try to count virtual tables? Like are there any likely uses for a cirial table where the count is both interesting and likely to be accessed often enough that it's worth caching? I have an issue open to add a setting to disable table counts entirely: - #1818 Maybe that should be expanded to automatically disable row counts for virtual tables entirely? Which would mean no count would be shown for them in the UI. If you desperately wanted a count you would then have to run a count(*) query against them explicitly.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1439009231,Exclude virtual tables from datasette inspect, https://github.com/simonw/datasette/issues/1884#issuecomment-1313962183,https://api.github.com/repos/simonw/datasette/issues/1884,1313962183,IC_kwDOBm6k_c5OUXjH,25778,eyeseast,2022-11-14T15:46:32Z,2022-11-14T15:46:32Z,CONTRIBUTOR,"It does work, though I think it's probably still worth excluding virtual tables that will always be zero. Here's the same inspection as before, now with `--load-extension spatialite`: ```json { ""alltheplaces"": { ""hash"": ""0843cfe414439ab903c22d1121b7ddbc643418c35c7f0edbcec82ef1452411df"", ""size"": 963375104, ""file"": ""alltheplaces.db"", ""tables"": { ""spatial_ref_sys"": { ""count"": 6215 }, ""spatialite_history"": { ""count"": 18 }, ""sqlite_sequence"": { ""count"": 2 }, ""geometry_columns"": { ""count"": 3 }, ""spatial_ref_sys_aux"": { ""count"": 6164 }, ""views_geometry_columns"": { ""count"": 0 }, ""virts_geometry_columns"": { ""count"": 0 }, ""geometry_columns_statistics"": { ""count"": 3 }, ""views_geometry_columns_statistics"": { ""count"": 0 }, ""virts_geometry_columns_statistics"": { ""count"": 0 }, ""geometry_columns_field_infos"": { ""count"": 0 }, ""views_geometry_columns_field_infos"": { ""count"": 0 }, ""virts_geometry_columns_field_infos"": { ""count"": 0 }, ""geometry_columns_time"": { ""count"": 3 }, ""geometry_columns_auth"": { ""count"": 3 }, ""views_geometry_columns_auth"": { ""count"": 0 }, ""virts_geometry_columns_auth"": { ""count"": 0 }, ""data_licenses"": { ""count"": 10 }, ""sql_statements_log"": { ""count"": 0 }, ""states"": { ""count"": 56 }, ""counties"": { ""count"": 3234 }, ""idx_states_geometry_rowid"": { ""count"": 56 }, ""idx_states_geometry_node"": { ""count"": 3 }, ""idx_states_geometry_parent"": { ""count"": 2 }, ""idx_counties_geometry_rowid"": { ""count"": 3234 }, ""idx_counties_geometry_node"": { ""count"": 98 }, ""idx_counties_geometry_parent"": { ""count"": 97 }, ""idx_places_geometry_rowid"": { ""count"": 1236796 }, ""idx_places_geometry_node"": { ""count"": 38163 }, ""idx_places_geometry_parent"": { ""count"": 38162 }, ""places"": { ""count"": 1332609 }, ""SpatialIndex"": { ""count"": 0 }, ""ElementaryGeometries"": { ""count"": 0 }, ""KNN"": { ""count"": 0 }, ""idx_states_geometry"": { ""count"": 56 }, ""idx_counties_geometry"": { ""count"": 3234 }, ""idx_places_geometry"": { ""count"": 1236796 } } } } ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1439009231,Exclude virtual tables from datasette inspect, https://github.com/simonw/datasette/issues/1884#issuecomment-1311269045,https://api.github.com/repos/simonw/datasette/issues/1884,1311269045,IC_kwDOBm6k_c5OKGC1,9599,simonw,2022-11-11T06:08:28Z,2022-11-11T06:08:28Z,OWNER,Does that work if you add `--load-extension spatialite`?,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1439009231,Exclude virtual tables from datasette inspect, https://github.com/simonw/datasette/issues/1884#issuecomment-1309735529,https://api.github.com/repos/simonw/datasette/issues/1884,1309735529,IC_kwDOBm6k_c5OEPpp,25778,eyeseast,2022-11-10T03:57:23Z,2022-11-10T03:57:23Z,CONTRIBUTOR,Here's how to get a list of virtual tables: https://stackoverflow.com/questions/46617118/how-to-fetch-names-of-virtual-tables,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1439009231,Exclude virtual tables from datasette inspect, https://github.com/simonw/sqlite-utils/issues/511#issuecomment-1304320521,https://api.github.com/repos/simonw/sqlite-utils/issues/511,1304320521,IC_kwDOCGYnMM5NvloJ,7908073,chapmanjacobd,2022-11-04T22:54:09Z,2022-11-04T22:59:54Z,CONTRIBUTOR,I ran `PRAGMA integrity_check` and it returned `ok`. but then I tried restoring from a backup and I didn't get this `IntegrityError: constraint failed` error. So I think it was just something wrong with my database. If it happens again I will first try to reindex and see if that fixes the issue,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1436539554,"[insert_all, upsert_all] IntegrityError: constraint failed", https://github.com/simonw/sqlite-utils/issues/511#issuecomment-1304078945,https://api.github.com/repos/simonw/sqlite-utils/issues/511,1304078945,IC_kwDOCGYnMM5Nuqph,7908073,chapmanjacobd,2022-11-04T19:38:36Z,2022-11-04T20:13:17Z,CONTRIBUTOR,"Even more bizarre, the source db only has one record and the target table has no conflicting record: ``` 875 0.3s lb:/ (main|✚2) [0|0]🌺 sqlite-utils tube_71.db 'select * from media where path = ""https://archive.org/details/088ghostofachanceroygetssackedrevengeofthelivinglunchdvdripxvidphz""' | jq [ { ""size"": null, ""time_created"": null, ""play_count"": 1, ""language"": null, ""view_count"": null, ""width"": null, ""height"": null, ""fps"": null, ""average_rating"": null, ""live_status"": null, ""age_limit"": null, ""uploader"": null, ""time_played"": 0, ""path"": ""https://archive.org/details/088ghostofachanceroygetssackedrevengeofthelivinglunchdvdripxvidphz"", ""id"": ""088ghostofachanceroygetssackedrevengeofthelivinglunchdvdripxvidphz/074 - Home Away from Home, Rainy Day Robot, Odie the Amazing DVDRip XviD [PhZ].mkv"", ""ie_key"": ""ArchiveOrg"", ""playlist_path"": ""https://archive.org/details/088ghostofachanceroygetssackedrevengeofthelivinglunchdvdripxvidphz"", ""duration"": 1424.05, ""tags"": null, ""title"": ""074 - Home Away from Home, Rainy Day Robot, Odie the Amazing DVDRip XviD [PhZ].mkv"" } ] 875 0.3s lb:/ (main|✚2) [0|0]🥧 sqlite-utils video.db 'select * from media where path = ""https://archive.org/details/088ghostofachanceroygetssackedrevengeofthelivinglunchdvdripxvidphz""' | jq [] ``` I've been able to use this code successfully several times before so not sure what's causing the issue. I guess the way that I'm handling multiple databases is an issue, though it hasn't ever inserted into the source db, not sure what's different. The only reasonable explanation is that it is trying to insert into the source db from the source db for some reason? Or maybe sqlite3 is checking the source db for primary key violation because the table name is the same","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1436539554,"[insert_all, upsert_all] IntegrityError: constraint failed", https://github.com/simonw/datasette/issues/1883#issuecomment-1311437901,https://api.github.com/repos/simonw/datasette/issues/1883,1311437901,IC_kwDOBm6k_c5OKvRN,31312775,mattmalcher,2022-11-11T09:20:21Z,2022-11-11T09:20:21Z,NONE,Amazing - thank you for fixing and releasing that so quickly and for showing your process! <3 ,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1435917503,Errors when using table filters behind a proxy, https://github.com/simonw/datasette/issues/1883#issuecomment-1311314981,https://api.github.com/repos/simonw/datasette/issues/1883,1311314981,IC_kwDOBm6k_c5OKRQl,9599,simonw,2022-11-11T07:15:48Z,2022-11-11T07:15:48Z,OWNER,I released that fix in Datasette 0.63.1: https://docs.datasette.io/en/stable/changelog.html#v0-63-1,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1435917503,Errors when using table filters behind a proxy, https://github.com/simonw/datasette/issues/1883#issuecomment-1311299535,https://api.github.com/repos/simonw/datasette/issues/1883,1311299535,IC_kwDOBm6k_c5OKNfP,9599,simonw,2022-11-11T06:54:58Z,2022-11-11T06:54:58Z,OWNER,"This time deployed with: ``` cd demos/apache-proxy fly deploy --build-arg DATASETTE_REF=8d9a957c6329d26cc1e417b5d6911640d74765eb ``` To ensure the exact commit with the fix. And that fixed it! ``` % curl -i 'https://datasette-apache-proxy-demo.datasette.io/prefix/fixtures/binary_data?_filter_column=rowid&_filter_op=exact&_filter_value=1&_sort=rowid' HTTP/2 302 date: Fri, 11 Nov 2022 06:54:45 GMT server: Fly/b1863e2e7 (2022-11-09) location: /prefix/fixtures/binary_data?_sort=rowid&rowid__exact=1 link: ; rel=preload content-type: text/plain x-proxied-by: Apache2 Debian via: 2 fly.io fly-request-id: 01GHJQGBSXBR7E53TY0EKMQ9PA-sjc ``` ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1435917503,Errors when using table filters behind a proxy, https://github.com/simonw/datasette/issues/1883#issuecomment-1311292463,https://api.github.com/repos/simonw/datasette/issues/1883,1311292463,IC_kwDOBm6k_c5OKLwv,9599,simonw,2022-11-11T06:44:24Z,2022-11-11T06:44:24Z,OWNER,"Modifying that test to the following does indeed cause a failure: ```python def test_base_url_affects_filter_redirects(app_client_base_url_prefix): response = app_client_base_url_prefix.get( ""/fixtures/binary_data?_filter_column=rowid&_filter_op=exact&_filter_value=1&_sort=rowid"" ) assert response.status == 302 assert ( response.headers[""location""] == ""/prefix/fixtures/binary_data?_sort=rowid&rowid__exact=1"" ) ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1435917503,Errors when using table filters behind a proxy, https://github.com/simonw/datasette/issues/1883#issuecomment-1311291632,https://api.github.com/repos/simonw/datasette/issues/1883,1311291632,IC_kwDOBm6k_c5OKLjw,9599,simonw,2022-11-11T06:43:00Z,2022-11-11T06:43:00Z,OWNER,"https://datasette-apache-proxy-demo.datasette.io/prefix/-/asgi-scope is useful: It confirms that `/prefix/` is nowhere to be seen in the incoming request data: ``` 'path': '/-/asgi-scope', 'query_string': b'', 'raw_path': b'/-/asgi-scope', ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1435917503,Errors when using table filters behind a proxy, https://github.com/simonw/datasette/issues/1883#issuecomment-1311290115,https://api.github.com/repos/simonw/datasette/issues/1883,1311290115,IC_kwDOBm6k_c5OKLMD,9599,simonw,2022-11-11T06:40:14Z,2022-11-11T06:41:56Z,OWNER,"I modified that config file to have this line instead: ``` ProxyPass /prefix/ http://127.0.0.1:8001/ nocanon ``` And then deployed it by running: flyctl deploy --build-arg DATASETTE_REF=main This does NOT seem to have fixed the bug: ``` ~ % curl -i 'https://datasette-apache-proxy-demo.datasette.io/prefix/fixtures/binary_data?_filter_column=rowid&_filter_op=exact&_filter_value=1&_sort=rowid' HTTP/2 302 date: Fri, 11 Nov 2022 06:40:01 GMT server: Fly/b1863e2e7 (2022-11-09) location: /fixtures/binary_data?_sort=rowid&rowid__exact=1 link: ; rel=preload content-type: text/plain x-proxied-by: Apache2 Debian via: 2 fly.io fly-request-id: 01GHJPNCF51CJ626EWZEHK2CH9-sjc ``` https://datasette-apache-proxy-demo.datasette.io/prefix/-/versions seems to confirm that this is the latest deployed version (0.63), so it looks like the deploy worked.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1435917503,Errors when using table filters behind a proxy, https://github.com/simonw/datasette/issues/1883#issuecomment-1311286593,https://api.github.com/repos/simonw/datasette/issues/1883,1311286593,IC_kwDOBm6k_c5OKKVB,9599,simonw,2022-11-11T06:34:09Z,2022-11-11T06:34:09Z,OWNER,"https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#proxypass includes this note: > Normally, mod_proxy will canonicalise ProxyPassed URLs. But this may be incompatible with some backends, particularly those that make use of *PATH_INFO*. The optional *nocanon* keyword suppresses this and passes the URL path ""raw"" to the backend.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1435917503,Errors when using table filters behind a proxy, https://github.com/simonw/datasette/issues/1883#issuecomment-1311284537,https://api.github.com/repos/simonw/datasette/issues/1883,1311284537,IC_kwDOBm6k_c5OKJ05,9599,simonw,2022-11-11T06:30:38Z,2022-11-11T06:30:38Z,OWNER,"Is there a chance that it's Apache that's messing with that `location:` header here, not Datasette? https://github.com/simonw/datasette/blob/bbaab3b38ec2ce5944239ffbe2dd53328df40fff/demos/apache-proxy/000-default.conf#L7-L13","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1435917503,Errors when using table filters behind a proxy, https://github.com/simonw/datasette/issues/1883#issuecomment-1311283301,https://api.github.com/repos/simonw/datasette/issues/1883,1311283301,IC_kwDOBm6k_c5OKJhl,9599,simonw,2022-11-11T06:28:38Z,2022-11-11T06:29:33Z,OWNER,"`path_with_added_args(request, redirect_params)` should be preserving the current path from the request. https://github.com/simonw/datasette/blob/bbaab3b38ec2ce5944239ffbe2dd53328df40fff/datasette/utils/__init__.py#L273-L286","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1435917503,Errors when using table filters behind a proxy, https://github.com/simonw/datasette/issues/1883#issuecomment-1311282970,https://api.github.com/repos/simonw/datasette/issues/1883,1311282970,IC_kwDOBm6k_c5OKJca,9599,simonw,2022-11-11T06:28:05Z,2022-11-11T06:28:05Z,OWNER,Relevant code: https://github.com/simonw/datasette/blob/bbaab3b38ec2ce5944239ffbe2dd53328df40fff/datasette/views/table.py#L227-L249,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1435917503,Errors when using table filters behind a proxy, https://github.com/simonw/datasette/issues/1883#issuecomment-1311280709,https://api.github.com/repos/simonw/datasette/issues/1883,1311280709,IC_kwDOBm6k_c5OKI5F,9599,simonw,2022-11-11T06:25:27Z,2022-11-11T06:25:27Z,OWNER,"I tried adding this test but it passed! I expected it to fail: ```python def test_base_url_affects_filter_redirects(app_client_base_url_prefix): response = app_client_base_url_prefix.get( ""/prefix/fixtures/binary_data?_filter_column=rowid&_filter_op=exact&_filter_value=1&_sort=rowid"" ) assert response.status == 302 assert ( response.headers[""location""] == ""/prefix/fixtures/binary_data?_sort=rowid&rowid__exact=1"" ) ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1435917503,Errors when using table filters behind a proxy, https://github.com/simonw/datasette/issues/1883#issuecomment-1311278678,https://api.github.com/repos/simonw/datasette/issues/1883,1311278678,IC_kwDOBm6k_c5OKIZW,9599,simonw,2022-11-11T06:22:37Z,2022-11-11T06:22:37Z,OWNER,"If you view source on that page the HTML looks correct: ```html
    ``` (I just added a test that confirms this too.) But... it looks like the bug is in the redirection code. https://datasette-apache-proxy-demo.datasette.io/prefix/fixtures/binary_data?_filter_column=rowid&_filter_op=exact&_filter_value=1&_sort=rowid returns the following: location: /fixtures/binary_data?_sort=rowid&rowid__exact=1 Which is incorrect.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1435917503,Errors when using table filters behind a proxy, https://github.com/simonw/datasette/issues/1883#issuecomment-1311273461,https://api.github.com/repos/simonw/datasette/issues/1883,1311273461,IC_kwDOBm6k_c5OKHH1,9599,simonw,2022-11-11T06:16:08Z,2022-11-11T06:16:08Z,OWNER,"Great catch, thanks!","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1435917503,Errors when using table filters behind a proxy, https://github.com/simonw/datasette/issues/1882#issuecomment-1314813205,https://api.github.com/repos/simonw/datasette/issues/1882,1314813205,IC_kwDOBm6k_c5OXnUV,9599,simonw,2022-11-15T06:00:41Z,2022-11-15T06:00:41Z,OWNER,"Documentation: - https://docs.datasette.io/en/1.0-dev/json_api.html#creating-a-table - https://docs.datasette.io/en/1.0-dev/json_api.html#creating-a-table-from-example-data Wrote a TIL about how I wrote some of those tests with Copilot: https://til.simonwillison.net/gpt3/writing-test-with-copilot","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1435294468,`/db/-/create` API for creating tables, https://github.com/simonw/datasette/issues/1882#issuecomment-1312582512,https://api.github.com/repos/simonw/datasette/issues/1882,1312582512,IC_kwDOBm6k_c5OPGtw,9599,simonw,2022-11-12T22:11:18Z,2022-11-12T22:11:18Z,OWNER,"I like this: ```json { ""ok"": true, ""database"": ""data"", ""table"": ""agai2n"", ""table_url"": ""http://127.0.0.1:8001/data/agai2n"", ""schema"": ""CREATE TABLE [agai2n] (\n [hello] INTEGER\n)"" } ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1435294468,`/db/-/create` API for creating tables, https://github.com/simonw/datasette/issues/1882#issuecomment-1312581121,https://api.github.com/repos/simonw/datasette/issues/1882,1312581121,IC_kwDOBm6k_c5OPGYB,9599,simonw,2022-11-12T22:01:32Z,2022-11-12T22:01:32Z,OWNER,I'm going to change it to `table` in the output AND the input.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1435294468,`/db/-/create` API for creating tables, https://github.com/simonw/datasette/issues/1882#issuecomment-1312581008,https://api.github.com/repos/simonw/datasette/issues/1882,1312581008,IC_kwDOBm6k_c5OPGWQ,9599,simonw,2022-11-12T22:00:52Z,2022-11-12T22:00:52Z,OWNER,"Tried out my prototype in the API explorer: The `""name""` on the output is bothering me a bit - should it be `table_name` or `table` instead? Problem is I really like `name` for the input, so should it be consistent to have the same name on the output here, or should I aim for consistency with other endpoints that might return `table` rather than the ambiguous `name` elsewhere?","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1435294468,`/db/-/create` API for creating tables, https://github.com/simonw/datasette/issues/1882#issuecomment-1312580348,https://api.github.com/repos/simonw/datasette/issues/1882,1312580348,IC_kwDOBm6k_c5OPGL8,9599,simonw,2022-11-12T21:55:54Z,2022-11-12T21:56:45Z,OWNER,"What should this API return? I think the name of the table (`name`), the URL to that table (`table_url` - for consistency with how faceting API works already) and the schema of the table (`schema`).","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1435294468,`/db/-/create` API for creating tables, https://github.com/simonw/datasette/issues/1882#issuecomment-1312575048,https://api.github.com/repos/simonw/datasette/issues/1882,1312575048,IC_kwDOBm6k_c5OPE5I,9599,simonw,2022-11-12T21:22:58Z,2022-11-12T21:22:58Z,OWNER,Need to validate the table name. SQLite supports almost any table name - but they can't contain a newline character and cannot start with `sqlite_` - according to https://stackoverflow.com/questions/3694276/what-are-valid-table-names-in-sqlite/43049720#43049720,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1435294468,`/db/-/create` API for creating tables, https://github.com/simonw/datasette/issues/1882#issuecomment-1312556044,https://api.github.com/repos/simonw/datasette/issues/1882,1312556044,IC_kwDOBm6k_c5OPAQM,9599,simonw,2022-11-12T19:29:11Z,2022-11-12T19:29:11Z,OWNER,"Thought of an edge-case: with `sqlite-utils` one feature I really like is that I can pipe data into it without caring if the table already exists or not: cat data.json | sqlite-utils insert my.db mytable - How could this new API support that? I thought about adding a `""create"": true` option to `/db/table/-/insert` which creates the table if it doesn't already exist, but if I do that I'll need to start adding other options to that endpoint - to set the primary key, add foreign keys and suchlike - which would be ignored except for the cases where the table was being created from scratch. This doesn't feel right to me - I want to keep those options here, on `/db/-/create`. One idea I had was to implement it such that you can call `/db/-/create` multiple times for the same table, but only if you are using the `""rows""` option. If so, and if the rows can be safely inserted, it would let you do that. But instead, I'm going to outsource this to the CLI tool I plan to write that feeds data into this API. I'm already planning to use that tool for CSV inserts (so the API doesn't need to accept CSV directly). I think it's a good place for other usability enhancements like ""insert this, creating the table if it does not exist"" as well.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1435294468,`/db/-/create` API for creating tables, https://github.com/simonw/datasette/issues/1882#issuecomment-1302818784,https://api.github.com/repos/simonw/datasette/issues/1882,1302818784,IC_kwDOBm6k_c5Np2_g,9599,simonw,2022-11-04T00:25:18Z,2022-11-04T16:12:39Z,OWNER,"On that basis I think the core API design should change to this: ``` POST /db/-/create Authorization: Bearer xxx Content-Type: application/json { ""name"": ""my new table"", ""columns"": [ { ""name"": ""id"", ""type"": ""integer"" }, { ""name"": ""title"", ""type"": ""text"" } ] ""pk"": ""id"" } ``` This leaves room for a `""rows"": []` key at the root too. Having that as a child of `""table""` felt unintuitive to me, and I didn't like the way this looked either: ```json { ""table"": { ""name"": ""my_new_table"" }, ""rows"": [ {""id"": 1, ""title"": ""Title""} ] } ``` Weird to have the table `name` nested inside `table` when `rows` wasn't.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1435294468,`/db/-/create` API for creating tables, https://github.com/simonw/datasette/issues/1882#issuecomment-1302818153,https://api.github.com/repos/simonw/datasette/issues/1882,1302818153,IC_kwDOBm6k_c5Np21p,9599,simonw,2022-11-04T00:23:58Z,2022-11-04T00:23:58Z,OWNER,"I made a decision here that this endpoint should also accept an optional `""rows"": [...]` list which is used to automatically create the table using a schema derived from those example rows (which then get inserted): - https://github.com/simonw/datasette/issues/1862#issuecomment-1302817807","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1435294468,`/db/-/create` API for creating tables, https://github.com/simonw/datasette/issues/1882#issuecomment-1302721916,https://api.github.com/repos/simonw/datasette/issues/1882,1302721916,IC_kwDOBm6k_c5NpfV8,9599,simonw,2022-11-03T21:58:50Z,2022-11-03T21:59:17Z,OWNER,"Mocked up a quick HTML+JavaScript form for creating that JSON structure using some iteration against Copilot prompts: ```html
    /* JSON format:
    {
      ""table"": {
          ""name"": ""my new table"",
          ""columns"": [
              {
                  ""name"": ""id"",
                  ""type"": ""integer""
              },
              {
                  ""name"": ""title"",
                  ""type"": ""text""
              }
          ]
         ""pk"": ""id""
      }
    }
    
    HTML form with Javascript for creating this JSON:
    */


    Current columns:

      ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1435294468,`/db/-/create` API for creating tables, https://github.com/simonw/datasette/issues/1882#issuecomment-1302716350,https://api.github.com/repos/simonw/datasette/issues/1882,1302716350,IC_kwDOBm6k_c5Npd--,9599,simonw,2022-11-03T21:51:14Z,2022-11-03T22:35:54Z,OWNER,"Validating this JSON object is getting a tiny bit complex. I'm tempted to adopt https://pydantic-docs.helpmanual.io/ at this point. The `create_model` example on https://stackoverflow.com/questions/66168517/generate-dynamic-model-using-pydantic/66168682#66168682 is particularly relevant, especially when I work on this issue: - #1863 ```python from pydantic import create_model d = {""strategy"": {""name"": ""test_strat2"", ""periods"": 10}} Strategy = create_model(""Strategy"", **d[""strategy""]) print(Strategy.schema_json(indent=2)) ``` `create_model()`: https://pydantic-docs.helpmanual.io/usage/models/#dynamic-model-creation","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1435294468,`/db/-/create` API for creating tables, https://github.com/simonw/datasette/issues/1882#issuecomment-1302715662,https://api.github.com/repos/simonw/datasette/issues/1882,1302715662,IC_kwDOBm6k_c5Npd0O,9599,simonw,2022-11-03T21:50:27Z,2022-11-03T21:50:27Z,OWNER,"API design for this: ``` POST /db/-/create Authorization: Bearer xxx Content-Type: application/json { ""table"": { ""name"": ""my new table"", ""columns"": [ { ""name"": ""id"", ""type"": ""integer"" }, { ""name"": ""title"", ""type"": ""text"" } ] ""pk"": ""id"" } } ``` Supported column types are: - `integer` - `text` - `float` (even though SQLite calls it a ""real"") - `blob` This matches my design for `sqlite-utils`: https://sqlite-utils.datasette.io/en/stable/cli.html#cli-create-table","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1435294468,`/db/-/create` API for creating tables, https://github.com/simonw/sqlite-utils/issues/510#issuecomment-1320394127,https://api.github.com/repos/simonw/sqlite-utils/issues/510,1320394127,IC_kwDOCGYnMM5Os52P,1176293,ar-jan,2022-11-18T18:37:51Z,2022-11-18T18:37:51Z,NONE,"I guess it is not incorrect when it says the version is `4`, though it is confusing. Maybe it doesn't even refer to FTS4/FTS5 versions, but something else? In any case, it's not related to sqlite-utils, but SQLite itself.","{""total_count"": 1, ""+1"": 1, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1434911255,Cannot enable FTS5 despite it being available, https://github.com/simonw/sqlite-utils/issues/510#issuecomment-1318777114,https://api.github.com/repos/simonw/sqlite-utils/issues/510,1318777114,IC_kwDOCGYnMM5OmvEa,7908073,chapmanjacobd,2022-11-17T15:09:47Z,2022-11-17T15:09:47Z,CONTRIBUTOR,"why close? is the only problem that the _config table that incorrectly says 4 for fts5? if so, that's still something that should be fixed","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1434911255,Cannot enable FTS5 despite it being available, https://github.com/simonw/sqlite-utils/issues/510#issuecomment-1318431389,https://api.github.com/repos/simonw/sqlite-utils/issues/510,1318431389,IC_kwDOCGYnMM5Olaqd,1176293,ar-jan,2022-11-17T10:36:28Z,2022-11-17T10:36:28Z,NONE,The virtual table's _config `version: 4` seems to indicate FTS5.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1434911255,Cannot enable FTS5 despite it being available, https://github.com/simonw/datasette/issues/1881#issuecomment-1302813449,https://api.github.com/repos/simonw/datasette/issues/1881,1302813449,IC_kwDOBm6k_c5Np1sJ,9599,simonw,2022-11-04T00:14:07Z,2022-11-04T00:14:07Z,OWNER,"Tool is now live here: https://latest-1-0-dev.datasette.io/-/permissions Needs root perms, so access this first: https://latest-1-0-dev.datasette.io/login-as-root","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1434094365,Tool for simulating permission checks against actors, https://github.com/simonw/datasette/issues/1881#issuecomment-1302812918,https://api.github.com/repos/simonw/datasette/issues/1881,1302812918,IC_kwDOBm6k_c5Np1j2,9599,simonw,2022-11-04T00:13:05Z,2022-11-04T00:13:05Z,OWNER,Has tests now.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1434094365,Tool for simulating permission checks against actors, https://github.com/simonw/datasette/issues/1881#issuecomment-1301645921,https://api.github.com/repos/simonw/datasette/issues/1881,1301645921,IC_kwDOBm6k_c5NlYph,9599,simonw,2022-11-03T05:10:05Z,2022-12-09T01:38:21Z,OWNER,"I'd love to come up with a good short name for the second part of the resource two-tuple, the thing which is usually the name of a table but could also be the name of a SQL view or the name of a canned query. Idea 8th December: why not call it resource? A resource could be a thing that lives inside a database.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1434094365,Tool for simulating permission checks against actors, https://github.com/simonw/datasette/issues/1881#issuecomment-1301639741,https://api.github.com/repos/simonw/datasette/issues/1881,1301639741,IC_kwDOBm6k_c5NlXI9,9599,simonw,2022-11-03T04:58:21Z,2022-11-03T04:58:21Z,OWNER,"The whole `database_name` or `(database_name, table_name)` tuple for resource is a bit of a code smell. Maybe this is a chance to tidy that up too?","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1434094365,Tool for simulating permission checks against actors, https://github.com/simonw/datasette/issues/1881#issuecomment-1301639370,https://api.github.com/repos/simonw/datasette/issues/1881,1301639370,IC_kwDOBm6k_c5NlXDK,9599,simonw,2022-11-03T04:57:21Z,2022-11-03T04:57:21Z,OWNER,"The plugin hook would be called `register_permissions()`, for consistency with `register_routes()` and `register_commands()`.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1434094365,Tool for simulating permission checks against actors, https://github.com/simonw/datasette/issues/1881#issuecomment-1301638918,https://api.github.com/repos/simonw/datasette/issues/1881,1301638918,IC_kwDOBm6k_c5NlW8G,9599,simonw,2022-11-03T04:56:06Z,2022-11-03T04:56:06Z,OWNER,"I've also introduced a new concept of a permission abbreviation, which like the permission name needs to be globally unique. That's a problem for plugins - they might just be able to guarantee that their permission long-form name is unique among other plugins (through sensible naming conventions) but the thing where they declare a initial-letters-only abbreviation is far more risky. I think abbreviations are optional - they are provided for core permissions but plugins are advised not to use them. Also Datasette could check that the installed plugins do not provide conflicting permissions on startup and refuse to start if they do.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1434094365,Tool for simulating permission checks against actors, https://github.com/simonw/datasette/issues/1881#issuecomment-1301638156,https://api.github.com/repos/simonw/datasette/issues/1881,1301638156,IC_kwDOBm6k_c5NlWwM,9599,simonw,2022-11-03T04:54:00Z,2022-11-03T04:54:00Z,OWNER,"If I have the permissions defined like this: ```python PERMISSIONS = ( Permission(""view-instance"", ""vi"", False, False, True), Permission(""view-database"", ""vd"", True, False, True), Permission(""view-database-download"", ""vdd"", True, False, True), Permission(""view-table"", ""vt"", True, True, True), Permission(""view-query"", ""vq"", True, True, True), Permission(""insert-row"", ""ir"", True, True, False), Permission(""delete-row"", ""dr"", True, True, False), Permission(""drop-table"", ""dt"", True, True, False), Permission(""execute-sql"", ""es"", True, False, True), Permission(""permissions-debug"", ""pd"", False, False, False), Permission(""debug-menu"", ""dm"", False, False, False), ) ``` Instead of just calling them by their undeclared names in places like this: ```python await self.ds.permission_allowed( request.actor, ""execute-sql"", database, default=True ) ``` On the one hand I can ditch that confusing `default=True` option - whether a permission is on by default becomes a characteristic of that `Permission()` itself, which feels much neater. On the other hand though, plugins that introduce their own permissions - like https://datasette.io/plugins/datasette-edit-schema - will need a way to register those permissions with Datasette core. Probably another plugin hook.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1434094365,Tool for simulating permission checks against actors, https://github.com/simonw/datasette/issues/1881#issuecomment-1301635906,https://api.github.com/repos/simonw/datasette/issues/1881,1301635906,IC_kwDOBm6k_c5NlWNC,9599,simonw,2022-11-03T04:48:09Z,2022-11-03T04:48:09Z,OWNER,"I built this prototype on the http://127.0.0.1:8001/-/allow-debug page, which is open to anyone to visit. But... I just realized that using this tool can leak information - you can use it to guess the names of invisible databases and tables and run theoretical permission checks against them. Using the tool also pollutes the list of permission checks that show up on the root-anlo `/-/permissions` page. So.... I'm going to restrict the usage of this tool to users with access to `/-/permissions` and put it on that page instead. ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1434094365,Tool for simulating permission checks against actors, https://github.com/simonw/datasette/issues/1881#issuecomment-1301635340,https://api.github.com/repos/simonw/datasette/issues/1881,1301635340,IC_kwDOBm6k_c5NlWEM,9599,simonw,2022-11-03T04:46:41Z,2022-11-03T04:46:41Z,OWNER,"Built this prototype: ![prototype](https://user-images.githubusercontent.com/9599/199649219-f146e43b-bfb5-45e6-9777-956f21a79887.gif) In building it I realized I needed to know which permissions took a table, a database, both or neither. So I had to bake that into the code. Here's the prototype so far (which includes a prototype of the logic for the `_r` field on actor, see #1855): ```diff diff --git a/datasette/default_permissions.py b/datasette/default_permissions.py index 32b0c758..f68aa38f 100644 --- a/datasette/default_permissions.py +++ b/datasette/default_permissions.py @@ -6,8 +6,8 @@ import json import time -@hookimpl(tryfirst=True) -def permission_allowed(datasette, actor, action, resource): +@hookimpl(tryfirst=True, specname=""permission_allowed"") +def permission_allowed_default(datasette, actor, action, resource): async def inner(): if action in ( ""permissions-debug"", @@ -57,6 +57,44 @@ def permission_allowed(datasette, actor, action, resource): return inner +@hookimpl(specname=""permission_allowed"") +def permission_allowed_actor_restrictions(actor, action, resource): + if actor is None: + return None + _r = actor.get(""_r"") + if not _r: + # No restrictions, so we have no opinion + return None + action_initials = """".join([word[0] for word in action.split(""-"")]) + # If _r is defined then we use those to further restrict the actor + # Crucially, we only use this to say NO (return False) - we never + # use it to return YES (True) because that might over-ride other + # restrictions placed on this actor + all_allowed = _r.get(""a"") + if all_allowed is not None: + assert isinstance(all_allowed, list) + if action_initials in all_allowed: + return None + # How about for the current database? + if action in (""view-database"", ""view-database-download"", ""execute-sql""): + database_allowed = _r.get(""d"", {}).get(resource) + if database_allowed is not None: + assert isinstance(database_allowed, list) + if action_initials in database_allowed: + return None + # Or the current table? That's any time the resource is (database, table) + if not isinstance(resource, str) and len(resource) == 2: + database, table = resource + table_allowed = _r.get(""t"", {}).get(database, {}).get(table) + # TODO: What should this do for canned queries? + if table_allowed is not None: + assert isinstance(table_allowed, list) + if action_initials in table_allowed: + return None + # This action is not specifically allowed, so reject it + return False + + @hookimpl def actor_from_request(datasette, request): prefix = ""dstok_"" diff --git a/datasette/templates/allow_debug.html b/datasette/templates/allow_debug.html index 0f1b30f0..ae43f0f5 100644 --- a/datasette/templates/allow_debug.html +++ b/datasette/templates/allow_debug.html @@ -35,7 +35,7 @@ p.message-warning {

      Use this tool to try out different actor and allow combinations. See Defining permissions with ""allow"" blocks for documentation.

      -
      +

      @@ -55,4 +55,82 @@ p.message-warning { {% if result == ""False"" %}

      Result: deny

      {% endif %} +

      Test permission check

      + +

      This tool lets you simulate an actor and a permission check for that actor.

      + + + +
      +

      + +
      +
      +

      +

      + +

      +

      +
      +
      + +
      + + + + + {% endblock %} diff --git a/datasette/views/special.py b/datasette/views/special.py index 9922a621..d46fc280 100644 --- a/datasette/views/special.py +++ b/datasette/views/special.py @@ -1,6 +1,8 @@ import json +from datasette.permissions import PERMISSIONS from datasette.utils.asgi import Response, Forbidden from datasette.utils import actor_matches_allow, add_cors_headers +from datasette.permissions import PERMISSIONS from .base import BaseView import secrets import time @@ -138,9 +140,34 @@ class AllowDebugView(BaseView): ""error"": ""\n\n"".join(errors) if errors else """", ""actor_input"": actor_input, ""allow_input"": allow_input, + ""permissions"": PERMISSIONS, }, ) + async def post(self, request): + vars = await request.post_vars() + actor = json.loads(vars[""actor""]) + permission = vars[""permission""] + resource_1 = vars[""resource_1""] + resource_2 = vars[""resource_2""] + resource = [] + if resource_1: + resource.append(resource_1) + if resource_2: + resource.append(resource_2) + resource = tuple(resource) + result = await self.ds.permission_allowed( + actor, permission, resource, default=""USE_DEFAULT"" + ) + return Response.json( + { + ""actor"": actor, + ""permission"": permission, + ""resource"": resource, + ""result"": result, + } + ) + class MessagesDebugView(BaseView): name = ""messages_debug"" ``` ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1434094365,Tool for simulating permission checks against actors, https://github.com/simonw/datasette/issues/1880#issuecomment-1317420812,https://api.github.com/repos/simonw/datasette/issues/1880,1317420812,IC_kwDOBm6k_c5Ohj8M,525934,amitkoth,2022-11-16T17:50:29Z,2022-11-16T17:50:29Z,NONE,"I appreciate your response @simonw - thanks! I'll clarify what we need further - let's imagine we have 2000 SQLLite databases (for 2000 tenants), but we only want to run _one_ datasette instance for each of those tenants to query/use datasette against their _own_ database only. This means the ""connection"" between datasette and the SQLLite database would be dynamic, based on the tenantID that's required on an incoming request. Is there any specific config or other considerations in this use case, to minimize memory use on a single, efficient VM and serve queries to all these tenants? cc @muadham","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1433576351,Datasette with many and large databases > Memory use, https://github.com/simonw/datasette/issues/1880#issuecomment-1311273063,https://api.github.com/repos/simonw/datasette/issues/1880,1311273063,IC_kwDOBm6k_c5OKHBn,9599,simonw,2022-11-11T06:15:28Z,2022-11-11T06:15:28Z,OWNER,"The `_internal` database is intended to help Datasette handle much larger attached databases. Right now Datasette attempts to show every database on the https://latest.datasette.io/ index page and every table on the https://latest.datasette.io/fixtures database index page - but these are not paginated. If you had a database containing 1,000 tables the database index page would get pretty slow. So I want to be able to paginate (and search) those. But to paginate them it's useful to have them in a database table itself, since then I can paginate using SQL. My plan for `_internal` is to use it to implement those advanced browsing features. I've not completed this work yet though. See this issue for more details on that: - #417","{""total_count"": 1, ""+1"": 1, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1433576351,Datasette with many and large databases > Memory use, https://github.com/simonw/datasette/issues/1880#issuecomment-1311271298,https://api.github.com/repos/simonw/datasette/issues/1880,1311271298,IC_kwDOBm6k_c5OKGmC,9599,simonw,2022-11-11T06:12:29Z,2022-11-11T06:12:29Z,OWNER,"I think you may have misunderstood this feature. This is talking about the `_internal` in-memory database, which maintains a set of tables that list the databases and tables that are attached to Datasette. They're not a copy of the data itself - just a list of table names, column names and database names. You can see what that database looks like by signing in as root - running `datasette --root` and clicking the link. Or you can see an example here: - Click the button on https://latest.datasette.io/login-as-root - Now visit https://latest.datasette.io/_internal For the example instance that looks like this: The two most interesting tables in there are these ones: As you can see, it's just the table schema itself and the columns that make up the tables. Even if you have hundreds of databases connected each with hundreds of tables this should still only add up to a few MB of RAM.","{""total_count"": 1, ""+1"": 1, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1433576351,Datasette with many and large databases > Memory use, https://github.com/simonw/datasette/issues/1880#issuecomment-1301043042,https://api.github.com/repos/simonw/datasette/issues/1880,1301043042,IC_kwDOBm6k_c5NjFdi,525934,amitkoth,2022-11-02T18:20:14Z,2022-11-02T18:20:14Z,NONE,"Follow on question - is all memory use @simonw - for both datasette and SQLlite confined to the ""query time"" itself i.e. the memory use is relevant only to a particular transaction or query - and then subsequently released?","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1433576351,Datasette with many and large databases > Memory use, https://github.com/simonw/datasette/issues/1879#issuecomment-1299102755,https://api.github.com/repos/simonw/datasette/issues/1879,1299102755,IC_kwDOBm6k_c5Nbrwj,9599,simonw,2022-11-01T20:31:37Z,2022-11-01T20:31:37Z,OWNER,And some JavaScript that can spot if Datasette thinks it is being served over HTTP when it's actually being served over HTTPS.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1432037325,Make it easier to fix URL proxy problems, https://github.com/simonw/datasette/issues/1879#issuecomment-1299102108,https://api.github.com/repos/simonw/datasette/issues/1879,1299102108,IC_kwDOBm6k_c5Nbrmc,9599,simonw,2022-11-01T20:30:54Z,2022-11-01T20:33:06Z,OWNER,One idea: add a `/-/debug` page (or `/-/tips` or `/-/checks`) which shows the incoming requests headers and could even detect if there's an `x-forwarded-host` header that isn't being repeated and show a tip on how to fix that.,"{""total_count"": 1, ""+1"": 1, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1432037325,Make it easier to fix URL proxy problems, https://github.com/simonw/datasette/issues/1879#issuecomment-1299098458,https://api.github.com/repos/simonw/datasette/issues/1879,1299098458,IC_kwDOBm6k_c5Nbqta,9599,simonw,2022-11-01T20:27:40Z,2022-11-01T20:33:52Z,OWNER,"https://github.com/simonw/datasette-x-forwarded-host/blob/main/datasette_x_forwarded_host/__init__.py could happen in core controlled by: `--setting trust_forwarded_host 1`","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1432037325,Make it easier to fix URL proxy problems, https://github.com/simonw/datasette/issues/1879#issuecomment-1299096850,https://api.github.com/repos/simonw/datasette/issues/1879,1299096850,IC_kwDOBm6k_c5NbqUS,9599,simonw,2022-11-01T20:26:12Z,2022-11-01T20:26:12Z,OWNER,"The other relevant plugin here is https://datasette.io/plugins/datasette-x-forwarded-host Maybe that should be rolled into core too?","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1432037325,Make it easier to fix URL proxy problems, https://github.com/simonw/datasette/issues/1879#issuecomment-1299090678,https://api.github.com/repos/simonw/datasette/issues/1879,1299090678,IC_kwDOBm6k_c5Nboz2,9599,simonw,2022-11-01T20:20:28Z,2022-11-01T20:20:28Z,OWNER,My first step in debugging these is to install https://datasette.io/plugins/datasette-debug-asgi - but now I'm thinking maybe something like that should be part of core.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1432037325,Make it easier to fix URL proxy problems, https://github.com/simonw/datasette/issues/1878#issuecomment-1336100218,https://api.github.com/repos/simonw/datasette/issues/1878,1336100218,IC_kwDOBm6k_c5Po0V6,9599,simonw,2022-12-03T07:02:15Z,2022-12-03T07:02:15Z,OWNER,"Moved this work to a PR: - #1931","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1432013704,/db/table/-/upsert API, https://github.com/simonw/datasette/issues/1878#issuecomment-1336094562,https://api.github.com/repos/simonw/datasette/issues/1878,1336094562,IC_kwDOBm6k_c5Poy9i,9599,simonw,2022-12-03T06:27:50Z,2022-12-03T06:29:06Z,OWNER,"This adds it to the API explorer: ```diff diff --git a/datasette/views/special.py b/datasette/views/special.py index 1f84b094..1b4a9d3c 100644 --- a/datasette/views/special.py +++ b/datasette/views/special.py @@ -316,21 +316,37 @@ class ApiExplorerView(BaseView): request.actor, ""insert-row"", (name, table) ): pks = await db.primary_keys(table) - table_links.append( - { - ""path"": self.ds.urls.table(name, table) + ""/-/insert"", - ""method"": ""POST"", - ""label"": ""Insert rows into {}"".format(table), - ""json"": { - ""rows"": [ - { - column: None - for column in await db.table_columns(table) - if column not in pks - } - ] + table_links.extend( + [ + { + ""path"": self.ds.urls.table(name, table) + ""/-/insert"", + ""method"": ""POST"", + ""label"": ""Insert rows into {}"".format(table), + ""json"": { + ""rows"": [ + { + column: None + for column in await db.table_columns(table) + if column not in pks + } + ] + }, }, - } + { + ""path"": self.ds.urls.table(name, table) + ""/-/upsert"", + ""method"": ""POST"", + ""label"": ""Upsert rows into {}"".format(table), + ""json"": { + ""rows"": [ + { + column: None + for column in await db.table_columns(table) + if column not in pks + } + ] + }, + }, + ] ) if await self.ds.permission_allowed( request.actor, ""drop-table"", (name, table) ``` Except it doesn't quite, because the example JSON this generates is invalid as it does not include the primary key column. (Made me notice that the way example columns are created for `/-/insert` will fail for tables that don't have an auto-incrementing primary key)","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1432013704,/db/table/-/upsert API, https://github.com/simonw/datasette/issues/1878#issuecomment-1336094470,https://api.github.com/repos/simonw/datasette/issues/1878,1336094470,IC_kwDOBm6k_c5Poy8G,9599,simonw,2022-12-03T06:27:13Z,2022-12-03T06:27:13Z,OWNER,"Tests are going to need to cover both rowid-only and compound primary key tables, including all of the error states.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1432013704,/db/table/-/upsert API, https://github.com/simonw/datasette/issues/1878#issuecomment-1336094381,https://api.github.com/repos/simonw/datasette/issues/1878,1336094381,IC_kwDOBm6k_c5Poy6t,9599,simonw,2022-12-03T06:26:25Z,2022-12-03T06:26:25Z,OWNER,"Initial prototype: ```diff diff --git a/datasette/app.py b/datasette/app.py index 125b4969..282c0984 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -40,7 +40,7 @@ from .views.special import ( PermissionsDebugView, MessagesDebugView, ) -from .views.table import TableView, TableInsertView, TableDropView +from .views.table import TableView, TableInsertView, TableUpsertView, TableDropView from .views.row import RowView, RowDeleteView, RowUpdateView from .renderer import json_renderer from .url_builder import Urls @@ -1292,6 +1292,10 @@ class Datasette: TableInsertView.as_view(self), r""/(?P[^\/\.]+)/(?P[^\/\.]+)/-/insert$"", ) + add_route( + TableUpsertView.as_view(self), + r""/(?P[^\/\.]+)/(?P
      [^\/\.]+)/-/upsert$"", + ) add_route( TableDropView.as_view(self), r""/(?P[^\/\.]+)/(?P
      [^\/\.]+)/-/drop$"", diff --git a/datasette/views/table.py b/datasette/views/table.py index 7ba78c11..ae0d6366 100644 --- a/datasette/views/table.py +++ b/datasette/views/table.py @@ -1074,9 +1074,15 @@ class TableInsertView(BaseView): def __init__(self, datasette): self.ds = datasette - async def _validate_data(self, request, db, table_name): + async def _validate_data(self, request, db, table_name, pks, upsert): errors = [] + pks_list = [] + if isinstance(pks, str): + pks_list = [pks] + else: + pks_list = list(pks) + def _errors(errors): return None, errors, {} @@ -1135,6 +1141,15 @@ class TableInsertView(BaseView): # Validate columns of each row columns = set(await db.table_columns(table_name)) for i, row in enumerate(rows): + if upsert: + # It MUST have the primary key + missing_pks = [pk for pk in pks_list if pk not in row] + if missing_pks: + errors.append( + 'Row {} is missing primary key column(s): ""{}""'.format( + i, '"", ""'.join(missing_pks) + ) + ) invalid_columns = set(row.keys()) - columns if invalid_columns: errors.append( @@ -1146,7 +1161,7 @@ class TableInsertView(BaseView): return _errors(errors) return rows, errors, extras - async def post(self, request): + async def post(self, request, upsert=False): try: resolved = await self.ds.resolve_table(request) except NotFound as e: @@ -1164,7 +1179,12 @@ class TableInsertView(BaseView): request.actor, ""insert-row"", resource=(database_name, table_name) ): return _error([""Permission denied""], 403) - rows, errors, extras = await self._validate_data(request, db, table_name) + + pks = await db.primary_keys(table_name) + + rows, errors, extras = await self._validate_data( + request, db, table_name, pks, upsert + ) if errors: return _error(errors, 400) @@ -1172,15 +1192,19 @@ class TableInsertView(BaseView): replace = extras.get(""replace"") should_return = bool(extras.get(""return"", False)) - # Insert rows - def insert_rows(conn): + + def insert_or_upsert_rows(conn): table = sqlite_utils.Database(conn)[table_name] + kwargs = {} + if upsert: + kwargs[""pk""] = pks[0] if len(pks) == 1 else pks + else: + kwargs = {""ignore"": ignore, ""replace"": replace} if should_return: rowids = [] + method = table.upsert if upsert else table.insert for row in rows: - rowids.append( - table.insert(row, ignore=ignore, replace=replace).last_rowid - ) + rowids.append(method(row, **kwargs).last_rowid) return list( table.rows_where( ""rowid in ({})"".format("","".join(""?"" for _ in rowids)), @@ -1188,10 +1212,11 @@ class TableInsertView(BaseView): ) ) else: - table.insert_all(rows, ignore=ignore, replace=replace) + method_all = table.upsert_all if upsert else table.insert_all + method_all(rows, **kwargs) try: - rows = await db.execute_write_fn(insert_rows) + rows = await db.execute_write_fn(insert_or_upsert_rows) except Exception as e: return _error([str(e)]) result = {""ok"": True} @@ -1200,6 +1225,13 @@ class TableInsertView(BaseView): return Response.json(result, status=201) +class TableUpsertView(TableInsertView): + name = ""table-upsert"" + + async def post(self, request): + return await super().post(request, upsert=True) + + class TableDropView(BaseView): name = ""table-drop"" ``` Manual testing reveals that this mostly works... but it's not doing the right thing for `""return"": true` - it always returns an empty list.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1432013704,/db/table/-/upsert API, https://github.com/simonw/datasette/issues/1878#issuecomment-1336073212,https://api.github.com/repos/simonw/datasette/issues/1878,1336073212,IC_kwDOBm6k_c5Potv8,9599,simonw,2022-12-03T05:38:49Z,2022-12-03T05:38:49Z,OWNER,And on Discord today: https://discord.com/channels/823971286308356157/823971286941302908/1048426072066236536,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1432013704,/db/table/-/upsert API, https://github.com/simonw/datasette/issues/1878#issuecomment-1336070843,https://api.github.com/repos/simonw/datasette/issues/1878,1336070843,IC_kwDOBm6k_c5PotK7,9599,simonw,2022-12-03T05:37:53Z,2022-12-03T05:37:53Z,OWNER,Also requested here: https://news.ycombinator.com/item?id=33839894,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1432013704,/db/table/-/upsert API, https://github.com/simonw/datasette/issues/1878#issuecomment-1312534826,https://api.github.com/repos/simonw/datasette/issues/1878,1312534826,IC_kwDOBm6k_c5OO7Eq,18738650,stevecrawshaw,2022-11-12T17:34:58Z,2022-11-12T17:34:58Z,NONE,"Hi Simon. I have just started experimenting with datasette in earnest, looking at it's suitability for air quality open data. A bulk upsert \ upsert_all would be very useful for me in enabling real time data to be pushed from a sql server database with FME server to a datasette db. An hourly process queries the last 2 hours of data and pushes that to my database, inserting new data and updating existing combinations of pk siteid and date_time. This is already implemented on our current [open data portal](https://opendata.bristol.gov.uk/explore/dataset/air-quality-data-continuous/table/?disjunctive.location&sort=date_time). Excited to see your progress with this! Thank you for this amazing software.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1432013704,/db/table/-/upsert API, https://github.com/simonw/datasette/issues/1878#issuecomment-1299071456,https://api.github.com/repos/simonw/datasette/issues/1878,1299071456,IC_kwDOBm6k_c5NbkHg,9599,simonw,2022-11-01T20:02:43Z,2022-11-01T20:02:43Z,OWNER,"Note that ""update"" is partially covered by the `replace` option to `/-/insert`, added here: - https://github.com/simonw/datasette/issues/1873#issuecomment-1298885451 ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1432013704,/db/table/-/upsert API, https://github.com/simonw/datasette/issues/1877#issuecomment-1331201207,https://api.github.com/repos/simonw/datasette/issues/1877,1331201207,IC_kwDOBm6k_c5PWIS3,9599,simonw,2022-11-29T19:44:07Z,2022-11-29T19:44:07Z,OWNER,"I fixed the duplicate logic issue here: https://github.com/simonw/datasette/commit/ee64130fa8a5ff4a24791916c696e10cf2375102 - #1896 Decided not to address `views/table.py`.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1432012302,Refactor and tidy up final write API code, https://github.com/simonw/datasette/issues/1876#issuecomment-1298856054,https://api.github.com/repos/simonw/datasette/issues/1876,1298856054,IC_kwDOBm6k_c5Navh2,9599,simonw,2022-11-01T17:16:01Z,2022-11-01T17:16:01Z,OWNER,`ta.style.height = ta.scrollHeight + 'px'` is an easy way to do that.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1431786951,SQL query should wrap on SQL interrupted screen, https://github.com/simonw/datasette/issues/1876#issuecomment-1298854321,https://api.github.com/repos/simonw/datasette/issues/1876,1298854321,IC_kwDOBm6k_c5NavGx,9599,simonw,2022-11-01T17:14:33Z,2022-11-01T17:14:33Z,OWNER,"I could use a `textarea` here (would need to figure out a neat pattern to expand it to fit the query): ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1431786951,SQL query should wrap on SQL interrupted screen, https://github.com/simonw/datasette/issues/1875#issuecomment-1314620086,https://api.github.com/repos/simonw/datasette/issues/1875,1314620086,IC_kwDOBm6k_c5OW4K2,9599,simonw,2022-11-15T01:09:56Z,2022-11-15T01:09:56Z,OWNER,"Rough initial prototype: ```diff diff --git a/datasette/views/table.py b/datasette/views/table.py index 8b987221..518ac578 100644 --- a/datasette/views/table.py +++ b/datasette/views/table.py @@ -1103,19 +1103,30 @@ class TableInsertView(BaseView): except json.JSONDecodeError as e: return _errors([""Invalid JSON: {}"".format(e)]) if not isinstance(data, dict): - return _errors([""JSON must be a dictionary""]) + return _errors([{""detail"": ""JSON must be a dictionary"", ""pointer"": ""#/""}]) keys = data.keys() # keys must contain ""row"" or ""rows"" if ""row"" not in keys and ""rows"" not in keys: return _errors(['JSON must have one or other of ""row"" or ""rows""']) rows = [] + was_single_row = False if ""row"" in keys: if ""rows"" in keys: - return _errors(['Cannot use ""row"" and ""rows"" at the same time']) + return _errors( + [ + { + ""detail"": 'Cannot use ""row"" and ""rows"" at the same time', + ""pointer"": ""#/row"", + } + ] + ) + was_single_row = True row = data[""row""] if not isinstance(row, dict): - return _errors(['""row"" must be a dictionary']) + return _errors( + [{""detail"": '""row"" must be a dictionary', ""pointer"": ""#/row""}] + ) rows = [row] data[""return""] = True else: @@ -1152,9 +1163,12 @@ class TableInsertView(BaseView): invalid_columns = set(row.keys()) - columns if invalid_columns: errors.append( - ""Row {} has invalid columns: {}"".format( - i, "", "".join(sorted(invalid_columns)) - ) + { + ""detail"": ""Invalid columns: {}"".format( + "", "".join(sorted(invalid_columns)) + ), + ""pointer"": ""#/blah/"", + } ) if errors: return _errors(errors) ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1430797211,Figure out design for JSON errors (consider RFC 7807), https://github.com/simonw/datasette/issues/1875#issuecomment-1314615592,https://api.github.com/repos/simonw/datasette/issues/1875,1314615592,IC_kwDOBm6k_c5OW3Eo,9599,simonw,2022-11-15T01:04:28Z,2022-11-15T01:04:28Z,OWNER,"Worth noting this bit in RFC 7807: > The fictional problem type here defines the ""errors"" extension, an > array that describes the details of each validation error. Each > member is an object containing ""detail"" to describe the issue, and > ""pointer"" to locate the problem within the request's content using a > JSON Pointer [JSON-POINTER]. So the list of `""errors""` with JSON Pointer isn't technically part of the spec, it's an imaginary extension. It fits what I need to do though, so I'm inclined to stick with it anyway.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1430797211,Figure out design for JSON errors (consider RFC 7807), https://github.com/simonw/datasette/issues/1875#issuecomment-1314545407,https://api.github.com/repos/simonw/datasette/issues/1875,1314545407,IC_kwDOBm6k_c5OWl7_,9599,simonw,2022-11-14T23:30:34Z,2022-11-14T23:30:34Z,OWNER,TIL: https://til.simonwillison.net/json/json-pointer,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1430797211,Figure out design for JSON errors (consider RFC 7807), https://github.com/simonw/datasette/issues/1875#issuecomment-1314491884,https://api.github.com/repos/simonw/datasette/issues/1875,1314491884,IC_kwDOBm6k_c5OWY3s,9599,simonw,2022-11-14T22:26:11Z,2022-11-14T22:26:54Z,OWNER,"Spec looks pretty simple: > A JSON Pointer is a Unicode string (see [RFC4627], Section 3) > containing a sequence of zero or more reference tokens, each prefixed > by a `/` (%x2F) character. > > Because the characters `~` (%x7E) and `/` (%x2F) have special > meanings in JSON Pointer, `~` needs to be encoded as `~0` and `/` > needs to be encoded as `~1` when these characters appear in a > reference token.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1430797211,Figure out design for JSON errors (consider RFC 7807), https://github.com/simonw/datasette/issues/1875#issuecomment-1314491150,https://api.github.com/repos/simonw/datasette/issues/1875,1314491150,IC_kwDOBm6k_c5OWYsO,9599,simonw,2022-11-14T22:25:20Z,2022-11-14T22:25:20Z,OWNER,"That's using JSON Pointer: https://www.rfc-editor.org/rfc/rfc6901 There's a Python library for that here https://github.com/stefankoegl/python-json-pointer/blob/master/jsonpointer.py - which looks simple and clean and well maintained and documented, but it only handles the ""what is at this pointer within this JSON object"" case - I need to generate the correct JSON pointer to explain where my error is. So I think I'll end up hand-rolling this.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1430797211,Figure out design for JSON errors (consider RFC 7807), https://github.com/simonw/datasette/issues/1875#issuecomment-1314488010,https://api.github.com/repos/simonw/datasette/issues/1875,1314488010,IC_kwDOBm6k_c5OWX7K,9599,simonw,2022-11-14T22:21:43Z,2022-11-14T22:21:43Z,OWNER,"Here's the most relevant example from the RFC spec: ``` POST /details HTTP/1.1 Host: account.example.com Accept: application/json ``` ```json { ""age"": 42.3, ""profile"": { ""color"": ""yellow"" } } ``` ``` HTTP/1.1 400 Bad Request Content-Type: application/problem+json Content-Language: en ``` ```json { ""type"": ""https://example.net/validation-error"", ""title"": ""Your request is not valid."", ""errors"": [ { ""detail"": ""must be a positive integer"", ""pointer"": ""#/age"" }, { ""detail"": ""must be 'green', 'red' or 'blue'"", ""pointer"": ""#/profile/color"" } ] } ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1430797211,Figure out design for JSON errors (consider RFC 7807), https://github.com/simonw/datasette/issues/1875#issuecomment-1313114283,https://api.github.com/repos/simonw/datasette/issues/1875,1313114283,IC_kwDOBm6k_c5ORIir,9599,simonw,2022-11-14T05:20:00Z,2022-11-14T05:20:00Z,OWNER,"I started a conversation about JSON error standards on Mastodon here: https://fedi.simonwillison.net/web/@simon/109338725610487457 Quite a few people pointed to this RFC independently.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1430797211,Figure out design for JSON errors (consider RFC 7807), https://github.com/simonw/sqlite-utils/pull/508#issuecomment-1297788531,https://api.github.com/repos/simonw/sqlite-utils/issues/508,1297788531,IC_kwDOCGYnMM5NWq5z,7908073,chapmanjacobd,2022-10-31T22:54:33Z,2022-11-17T15:11:16Z,CONTRIBUTOR,"Maybe this is actually a problem in the python sqlite bindings. Given [SQLITE's stance on this](https://www.sqlite.org/invalidutf.html) they should probably use `encode('utf-8', 'surrogatepass')`. As far as I understand the error here won't actually be resolved by this PR as-is. We would need to modify the data with `surrogateescape`... :/ or modify the sqlite3 module to use `surrogatepass`","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1430563092,Allow surrogates in parameters, https://github.com/simonw/sqlite-utils/pull/508#issuecomment-1297754631,https://api.github.com/repos/simonw/sqlite-utils/issues/508,1297754631,IC_kwDOCGYnMM5NWioH,22429695,codecov[bot],2022-10-31T22:14:48Z,2022-10-31T22:53:59Z,NONE,"# [Codecov](https://codecov.io/gh/simonw/sqlite-utils/pull/508?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) Report Base: **96.25**% // Head: **96.09**% // Decreases project coverage by **`-0.15%`** :warning: > Coverage data is based on head [(`2d6a149`)](https://codecov.io/gh/simonw/sqlite-utils/pull/508?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) compared to base [(`529110e`)](https://codecov.io/gh/simonw/sqlite-utils/commit/529110e7d8c4a6b1bbf5fb61f2e29d72aa95a611?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). > Patch coverage: 63.63% of modified lines in pull request are covered. > :exclamation: Current head 2d6a149 differs from pull request most recent head 43a8c4c. Consider uploading reports for the commit 43a8c4c to get more accurate results
      Additional details and impacted files ```diff @@ Coverage Diff @@ ## main #508 +/- ## ========================================== - Coverage 96.25% 96.09% -0.16% ========================================== Files 4 4 Lines 2401 2407 +6 ========================================== + Hits 2311 2313 +2 - Misses 90 94 +4 ``` | [Impacted Files](https://codecov.io/gh/simonw/sqlite-utils/pull/508?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) | Coverage Δ | | |---|---|---| | [sqlite\_utils/db.py](https://codecov.io/gh/simonw/sqlite-utils/pull/508/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-c3FsaXRlX3V0aWxzL2RiLnB5) | `96.79% <63.63%> (-0.30%)` | :arrow_down: | Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison)
      [:umbrella: View full report at Codecov](https://codecov.io/gh/simonw/sqlite-utils/pull/508?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). :loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1430563092,Allow surrogates in parameters, https://github.com/simonw/sqlite-utils/issues/507#issuecomment-1297859539,https://api.github.com/repos/simonw/sqlite-utils/issues/507,1297859539,IC_kwDOCGYnMM5NW8PT,7908073,chapmanjacobd,2022-11-01T00:40:16Z,2022-11-01T00:40:16Z,CONTRIBUTOR,"Ideally people could fix their data if they run into this issue. If you are using filenames try [convmv](https://linux.die.net/man/1/convmv) ``` convmv --preserve-mtimes -f utf8 -t utf8 --notest -i -r . ``` maybe this script will also help: ```py import argparse, shutil from pathlib import Path import ftfy from xklb import utils from xklb.utils import log def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument(""paths"", nargs='*') parser.add_argument(""--verbose"", ""-v"", action=""count"", default=0) args = parser.parse_args() log.info(utils.dict_filter_bool(args.__dict__)) return args def rename_invalid_paths() -> None: args = parse_args() for path in args.paths: log.info(path) for p in sorted([str(p) for p in Path(path).rglob(""*"")], key=len): fixed = ftfy.fix_text(p, uncurl_quotes=False).replace(""\r\n"", ""\n"").replace(""\r"", ""\n"").replace(""\n"", """") if p != fixed: try: shutil.move(p, fixed) except FileNotFoundError: log.warning(""FileNotFound. %s"", p) else: log.info(fixed) if __name__ == ""__main__"": rename_invalid_paths() ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1430325103,conn.execute: UnicodeEncodeError: 'utf-8' codec can't encode character, https://github.com/simonw/datasette/issues/1874#issuecomment-1313127054,https://api.github.com/repos/simonw/datasette/issues/1874,1313127054,IC_kwDOBm6k_c5ORLqO,9599,simonw,2022-11-14T05:45:00Z,2022-11-14T05:45:00Z,OWNER,"Demo: https://latest-1-0-dev.datasette.io/-/api#path=%2Ffixtures%2Ffacetable%2F-%2Fdrop&json=&method=POST ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1429030341,API to drop a table, https://github.com/simonw/datasette/issues/1874#issuecomment-1313125123,https://api.github.com/repos/simonw/datasette/issues/1874,1313125123,IC_kwDOBm6k_c5ORLMD,9599,simonw,2022-11-14T05:41:20Z,2022-11-14T05:42:23Z,OWNER,"I also changed the confirmation JSON returned by this endpoint to add the `database` and `table` like so: ```json { ""ok"": true, ""database"": ""data"", ""table"": ""docs"", ""row_count"": 1, ""message"": ""Pass \""confirm\"": true to confirm"" } ``` Updated docs: https://docs.datasette.io/en/1.0-dev/json_api.html#dropping-tables","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1429030341,API to drop a table, https://github.com/simonw/datasette/issues/1874#issuecomment-1313119558,https://api.github.com/repos/simonw/datasette/issues/1874,1313119558,IC_kwDOBm6k_c5ORJ1G,9599,simonw,2022-11-14T05:30:27Z,2022-11-14T05:30:27Z,OWNER,Found a bug: you get a 500 error if you try this against an immutable database.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1429030341,API to drop a table, https://github.com/simonw/datasette/issues/1874#issuecomment-1296363981,https://api.github.com/repos/simonw/datasette/issues/1874,1296363981,IC_kwDOBm6k_c5NRPHN,9599,simonw,2022-10-30T22:19:47Z,2022-10-30T22:19:47Z,OWNER,Documentation: https://docs.datasette.io/en/1.0-dev/json_api.html#dropping-tables,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1429030341,API to drop a table, https://github.com/simonw/sqlite-utils/issues/506#issuecomment-1298879701,https://api.github.com/repos/simonw/sqlite-utils/issues/506,1298879701,IC_kwDOCGYnMM5Na1TV,9599,simonw,2022-11-01T17:37:13Z,2022-11-01T17:37:13Z,OWNER,"The question I was originally trying to answer here was this: how many rows were actually inserted by that call to `.insert_all()`? I don't know that `.rowcount` would ever be useful here, since the ""correct"" answer depends on other factors - had I determined to ignore or replace records with a primary key that matches an existing record for example? So I think if people need `rowcount` they can get it by using a `cursor` directly.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1429029604,Make `cursor.rowcount` accessible (wontfix), https://github.com/simonw/sqlite-utils/issues/506#issuecomment-1298877872,https://api.github.com/repos/simonw/sqlite-utils/issues/506,1298877872,IC_kwDOCGYnMM5Na02w,9599,simonw,2022-11-01T17:35:30Z,2022-11-01T17:35:30Z,OWNER,"This may not make sense. First, `.last_rowid` is a property on table - but that doesn't make sense for `rowcount` since it should clearly be a property on the database itself (you can run a query directly using `db.execute()` without going through a `Table` object). So I tried this prototype: ```diff diff --git a/docs/python-api.rst b/docs/python-api.rst index 206e5e6..78d3a8d 100644 --- a/docs/python-api.rst +++ b/docs/python-api.rst @@ -186,6 +186,15 @@ The ``db.query(sql)`` function executes a SQL query and returns an iterator over # {'name': 'Cleo'} # {'name': 'Pancakes'} +After executing a query the ``db.rowcount`` property on that database instance will reflect the number of rows affected by any insert, update or delete operations performed by that query: + +.. code-block:: python + + db = Database(memory=True) + db[""dogs""].insert_all([{""name"": ""Cleo""}, {""name"": ""Pancakes""}]) + print(db.rowcount) + # Outputs: 2 + .. _python_api_execute: db.execute(sql, params) diff --git a/sqlite_utils/db.py b/sqlite_utils/db.py index a06f4b7..c19c2dd 100644 --- a/sqlite_utils/db.py +++ b/sqlite_utils/db.py @@ -294,6 +294,8 @@ class Database: _counts_table_name = ""_counts"" use_counts_table = False + # Number of rows inserted, updated or deleted + rowcount: Optional[int] = None def __init__( self, @@ -480,9 +482,11 @@ class Database: if self._tracer: self._tracer(sql, parameters) if parameters is not None: - return self.conn.execute(sql, parameters) + cursor = self.conn.execute(sql, parameters) else: - return self.conn.execute(sql) + cursor = self.conn.execute(sql) + self.rowcount = cursor.rowcount + return cursor def executescript(self, sql: str) -> sqlite3.Cursor: """""" ``` But this happens: ```pycon >>> from sqlite_utils import Database >>> db = Database(memory=True) >>> db[""dogs""].insert_all([{""name"": ""Cleo""}, {""name"": ""Pancakes""}])
      >>> db.rowcount -1 ``` Turning on query tracing demonstrates why: ```pycon >>> db = Database(memory=True, tracer=print) PRAGMA recursive_triggers=on; None >>> db[""dogs""].insert_all([{""name"": ""Cleo""}, {""name"": ""Pancakes""}]) select name from sqlite_master where type = 'view' None select name from sqlite_master where type = 'table' None select name from sqlite_master where type = 'view' None CREATE TABLE [dogs] ( [name] TEXT ); None select name from sqlite_master where type = 'view' None INSERT INTO [dogs] ([name]) VALUES (?), (?); ['Cleo', 'Pancakes'] select name from sqlite_master where type = 'table' None select name from sqlite_master where type = 'table' None PRAGMA table_info([dogs]) None
      >>> ``` The `.insert_all()` function does a bunch of other queries too, so `.rowcount` is quickly over-ridden by the same result from extra queries that it executed.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1429029604,Make `cursor.rowcount` accessible (wontfix), https://github.com/simonw/sqlite-utils/issues/506#issuecomment-1296358636,https://api.github.com/repos/simonw/sqlite-utils/issues/506,1296358636,IC_kwDOCGYnMM5NRNzs,9599,simonw,2022-10-30T21:52:11Z,2022-10-30T21:52:11Z,OWNER,This could work in a similar way to `db.insert(...).last_rowid`.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1429029604,Make `cursor.rowcount` accessible (wontfix), https://github.com/simonw/datasette/issues/1873#issuecomment-1298919552,https://api.github.com/repos/simonw/datasette/issues/1873,1298919552,IC_kwDOBm6k_c5Na_CA,9599,simonw,2022-11-01T18:11:27Z,2022-11-01T18:11:27Z,OWNER,"I forgot to document `ignore` and `replace`. Also I need to add tests that cover: - Forgetting to include a primary key on a non-autoincrement table - Compound primary keys - Rowid only tables with and without rowid specified I think my validation logic here will get caught out by the fact that `rowid` does not show up as a valid column name: https://github.com/simonw/datasette/blob/9bec7c38eb93cde5afb16df9bdd96aea2a5b0459/datasette/views/table.py#L1151-L1160 ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1428630253,Ensure insert API has good tests for rowid and compound primark key tables, https://github.com/simonw/datasette/issues/1873#issuecomment-1298905135,https://api.github.com/repos/simonw/datasette/issues/1873,1298905135,IC_kwDOBm6k_c5Na7gv,9599,simonw,2022-11-01T17:59:59Z,2022-11-01T17:59:59Z,OWNER,"It's a bit surprising that you can send `""ignore"": true, ""return_rows"": true` and the returned `""inserted""` key will list rows that were NOT inserted (since they were ignored). Three options: 1. Ignore that and document it 2. Fix it so `""inserted""` only returns rows that were actually inserted (bit tricky) 3. Change the name of `""inserted""` to something else I'm picking 3 - I'm going to change it to be called `""rows""` instead.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1428630253,Ensure insert API has good tests for rowid and compound primark key tables, https://github.com/simonw/datasette/issues/1873#issuecomment-1298885451,https://api.github.com/repos/simonw/datasette/issues/1873,1298885451,IC_kwDOBm6k_c5Na2tL,9599,simonw,2022-11-01T17:42:20Z,2022-11-01T17:42:20Z,OWNER,"Design decision: ```json { ""rows"": [{""id"": 1, ""title"": ""The title""}], ""ignore"": true } ``` Or `""replace"": true`.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1428630253,Ensure insert API has good tests for rowid and compound primark key tables, https://github.com/simonw/datasette/issues/1873#issuecomment-1296343716,https://api.github.com/repos/simonw/datasette/issues/1873,1296343716,IC_kwDOBm6k_c5NRKKk,9599,simonw,2022-10-30T20:24:55Z,2022-10-30T20:24:55Z,OWNER,"I think the key feature I need here is going to be the equivalent of `ignore=True` and `replace=True` for dealing with primary key collisions, see https://sqlite-utils.datasette.io/en/stable/reference.html#sqlite_utils.db.Table.insert","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1428630253,Ensure insert API has good tests for rowid and compound primark key tables, https://github.com/simonw/datasette/issues/1873#issuecomment-1296343317,https://api.github.com/repos/simonw/datasette/issues/1873,1296343317,IC_kwDOBm6k_c5NRKEV,9599,simonw,2022-10-30T20:22:40Z,2022-10-30T20:22:40Z,OWNER,"So maybe they're not actually worth worrying about separately, because they are guaranteed to have a primary key set.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1428630253,Ensure insert API has good tests for rowid and compound primark key tables, https://github.com/simonw/datasette/issues/1873#issuecomment-1296343173,https://api.github.com/repos/simonw/datasette/issues/1873,1296343173,IC_kwDOBm6k_c5NRKCF,9599,simonw,2022-10-30T20:21:54Z,2022-10-30T20:22:20Z,OWNER,"One last case to consider: `WITHOUT ROWID` tables. https://www.sqlite.org/withoutrowid.html > By default, every row in SQLite has a special column, usually called the ""[rowid](https://www.sqlite.org/lang_createtable.html#rowid)"", that uniquely identifies that row within the table. However if the phrase ""WITHOUT ROWID"" is added to the end of a [CREATE TABLE](https://www.sqlite.org/lang_createtable.html) statement, then the special ""rowid"" column is omitted. There are sometimes space and performance advantages to omitting the rowid. > > ... > > Every WITHOUT ROWID table must have a [PRIMARY KEY](https://www.sqlite.org/lang_createtable.html#primkeyconst). An error is raised if a CREATE TABLE statement with the WITHOUT ROWID clause lacks a PRIMARY KEY.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1428630253,Ensure insert API has good tests for rowid and compound primark key tables, https://github.com/simonw/datasette/issues/1873#issuecomment-1296343014,https://api.github.com/repos/simonw/datasette/issues/1873,1296343014,IC_kwDOBm6k_c5NRJ_m,9599,simonw,2022-10-30T20:21:01Z,2022-10-30T20:21:01Z,OWNER,"Actually, for simplicity I'm going to say that you can always set the primary key, even for auto-incrementing primary key columns... but you cannot set it on pure `rowid` columns.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1428630253,Ensure insert API has good tests for rowid and compound primark key tables, https://github.com/simonw/datasette/issues/1873#issuecomment-1296342814,https://api.github.com/repos/simonw/datasette/issues/1873,1296342814,IC_kwDOBm6k_c5NRJ8e,9599,simonw,2022-10-30T20:20:05Z,2022-10-30T20:20:05Z,OWNER,"Some notes on what Datasette does already https://latest.datasette.io/fixtures/tags.json?_shape=array returns: ```json [ { ""tag"": ""canine"" }, { ""tag"": ""feline"" } ] ``` That table is defined [like this](https://latest.datasette.io/fixtures/tags): ```sql CREATE TABLE tags ( tag TEXT PRIMARY KEY ); ``` Here's a `rowid` table with no explicit primary key: https://latest.datasette.io/fixtures/binary_data https://latest.datasette.io/fixtures/binary_data.json?_shape=array ```json [ { ""rowid"": 1, ""data"": { ""$base64"": true, ""encoded"": ""FRwCx60F/g=="" } }, { ""rowid"": 2, ""data"": { ""$base64"": true, ""encoded"": ""FRwDx60F/g=="" } }, { ""rowid"": 3, ""data"": null } ] ``` ```sql CREATE TABLE binary_data ( data BLOB ); ``` https://latest.datasette.io/fixtures/simple_primary_key has a text primary key: https://latest.datasette.io/fixtures/simple_primary_key.json?_shape=array ```json [ { ""id"": ""1"", ""content"": ""hello"" }, { ""id"": ""2"", ""content"": ""world"" }, { ""id"": ""3"", ""content"": """" }, { ""id"": ""4"", ""content"": ""RENDER_CELL_DEMO"" }, { ""id"": ""5"", ""content"": ""RENDER_CELL_ASYNC"" } ] ``` ```sql CREATE TABLE simple_primary_key ( id varchar(30) primary key, content text ); ``` https://latest.datasette.io/fixtures/compound_primary_key is a compound primary key. https://latest.datasette.io/fixtures/compound_primary_key.json?_shape=array ```json [ { ""pk1"": ""a"", ""pk2"": ""b"", ""content"": ""c"" }, { ""pk1"": ""a/b"", ""pk2"": "".c-d"", ""content"": ""c"" } ] ``` ```sql CREATE TABLE compound_primary_key ( pk1 varchar(30), pk2 varchar(30), content text, PRIMARY KEY (pk1, pk2) ); ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1428630253,Ensure insert API has good tests for rowid and compound primark key tables, https://github.com/simonw/datasette/issues/1873#issuecomment-1296341469,https://api.github.com/repos/simonw/datasette/issues/1873,1296341469,IC_kwDOBm6k_c5NRJnd,9599,simonw,2022-10-30T20:13:50Z,2022-10-30T20:13:50Z,OWNER,"I checked and SQLite itself does allow you to set the `rowid` on that kind of table - it then increments from whatever you inserted: ``` % sqlite3 /tmp/t.db SQLite version 3.39.4 2022-09-07 20:51:41 Enter "".help"" for usage hints. sqlite> create table docs (title text); sqlite> insert into docs (title) values ('one'); sqlite> select rowid, title from docs; 1|one sqlite> insert into docs (rowid, title) values (3, 'three'); sqlite> select rowid, title from docs; 1|one 3|three sqlite> insert into docs (title) values ('another'); sqlite> select rowid, title from docs; 1|one 3|three 4|another ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1428630253,Ensure insert API has good tests for rowid and compound primark key tables, https://github.com/simonw/datasette/issues/1873#issuecomment-1296341055,https://api.github.com/repos/simonw/datasette/issues/1873,1296341055,IC_kwDOBm6k_c5NRJg_,9599,simonw,2022-10-30T20:11:47Z,2022-10-30T20:12:30Z,OWNER,"If a table has an auto-incrementing primary key, should you be allowed to insert records with an explicit key into it? I'm torn on this one. It's something you can do with direct database access, but it's something I very rarely want to do. I'm inclined to disallow it and say that if you want that you can get it using a writable canned query instead. Likewise, I'm not going to provide a way to set the `rowid` explicitly on a freshly inserted row.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1428630253,Ensure insert API has good tests for rowid and compound primark key tables, https://github.com/simonw/datasette/issues/1873#issuecomment-1296131681,https://api.github.com/repos/simonw/datasette/issues/1873,1296131681,IC_kwDOBm6k_c5NQWZh,9599,simonw,2022-10-30T06:27:12Z,2022-10-30T06:27:12Z,OWNER,Relevant TODO: https://github.com/simonw/datasette/blob/c35859ae3df163406f1a1895ccf9803e933b2d8e/datasette/views/table.py#L1131-L1135,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1428630253,Ensure insert API has good tests for rowid and compound primark key tables, https://github.com/simonw/datasette/issues/1872#issuecomment-1296131343,https://api.github.com/repos/simonw/datasette/issues/1872,1296131343,IC_kwDOBm6k_c5NQWUP,9599,simonw,2022-10-30T06:26:01Z,2022-10-30T06:26:01Z,OWNER,"Good spot fixing that! Sorry about this - it was a change in Datasette 0.63 which should have been better called out. My goal for Datasette 1.0 (which I aim to have out by the end of the year) is to introduce a formal process for avoiding problems like this, with very clear documentation when something like this might happen.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1428560020,"SITE-BUSTING ERROR: ""render_template() called before await ds.invoke_startup()""", https://github.com/simonw/datasette/issues/1872#issuecomment-1296080804,https://api.github.com/repos/simonw/datasette/issues/1872,1296080804,IC_kwDOBm6k_c5NQJ-k,192568,mroswell,2022-10-30T03:06:32Z,2022-10-30T03:06:32Z,CONTRIBUTOR,"I updated datasette-publish-vercel to 0.14.2 in requirements.txt And the site is back up! Is there a way that we can get some sort of notice when something like this will have critical impact on website function?","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1428560020,"SITE-BUSTING ERROR: ""render_template() called before await ds.invoke_startup()""", https://github.com/simonw/datasette/issues/1872#issuecomment-1296076803,https://api.github.com/repos/simonw/datasette/issues/1872,1296076803,IC_kwDOBm6k_c5NQJAD,192568,mroswell,2022-10-30T02:50:34Z,2022-10-30T02:50:34Z,CONTRIBUTOR,"should this issue be under https://github.com/simonw/datasette-publish-vercel/issues ? Perhaps I just need to update: datasette-publish-vercel==0.11 in requirements.txt? I'll try that and see what happens... ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1428560020,"SITE-BUSTING ERROR: ""render_template() called before await ds.invoke_startup()""", https://github.com/simonw/datasette/issues/1871#issuecomment-1313125870,https://api.github.com/repos/simonw/datasette/issues/1871,1313125870,IC_kwDOBm6k_c5ORLXu,9599,simonw,2022-11-14T05:42:50Z,2022-11-14T05:42:50Z,OWNER,Demo: https://latest-1-0-dev.datasette.io/-/api#path=%2Ffixtures%2Ffacetable%2F-%2Fdrop&json=%7B%22confirm%22%3A+true%7D&method=POST,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1427293909,API explorer tool, https://github.com/simonw/datasette/issues/1871#issuecomment-1313097057,https://api.github.com/repos/simonw/datasette/issues/1871,1313097057,IC_kwDOBm6k_c5OREVh,9599,simonw,2022-11-14T04:59:28Z,2022-11-14T04:59:28Z,OWNER,In playing with the API explorer just now I realized it's way too easy to accidentally drop a table using it.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1427293909,API explorer tool, https://github.com/simonw/datasette/issues/1871#issuecomment-1313072900,https://api.github.com/repos/simonw/datasette/issues/1871,1313072900,IC_kwDOBm6k_c5OQ-cE,9599,simonw,2022-11-14T04:15:50Z,2022-11-14T04:15:50Z,OWNER,"For the example links - I'm going to have these at the bottom of the page so you don't have to scroll past them. Ideally these would take the user's permissions into account. This could make the page expensive to load, but I'm going to risk it for the moment. Something like this then: > - data > - /data/-/create - create table > - /data/table1/-/insert - insert into table1 > - /data/table1/-/drop - drop table1 I won't bother with per-row demo links (for update and delete) because there could be thousands of them for each table.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1427293909,API explorer tool, https://github.com/simonw/datasette/issues/1871#issuecomment-1313062699,https://api.github.com/repos/simonw/datasette/issues/1871,1313062699,IC_kwDOBm6k_c5OQ78r,9599,simonw,2022-11-14T04:03:29Z,2022-11-14T04:12:41Z,OWNER,"Two things left before I close this issue: - [x] I want to preserve the state of the forms in the URL - probably after a `#` - [ ] Instead of hard-coding the current examples, I want to provide a list of links which populate the forms","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1427293909,API explorer tool, https://github.com/simonw/datasette/issues/1871#issuecomment-1312822353,https://api.github.com/repos/simonw/datasette/issues/1871,1312822353,IC_kwDOBm6k_c5OQBRR,9599,simonw,2022-11-13T21:07:40Z,2022-11-13T21:07:40Z,OWNER,I'm going to need extra code to toggle POST closed when GET opens and vice-versa.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1427293909,API explorer tool, https://github.com/simonw/datasette/issues/1871#issuecomment-1312821031,https://api.github.com/repos/simonw/datasette/issues/1871,1312821031,IC_kwDOBm6k_c5OQA8n,9599,simonw,2022-11-13T21:02:06Z,2022-11-13T21:03:11Z,OWNER,"Actually no, I'm going to add a class of `details-menu` to the other details elements that SHOULD be closed. That way custom templates using `
      ` won't close in a surprising way.","{""total_count"": 1, ""+1"": 1, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1427293909,API explorer tool, https://github.com/simonw/datasette/issues/1871#issuecomment-1312816451,https://api.github.com/repos/simonw/datasette/issues/1871,1312816451,IC_kwDOBm6k_c5OP_1D,9599,simonw,2022-11-13T20:39:26Z,2022-11-13T20:39:34Z,OWNER,I'm going to add a special `no-auto-close` class to these and teach that code not to close them.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1427293909,API explorer tool, https://github.com/simonw/datasette/issues/1871#issuecomment-1312816292,https://api.github.com/repos/simonw/datasette/issues/1871,1312816292,IC_kwDOBm6k_c5OP_yk,9599,simonw,2022-11-13T20:38:42Z,2022-11-13T20:38:42Z,OWNER,"The current API explorer uses details/summary elements for the GET and POST dialogs. I only want one of these to be open at a time, to reflect that you can make either a GET or a POST. I just noticed that clicking anywhere else on the page closes both elements, which isn't what I want to happen. Turns out that's because of this code I added as part of Datasette's menu implementation! https://github.com/simonw/datasette/blob/9f54f00a50a4d950cfd69a0ff3526ae82c858826/datasette/templates/_close_open_menus.html#L2-L15","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1427293909,API explorer tool, https://github.com/simonw/datasette/issues/1871#issuecomment-1309650806,https://api.github.com/repos/simonw/datasette/issues/1871,1309650806,IC_kwDOBm6k_c5OD692,3556,davidbgk,2022-11-10T01:38:58Z,2022-11-10T01:38:58Z,CONTRIBUTOR,"> Realized the API explorer doesn't need the API key piece at all - it can work with standard cookie-based auth. > > This also reflects how most plugins are likely to use this API, where they'll be adding JavaScript that uses `fetch()` to call the write API directly. I agree (that's what I did with the previous insert plugin), maybe a complete example using `fetch()` in the documentation would be valuable as a “Getting started with the API” or similar?","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1427293909,API explorer tool, https://github.com/simonw/datasette/issues/1871#issuecomment-1302815105,https://api.github.com/repos/simonw/datasette/issues/1871,1302815105,IC_kwDOBm6k_c5Np2GB,9599,simonw,2022-11-04T00:17:23Z,2022-11-04T00:17:23Z,OWNER,"I'll probably enhance it a bit more though, I want to provide a UI that lists all the tables you can explore and lets you click to pre-fill the forms with them. Though at that point what should I do about the other endpoints? Probably list those too. Gets a bit complex, especially with the row-level update and delete endpoints.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1427293909,API explorer tool, https://github.com/simonw/datasette/issues/1871#issuecomment-1302814693,https://api.github.com/repos/simonw/datasette/issues/1871,1302814693,IC_kwDOBm6k_c5Np1_l,9599,simonw,2022-11-04T00:16:36Z,2022-11-04T00:16:36Z,OWNER,"I can close this issue once I fix it so it no longer hard-codes a potentially invalid example endpoint: https://github.com/simonw/datasette/blob/bcc781f4c50a8870e3389c4e60acb625c34b0317/datasette/templates/api_explorer.html#L24-L26 https://github.com/simonw/datasette/blob/bcc781f4c50a8870e3389c4e60acb625c34b0317/datasette/templates/api_explorer.html#L34-L35","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1427293909,API explorer tool, https://github.com/simonw/datasette/issues/1871#issuecomment-1299607082,https://api.github.com/repos/simonw/datasette/issues/1871,1299607082,IC_kwDOBm6k_c5Ndm4q,9599,simonw,2022-11-02T05:45:31Z,2022-11-02T05:45:31Z,OWNER,"I'm going to add a link to the Datasette API docs for the current running version of Datasette, e.g. to https://docs.datasette.io/en/0.63/json_api.html","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1427293909,API explorer tool, https://github.com/simonw/datasette/issues/1871#issuecomment-1299600257,https://api.github.com/repos/simonw/datasette/issues/1871,1299600257,IC_kwDOBm6k_c5NdlOB,9599,simonw,2022-11-02T05:36:40Z,2022-11-02T05:36:40Z,OWNER,"The API Explorer should definitely link to the `/-/create-token` page for users who have permission though. And it should probably go in the Datasette application menu?","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1427293909,API explorer tool, https://github.com/simonw/datasette/issues/1871#issuecomment-1299599461,https://api.github.com/repos/simonw/datasette/issues/1871,1299599461,IC_kwDOBm6k_c5NdlBl,9599,simonw,2022-11-02T05:35:36Z,2022-11-02T05:36:15Z,OWNER,"Here's a slightly wild idea: what if there was a button on `/-/api` that you could click to turn on ""API explorer mode"" for the rest of the Datasette interface - which sets a cookie, and that cookie means you then see ""API explorer"" links in all sorts of other relevant places in the Datasette UI (maybe tucked away in cog menus). Only reason I don't want to show these to everyone is that I don't think this is a very user-friendly feature: if you don't know what an API is I don't want to expose you to it unnecessarily.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1427293909,API explorer tool, https://github.com/simonw/datasette/issues/1871#issuecomment-1299598570,https://api.github.com/repos/simonw/datasette/issues/1871,1299598570,IC_kwDOBm6k_c5Ndkzq,9599,simonw,2022-11-02T05:34:28Z,2022-11-02T05:34:28Z,OWNER,"This is pretty useful now. Two features I still want to add: - The ability to link to the API explorer such that the form is pre-filled with material from the URL. Need to guard against clickjacking first though, so no-one can link to it in an invisible iframe and trick the user into hitting POST. - Some kind of list of endpoints so people can click links to start using the API explorer. A list of every table the user can write to with each of their `/db/table/-/insert` endpoints for example.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1427293909,API explorer tool, https://github.com/simonw/datasette/issues/1871#issuecomment-1299597066,https://api.github.com/repos/simonw/datasette/issues/1871,1299597066,IC_kwDOBm6k_c5NdkcK,9599,simonw,2022-11-02T05:32:22Z,2022-11-02T05:32:22Z,OWNER,"Demo of the latest API explorer: ![explorer](https://user-images.githubusercontent.com/9599/199406184-1292df42-25ea-4daf-8b54-ca26170ec1ea.gif) ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1427293909,API explorer tool, https://github.com/simonw/datasette/issues/1871#issuecomment-1299388341,https://api.github.com/repos/simonw/datasette/issues/1871,1299388341,IC_kwDOBm6k_c5Ncxe1,9599,simonw,2022-11-02T00:24:28Z,2022-11-02T00:25:00Z,OWNER,"I want JSON syntax highlighting. https://github.com/luyilin/json-format-highlight is an MIT licensed tiny highlighter that looks decent for this. https://unpkg.com/json-format-highlight@1.0.1/dist/json-format-highlight.js","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1427293909,API explorer tool, https://github.com/simonw/datasette/issues/1871#issuecomment-1299349741,https://api.github.com/repos/simonw/datasette/issues/1871,1299349741,IC_kwDOBm6k_c5NcoDt,9599,simonw,2022-11-01T23:22:55Z,2022-11-01T23:22:55Z,OWNER,"It's weird that the API explorer only lets you explore POST APIs. It should probably also let you explore GET APIs, or be renamed.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1427293909,API explorer tool, https://github.com/simonw/datasette/issues/1871#issuecomment-1296339386,https://api.github.com/repos/simonw/datasette/issues/1871,1296339386,IC_kwDOBm6k_c5NRJG6,9599,simonw,2022-10-30T20:03:04Z,2022-10-30T20:03:04Z,OWNER,"I do need to skip CSRF for these API calls. I'm going to start out by doing that using the `skip_csrf()` hook to skip CSRF checks on anything with a `content-type: application/json` request header. ```python @hookimpl def skip_csrf(scope): if scope[""type""] == ""http"": headers = scope.get(""headers"") if dict(headers).get(b'content-type') == b'application/json': return True ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1427293909,API explorer tool, https://github.com/simonw/datasette/issues/1871#issuecomment-1296339205,https://api.github.com/repos/simonw/datasette/issues/1871,1296339205,IC_kwDOBm6k_c5NRJEF,9599,simonw,2022-10-30T20:02:05Z,2022-10-30T20:02:05Z,OWNER,"Realized the API explorer doesn't need the API key piece at all - it can work with standard cookie-based auth. This also reflects how most plugins are likely to use this API, where they'll be adding JavaScript that uses `fetch()` to call the write API directly.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1427293909,API explorer tool, https://github.com/simonw/datasette/issues/1871#issuecomment-1296131872,https://api.github.com/repos/simonw/datasette/issues/1871,1296131872,IC_kwDOBm6k_c5NQWcg,9599,simonw,2022-10-30T06:27:56Z,2022-10-30T06:27:56Z,OWNER,Initial prototype API explorer is now live at https://latest-1-0-dev.datasette.io/-/api,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1427293909,API explorer tool, https://github.com/simonw/datasette/issues/1871#issuecomment-1296130073,https://api.github.com/repos/simonw/datasette/issues/1871,1296130073,IC_kwDOBm6k_c5NQWAZ,9599,simonw,2022-10-30T06:20:56Z,2022-10-30T06:20:56Z,OWNER,"That initial prototype looks like this: It currently shows the returned JSON from the API in an `alert()`. Next I should make that part of the page instead.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1427293909,API explorer tool, https://github.com/simonw/datasette/issues/1871#issuecomment-1296126389,https://api.github.com/repos/simonw/datasette/issues/1871,1296126389,IC_kwDOBm6k_c5NQVG1,9599,simonw,2022-10-30T06:04:48Z,2022-10-30T06:04:48Z,OWNER,"This is even more important now I have pushed: - #1866","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1427293909,API explorer tool, https://github.com/simonw/datasette/issues/1871#issuecomment-1296114136,https://api.github.com/repos/simonw/datasette/issues/1871,1296114136,IC_kwDOBm6k_c5NQSHY,9599,simonw,2022-10-30T05:15:40Z,2022-10-30T05:15:40Z,OWNER,"Host it at `/-/api` It's an input box with a path in and a textarea you can put JSON in, plus a submit button to post the request. It lists the API endpoints you can use - click on a link to populate the form field plus a example. ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1427293909,API explorer tool, https://github.com/simonw/datasette/pull/1870#issuecomment-1295667649,https://api.github.com/repos/simonw/datasette/issues/1870,1295667649,IC_kwDOBm6k_c5NOlHB,536941,fgregg,2022-10-29T00:52:43Z,2022-10-29T00:53:43Z,CONTRIBUTOR,"> Are you saying that I can build a container, but then when I run it and it does `datasette serve -i data.db ...` it will somehow modify the image, or create a new modified filesystem layer in the runtime environment, as a result of running that `serve` command? Somehow, `datasette serve -i data.db` will lead to the `data.db` being modified, which will trigger a [copy-on-write](https://docs.docker.com/storage/storagedriver/#the-copy-on-write-cow-strategy) of `data.db` into the read-write layer of the container. I don't understand **how** that happens. it kind of feels like a bug in sqlite, but i can't quite follow the sqlite code.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1426379903,"don't use immutable=1, only mode=ro", https://github.com/simonw/datasette/pull/1870#issuecomment-1295660092,https://api.github.com/repos/simonw/datasette/issues/1870,1295660092,IC_kwDOBm6k_c5NOjQ8,9599,simonw,2022-10-29T00:25:26Z,2022-10-29T00:25:26Z,OWNER,"Saw your comment here too: https://github.com/simonw/datasette/issues/1480#issuecomment-1271101072 > switching from `immutable=1` to `mode=ro` completely addressed this. see https://github.com/simonw/datasette/issues/1836#issuecomment-1271100651 for details. So maybe we need a special case for containers that are intended to be run using Docker - the ones produced by `datasette package` and `datasette publish cloudrun`? Those are cases where the `-i` option should actually be opened in read-only mode, not immutable mode. Maybe a `datasette serve --irw data.db` option for opening a file in immutable-but-actually-read-only mode? Bit ugly though. I should run some benchmarks to figure out if `immutable` really does offer significant performance benefits.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1426379903,"don't use immutable=1, only mode=ro", https://github.com/simonw/datasette/pull/1870#issuecomment-1295657771,https://api.github.com/repos/simonw/datasette/issues/1870,1295657771,IC_kwDOBm6k_c5NOisr,9599,simonw,2022-10-29T00:19:03Z,2022-10-29T00:19:03Z,OWNER,"Just saw your comment here: https://github.com/simonw/datasette/issues/1836#issuecomment-1272357976 > when you are running from docker, you **always** will want to run as `mode=ro` because the same thing that is causing duplication in the inspect layer will cause duplication in the final container read/write layer when `datasette serve` runs. I don't understand this. My mental model of how Docker works is that the image itself is created using `docker build`... but then when the image runs later on (`docker run`) the image itself isn't touched at all. Are you saying that I can build a container, but then when I run it and it does `datasette serve -i data.db ...` it will somehow modify the image, or create a new modified filesystem layer in the runtime environment, as a result of running that `serve` command?","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1426379903,"don't use immutable=1, only mode=ro", https://github.com/simonw/datasette/pull/1870#issuecomment-1294285471,https://api.github.com/repos/simonw/datasette/issues/1870,1294285471,IC_kwDOBm6k_c5NJTqf,536941,fgregg,2022-10-28T01:06:03Z,2022-10-28T01:06:03Z,CONTRIBUTOR,"as far as i can tell, [this is where the ""immutable"" argument is used](https://github.com/sqlite/sqlite/blob/c97bb14fab566f6fa8d967c8fd1e90f3702d5b73/src/pager.c#L4926-L4931) in sqlite: ```c pPager->noLock = sqlite3_uri_boolean(pPager->zFilename, ""nolock"", 0); if( (iDc & SQLITE_IOCAP_IMMUTABLE)!=0 || sqlite3_uri_boolean(pPager->zFilename, ""immutable"", 0) ){ vfsFlags |= SQLITE_OPEN_READONLY; goto act_like_temp_file; } ``` so it does set the read only flag, but then has a goto.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1426379903,"don't use immutable=1, only mode=ro", https://github.com/simonw/datasette/pull/1870#issuecomment-1294238862,https://api.github.com/repos/simonw/datasette/issues/1870,1294238862,IC_kwDOBm6k_c5NJISO,22429695,codecov[bot],2022-10-27T23:44:25Z,2022-10-27T23:44:25Z,NONE,"# [Codecov](https://codecov.io/gh/simonw/datasette/pull/1870?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) Report Base: **92.55**% // Head: **92.55**% // No change to project coverage :thumbsup: > Coverage data is based on head [(`4faa4fd`)](https://codecov.io/gh/simonw/datasette/pull/1870?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) compared to base [(`bf00b0b`)](https://codecov.io/gh/simonw/datasette/commit/bf00b0b59b6692bdec597ac9db4e0b497c5a47b4?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). > Patch has no changes to coverable lines.
      Additional details and impacted files ```diff @@ Coverage Diff @@ ## main #1870 +/- ## ======================================= Coverage 92.55% 92.55% ======================================= Files 35 35 Lines 4432 4432 ======================================= Hits 4102 4102 Misses 330 330 ``` | [Impacted Files](https://codecov.io/gh/simonw/datasette/pull/1870?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) | Coverage Δ | | |---|---|---| | [datasette/app.py](https://codecov.io/gh/simonw/datasette/pull/1870/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-ZGF0YXNldHRlL2FwcC5weQ==) | `94.30% <ø> (ø)` | | Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison)
      [:umbrella: View full report at Codecov](https://codecov.io/gh/simonw/datasette/pull/1870?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). :loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1426379903,"don't use immutable=1, only mode=ro", https://github.com/simonw/datasette/pull/1870#issuecomment-1294237783,https://api.github.com/repos/simonw/datasette/issues/1870,1294237783,IC_kwDOBm6k_c5NJIBX,536941,fgregg,2022-10-27T23:42:18Z,2022-10-27T23:42:18Z,CONTRIBUTOR,Relevant sqlite forum thread: https://www.sqlite.org/forum/forumpost/02f7bda329f41e30451472421cf9ce7f715b768ce3db02797db1768e47950d48,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1426379903,"don't use immutable=1, only mode=ro", https://github.com/simonw/datasette/issues/1869#issuecomment-1294181485,https://api.github.com/repos/simonw/datasette/issues/1869,1294181485,IC_kwDOBm6k_c5NI6Rt,9599,simonw,2022-10-27T22:24:37Z,2022-10-27T22:24:37Z,OWNER,"https://docs.datasette.io/en/stable/changelog.html#v0-63 Annotated release notes: https://simonwillison.net/2022/Oct/27/datasette-0-63/","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1426253476,Release 0.63, https://github.com/simonw/datasette/issues/1869#issuecomment-1294105558,https://api.github.com/repos/simonw/datasette/issues/1869,1294105558,IC_kwDOBm6k_c5NInvW,9599,simonw,2022-10-27T21:44:13Z,2022-10-27T21:44:13Z,OWNER,I'm going to do annotated release notes for this one.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1426253476,Release 0.63,