{"html_url": "https://github.com/simonw/datasette/issues/2104#issuecomment-1641082395", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2104", "id": 1641082395, "node_id": "IC_kwDOBm6k_c5h0O4b", "user": {"value": 15178711, "label": "asg017"}, "created_at": "2023-07-18T22:41:37Z", "updated_at": "2023-07-18T22:41:37Z", "author_association": "CONTRIBUTOR", "body": "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", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1808215339, "label": "Tables starting with an underscore should be treated as hidden"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2104#issuecomment-1638552567", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2104", "id": 1638552567, "node_id": "IC_kwDOBm6k_c5hqlP3", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-07-17T17:14:20Z", "updated_at": "2023-07-17T17:14:20Z", "author_association": "OWNER", "body": "Relevant code: https://github.com/simonw/datasette/blob/0f7192b6154edb576c41b55bd3f2a3f53e5f436a/datasette/database.py#L391-L451", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1808215339, "label": "Tables starting with an underscore should be treated as hidden"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2102#issuecomment-1640064620", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2102", "id": 1640064620, "node_id": "IC_kwDOBm6k_c5hwWZs", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-07-18T11:47:21Z", "updated_at": "2023-07-18T11:47:21Z", "author_association": "OWNER", "body": "I think I've figured out the problem here.\r\n\r\nThe question being asked is \"can this actor access this resource, which is within this database within this instance\".\r\n\r\nThe 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.\r\n\r\nBut 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.\r\n\r\nSo 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.\r\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1805076818, "label": "API tokens with view-table but not view-database/view-instance cannot access the table"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2102#issuecomment-1638567228", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2102", "id": 1638567228, "node_id": "IC_kwDOBm6k_c5hqo08", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-07-17T17:24:19Z", "updated_at": "2023-07-17T17:25:12Z", "author_association": "OWNER", "body": "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:\r\n```json\r\n{\r\n \"_r\": {\r\n \"r\": {\r\n \"fixtures\": {\r\n \"sortable\": [\r\n \"vt\"\r\n ]\r\n }\r\n }\r\n },\r\n \"a\": \"root\",\r\n \"d\": 3600,\r\n \"t\": 1689614483\r\n}\r\n```\r\nRun like this:\r\n```\r\ncurl -H 'Authorization: Bearer dstok_eyJhIjoicm9vdCIsInQiOjE2ODk2MTQ0ODMsImQiOjM2MDAsIl9yIjp7InIiOnsiZml4dHVyZXMiOnsic29ydGFibGUiOlsidnQiXX19fX0.n-VGxxawz1Q0WK7sqLfhXUgcvY0' \\\r\n https://latest.datasette.io/fixtures/sortable.json\r\n```\r\nReturned an HTML Forbidden page:\r\n```html\r\n\r\n\r\n\r\n Forbidden\r\n \r\n...\r\n```\r\nSame token againts `/-/actor.json` returns:\r\n```json\r\n{\r\n \"actor\": {\r\n \"id\": \"root\",\r\n \"token\": \"dstok\",\r\n \"_r\": {\r\n \"r\": {\r\n \"fixtures\": {\r\n \"sortable\": [\r\n \"vt\"\r\n ]\r\n }\r\n }\r\n },\r\n \"token_expires\": 1689618083\r\n }\r\n}\r\n```\r\nReminder - `\"_r\"` means restrict, `\"r\"` means resource.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1805076818, "label": "API tokens with view-table but not view-database/view-instance cannot access the table"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2102#issuecomment-1636093730", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2102", "id": 1636093730, "node_id": "IC_kwDOBm6k_c5hhM8i", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-07-14T16:26:27Z", "updated_at": "2023-07-14T16:32:49Z", "author_association": "OWNER", "body": "Here's that crucial comment:\r\n\r\n> If _r is defined then we use those to further restrict the actor.\r\n>\r\n>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\r\n\r\nSo that's why I implemented it like this.\r\n\r\nThe 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.\r\n\r\nSo 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`.\r\n\r\nI'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?\r\n\r\nMaybe 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.\r\n\r\nAlso, do `Permission` instances have enough information to implement this kind of cascade without hard-coding anything? \r\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1805076818, "label": "API tokens with view-table but not view-database/view-instance cannot access the table"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2102#issuecomment-1636053060", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2102", "id": 1636053060, "node_id": "IC_kwDOBm6k_c5hhDBE", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-07-14T15:51:36Z", "updated_at": "2023-07-14T16:14:05Z", "author_association": "OWNER", "body": "This might only be an issue with the code that checks `_r` on actors.\r\n\r\nhttps://github.com/simonw/datasette/blob/0f7192b6154edb576c41b55bd3f2a3f53e5f436a/datasette/default_permissions.py#L185-L222\r\n\r\nAdded in https://github.com/simonw/datasette/commit/bcc781f4c50a8870e3389c4e60acb625c34b0317 - refs:\r\n\r\n- #1855 ", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1805076818, "label": "API tokens with view-table but not view-database/view-instance cannot access the table"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2102#issuecomment-1636042066", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2102", "id": 1636042066, "node_id": "IC_kwDOBm6k_c5hhAVS", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-07-14T15:41:54Z", "updated_at": "2023-07-14T15:42:32Z", "author_association": "OWNER", "body": "I tried some code spelunking and came across https://github.com/simonw/datasette/commit/d6e03b04302a0852e7133dc030eab50177c37be7 which says:\r\n\r\n> - If you have table permission but not database permission you can now view the table page\r\n\r\nRefs:\r\n- #832 \r\n\r\nWhich suggests that my initial design decision wasn't what appears to be implemented today.\r\n\r\nNeeds more investigation.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1805076818, "label": "API tokens with view-table but not view-database/view-instance cannot access the table"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2102#issuecomment-1636040164", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2102", "id": 1636040164, "node_id": "IC_kwDOBm6k_c5hg_3k", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-07-14T15:40:21Z", "updated_at": "2023-07-14T15:40:21Z", "author_association": "OWNER", "body": "Relevant code: \r\nhttps://github.com/simonw/datasette/blob/0f7192b6154edb576c41b55bd3f2a3f53e5f436a/datasette/app.py#L822-L855", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1805076818, "label": "API tokens with view-table but not view-database/view-instance cannot access the table"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2102#issuecomment-1636036312", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2102", "id": 1636036312, "node_id": "IC_kwDOBm6k_c5hg-7Y", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-07-14T15:37:14Z", "updated_at": "2023-07-14T15:37:14Z", "author_association": "OWNER", "body": "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.\r\n\r\nBut 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.\r\n\r\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1805076818, "label": "API tokens with view-table but not view-database/view-instance cannot access the table"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2101#issuecomment-1634443907", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2101", "id": 1634443907, "node_id": "IC_kwDOBm6k_c5ha6KD", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-07-13T15:24:17Z", "updated_at": "2023-07-13T15:24:17Z", "author_association": "OWNER", "body": "https://github.com/simonw/datasette/blob/0f7192b6154edb576c41b55bd3f2a3f53e5f436a/datasette/views/table.py#L486-L506", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1803264272, "label": "alter: true support for JSON write API"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/567#issuecomment-1638926655", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/567", "id": 1638926655, "node_id": "IC_kwDOCGYnMM5hsAk_", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-07-17T21:42:37Z", "updated_at": "2023-07-17T21:42:37Z", "author_association": "OWNER", "body": "I really like this. I'm also interested in:\r\n- 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\r\n- 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\r\n\r\nThe upload-data-to-Datasette problem is planned to be solved by a future version of https://github.com/simonw/dclient ", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1801394744, "label": "Plugin system"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/567#issuecomment-1638910473", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/567", "id": 1638910473, "node_id": "IC_kwDOCGYnMM5hr8oJ", "user": {"value": 15178711, "label": "asg017"}, "created_at": "2023-07-17T21:27:41Z", "updated_at": "2023-07-17T21:27:41Z", "author_association": "NONE", "body": "Another use-case: I want to make a `sqlite-utils` plugin that'll help me insert data into Datasette. \r\n\r\n```bash\r\nsqlite-utils insert-datasette \\\r\n --token $DATASETTE_API_KEY \\\r\n https://latest.datasette.io/fixtures/my-table \\\r\n 'select ...'\r\n```\r\n\r\n\r\nThis 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.)", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1801394744, "label": "Plugin system"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/566#issuecomment-1627598570", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/566", "id": 1627598570, "node_id": "IC_kwDOCGYnMM5hAy7q", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-07-09T04:13:34Z", "updated_at": "2023-07-09T04:13:34Z", "author_association": "OWNER", "body": "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.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1795219865, "label": "`--no-headers` doesn't work on most formats"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/566#issuecomment-1627597872", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/566", "id": 1627597872, "node_id": "IC_kwDOCGYnMM5hAyww", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-07-09T04:09:56Z", "updated_at": "2023-07-09T04:09:56Z", "author_association": "OWNER", "body": "Thanks, looks like a bug.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1795219865, "label": "`--no-headers` doesn't work on most formats"}, "performed_via_github_app": null} {"html_url": "https://github.com/dogsheep/pocket-to-sqlite/issues/12#issuecomment-1627564127", "issue_url": "https://api.github.com/repos/dogsheep/pocket-to-sqlite/issues/12", "id": 1627564127, "node_id": "IC_kwDODLZ_YM5hAqhf", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-07-09T01:19:42Z", "updated_at": "2023-07-09T01:19:42Z", "author_association": "MEMBER", "body": "https://github.com/dogsheep/pocket-to-sqlite/tree/0.2.3 and https://pypi.org/project/pocket-to-sqlite/0.2.3/", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1795187493, "label": "Switch to pyproject.toml"}, "performed_via_github_app": null} {"html_url": "https://github.com/dogsheep/pocket-to-sqlite/issues/12#issuecomment-1627563202", "issue_url": "https://api.github.com/repos/dogsheep/pocket-to-sqlite/issues/12", "id": 1627563202, "node_id": "IC_kwDODLZ_YM5hAqTC", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-07-09T01:14:27Z", "updated_at": "2023-07-09T01:14:27Z", "author_association": "MEMBER", "body": "I tested this locally with `python -m build` and then `pip install ...whl` in a fresh virtual environment.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1795187493, "label": "Switch to pyproject.toml"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/565#issuecomment-1618380888", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/565", "id": 1618380888, "node_id": "IC_kwDOCGYnMM5gdohY", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-07-03T14:09:11Z", "updated_at": "2023-07-03T14:09:31Z", "author_association": "OWNER", "body": "For the CLI:\r\n\r\n```bash\r\nsqlite-utils rename-table data.db old_table_name new_table_name\r\n```\r\n\r\nFor the Python code, should it go on Table or on Database?\r\n```python\r\ndb[\"foo\"].rename_table(\"bar\")\r\n\r\ndb.rename_table(\"foo\", \"bar\")\r\n```\r\nI think I like the second better, it's slightly more clear.\r\n\r\nAlso 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.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1786258502, "label": "Table renaming utilities"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/563#issuecomment-1617395444", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/563", "id": 1617395444, "node_id": "IC_kwDOCGYnMM5gZ370", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-07-03T05:44:43Z", "updated_at": "2023-07-03T05:44:43Z", "author_association": "OWNER", "body": "Documentation at the bottom of this section: https://sqlite-utils.datasette.io/en/latest/cli.html#inserting-csv-or-tsv-data", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1785360409, "label": "`--empty-null` option when importing CSV"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/562#issuecomment-1616782404", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/562", "id": 1616782404, "node_id": "IC_kwDOCGYnMM5gXiRE", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-07-02T19:24:14Z", "updated_at": "2023-07-02T19:26:39Z", "author_association": "OWNER", "body": "[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.\r\n\r\nThe 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.\r\n\r\nExample for https://datasette.io/content/users\r\n\r\n```sql\r\nCREATE TABLE [users] (\r\n [login] TEXT,\r\n [id] INTEGER PRIMARY KEY,\r\n [node_id] TEXT,\r\n [avatar_url] TEXT,\r\n [gravatar_id] TEXT,\r\n [html_url] TEXT,\r\n [type] TEXT,\r\n [site_admin] INTEGER,\r\n [name] TEXT\r\n);\r\n```\r\nAnd the dataclass:\r\n```python\r\nfrom dataclasses import dataclass\r\n\r\n@dataclass\r\nclass User:\r\n id: int\r\n login: str\r\n node_id: str\r\n avatar_url: str\r\n gravatar_id: str\r\n html_url: str\r\n type: str\r\n site_admin: int\r\n name: str\r\n```\r\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1784794489, "label": "Explore the intersection between sqlite-utils and dataclasses"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2093#issuecomment-1616286848", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2093", "id": 1616286848, "node_id": "IC_kwDOBm6k_c5gVpSA", "user": {"value": 15178711, "label": "asg017"}, "created_at": "2023-07-02T02:17:46Z", "updated_at": "2023-07-02T02:17:46Z", "author_association": "CONTRIBUTOR", "body": "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. \r\n\r\nThe next question will be how precedence should work - i'd imagine metadata.json > plugins > datasette_metadata > sqlite-docs", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1781530343, "label": "Proposal: Combine settings, metadata, static, etc. into a single `datasette.toml` File"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2093#issuecomment-1616195496", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2093", "id": 1616195496, "node_id": "IC_kwDOBm6k_c5gVS-o", "user": {"value": 273509, "label": "terinjokes"}, "created_at": "2023-07-02T00:06:54Z", "updated_at": "2023-07-02T00:07:17Z", "author_association": "NONE", "body": "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.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1781530343, "label": "Proposal: Combine settings, metadata, static, etc. into a single `datasette.toml` File"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2093#issuecomment-1614652001", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2093", "id": 1614652001, "node_id": "IC_kwDOBm6k_c5gPaJh", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-06-30T13:27:13Z", "updated_at": "2023-06-30T13:27:13Z", "author_association": "OWNER", "body": "I agree, settings in the DB doesn't make sense but metadata does.\r\n\r\nOn 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", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1781530343, "label": "Proposal: Combine settings, metadata, static, etc. into a single `datasette.toml` File"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2093#issuecomment-1613896210", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2093", "id": 1613896210, "node_id": "IC_kwDOBm6k_c5gMhoS", "user": {"value": 15178711, "label": "asg017"}, "created_at": "2023-06-29T22:53:33Z", "updated_at": "2023-06-29T22:53:33Z", "author_association": "CONTRIBUTOR", "body": "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", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1781530343, "label": "Proposal: Combine settings, metadata, static, etc. into a single `datasette.toml` File"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2093#issuecomment-1613895188", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2093", "id": 1613895188, "node_id": "IC_kwDOBm6k_c5gMhYU", "user": {"value": 15178711, "label": "asg017"}, "created_at": "2023-06-29T22:51:53Z", "updated_at": "2023-06-29T22:51:53Z", "author_association": "CONTRIBUTOR", "body": "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. \r\n\r\nThough 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.*`.\r\n\r\nAnd 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.*`", "reactions": "{\"total_count\": 1, \"+1\": 1, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1781530343, "label": "Proposal: Combine settings, metadata, static, etc. into a single `datasette.toml` File"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2093#issuecomment-1613889979", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2093", "id": 1613889979, "node_id": "IC_kwDOBm6k_c5gMgG7", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-06-29T22:44:08Z", "updated_at": "2023-06-30T13:25:39Z", "author_association": "OWNER", "body": "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.\r\n\r\nSo 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:\r\n\r\n```\r\ndatasette publish heroku my_database.db \\\r\n --name my-heroku-app-demo \\\r\n --install=datasette-auth-github \\\r\n --plugin-secret datasette-auth-github client_id your_client_id \\\r\n --plugin-secret datasette-auth-github client_secret your_client_secret\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1781530343, "label": "Proposal: Combine settings, metadata, static, etc. into a single `datasette.toml` File"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2093#issuecomment-1613887492", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2093", "id": 1613887492, "node_id": "IC_kwDOBm6k_c5gMfgE", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-06-29T22:40:25Z", "updated_at": "2023-06-29T22:40:25Z", "author_association": "OWNER", "body": "I'm strongly in favour of combining settings, configuration and plugin configuration.\r\n\r\nI'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.\r\n\r\nI'm not yet sold on TOML - I actually find it less intuitive than YAML, surprisingly. They all have their warts I guess.\r\n\r\nDatasette 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.\r\n\r\nIn 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.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1781530343, "label": "Proposal: Combine settings, metadata, static, etc. into a single `datasette.toml` File"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2092#issuecomment-1613381990", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2092", "id": 1613381990, "node_id": "IC_kwDOBm6k_c5gKkFm", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-06-29T15:23:04Z", "updated_at": "2023-06-29T15:26:28Z", "author_association": "OWNER", "body": "Felt lazy:\r\n```bash\r\nsymbex test_homepage -f tests/test_api.py | \\\r\n llm -m 4 --system 'Change all of the == some integer tests in this code to isinstance(thing, int) instead'\r\n```\r\nOutput:\r\n```python\r\n# File: tests/test_api.py Line: 26\r\n@pytest.mark.asyncio\r\nasync def test_homepage(ds_client):\r\n response = await ds_client.get(\"/.json\")\r\n assert response.status_code == 200\r\n assert \"application/json; charset=utf-8\" == response.headers[\"content-type\"]\r\n data = response.json()\r\n assert isinstance(data.keys(), int)\r\n d = data[\"fixtures\"]\r\n assert d[\"name\"] == \"fixtures\"\r\n assert isinstance(d[\"tables_count\"], int)\r\n assert isinstance(len(d[\"tables_and_views_truncated\"]), int)\r\n assert d[\"tables_and_views_more\"] is True\r\n # 4 hidden FTS tables + no_primary_key (hidden in metadata)\r\n assert isinstance(d[\"hidden_tables_count\"], int)\r\n # 201 in no_primary_key, plus 6 in other hidden tables:\r\n assert isinstance(d[\"hidden_table_rows_sum\"], int), data\r\n assert isinstance(d[\"views_count\"], int)\r\n```\r\nI'll use most of that and delete the obsoleted comments.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1781047747, "label": "test_homepage intermittent failure"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2092#issuecomment-1613375407", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2092", "id": 1613375407, "node_id": "IC_kwDOBm6k_c5gKiev", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-06-29T15:20:52Z", "updated_at": "2023-06-29T15:21:05Z", "author_association": "OWNER", "body": "I'm going to remove this assertion entirely. The homepage JSON needs a refactor anyway.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1781047747, "label": "test_homepage intermittent failure"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2091#issuecomment-1613369355", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2091", "id": 1613369355, "node_id": "IC_kwDOBm6k_c5gKhAL", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-06-29T15:18:34Z", "updated_at": "2023-06-29T15:18:34Z", "author_association": "OWNER", "body": "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", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1781022369, "label": "Drop support for Python 3.7"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2091#issuecomment-1613360413", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2091", "id": 1613360413, "node_id": "IC_kwDOBm6k_c5gKe0d", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-06-29T15:13:04Z", "updated_at": "2023-06-29T15:13:04Z", "author_association": "OWNER", "body": "One problem: https://glitch.com/ still provides 3.7:\r\n```\r\n$ python3 --version\r\nPython 3.7.10\r\n```\r\n\"image\"\r\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1781022369, "label": "Drop support for Python 3.7"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2090#issuecomment-1613346412", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2090", "id": 1613346412, "node_id": "IC_kwDOBm6k_c5gKbZs", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-06-29T15:05:04Z", "updated_at": "2023-06-29T15:05:04Z", "author_association": "OWNER", "body": "Decided to fix just those \"Ambiguous variable name\" ones:\r\n\r\n```bash\r\nruff check . | grep E741\r\n```\r\nThen iterated through and fixed them all.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1781005740, "label": "Adopt ruff for linting"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2090#issuecomment-1613339404", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2090", "id": 1613339404, "node_id": "IC_kwDOBm6k_c5gKZsM", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-06-29T15:01:01Z", "updated_at": "2023-06-29T15:01:20Z", "author_association": "OWNER", "body": "I tried it just now and got some interesting results.\r\n\r\nI dropped in a `ruff.toml` file:\r\n```toml\r\nline-length = 160\r\n```\r\nBecause the default line length limit of 88 was causing a lot of noisy errors.\r\n\r\nThen run:\r\n\r\n```bash\r\npip install ruff\r\nruff check .\r\n```\r\nPlenty 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.\r\n\r\nSome lines in tests are longer than even 160 chars, e.g.:\r\n\r\nhttps://github.com/simonw/datasette/blob/99ba05118891db9dc30f1dca22ad6709775560de/tests/test_html.py#L673-L681\r\n\r\nThese can have ` # noqa: E501` added to the end of those lines to skip the check for them.\r\n\r\nThat got it down to:\r\n\r\n```\r\n% ruff check . \r\ndatasette/views/table.py:23:5: F811 Redefinition of unused `format_bytes` from line 19\r\nrun_tests.py:2:5: E401 Multiple imports on one line\r\ntests/test_api.py:591:40: F811 Redefinition of unused `app_client_no_files` from line 7\r\ntests/test_api.py:629:35: F811 Redefinition of unused `app_client_no_files` from line 7\r\ntests/test_api.py:635:54: F811 Redefinition of unused `app_client_with_dot` from line 8\r\ntests/test_api.py:661:25: F811 Redefinition of unused `app_client_shorter_time_limit` from line 9\r\ntests/test_api.py:759:25: F811 Redefinition of unused `app_client_two_attached_databases_one_immutable` from line 10\r\ntests/test_api.py:892:28: F811 Redefinition of unused `app_client_larger_cache_size` from line 11\r\ntests/test_api.py:928:5: F811 Redefinition of unused `app_client_with_cors` from line 12\r\ntests/test_api.py:929:5: F811 Redefinition of unused `app_client_two_attached_databases_one_immutable` from line 10\r\ntests/test_api.py:969:38: F811 Redefinition of unused `app_client_two_attached_databases` from line 13\r\ntests/test_api.py:976:39: F811 Redefinition of unused `app_client_conflicting_database_names` from line 14\r\ntests/test_api.py:987:38: F811 Redefinition of unused `app_client_immutable_and_inspect_file` from line 15\r\ntests/test_api.py:1002:24: F811 Redefinition of unused `app_client` from line 6\r\ntests/test_csv.py:67:33: F811 Redefinition of unused `app_client_with_cors` from line 6\r\ntests/test_csv.py:157:21: F811 Redefinition of unused `app_client_csv_max_mb_one` from line 5\r\ntests/test_csv.py:198:20: F811 Redefinition of unused `app_client_with_trace` from line 7\r\ntests/test_csv.py:209:53: F811 Redefinition of unused `app_client_with_trace` from line 7\r\ntests/test_csv.py:215:53: F811 Redefinition of unused `app_client_with_trace` from line 7\r\ntests/test_filters.py:102:11: F811 Redefinition of unused `test_through_filters_from_request` from line 81\r\ntests/test_html.py:19:19: F811 Redefinition of unused `app_client_two_attached_databases` from line 7\r\ntests/test_html.py:175:25: F811 Redefinition of unused `app_client_shorter_time_limit` from line 6\r\ntests/test_html.py:469:51: F811 Redefinition of unused `app_client` from line 4\r\ntests/test_html.py:797:26: F811 Redefinition of unused `app_client_base_url_prefix` from line 5\r\ntests/test_html.py:840:44: F811 Redefinition of unused `app_client_base_url_prefix` from line 5\r\ntests/test_html.py:850:51: F811 Redefinition of unused `app_client_base_url_prefix` from line 5\r\ntests/test_pagination.py:50:43: F821 Undefined name `parse_next`\r\ntests/test_pagination.py:82:7: F811 Redefinition of unused `KeysetPaginator` from line 36\r\ntests/test_plugins.py:115:15: E741 Ambiguous variable name: `l`\r\ntests/test_plugins.py:482:161: E501 Line too long (170 > 160 characters)\r\ntests/test_plugins.py:543:29: E741 Ambiguous variable name: `l`\r\ntests/test_plugins.py:563:161: E501 Line too long (170 > 160 characters)\r\ntests/test_plugins.py:940:62: E741 Ambiguous variable name: `l`\r\ntests/test_table_api.py:739:5: F811 Redefinition of unused `app_client_returned_rows_matches_page_size` from line 6\r\ntests/test_table_api.py:1066:45: F811 Redefinition of unused `app_client_with_trace` from line 5\r\ntests/test_table_html.py:484:29: E741 Ambiguous variable name: `l`\r\ntests/test_table_html.py:524:29: E741 Ambiguous variable name: `l`\r\ntests/test_table_html.py:675:161: E501 Line too long (165 > 160 characters)\r\ntests/test_table_html.py:897:161: E501 Line too long (164 > 160 characters)\r\ntests/test_table_html.py:902:161: E501 Line too long (164 > 160 characters)\r\ntests/test_utils.py:141:161: E501 Line too long (176 > 160 characters)\r\nFound 41 errors.\r\n```\r\nThose \"Redefinition of unused `app_client_two_attached_databases`\" lines are caused because of the fixtures pattern I'm using here:\r\n\r\nhttps://github.com/simonw/datasette/blob/99ba05118891db9dc30f1dca22ad6709775560de/tests/test_html.py#L3-L20\r\n\r\nI could fix that by getting rid of `fixtures.py` and moving those into `conftest.py`.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1781005740, "label": "Adopt ruff for linting"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2089#issuecomment-1613316722", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2089", "id": 1613316722, "node_id": "IC_kwDOBm6k_c5gKUJy", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-06-29T14:48:10Z", "updated_at": "2023-06-29T14:48:10Z", "author_association": "OWNER", "body": "Spell check is passing now.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1780973290, "label": "codespell test failure"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2089#issuecomment-1613315851", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2089", "id": 1613315851, "node_id": "IC_kwDOBm6k_c5gKT8L", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-06-29T14:47:38Z", "updated_at": "2023-06-29T14:47:38Z", "author_association": "OWNER", "body": "Confirmed, this was a 2.2.5 change: https://github.com/codespell-project/codespell/releases/tag/v2.2.5\r\n\r\n> - Add displaing->displaying by [@peternewman](https://github.com/peternewman) in [#2808](https://github.com/codespell-project/codespell/pull/2808)", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1780973290, "label": "codespell test failure"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2089#issuecomment-1613307716", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2089", "id": 1613307716, "node_id": "IC_kwDOBm6k_c5gKR9E", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-06-29T14:42:23Z", "updated_at": "2023-06-29T14:42:23Z", "author_association": "OWNER", "body": "Yes, upgrading locally got me the correct version and the test failure:\r\n```\r\n% pip install -U codespell\r\nRequirement already satisfied: codespell in /Users/simon/.local/share/virtualenvs/datasette-AWNrQs95/lib/python3.10/site-packages (2.2.2)\r\nCollecting codespell\r\n Downloading codespell-2.2.5-py3-none-any.whl (242 kB)\r\n \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 242.7/242.7 kB 4.9 MB/s eta 0:00:00\r\nInstalling collected packages: codespell\r\n Attempting uninstall: codespell\r\n Found existing installation: codespell 2.2.2\r\n Uninstalling codespell-2.2.2:\r\n Successfully uninstalled codespell-2.2.2\r\nSuccessfully installed codespell-2.2.5\r\n% codespell docs/metadata.rst\r\ndocs/metadata.rst:192: displaing ==> displaying\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1780973290, "label": "codespell test failure"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2089#issuecomment-1613306787", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2089", "id": 1613306787, "node_id": "IC_kwDOBm6k_c5gKRuj", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-06-29T14:41:47Z", "updated_at": "2023-06-29T14:41:47Z", "author_association": "OWNER", "body": "Looks like in CI it's running 2.2.5:\r\n```\r\nCollecting codespell (from datasette==1.0a2)\r\n Downloading codespell-2.2.5-py3-none-any.whl (242 kB)\r\n \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 242.7/242.7 kB 31.1 MB/s eta 0:00:00\r\n```\r\nBut on my laptop it's 2.2.2:\r\n```\r\n% codespell --version\r\n2.2.2\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1780973290, "label": "codespell test failure"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2089#issuecomment-1613305070", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2089", "id": 1613305070, "node_id": "IC_kwDOBm6k_c5gKRTu", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-06-29T14:40:44Z", "updated_at": "2023-06-29T14:40:44Z", "author_association": "OWNER", "body": "I'm not sure why I can't duplicate this failure in my local development environment:\r\n```\r\n% codespell docs/metadata.rst\r\n```\r\nIt finds no errors.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1780973290, "label": "codespell test failure"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/561#issuecomment-1610040517", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/561", "id": 1610040517, "node_id": "IC_kwDOCGYnMM5f90TF", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-06-27T18:44:31Z", "updated_at": "2023-06-27T18:44:38Z", "author_association": "OWNER", "body": "Got this working:\r\n```bash\r\nsqlite-utils insert /tmp/playground.db Playground_Submission_Data \\\r\n ~/Downloads/Playground_Submission_Data.csv --csv --stop-after 2000\r\n```\r\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1777548699, "label": "`--stop-after` option for `insert` and `upsert` commands"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/pull/560#issuecomment-1606315321", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/560", "id": 1606315321, "node_id": "IC_kwDOCGYnMM5fvm05", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-06-25T23:18:33Z", "updated_at": "2023-06-25T23:18:33Z", "author_association": "OWNER", "body": "Documentation preview: https://sqlite-utils--560.org.readthedocs.build/en/560/installation.html#alternatives-to-sqlite3", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1773458985, "label": "Use sqlean if available in environment"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/pull/560#issuecomment-1606310630", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/560", "id": 1606310630, "node_id": "IC_kwDOCGYnMM5fvlrm", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-06-25T23:06:07Z", "updated_at": "2023-06-25T23:06:07Z", "author_association": "OWNER", "body": "Filed an issue about the above with `pysqlite3` (which `sqlean.py` is based on) here:\r\n- https://github.com/coleifer/pysqlite3/issues/58", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1773458985, "label": "Use sqlean if available in environment"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/pull/560#issuecomment-1606297356", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/560", "id": 1606297356, "node_id": "IC_kwDOCGYnMM5fvicM", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-06-25T22:42:41Z", "updated_at": "2023-06-25T22:42:41Z", "author_association": "OWNER", "body": "Yes that does seem to do the trick:\r\n```pycon\r\n>>> import sqlean\r\n>>> db = sqlean.connect(\"/tmp/4.db\")\r\n>>> db.execute('PRAGMA journal_mode;').fetchall()\r\n[('delete',)]\r\n>>> db.isolation_level\r\n''\r\n>>> db.execute('PRAGMA journal_mode=wal;')\r\nTraceback (most recent call last):\r\n File \"\", line 1, in \r\nsqlean.dbapi2.OperationalError: cannot change into wal mode from within a transaction\r\n>>> db.isolation_level = None\r\n>>> db.isolation_level\r\n>>> db.execute('PRAGMA journal_mode=wal;')\r\n\r\n```\r\nWeird how `isolation_level` of empty string causes the error, but setting that to `None` fixes the error.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1773458985, "label": "Use sqlean if available in environment"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/pull/560#issuecomment-1606294627", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/560", "id": 1606294627, "node_id": "IC_kwDOCGYnMM5fvhxj", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-06-25T22:40:10Z", "updated_at": "2023-06-25T22:40:10Z", "author_association": "OWNER", "body": "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`.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1773458985, "label": "Use sqlean if available in environment"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/pull/560#issuecomment-1606293382", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/560", "id": 1606293382, "node_id": "IC_kwDOCGYnMM5fvheG", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-06-25T22:34:47Z", "updated_at": "2023-06-25T22:34:47Z", "author_association": "OWNER", "body": "```pycon\r\n>>> import sqlite3\r\n>>> db = sqlite3.connect(\"/tmp/1.db\")\r\n>>> db.execute('PRAGMA journal_mode=wal;')\r\n\r\n>>> import sqlean\r\n>>> db2 = sqlean.connect(\"/tmp/2.db\")\r\n>>> db2.execute('PRAGMA journal_mode=wal;')\r\nTraceback (most recent call last):\r\n File \"\", line 1, in \r\nsqlean.dbapi2.OperationalError: cannot change into wal mode from within a transaction\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1773458985, "label": "Use sqlean if available in environment"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/pull/560#issuecomment-1606290917", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/560", "id": 1606290917, "node_id": "IC_kwDOCGYnMM5fvg3l", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-06-25T22:32:28Z", "updated_at": "2023-06-25T22:32:28Z", "author_association": "OWNER", "body": "I've fixed most of the test failures, but I still need to fix this one:\r\n\r\n> cannot change into wal mode from within a transaction", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1773458985, "label": "Use sqlean if available in environment"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/pull/560#issuecomment-1606273005", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/560", "id": 1606273005, "node_id": "IC_kwDOCGYnMM5fvcft", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-06-25T21:47:47Z", "updated_at": "2023-06-25T21:47:47Z", "author_association": "OWNER", "body": "I can use https://github.com/simonw/sqlite-dump as an optional dependency to handle the missing `.iterdump()` method.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1773458985, "label": "Use sqlean if available in environment"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/pull/560#issuecomment-1606270887", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/560", "id": 1606270887, "node_id": "IC_kwDOCGYnMM5fvb-n", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-06-25T21:37:12Z", "updated_at": "2023-06-26T08:21:00Z", "author_association": "OWNER", "body": "On my own laptop I got a crash running the tests - details here:\r\n\r\n- https://github.com/nalgeon/sqlean.py/issues/3", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1773458985, "label": "Use sqlean if available in environment"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/pull/560#issuecomment-1606270055", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/560", "id": 1606270055, "node_id": "IC_kwDOCGYnMM5fvbxn", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-06-25T21:31:56Z", "updated_at": "2023-06-25T21:31:56Z", "author_association": "OWNER", "body": "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\r\n\r\nA bunch of these, because `pysqlite3` chooses not to implement `.iterdump()`:\r\n```\r\n @pytest.fixture\r\n def db_to_analyze_path(db_to_analyze, tmpdir):\r\n path = str(tmpdir / \"test.db\")\r\n db = sqlite3.connect(path)\r\n> db.executescript(\"\\n\".join(db_to_analyze.conn.iterdump()))\r\nE AttributeError: 'sqlean.dbapi2.Connection' object has no attribute 'iterdump'\r\n```\r\nAlso some of these:\r\n```\r\n def test_analyze_whole_database(db):\r\n assert set(db.table_names()) == {\"one_index\", \"two_indexes\"}\r\n db.analyze()\r\n> assert set(db.table_names()) == {\"one_index\", \"two_indexes\", \"sqlite_stat1\"}\r\nE AssertionError: assert {'one_index',...'two_indexes'} == {'one_index',...'two_indexes'}\r\nE Extra items in the left set:\r\nE 'sqlite_stat4'\r\nE Full diff:\r\nE - {'two_indexes', 'sqlite_stat1', 'one_index'}\r\nE + {'two_indexes', 'sqlite_stat1', 'sqlite_stat4', 'one_index'}\r\nE ? ++++++++++++++++\r\n```\r\nApparently `sqlean.py` adds a `sqlite_stat4` table that the tests are not expecting.\r\n\r\nPlus some errors that look like this:\r\n```\r\n def test_enable_wal():\r\n runner = CliRunner()\r\n dbs = [\"test.db\", \"test2.db\"]\r\n with runner.isolated_filesystem():\r\n for dbname in dbs:\r\n db = Database(dbname)\r\n db[\"t\"].create({\"pk\": int}, pk=\"pk\")\r\n assert db.journal_mode == \"delete\"\r\n result = runner.invoke(cli.cli, [\"enable-wal\"] + dbs)\r\n> assert 0 == result.exit_code\r\nE AssertionError: assert 0 == 1\r\nE + where 1 = .exit_code\r\n```\r\nTest summary:\r\n```\r\n============ 13 failed, 909 passed, 16 skipped, 2 errors in 19.29s =============\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1773458985, "label": "Use sqlean if available in environment"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/pull/560#issuecomment-1606237836", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/560", "id": 1606237836, "node_id": "IC_kwDOCGYnMM5fvT6M", "user": {"value": 22429695, "label": "codecov[bot]"}, "created_at": "2023-06-25T19:49:45Z", "updated_at": "2023-06-26T08:20:59Z", "author_association": "NONE", "body": "## [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\nPatch coverage: **`80.55`**% and project coverage change: **`-0.15`** :warning:\n> 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%.\n\n
Additional details and impacted files\n\n\n```diff\n@@ Coverage Diff @@\n## main #560 +/- ##\n==========================================\n- Coverage 96.36% 96.22% -0.15% \n==========================================\n Files 6 6 \n Lines 2726 2752 +26 \n==========================================\n+ Hits 2627 2648 +21 \n- Misses 99 104 +5 \n```\n\n\n| [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 \u0394 | |\n|---|---|---|\n| [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: |\n| [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: |\n| [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%> (\u00f8)` | |\n\n\n
\n\n[: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). \n: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).\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1773458985, "label": "Use sqlean if available in environment"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2087#issuecomment-1636134091", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2087", "id": 1636134091, "node_id": "IC_kwDOBm6k_c5hhWzL", "user": {"value": 653549, "label": "adarshp"}, "created_at": "2023-07-14T17:02:03Z", "updated_at": "2023-07-14T17:02:03Z", "author_association": "NONE", "body": "@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. \r\n\r\nFor 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).", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1765870617, "label": "`--settings settings.json` option"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2087#issuecomment-1616853644", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2087", "id": 1616853644, "node_id": "IC_kwDOBm6k_c5gXzqM", "user": {"value": 15178711, "label": "asg017"}, "created_at": "2023-07-02T22:00:48Z", "updated_at": "2023-07-02T22:00:48Z", "author_association": "CONTRIBUTOR", "body": "I just saw in the docs that Dasette auto-detects `settings.json`:\r\n\r\n> settings.json - settings that would normally be passed using --setting - here they should be stored as a JSON object of key/value pairs\r\n> [*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)", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1765870617, "label": "`--settings settings.json` option"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/557#issuecomment-1590531892", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/557", "id": 1590531892, "node_id": "IC_kwDOCGYnMM5ezZc0", "user": {"value": 7908073, "label": "chapmanjacobd"}, "created_at": "2023-06-14T06:09:21Z", "updated_at": "2023-06-14T06:09:21Z", "author_association": "CONTRIBUTOR", "body": "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\r\n\r\n```\r\nCREATE TABLE my_table (\r\n id INTEGER PRIMARY KEY,\r\n column1 TEXT,\r\n column2 TEXT,\r\n column3 TEXT\r\n);\r\n\r\nINSERT INTO my_table (column1, column2, column3)\r\nVALUES\r\n ('Value 1', 'Duplicate 1', 'Duplicate A'),\r\n ('Value 2', 'Duplicate 2', 'Duplicate B'),\r\n ('Value 3', 'Duplicate 2', 'Duplicate C'),\r\n ('Value 4', 'Duplicate 3', 'Duplicate D'),\r\n ('Value 5', 'Duplicate 3', 'Duplicate E'),\r\n ('Value 6', 'Duplicate 3', 'Duplicate F');\r\n```\r\n\r\n```\r\nlibrary dedupe-db test.db my_table --bk column2\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1740150327, "label": "Aliased ROWID option for tables created from alter=True commands"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/557#issuecomment-1577355134", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/557", "id": 1577355134, "node_id": "IC_kwDOCGYnMM5eBId-", "user": {"value": 7908073, "label": "chapmanjacobd"}, "created_at": "2023-06-05T19:26:26Z", "updated_at": "2023-06-05T19:26:26Z", "author_association": "CONTRIBUTOR", "body": "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", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1740150327, "label": "Aliased ROWID option for tables created from alter=True commands"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/556#issuecomment-1575310378", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/556", "id": 1575310378, "node_id": "IC_kwDOCGYnMM5d5VQq", "user": {"value": 601708, "label": "mcint"}, "created_at": "2023-06-04T01:21:15Z", "updated_at": "2023-06-04T01:21:15Z", "author_association": "CONTRIBUTOR", "body": "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).", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1740026046, "label": "Support storing incrementally piped values"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/555#issuecomment-1592047502", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/555", "id": 1592047502, "node_id": "IC_kwDOCGYnMM5e5LeO", "user": {"value": 7908073, "label": "chapmanjacobd"}, "created_at": "2023-06-14T22:00:10Z", "updated_at": "2023-06-14T22:01:57Z", "author_association": "CONTRIBUTOR", "body": "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.\r\n\r\nThat 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...\r\n\r\nYou may also try chunking like this:\r\n\r\n```py\r\ndef chunks(lst, n) -> Generator:\r\n for i in range(0, len(lst), n):\r\n yield lst[i : i + n]\r\n\r\nSQLITE_PARAM_LIMIT = 32765\r\n\r\ndata = []\r\nchunked = chunks(video_ids, consts.SQLITE_PARAM_LIMIT)\r\nfor ids in chunked:\r\n data.expand(\r\n list(\r\n db.query(\r\n f\"\"\"SELECT * from videos\r\n WHERE id in (\"\"\"\r\n + \",\".join([\"?\"] * len(ids))\r\n + \")\",\r\n (*ids,),\r\n )\r\n )\r\n )\r\n```\r\n\r\nbut that actually won't work with your `NOT IN` requirements. You need to query the full resultset to check any row.\r\n\r\nSince you are doing stuff with files/videos in SQLITE you might be interested in my side project: https://github.com/chapmanjacobd/library", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1733198948, "label": "Filter table by a large bunch of ids"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/pull/2080#issuecomment-1563650990", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2080", "id": 1563650990, "node_id": "IC_kwDOBm6k_c5dM2uu", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-26T00:08:59Z", "updated_at": "2023-05-26T00:08:59Z", "author_association": "OWNER", "body": "I'm not going to document this yet, I want to let it bake for a bit longer first.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726603778, "label": "New View base class"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/pull/2080#issuecomment-1563629348", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2080", "id": 1563629348, "node_id": "IC_kwDOBm6k_c5dMxck", "user": {"value": 22429695, "label": "codecov[bot]"}, "created_at": "2023-05-25T23:31:10Z", "updated_at": "2023-05-26T00:07:34Z", "author_association": "NONE", "body": "## [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\nPatch coverage: **`95.45`**% and project coverage change: **`+0.01`** :tada:\n> 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%.\n\n> :exclamation: Current head df5fd73 differs from pull request most recent head e990fbc. Consider uploading reports for the commit e990fbc to get more accurate results\n\n
Additional details and impacted files\n\n\n```diff\n@@ Coverage Diff @@\n## main #2080 +/- ##\n==========================================\n+ Coverage 92.40% 92.41% +0.01% \n==========================================\n Files 39 39 \n Lines 5768 5790 +22 \n==========================================\n+ Hits 5330 5351 +21 \n- Misses 438 439 +1 \n```\n\n\n| [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 \u0394 | |\n|---|---|---|\n| [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: |\n\n\n
\n\n[: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). \n: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).\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726603778, "label": "New View base class"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/pull/2080#issuecomment-1563626231", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2080", "id": 1563626231, "node_id": "IC_kwDOBm6k_c5dMwr3", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T23:25:17Z", "updated_at": "2023-05-25T23:25:17Z", "author_association": "OWNER", "body": "I'm going to try using this for the `/-/patterns` page.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726603778, "label": "New View base class"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2079#issuecomment-1563607291", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2079", "id": 1563607291, "node_id": "IC_kwDOBm6k_c5dMsD7", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T22:56:28Z", "updated_at": "2023-05-25T22:56:28Z", "author_association": "OWNER", "body": "Wrote this up as a TIL: https://til.simonwillison.net/http/testing-cors-max-age", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726531350, "label": "Datasette should serve Access-Control-Max-Age"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2079#issuecomment-1563597589", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2079", "id": 1563597589, "node_id": "IC_kwDOBm6k_c5dMpsV", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T22:42:07Z", "updated_at": "2023-05-25T22:42:07Z", "author_association": "OWNER", "body": "Mystery solved as to why I wasn't seeing this work:\r\n\r\n\"CleanShot\r\n\r\nI had \"Disable Cache\" checked!\r\n\r\nI ran this experiment after un-checking that box:\r\n\r\n```javascript\r\nfetch('https://latest.datasette.io/ephemeral/foo/1/-/update', {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json'\r\n },\r\n body: JSON.stringify({ test: 'test' })\r\n});\r\n// And run it again\r\nfetch('https://latest.datasette.io/ephemeral/foo/1/-/update', {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json'\r\n },\r\n body: JSON.stringify({ test: 'test' })\r\n});\r\n// Now try a thing that doesn't serve that max-age header yet:\r\nfetch('https://latest-with-plugins.datasette.io/ephemeral/foo/1/-/update', {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json'\r\n },\r\n body: JSON.stringify({ test: 'test' })\r\n});\r\n// And a second time but within 5s\r\nfetch('https://latest-with-plugins.datasette.io/ephemeral/foo/1/-/update', {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json'\r\n },\r\n body: JSON.stringify({ test: 'test' })\r\n});\r\n// Third time after waiting longer than 5s\r\nfetch('https://latest-with-plugins.datasette.io/ephemeral/foo/1/-/update', {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json'\r\n },\r\n body: JSON.stringify({ test: 'test' })\r\n});\r\n// Try that original one again - still within the 1hr cache time\r\nfetch('https://latest.datasette.io/ephemeral/foo/1/-/update', {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json'\r\n },\r\n body: JSON.stringify({ test: 'test' })\r\n});\r\n```\r\nThe 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.\r\n\r\n\"image\"\r\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726531350, "label": "Datasette should serve Access-Control-Max-Age"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2079#issuecomment-1563588199", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2079", "id": 1563588199, "node_id": "IC_kwDOBm6k_c5dMnZn", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T22:29:47Z", "updated_at": "2023-05-25T22:30:12Z", "author_association": "OWNER", "body": "https://fetch.spec.whatwg.org/#http-access-control-max-age says:\r\n\r\n> 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.\r\n\r\nSo there was already a 5s cache anyway.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726531350, "label": "Datasette should serve Access-Control-Max-Age"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2079#issuecomment-1563587230", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2079", "id": 1563587230, "node_id": "IC_kwDOBm6k_c5dMnKe", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T22:28:20Z", "updated_at": "2023-05-25T22:28:20Z", "author_association": "OWNER", "body": "Weird... after the deploy went out:\r\n\r\n\"image\"\r\n\r\nBut the request did indeed get the new header:\r\n\r\n\"image\"\r\n\r\nSo I'm not sure why it's making multiple `POST` requests like that.\r\n\r\nMaybe it's because the attempted `POST` failed with a 404?", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726531350, "label": "Datasette should serve Access-Control-Max-Age"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2079#issuecomment-1563565407", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2079", "id": 1563565407, "node_id": "IC_kwDOBm6k_c5dMh1f", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T22:09:53Z", "updated_at": "2023-05-25T22:09:53Z", "author_association": "OWNER", "body": "Updated docs: https://docs.datasette.io/en/latest/json_api.html#enabling-cors", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726531350, "label": "Datasette should serve Access-Control-Max-Age"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2079#issuecomment-1563563438", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2079", "id": 1563563438, "node_id": "IC_kwDOBm6k_c5dMhWu", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T22:08:28Z", "updated_at": "2023-05-25T22:08:28Z", "author_association": "OWNER", "body": "I ran this on https://www.example.com/ twice using the console:\r\n```javascript\r\nfetch(\r\n `https://latest.datasette.io/ephemeral/foo/1/-/update`,\r\n {\r\n method: \"POST\",\r\n mode: \"cors\",\r\n headers: {\r\n Authorization: `Bearer tok`,\r\n \"Content-Type\": \"application/json\",\r\n },\r\n body: JSON.stringify({update: {blah: 1}}),\r\n }\r\n)\r\n .then((r) => r.json())\r\n .then((data) => {\r\n console.log(data);\r\n });\r\n```\r\nAnd got this in the network pane:\r\n\r\n\"image\"\r\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726531350, "label": "Datasette should serve Access-Control-Max-Age"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2079#issuecomment-1563558915", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2079", "id": 1563558915, "node_id": "IC_kwDOBm6k_c5dMgQD", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T22:04:41Z", "updated_at": "2023-05-25T22:04:41Z", "author_association": "OWNER", "body": "I'm going with 3600 for 1 hour instead of 600 for 10 minutes.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726531350, "label": "Datasette should serve Access-Control-Max-Age"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2079#issuecomment-1563547097", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2079", "id": 1563547097, "node_id": "IC_kwDOBm6k_c5dMdXZ", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T21:51:38Z", "updated_at": "2023-05-25T21:51:38Z", "author_association": "OWNER", "body": "Also need to update this documentation:\r\n\r\nhttps://github.com/simonw/datasette/blob/9584879534ff0556e04e4c420262972884cac87b/docs/json_api.rst?plain=1#L453-L465\r\n\r\nOr maybe make that automated via `cog`.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726531350, "label": "Datasette should serve Access-Control-Max-Age"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2078#issuecomment-1563625093", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2078", "id": 1563625093, "node_id": "IC_kwDOBm6k_c5dMwaF", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T23:23:15Z", "updated_at": "2023-05-25T23:23:15Z", "author_association": "OWNER", "body": "Rest of the work on this will happen in the PR:\r\n- #2080", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726236847, "label": "Resolve the difference between `wrap_view()` and `BaseView`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2078#issuecomment-1563522011", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2078", "id": 1563522011, "node_id": "IC_kwDOBm6k_c5dMXPb", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T21:22:30Z", "updated_at": "2023-05-25T21:22:30Z", "author_association": "OWNER", "body": "This is bad:\r\n```python\r\n async def __call__(self, request, datasette):\r\n try:\r\n handler = getattr(self, request.method.lower())\r\n return await handler(request, datasette)\r\n except AttributeError:\r\n return await self.method_not_allowed(request)\r\n```\r\nBecause it hides any `AttributeError` exceptions that might occur in the view code.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726236847, "label": "Resolve the difference between `wrap_view()` and `BaseView`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2078#issuecomment-1563511171", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2078", "id": 1563511171, "node_id": "IC_kwDOBm6k_c5dMUmD", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T21:11:20Z", "updated_at": "2023-05-25T21:13:05Z", "author_association": "OWNER", "body": "I'm going to call this `VerbView` for the moment. Might even rename it to `View` later.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726236847, "label": "Resolve the difference between `wrap_view()` and `BaseView`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2078#issuecomment-1563498048", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2078", "id": 1563498048, "node_id": "IC_kwDOBm6k_c5dMRZA", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T20:57:52Z", "updated_at": "2023-05-25T20:58:13Z", "author_association": "OWNER", "body": "Here's a new `BaseView` class that automatically populates `OPTIONS` based on available methods:\r\n```python\r\nclass BaseView:\r\n async def head(self, *args, **kwargs):\r\n try:\r\n response = await self.get(*args, **kwargs)\r\n response.body = b\"\"\r\n return response\r\n except AttributeError:\r\n raise\r\n\r\n async def method_not_allowed(self, request):\r\n if (\r\n request.path.endswith(\".json\")\r\n or request.headers.get(\"content-type\") == \"application/json\"\r\n ):\r\n response = Response.json(\r\n {\"ok\": False, \"error\": \"Method not allowed\"}, status=405\r\n )\r\n else:\r\n response = Response.text(\"Method not allowed\", status=405)\r\n return response\r\n\r\n async def options(self, request, *args, **kwargs):\r\n response = Response.text(\"ok\")\r\n response.headers[\"allow\"] = \", \".join(\r\n method.upper()\r\n for method in (\"head\", \"get\", \"post\", \"put\", \"patch\", \"delete\")\r\n if hasattr(self, method)\r\n )\r\n return response\r\n\r\n async def __call__(self, request, datasette):\r\n try:\r\n handler = getattr(self, request.method.lower())\r\n return await handler(request, datasette)\r\n except AttributeError:\r\n return await self.method_not_allowed(request)\r\n\r\n\r\nclass DemoView(BaseView):\r\n async def get(self, datasette, request):\r\n return Response.text(\"Hello there! {} - {}\".format(datasette, request))\r\n\r\n post = get\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726236847, "label": "Resolve the difference between `wrap_view()` and `BaseView`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2078#issuecomment-1563488929", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2078", "id": 1563488929, "node_id": "IC_kwDOBm6k_c5dMPKh", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T20:48:12Z", "updated_at": "2023-05-25T20:48:39Z", "author_association": "OWNER", "body": "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.\r\n\r\nSo the `get` and `post` and suchlike methods should take `datasette` and `request` too.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726236847, "label": "Resolve the difference between `wrap_view()` and `BaseView`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2078#issuecomment-1563444296", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2078", "id": 1563444296, "node_id": "IC_kwDOBm6k_c5dMERI", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T20:06:08Z", "updated_at": "2023-05-25T20:06:08Z", "author_association": "OWNER", "body": "This prototype seems to work well:\r\n\r\n```diff\r\ndiff --git a/datasette/app.py b/datasette/app.py\r\nindex d7dace67..ed0edf28 100644\r\n--- a/datasette/app.py\r\n+++ b/datasette/app.py\r\n@@ -17,6 +17,7 @@ import secrets\r\n import sys\r\n import threading\r\n import time\r\n+import types\r\n import urllib.parse\r\n from concurrent import futures\r\n from pathlib import Path\r\n@@ -1266,6 +1267,8 @@ class Datasette:\r\n # TODO: /favicon.ico and /-/static/ deserve far-future cache expires\r\n add_route(favicon, \"/favicon.ico\")\r\n \r\n+ add_route(wrap_view(DemoView, self), '/demo')\r\n+\r\n add_route(\r\n asgi_static(app_root / \"datasette\" / \"static\"), r\"/-/static/(?P.*)$\"\r\n )\r\n@@ -1673,8 +1676,46 @@ def _cleaner_task_str(task):\r\n return _cleaner_task_str_re.sub(\"\", s)\r\n \r\n \r\n-def wrap_view(view_fn, datasette):\r\n- @functools.wraps(view_fn)\r\n+class DemoView:\r\n+ async def __call__(self, datasette, request):\r\n+ return Response.text(\"Hello there! {} - {}\".format(datasette, request))\r\n+\r\n+def wrap_view(view_fn_or_class, datasette):\r\n+ is_function = isinstance(view_fn_or_class, types.FunctionType)\r\n+ if is_function:\r\n+ return wrap_view_function(view_fn_or_class, datasette)\r\n+ else:\r\n+ if not isinstance(view_fn_or_class, type):\r\n+ raise ValueError(\"view_fn_or_class must be a function or a class\")\r\n+ return wrap_view_class(view_fn_or_class, datasette)\r\n+\r\n+\r\n+def wrap_view_class(view_class, datasette):\r\n+ async def async_view_for_class(request, send):\r\n+ instance = view_class()\r\n+ if inspect.iscoroutinefunction(instance.__call__):\r\n+ return await async_call_with_supported_arguments(\r\n+ instance.__call__,\r\n+ scope=request.scope,\r\n+ receive=request.receive,\r\n+ send=send,\r\n+ request=request,\r\n+ datasette=datasette,\r\n+ )\r\n+ else:\r\n+ return call_with_supported_arguments(\r\n+ instance.__call__,\r\n+ scope=request.scope,\r\n+ receive=request.receive,\r\n+ send=send,\r\n+ request=request,\r\n+ datasette=datasette,\r\n+ )\r\n+\r\n+ return async_view_for_class\r\n+\r\n+\r\n+def wrap_view_function(view_fn, datasette):\r\n async def async_view_fn(request, send):\r\n if inspect.iscoroutinefunction(view_fn):\r\n response = await async_call_with_supported_arguments(\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726236847, "label": "Resolve the difference between `wrap_view()` and `BaseView`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2078#issuecomment-1563419066", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2078", "id": 1563419066, "node_id": "IC_kwDOBm6k_c5dL-G6", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T19:42:16Z", "updated_at": "2023-05-25T19:43:08Z", "author_association": "OWNER", "body": "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.\r\n\r\nThe neat thing about it is that it can reduce the risk of having a class instance that accidentally shares state between requests.\r\n\r\nIt also encourages that each class only responds based on the `datasette, request, ...` objects that are passed to its methods.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726236847, "label": "Resolve the difference between `wrap_view()` and `BaseView`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2078#issuecomment-1563359114", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2078", "id": 1563359114, "node_id": "IC_kwDOBm6k_c5dLveK", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T18:47:57Z", "updated_at": "2023-05-25T18:47:57Z", "author_association": "OWNER", "body": "Oops, that broke everything:\r\n```\r\n @documented\r\n async def await_me_maybe(value: typing.Any) -> typing.Any:\r\n \"If value is callable, call it. If awaitable, await it. Otherwise return it.\"\r\n> if callable(value):\r\nE TypeError: 'module' object is not callable\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726236847, "label": "Resolve the difference between `wrap_view()` and `BaseView`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2078#issuecomment-1563329245", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2078", "id": 1563329245, "node_id": "IC_kwDOBm6k_c5dLoLd", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T18:26:47Z", "updated_at": "2023-05-25T18:28:08Z", "author_association": "OWNER", "body": "With type hints and a namedtuple:\r\n```python\r\nimport asyncio\r\nimport types\r\nfrom typing import NamedTuple, Any\r\n\r\n\r\nclass CallableStatus(NamedTuple):\r\n is_callable: bool\r\n is_async_callable: bool\r\n\r\n\r\ndef check_callable(obj: Any) -> CallableStatus:\r\n if not callable(obj):\r\n return CallableStatus(False, False)\r\n\r\n if isinstance(obj, type):\r\n # It's a class\r\n return CallableStatus(True, False)\r\n\r\n if isinstance(obj, types.FunctionType):\r\n return CallableStatus(True, asyncio.iscoroutinefunction(obj))\r\n\r\n if hasattr(obj, \"__call__\"):\r\n return CallableStatus(True, asyncio.iscoroutinefunction(obj.__call__))\r\n\r\n assert False, \"obj {} is somehow callable with no __call__ method\".format(repr(obj))\r\n```\r\n```python\r\nfor thing in (\r\n async_func,\r\n non_async_func,\r\n AsyncClass(),\r\n NotAsyncClass(),\r\n ClassNoCall(),\r\n AsyncClass,\r\n NotAsyncClass,\r\n ClassNoCall,\r\n):\r\n print(thing, check_callable(thing))\r\n```\r\n```\r\n CallableStatus(is_callable=True, is_async_callable=True)\r\n CallableStatus(is_callable=True, is_async_callable=False)\r\n<__main__.AsyncClass object at 0x106ba7490> CallableStatus(is_callable=True, is_async_callable=True)\r\n<__main__.NotAsyncClass object at 0x106740150> CallableStatus(is_callable=True, is_async_callable=False)\r\n<__main__.ClassNoCall object at 0x10676d910> CallableStatus(is_callable=False, is_async_callable=False)\r\n CallableStatus(is_callable=True, is_async_callable=False)\r\n CallableStatus(is_callable=True, is_async_callable=False)\r\n CallableStatus(is_callable=True, is_async_callable=False)\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726236847, "label": "Resolve the difference between `wrap_view()` and `BaseView`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2078#issuecomment-1563326000", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2078", "id": 1563326000, "node_id": "IC_kwDOBm6k_c5dLnYw", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T18:23:38Z", "updated_at": "2023-05-25T18:23:38Z", "author_association": "OWNER", "body": "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.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726236847, "label": "Resolve the difference between `wrap_view()` and `BaseView`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2078#issuecomment-1563318598", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2078", "id": 1563318598, "node_id": "IC_kwDOBm6k_c5dLllG", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T18:17:03Z", "updated_at": "2023-05-25T18:21:25Z", "author_association": "OWNER", "body": "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).\r\n\r\nI tried this:\r\n```python\r\ndef is_callable(obj):\r\n \"Returns (is_callable, is_async_callable)\"\r\n if not callable(obj):\r\n return False, False\r\n\r\n if isinstance(obj, types.FunctionType):\r\n return True, asyncio.iscoroutinefunction(obj)\r\n \r\n if hasattr(obj, '__call__'):\r\n return True, asyncio.iscoroutinefunction(obj.__call__)\r\n\r\n return False, False\r\n```\r\n```python\r\nfor thing in (\r\n async_func, non_async_func, AsyncClass(), NotAsyncClass(), ClassNoCall(),\r\n AsyncClass, NotAsyncClass, ClassNoCall\r\n):\r\n print(thing, is_callable(thing))\r\n```\r\nAnd got:\r\n```\r\n (True, True)\r\n (True, False)\r\n<__main__.AsyncClass object at 0x106cce490> (True, True)\r\n<__main__.NotAsyncClass object at 0x106ccf710> (True, False)\r\n<__main__.ClassNoCall object at 0x106ccc810> (False, False)\r\n (True, True)\r\n (True, False)\r\n (True, False)\r\n```\r\nWhich 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).\r\n\r\nSo I'm going to detect classes using `isinstance(obj, type)`.\r\n\r\n```python\r\ndef is_callable(obj):\r\n \"Returns (is_callable, is_async_callable)\"\r\n if not callable(obj):\r\n return False, False\r\n\r\n if isinstance(obj, type):\r\n # It's a class\r\n return True, False\r\n \r\n if isinstance(obj, types.FunctionType):\r\n return True, asyncio.iscoroutinefunction(obj)\r\n \r\n if hasattr(obj, '__call__'):\r\n return True, asyncio.iscoroutinefunction(obj.__call__)\r\n\r\n assert False, \"obj {} somehow is callable with no __call__ method\".format(obj)\r\n```\r\nI am reasonably confident the `AssertionError` can never be raised.\r\n\r\nAnd now:\r\n```\r\n (True, True)\r\n (True, False)\r\n<__main__.AsyncClass object at 0x106ccfa50> (True, True)\r\n<__main__.NotAsyncClass object at 0x106ccc8d0> (True, False)\r\n<__main__.ClassNoCall object at 0x106cd7690> (False, False)\r\n (True, False)\r\n (True, False)\r\n (True, False)\r\n```\r\nWhich is what I wanted.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726236847, "label": "Resolve the difference between `wrap_view()` and `BaseView`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2078#issuecomment-1563308919", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2078", "id": 1563308919, "node_id": "IC_kwDOBm6k_c5dLjN3", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T18:08:34Z", "updated_at": "2023-05-25T18:08:34Z", "author_association": "OWNER", "body": "After much fiddling this seems to work:\r\n```python\r\nimport asyncio, types\r\n\r\ndef is_async_callable(obj):\r\n if not callable(obj):\r\n raise ValueError(\"Object is not callable\")\r\n\r\n if isinstance(obj, types.FunctionType):\r\n return asyncio.iscoroutinefunction(obj)\r\n \r\n if hasattr(obj, '__call__'):\r\n return asyncio.iscoroutinefunction(obj.__call__)\r\n\r\n raise ValueError(\"Not a function and has no __call__ attribute\")\r\n```\r\nTested like so:\r\n```python\r\nclass AsyncClass:\r\n async def __call__(self):\r\n pass\r\n\r\nclass NotAsyncClass:\r\n def __call__(self):\r\n pass\r\n\r\nclass ClassNoCall:\r\n pass\r\n \r\nasync def async_func():\r\n pass\r\n\r\ndef non_async_func():\r\n pass\r\n\r\nfor thing in (AsyncClass(), NotAsyncClass(), ClassNoCall(), async_func, non_async_func):\r\n try:\r\n print(thing, is_async_callable(thing))\r\n except Exception as ex:\r\n print(thing, ex)\r\n```\r\n```\r\n<__main__.AsyncClass object at 0x106c32150> True\r\n<__main__.NotAsyncClass object at 0x106c32390> False\r\n<__main__.ClassNoCall object at 0x106c32750> Object is not callable\r\n True\r\n False\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726236847, "label": "Resolve the difference between `wrap_view()` and `BaseView`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2078#issuecomment-1563294669", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2078", "id": 1563294669, "node_id": "IC_kwDOBm6k_c5dLfvN", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T17:57:06Z", "updated_at": "2023-05-25T17:57:06Z", "author_association": "OWNER", "body": "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:\r\n\r\n```python\r\ndef iscoroutinefunction(obj):\r\n if inspect.iscoroutinefunction(obj):\r\n return True\r\n if hasattr(obj, '__call__') and inspect.iscoroutinefunction(obj.__call__):\r\n return True\r\n return False\r\n```\r\nFrom https://github.com/encode/starlette/issues/886#issuecomment-606585152", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726236847, "label": "Resolve the difference between `wrap_view()` and `BaseView`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2078#issuecomment-1563292373", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2078", "id": 1563292373, "node_id": "IC_kwDOBm6k_c5dLfLV", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T17:55:12Z", "updated_at": "2023-05-25T17:55:30Z", "author_association": "OWNER", "body": "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.\r\n\r\nHaving two layers of parameter detection feels a little bit untidy, but I think it will work.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726236847, "label": "Resolve the difference between `wrap_view()` and `BaseView`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2078#issuecomment-1563283939", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2078", "id": 1563283939, "node_id": "IC_kwDOBm6k_c5dLdHj", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T17:47:38Z", "updated_at": "2023-05-25T17:47:38Z", "author_association": "OWNER", "body": "The idea behind `wrap_view()` is dependency injection - it's mainly used by plugins:\r\n\r\nhttps://docs.datasette.io/en/0.64.3/plugin_hooks.html#register-routes-datasette\r\n\r\nBut I like the pattern so I started using it for some of Datasette's own features.\r\n\r\nI should use it for _all_ of Datasette's own features.\r\n\r\nBut I still like the way `BaseView` helps with running different code for GET/POST/etc verbs.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726236847, "label": "Resolve the difference between `wrap_view()` and `BaseView`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2078#issuecomment-1563282327", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2078", "id": 1563282327, "node_id": "IC_kwDOBm6k_c5dLcuX", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T17:46:05Z", "updated_at": "2023-05-25T17:46:05Z", "author_association": "OWNER", "body": "Here's what `wrap_view()` does:\r\n\r\nhttps://github.com/simonw/datasette/blob/49184c569cd70efbda4f3f062afef3a34401d8d5/datasette/app.py#L1676-L1700\r\n\r\nIt's used e.g. here:\r\n\r\nhttps://github.com/simonw/datasette/blob/49184c569cd70efbda4f3f062afef3a34401d8d5/datasette/app.py#L1371-L1375\r\n\r\nThe `BaseView` thing meanwhile works like this:\r\n\r\nhttps://github.com/simonw/datasette/blob/d97e82df3c8a3f2e97038d7080167be9bb74a68d/datasette/views/base.py#L56-L157", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726236847, "label": "Resolve the difference between `wrap_view()` and `BaseView`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/554#issuecomment-1557607516", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/554", "id": 1557607516, "node_id": "IC_kwDOCGYnMM5c1zRc", "user": {"value": 1231935, "label": "xavdid"}, "created_at": "2023-05-22T17:18:33Z", "updated_at": "2023-05-22T17:18:33Z", "author_association": "NONE", "body": "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:\r\n\r\n```py\r\ndef save_items(table, items):\r\n db[\"users\"].insert(build_user(items[0]), pk=\"id\",ignore=True)\r\n db[table].insert_all(items)\r\n\r\nif comments := fetch_comments():\r\n save_items('comments', comments)\r\n\r\nif posts := fetch_posts():\r\n save_items('posts', posts)\r\n```\r\n\r\nSo 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.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1720096994, "label": "`IndexError` when doing `.insert(..., pk='id')` after `insert_all`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/pull/2077#issuecomment-1613290899", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2077", "id": 1613290899, "node_id": "IC_kwDOBm6k_c5gKN2T", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-06-29T14:32:16Z", "updated_at": "2023-06-29T14:32:16Z", "author_association": "OWNER", "body": "@dependabot recreate", "reactions": "{\"total_count\": 1, \"+1\": 1, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1719759468, "label": "Bump furo from 2023.3.27 to 2023.5.20"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/pull/2077#issuecomment-1557289070", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2077", "id": 1557289070, "node_id": "IC_kwDOBm6k_c5c0lhu", "user": {"value": 22429695, "label": "codecov[bot]"}, "created_at": "2023-05-22T14:08:33Z", "updated_at": "2023-06-29T14:40:35Z", "author_association": "NONE", "body": "## [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\nPatch and project coverage have no change.\n> 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%.\n\n
Additional details and impacted files\n\n\n```diff\n@@ Coverage Diff @@\n## main #2077 +/- ##\n=======================================\n Coverage 92.40% 92.40% \n=======================================\n Files 39 39 \n Lines 5803 5803 \n=======================================\n Hits 5362 5362 \n Misses 441 441 \n```\n\n\n\n\n
\n\n[: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). \n: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).\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1719759468, "label": "Bump furo from 2023.3.27 to 2023.5.20"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/pull/553#issuecomment-1556288300", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/553", "id": 1556288300, "node_id": "IC_kwDOCGYnMM5cwxMs", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-21T20:48:01Z", "updated_at": "2023-05-21T20:48:01Z", "author_association": "OWNER", "body": "If https://sqlite-utils--553.org.readthedocs.build/en/553/cli.html#running-sql-queries looks good I can merge this.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1718635018, "label": "Reformatted CLI examples in docs"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/pull/553#issuecomment-1556287870", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/553", "id": 1556287870, "node_id": "IC_kwDOCGYnMM5cwxF-", "user": {"value": 22429695, "label": "codecov[bot]"}, "created_at": "2023-05-21T20:45:58Z", "updated_at": "2023-05-21T20:57:08Z", "author_association": "NONE", "body": "## [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\nPatch and project coverage have no change.\n> 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%.\n\n> :exclamation: Current head 0b81794 differs from pull request most recent head 21036a5. Consider uploading reports for the commit 21036a5 to get more accurate results\n\n
Additional details and impacted files\n\n\n```diff\n@@ Coverage Diff @@\n## main #553 +/- ##\n=======================================\n Coverage 96.36% 96.36% \n=======================================\n Files 6 6 \n Lines 2726 2726 \n=======================================\n Hits 2627 2627 \n Misses 99 99 \n```\n\n\n\n\n
\n\n[: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). \n: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).\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1718635018, "label": "Reformatted CLI examples in docs"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/552#issuecomment-1556292204", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/552", "id": 1556292204, "node_id": "IC_kwDOCGYnMM5cwyJs", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-21T21:05:15Z", "updated_at": "2023-05-21T21:05:15Z", "author_association": "OWNER", "body": "Now live at https://sqlite-utils.datasette.io/en/latest/installation.html#setting-up-shell-completion", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1718612569, "label": "Document how to setup shell auto-completion"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/551#issuecomment-1556291915", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/551", "id": 1556291915, "node_id": "IC_kwDOCGYnMM5cwyFL", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-21T21:04:03Z", "updated_at": "2023-05-21T21:04:03Z", "author_association": "OWNER", "body": "Now live at https://sqlite-utils.datasette.io/en/latest/cli.html", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1718607907, "label": "Make as many examples in the CLI docs as possible copy-and-pastable"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/551#issuecomment-1556288270", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/551", "id": 1556288270, "node_id": "IC_kwDOCGYnMM5cwxMO", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-21T20:47:51Z", "updated_at": "2023-05-21T20:47:51Z", "author_association": "OWNER", "body": "This page has all of the changes: https://sqlite-utils--553.org.readthedocs.build/en/553/cli.html#running-sql-queries", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1718607907, "label": "Make as many examples in the CLI docs as possible copy-and-pastable"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/551#issuecomment-1556287599", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/551", "id": 1556287599, "node_id": "IC_kwDOCGYnMM5cwxBv", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-21T20:44:55Z", "updated_at": "2023-05-21T20:44:55Z", "author_association": "OWNER", "body": "Put this in a PR so I can preview it: \r\n- #553\r\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1718607907, "label": "Make as many examples in the CLI docs as possible copy-and-pastable"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/551#issuecomment-1556265772", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/551", "id": 1556265772, "node_id": "IC_kwDOCGYnMM5cwrss", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-21T19:16:15Z", "updated_at": "2023-05-21T19:16:15Z", "author_association": "OWNER", "body": "Another option:\r\n\r\n\"image\"\r\n\r\nThat's using this markup:\r\n\r\n```\r\nNewline-delimited JSON\r\n~~~~~~~~~~~~~~~~~~~~~~\r\n\r\nUse ``--nl`` to get back newline-delimited JSON objects:\r\n\r\n.. code-block:: bash\r\n\r\n sqlite-utils dogs.db \"select * from dogs\" --nl\r\n\r\n.. code-block:: output\r\n\r\n {\"id\": 1, \"age\": 4, \"name\": \"Cleo\"}\r\n {\"id\": 2, \"age\": 2, \"name\": \"Pancakes\"}\r\n```\r\nAnd this extra CSS:\r\n\r\n```css\r\n.highlight-output .highlight {\r\n border-left: 9px solid #30c94f;\r\n}\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1718607907, "label": "Make as many examples in the CLI docs as possible copy-and-pastable"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/551#issuecomment-1556263182", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/551", "id": 1556263182, "node_id": "IC_kwDOCGYnMM5cwrEO", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-21T19:06:48Z", "updated_at": "2023-05-21T19:06:48Z", "author_association": "OWNER", "body": "I could split them up into two blocks like this:\r\n\r\n\"image\"\r\n\r\nI do miss the visual indication that one of these is the command and one is the output though.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1718607907, "label": "Make as many examples in the CLI docs as possible copy-and-pastable"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/551#issuecomment-1556262574", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/551", "id": 1556262574, "node_id": "IC_kwDOCGYnMM5cwq6u", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-21T19:04:59Z", "updated_at": "2023-05-21T19:04:59Z", "author_association": "OWNER", "body": "I wrote the docs like this because early examples include both the command and its output:\r\n\r\nhttps://sqlite-utils.datasette.io/en/stable/cli.html#returning-json\r\n\r\n\"image\"", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1718607907, "label": "Make as many examples in the CLI docs as possible copy-and-pastable"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/550#issuecomment-1556255309", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/550", "id": 1556255309, "node_id": "IC_kwDOCGYnMM5cwpJN", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-21T18:42:25Z", "updated_at": "2023-05-21T18:42:25Z", "author_association": "OWNER", "body": "Tests passed here: https://github.com/simonw/sqlite-utils/actions/runs/5039119716", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1718595700, "label": "AttributeError: 'EntryPoints' object has no attribute 'get' for flake8 on Python 3.7"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/550#issuecomment-1556250236", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/550", "id": 1556250236, "node_id": "IC_kwDOCGYnMM5cwn58", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-21T18:25:26Z", "updated_at": "2023-05-21T18:25:26Z", "author_association": "OWNER", "body": "Relevant issues:\r\n- https://github.com/python/importlib_metadata/issues/406\r\n- https://github.com/PyCQA/flake8/issues/1701\r\n\r\nIt looks to me like this is only a problem for `flake8` on Python 3.7 - 3.8 and higher work OK.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1718595700, "label": "AttributeError: 'EntryPoints' object has no attribute 'get' for flake8 on Python 3.7"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/550#issuecomment-1556249984", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/550", "id": 1556249984, "node_id": "IC_kwDOCGYnMM5cwn2A", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-21T18:24:48Z", "updated_at": "2023-05-21T18:24:48Z", "author_association": "OWNER", "body": "This is blocking:\r\n- #549", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1718595700, "label": "AttributeError: 'EntryPoints' object has no attribute 'get' for flake8 on Python 3.7"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/pull/549#issuecomment-1556242262", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/549", "id": 1556242262, "node_id": "IC_kwDOCGYnMM5cwl9W", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-21T18:00:05Z", "updated_at": "2023-05-21T18:00:05Z", "author_association": "OWNER", "body": "Failing `mypy` test: https://github.com/simonw/sqlite-utils/actions/runs/5038983349/jobs/9036828465\r\n```\r\nsqlite_utils/cli.py:37: error: Skipping analyzing \"trogon\": module is installed, but missing library stubs or py.typed marker [import]\r\nsqlite_utils/cli.py:37: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports\r\nFound 1 error in 1 file (checked 52 source files)\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1718586377, "label": "TUI powered by Trogon"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/pull/549#issuecomment-1556241812", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/549", "id": 1556241812, "node_id": "IC_kwDOCGYnMM5cwl2U", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-21T17:58:25Z", "updated_at": "2023-05-21T17:58:25Z", "author_association": "OWNER", "body": "Documentation: https://sqlite-utils--549.org.readthedocs.build/en/549/cli.html#cli-tui", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1718586377, "label": "TUI powered by Trogon"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/pull/549#issuecomment-1556241555", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/549", "id": 1556241555, "node_id": "IC_kwDOCGYnMM5cwlyT", "user": {"value": 22429695, "label": "codecov[bot]"}, "created_at": "2023-05-21T17:57:24Z", "updated_at": "2023-05-21T18:28:44Z", "author_association": "NONE", "body": "## [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\nPatch coverage: **`83.33`**% and project coverage change: **`+0.06`** :tada:\n> 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%.\n\n
Additional details and impacted files\n\n\n```diff\n@@ Coverage Diff @@\n## main #549 +/- ##\n==========================================\n+ Coverage 96.30% 96.36% +0.06% \n==========================================\n Files 6 6 \n Lines 2707 2726 +19 \n==========================================\n+ Hits 2607 2627 +20 \n+ Misses 100 99 -1 \n```\n\n\n| [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 \u0394 | |\n|---|---|---|\n| [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: |\n\n... 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)\n\n\n
\n\n[: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). \n: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).\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1718586377, "label": "TUI powered by Trogon"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/548#issuecomment-1556231832", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/548", "id": 1556231832, "node_id": "IC_kwDOCGYnMM5cwjaY", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-21T17:24:13Z", "updated_at": "2023-05-21T17:24:13Z", "author_association": "OWNER", "body": "Oh, I see why that is now:\r\n\r\nhttps://github.com/simonw/sqlite-utils/blob/6027f3ea6939a399aeef2578fca17efec0e539df/sqlite_utils/cli.py#L2670-L2679\r\n\r\nThis is because of the following command:\r\n\r\n sqlite-utils analyze-tables table1 table2 --column x\r\n\r\nSince 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.\r\n\r\nI'm going to change this so if the column is not on ANY of those tables you get an error.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1718576761, "label": "analyze-tables should validate provide --column names"}, "performed_via_github_app": null}