Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit d6d3e80

Browse files
committedApr 4, 2024
chore(python): generate model for POST endpoints
Part of #16
1 parent 1c99473 commit d6d3e80

File tree

2 files changed

+129
-2
lines changed

2 files changed

+129
-2
lines changed
 

‎examples/python/fastapi/postgres/routes.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,18 @@
33

44
from fastapi import APIRouter, Depends, HTTPException, status
55

6+
from pydantic import BaseModel
7+
68
from db import db_connection
79

810
router = APIRouter()
911

12+
class CreateCategoryDto(BaseModel):
13+
name: str
14+
name_ru: str
15+
slug: str
16+
user_id: int
17+
1018

1119
@router.get('/v1/categories/count')
1220
def get_v1_categories_count(conn=Depends(db_connection)):
@@ -82,7 +90,7 @@ def get_list_v1_categories(limit, conn=Depends(db_connection)):
8290

8391

8492
@router.post('/v1/categories', status_code = status.HTTP_204_NO_CONTENT)
85-
def post_v1_categories():
93+
def post_v1_categories(payload: CreateCategoryDto):
8694
pass
8795

8896

‎src/templates/routes.py.ejs

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import psycopg2.extras
44
<%# https://fastapi.tiangolo.com/reference/status/ -%>
55
from fastapi import APIRouter, Depends, HTTPException, status
66
7+
<%# LATER: add only when POST/PUT endpoints are present -%>
8+
from pydantic import BaseModel
9+
710
from db import db_connection
811

912
router = APIRouter()
@@ -38,7 +41,119 @@ function formatQueryForPython(query, indentLevel) {
3841
return `"${sql}"`
3942
}
4043
44+
// {'values':
45+
// [
46+
// {
47+
// type: 'expr_list',
48+
// value: [ { type: 'param', value: 'user_id' } ]
49+
// }
50+
// ]
51+
// } => [ 'user_id' ]
52+
function extractInsertValues(queryAst) {
53+
const values = queryAst.values.flatMap(elem => elem.value)
54+
.map(elem => elem.type === 'param' ? elem.value : null)
55+
.filter(elem => elem); // filter out nulls
56+
return Array.from(new Set(values));
57+
}
58+
59+
// LATER: reduce duplication with routes.go.ejs
60+
function extractProperties(queryAst) {
61+
if (queryAst.type === 'insert') {
62+
return extractInsertValues(queryAst);
63+
}
64+
return [];
65+
}
66+
67+
// LATER: try to reduce duplication with routes.go.ejs
68+
function findOutType(fieldsInfo, fieldName) {
69+
const defaultType = 'str';
70+
const hasTypeInfo = fieldsInfo.hasOwnProperty(fieldName) && fieldsInfo[fieldName].hasOwnProperty('type');
71+
if (hasTypeInfo && fieldsInfo[fieldName].type === 'integer') {
72+
return 'int';
73+
}
74+
return defaultType;
75+
}
76+
77+
// LATER: reduce duplication with routes.go.ejs
78+
function addTypes(props, fieldsInfo) {
79+
return props.map(prop => {
80+
return {
81+
"name": prop,
82+
"type": findOutType(fieldsInfo, prop),
83+
}
84+
});
85+
}
86+
87+
// LATER: reduce duplication with routes.go.ejs
88+
function query2dto(parser, method) {
89+
const query = removePlaceholders(method.query);
90+
const queryAst = parser.astify(query);
91+
const props = extractProperties(queryAst);
92+
if (props.length === 0) {
93+
console.warn('Could not create DTO for query:', formatQuery(query));
94+
console.debug('Query AST:');
95+
console.debug(queryAst);
96+
return null;
97+
}
98+
const fieldsInfo = method.dto && method.dto.fields ? method.dto.fields : {};
99+
const propsWithTypes = addTypes(props, fieldsInfo);
100+
const hasName = method.dto && method.dto.name && method.dto.name.length > 0;
101+
const name = hasName ? method.dto.name : "Dto" + ++globalDtoCounter;
102+
return {
103+
"name": name,
104+
"hasUserProvidedName": hasName,
105+
"props": propsWithTypes,
106+
// required for de-duplication
107+
// [ {name:foo, type:int}, {name:bar, type:string} ] => "foo=int bar=string"
108+
// LATER: sort before join
109+
"signature": propsWithTypes.map(field => `${field.name}=${field.type}`).join(' ')
110+
};
111+
}
112+
113+
// https://fastapi.tiangolo.com/tutorial/body/
114+
function dto2model(dto) {
115+
let result = `class ${dto.name}(BaseModel):\n`;
116+
dto.props.forEach(prop => {
117+
result += ` ${prop.name}: ${prop.type}\n`
118+
});
119+
return result;
120+
}
121+
122+
let globalDtoCounter = 0;
123+
const dtoCache = {};
124+
125+
// LATER: reduce duplication with routes.go.ejs
126+
function cacheDto(dto) {
127+
dtoCache[dto.signature] = dto.name;
128+
return dto;
129+
}
130+
131+
// LATER: reduce duplication with routes.go.ejs
132+
function dtoInCache(dto) {
133+
// always prefer user specified name even when we have a similar DTO in cache
134+
if (dto.hasUserProvidedName) {
135+
return false;
136+
}
137+
return dtoCache.hasOwnProperty(dto.signature);
138+
}
41139
140+
// Generate models
141+
const verbs_with_dto = [ 'post' ]
142+
endpoints.forEach(function(endpoint) {
143+
const dtos = endpoint.methods
144+
.filter(method => verbs_with_dto.includes(method.verb))
145+
.map(method => query2dto(sqlParser, method))
146+
.filter(elem => elem) // filter out nulls
147+
.filter(dto => !dtoInCache(dto))
148+
.map(dto => dto2model(cacheDto(dto)))
149+
.forEach(model => {
150+
%>
151+
<%- model -%>
152+
<%
153+
});
154+
});
155+
156+
// Generate endpoints
42157
endpoints.forEach(function(endpoint) {
43158
const path = convertToFastApiPath(endpoint.path)
44159
const argsFromPath = extractParamsFromPath(endpoint.path)
@@ -119,10 +234,14 @@ def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>):
119234
<%
120235
}
121236
if (method.name === 'post') {
237+
const dto = query2dto(sqlParser, method);
238+
// LATER: do we really need signature and cache?
239+
const cacheKey = dto ? dto.signature : null;
240+
const model = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name;
122241
%>
123242
124243
@router.post('<%- path %>', status_code = status.HTTP_204_NO_CONTENT)
125-
def <%- pythonMethodName %>():
244+
def <%- pythonMethodName %>(payload: <%- model %>):
126245
pass
127246
<%
128247

0 commit comments

Comments
 (0)
Please sign in to comment.