-
-
Notifications
You must be signed in to change notification settings - Fork 691
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make detailed notes on how table, query and row views work right now #2054
Comments
I'm going to make notes against the code in the most recent alpha release, ignoring the recent work I did to refactor https://github.com/simonw/datasette/tree/1.0a2/datasette/views |
These classes - |
Here's datasette/datasette/views/base.py Lines 56 to 145 in 8b9d7fd
It has methods for the datasette/datasette/views/base.py Lines 71 to 81 in 8b9d7fd
Also adds CORS headers to anything if CORS mode is on: datasette/datasette/views/base.py Lines 106 to 107 in 8b9d7fd
And adds the datasette/datasette/views/base.py Lines 112 to 122 in 8b9d7fd
And has special code for setting the datasette/datasette/views/base.py Lines 124 to 136 in 8b9d7fd
|
The datasette/datasette/views/base.py Lines 160 to 544 in 8b9d7fd
It has a datasette/datasette/views/base.py Lines 163 to 172 in 8b9d7fd
It uses this method a lot, which has to be over-ridden in the datasette/datasette/views/base.py Lines 174 to 175 in 8b9d7fd
This method: datasette/datasette/views/base.py Line 180 in 8b9d7fd
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 The Plus it catches interrupted queries and returns a special error page for those (and other error messages too): datasette/datasette/views/base.py Lines 381 to 408 in 8b9d7fd
It adds the time taken to execute the queries: datasette/datasette/views/base.py Lines 410 to 411 in 8b9d7fd
|
There are actually five classes that subclass datasette/datasette/views/row.py Line 16 in 8b9d7fd
datasette/datasette/views/database.py Line 34 in 8b9d7fd
datasette/datasette/views/database.py Line 172 in 8b9d7fd
datasette/datasette/views/database.py Line 215 in 8b9d7fd
datasette/datasette/views/table.py Line 72 in 8b9d7fd
I don't think |
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. |
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:
Flask: 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: 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: 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: 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: 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: 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.") from django.urls import path
from .views import ExampleView
urlpatterns = [
path('example/', ExampleView.as_view(), name='example'),
] Rocket: #![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: 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: 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: 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) |
Turned this into a TIL: https://til.simonwillison.net/gpt3/gpt4-api-design |
I think there are two main approaches:
The decorator approach is calling out to me a bit now. |
I'm contemplating a new approach: using a class with static methods. Something like this: 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 It can offer default HEAD and OPTIONS methods too. |
I actually quite like that. I could even use 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. |
you should have a look at Roda written in ruby . |
Ooh that one's really interesting - very different from the others: # 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 |
Research to help influence the following:
The text was updated successfully, but these errors were encountered: