@kernlang/python 3.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE +678 -0
  2. package/README.md +26 -0
  3. package/dist/codegen-body-python.d.ts +152 -0
  4. package/dist/codegen-body-python.js +1648 -0
  5. package/dist/codegen-body-python.js.map +1 -0
  6. package/dist/codegen-helpers.d.ts +21 -0
  7. package/dist/codegen-helpers.js +352 -0
  8. package/dist/codegen-helpers.js.map +1 -0
  9. package/dist/codegen-python.d.ts +17 -0
  10. package/dist/codegen-python.js +106 -0
  11. package/dist/codegen-python.js.map +1 -0
  12. package/dist/fastapi-middleware.d.ts +8 -0
  13. package/dist/fastapi-middleware.js +87 -0
  14. package/dist/fastapi-middleware.js.map +1 -0
  15. package/dist/fastapi-portable.d.ts +9 -0
  16. package/dist/fastapi-portable.js +295 -0
  17. package/dist/fastapi-portable.js.map +1 -0
  18. package/dist/fastapi-raw-handler.d.ts +28 -0
  19. package/dist/fastapi-raw-handler.js +282 -0
  20. package/dist/fastapi-raw-handler.js.map +1 -0
  21. package/dist/fastapi-response.d.ts +13 -0
  22. package/dist/fastapi-response.js +150 -0
  23. package/dist/fastapi-response.js.map +1 -0
  24. package/dist/fastapi-route.d.ts +12 -0
  25. package/dist/fastapi-route.js +629 -0
  26. package/dist/fastapi-route.js.map +1 -0
  27. package/dist/fastapi-types.d.ts +39 -0
  28. package/dist/fastapi-types.js +5 -0
  29. package/dist/fastapi-types.js.map +1 -0
  30. package/dist/fastapi-utils.d.ts +16 -0
  31. package/dist/fastapi-utils.js +99 -0
  32. package/dist/fastapi-utils.js.map +1 -0
  33. package/dist/fastapi-websocket.d.ts +6 -0
  34. package/dist/fastapi-websocket.js +77 -0
  35. package/dist/fastapi-websocket.js.map +1 -0
  36. package/dist/generators/core.d.ts +23 -0
  37. package/dist/generators/core.js +906 -0
  38. package/dist/generators/core.js.map +1 -0
  39. package/dist/generators/data.d.ts +15 -0
  40. package/dist/generators/data.js +443 -0
  41. package/dist/generators/data.js.map +1 -0
  42. package/dist/generators/ground.d.ts +20 -0
  43. package/dist/generators/ground.js +333 -0
  44. package/dist/generators/ground.js.map +1 -0
  45. package/dist/generators/infra.d.ts +8 -0
  46. package/dist/generators/infra.js +109 -0
  47. package/dist/generators/infra.js.map +1 -0
  48. package/dist/index.d.ts +6 -0
  49. package/dist/index.js +7 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/ir-semantics/python-leg.d.ts +45 -0
  52. package/dist/ir-semantics/python-leg.js +291 -0
  53. package/dist/ir-semantics/python-leg.js.map +1 -0
  54. package/dist/python-stdlib-preamble.d.ts +32 -0
  55. package/dist/python-stdlib-preamble.js +86 -0
  56. package/dist/python-stdlib-preamble.js.map +1 -0
  57. package/dist/transpiler-fastapi.d.ts +8 -0
  58. package/dist/transpiler-fastapi.js +593 -0
  59. package/dist/transpiler-fastapi.js.map +1 -0
  60. package/dist/type-map.d.ts +14 -0
  61. package/dist/type-map.js +288 -0
  62. package/dist/type-map.js.map +1 -0
  63. package/package.json +37 -0
