ErisPulse 2.3.3.dev0__py3-none-any.whl → 2.3.4.dev0__py3-none-any.whl

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 (70) hide show
  1. ErisPulse/Core/Bases/__init__.pyi +14 -0
  2. ErisPulse/Core/Bases/adapter.py +13 -1
  3. ErisPulse/Core/Bases/adapter.pyi +140 -0
  4. ErisPulse/Core/Bases/module.pyi +52 -0
  5. ErisPulse/Core/Event/__init__.pyi +26 -0
  6. ErisPulse/Core/Event/base.pyi +62 -0
  7. ErisPulse/Core/Event/command.pyi +113 -0
  8. ErisPulse/Core/Event/exceptions.pyi +43 -0
  9. ErisPulse/Core/Event/message.pyi +93 -0
  10. ErisPulse/Core/Event/meta.pyi +92 -0
  11. ErisPulse/Core/Event/notice.pyi +108 -0
  12. ErisPulse/Core/Event/request.pyi +76 -0
  13. ErisPulse/Core/Event/wrapper.py +2 -3
  14. ErisPulse/Core/Event/wrapper.pyi +403 -0
  15. ErisPulse/Core/__init__.py +16 -13
  16. ErisPulse/Core/__init__.pyi +16 -0
  17. ErisPulse/Core/_self_config.pyi +72 -0
  18. ErisPulse/Core/adapter.pyi +229 -0
  19. ErisPulse/Core/config.pyi +70 -0
  20. ErisPulse/Core/exceptions.pyi +60 -0
  21. ErisPulse/Core/lifecycle.py +6 -1
  22. ErisPulse/Core/lifecycle.pyi +92 -0
  23. ErisPulse/Core/logger.pyi +168 -0
  24. ErisPulse/Core/module.pyi +178 -0
  25. ErisPulse/Core/router.pyi +120 -0
  26. ErisPulse/Core/storage.pyi +273 -0
  27. ErisPulse/__init__.py +10 -9
  28. ErisPulse/__init__.pyi +309 -0
  29. ErisPulse/__main__.pyi +24 -0
  30. ErisPulse/sdk_protocol.py +143 -0
  31. ErisPulse/sdk_protocol.pyi +97 -0
  32. ErisPulse/utils/__init__.py +1 -1
  33. ErisPulse/utils/__init__.pyi +16 -0
  34. ErisPulse/utils/cli/__init__.py +11 -0
  35. ErisPulse/utils/cli/__init__.pyi +13 -0
  36. ErisPulse/utils/cli/__main__.py +225 -0
  37. ErisPulse/utils/cli/__main__.pyi +81 -0
  38. ErisPulse/utils/cli/base.py +52 -0
  39. ErisPulse/utils/cli/base.pyi +50 -0
  40. ErisPulse/utils/cli/commands/__init__.py +6 -0
  41. ErisPulse/utils/cli/commands/__init__.pyi +12 -0
  42. ErisPulse/utils/cli/commands/init.py +400 -0
  43. ErisPulse/utils/cli/commands/init.pyi +82 -0
  44. ErisPulse/utils/cli/commands/install.py +307 -0
  45. ErisPulse/utils/cli/commands/install.pyi +70 -0
  46. ErisPulse/utils/cli/commands/list.py +165 -0
  47. ErisPulse/utils/cli/commands/list.pyi +56 -0
  48. ErisPulse/utils/cli/commands/list_remote.py +128 -0
  49. ErisPulse/utils/cli/commands/list_remote.pyi +47 -0
  50. ErisPulse/utils/cli/commands/run.py +112 -0
  51. ErisPulse/utils/cli/commands/run.pyi +48 -0
  52. ErisPulse/utils/cli/commands/self_update.py +237 -0
  53. ErisPulse/utils/cli/commands/self_update.pyi +59 -0
  54. ErisPulse/utils/cli/commands/uninstall.py +37 -0
  55. ErisPulse/utils/cli/commands/uninstall.pyi +37 -0
  56. ErisPulse/utils/cli/commands/upgrade.py +62 -0
  57. ErisPulse/utils/cli/commands/upgrade.pyi +38 -0
  58. ErisPulse/utils/cli/registry.py +112 -0
  59. ErisPulse/utils/cli/registry.pyi +99 -0
  60. ErisPulse/utils/console.pyi +20 -0
  61. ErisPulse/utils/package_manager.pyi +224 -0
  62. ErisPulse/utils/reload_handler.pyi +64 -0
  63. {erispulse-2.3.3.dev0.dist-info → erispulse-2.3.4.dev0.dist-info}/METADATA +6 -20
  64. erispulse-2.3.4.dev0.dist-info/RECORD +89 -0
  65. {erispulse-2.3.3.dev0.dist-info → erispulse-2.3.4.dev0.dist-info}/licenses/LICENSE +3 -3
  66. ErisPulse/Core/ux.py +0 -635
  67. ErisPulse/utils/cli.py +0 -1097
  68. erispulse-2.3.3.dev0.dist-info/RECORD +0 -35
  69. {erispulse-2.3.3.dev0.dist-info → erispulse-2.3.4.dev0.dist-info}/WHEEL +0 -0
  70. {erispulse-2.3.3.dev0.dist-info → erispulse-2.3.4.dev0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,400 @@
1
+ """
2
+ Init 命令实现
3
+
4
+ 交互式初始化 ErisPulse 项目
5
+ """
6
+
7
+ import asyncio
8
+ import concurrent.futures
9
+ import subprocess
10
+ import sys
11
+ from argparse import ArgumentParser
12
+ from pathlib import Path
13
+ from rich.prompt import Confirm, Prompt
14
+
15
+ from ...console import console
16
+ from ...package_manager import PackageManager
17
+ from ..base import Command
18
+
19
+
20
+ class InitCommand(Command):
21
+ """初始化命令"""
22
+
23
+ name = "init"
24
+ description = "交互式初始化 ErisPulse 项目"
25
+
26
+ def __init__(self):
27
+ """初始化命令"""
28
+ self.package_manager = PackageManager()
29
+
30
+ def add_arguments(self, parser: ArgumentParser):
31
+ """添加命令参数"""
32
+ parser.add_argument(
33
+ '--project-name', '-n',
34
+ help='项目名称 (可选,交互式初始化时将会询问)'
35
+ )
36
+ parser.add_argument(
37
+ '--quick', '-q',
38
+ action='store_true',
39
+ help='快速模式,跳过交互式配置'
40
+ )
41
+ parser.add_argument(
42
+ '--force', '-f',
43
+ action='store_true',
44
+ help='强制覆盖现有配置'
45
+ )
46
+
47
+ def execute(self, args):
48
+ """执行命令"""
49
+ if args.quick and args.project_name:
50
+ # 快速模式:只创建项目,不进行交互配置
51
+ success = self._init_project(args.project_name, [])
52
+ else:
53
+ # 交互式模式:引导用户完成项目和配置设置
54
+ success = self._interactive_init(args.project_name, args.force)
55
+
56
+ if success:
57
+ console.print("[success]项目初始化完成[/]")
58
+ else:
59
+ console.print("[error]项目初始化失败[/]")
60
+ sys.exit(1)
61
+
62
+ def _init_project(self, project_name: str, adapter_list: list = None) -> bool:
63
+ """
64
+ 初始化新项目
65
+
66
+ :param project_name: 项目名称
67
+ :param adapter_list: 需要初始化的适配器列表
68
+ :return: 是否初始化成功
69
+ """
70
+ try:
71
+ project_path = Path(project_name)
72
+ if project_path.exists():
73
+ if project_path.is_dir():
74
+ console.print(f"[yellow]目录 {project_name} 已存在[/yellow]")
75
+ else:
76
+ console.print(f"[red]文件 {project_name} 已存在且不是目录[/red]")
77
+ return False
78
+ else:
79
+ project_path.mkdir()
80
+ console.print(f"[green]创建项目目录: {project_name}[/green]")
81
+
82
+ # 创建基本目录结构
83
+ dirs = ["config", "logs"]
84
+ for dir_name in dirs:
85
+ dir_path = project_path / dir_name
86
+ dir_path.mkdir(exist_ok=True)
87
+ console.print(f"[green]创建目录: {dir_name}[/green]")
88
+
89
+ # 创建配置文件
90
+ config_file = project_path / "config.toml"
91
+ if not config_file.exists():
92
+ with open(config_file, "w", encoding="utf-8") as f:
93
+ f.write("# ErisPulse 配置文件\n\n")
94
+ f.write("[ErisPulse]\n")
95
+ f.write("# 全局配置\n\n")
96
+ f.write("[ErisPulse.server]\n")
97
+ f.write('host = "0.0.0.0"\n')
98
+ f.write("port = 8000\n\n")
99
+ f.write("[ErisPulse.logger]\n")
100
+ f.write('level = "INFO"\n')
101
+ f.write("log_files = [\"logs/app.log\"]\n")
102
+ f.write("memory_limit = 1000\n\n")
103
+
104
+ # 添加适配器配置
105
+ if adapter_list:
106
+ f.write("[ErisPulse.adapters]\n")
107
+ f.write("# 适配器配置\n\n")
108
+ f.write("[ErisPulse.adapters.status]\n")
109
+ for adapter in adapter_list:
110
+ f.write(f'{adapter} = false # 默认禁用,需要时启用\n')
111
+ f.write("\n")
112
+
113
+ console.print("[green]创建配置文件: config.toml[/green]")
114
+
115
+ # 创建主程序文件
116
+ main_file = project_path / "main.py"
117
+ if not main_file.exists():
118
+ with open(main_file, "w", encoding="utf-8") as f:
119
+ f.write('"""')
120
+ f.write(f"\n{project_name} 主程序\n\n")
121
+ f.write("这是 ErisPulse 自动生成的主程序文件\n")
122
+ f.write("您可以根据需要修改此文件\n")
123
+ f.write('"""\n\n')
124
+ f.write("import asyncio\n")
125
+ f.write("from ErisPulse import sdk\n\n")
126
+ f.write("async def main():\n")
127
+ f.write(' """主程序入口"""\n')
128
+ f.write(" # 初始化 SDK\n")
129
+ f.write(" await sdk.init()\n\n")
130
+ f.write(" # 启动适配器\n")
131
+ f.write(" await sdk.adapter.startup()\n\n")
132
+ f.write(' print("ErisPulse 已启动,按 Ctrl+C 退出")\n')
133
+ f.write(" try:\n")
134
+ f.write(" while True:\n")
135
+ f.write(" await asyncio.sleep(1)\n")
136
+ f.write(" except KeyboardInterrupt:\n")
137
+ f.write(" print(\"\\n正在关闭...\")\n")
138
+ f.write(" await sdk.adapter.shutdown()\n\n")
139
+ f.write("if __name__ == \"__main__\":\n")
140
+ f.write(" asyncio.run(main())\n")
141
+
142
+ console.print("[green]创建主程序文件: main.py[/green]")
143
+
144
+ console.print("\n[bold green]项目 {} 初始化成功![/bold green]".format(project_name))
145
+ console.print("\n[cyan]接下来您可以:[/cyan]")
146
+ console.print(f"1. 编辑 {project_name}/config.toml 配置适配器")
147
+ console.print(f"2. 运行 [cyan]cd {project_name} \n ep run[/cyan] 启动项目")
148
+ console.print("\n访问 https://github.com/ErisPulse/ErisPulse/tree/main/docs 获取更多信息和文档")
149
+ return True
150
+
151
+ except Exception as e:
152
+ console.print(f"[red]初始化项目失败: {e}[/]")
153
+ return False
154
+
155
+ async def _fetch_available_adapters(self):
156
+ """
157
+ 从云端获取可用适配器列表
158
+
159
+ :return: 适配器名称到描述的映射
160
+ """
161
+ try:
162
+ # 使用与 PackageManager 相同的机制获取远程包列表
163
+ remote_packages = await self.package_manager.get_remote_packages()
164
+
165
+ adapters = {}
166
+ for name, info in remote_packages.get("adapters", {}).items():
167
+ adapters[name] = info.get("description", "")
168
+
169
+ if adapters:
170
+ return adapters
171
+ else:
172
+ console.print("[yellow]从远程源获取的适配器列表为空[/yellow]")
173
+ except Exception as e:
174
+ console.print(f"[red]从远程源获取适配器列表时出错: {e}[/red]")
175
+
176
+ # 如果云端请求失败,返回默认适配器列表
177
+ console.print("[yellow]使用默认适配器列表[/yellow]")
178
+ return {
179
+ "yunhu": "云湖平台适配器",
180
+ "telegram": "Telegram机器人适配器",
181
+ "onebot11": "OneBot11标准适配器",
182
+ "email": "邮件适配器"
183
+ }
184
+
185
+ def _configure_adapters_interactive_sync(self, project_path: str = None):
186
+ """
187
+ 交互式配置适配器的同步版本
188
+
189
+ :param project_path: 项目路径
190
+ """
191
+ from ErisPulse import config
192
+
193
+ # 如果提供了项目路径,则加载项目配置
194
+ if project_path:
195
+ project_config_path = Path(project_path) / "config.toml"
196
+ if project_config_path.exists():
197
+ config.CONFIG_FILE = str(project_config_path)
198
+ config.reload()
199
+ console.print(f"[green]已加载项目配置: {project_config_path}[/green]")
200
+
201
+ console.print("\n[bold]配置适配器[/bold]")
202
+ console.print("[info]正在从云端获取可用适配器列表...[/info]")
203
+
204
+ # 获取可用适配器列表(同步方式)
205
+ try:
206
+ with concurrent.futures.ThreadPoolExecutor() as executor:
207
+ future = executor.submit(asyncio.run, self._fetch_available_adapters())
208
+ adapters = future.result(timeout=10)
209
+ except Exception as e:
210
+ console.print(f"[red]获取适配器列表失败: {e}[/red]")
211
+ adapters = {}
212
+
213
+ if not adapters:
214
+ console.print("[red]未能获取到适配器列表[/red]")
215
+ return
216
+
217
+ # 显示可用适配器列表
218
+ adapter_list = list(adapters.items())
219
+ for i, (name, desc) in enumerate(adapter_list, 1):
220
+ console.print(f" {i}. {name} - {desc}")
221
+
222
+ # 选择适配器
223
+ selected_indices = Prompt.ask("\n[cyan]请输入要启用的适配器序号,多个用逗号分隔 (如: 1,3):[/cyan] ")
224
+ if not selected_indices:
225
+ console.print("[info]未选择任何适配器[/info]")
226
+ return
227
+
228
+ try:
229
+ indices = [int(idx.strip()) for idx in selected_indices.split(",")]
230
+ enabled_adapters = []
231
+
232
+ for idx in indices:
233
+ if 1 <= idx <= len(adapter_list):
234
+ adapter_name = adapter_list[idx-1][0]
235
+ enabled_adapters.append(adapter_name)
236
+ config.setConfig(f"ErisPulse.adapters.status.{adapter_name}", True)
237
+ console.print(f"[green]已启用适配器: {adapter_name}[/green]")
238
+ else:
239
+ console.print(f"[red]无效的序号: {idx}[/]")
240
+
241
+ # 禁用未选择的适配器
242
+ all_adapter_names = [name for name, _ in adapter_list]
243
+ for name in all_adapter_names:
244
+ if name not in enabled_adapters:
245
+ config.setConfig(f"ErisPulse.adapters.status.{name}", False)
246
+
247
+ console.print(f"\n[info]已启用 {len(enabled_adapters)} 个适配器[/info]")
248
+
249
+ # 询问是否要安装适配器
250
+ if enabled_adapters and Confirm.ask("\n[cyan]是否要安装选中的适配器?[/cyan]", default=True):
251
+ self._install_adapters(enabled_adapters, adapters)
252
+
253
+ # 保存配置
254
+ config.force_save()
255
+
256
+ except ValueError:
257
+ console.print("[red]输入格式错误,请输入数字序号[/red]")
258
+
259
+ def _install_adapters(self, adapter_names, adapters_info):
260
+ """
261
+ 安装选中的适配器
262
+
263
+ :param adapter_names: 适配器名称列表
264
+ :param adapters_info: 适配器信息字典
265
+ """
266
+ from ...package_manager import PackageManager
267
+ pkg_manager = PackageManager()
268
+
269
+ for adapter_name in adapter_names:
270
+ # 获取包名
271
+ package_name = None
272
+ try:
273
+ remote_packages = pkg_manager._cache.get("remote_packages", {})
274
+ if not remote_packages:
275
+ # 如果没有缓存,尝试同步获取
276
+ with concurrent.futures.ThreadPoolExecutor() as executor:
277
+ future = executor.submit(asyncio.run, pkg_manager.get_remote_packages())
278
+ remote_packages = future.result(timeout=10)
279
+
280
+ if adapter_name in remote_packages.get("adapters", {}):
281
+ package_name = remote_packages["adapters"][adapter_name].get("package")
282
+ except Exception:
283
+ pass
284
+
285
+ # 如果没有找到包名,使用适配器名称作为包名
286
+ if not package_name:
287
+ package_name = adapter_name
288
+
289
+ # 安装适配器
290
+ console.print(f"[info]正在安装适配器: {adapter_name} ({package_name})[/info]")
291
+ success = pkg_manager.install_package([package_name])
292
+
293
+ if success:
294
+ console.print(f"[green]适配器 {adapter_name} 安装成功[/green]")
295
+ else:
296
+ # 如果标准安装失败,尝试使用 uv
297
+ console.print("[yellow]标准安装失败,尝试使用 uv 安装...[/yellow]")
298
+ try:
299
+ result = subprocess.run(
300
+ [sys.executable, "-m", "uv", "pip", "install", package_name],
301
+ capture_output=True,
302
+ text=True,
303
+ timeout=300
304
+ )
305
+
306
+ if result.returncode == 0:
307
+ console.print(f"[green]适配器 {adapter_name} 通过 uv 安装成功[/green]")
308
+ else:
309
+ console.print(f"[red]适配器 {adapter_name} 通过 uv 安装失败[/red]")
310
+ except Exception as e:
311
+ console.print(f"[red]适配器 {adapter_name} 通过 uv 安装时出错: {e}[/]")
312
+
313
+ def _interactive_init(self, project_name: str = None, force: bool = False) -> bool:
314
+ """
315
+ 交互式初始化项目
316
+
317
+ :param project_name: 项目名称
318
+ :param force: 是否强制覆盖
319
+ :return: 是否初始化成功
320
+ """
321
+ try:
322
+ # 获取项目名称(如果未提供)
323
+ if not project_name:
324
+ project_name = Prompt.ask("[cyan]请输入项目名称 (默认: my_erispulse_project):[/cyan] ")
325
+ if not project_name:
326
+ project_name = "my_erispulse_project"
327
+
328
+ # 检查项目是否已存在
329
+ project_path = Path(project_name)
330
+ if project_path.exists() and not force:
331
+ if not Confirm.ask(f"[yellow]目录 {project_name} 已存在,是否覆盖?[/]", default=False):
332
+ console.print("[info]操作已取消[/]")
333
+ return False
334
+
335
+ # 创建项目
336
+ if not self._init_project(project_name, []):
337
+ return False
338
+
339
+ # 加载项目配置
340
+ from ErisPulse import config
341
+ project_config_path = project_path / "config.toml"
342
+
343
+ config.CONFIG_FILE = str(project_config_path)
344
+ config.reload()
345
+
346
+ # 交互式配置向导
347
+ console.print("\n[bold blue]现在进行基本配置:[/bold blue]")
348
+
349
+ # 获取日志级别配置
350
+ current_level = config.getConfig("ErisPulse.logger.level", "INFO")
351
+ console.print(f"\n当前日志级别: [cyan]{current_level}[/]")
352
+ new_level = Prompt.ask("[yellow]请输入新的日志级别 (DEBUG/INFO/WARNING/ERROR/CRITICAL),回车保持当前值:[/yellow] ")
353
+
354
+ if new_level and new_level.upper() in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
355
+ config.setConfig("ErisPulse.logger.level", new_level.upper())
356
+ console.print(f"[green]日志级别已更新为: {new_level.upper()}[/]")
357
+ elif new_level:
358
+ console.print(f"[red]无效的日志级别: {new_level}[/red]")
359
+
360
+ # 获取服务器配置
361
+ console.print("\n[bold]服务器配置[/bold]")
362
+ current_host = config.getConfig("ErisPulse.server.host", "0.0.0.0")
363
+ current_port = config.getConfig("ErisPulse.server.port", 8000)
364
+
365
+ console.print(f"当前主机: [cyan]{current_host}[/]")
366
+ new_host = Prompt.ask("[yellow]请输入主机地址,回车保持当前值:[/yellow] ")
367
+
368
+ if new_host:
369
+ config.setConfig("ErisPulse.server.host", new_host)
370
+ console.print(f"[green]主机地址已更新为: {new_host}[/]")
371
+
372
+ console.print(f"当前端口: [cyan]{current_port}[/]")
373
+ new_port = Prompt.ask("[yellow]请输入端口号,回车保持当前值:[/yellow] ")
374
+
375
+ if new_port:
376
+ try:
377
+ port_int = int(new_port)
378
+ config.setConfig("ErisPulse.server.port", port_int)
379
+ console.print(f"[green]端口已更新为: {port_int}[/]")
380
+ except ValueError:
381
+ console.print(f"[red]无效的端口号: {new_port}[/red]")
382
+
383
+ # 询问是否要配置适配器
384
+ if Confirm.ask("\n[cyan]是否要配置适配器?[/cyan]", default=True):
385
+ self._configure_adapters_interactive_sync(str(project_path))
386
+
387
+ # 保存配置
388
+ config.force_save()
389
+ console.print("\n[bold green]项目和配置初始化完成![/bold green]")
390
+
391
+ # 显示下一步操作
392
+ console.print("\n[cyan]接下来您可以:[/cyan]")
393
+ console.print(f"1. 编辑 {project_name}/config.toml 进一步配置")
394
+ console.print(f"2. 运行 [cyan]cd {project_name} \n ep run[/] 启动项目")
395
+
396
+ return True
397
+
398
+ except Exception as e:
399
+ console.print(f"[red]交互式初始化失败: {e}[/]")
400
+ return False
@@ -0,0 +1,82 @@
1
+ # type: ignore
2
+ #
3
+ # Auto-generated type stub for init.py
4
+ # DO NOT EDIT MANUALLY - Generated by generate-type-stubs.py
5
+ #
6
+
7
+ """
8
+ Init 命令实现
9
+
10
+ 交互式初始化 ErisPulse 项目
11
+ """
12
+
13
+ import asyncio
14
+ import concurrent.futures
15
+ import subprocess
16
+ import sys
17
+ from argparse import ArgumentParser
18
+ from pathlib import Path
19
+ from rich.prompt import Confirm, Prompt
20
+ from ...console import console
21
+ from ...package_manager import PackageManager
22
+ from ..base import Command
23
+
24
+ class InitCommand(Command):
25
+ """
26
+ 初始化命令
27
+ """
28
+ def __init__(self: None) -> ...:
29
+ """
30
+ 初始化命令
31
+ """
32
+ ...
33
+ def add_arguments(self: object, parser: ArgumentParser) -> ...:
34
+ """
35
+ 添加命令参数
36
+ """
37
+ ...
38
+ def execute(self: object, args: ...) -> ...:
39
+ """
40
+ 执行命令
41
+ """
42
+ ...
43
+ def _init_project(self: object, project_name: str, adapter_list: list = ...) -> bool:
44
+ """
45
+ 初始化新项目
46
+
47
+ :param project_name: 项目名称
48
+ :param adapter_list: 需要初始化的适配器列表
49
+ :return: 是否初始化成功
50
+ """
51
+ ...
52
+ async def _fetch_available_adapters(self: object) -> ...:
53
+ """
54
+ 从云端获取可用适配器列表
55
+
56
+ :return: 适配器名称到描述的映射
57
+ """
58
+ ...
59
+ def _configure_adapters_interactive_sync(self: object, project_path: str = ...) -> ...:
60
+ """
61
+ 交互式配置适配器的同步版本
62
+
63
+ :param project_path: 项目路径
64
+ """
65
+ ...
66
+ def _install_adapters(self: object, adapter_names: ..., adapters_info: ...) -> ...:
67
+ """
68
+ 安装选中的适配器
69
+
70
+ :param adapter_names: 适配器名称列表
71
+ :param adapters_info: 适配器信息字典
72
+ """
73
+ ...
74
+ def _interactive_init(self: object, project_name: str = ..., force: bool = ...) -> bool:
75
+ """
76
+ 交互式初始化项目
77
+
78
+ :param project_name: 项目名称
79
+ :param force: 是否强制覆盖
80
+ :return: 是否初始化成功
81
+ """
82
+ ...