{"html_url": "https://github.com/simonw/datasette/issues/2054#issuecomment-1499452122", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2054", "id": 1499452122, "node_id": "IC_kwDOBm6k_c5ZX9La", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-04-06T18:21:51Z", "updated_at": "2023-04-06T18:21:51Z", "author_association": "OWNER", "body": "I'm going to make notes against the code in the most recent alpha release, ignoring the recent work I did to refactor `TableView`.\r\n\r\nhttps://github.com/simonw/datasette/tree/1.0a2/datasette/views", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1657861026, "label": "Make detailed notes on how table, query and row views work right now"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2054#issuecomment-1499457201", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2054", "id": 1499457201, "node_id": "IC_kwDOBm6k_c5ZX-ax", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-04-06T18:26:39Z", "updated_at": "2023-04-06T18:26:39Z", "author_association": "OWNER", "body": "These classes - `TableView` and `RowView` and `QueryView` - all subclass `DataView` which subclasses` BaseView`.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1657861026, "label": "Make detailed notes on how table, query and row views work right now"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2054#issuecomment-1499457291", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2054", "id": 1499457291, "node_id": "IC_kwDOBm6k_c5ZX-cL", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-04-06T18:26:45Z", "updated_at": "2023-04-06T18:26:45Z", "author_association": "OWNER", "body": "\r\nHere's `BaseView`:\r\n\r\nhttps://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/base.py#L56-L145\r\n\r\nIt has methods for the `options`, `get`, `post`, `delete`, `put`, `patch` and `head` HTTP verbs, most defaulting to returinng a 405 Method not allowed message in plain text or JSON, depending on this check:\r\n\r\nhttps://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/base.py#L71-L81\r\n\r\nAlso adds CORS headers to anything if CORS mode is on:\r\n\r\nhttps://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/base.py#L106-L107\r\n\r\nAnd adds the `database_color` (weirdly) and the `select_templates` variables to the template context:\r\n\r\nhttps://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/base.py#L112-L122\r\n\r\nAnd has special code for setting the `Link: ...; rel=\"alternate\"` HTTP header:\r\n\r\nhttps://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/base.py#L124-L136", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1657861026, "label": "Make detailed notes on how table, query and row views work right now"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2054#issuecomment-1499462324", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2054", "id": 1499462324, "node_id": "IC_kwDOBm6k_c5ZX_q0", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-04-06T18:31:56Z", "updated_at": "2023-04-06T18:31:56Z", "author_association": "OWNER", "body": "The `DataView` class does a LOT of work - mostly involving CSV responses.\r\n\r\nhttps://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/base.py#L160-L544\r\n\r\nIt has a `redirect()` method with some complex logic and CORS handling:\r\n\r\nhttps://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/base.py#L163-L172\r\n\r\nIt uses this method a lot, which has to be over-ridden in the `TableView` and `RowView` and `QueryView` subclasses:\r\n\r\nhttps://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/base.py#L174-L175\r\n\r\nThis method:\r\n\r\nhttps://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/base.py#L180\r\n\r\nIs the bulk of the complexity, because it knows how to both turn a list of SQLite rows into a CSV file but also knows how to call `.data()` repeatedly with different pagination arguments in order to stream CSV back for a large table.\r\n\r\nThe `async def get()` method for GET requests is also very complicated. It mainly handles format stuff - knowing how to render HTML v.s. JSON v.s. CSV v.s. other formats specified using this plugin hook: https://docs.datasette.io/en/1.0a2/plugin_hooks.html#register-output-renderer-datasette\r\n\r\nPlus it catches interrupted queries and returns a special error page for those (and other error messages too): https://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/base.py#L381-L408\r\n\r\nIt adds the time taken to execute the queries: https://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/base.py#L410-L411", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1657861026, "label": "Make detailed notes on how table, query and row views work right now"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2054#issuecomment-1499465648", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2054", "id": 1499465648, "node_id": "IC_kwDOBm6k_c5ZYAew", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-04-06T18:35:03Z", "updated_at": "2023-04-06T18:35:03Z", "author_association": "OWNER", "body": "There are actually five classes that subclass `DataView`:\r\n\r\nhttps://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/row.py#L16\r\n\r\nhttps://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/database.py#L34\r\n\r\nhttps://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/database.py#L172\r\n\r\nhttps://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/database.py#L215\r\n\r\nhttps://github.com/simonw/datasette/blob/8b9d7fdbd8de7e74414cc29e3005382669a812dc/datasette/views/table.py#L72\r\n\r\nI don't think `DatabaseView` and `DatabaseDownload` should have subclassed that at all, since they don't return a table of data.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1657861026, "label": "Make detailed notes on how table, query and row views work right now"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2054#issuecomment-1499467703", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2054", "id": 1499467703, "node_id": "IC_kwDOBm6k_c5ZYA-3", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-04-06T18:37:22Z", "updated_at": "2023-04-06T18:57:22Z", "author_association": "OWNER", "body": "I've been hoping to move entirely away from class-based views, but now I'm wondering if that's the right decision given the need to support HTTP verbs.\r\n\r\nIt is cleaner to have a class that has methods for each verb, or to have a single function that can behave differently depending on the verb?\r\n\r\nOr should I have a mechanism for dispatching to separate functions based on the verbs that isn't implemented in a base class?\r\n\r\nThe trouble with using base classes is that I've already shown that they tempt me to put logic in weird places, which makes code harder to modify later on.\r\n\r\nThe thing here is that the ONLY thing I want to use the base class for is as an HTTP verb switcher - which makes me doubt that I should risk taking on the other temptations of having a base class.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1657861026, "label": "Make detailed notes on how table, query and row views work right now"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2054#issuecomment-1499491258", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2054", "id": 1499491258, "node_id": "IC_kwDOBm6k_c5ZYGu6", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-04-06T19:00:16Z", "updated_at": "2023-04-06T20:40:16Z", "author_association": "OWNER", "body": "I asked GPT-4 to compare URL routing based on path and verb approaches in different languages and frameworks and spit out some examples. Full transcript here: https://gist.github.com/simonw/fa2379b97420404a81b0fcdb4db79657\r\n\r\nInitial prompt:\r\n\r\n> Show me code examples of different web frameworks in Python and JavaScript and Go illustrating how HTTP routing works - in particular the problem of mapping an incoming HTTP request to some code based on both the URL path and the HTTP verb\r\n>\r\n> Don't show installation instructions, just get straight to the code snippets\r\n\r\nFlask:\r\n```python\r\nfrom flask import Flask\r\n\r\napp = Flask(__name__)\r\n\r\n@app.route('/example', methods=['GET'])\r\ndef get_example():\r\n return \"This is a GET request.\"\r\n\r\n@app.route('/example', methods=['POST'])\r\ndef post_example():\r\n return \"This is a POST request.\"\r\n\r\nif __name__ == '__main__':\r\n app.run()\r\n```\r\nExpress:\r\n```javascript\r\nconst express = require('express');\r\nconst app = express();\r\n\r\napp.get('/example', (req, res) => {\r\n res.send('This is a GET request.');\r\n});\r\n\r\napp.post('/example', (req, res) => {\r\n res.send('This is a POST request.');\r\n});\r\n\r\napp.listen(3000, () => {\r\n console.log('Server is running on port 3000');\r\n});\r\n```\r\nGin:\r\n```go\r\npackage main\r\n\r\nimport (\r\n\t\"github.com/gin-gonic/gin\"\r\n)\r\n\r\nfunc main() {\r\n\tr := gin.Default()\r\n\r\n\tr.GET(\"/example\", func(c *gin.Context) {\r\n\t\tc.String(200, \"This is a GET request.\")\r\n\t})\r\n\r\n\tr.POST(\"/example\", func(c *gin.Context) {\r\n\t\tc.String(200, \"This is a POST request.\")\r\n\t})\r\n\r\n\tr.Run()\r\n}\r\n```\r\nActix-web:\r\n```rust\r\nuse actix_web::{web, App, HttpResponse, HttpServer, Responder};\r\n\r\nasync fn get_example() -> impl Responder {\r\n HttpResponse::Ok().body(\"This is a GET request.\")\r\n}\r\n\r\nasync fn post_example() -> impl Responder {\r\n HttpResponse::Ok().body(\"This is a POST request.\")\r\n}\r\n\r\n#[actix_web::main]\r\nasync fn main() -> std::io::Result<()> {\r\n HttpServer::new(|| {\r\n App::new()\r\n .route(\"/example\", web::get().to(get_example))\r\n .route(\"/example\", web::post().to(post_example))\r\n })\r\n .bind(\"127.0.0.1:8080\")?\r\n .run()\r\n .await\r\n}\r\n```\r\nKoa:\r\n```javascript\r\nconst Koa = require('koa');\r\nconst Router = require('koa-router');\r\n\r\nconst app = new Koa();\r\nconst router = new Router();\r\n\r\nrouter.get('/example', (ctx) => {\r\n ctx.body = 'This is a GET request.';\r\n});\r\n\r\nrouter.post('/example', (ctx) => {\r\n ctx.body = 'This is a POST request.';\r\n});\r\n\r\napp.use(router.routes());\r\napp.use(router.allowedMethods());\r\n\r\napp.listen(3000, () => {\r\n console.log('Server is running on port 3000');\r\n});\r\n```\r\nDjango:\r\n```python\r\nfrom django.http import HttpResponse\r\nfrom django.views import View\r\n\r\nclass ExampleView(View):\r\n def get(self, request):\r\n return HttpResponse(\"This is a GET request.\")\r\n\r\n def post(self, request):\r\n return HttpResponse(\"This is a POST request.\")\r\n```\r\n```python\r\nfrom django.urls import path\r\nfrom .views import ExampleView\r\n\r\nurlpatterns = [\r\n path('example/', ExampleView.as_view(), name='example'),\r\n]\r\n```\r\nRocket:\r\n```rust\r\n#![feature(proc_macro_hygiene, decl_macro)]\r\n\r\n#[macro_use] extern crate rocket;\r\n\r\nuse rocket::response::content;\r\n\r\n#[get(\"/example\")]\r\nfn get_example() -> content::Plain<&'static str> {\r\n content::Plain(\"This is a GET request.\")\r\n}\r\n\r\n#[post(\"/example\")]\r\nfn post_example() -> content::Plain<&'static str> {\r\n content::Plain(\"This is a POST request.\")\r\n}\r\n\r\n#[launch]\r\nfn rocket() -> _ {\r\n rocket::build().mount(\"/\", routes![get_example, post_example])\r\n}\r\n```\r\nElixir/Phoenix:\r\n```elixir\r\ndefmodule ExampleAppWeb.Router do\r\n use ExampleAppWeb, :router\r\n\r\n pipeline :api do\r\n plug :accepts, [\"json\"]\r\n end\r\n\r\n scope \"/api\", ExampleAppWeb do\r\n pipe_through :api\r\n\r\n get \"/example\", ExampleController, :get_example\r\n post \"/example\", ExampleController, :post_example\r\n end\r\nend\r\n```\r\nBottle:\r\n```python\r\nfrom bottle import Bottle, run\r\n\r\napp = Bottle()\r\n\r\n@app.route('/example', method='GET')\r\ndef get_example():\r\n return \"This is a GET request.\"\r\n\r\n@app.route('/example', method='POST')\r\ndef post_example():\r\n return \"This is a POST request.\"\r\n\r\nif __name__ == '__main__':\r\n run(app, host='localhost', port=8080)\r\n```\r\nFastAPI:\r\n```python\r\nfrom fastapi import FastAPI\r\n\r\napp = FastAPI()\r\n\r\n@app.get(\"/example\")\r\nasync def get_example():\r\n return {\"message\": \"This is a GET request.\"}\r\n\r\n@app.post(\"/example\")\r\nasync def post_example():\r\n return {\"message\": \"This is a POST request.\"}\r\n\r\nif __name__ == \"__main__\":\r\n import uvicorn\r\n uvicorn.run(app, host=\"127.0.0.1\", port=8000)\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1657861026, "label": "Make detailed notes on how table, query and row views work right now"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2054#issuecomment-1499530921", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2054", "id": 1499530921, "node_id": "IC_kwDOBm6k_c5ZYQap", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-04-06T19:41:52Z", "updated_at": "2023-04-06T19:42:09Z", "author_association": "OWNER", "body": "Turned this into a TIL: https://til.simonwillison.net/gpt3/gpt4-api-design", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1657861026, "label": "Make detailed notes on how table, query and row views work right now"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2054#issuecomment-1499596941", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2054", "id": 1499596941, "node_id": "IC_kwDOBm6k_c5ZYgiN", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-04-06T20:39:35Z", "updated_at": "2023-04-06T20:39:35Z", "author_association": "OWNER", "body": "I think there are two main approaches:\r\n\r\n- Map path to a function, that function then behaves differently on GET/POST/etc\r\n- Map method AND path to a function - the `app.get('/example')` v.s. `app.post('/example')` ones\r\n\r\nThe decorator approach is calling out to me a bit now.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1657861026, "label": "Make detailed notes on how table, query and row views work right now"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2054#issuecomment-1499604066", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2054", "id": 1499604066, "node_id": "IC_kwDOBm6k_c5ZYiRi", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-04-06T20:47:30Z", "updated_at": "2023-04-06T20:47:30Z", "author_association": "OWNER", "body": "I'm contemplating a new approach: using a class with static methods. Something like this:\r\n\r\n```python\r\nclass TableView(MethodRouter):\r\n @staticmethod\r\n async def get(request):\r\n return Response.text(\"GET\")\r\n\r\n @staticmethod\r\n async def post(request):\r\n return Response.text(\"POST\")\r\n```\r\nSo effectively the class is just there to bundle together verb implementations, and to provide a `route(request)` method which knows how to dispatch them to the right place.\r\n\r\nIt can offer default HEAD and OPTIONS methods too.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1657861026, "label": "Make detailed notes on how table, query and row views work right now"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2054#issuecomment-1499604822", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2054", "id": 1499604822, "node_id": "IC_kwDOBm6k_c5ZYidW", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-04-06T20:48:19Z", "updated_at": "2023-04-06T20:48:55Z", "author_association": "OWNER", "body": "I actually quite like that. I could even use `@classmethod` and have utility methods defined on that class that both `get()` and `post()` could call.\r\n\r\nThe crucial rule here is NO INSTANCE STATE - that's what makes routing to classes particularly damaging, and encourages code that's hard to maintain.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1657861026, "label": "Make detailed notes on how table, query and row views work right now"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2054#issuecomment-1499797384", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2054", "id": 1499797384, "node_id": "IC_kwDOBm6k_c5ZZReI", "user": {"value": 6213, "label": "dsisnero"}, "created_at": "2023-04-07T00:46:50Z", "updated_at": "2023-04-07T00:46:50Z", "author_association": "NONE", "body": "you should have a look at Roda written in ruby . ", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1657861026, "label": "Make detailed notes on how table, query and row views work right now"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2054#issuecomment-1500608101", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2054", "id": 1500608101, "node_id": "IC_kwDOBm6k_c5ZcXZl", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-04-07T20:14:38Z", "updated_at": "2023-04-07T20:14:38Z", "author_association": "OWNER", "body": "Ooh that one's really interesting - very different from the others:\r\n```ruby\r\n# app.rb\r\nrequire \"roda\"\r\n\r\nclass App < Roda\r\n route do |r|\r\n r.root do\r\n \"Home page\"\r\n end\r\n\r\n r.on \"pages\" do\r\n r.get \":slug\" do |slug|\r\n \"Page: #{slug}\"\r\n end\r\n end\r\n\r\n r.on \"news\" do\r\n r.get \":yyyy/:mm/:dd\" do |yyyy, mm, dd|\r\n \"News for #{yyyy}/#{mm}/#{dd}\"\r\n end\r\n end\r\n end\r\nend\r\n\r\n# config.ru\r\nrequire_relative \"app\"\r\nrun App.freeze.app\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1657861026, "label": "Make detailed notes on how table, query and row views work right now"}, "performed_via_github_app": null}