html_url,issue_url,id,node_id,user,created_at,updated_at,author_association,body,reactions,issue,performed_via_github_app
https://github.com/simonw/datasette/issues/2054#issuecomment-1500608101,https://api.github.com/repos/simonw/datasette/issues/2054,1500608101,IC_kwDOBm6k_c5ZcXZl,9599,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,
https://github.com/simonw/datasette/issues/2054#issuecomment-1499604822,https://api.github.com/repos/simonw/datasette/issues/2054,1499604822,IC_kwDOBm6k_c5ZYidW,9599,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,
https://github.com/simonw/datasette/issues/2054#issuecomment-1499604066,https://api.github.com/repos/simonw/datasette/issues/2054,1499604066,IC_kwDOBm6k_c5ZYiRi,9599,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,
https://github.com/simonw/datasette/issues/2054#issuecomment-1499491258,https://api.github.com/repos/simonw/datasette/issues/2054,1499491258,IC_kwDOBm6k_c5ZYGu6,9599,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,
https://github.com/simonw/datasette/issues/2054#issuecomment-1499596941,https://api.github.com/repos/simonw/datasette/issues/2054,1499596941,IC_kwDOBm6k_c5ZYgiN,9599,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,
https://github.com/simonw/datasette/issues/2054#issuecomment-1499530921,https://api.github.com/repos/simonw/datasette/issues/2054,1499530921,IC_kwDOBm6k_c5ZYQap,9599,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,
https://github.com/simonw/datasette/issues/2054#issuecomment-1499467703,https://api.github.com/repos/simonw/datasette/issues/2054,1499467703,IC_kwDOBm6k_c5ZYA-3,9599,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,
https://github.com/simonw/datasette/issues/2054#issuecomment-1499465648,https://api.github.com/repos/simonw/datasette/issues/2054,1499465648,IC_kwDOBm6k_c5ZYAew,9599,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,
https://github.com/simonw/datasette/issues/2054#issuecomment-1499462324,https://api.github.com/repos/simonw/datasette/issues/2054,1499462324,IC_kwDOBm6k_c5ZX_q0,9599,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,
https://github.com/simonw/datasette/issues/2054#issuecomment-1499457291,https://api.github.com/repos/simonw/datasette/issues/2054,1499457291,IC_kwDOBm6k_c5ZX-cL,9599,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,
https://github.com/simonw/datasette/issues/2054#issuecomment-1499457201,https://api.github.com/repos/simonw/datasette/issues/2054,1499457201,IC_kwDOBm6k_c5ZX-ax,9599,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,
https://github.com/simonw/datasette/issues/2054#issuecomment-1499452122,https://api.github.com/repos/simonw/datasette/issues/2054,1499452122,IC_kwDOBm6k_c5ZX9La,9599,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,