@@ -0,0 +1,629 @@
1
+ /**
2
+ * Route artifact builders for the FastAPI transpiler.
3
+ *
4
+ * generateStreamRoute — SSE streaming route
5
+ * generateTimerRoute — timeout-wrapped route
6
+ * buildRouteArtifact — main route artifact builder
7
+ */
8
+ import { getChildren, getFirstChild, getProps } from '@kernlang/core';
9
+ import { emitNativeKernBodyPythonWithImports } from './codegen-body-python.js';
10
+ import { generatePortableHandlerFastAPI } from './fastapi-portable.js';
11
+ import { hasObjectShorthandOutsideStrings, isUnsupportedJsHandlerBody, stripStringsForJsCheck, unsupportedRawHandlerBody, } from './fastapi-raw-handler.js';
12
+ import { HTTP_METHODS } from './fastapi-types.js';
13
+ import { analyzeRouteCapabilities, buildPydanticModel, buildSchema, convertPath, derivePathParams, escapePyStr, indentHandler, routeFileBase, slugify, } from './fastapi-utils.js';
14
+ import { toSnakeCase } from './type-map.js';
15
+ // ── SSE Stream code generator ────────────────────────────────────────────
16
+ export function generateStreamRoute(_routeNode, caps, method, fastapiPath, pathParams) {
17
+ const lines = [];
18
+ const handlerNode = caps.streamNode ? getFirstChild(caps.streamNode, 'handler') : undefined;
19
+ const handlerProps = handlerNode ? getProps(handlerNode) : {};
20
+ const handlerCode = typeof handlerProps.code === 'string' ? String(handlerProps.code) : '';
21
+ const paramStr = pathParams.length > 0 ? pathParams.map((p) => `${p}: str`).join(', ') : '';
22
+ lines.push(`@router.${method}("${fastapiPath}")`);
23
+ lines.push(`async def ${toSnakeCase(method)}_${slugify(fastapiPath)}(${paramStr}):`);
24
+ lines.push(` async def event_generator():`);
25
+ if (caps.hasSpawn && caps.spawnNode) {
26
+ const spawnProps = getProps(caps.spawnNode);
27
+ const binary = String(spawnProps.binary || 'echo');
28
+ const args = spawnProps.args;
29
+ const timeoutSec = Number(spawnProps.timeout) || 0;
30
+ // Security: reject dynamic binary names
31
+ if (binary.includes('{{') || binary.includes('req.') || binary.includes('request.')) {
32
+ lines.push(` # ERROR: Dynamic binary is not allowed for security. Use a static binary name.`);
33
+ lines.push(` yield "data: {\\"error\\": \\"Dynamic binary not allowed\\"}\\n\\n"`);
34
+ }
35
+ else {
36
+ lines.push(` process = await asyncio.create_subprocess_exec(`);
37
+ lines.push(` "${escapePyStr(binary)}",`);
38
+ if (args) {
39
+ const argsClean = args
40
+ .replace(/^\[|\]$/g, '')
41
+ .split(',')
42
+ .map((a) => a.trim().replace(/^['"]|['"]$/g, ''));
43
+ for (const arg of argsClean) {
44
+ lines.push(` "${escapePyStr(arg)}",`);
45
+ }
46
+ }
47
+ lines.push(` stdout=asyncio.subprocess.PIPE,`);
48
+ lines.push(` stderr=asyncio.subprocess.PIPE,`);
49
+ lines.push(` )`);
50
+ // stdout streaming with null guard
51
+ const onNodes = getChildren(caps.spawnNode, 'on');
52
+ const stdoutHandler = onNodes.find((n) => {
53
+ const op = getProps(n);
54
+ return String(op.name || op.event || '') === 'stdout';
55
+ });
56
+ lines.push(` if process.stdout:`);
57
+ if (stdoutHandler) {
58
+ const stdoutHandlerNode = getFirstChild(stdoutHandler, 'handler');
59
+ const stdoutCode = stdoutHandlerNode ? String(getProps(stdoutHandlerNode).code || '') : '';
60
+ // B7 (Codex review on 4115c0bb): if the stdout handler body is
61
+ // un-lowerable JS, hoist the NotImplementedError OUTSIDE the
62
+ // `async for chunk in process.stdout` loop. Inside the loop the
63
+ // raise would never fire if the subprocess emits zero stdout
64
+ // — silent failure. Failing fast at the generator's `if
65
+ // process.stdout:` branch makes the error path deterministic.
66
+ if (stdoutCode && isUnsupportedJsHandlerBody(stdoutCode)) {
67
+ lines.push(...unsupportedRawHandlerBody(' '));
68
+ }
69
+ else {
70
+ lines.push(` async for chunk in process.stdout:`);
71
+ if (stdoutCode) {
72
+ lines.push(...indentHandler(stdoutCode, ' '));
73
+ }
74
+ else {
75
+ lines.push(` yield f"data: {chunk.decode()}\\n\\n"`);
76
+ }
77
+ }
78
+ }
79
+ else {
80
+ lines.push(` async for chunk in process.stdout:`);
81
+ lines.push(` yield f"data: {chunk.decode()}\\n\\n"`);
82
+ }
83
+ }
84
+ lines.push(` await process.wait()`);
85
+ if (timeoutSec > 0) {
86
+ // Wrap with timeout
87
+ lines.push(` # timeout: ${timeoutSec}s`);
88
+ }
89
+ }
90
+ else if (handlerCode) {
91
+ if (isUnsupportedJsHandlerBody(handlerCode)) {
92
+ lines.push(...unsupportedRawHandlerBody(' '));
93
+ }
94
+ else {
95
+ lines.push(...indentHandler(handlerCode, ' '));
96
+ }
97
+ }
98
+ else {
99
+ lines.push(` yield "data: [DONE]\\n\\n"`);
100
+ }
101
+ lines.push(` return StreamingResponse(`);
102
+ lines.push(` event_generator(),`);
103
+ lines.push(` media_type="text/event-stream",`);
104
+ lines.push(` headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},`);
105
+ lines.push(` )`);
106
+ return lines;
107
+ }
108
+ // ── Timer code generator ─────────────────────────────────────────────────
109
+ export function generateTimerRoute(_routeNode, caps, method, fastapiPath, pathParams, handlerCode) {
110
+ const lines = [];
111
+ const timerProps = getProps(caps.timerNode);
112
+ const timeoutSec = Number(Object.values(timerProps).find((v) => typeof v === 'string' && !Number.isNaN(Number(v))) ||
113
+ timerProps.timeout ||
114
+ 15);
115
+ const timerHandlerNode = getFirstChild(caps.timerNode, 'handler');
116
+ const timerHandlerCode = timerHandlerNode ? String(getProps(timerHandlerNode).code || '') : '';
117
+ const paramStr = pathParams.length > 0 ? pathParams.map((p) => `${p}: str`).join(', ') : '';
118
+ lines.push(`@router.${method}("${fastapiPath}")`);
119
+ lines.push(`async def ${toSnakeCase(method)}_${slugify(fastapiPath)}(${paramStr}):`);
120
+ lines.push(` async def _work():`);
121
+ if (timerHandlerCode) {
122
+ lines.push(...indentHandler(timerHandlerCode, ' '));
123
+ }
124
+ if (handlerCode) {
125
+ lines.push(...indentHandler(handlerCode, ' '));
126
+ }
127
+ lines.push(` try:`);
128
+ lines.push(` return await asyncio.wait_for(_work(), timeout=${timeoutSec})`);
129
+ lines.push(` except asyncio.TimeoutError:`);
130
+ // Check for custom timeout handler
131
+ const onTimeoutNode = (caps.timerNode.children || []).find((c) => c.type === 'on' && (getProps(c).name === 'timeout' || getProps(c).event === 'timeout'));
132
+ if (onTimeoutNode) {
133
+ const timeoutHandler = getFirstChild(onTimeoutNode, 'handler');
134
+ const timeoutCode = timeoutHandler ? String(getProps(timeoutHandler).code || '') : '';
135
+ if (timeoutCode) {
136
+ lines.push(...indentHandler(timeoutCode, ' '));
137
+ }
138
+ else {
139
+ lines.push(` raise HTTPException(status_code=408, detail="Request timed out")`);
140
+ }
141
+ }
142
+ else {
143
+ lines.push(` raise HTTPException(status_code=408, detail="Request timed out")`);
144
+ }
145
+ return lines;
146
+ }
147
+ // ── Route artifact builder ───────────────────────────────────────────────
148
+ function replaceJsLiteralsOutsideStrings(expr) {
149
+ let output = '';
150
+ let index = 0;
151
+ let quote = null;
152
+ let escaped = false;
153
+ while (index < expr.length) {
154
+ const char = expr[index];
155
+ if (quote) {
156
+ output += char;
157
+ if (escaped) {
158
+ escaped = false;
159
+ }
160
+ else if (char === '\\') {
161
+ escaped = true;
162
+ }
163
+ else if (char === quote) {
164
+ quote = null;
165
+ }
166
+ index += 1;
167
+ continue;
168
+ }
169
+ if (char === '"' || char === "'" || char === '`') {
170
+ quote = char;
171
+ output += char;
172
+ index += 1;
173
+ continue;
174
+ }
175
+ if (/[A-Za-z_$]/.test(char)) {
176
+ let end = index + 1;
177
+ while (end < expr.length && /[\w$]/.test(expr[end]))
178
+ end += 1;
179
+ const word = expr.slice(index, end);
180
+ output += word === 'true' ? 'True' : word === 'false' ? 'False' : word === 'null' ? 'None' : word;
181
+ index = end;
182
+ continue;
183
+ }
184
+ output += char;
185
+ index += 1;
186
+ }
187
+ return output;
188
+ }
189
+ function quoteObjectKeysOutsideStrings(expr) {
190
+ let output = '';
191
+ let index = 0;
192
+ let quote = null;
193
+ let escaped = false;
194
+ while (index < expr.length) {
195
+ const char = expr[index];
196
+ if (quote) {
197
+ output += char;
198
+ if (escaped) {
199
+ escaped = false;
200
+ }
201
+ else if (char === '\\') {
202
+ escaped = true;
203
+ }
204
+ else if (char === quote) {
205
+ quote = null;
206
+ }
207
+ index += 1;
208
+ continue;
209
+ }
210
+ if (char === '"' || char === "'" || char === '`') {
211
+ quote = char;
212
+ output += char;
213
+ index += 1;
214
+ continue;
215
+ }
216
+ if (char !== '{' && char !== ',') {
217
+ output += char;
218
+ index += 1;
219
+ continue;
220
+ }
221
+ output += char;
222
+ index += 1;
223
+ const whitespaceStart = index;
224
+ while (index < expr.length && /\s/.test(expr[index]))
225
+ index += 1;
226
+ const whitespace = expr.slice(whitespaceStart, index);
227
+ const keyStart = index;
228
+ if (index < expr.length && /[A-Za-z_$]/.test(expr[index])) {
229
+ index += 1;
230
+ while (index < expr.length && /[\w$]/.test(expr[index]))
231
+ index += 1;
232
+ const key = expr.slice(keyStart, index);
233
+ const afterKeyStart = index;
234
+ while (index < expr.length && /\s/.test(expr[index]))
235
+ index += 1;
236
+ if (expr[index] === ':') {
237
+ output += `${whitespace}"${key}"${expr.slice(afterKeyStart, index)}:`;
238
+ index += 1;
239
+ continue;
240
+ }
241
+ }
242
+ output += whitespace;
243
+ output += expr.slice(keyStart, index);
244
+ }
245
+ return output;
246
+ }
247
+ function lowerJsValueExpressionForPython(expr) {
248
+ return quoteObjectKeysOutsideStrings(replaceJsLiteralsOutsideStrings(expr.trim().replace(/;$/, '')));
249
+ }
250
+ // Whether a JS value expression is safe to lower into Python via the
251
+ // literal/key-quote passes alone. Rejects constructs the lowerers don't
252
+ // understand — backtick template literals, object-property shorthand,
253
+ // and JS `new X(...)` construction (Python has no `new` keyword, so it
254
+ // becomes `SyntaxError` on `ast.parse`).
255
+ function isLowerableJsValueExpression(expr) {
256
+ // Run keyword checks on a string-stripped view so a payload like
257
+ // `{ msg: "example: new Date()" }` (where `new Date()` appears only
258
+ // inside a string literal) doesn't false-positive. Codex flagged the
259
+ // raw-text scan on commit 85593a3f.
260
+ const stripped = stripStringsForJsCheck(expr);
261
+ // Backticks inside strings are stripped to `_`; an unmatched backtick
262
+ // outside strings (i.e., a JS template literal) survives.
263
+ if (/`/.test(stripped))
264
+ return false;
265
+ if (hasObjectShorthandOutsideStrings(expr))
266
+ return false;
267
+ // JS construction `new Date()`, `new AbortController()`, etc.
268
+ // Drop the PascalCase constraint per Gemini+Codex review on ae9663cf
269
+ // / 85593a3f — `new foo()`, `new globalThis.Date()`, etc. are all
270
+ // un-lowerable. Match any identifier (possibly dotted) following
271
+ // `new`. Two variants: with parens (`new X(...)`) and without
272
+ // (`new X` — valid JS, invalid Python). Negative lookbehind avoids
273
+ // Python `for new in items:` false-positive (Codex+Gemini fix-up 5
274
+ // review).
275
+ // Parens form: allow newlines (`\s+` instead of horizontal-only).
276
+ //
277
+ // CRITICAL distinction from `isUnsupportedJsHandlerBody`:
278
+ // `isLowerableJsValueExpression` is called on EXPRESSION content
279
+ // (e.g., the JSON payload of `res.json({...})`), not on full handler
280
+ // bodies. In expression context, there are no statement boundaries —
281
+ // a Python-valid construct like `return new\nDate()` (two statements)
282
+ // simply does not occur here. The expression IS one syntactic unit.
283
+ //
284
+ // So `new\nDate()` inside an expression payload is unambiguously JS
285
+ // construction; the Python statement-cross argument used in the
286
+ // handler-body guard doesn't apply. Codex fix-up 16 review flagged
287
+ // that my fix-up 16 over-corrected by applying statement-level
288
+ // reasoning to this expression-level gate.
289
+ if (/\bnew\s+[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*\s*\(/.test(stripped))
290
+ return false;
291
+ // No-parens `new IDENT` form. Same asymmetric reasoning as the
292
+ // parens form above: this is an EXPRESSION-level gate (`res.json(X)`
293
+ // payload), so `new\nDate` is unambiguously JS construction — no
294
+ // statement boundaries within X. Use `\s+` (newlines OK).
295
+ // Gemini fix-up 18 review pointed out that I'd only relaxed the
296
+ // parens form, leaving this no-parens form horizontal-only by
297
+ // accident — a false-negative for `res.json({ x: new\nDate })`.
298
+ //
299
+ // The negative lookahead still excludes Python idioms `new is`,
300
+ // `new in`, `new for`, etc. — those checks are language-content,
301
+ // not whitespace-shape, so they remain.
302
+ //
303
+ // Lookbehind kept on `\bfor\s+` (with `\s+`, not `[^\S\r\n]+`) so
304
+ // newline-separated `for new` patterns also get the Python-idiom
305
+ // suppression in expression context.
306
+ if (/(?<!\bfor\s+)\bnew\s+(?!(?:is|in|for|if|else|and|or|not)\b)[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*\b/.test(stripped))
307
+ return false;
308
+ return true;
309
+ }
310
+ function lowerRawHandlerBodyForPython(code, indent, imports) {
311
+ const statement = code.trim();
312
+ if (!statement || statement.includes('\n'))
313
+ return null;
314
+ const statusJson = statement.match(/^(?:return\s+)?res\.status\((\d+)\)\.json\(([\s\S]*)\);?$/) ??
315
+ statement.match(/^(?:return\s+)?response\.status\((\d+)\)\.json\(([\s\S]*)\);?$/);
316
+ if (statusJson) {
317
+ if (!statusJson[2].trim() || !isLowerableJsValueExpression(statusJson[2])) {
318
+ return null;
319
+ }
320
+ imports.add('from fastapi.responses import JSONResponse');
321
+ return [
322
+ `${indent}return JSONResponse(content=${lowerJsValueExpressionForPython(statusJson[2])}, status_code=${statusJson[1]})`,
323
+ ];
324
+ }
325
+ const json = statement.match(/^(?:return\s+)?res\.json\(([\s\S]*)\);?$/);
326
+ if (json) {
327
+ if (!json[1].trim() || !isLowerableJsValueExpression(json[1]))
328
+ return null;
329
+ return [`${indent}return ${lowerJsValueExpressionForPython(json[1])}`];
330
+ }
331
+ const directReturn = statement.match(/^return\s+([\s\S]*?);?$/);
332
+ if (directReturn) {
333
+ if (!isLowerableJsValueExpression(directReturn[1]))
334
+ return null;
335
+ return [`${indent}return ${lowerJsValueExpressionForPython(directReturn[1])}`];
336
+ }
337
+ return null;
338
+ }
339
+ export function buildRouteArtifact(routeNode, routeIndex, sourceMap) {
340
+ const props = getProps(routeNode);
341
+ const method = String(props.method || 'get').toLowerCase();
342
+ const normalizedMethod = HTTP_METHODS.has(method) ? method : 'get';
343
+ const path = String(props.path || '/');
344
+ const fastapiPath = convertPath(path);
345
+ const fileBase = routeFileBase(normalizedMethod, path, routeIndex);
346
+ const routerName = `${fileBase}_router`;
347
+ const schema = buildSchema(getFirstChild(routeNode, 'schema'));
348
+ const caps = analyzeRouteCapabilities(routeNode);
349
+ const pathParams = derivePathParams(path);
350
+ // Portable route children: derive, guard, respond, branch, each, collect
351
+ const deriveNodes = getChildren(routeNode, 'derive');
352
+ const guardNodes = getChildren(routeNode, 'guard');
353
+ const respondNode = getFirstChild(routeNode, 'respond');
354
+ const branchNodes = getChildren(routeNode, 'branch');
355
+ const eachNodes = getChildren(routeNode, 'each');
356
+ const collectNodes = getChildren(routeNode, 'collect');
357
+ const effectNodes = getChildren(routeNode, 'effect');
358
+ const hasPortableNodes = deriveNodes.length > 0 ||
359
+ guardNodes.length > 0 ||
360
+ !!respondNode ||
361
+ branchNodes.length > 0 ||
362
+ eachNodes.length > 0 ||
363
+ collectNodes.length > 0 ||
364
+ effectNodes.length > 0;
365
+ // Get handler code
366
+ const handlerNode = caps.hasStream
367
+ ? getFirstChild(caps.streamNode, 'handler')
368
+ : caps.hasTimer
369
+ ? null
370
+ : getFirstChild(routeNode, 'handler');
371
+ const routeHandlerNode = getFirstChild(routeNode, 'handler');
372
+ const handlerProps = handlerNode ? getProps(handlerNode) : {};
373
+ const routeHandlerCode = routeHandlerNode ? String(getProps(routeHandlerNode).code || '') : '';
374
+ const handlerCode = typeof handlerProps.code === 'string' ? String(handlerProps.code) : '';
375
+ const lines = [];
376
+ const imports = new Set();
377
+ imports.add('from fastapi import APIRouter');
378
+ if (caps.hasStream) {
379
+ imports.add('from fastapi.responses import StreamingResponse');
380
+ imports.add('import asyncio');
381
+ }
382
+ if (caps.hasTimer) {
383
+ imports.add('from fastapi import HTTPException');
384
+ imports.add('import asyncio');
385
+ }
386
+ if (caps.hasSpawn) {
387
+ imports.add('import asyncio');
388
+ }
389
+ // v3 route children: params, auth, validate, error, middleware
390
+ const paramsNodes = getChildren(routeNode, 'params');
391
+ const queryParams = [];
392
+ for (const paramNode of paramsNodes) {
393
+ const paramItems = getProps(paramNode).items;
394
+ if (paramItems)
395
+ queryParams.push(...paramItems);
396
+ }
397
+ // Route-level middleware → Depends() in FastAPI
398
+ const routeMiddleware = getChildren(routeNode, 'middleware');
399
+ const middlewareDeps = [];
400
+ for (const mwNode of routeMiddleware) {
401
+ const mwProps = getProps(mwNode);
402
+ const mwNames = mwProps.names;
403
+ if (mwNames && Array.isArray(mwNames)) {
404
+ for (const mwName of mwNames) {
405
+ middlewareDeps.push(toSnakeCase(mwName));
406
+ }
407
+ }
408
+ else if (mwProps.name) {
409
+ middlewareDeps.push(toSnakeCase(String(mwProps.name)));
410
+ }
411
+ }
412
+ if (middlewareDeps.length > 0) {
413
+ imports.add('from fastapi import Depends');
414
+ }
415
+ const authNode = getFirstChild(routeNode, 'auth');
416
+ const validateNode = getFirstChild(routeNode, 'validate');
417
+ const errorNodes = getChildren(routeNode, 'error').filter((n) => typeof getProps(n).status === 'number');
418
+ // Auth requires Depends import
419
+ if (authNode) {
420
+ imports.add('from fastapi import Depends');
421
+ }
422
+ // Error responses require HTTPException
423
+ if (errorNodes.length > 0) {
424
+ imports.add('from fastapi import HTTPException');
425
+ }
426
+ // Schema — generate Pydantic models
427
+ const modelLines = [];
428
+ if (schema.body) {
429
+ imports.add('from pydantic import BaseModel');
430
+ const bodyModel = buildPydanticModel('RequestBody', schema.body);
431
+ modelLines.push(...bodyModel);
432
+ modelLines.push('');
433
+ }
434
+ if (schema.response) {
435
+ imports.add('from pydantic import BaseModel');
436
+ const respModel = buildPydanticModel('ResponseBody', schema.response);
437
+ modelLines.push(...respModel);
438
+ modelLines.push('');
439
+ }
440
+ // Slice 4a review fix (Codex+Gemini critical): stream/timer/portable
441
+ // routes do not support `lang=kern` yet — fail loud at codegen instead
442
+ // of silently swallowing the opt-in and emitting a broken handler.
443
+ // For stream routes, the handler is nested inside `streamNode`; for
444
+ // timer routes, inside `timerNode`. Resolve lang=kern off whichever
445
+ // handler the route configuration points to.
446
+ const streamHandlerNode = caps.streamNode ? getFirstChild(caps.streamNode, 'handler') : undefined;
447
+ const timerHandlerNode = caps.timerNode ? getFirstChild(caps.timerNode, 'handler') : undefined;
448
+ const isKernHandler = !caps.hasStream &&
449
+ !caps.hasTimer &&
450
+ handlerNode !== null &&
451
+ handlerNode !== undefined &&
452
+ handlerProps.lang === 'kern';
453
+ if (caps.hasStream && streamHandlerNode && getProps(streamHandlerNode).lang === 'kern') {
454
+ throw new Error("FastAPI route 'stream' handler with lang=kern is not yet supported. " +
455
+ 'Use a non-stream route or a raw `<<<...>>>` body until slice 4c lands streaming response translation.');
456
+ }
457
+ if (caps.hasTimer && timerHandlerNode && getProps(timerHandlerNode).lang === 'kern') {
458
+ throw new Error("FastAPI route 'timer' handler with lang=kern is not yet supported. " +
459
+ 'Use a non-timer route or a raw `<<<...>>>` body until slice 4c lands timer response translation.');
460
+ }
461
+ if (isKernHandler && hasPortableNodes) {
462
+ throw new Error('FastAPI route has BOTH portable nodes (derive/guard/respond/branch/each/collect/effect) AND a `lang=kern` handler. ' +
463
+ 'Choose one path: portable nodes for declarative composition, or `lang=kern` for native KERN bodies.');
464
+ }
465
+ // Generate handler body lines first (may add to imports)
466
+ const bodyLines = [];
467
+ // Route handler
468
+ if (caps.hasStream) {
469
+ bodyLines.push(...generateStreamRoute(routeNode, caps, normalizedMethod, fastapiPath, pathParams));
470
+ }
471
+ else if (caps.hasTimer && caps.timerNode) {
472
+ bodyLines.push(...generateTimerRoute(routeNode, caps, normalizedMethod, fastapiPath, pathParams, routeHandlerCode));
473
+ }
474
+ else {
475
+ // Standard route — build function signature
476
+ const paramParts = [];
477
+ for (const param of pathParams) {
478
+ paramParts.push(`${param}: str`);
479
+ }
480
+ // v3 query params with types and defaults
481
+ for (const qp of queryParams) {
482
+ const pyType = qp.type === 'number' ? 'int' : qp.type === 'boolean' ? 'bool' : 'str';
483
+ if (qp.default !== undefined) {
484
+ paramParts.push(`${toSnakeCase(qp.name)}: ${pyType} = ${qp.default}`);
485
+ }
486
+ else {
487
+ paramParts.push(`${toSnakeCase(qp.name)}: ${pyType}`);
488
+ }
489
+ }
490
+ if (schema.body) {
491
+ paramParts.push('body: RequestBody');
492
+ }
493
+ // v3 validate — method-aware: body param for POST/PUT/PATCH, Depends for GET/DELETE
494
+ if (validateNode && !schema.body) {
495
+ const validateSchema = String(getProps(validateNode).schema || '');
496
+ if (validateSchema) {
497
+ const bodyMethods = new Set(['post', 'put', 'patch']);
498
+ if (bodyMethods.has(normalizedMethod)) {
499
+ paramParts.push(`body: ${validateSchema}`);
500
+ }
501
+ else {
502
+ imports.add('from fastapi import Depends');
503
+ paramParts.push(`validated = Depends(${toSnakeCase(validateSchema)})`);
504
+ }
505
+ }
506
+ }
507
+ // v3 route-level middleware → Depends()
508
+ for (const dep of middlewareDeps) {
509
+ paramParts.push(`_${dep} = Depends(${dep})`);
510
+ }
511
+ // v3 auth — add Depends(auth_required)
512
+ if (authNode) {
513
+ const authMode = String(getProps(authNode).mode || 'required');
514
+ const authFunc = authMode === 'optional' ? 'auth_optional' : 'auth_required';
515
+ paramParts.push(`user = Depends(${authFunc})`);
516
+ }
517
+ const paramStr = paramParts.join(', ');
518
+ bodyLines.push(`@router.${normalizedMethod}("${fastapiPath}")`);
519
+ bodyLines.push(`async def ${toSnakeCase(normalizedMethod)}_${slugify(fastapiPath)}(${paramStr}):`);
520
+ // v3 error contract as docstring
521
+ if (errorNodes.length > 0) {
522
+ bodyLines.push(` """Errors: ${errorNodes.map((n) => `${getProps(n).status} ${getProps(n).message || ''}`).join(', ')}"""`);
523
+ }
524
+ if (hasPortableNodes) {
525
+ bodyLines.push(...generatePortableHandlerFastAPI(routeNode, ' ', pathParams, imports));
526
+ }
527
+ else if (isKernHandler) {
528
+ // Slice 4a — native KERN handler body (Python target).
529
+ // - Path params: camelCase as-is in the signature (line 300), so
530
+ // they pass through the body unchanged. NO symbol-map entry.
531
+ // - Query params: snake-cased in the signature (lines 307/309),
532
+ // so each camelCase→snake rename feeds the body symbol map.
533
+ // - Body emitter returns required imports (e.g. `math` ⇒
534
+ // `import math as __k_math`); aliased via slice 3 review fix.
535
+ // - propagateStyle: 'http-exception' (slice 4a review fix Gemini
536
+ // #5) so `?` err short-circuit raises HTTPException(500)
537
+ // instead of returning the err object as a 200-OK JSON body.
538
+ //
539
+ // Slice 4a review fix (OpenCode #1, Gemini #4) — collision detection.
540
+ // Two query params that snake-case to the same Python name (e.g.
541
+ // `xCount` + `x_count`) would emit `def f(x_count, x_count)` —
542
+ // SyntaxError at import. Detect at codegen with a clear message.
543
+ // Also detect path-vs-query name collisions (OpenCode #2, Gemini #4):
544
+ // `/users/:id` + `params items=[{name:'id'}]` would emit two `id`
545
+ // params in the signature.
546
+ const claimedSnake = new Set(pathParams);
547
+ const symbolMap = {};
548
+ for (const qp of queryParams) {
549
+ const snake = toSnakeCase(qp.name);
550
+ if (claimedSnake.has(snake)) {
551
+ throw new Error(`KERN-FastAPI route codegen: query param '${qp.name}' snake-cases to '${snake}', which collides with another param on this route ` +
552
+ '(another query param OR a path param of the same name). Rename one to disambiguate.');
553
+ }
554
+ claimedSnake.add(snake);
555
+ if (snake !== qp.name)
556
+ symbolMap[qp.name] = snake;
557
+ }
558
+ const { code: kernBody, imports: bodyImports, usedPropagation, helpers: bodyHelpers, } = emitNativeKernBodyPythonWithImports(handlerNode, { symbolMap, propagateStyle: 'http-exception' });
559
+ for (const mod of bodyImports) {
560
+ imports.add(`import ${mod} as __k_${mod}`);
561
+ }
562
+ // PR-4 — runtime helpers (e.g. `_kern_pairs`) are emitted into the
563
+ // imports block as raw multi-line defs; set semantics dedup across
564
+ // multiple handlers in the same file, and Python is happy to declare
565
+ // module-level helpers in any order before the route function defs.
566
+ for (const helper of bodyHelpers) {
567
+ imports.add(helper);
568
+ }
569
+ if (usedPropagation) {
570
+ // Slice 4a review fix (Gemini #5) — `?` err is now translated
571
+ // into HTTPException(500), so the import is required.
572
+ imports.add('from fastapi import HTTPException');
573
+ }
574
+ if (kernBody) {
575
+ for (const kernLine of kernBody.split('\n')) {
576
+ bodyLines.push(` ${kernLine}`);
577
+ }
578
+ }
579
+ else {
580
+ bodyLines.push(` return {"error": "Route handler not implemented"}`);
581
+ }
582
+ }
583
+ else if (handlerCode) {
584
+ bodyLines.push(...(lowerRawHandlerBodyForPython(handlerCode, ' ', imports) ??
585
+ (isUnsupportedJsHandlerBody(handlerCode)
586
+ ? unsupportedRawHandlerBody(' ')
587
+ : indentHandler(handlerCode, ' '))));
588
+ }
589
+ else if (routeHandlerCode) {
590
+ bodyLines.push(...(lowerRawHandlerBodyForPython(routeHandlerCode, ' ', imports) ??
591
+ (isUnsupportedJsHandlerBody(routeHandlerCode)
592
+ ? unsupportedRawHandlerBody(' ')
593
+ : indentHandler(routeHandlerCode, ' '))));
594
+ }
595
+ else {
596
+ bodyLines.push(` return {"error": "Route handler not implemented"}`);
597
+ }
598
+ }
599
+ // Write imports (after all imports.add() calls, including from portable handler)
600
+ for (const imp of [...imports].sort()) {
601
+ lines.push(imp);
602
+ }
603
+ lines.push('');
604
+ // Router
605
+ lines.push(`router = APIRouter()`);
606
+ lines.push('');
607
+ // Model definitions
608
+ if (modelLines.length > 0) {
609
+ lines.push(...modelLines);
610
+ }
611
+ // Append handler body
612
+ lines.push(...bodyLines);
613
+ sourceMap.push({
614
+ irLine: routeNode.loc?.line || 0,
615
+ irCol: routeNode.loc?.col || 1,
616
+ outLine: 1,
617
+ outCol: 1,
618
+ });
619
+ return {
620
+ routerName,
621
+ fileBase,
622
+ artifact: {
623
+ path: `routes/${fileBase}.py`,
624
+ content: lines.join('\n'),
625
+ type: 'route',
626
+ },
627
+ };
628
+ }
629
+ //# sourceMappingURL=fastapi-route.js.map