naider 1.11.1 → 1.12.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.
@@ -141,7 +141,13 @@ export class PythonGenerator {
141
141
  case 'WsDecl': return this.visitWs(node);
142
142
  case 'SessionDecl': return this.visitSession('app', node);
143
143
  case 'CacheDecl': return this.visitCache('app', node);
144
+ case 'EveryDecl': return this.visitEvery(node);
144
145
  case 'PromptDecl': return this.visitPrompt(node);
146
+ case 'Page': return this.visitPage(node);
147
+ case 'CliApp': return this.visitCli(node);
148
+ case 'MailConfig': return this.visitMail(node);
149
+ case 'DesktopApp': return this.visitDesktop(node);
150
+ case 'Screen': return this.visitScreen(node);
145
151
  default:
146
152
  this.emit(`# unknown: ${node.type}`);
147
153
  }
@@ -447,6 +453,8 @@ export class PythonGenerator {
447
453
  this.visitSession(node.name, child);
448
454
  } else if (child.type === 'CacheDecl') {
449
455
  this.visitCache(node.name, child);
456
+ } else if (child.type === 'GraphqlDecl') {
457
+ this.visitGraphql(node.name, child);
450
458
  } else {
451
459
  this.visitStatement(child);
452
460
  }
@@ -1427,11 +1435,24 @@ export class PythonGenerator {
1427
1435
  this.emit(`# cache: ${path} for ${ttl}`);
1428
1436
  }
1429
1437
 
1430
- // ===== Discord Bot (discord.py) =====
1438
+ // ===== Bot (multi-platform) =====
1431
1439
 
