naider 1.9.0 → 1.10.0

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.
@@ -0,0 +1,1449 @@
1
+ export class PythonGenerator {
2
+ constructor(options = {}) {
3
+ this.indent = 0;
4
+ this.output = [];
5
+ this.models = new Map();
6
+ this.schemas = new Map();
7
+ this.usesFlask = false;
8
+ this.usesCors = false;
9
+ this.usesJwt = false;
10
+ this.usesSqlite = false;
11
+ this.usesOs = false;
12
+ this.hasTests = false;
13
+ this.hasAsserts = false;
14
+ this.imports = new Set();
15
+ this.fromImports = new Map(); // module -> Set of names
16
+ this.sourceMap = [];
17
+ this.currentSourceLine = 0;
18
+ this.sourceFile = options.sourceFile || null;
19
+ this.authSecret = null;
20
+ this.dbVar = null;
21
+ }
22
+
23
+ generate(ast) {
24
+ this.visitProgram(ast);
25
+
26
+ const preamble = [];
27
+
28
+ // Collect standard library imports
29
+ if (this.usesOs) {
30
+ this.addFromImport('os', 'environ');
31
+ }
32
+
33
+ // Emit top-level imports
34
+ for (const mod of this.imports) {
35
+ preamble.push(`import ${mod}`);
36
+ }
37
+
38
+ // Emit from-imports
39
+ for (const [mod, names] of this.fromImports) {
40
+ const sorted = [...names].sort();
41
+ preamble.push(`from ${mod} import ${sorted.join(', ')}`);
42
+ }
43
+
44
+ if (this.usesFlask) {
45
+ preamble.push('from flask import Flask, request, jsonify, redirect, send_file');
46
+ }
47
+
48
+ if (this.usesCors) {
49
+ preamble.push('from flask_cors import CORS');
50
+ }
51
+
52
+ if (this.usesJwt) {
53
+ preamble.push('import jwt');
54
+ preamble.push('from functools import wraps');
55
+ }
56
+
57
+ if (this.usesSqlite) {
58
+ preamble.push('import sqlite3');
59
+ }
60
+
61
+ if (this.hasTests) {
62
+ preamble.push('import unittest');
63
+ }
64
+
65
+ if (preamble.length > 0) {
66
+ preamble.push('');
67
+ this.output.unshift(...preamble);
68
+ }
69
+
70
+ return this.output.join('\n');
71
+ }
72
+
73
+ emit(line) {
74
+ this.output.push(' '.repeat(this.indent) + line);
75
+ this.sourceMap.push(this.currentSourceLine);
76
+ }
77
+
78
+ emitRaw(line) {
79
+ this.output.push(line);
80
+ this.sourceMap.push(this.currentSourceLine);
81
+ }
82
+
83
+ addImport(mod) {
84
+ this.imports.add(mod);
85
+ }
86
+
87
+ addFromImport(mod, name) {
88
+ if (!this.fromImports.has(mod)) {
89
+ this.fromImports.set(mod, new Set());
90
+ }
91
+ this.fromImports.get(mod).add(name);
92
+ }
93
+
94
+ visitProgram(node) {
95
+ for (const stmt of node.body) {
96
+ this.visitStatement(stmt);
97
+ }
98
+ }
99
+
100
+ visitStatement(node) {
101
+ if (node._line) this.currentSourceLine = node._line;
102
+ switch (node.type) {
103
+ case 'Use': return this.visitUse(node);
104
+ case 'UseDestructured': return this.visitUseDestructured(node);
105
+ case 'Function': return this.visitFunction(node);
106
+ case 'Return': return this.visitReturn(node);
107
+ case 'ReturnStatus': return this.visitReturnStatus(node);
108
+ case 'ReturnMethod': return this.visitReturnMethod(node);
109
+ case 'TypedVar': return this.visitTypedVar(node);
110
+ case 'If': return this.visitIf(node);
111
+ case 'Each': return this.visitEach(node);
112
+ case 'For': return this.visitFor(node);
113
+ case 'While': return this.visitWhile(node);
114
+ case 'Match': return this.visitMatch(node);
115
+ case 'Try': return this.visitTry(node);
116
+ case 'Server': return this.visitServer(node);
117
+ case 'Model': return this.visitModel(node);
118
+ case 'On': return this.visitOn(node);
119
+ case 'Log': return this.visitLog(node);
120
+ case 'Throw': return this.visitThrow(node);
121
+ case 'Break': this.emit('break'); return;
122
+ case 'Continue': this.emit('continue'); return;
123
+ case 'Assignment': return this.visitAssignment(node);
124
+ case 'CompoundAssign': return this.visitCompoundAssign(node);
125
+ case 'ExprStatement': this.emit(this.expr(node.expression)); return;
126
+ case 'DbConnect': return this.visitDbConnect(node);
127
+ case 'DbDir': return this.visitDbDir(node);
128
+ case 'DbSql': return this.visitDbSql(node);
129
+ case 'AwaitAll': return this.visitAwaitAllStatement(node);
130
+ case 'SchemaDecl': return this.visitSchema(node);
131
+ case 'AuthDecl': return this.visitAuth('app', node);
132
+ case 'CorsDecl': return this.visitCors('app', node);
133
+ case 'EnvDecl': return this.visitEnv(node);
134
+ case 'TestDecl': return this.visitTest(node);
135
+ case 'AssertStmt': return this.visitAssert(node);
136
+ case 'ErrorHandler': return this.visitErrorHandler('app', node);
137
+ case 'CrudDecl': return this.visitCrud('app', node);
138
+ case 'StaticDecl': return this.visitStatic('app', node);
139
+ case 'LimitDecl': return this.visitLimit('app', node);
140
+ case 'WsDecl': return this.visitWs(node);
141
+ case 'SessionDecl': return this.visitSession('app', node);
142
+ case 'CacheDecl': return this.visitCache('app', node);
143
+ case 'PromptDecl': return this.visitPrompt(node);
144
+ default:
145
+ this.emit(`# unknown: ${node.type}`);
146
+ }
147
+ }
148
+
149
+ // ===== Imports =====
150
+
151
+ visitUse(node) {
152
+ const raw = node.source ? this.rawString(node.source) : node.name;
153
+ const alias = node.alias || node.name;
154
+
155
+ // Rewrite common JS packages to Python equivalents
156
+ const pyModule = this.mapModuleName(raw);
157
+
158
+ if (alias !== pyModule) {
159
+ this.emit(`import ${pyModule} as ${alias}`);
160
+ } else {
161
+ this.emit(`import ${pyModule}`);
162
+ }
163
+ }
164
+
165
+ visitUseDestructured(node) {
166
+ const raw = this.rawString(node.source);
167
+ const pyModule = this.mapModuleName(raw);
168
+ const names = node.names.map(n => {
169
+ if (n.alias) return `${n.name} as ${n.alias}`;
170
+ return n.name;
171
+ }).join(', ');
172
+ this.emit(`from ${pyModule} import ${names}`);
173
+ }
174
+
175
+ mapModuleName(name) {
176
+ // Strip quotes if present
177
+ const clean = name.replace(/^['"]|['"]$/g, '');
178
+ const map = {
179
+ 'express': 'flask',
180
+ 'axios': 'requests',
181
+ 'lodash': 'itertools',
182
+ 'moment': 'datetime',
183
+ 'fs': 'pathlib',
184
+ 'path': 'os.path',
185
+ 'crypto': 'hashlib',
186
+ 'uuid': 'uuid',
187
+ };
188
+ return map[clean] || clean;
189
+ }
190
+
191
+ // ===== Functions =====
192
+
193
+ visitFunction(node) {
194
+ const async = node.isAsync ? 'async ' : '';
195
+ const params = node.params.map(p => {
196
+ let s = '';
197
+ if (p.spread) s += '*';
198
+ s += p.name;
199
+ if (p.defaultValue) s += `=${this.expr(p.defaultValue)}`;
200
+ return s;
201
+ }).join(', ');
202
+
203
+ this.emit(`${async}def ${node.name}(${params}):`);
204
+ this.indent++;
205
+ if (node.body.length === 0) {
206
+ this.emit('pass');
207
+ } else {
208
+ for (const stmt of node.body) {
209
+ this.visitStatement(stmt);
210
+ }
211
+ }
212
+ this.indent--;
213
+ this.emitRaw('');
214
+ }
215
+
216
+ visitReturn(node) {
217
+ if (node.value === null) {
218
+ this.emit('return');
219
+ } else {
220
+ this.emit(`return ${this.expr(node.value)}`);
221
+ }
222
+ }
223
+
224
+ visitReturnStatus(node) {
225
+ this.emit(`return jsonify(${this.expr(node.body)}), ${this.expr(node.statusCode)}`);
226
+ }
227
+
228
+ visitReturnMethod(node) {
229
+ const val = this.expr(node.value);
230
+ switch (node.method) {
231
+ case 'redirect':
232
+ this.emit(`return redirect(${val})`);
233
+ break;
234
+ case 'html':
235
+ this.emit(`return ${val}, 200, {'Content-Type': 'text/html'}`);
236
+ break;
237
+ case 'text':
238
+ this.emit(`return ${val}, 200, {'Content-Type': 'text/plain'}`);
239
+ break;
240
+ case 'file':
241
+ this.emit(`return send_file(${val})`);
242
+ break;
243
+ default:
244
+ this.emit(`return ${val}`);
245
+ }
246
+ }
247
+
248
+ // ===== Variables =====
249
+
250
+ visitTypedVar(node) {
251
+ this.emit(`${node.name} = ${this.expr(node.value)}`);
252
+ }
253
+
254
+ visitAssignment(node) {
255
+ const value = this.expr(node.value);
256
+
257
+ if (node.target.type === 'Array') {
258
+ const names = node.target.elements.map(e => this.expr(e)).join(', ');
259
+ this.emit(`${names} = ${value}`);
260
+ } else if (node.target.type === 'Object') {
261
+ // Python doesn't have object destructuring directly;
262
+ // emit individual assignments from dict
263
+ const tempVar = '__d';
264
+ this.emit(`${tempVar} = ${value}`);
265
+ for (const p of node.target.properties) {
266
+ const key = p.type === 'shorthand' ? p.key : p.key;
267
+ const varName = p.type === 'shorthand' ? p.key : this.expr(p.value);
268
+ this.emit(`${varName} = ${tempVar}[${JSON.stringify(key)}]`);
269
+ }
270
+ } else {
271
+ this.emit(`${this.expr(node.target)} = ${value}`);
272
+ }
273
+ }
274
+
275
+ visitCompoundAssign(node) {
276
+ this.emit(`${this.expr(node.target)} ${node.op} ${this.expr(node.value)}`);
277
+ }
278
+
279
+ // ===== Control flow =====
280
+
281
+ visitIf(node) {
282
+ this.emit(`if ${this.expr(node.condition)}:`);
283
+ this.indent++;
284
+ if (node.body.length === 0) {
285
+ this.emit('pass');
286
+ } else {
287
+ for (const stmt of node.body) this.visitStatement(stmt);
288
+ }
289
+ this.indent--;
290
+
291
+ for (const elif of node.elifs) {
292
+ this.emit(`elif ${this.expr(elif.condition)}:`);
293
+ this.indent++;
294
+ if (elif.body.length === 0) {
295
+ this.emit('pass');
296
+ } else {
297
+ for (const stmt of elif.body) this.visitStatement(stmt);
298
+ }
299
+ this.indent--;
300
+ }
301
+
302
+ if (node.elseBody) {
303
+ this.emit('else:');
304
+ this.indent++;
305
+ if (node.elseBody.length === 0) {
306
+ this.emit('pass');
307
+ } else {
308
+ for (const stmt of node.elseBody) this.visitStatement(stmt);
309
+ }
310
+ this.indent--;
311
+ }
312
+ }
313
+
314
+ visitEach(node) {
315
+ const collection = this.expr(node.collection);
316
+ if (node.key) {
317
+ this.emit(`for ${node.key}, ${node.value} in ${collection}.items():`);
318
+ } else {
319
+ this.emit(`for ${node.value} in ${collection}:`);
320
+ }
321
+ this.indent++;
322
+ if (node.body.length === 0) {
323
+ this.emit('pass');
324
+ } else {
325
+ for (const stmt of node.body) this.visitStatement(stmt);
326
+ }
327
+ this.indent--;
328
+ }
329
+
330
+ visitFor(node) {
331
+ const varName = node.varName;
332
+ const start = this.expr(node.start);
333
+ const end = this.expr(node.end);
334
+ this.emit(`for ${varName} in range(${start}, ${end}):`);
335
+ this.indent++;
336
+ if (node.body.length === 0) {
337
+ this.emit('pass');
338
+ } else {
339
+ for (const stmt of node.body) this.visitStatement(stmt);
340
+ }
341
+ this.indent--;
342
+ }
343
+
344
+ visitWhile(node) {
345
+ this.emit(`while ${this.expr(node.condition)}:`);
346
+ this.indent++;
347
+ if (node.body.length === 0) {
348
+ this.emit('pass');
349
+ } else {
350
+ for (const stmt of node.body) this.visitStatement(stmt);
351
+ }
352
+ this.indent--;
353
+ }
354
+
355
+ visitMatch(node) {
356
+ this.emit(`match ${this.expr(node.value)}:`);
357
+ this.indent++;
358
+ for (const c of node.cases) {
359
+ if (c.pattern.type === 'DefaultPattern') {
360
+ this.emit('case _:');
361
+ } else {
362
+ this.emit(`case ${this.expr(c.pattern)}:`);
363
+ }
364
+ this.indent++;
365
+ if (c.body.length === 0) {
366
+ this.emit('pass');
367
+ } else {
368
+ for (const stmt of c.body) this.visitStatement(stmt);
369
+ }
370
+ this.indent--;
371
+ }
372
+ this.indent--;
373
+ }
374
+
375
+ visitTry(node) {
376
+ this.emit('try:');
377
+ this.indent++;
378
+ if (node.body.length === 0) {
379
+ this.emit('pass');
380
+ } else {
381
+ for (const stmt of node.body) this.visitStatement(stmt);
382
+ }
383
+ this.indent--;
384
+
385
+ if (node.catchBody) {
386
+ const catchParam = node.catchVar || 'e';
387
+ this.emit(`except Exception as ${catchParam}:`);
388
+ this.indent++;
389
+ if (node.catchBody.length === 0) {
390
+ this.emit('pass');
391
+ } else {
392
+ for (const stmt of node.catchBody) this.visitStatement(stmt);
393
+ }
394
+ this.indent--;
395
+ }
396
+
397
+ if (node.ensureBody) {
398
+ this.emit('finally:');
399
+ this.indent++;
400
+ if (node.ensureBody.length === 0) {
401
+ this.emit('pass');
402
+ } else {
403
+ for (const stmt of node.ensureBody) this.visitStatement(stmt);
404
+ }
405
+ this.indent--;
406
+ }
407
+ }
408
+
409
+ // ===== Server (Flask) =====
410
+
411
+ visitServer(node) {
412
+ this.usesFlask = true;
413
+
414
+ this.emitRaw('');
415
+ this.emit(`${node.name} = Flask(__name__)`);
416
+ this.emitRaw('');
417
+
418
+ for (const mid of node.middleware) {
419
+ this.emitMiddleware(node.name, mid);
420
+ }
421
+
422
+ const errorHandlers = [];
423
+
424
+ for (const child of node.routes) {
425
+ if (child.type === 'Route') {
426
+ this.visitRoute(node.name, child);
427
+ } else if (child.type === 'AuthDecl') {
428
+ this.visitAuth(node.name, child);
429
+ } else if (child.type === 'CorsDecl') {
430
+ this.visitCors(node.name, child);
431
+ } else if (child.type === 'ErrorHandler') {
432
+ errorHandlers.push(child);
433
+ } else if (child.type === 'SchemaDecl') {
434
+ this.visitSchema(child);
435
+ } else if (child.type === 'GroupDecl') {
436
+ this.visitGroup(node.name, child);
437
+ } else if (child.type === 'CrudDecl') {
438
+ this.visitCrud(node.name, child);
439
+ } else if (child.type === 'StaticDecl') {
440
+ this.visitStatic(node.name, child);
441
+ } else if (child.type === 'LimitDecl') {
442
+ this.visitLimit(node.name, child);
443
+ } else if (child.type === 'WsDecl') {
444
+ this.visitWs(child);
445
+ } else if (child.type === 'SessionDecl') {
446
+ this.visitSession(node.name, child);
447
+ } else if (child.type === 'CacheDecl') {
448
+ this.visitCache(node.name, child);
449
+ } else {
450
+ this.visitStatement(child);
451
+ }
452
+ }
453
+
454
+ for (const eh of errorHandlers) {
455
+ this.visitErrorHandler(node.name, eh);
456
+ }
457
+
458
+ const port = node.port ? this.expr(node.port) : '3000';
459
+ this.emitRaw('');
460
+ this.emit(`if __name__ == '__main__':`);
461
+ this.indent++;
462
+ this.emit(`${node.name}.run(port=${port}, debug=True)`);
463
+ this.indent--;
464
+ }
465
+
466
+ visitRoute(appName, route) {
467
+ const method = route.method === 'del' ? 'DELETE' : route.method.toUpperCase();
468
+ const path = this.flaskPath(route.path);
469
+
470
+ this.emit(`@${appName}.route(${path}, methods=['${method}'])`);
471
+
472
+ // Generate a function name from the route
473
+ const funcName = this.routeFuncName(route.method, route.path);
474
+ const async = this.bodyUsesAwait(route.body) ? 'async ' : '';
475
+
476
+ this.emit(`${async}def ${funcName}():`);
477
+ this.indent++;
478
+
479
+ for (let i = 0; i < route.body.length; i++) {
480
+ const stmt = route.body[i];
481
+ if (stmt.type === 'Return' && stmt.value !== null) {
482
+ this.emit(`return jsonify(${this.expr(stmt.value)})`);
483
+ } else {
484
+ this.visitStatement(stmt);
485
+ }
486
+ }
487
+
488
+ if (route.body.length === 0) {
489
+ this.emit('pass');
490
+ }
491
+
492
+ this.indent--;
493
+ this.emitRaw('');
494
+ }
495
+
496
+ flaskPath(strData) {
497
+ // Convert Express-style :param to Flask <param>
498
+ let raw = this.rawString(strData);
499
+ const converted = raw.replace(/:(\w+)/g, '<$1>');
500
+ return JSON.stringify(converted);
501
+ }
502
+
503
+ routeFuncName(method, pathData) {
504
+ const raw = this.rawString(pathData);
505
+ const clean = raw.replace(/[^a-zA-Z0-9]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '');
506
+ return `${method}_${clean || 'root'}`;
507
+ }
508
+
509
+ emitMiddleware(appName, mid) {
510
+ const funcName = `__middleware_${Date.now()}`;
511
+ this.emit(`@${appName}.before_request`);
512
+ this.emit(`def ${funcName}():`);
513
+ this.indent++;
514
+ if (mid.body.length === 0) {
515
+ this.emit('pass');
516
+ } else {
517
+ for (const stmt of mid.body) this.visitStatement(stmt);
518
+ }
519
+ this.indent--;
520
+ this.emitRaw('');
521
+ }
522
+
523
+ visitAuth(appName, node) {
524
+ this.usesJwt = true;
525
+ const secret = this.expr(node.secret);
526
+ this.authSecret = secret;
527
+
528
+ this.emitRaw('');
529
+ this.emit(`__auth_secret = ${secret}`);
530
+ this.emitRaw('');
531
+ this.emit('def require_auth(f):');
532
+ this.indent++;
533
+ this.emit('@wraps(f)');
534
+ this.emit('def decorated(*args, **kwargs):');
535
+ this.indent++;
536
+ this.emit("token = request.headers.get('Authorization', '').replace('Bearer ', '')");
537
+ this.emit('if not token:');
538
+ this.indent++;
539
+ this.emit("return jsonify({'error': 'No token provided'}), 401");
540
+ this.indent--;
541
+ this.emit('try:');
542
+ this.indent++;
543
+ this.emit("request.user = jwt.decode(token, __auth_secret, algorithms=['HS256'])");
544
+ this.indent--;
545
+ this.emit('except jwt.InvalidTokenError:');
546
+ this.indent++;
547
+ this.emit("return jsonify({'error': 'Invalid token'}), 401");
548
+ this.indent--;
549
+ this.emit('return f(*args, **kwargs)');
550
+ this.indent--;
551
+ this.emit('return decorated');
552
+ this.indent--;
553
+ this.emitRaw('');
554
+ }
555
+
556
+ visitCors(appName, node) {
557
+ this.usesCors = true;
558
+ const origins = this.expr(node.origins);
559
+ if (node.origins.type === 'String') {
560
+ this.emit(`CORS(${appName}, origins=[${origins}])`);
561
+ } else {
562
+ this.emit(`CORS(${appName}, origins=${origins})`);
563
+ }
564
+ this.emitRaw('');
565
+ }
566
+
567
+ visitGroup(appName, node) {
568
+ const prefix = this.rawString(node.prefix);
569
+ this.addFromImport('flask', 'Blueprint');
570
+ this.groupCounter = (this.groupCounter || 0) + 1;
571
+ const bpName = `__bp${this.groupCounter}`;
572
+ this.emit(`${bpName} = Blueprint('group_${this.groupCounter}', __name__, url_prefix=${JSON.stringify(prefix)})`);
573
+
574
+ for (const child of node.routes) {
575
+ if (child.type === 'Route') {
576
+ this.visitRoute(bpName, child);
577
+ } else {
578
+ this.visitStatement(child);
579
+ }
580
+ }
581
+
582
+ this.emit(`${appName}.register_blueprint(${bpName})`);
583
+ this.emitRaw('');
584
+ }
585
+
586
+ visitErrorHandler(appName, node) {
587
+ this.emit(`@${appName}.errorhandler(Exception)`);
588
+ this.emit('def handle_error(error):');
589
+ this.indent++;
590
+ if (node.body.length === 0) {
591
+ this.emit("return jsonify({'error': str(error)}), 500");
592
+ } else {
593
+ for (const stmt of node.body) this.visitStatement(stmt);
594
+ }
595
+ this.indent--;
596
+ this.emitRaw('');
597
+ }
598
+
599
+ // ===== Model (class) =====
600
+
601
+ visitModel(node) {
602
+ this.models.set(node.name, node);
603
+
604
+ const ext = node.parent ? `(${node.parent})` : '';
605
+ this.emit(`class ${node.name}${ext}:`);
606
+ this.indent++;
607
+
608
+ if (node.fields.length > 0 || node.parent) {
609
+ const parentModel = node.parent ? this.models.get(node.parent) : null;
610
+ const parentFields = parentModel ? parentModel.fields : [];
611
+ const allFields = [...parentFields, ...node.fields];
612
+
613
+ const initParams = allFields.map(f => {
614
+ if (f.defaultValue) return `${f.name}=${this.expr(f.defaultValue)}`;
615
+ return f.name;
616
+ }).join(', ');
617
+
618
+ this.emit(`def __init__(self, ${initParams}):`);
619
+ this.indent++;
620
+ if (node.parent) {
621
+ const superArgs = parentFields.map(f => `${f.name}=${f.name}`).join(', ');
622
+ this.emit(`super().__init__(${superArgs})`);
623
+ }
624
+ for (const f of node.fields) {
625
+ this.emit(`self.${f.name} = ${f.name}`);
626
+ }
627
+ if (node.fields.length === 0 && !node.parent) {
628
+ this.emit('pass');
629
+ }
630
+ this.indent--;
631
+ this.emitRaw('');
632
+ }
633
+
634
+ for (const method of node.methods) {
635
+ const async = method.isAsync ? 'async ' : '';
636
+ const params = method.params.map(p => {
637
+ let s = '';
638
+ if (p.spread) s += '*';
639
+ s += p.name;
640
+ if (p.defaultValue) s += `=${this.expr(p.defaultValue)}`;
641
+ return s;
642
+ }).join(', ');
643
+
644
+ const selfParams = params ? `self, ${params}` : 'self';
645
+ this.emit(`${async}def ${method.name}(${selfParams}):`);
646
+ this.indent++;
647
+ if (method.body.length === 0) {
648
+ this.emit('pass');
649
+ } else {
650
+ for (const stmt of method.body) {
651
+ this.visitStatement(stmt);
652
+ }
653
+ }
654
+ this.indent--;
655
+ this.emitRaw('');
656
+ }
657
+
658
+ if (node.fields.length === 0 && node.methods.length === 0 && !node.parent) {
659
+ this.emit('pass');
660
+ this.emitRaw('');
661
+ }
662
+
663
+ this.indent--;
664
+ this.emitRaw('');
665
+ }
666
+
667
+ // ===== Events =====
668
+
669
+ visitOn(node) {
670
+ const event = this.expr(node.event);
671
+ // Python doesn't have native event emitters; emit a comment-based pattern
672
+ this.emit(`# on ${event}`);
673
+ this.emit(`def __on_${event.replace(/\./g, '_')}():`);
674
+ this.indent++;
675
+ if (node.body.length === 0) {
676
+ this.emit('pass');
677
+ } else {
678
+ for (const stmt of node.body) this.visitStatement(stmt);
679
+ }
680
+ this.indent--;
681
+ this.emitRaw('');
682
+ }
683
+
684
+ // ===== Logging and errors =====
685
+
686
+ visitLog(node) {
687
+ const args = node.args.map(a => this.expr(a)).join(', ');
688
+ if (node.level === 'warn') {
689
+ this.addImport('logging');
690
+ this.emit(`logging.warning(${args})`);
691
+ } else if (node.level === 'error') {
692
+ this.addImport('logging');
693
+ this.emit(`logging.error(${args})`);
694
+ } else {
695
+ this.emit(`print(${args})`);
696
+ }
697
+ }
698
+
699
+ visitThrow(node) {
700
+ const value = this.expr(node.value);
701
+ if (node.value.type === 'String') {
702
+ this.emit(`raise Exception(${value})`);
703
+ } else {
704
+ this.emit(`raise ${value}`);
705
+ }
706
+ }
707
+
708
+ // ===== Database =====
709
+
710
+ visitDbConnect(node) {
711
+ this.usesSqlite = true;
712
+ this.dbVar = 'db';
713
+ this.emit(`db = sqlite3.connect(${this.expr(node.connectionString)})`);
714
+ }
715
+
716
+ visitDbDir(node) {
717
+ const raw = node.path.raw || node.path.parts?.map(p => p.value).join('') || 'data/';
718
+ this.emit(`# db dir: ${raw}`);
719
+ }
720
+
721
+ visitDbSql(node) {
722
+ this.usesSqlite = true;
723
+ this.dbVar = '__db';
724
+ const conn = node.connection ? this.pyStringValue(node.connection) : '"data.db"';
725
+ this.emit(`__db = sqlite3.connect(${conn})`);
726
+ this.emit(`__db.execute("PRAGMA journal_mode=WAL")`);
727
+ this.emitRaw('');
728
+ }
729
+
730
+ visitAwaitAllStatement(node) {
731
+ this.addImport('asyncio');
732
+ const exprs = node.expressions.map(e => this.expr(e)).join(', ');
733
+ this.emit(`await asyncio.gather(${exprs})`);
734
+ }
735
+
736
+ // ===== Schema (dataclass / Pydantic style) =====
737
+
738
+ visitSchema(node) {
739
+ this.addFromImport('dataclasses', 'dataclass');
740
+ this.addFromImport('dataclasses', 'field');
741
+
742
+ this.emitRaw('');
743
+ this.emit('@dataclass');
744
+ this.emit(`class ${node.name}:`);
745
+ this.indent++;
746
+
747
+ for (const f of node.fields) {
748
+ const pyType = this.mapSchemaType(f.type);
749
+ const defaultVal = this.schemaDefault(f);
750
+
751
+ if (defaultVal !== null) {
752
+ this.emit(`${f.name}: ${pyType} = ${defaultVal}`);
753
+ } else {
754
+ this.emit(`${f.name}: ${pyType}`);
755
+ }
756
+ }
757
+
758
+ if (node.fields.length === 0) {
759
+ this.emit('pass');
760
+ }
761
+
762
+ this.indent--;
763
+ this.emitRaw('');
764
+
765
+ this.schemas.set(node.name, node);
766
+ }
767
+
768
+ mapSchemaType(type) {
769
+ const map = {
770
+ 'str': 'str',
771
+ 'string': 'str',
772
+ 'int': 'int',
773
+ 'integer': 'int',
774
+ 'num': 'float',
775
+ 'number': 'float',
776
+ 'bool': 'bool',
777
+ 'boolean': 'bool',
778
+ 'auto': 'int',
779
+ 'timestamp': 'str',
780
+ 'enum': 'str',
781
+ };
782
+ return map[type] || 'object';
783
+ }
784
+
785
+ schemaDefault(fieldNode) {
786
+ for (const mod of fieldNode.modifiers) {
787
+ if (mod.name === 'default') {
788
+ return this.expr(mod.args[0]);
789
+ }
790
+ }
791
+ // Optional fields get None as default
792
+ for (const mod of fieldNode.modifiers) {
793
+ if (mod.name === 'optional') return 'None';
794
+ }
795
+ if (fieldNode.type === 'auto') return 'None';
796
+ if (fieldNode.type === 'timestamp') return 'None';
797
+ return null;
798
+ }
799
+
800
+ // ===== Env =====
801
+
802
+ visitEnv(node) {
803
+ this.usesOs = true;
804
+ this.emitRaw('');
805
+ for (const v of node.vars) {
806
+ let defaultVal = null;
807
+ for (const mod of v.modifiers) {
808
+ if (mod.name === 'default') {
809
+ defaultVal = this.expr(mod.args[0]);
810
+ }
811
+ }
812
+
813
+ if (defaultVal !== null) {
814
+ this.emit(`${v.name} = os.environ.get(${JSON.stringify(v.name)}, ${defaultVal})`);
815
+ } else {
816
+ this.emit(`${v.name} = os.environ.get(${JSON.stringify(v.name)})`);
817
+ }
818
+
819
+ // Type coercion
820
+ if (v.type === 'int') {
821
+ this.emit(`${v.name} = int(${v.name}) if ${v.name} is not None else None`);
822
+ } else if (v.type === 'num') {
823
+ this.emit(`${v.name} = float(${v.name}) if ${v.name} is not None else None`);
824
+ } else if (v.type === 'bool') {
825
+ this.emit(`${v.name} = ${v.name}.lower() in ('true', '1', 'yes') if ${v.name} is not None else None`);
826
+ }
827
+ }
828
+ this.emitRaw('');
829
+ }
830
+
831
+ // ===== Tests =====
832
+
833
+ visitTest(node) {
834
+ this.hasTests = true;
835
+ const name = this.rawString(node.name);
836
+ const funcName = `test_${name.replace(/[^a-zA-Z0-9]/g, '_').replace(/_+/g, '_')}`;
837
+
838
+ this.emit(`def ${funcName}():`);
839
+ this.indent++;
840
+ if (node.body.length === 0) {
841
+ this.emit('pass');
842
+ } else {
843
+ for (const stmt of node.body) this.visitStatement(stmt);
844
+ }
845
+ this.indent--;
846
+ this.emitRaw('');
847
+ }
848
+
849
+ visitAssert(node) {
850
+ this.hasAsserts = true;
851
+ const exprNode = node.expr;
852
+ if (exprNode.type === 'Binary' && (exprNode.op === '===' || exprNode.op === '==')) {
853
+ this.emit(`assert ${this.expr(exprNode.left)} == ${this.expr(exprNode.right)}`);
854
+ } else if (exprNode.type === 'Binary' && (exprNode.op === '!==' || exprNode.op === '!=')) {
855
+ this.emit(`assert ${this.expr(exprNode.left)} != ${this.expr(exprNode.right)}`);
856
+ } else {
857
+ this.emit(`assert ${this.expr(exprNode)}`);
858
+ }
859
+ }
860
+
861
+ // ===== Expression generation =====
862
+
863
+ expr(node) {
864
+ if (!node) return 'None';
865
+
866
+ switch (node.type) {
867
+ case 'Number': return node.value;
868
+ case 'String': return this.generateString(node.value);
869
+ case 'Bool': return node.value ? 'True' : 'False';
870
+ case 'Null': return 'None';
871
+ case 'Self': return 'self';
872
+ case 'Identifier': return this.mapIdentifier(node.name);
873
+
874
+ case 'Binary': {
875
+ const op = this.mapBinaryOp(node.op);
876
+ const l = this.expr(node.left);
877
+ const r = this.expr(node.right);
878
+ const simple = node.left.type !== 'Binary' && node.right.type !== 'Binary';
879
+ return simple ? `${l} ${op} ${r}` : `(${l} ${op} ${r})`;
880
+ }
881
+
882
+ case 'Unary': {
883
+ const op = this.mapUnaryOp(node.op);
884
+ if (op === 'not ') return `not ${this.expr(node.expr)}`;
885
+ return `${op}${this.expr(node.expr)}`;
886
+ }
887
+
888
+ case 'Await':
889
+ return `await ${this.expr(node.expr)}`;
890
+
891
+ case 'AwaitAllExpr':
892
+ this.addImport('asyncio');
893
+ return `await asyncio.gather(${this.expr(node.expr)})`;
894
+
895
+ case 'Spread':
896
+ return `*${this.expr(node.expr)}`;
897
+
898
+ case 'New':
899
+ return this.expr(node.expr);
900
+
901
+ case 'TypeOf':
902
+ return `type(${this.expr(node.expr)}).__name__`;
903
+
904
+ case 'MemberAccess':
905
+ return this.mapMemberAccess(node);
906
+
907
+ case 'OptionalAccess':
908
+ // Python doesn't have ?. — use getattr with default
909
+ return `getattr(${this.expr(node.object)}, ${JSON.stringify(node.property)}, None)`;
910
+
911
+ case 'Call':
912
+ return this.generateCall(node);
913
+
914
+ case 'IndexAccess':
915
+ return `${this.expr(node.object)}[${this.expr(node.index)}]`;
916
+
917
+ case 'Array':
918
+ return `[${node.elements.map(e => this.expr(e)).join(', ')}]`;
919
+
920
+ case 'Object': {
921
+ const props = node.properties.map(p => {
922
+ if (p.type === 'spread') return `**${this.expr(p.value)}`;
923
+ if (p.type === 'shorthand') return `${JSON.stringify(p.key)}: ${p.key}`;
924
+ const key = typeof p.key === 'string' ? JSON.stringify(p.key) :
925
+ (p.key.type === 'String' ? this.generateString(p.key.value) :
926
+ (p.key.type === 'Computed' ? this.expr(p.key.expr) : JSON.stringify(this.expr(p.key))));
927
+ return `${key}: ${this.expr(p.value)}`;
928
+ }).join(', ');
929
+ return `{${props}}`;
930
+ }
931
+
932
+ case 'ArrowFn': {
933
+ const params = node.params.map(p => p.name).join(', ');
934
+ const body = this.expr(node.body);
935
+ return `lambda ${params}: ${body}`;
936
+ }
937
+
938
+ case 'Lambda': {
939
+ // Multi-line lambda must become a nested def in Python;
940
+ // for expression context, emit a simple lambda if single-expression
941
+ if (node.body.length === 1 && node.body[0].type === 'Return' && node.body[0].value) {
942
+ const params = node.params.map(p => p.name).join(', ');
943
+ return `lambda ${params}: ${this.expr(node.body[0].value)}`;
944
+ }
945
+ // For multi-statement lambdas, we cannot inline; use a helper comment
946
+ const params = node.params.map(p => p.name).join(', ');
947
+ return `lambda ${params}: None # multi-statement lambda - refactor to def`;
948
+ }
949
+
950
+ case 'Ternary':
951
+ return `(${this.expr(node.consequent)} if ${this.expr(node.condition)} else ${this.expr(node.alternate)})`;
952
+
953
+ case 'Pipe':
954
+ return this.generatePipe(node);
955
+
956
+ default:
957
+ return `None # expr:${node.type}`;
958
+ }
959
+ }
960
+
961
+ mapIdentifier(name) {
962
+ const map = {
963
+ 'null': 'None',
964
+ 'undefined': 'None',
965
+ 'true': 'True',
966
+ 'false': 'False',
967
+ 'this': 'self',
968
+ 'console': 'print',
969
+ };
970
+ return map[name] || name;
971
+ }
972
+
973
+ mapBinaryOp(op) {
974
+ const map = {
975
+ '===': '==',
976
+ '!==': '!=',
977
+ '==': '==',
978
+ '!=': '!=',
979
+ '&&': 'and',
980
+ '||': 'or',
981
+ '!': 'not',
982
+ '>': '>',
983
+ '<': '<',
984
+ '>=': '>=',
985
+ '<=': '<=',
986
+ '+': '+',
987
+ '-': '-',
988
+ '*': '*',
989
+ '/': '/',
990
+ '%': '%',
991
+ '**': '**',
992
+ 'and': 'and',
993
+ 'or': 'or',
994
+ 'instanceof': 'isinstance', // handled specially in Binary
995
+ 'in': 'in',
996
+ };
997
+ return map[op] || op;
998
+ }
999
+
1000
+ mapUnaryOp(op) {
1001
+ if (op === '!') return 'not ';
1002
+ if (op === 'not') return 'not ';
1003
+ return op;
1004
+ }
1005
+
1006
+ mapMemberAccess(node) {
1007
+ const obj = this.expr(node.object);
1008
+ const prop = node.property;
1009
+
1010
+ // Map common JS properties/methods to Python
1011
+ if (prop === 'length') return `len(${obj})`;
1012
+ if (prop === 'toString') return `str(${obj})`;
1013
+
1014
+ // request/req.body -> request.json in Flask
1015
+ if ((obj === 'request' || obj === 'req') && prop === 'body') return 'request.get_json()';
1016
+ if ((obj === 'request' || obj === 'req') && prop === 'query') return 'request.args';
1017
+ if ((obj === 'request' || obj === 'req') && prop === 'params') return 'request.view_args';
1018
+ if ((obj === 'request' || obj === 'req') && prop === 'headers') return 'request.headers';
1019
+ if ((obj === 'request' || obj === 'req') && prop === 'cookies') return 'request.cookies';
1020
+ if ((obj === 'request' || obj === 'req') && prop === 'method') return 'request.method';
1021
+ if ((obj === 'request' || obj === 'req') && prop === 'url') return 'request.url';
1022
+
1023
+ // JSON methods
1024
+ if (obj === 'JSON' && prop === 'parse') return 'json.loads';
1025
+ if (obj === 'JSON' && prop === 'stringify') return 'json.dumps';
1026
+
1027
+ // Math methods
1028
+ if (obj === 'Math' && prop === 'floor') return 'math.floor';
1029
+ if (obj === 'Math' && prop === 'ceil') return 'math.ceil';
1030
+ if (obj === 'Math' && prop === 'round') return 'round';
1031
+ if (obj === 'Math' && prop === 'abs') return 'abs';
1032
+ if (obj === 'Math' && prop === 'min') return 'min';
1033
+ if (obj === 'Math' && prop === 'max') return 'max';
1034
+ if (obj === 'Math' && prop === 'random') { this.addImport('random'); return 'random.random'; }
1035
+ if (obj === 'Math' && prop === 'PI') { this.addImport('math'); return 'math.pi'; }
1036
+
1037
+ // console methods
1038
+ if (obj === 'console' && prop === 'log') return 'print';
1039
+ if (obj === 'console' && prop === 'error') { this.addImport('logging'); return 'logging.error'; }
1040
+ if (obj === 'console' && prop === 'warn') { this.addImport('logging'); return 'logging.warning'; }
1041
+
1042
+ return `${obj}.${prop}`;
1043
+ }
1044
+
1045
+ generateCall(node) {
1046
+ const args = node.args.map(a => this.expr(a)).join(', ');
1047
+
1048
+ // Map common JS global functions to Python
1049
+ if (node.callee.type === 'Identifier') {
1050
+ const name = node.callee.name;
1051
+
1052
+ if (name === 'parseInt') return `int(${args})`;
1053
+ if (name === 'parseFloat') return `float(${args})`;
1054
+ if (name === 'String') return `str(${args})`;
1055
+ if (name === 'Number') return `float(${args})`;
1056
+ if (name === 'Boolean') return `bool(${args})`;
1057
+ if (name === 'isNaN') { this.addImport('math'); return `math.isnan(${args})`; }
1058
+ if (name === 'setTimeout') return `# setTimeout: use threading.Timer\nNone`;
1059
+ if (name === 'setInterval') return `# setInterval: use threading\nNone`;
1060
+
1061
+ if (name === 'sign' && this.authSecret) {
1062
+ this.usesJwt = true;
1063
+ if (node.args.length === 1) {
1064
+ return `jwt.encode(${args}, __auth_secret, algorithm='HS256')`;
1065
+ }
1066
+ return `jwt.encode(${args}, algorithm='HS256')`;
1067
+ }
1068
+ }
1069
+
1070
+ // Map common method calls
1071
+ if (node.callee.type === 'MemberAccess') {
1072
+ const obj = this.expr(node.callee.object);
1073
+ const prop = node.callee.property;
1074
+
1075
+ // Array/list methods
1076
+ if (prop === 'push') return `${obj}.append(${args})`;
1077
+ if (prop === 'pop') return `${obj}.pop(${args})`;
1078
+ if (prop === 'shift') return `${obj}.pop(0)`;
1079
+ if (prop === 'unshift') return `${obj}.insert(0, ${args})`;
1080
+ if (prop === 'indexOf') return `${obj}.index(${args})`;
1081
+ if (prop === 'includes') return `${args} in ${obj}`;
1082
+ if (prop === 'join') return `${args}.join(${obj})`;
1083
+ if (prop === 'slice') return `${obj}[${args}]`;
1084
+ if (prop === 'forEach') return `[${args}(x) for x in ${obj}]`;
1085
+ if (prop === 'map') return `list(map(${args}, ${obj}))`;
1086
+ if (prop === 'filter') return `list(filter(${args}, ${obj}))`;
1087
+ if (prop === 'find') return `next((x for x in ${obj} if ${args}(x)), None)`;
1088
+ if (prop === 'some') return `any(${args}(x) for x in ${obj})`;
1089
+ if (prop === 'every') return `all(${args}(x) for x in ${obj})`;
1090
+ if (prop === 'reduce') {
1091
+ this.addFromImport('functools', 'reduce');
1092
+ return `reduce(${args}, ${obj})`;
1093
+ }
1094
+ if (prop === 'flat') return `[item for sub in ${obj} for item in sub]`;
1095
+ if (prop === 'reverse') return `list(reversed(${obj}))`;
1096
+ if (prop === 'sort') return `sorted(${obj})`;
1097
+ if (prop === 'concat') return `${obj} + ${args}`;
1098
+ if (prop === 'splice') return `${obj}[${args}]`;
1099
+
1100
+ // String methods
1101
+ if (prop === 'split') return `${obj}.split(${args})`;
1102
+ if (prop === 'trim') return `${obj}.strip()`;
1103
+ if (prop === 'trimStart') return `${obj}.lstrip()`;
1104
+ if (prop === 'trimEnd') return `${obj}.rstrip()`;
1105
+ if (prop === 'toUpperCase') return `${obj}.upper()`;
1106
+ if (prop === 'toLowerCase') return `${obj}.lower()`;
1107
+ if (prop === 'startsWith') return `${obj}.startswith(${args})`;
1108
+ if (prop === 'endsWith') return `${obj}.endswith(${args})`;
1109
+ if (prop === 'replace') return `${obj}.replace(${args})`;
1110
+ if (prop === 'replaceAll') return `${obj}.replace(${args})`;
1111
+ if (prop === 'padStart') return `${obj}.rjust(${args})`;
1112
+ if (prop === 'padEnd') return `${obj}.ljust(${args})`;
1113
+ if (prop === 'charAt') return `${obj}[${args}]`;
1114
+ if (prop === 'substring') return `${obj}[${args}]`;
1115
+ if (prop === 'repeat') return `${obj} * ${args}`;
1116
+
1117
+ // Object methods
1118
+ if (obj === 'Object' && prop === 'keys') return `list(${args}.keys())`;
1119
+ if (obj === 'Object' && prop === 'values') return `list(${args}.values())`;
1120
+ if (obj === 'Object' && prop === 'entries') return `list(${args}.items())`;
1121
+ if (obj === 'Object' && prop === 'assign') return `{**${node.args.map(a => this.expr(a)).join(', **')}}`;
1122
+
1123
+ // Array static
1124
+ if (obj === 'Array' && prop === 'isArray') return `isinstance(${args}, list)`;
1125
+ if (obj === 'Array' && prop === 'from') return `list(${args})`;
1126
+
1127
+ // Promise
1128
+ if (obj === 'Promise' && prop === 'all') {
1129
+ this.addImport('asyncio');
1130
+ return `asyncio.gather(*${args})`;
1131
+ }
1132
+ if (obj === 'Promise' && prop === 'resolve') return `${args}`;
1133
+
1134
+ // Date
1135
+ if (obj === 'Date' && prop === 'now') {
1136
+ this.addImport('time');
1137
+ return `int(time.time() * 1000)`;
1138
+ }
1139
+
1140
+ // JSON in Flask context
1141
+ if (prop === 'json' && obj === 'request') {
1142
+ return 'request.get_json()';
1143
+ }
1144
+
1145
+ // auth methods
1146
+ if (obj === 'auth' && prop === 'sign') {
1147
+ this.usesJwt = true;
1148
+ if (node.args.length === 1 && this.authSecret) {
1149
+ return `jwt.encode(${args}, __auth_secret, algorithm='HS256')`;
1150
+ }
1151
+ return `jwt.encode(${args}, algorithm='HS256')`;
1152
+ }
1153
+ if (obj === 'auth' && prop === 'verify') {
1154
+ this.usesJwt = true;
1155
+ return `jwt.decode(${args}, algorithm='HS256')`;
1156
+ }
1157
+ }
1158
+
1159
+ const callee = this.expr(node.callee);
1160
+ return `${callee}(${args})`;
1161
+ }
1162
+
1163
+ generateString(strData) {
1164
+ if (!strData || !strData.parts) return '""';
1165
+
1166
+ const hasInterpolation = strData.parts.some(p => p.type === 'expr');
1167
+
1168
+ if (!hasInterpolation) {
1169
+ const raw = strData.parts.map(p => p.value).join('');
1170
+ return JSON.stringify(raw);
1171
+ }
1172
+
1173
+ // Use f-string for interpolation
1174
+ let result = 'f"';
1175
+ for (const part of strData.parts) {
1176
+ if (part.type === 'text') {
1177
+ result += part.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/{/g, '{{').replace(/}/g, '}}');
1178
+ } else {
1179
+ const exprCode = part.value.replace(/\bself\b/g, 'self');
1180
+ result += '{' + exprCode + '}';
1181
+ }
1182
+ }
1183
+ result += '"';
1184
+ return result;
1185
+ }
1186
+
1187
+ rawString(strData) {
1188
+ if (!strData) return '';
1189
+ if (strData.raw !== null && strData.raw !== undefined) return strData.raw;
1190
+ if (strData.parts) return strData.parts.map(p => p.value).join('');
1191
+ return '';
1192
+ }
1193
+
1194
+ pyStringValue(strData) {
1195
+ if (!strData || !strData.parts) return '""';
1196
+ if (strData.raw !== null && strData.raw !== undefined) {
1197
+ return JSON.stringify(strData.raw);
1198
+ }
1199
+ return this.generateString(strData);
1200
+ }
1201
+
1202
+ generatePipe(node) {
1203
+ const steps = [];
1204
+ let current = node;
1205
+ while (current.type === 'Pipe') {
1206
+ steps.unshift(current.right);
1207
+ current = current.left;
1208
+ }
1209
+ steps.unshift(current);
1210
+
1211
+ const LIST_METHODS = new Set(['filter', 'map', 'reduce', 'find', 'some', 'every', 'sort', 'reverse', 'join']);
1212
+
1213
+ let result = this.expr(steps[0]);
1214
+ for (let i = 1; i < steps.length; i++) {
1215
+ const step = steps[i];
1216
+ if (step.type === 'Call' && step.callee.type === 'Identifier') {
1217
+ const name = step.callee.name;
1218
+ const args = step.args.map(a => this.expr(a)).join(', ');
1219
+
1220
+ if (name === 'filter') {
1221
+ result = `list(filter(${args}, ${result}))`;
1222
+ } else if (name === 'map') {
1223
+ result = `list(map(${args}, ${result}))`;
1224
+ } else if (name === 'reduce') {
1225
+ this.addFromImport('functools', 'reduce');
1226
+ result = `reduce(${args}, ${result})`;
1227
+ } else if (name === 'sort') {
1228
+ result = `sorted(${result})`;
1229
+ } else if (name === 'reverse') {
1230
+ result = `list(reversed(${result}))`;
1231
+ } else if (name === 'join') {
1232
+ result = `${args}.join(${result})`;
1233
+ } else {
1234
+ result = `${name}(${result}${args ? ', ' + args : ''})`;
1235
+ }
1236
+ } else if (step.type === 'Identifier') {
1237
+ result = `${step.name}(${result})`;
1238
+ } else if (step.type === 'Call') {
1239
+ const callee = this.expr(step.callee);
1240
+ const args = step.args.map(a => this.expr(a)).join(', ');
1241
+ result = `${callee}(${result}${args ? ', ' + args : ''})`;
1242
+ } else {
1243
+ result = `(${this.expr(step)})(${result})`;
1244
+ }
1245
+ }
1246
+ return result;
1247
+ }
1248
+
1249
+ bodyUsesAwait(body) {
1250
+ for (const stmt of body) {
1251
+ if (this.stmtUsesAwait(stmt)) return true;
1252
+ }
1253
+ return false;
1254
+ }
1255
+
1256
+ stmtUsesAwait(node) {
1257
+ if (!node) return false;
1258
+ const json = JSON.stringify(node);
1259
+ return json.includes('"Await"') || json.includes('"AwaitAll"');
1260
+ }
1261
+
1262
+ statementToString(stmt) {
1263
+ const saved = this.output;
1264
+ const savedIndent = this.indent;
1265
+ this.output = [];
1266
+ this.indent = 0;
1267
+ this.visitStatement(stmt);
1268
+ const result = this.output.join('\n');
1269
+ this.output = saved;
1270
+ this.indent = savedIndent;
1271
+ return result;
1272
+ }
1273
+
1274
+ // ===== CRUD =====
1275
+
1276
+ visitCrud(appName, node) {
1277
+ const path = this.rawString(node.path);
1278
+ const schema = node.schemaName;
1279
+ const store = `__${schema.toLowerCase()}_store`;
1280
+ const idVar = `__${schema.toLowerCase()}_id`;
1281
+
1282
+ this.emitRaw('');
1283
+ this.emit(`${store} = []`);
1284
+ this.emit(`${idVar} = 1`);
1285
+ this.emitRaw('');
1286
+
1287
+ // List all
1288
+ this.emit(`@${appName}.route(${JSON.stringify(path)}, methods=['GET'])`);
1289
+ this.emit(`def ${schema.toLowerCase()}_list():`);
1290
+ this.indent++;
1291
+ this.emit(`return jsonify(${store})`);
1292
+ this.indent--;
1293
+ this.emitRaw('');
1294
+
1295
+ // Get by id
1296
+ this.emit(`@${appName}.route(${JSON.stringify(path + '/<id>')}, methods=['GET'])`);
1297
+ this.emit(`def ${schema.toLowerCase()}_get(id):`);
1298
+ this.indent++;
1299
+ this.emit(`item = next((i for i in ${store} if str(i['id']) == str(id)), None)`);
1300
+ this.emit(`if not item:`);
1301
+ this.indent++;
1302
+ this.emit(`return jsonify({'error': 'Not found'}), 404`);
1303
+ this.indent--;
1304
+ this.emit(`return jsonify(item)`);
1305
+ this.indent--;
1306
+ this.emitRaw('');
1307
+
1308
+ // Create
1309
+ this.emit(`@${appName}.route(${JSON.stringify(path)}, methods=['POST'])`);
1310
+ this.emit(`def ${schema.toLowerCase()}_create():`);
1311
+ this.indent++;
1312
+ this.emit(`global ${idVar}`);
1313
+ this.emit(`body = request.get_json()`);
1314
+ this.emit(`body['id'] = ${idVar}`);
1315
+ this.emit(`${idVar} += 1`);
1316
+ this.emit(`${store}.append(body)`);
1317
+ this.emit(`return jsonify(body), 201`);
1318
+ this.indent--;
1319
+ this.emitRaw('');
1320
+
1321
+ // Update
1322
+ this.emit(`@${appName}.route(${JSON.stringify(path + '/<id>')}, methods=['PUT'])`);
1323
+ this.emit(`def ${schema.toLowerCase()}_update(id):`);
1324
+ this.indent++;
1325
+ this.emit(`body = request.get_json()`);
1326
+ this.emit(`for i, item in enumerate(${store}):`);
1327
+ this.indent++;
1328
+ this.emit(`if str(item['id']) == str(id):`);
1329
+ this.indent++;
1330
+ this.emit(`${store}[i] = {**item, **body}`);
1331
+ this.emit(`return jsonify(${store}[i])`);
1332
+ this.indent--;
1333
+ this.indent--;
1334
+ this.emit(`return jsonify({'error': 'Not found'}), 404`);
1335
+ this.indent--;
1336
+ this.emitRaw('');
1337
+
1338
+ // Delete
1339
+ this.emit(`@${appName}.route(${JSON.stringify(path + '/<id>')}, methods=['DELETE'])`);
1340
+ this.emit(`def ${schema.toLowerCase()}_delete(id):`);
1341
+ this.indent++;
1342
+ this.emit(`global ${store}`);
1343
+ this.emit(`${store} = [i for i in ${store} if str(i['id']) != str(id)]`);
1344
+ this.emit(`return jsonify({'deleted': True})`);
1345
+ this.indent--;
1346
+ this.emitRaw('');
1347
+ }
1348
+
1349
+ // ===== Static files =====
1350
+
1351
+ visitStatic(appName, node) {
1352
+ let raw = node.path.raw || node.path.parts?.map(p => p.value).join('') || 'public';
1353
+ if (raw.startsWith('/')) raw = raw.slice(1);
1354
+ this.addFromImport('flask', 'send_from_directory');
1355
+ this.emitRaw('');
1356
+ this.emit(`@${appName}.route('/<path:filename>')`);
1357
+ this.emit(`def serve_static(filename):`);
1358
+ this.indent++;
1359
+ this.emit(`return send_from_directory(${JSON.stringify(raw)}, filename)`);
1360
+ this.indent--;
1361
+ this.emitRaw('');
1362
+ }
1363
+
1364
+ // ===== Rate limiting =====
1365
+
1366
+ visitLimit(appName, node) {
1367
+ const path = this.rawString(node.path);
1368
+ const max = this.expr(node.max);
1369
+ const window = this.expr(node.window);
1370
+ this.emit(`# rate limit: ${max} requests per ${window} on ${path}`);
1371
+ }
1372
+
1373
+ // ===== WebSocket =====
1374
+
1375
+ visitWs(node) {
1376
+ this.addImport('flask_socketio');
1377
+ this.addFromImport('flask_socketio', 'SocketIO');
1378
+ this.addFromImport('flask_socketio', 'emit');
1379
+ const events = node.events || [];
1380
+
1381
+ this.emitRaw('');
1382
+ this.emit(`socketio = SocketIO(app, cors_allowed_origins='*')`);
1383
+ this.emitRaw('');
1384
+
1385
+ for (const evt of events) {
1386
+ const evtName = evt.name.raw || evt.name.parts?.map(p => p.value).join('');
1387
+ if (evtName === 'connect' || evtName === 'open') {
1388
+ this.emit(`@socketio.on('connect')`);
1389
+ this.emit(`def handle_connect():`);
1390
+ } else if (evtName === 'message') {
1391
+ const param = evt.params[0] || 'data';
1392
+ this.emit(`@socketio.on('message')`);
1393
+ this.emit(`def handle_message(${param}):`);
1394
+ } else if (evtName === 'close') {
1395
+ this.emit(`@socketio.on('disconnect')`);
1396
+ this.emit(`def handle_disconnect():`);
1397
+ } else {
1398
+ this.emit(`@socketio.on(${JSON.stringify(evtName)})`);
1399
+ const params = evt.params.length > 0 ? evt.params.join(', ') : 'data';
1400
+ this.emit(`def handle_${evtName}(${params}):`);
1401
+ }
1402
+ this.indent++;
1403
+ if (evt.body.length === 0) {
1404
+ this.emit('pass');
1405
+ } else {
1406
+ for (const stmt of evt.body) this.visitStatement(stmt);
1407
+ }
1408
+ this.indent--;
1409
+ this.emitRaw('');
1410
+ }
1411
+ }
1412
+
1413
+ // ===== Session =====
1414
+
1415
+ visitSession(appName, node) {
1416
+ this.emit(`${appName}.secret_key = 'naide-session-secret'`);
1417
+ this.emit(`# Flask sessions enabled via app.secret_key`);
1418
+ this.emitRaw('');
1419
+ }
1420
+
1421
+ // ===== Cache =====
1422
+
1423
+ visitCache(appName, node) {
1424
+ const path = this.rawString(node.path);
1425
+ const ttl = this.rawString(node.ttl);
1426
+ this.emit(`# cache: ${path} for ${ttl}`);
1427
+ }
1428
+
1429
+ // ===== Prompt template =====
1430
+
1431
+ visitPrompt(node) {
1432
+ const name = node.name;
1433
+ const parts = node.parts.map(p => this.generateString(p)).join(' + "\\n" + ');
1434
+ const defaults = node.defaults ? this.expr(node.defaults) : '{}';
1435
+ this.emitRaw('');
1436
+ this.emit(`def ${name}(vars={}):`);
1437
+ this.indent++;
1438
+ this.emit(`defaults = ${defaults}`);
1439
+ this.emit(`merged = {**defaults, **vars}`);
1440
+ this.emit(`template = ${parts}`);
1441
+ this.emit(`for k, v in merged.items():`);
1442
+ this.indent++;
1443
+ this.emit(`template = template.replace('{' + k + '}', str(v))`);
1444
+ this.indent--;
1445
+ this.emit(`return template`);
1446
+ this.indent--;
1447
+ this.emitRaw('');
1448
+ }
1449
+ }