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