1432
1440
  visitBot(node) {
1433
- const tokenExpr = node.token ? this.expr(node.token) : "os.environ.get('DISCORD_TOKEN')";
1441
+ const tokenExpr = node.token ? this.expr(node.token) : "os.environ.get('BOT_TOKEN')";
1442
+ const botType = this.rawBotType(node.botType);
1434
1443
 
1444
+ if (botType === 'slack') return this.visitSlackBot(node, tokenExpr);
1445
+ if (botType === 'telegram') return this.visitTelegramBot(node, tokenExpr);
1446
+ if (botType === 'line') return this.visitLineBot(node, tokenExpr);
1447
+ return this.visitDiscordBot(node, tokenExpr);
1448
+ }
1449
+
1450
+ rawBotType(bt) {
1451
+ if (!bt) return 'discord';
1452
+ return bt.raw || bt.parts?.map(p => p.value).join('') || 'discord';
1453
+ }
1454
+
1455
+ visitDiscordBot(node, tokenExpr) {
1435
1456
  this.addImport('discord');
1436
1457
  this.addFromImport('discord.ext', 'commands');
1437
1458
  this.emitRaw('');
@@ -1496,6 +1517,424 @@ export class PythonGenerator {
1496
1517
  this.emitRaw('');
1497
1518
  }
1498
1519
 
1520
+ visitSlackBot(node, tokenExpr) {
1521
+ this.addFromImport('slack_bolt', 'App');
1522
+ this.emitRaw('');
1523
+ this.emit(`${node.name} = App(token=${tokenExpr}, signing_secret=os.environ.get('SLACK_SIGNING_SECRET'))`);
1524
+ this.usesOs = true;
1525
+ this.emitRaw('');
1526
+
1527
+ for (const handler of node.handlers) {
1528
+ if (handler.type === 'BotEvent') {
1529
+ const evtName = handler.event.raw || handler.event.parts?.map(p => p.value).join('');
1530
+ if (evtName === 'message') {
1531
+ this.emit(`@${node.name}.message("")`);
1532
+ this.emit(`def handle_message(message, say):`);
1533
+ } else {
1534
+ this.emit(`@${node.name}.event("${evtName}")`);
1535
+ this.emit(`def handle_${evtName}(event):`);
1536
+ }
1537
+ this.indent++;
1538
+ if (handler.body.length === 0) { this.emit('pass'); } else {
1539
+ for (const stmt of handler.body) this.visitStatement(stmt);
1540
+ }
1541
+ this.indent--;
1542
+ this.emitRaw('');
1543
+ } else if (handler.type === 'BotSlashCmd') {
1544
+ const cmdName = handler.name.raw || handler.name.parts?.map(p => p.value).join('');
1545
+ this.emit(`@${node.name}.command("/${cmdName}")`);
1546
+ this.emit(`def handle_${cmdName}(ack, respond, command):`);
1547
+ this.indent++;
1548
+ this.emit('ack()');
1549
+ for (const stmt of handler.body) this.visitStatement(stmt);
1550
+ this.indent--;
1551
+ this.emitRaw('');
1552
+ }
1553
+ }
1554
+
1555
+ this.emit(`if __name__ == '__main__':`);
1556
+ this.indent++;
1557
+ this.emit(`${node.name}.start(port=3000)`);
1558
+ this.indent--;
1559
+ this.emitRaw('');
1560
+ }
1561
+
1562
+ visitTelegramBot(node, tokenExpr) {
1563
+ this.addFromImport('telegram.ext', 'ApplicationBuilder');
1564
+ this.addFromImport('telegram.ext', 'MessageHandler');
1565
+ this.addFromImport('telegram.ext', 'CommandHandler');
1566
+ this.addFromImport('telegram.ext', 'filters');
1567
+ this.emitRaw('');
1568
+ this.emit(`${node.name} = ApplicationBuilder().token(${tokenExpr}).build()`);
1569
+ this.emitRaw('');
1570
+
1571
+ for (const handler of node.handlers) {
1572
+ if (handler.type === 'BotEvent') {
1573
+ const evtName = handler.event.raw || handler.event.parts?.map(p => p.value).join('');
1574
+ const params = handler.params.length > 0 ? handler.params.join(', ') : 'update, context';
1575
+ this.emit(`async def handle_${evtName}(${params}):`);
1576
+ this.indent++;
1577
+ if (handler.body.length === 0) { this.emit('pass'); } else {
1578
+ for (const stmt of handler.body) this.visitStatement(stmt);
1579
+ }
1580
+ this.indent--;
1581
+ this.emit(`${node.name}.add_handler(MessageHandler(filters.TEXT, handle_${evtName}))`);
1582
+ this.emitRaw('');
1583
+ } else if (handler.type === 'BotSlashCmd') {
1584
+ const cmdName = handler.name.raw || handler.name.parts?.map(p => p.value).join('');
1585
+ this.emit(`async def cmd_${cmdName}(update, context):`);
1586
+ this.indent++;
1587
+ if (handler.body.length === 0) { this.emit('pass'); } else {
1588
+ for (const stmt of handler.body) this.visitStatement(stmt);
1589
+ }
1590
+ this.indent--;
1591
+ this.emit(`${node.name}.add_handler(CommandHandler("${cmdName}", cmd_${cmdName}))`);
1592
+ this.emitRaw('');
1593
+ }
1594
+ }
1595
+
1596
+ this.emit(`${node.name}.run_polling()`);
1597
+ this.emitRaw('');
1598
+ }
1599
+
1600
+ visitLineBot(node, tokenExpr) {
1601
+ this.addFromImport('linebot', 'LineBotApi');
1602
+ this.addFromImport('linebot', 'WebhookHandler');
1603
+ this.addFromImport('flask', 'Flask');
1604
+ this.addFromImport('flask', 'request');
1605
+ this.addFromImport('flask', 'abort');
1606
+ this.emitRaw('');
1607
+ this.emit(`${node.name} = LineBotApi(${tokenExpr})`);
1608
+ this.emit(`__handler = WebhookHandler(os.environ.get('LINE_CHANNEL_SECRET'))`);
1609
+ this.usesOs = true;
1610
+ this.emit(`__app = Flask(__name__)`);
1611
+ this.emitRaw('');
1612
+
1613
+ this.emit(`@__app.route('/webhook', methods=['POST'])`);
1614
+ this.emit(`def webhook():`);
1615
+ this.indent++;
1616
+ this.emit(`signature = request.headers['X-Line-Signature']`);
1617
+ this.emit(`body = request.get_data(as_text=True)`);
1618
+ this.emit(`__handler.handle(body, signature)`);
1619
+ this.emit(`return 'OK'`);
1620
+ this.indent--;
1621
+ this.emitRaw('');
1622
+
1623
+ for (const handler of node.handlers) {
1624
+ if (handler.type === 'BotEvent') {
1625
+ const evtName = handler.event.raw || handler.event.parts?.map(p => p.value).join('');
1626
+ if (evtName === 'message') {
1627
+ this.emit(`@__handler.add(MessageEvent, message=TextMessage)`);
1628
+ this.emit(`def handle_message(event):`);
1629
+ } else {
1630
+ this.emit(`@__handler.add(${evtName})`);
1631
+ this.emit(`def handle_${evtName}(event):`);
1632
+ }
1633
+ this.indent++;
1634
+ if (handler.body.length === 0) { this.emit('pass'); } else {
1635
+ for (const stmt of handler.body) this.visitStatement(stmt);
1636
+ }
1637
+ this.indent--;
1638
+ this.emitRaw('');
1639
+ }
1640
+ }
1641
+
1642
+ this.emit(`if __name__ == '__main__':`);
1643
+ this.indent++;
1644
+ this.emit(`__app.run(port=3000)`);
1645
+ this.indent--;
1646
+ this.emitRaw('');
1647
+ }
1648
+
1649
+ // ===== Every (cron/scheduler) =====
1650
+
1651
+ visitEvery(node) {
1652
+ const raw = node.interval?.value?.raw || node.interval?.value?.parts?.map(p => p.value).join('') || '';
1653
+ const isCron = /^[\d*\/,-]+\s+[\d*\/,-]+\s+[\d*\/,-]+\s+[\d*\/,-]+\s+[\d*\/,-]+/.test(raw.trim());
1654
+
1655
+ if (isCron) {
1656
+ this.addFromImport('apscheduler.schedulers.blocking', 'BlockingScheduler');
1657
+ this.addFromImport('apscheduler.triggers.cron', 'CronTrigger');
1658
+ this.emitRaw('');
1659
+ const parts = raw.trim().split(/\s+/);
1660
+ this.emit(`__scheduler = BlockingScheduler()`);
1661
+ this.emit(`def __every_job():`);
1662
+ this.indent++;
1663
+ if (node.body.length === 0) { this.emit('pass'); } else {
1664
+ for (const stmt of node.body) this.visitStatement(stmt);
1665
+ }
1666
+ this.indent--;
1667
+ this.emit(`__scheduler.add_job(__every_job, CronTrigger(minute='${parts[0]}', hour='${parts[1]}', day='${parts[2]}', month='${parts[3]}', day_of_week='${parts[4]}'))`);
1668
+ this.emit(`__scheduler.start()`);
1669
+ } else {
1670
+ this.addImport('schedule');
1671
+ this.addImport('time');
1672
+ this.emitRaw('');
1673
+ const interval = this.expr(node.interval);
1674
+ this.emit(`def __every_job():`);
1675
+ this.indent++;
1676
+ if (node.body.length === 0) { this.emit('pass'); } else {
1677
+ for (const stmt of node.body) this.visitStatement(stmt);
1678
+ }
1679
+ this.indent--;
1680
+ this.emit(`schedule.every(${interval}).do(__every_job)`);
1681
+ this.emit(`while True:`);
1682
+ this.indent++;
1683
+ this.emit(`schedule.run_pending()`);
1684
+ this.emit(`time.sleep(1)`);
1685
+ this.indent--;
1686
+ }
1687
+ this.emitRaw('');
1688
+ }
1689
+
1690
+ // ===== GraphQL =====
1691
+
1692
+ visitGraphql(appName, node) {
1693
+ const path = this.generateString(node.path);
1694
+ this.addFromImport('flask_graphql', 'GraphQLView');
1695
+ this.addFromImport('graphene', '*');
1696
+ this.emitRaw('');
1697
+
1698
+ const schemaNames = [...(this.schemas || new Map()).keys()];
1699
+ if (schemaNames.length > 0) {
1700
+ for (const name of schemaNames) {
1701
+ const schema = this.schemas.get(name);
1702
+ this.emit(`class ${name}Type(ObjectType):`);
1703
+ this.indent++;
1704
+ for (const f of schema.fields) {
1705
+ const gqlType = this.toGraphqlType(f.type);
1706
+ this.emit(`${f.name} = ${gqlType}()`);
1707
+ }
1708
+ this.indent--;
1709
+ this.emitRaw('');
1710
+ }
1711
+ this.emit(`class Query(ObjectType):`);
1712
+ this.indent++;
1713
+ for (const name of schemaNames) {
1714
+ this.emit(`${name.toLowerCase()}s = List(${name}Type)`);
1715
+ this.emit(`def resolve_${name.toLowerCase()}s(self, info):`);
1716
+ this.indent++;
1717
+ this.emit(`return []`);
1718
+ this.indent--;
1719
+ }
1720
+ this.indent--;
1721
+ } else {
1722
+ this.emit(`class Query(ObjectType):`);
1723
+ this.indent++;
1724
+ this.emit(`hello = String()`);
1725
+ this.emit(`def resolve_hello(self, info):`);
1726
+ this.indent++;
1727
+ this.emit(`return 'Hello from NAIDE GraphQL'`);
1728
+ this.indent--;
1729
+ this.indent--;
1730
+ }
1731
+
1732
+ this.emitRaw('');
1733
+ this.emit(`__gql_schema = Schema(query=Query)`);
1734
+ const rawPath = this.rawString(node.path);
1735
+ this.emit(`${appName}.add_url_rule(${JSON.stringify(rawPath)}, view_func=GraphQLView.as_view('graphql', schema=__gql_schema, graphiql=True))`);
1736
+ this.emitRaw('');
1737
+ }
1738
+
1739
+ toGraphqlType(naideType) {
1740
+ const map = { 'str': 'graphene.String', 'int': 'graphene.Int', 'num': 'graphene.Float', 'bool': 'graphene.Boolean', 'auto': 'graphene.ID', 'timestamp': 'graphene.String', 'enum': 'graphene.String' };
1741
+ return map[naideType] || 'graphene.String';
1742
+ }
1743
+
1744
+ // ===== Page (HTML generation) =====
1745
+
1746
+ visitPage(node) {
1747
+ const filename = this.rawString(node.filename);
1748
+ const headParts = [];
1749
+ const bodyParts = [];
1750
+ let pageTitle = 'NAIDE Page';
1751
+
1752
+ for (const el of node.elements) {
1753
+ this.classifyPageElement(el, headParts, bodyParts, (t) => { pageTitle = t; });
1754
+ }
1755
+
1756
+ this.emit(`with open(${JSON.stringify(filename)}, 'w') as __f:`);
1757
+ this.indent++;
1758
+ this.emit(`__f.write("""<!DOCTYPE html>`);
1759
+ this.emit(`<html lang="en">`);
1760
+ this.emit(`<head>`);
1761
+ this.emit(`<meta charset="UTF-8">`);
1762
+ this.emit(`<meta name="viewport" content="width=device-width, initial-scale=1.0">`);
1763
+ this.emit(`<title>${pageTitle}</title>`);
1764
+ for (const h of headParts) this.emit(h);
1765
+ this.emit(`</head>`);
1766
+ this.emit(`<body>`);
1767
+ for (const b of bodyParts) this.emit(b);
1768
+ this.emit(`</body>`);
1769
+ this.emit(`</html>""")`);
1770
+ this.indent--;
1771
+ this.emit(`print(f"Generated: ${filename}")`);
1772
+ this.emitRaw('');
1773
+ }
1774
+
1775
+ classifyPageElement(el, head, body, setTitle) {
1776
+ const tag = el.tag;
1777
+ const arg0 = el.args[0] ? this.rawString(el.args[0]) : '';
1778
+ const arg1 = el.args[1] ? this.rawString(el.args[1]) : '';
1779
+ if (tag === 'title') { setTitle(arg0); return; }
1780
+ if (tag === 'style' || tag === 'link') { head.push(`<link rel="stylesheet" href="${arg0}">`); return; }
1781
+ if (tag === 'meta') { head.push(`<meta name="${arg0}" content="${arg1}">`); return; }
1782
+ if (tag === 'script') { body.push(`<script src="${arg0}"></script>`); return; }
1783
+ if (tag === 'img') { body.push(`<img src="${arg0}" alt="${arg1}">`); return; }
1784
+ if (tag === 'a') { body.push(`<a href="${arg0}">${arg1}</a>`); return; }
1785
+ if (tag === 'input') { body.push(`<input type="${arg0}" name="${arg1}">`); return; }
1786
+ if (el.children && el.children.length > 0) {
1787
+ const cls = arg0 ? ` class="${arg0}"` : '';
1788
+ body.push(`<${tag}${cls}>`);
1789
+ for (const child of el.children) this.classifyPageElement(child, head, body, setTitle);
1790
+ body.push(`</${tag}>`);
1791
+ } else {
1792
+ body.push(`<${tag}>${arg0}</${tag}>`);
1793
+ }
1794
+ }
1795
+
1796
+ // ===== CLI App =====
1797
+
1798
+ visitCli(node) {
1799
+ this.addImport('argparse');
1800
+ this.emitRaw('');
1801
+ const desc = node.description ? this.rawString(node.description) : node.name;
1802
+ this.emit(`__parser = argparse.ArgumentParser(description=${JSON.stringify(desc)})`);
1803
+
1804
+ for (const arg of node.args) {
1805
+ const argName = this.rawString(arg.name);
1806
+ const pyType = arg.type === 'int' ? 'int' : arg.type === 'num' ? 'float' : 'str';
1807
+ const desc = arg.description ? `, help=${JSON.stringify(this.rawString(arg.description))}` : '';
1808
+ this.emit(`__parser.add_argument('--${argName}', type=${pyType}${desc})`);
1809
+ }
1810
+ for (const flag of node.flags) {
1811
+ const s = this.rawString(flag.short);
1812
+ const l = this.rawString(flag.long);
1813
+ const desc = flag.description ? `, help=${JSON.stringify(this.rawString(flag.description))}` : '';
1814
+ this.emit(`__parser.add_argument('-${s}', '--${l}', action='store_true'${desc})`);
1815
+ }
1816
+
1817
+ this.emit(`args = __parser.parse_args()`);
1818
+ this.emitRaw('');
1819
+
1820
+ if (node.run) {
1821
+ const runParam = node.run.params.length > 0 ? node.run.params[0] : 'args';
1822
+ if (runParam !== 'args') {
1823
+ this.emit(`${runParam} = args`);
1824
+ }
1825
+ for (const stmt of node.run.body) this.visitStatement(stmt);
1826
+ }
1827
+ this.emitRaw('');
1828
+ }
1829
+
1830
+ // ===== Mail =====
1831
+
1832
+ visitMail(node) {
1833
+ this.addImport('smtplib');
1834
+ this.addFromImport('email.mime.text', 'MIMEText');
1835
+ this.addFromImport('email.mime.multipart', 'MIMEMultipart');
1836
+ this.usesOs = true;
1837
+ this.emitRaw('');
1838
+
1839
+ const host = this.generateString(node.host);
1840
+ const port = this.expr(node.port);
1841
+ const user = node.user ? this.expr(node.user) : "os.environ.get('MAIL_USER')";
1842
+ const pass = node.pass ? this.expr(node.pass) : "os.environ.get('MAIL_PASS')";
1843
+
1844
+ this.emit(`class Mail:`);
1845
+ this.indent++;
1846
+ this.emit(`def __init__(self, host, port, user, password):`);
1847
+ this.indent++;
1848
+ this.emit(`self.host = host`);
1849
+ this.emit(`self.port = port`);
1850
+ this.emit(`self.user = user`);
1851
+ this.emit(`self.password = password`);
1852
+ this.indent--;
1853
+ this.emitRaw('');
1854
+ this.emit(`def send(self, to, subject, body):`);
1855
+ this.indent++;
1856
+ this.emit(`msg = MIMEText(body)`);
1857
+ this.emit(`msg['Subject'] = subject`);
1858
+ this.emit(`msg['From'] = self.user`);
1859
+ this.emit(`msg['To'] = to`);
1860
+ this.emit(`with smtplib.SMTP(self.host, self.port) as server:`);
1861
+ this.indent++;
1862
+ this.emit(`server.starttls()`);
1863
+ this.emit(`server.login(self.user, self.password)`);
1864
+ this.emit(`server.send_message(msg)`);
1865
+ this.indent--;
1866
+ this.indent--;
1867
+ this.indent--;
1868
+ this.emitRaw('');
1869
+ this.emit(`mail = Mail(${host}, ${port}, ${user}, ${pass})`);
1870
+ this.emitRaw('');
1871
+ }
1872
+
1873
+ // ===== Desktop (pywebview) =====
1874
+
1875
+ visitDesktop(node) {
1876
+ this.addImport('webview');
1877
+ this.emitRaw('');
1878
+ const title = node.title ? this.rawString(node.title) : node.name;
1879
+ const width = node.width ? this.expr(node.width) : '800';
1880
+ const height = node.height ? this.expr(node.height) : '600';
1881
+ const load = node.load ? this.expr(node.load) : '"index.html"';
1882
+ this.emit(`webview.create_window(${JSON.stringify(title)}, ${load}, width=${width}, height=${height})`);
1883
+ this.emit(`webview.start()`);
1884
+ this.emitRaw('');
1885
+ }
1886
+
1887
+ // ===== Screen (Kivy) =====
1888
+
1889
+ visitScreen(node) {
1890
+ this.addFromImport('kivy.app', 'App');
1891
+ this.addFromImport('kivy.uix.boxlayout', 'BoxLayout');
1892
+ this.addFromImport('kivy.uix.label', 'Label');
1893
+ this.addFromImport('kivy.uix.button', 'Button');
1894
+ this.addFromImport('kivy.uix.textinput', 'TextInput');
1895
+ this.emitRaw('');
1896
+
1897
+ this.emit(`class ${node.name}Screen(BoxLayout):`);
1898
+ this.indent++;
1899
+ this.emit(`def __init__(self, **kwargs):`);
1900
+ this.indent++;
1901
+ this.emit(`super().__init__(**kwargs)`);
1902
+ this.emit(`self.orientation = 'vertical'`);
1903
+ this.emit(`self.padding = 16`);
1904
+
1905
+ for (const el of node.elements) {
1906
+ const tag = el.tag;
1907
+ const arg0 = el.args[0] ? this.rawString(el.args[0]) : '';
1908
+ if (tag === 'text') {
1909
+ this.emit(`self.add_widget(Label(text=${JSON.stringify(arg0)}))`);
1910
+ } else if (tag === 'button') {
1911
+ this.emit(`self.add_widget(Button(text=${JSON.stringify(arg0)}))`);
1912
+ } else if (tag === 'input') {
1913
+ this.emit(`self.add_widget(TextInput(hint_text=${JSON.stringify(arg0)}))`);
1914
+ } else {
1915
+ this.emit(`self.add_widget(Label(text=${JSON.stringify(arg0)}))`);
1916
+ }
1917
+ }
1918
+
1919
+ this.indent--;
1920
+ this.indent--;
1921
+ this.emitRaw('');
1922
+
1923
+ this.emit(`class ${node.name}App(App):`);
1924
+ this.indent++;
1925
+ this.emit(`def build(self):`);
1926
+ this.indent++;
1927
+ this.emit(`return ${node.name}Screen()`);
1928
+ this.indent--;
1929
+ this.indent--;
1930
+ this.emitRaw('');
1931
+ this.emit(`if __name__ == '__main__':`);
1932
+ this.indent++;
1933
+ this.emit(`${node.name}App().run()`);
1934
+ this.indent--;
1935
+ this.emitRaw('');
1936
+ }
1937
+
1499
1938
  // ===== Prompt template =====
1500
1939
 
1501
1940
  visitPrompt(node) {