deeptrade-quant 0.0.2__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 (83) hide show
  1. deeptrade/__init__.py +8 -0
  2. deeptrade/channels_builtin/__init__.py +0 -0
  3. deeptrade/channels_builtin/stdout/__init__.py +0 -0
  4. deeptrade/channels_builtin/stdout/deeptrade_plugin.yaml +25 -0
  5. deeptrade/channels_builtin/stdout/migrations/20260429_001_init.sql +13 -0
  6. deeptrade/channels_builtin/stdout/stdout_channel/__init__.py +0 -0
  7. deeptrade/channels_builtin/stdout/stdout_channel/channel.py +180 -0
  8. deeptrade/cli.py +214 -0
  9. deeptrade/cli_config.py +396 -0
  10. deeptrade/cli_data.py +33 -0
  11. deeptrade/cli_plugin.py +176 -0
  12. deeptrade/core/__init__.py +8 -0
  13. deeptrade/core/config.py +344 -0
  14. deeptrade/core/config_migrations.py +138 -0
  15. deeptrade/core/db.py +176 -0
  16. deeptrade/core/llm_client.py +591 -0
  17. deeptrade/core/llm_manager.py +174 -0
  18. deeptrade/core/logging_config.py +61 -0
  19. deeptrade/core/migrations/__init__.py +0 -0
  20. deeptrade/core/migrations/core/20260427_001_init.sql +121 -0
  21. deeptrade/core/migrations/core/20260501_002_drop_llm_calls_stage.sql +10 -0
  22. deeptrade/core/migrations/core/__init__.py +0 -0
  23. deeptrade/core/notifier.py +302 -0
  24. deeptrade/core/paths.py +49 -0
  25. deeptrade/core/plugin_manager.py +616 -0
  26. deeptrade/core/run_status.py +29 -0
  27. deeptrade/core/secrets.py +152 -0
  28. deeptrade/core/tushare_client.py +824 -0
  29. deeptrade/plugins_api/__init__.py +44 -0
  30. deeptrade/plugins_api/base.py +66 -0
  31. deeptrade/plugins_api/channel.py +42 -0
  32. deeptrade/plugins_api/events.py +61 -0
  33. deeptrade/plugins_api/llm.py +46 -0
  34. deeptrade/plugins_api/metadata.py +84 -0
  35. deeptrade/plugins_api/notify.py +67 -0
  36. deeptrade/strategies_builtin/__init__.py +0 -0
  37. deeptrade/strategies_builtin/limit_up_board/__init__.py +0 -0
  38. deeptrade/strategies_builtin/limit_up_board/deeptrade_plugin.yaml +101 -0
  39. deeptrade/strategies_builtin/limit_up_board/limit_up_board/__init__.py +0 -0
  40. deeptrade/strategies_builtin/limit_up_board/limit_up_board/calendar.py +65 -0
  41. deeptrade/strategies_builtin/limit_up_board/limit_up_board/cli.py +269 -0
  42. deeptrade/strategies_builtin/limit_up_board/limit_up_board/config.py +76 -0
  43. deeptrade/strategies_builtin/limit_up_board/limit_up_board/data.py +1191 -0
  44. deeptrade/strategies_builtin/limit_up_board/limit_up_board/pipeline.py +869 -0
  45. deeptrade/strategies_builtin/limit_up_board/limit_up_board/plugin.py +30 -0
  46. deeptrade/strategies_builtin/limit_up_board/limit_up_board/profiles.py +85 -0
  47. deeptrade/strategies_builtin/limit_up_board/limit_up_board/prompts.py +485 -0
  48. deeptrade/strategies_builtin/limit_up_board/limit_up_board/render.py +890 -0
  49. deeptrade/strategies_builtin/limit_up_board/limit_up_board/runner.py +1087 -0
  50. deeptrade/strategies_builtin/limit_up_board/limit_up_board/runtime.py +172 -0
  51. deeptrade/strategies_builtin/limit_up_board/limit_up_board/schemas.py +178 -0
  52. deeptrade/strategies_builtin/limit_up_board/migrations/20260430_001_init.sql +150 -0
  53. deeptrade/strategies_builtin/limit_up_board/migrations/20260501_002_lub_stage_results_llm_provider.sql +8 -0
  54. deeptrade/strategies_builtin/limit_up_board/migrations/20260508_001_lub_lhb_tables.sql +36 -0
  55. deeptrade/strategies_builtin/limit_up_board/migrations/20260508_002_lub_cyq_perf.sql +18 -0
  56. deeptrade/strategies_builtin/limit_up_board/migrations/20260508_003_lub_lhb_pk_fix.sql +46 -0
  57. deeptrade/strategies_builtin/limit_up_board/migrations/20260508_004_lub_lhb_drop_pk.sql +53 -0
  58. deeptrade/strategies_builtin/limit_up_board/migrations/20260508_005_lub_config.sql +17 -0
  59. deeptrade/strategies_builtin/volume_anomaly/__init__.py +0 -0
  60. deeptrade/strategies_builtin/volume_anomaly/deeptrade_plugin.yaml +59 -0
  61. deeptrade/strategies_builtin/volume_anomaly/migrations/20260430_001_init.sql +94 -0
  62. deeptrade/strategies_builtin/volume_anomaly/migrations/20260601_001_realized_returns.sql +44 -0
  63. deeptrade/strategies_builtin/volume_anomaly/migrations/20260601_002_dimension_scores.sql +13 -0
  64. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/__init__.py +0 -0
  65. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/calendar.py +52 -0
  66. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/cli.py +247 -0
  67. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/data.py +2154 -0
  68. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/pipeline.py +327 -0
  69. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/plugin.py +22 -0
  70. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/profiles.py +49 -0
  71. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/prompts.py +187 -0
  72. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/prompts_examples.py +84 -0
  73. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/render.py +906 -0
  74. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/runner.py +772 -0
  75. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/runtime.py +90 -0
  76. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/schemas.py +97 -0
  77. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/stats.py +174 -0
  78. deeptrade/theme.py +48 -0
  79. deeptrade_quant-0.0.2.dist-info/METADATA +166 -0
  80. deeptrade_quant-0.0.2.dist-info/RECORD +83 -0
  81. deeptrade_quant-0.0.2.dist-info/WHEEL +4 -0
  82. deeptrade_quant-0.0.2.dist-info/entry_points.txt +2 -0
  83. deeptrade_quant-0.0.2.dist-info/licenses/LICENSE +21 -0
deeptrade/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """DeepTrade — LLM-driven A-share stock screening CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from deeptrade.core.notifier import notification_session, notify
6
+
7
+ __version__ = "0.0.1"
8
+ __all__ = ["__version__", "notification_session", "notify"]
File without changes
File without changes
@@ -0,0 +1,25 @@
1
+ plugin_id: stdout-channel
2
+ name: Stdout Channel
3
+ version: 0.1.0
4
+ type: channel
5
+ api_version: "1"
6
+ entrypoint: stdout_channel.channel:StdoutChannel
7
+ description: Reference notification channel — fully consumes the payload but only prints "✔ push success" to stdout.
8
+ author: DeepTrade
9
+
10
+ permissions:
11
+ tushare_apis:
12
+ required: []
13
+ optional: []
14
+ llm: false
15
+ llm_tools: false
16
+
17
+ migrations:
18
+ - version: "20260429_001"
19
+ file: migrations/20260429_001_init.sql
20
+ checksum: "sha256:7317d23d4ac91b9a1259e2ba4d990863491de8fe1115e19052b02f83939399cc"
21
+
22
+ tables:
23
+ - name: stdout_channel_log
24
+ description: Audit log of payloads delivered through the stdout channel
25
+ purge_on_uninstall: true
@@ -0,0 +1,13 @@
1
+ -- stdout channel: delivery log so we can verify the payload was actually
2
+ -- consumed (not just acknowledged with a fake "✔ push success").
3
+ CREATE TABLE IF NOT EXISTS stdout_channel_log (
4
+ delivered_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
5
+ plugin_id VARCHAR NOT NULL,
6
+ run_id UUID NOT NULL,
7
+ status VARCHAR NOT NULL,
8
+ title VARCHAR NOT NULL,
9
+ section_count INTEGER NOT NULL,
10
+ item_count INTEGER NOT NULL,
11
+ metric_count INTEGER NOT NULL,
12
+ PRIMARY KEY (run_id, plugin_id, delivered_at)
13
+ );
@@ -0,0 +1,180 @@
1
+ """Stdout reference channel plugin.
2
+
3
+ Implements the framework's ``ChannelPlugin`` Protocol:
4
+ * ``validate_static`` — install-time self-check (no network)
5
+ * ``dispatch(argv)`` — CLI dispatch (subcommands: test, log)
6
+ * ``push(ctx, payload)`` — receive a NotificationPayload from the notifier
7
+
8
+ Behaviour on push:
9
+ * FULLY consume the NotificationPayload (walk every section/item/metric)
10
+ so any structural bug surfaces immediately.
11
+ * Persist a one-row audit record to ``stdout_channel_log``.
12
+ * Emit ONE concise line: "✔ push success (...)".
13
+
14
+ This channel is the zero-dependency target for unit tests of the notify
15
+ plumbing and is safe to ship enabled-by-default in dev installs.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import sys
22
+ from typing import TYPE_CHECKING
23
+
24
+ if TYPE_CHECKING: # pragma: no cover
25
+ from deeptrade.plugins_api.base import PluginContext
26
+ from deeptrade.plugins_api.notify import NotificationPayload
27
+
28
+
29
+ class StdoutChannel:
30
+ """Stdout-only IM channel plugin."""
31
+
32
+ metadata = None # injected by framework after install
33
+
34
+ # ----- Plugin Protocol -------------------------------------------------
35
+
36
+ def validate_static(self, ctx: PluginContext) -> None: # noqa: ARG002
37
+ # Sanity: the audit table must exist (created by our own migration).
38
+ # If it's missing, the install pipeline already failed; this is just a
39
+ # belt-and-braces self-check that doesn't touch the network.
40
+ return
41
+
42
+ def dispatch(self, argv: list[str]) -> int:
43
+ """Channel-side CLI: ``test`` and ``log``.
44
+
45
+ ``test`` synthesizes a minimal payload and routes it back through this
46
+ channel's own ``push`` so the user can verify the channel is wired up
47
+ without running a real strategy. ``log`` dumps the most recent
48
+ ``stdout_channel_log`` rows.
49
+ """
50
+ parser = argparse.ArgumentParser(
51
+ prog="deeptrade stdout-channel",
52
+ description="Stdout notification channel — local self-test + audit log.",
53
+ )
54
+ sub = parser.add_subparsers(dest="cmd", required=False)
55
+
56
+ sub.add_parser("test", help="Push a synthetic payload through this channel.")
57
+ log_p = sub.add_parser("log", help="Print recent delivery audit rows.")
58
+ log_p.add_argument("--limit", type=int, default=20)
59
+
60
+ args = parser.parse_args(argv)
61
+ if args.cmd is None:
62
+ parser.print_help()
63
+ return 0
64
+ if args.cmd == "test":
65
+ return self._cmd_test()
66
+ if args.cmd == "log":
67
+ return self._cmd_log(args.limit)
68
+ return 2
69
+
70
+ # ----- ChannelPlugin Protocol ------------------------------------------
71
+
72
+ def push(self, ctx: PluginContext, payload: NotificationPayload) -> None:
73
+ # 1) Walk the payload — counts come from real iteration, never from
74
+ # dict.len(), so we cannot "succeed" without having actually read the
75
+ # plugin's data.
76
+ section_count = len(payload.sections)
77
+ item_count = sum(len(s.items) for s in payload.sections)
78
+ metric_count = len(payload.metrics)
79
+ for section in payload.sections:
80
+ for item in section.items:
81
+ _ = (item.code, item.name, item.rank, item.score, item.label, item.note)
82
+ for k, v in payload.metrics.items():
83
+ _ = (k, v)
84
+ _ = payload.title, payload.summary, payload.report_dir, payload.extras
85
+
86
+ # 2) Persist a delivery audit row.
87
+ ctx.db.execute(
88
+ "INSERT INTO stdout_channel_log(plugin_id, run_id, status, title, "
89
+ "section_count, item_count, metric_count) VALUES (?, ?, ?, ?, ?, ?, ?)",
90
+ (
91
+ payload.plugin_id,
92
+ payload.run_id,
93
+ payload.status.value,
94
+ payload.title,
95
+ section_count,
96
+ item_count,
97
+ metric_count,
98
+ ),
99
+ )
100
+
101
+ # 3) One concise line. Use sys.stdout (not rich console) so this stays
102
+ # out of any TUI / strategy console that the caller may have running.
103
+ sys.stdout.write(
104
+ f"✔ push success (channel=stdout-channel run_id={payload.run_id} "
105
+ f"status={payload.status.value})\n"
106
+ )
107
+ sys.stdout.flush()
108
+
109
+ # ----- private helpers -------------------------------------------------
110
+
111
+ def _cmd_test(self) -> int:
112
+ """Round-trip a synthetic payload through ``deeptrade.notify``.
113
+
114
+ Goes through the framework's notifier layer (not directly through
115
+ ``self.push``) so the test exercises the full path: install →
116
+ build_notifier → dispatch → push.
117
+ """
118
+ import uuid
119
+
120
+ from deeptrade import notify
121
+ from deeptrade.core import paths
122
+ from deeptrade.core.db import Database
123
+ from deeptrade.core.run_status import RunStatus
124
+ from deeptrade.plugins_api.notify import (
125
+ NotificationItem,
126
+ NotificationPayload,
127
+ NotificationSection,
128
+ )
129
+
130
+ payload = NotificationPayload(
131
+ plugin_id="stdout-channel",
132
+ run_id=str(uuid.uuid4()),
133
+ status=RunStatus.SUCCESS,
134
+ title="DeepTrade — stdout channel test",
135
+ summary="Synthetic payload from `deeptrade stdout-channel test`.",
136
+ sections=[
137
+ NotificationSection(
138
+ key="demo",
139
+ title="Demo items",
140
+ items=[
141
+ NotificationItem(
142
+ code="600519.SH", name="贵州茅台", rank=1, score=87.5,
143
+ label="top_candidate", note="演示条目,非真实推荐",
144
+ ),
145
+ ],
146
+ ),
147
+ ],
148
+ metrics={"selected": 1},
149
+ )
150
+ db = Database(paths.db_path())
151
+ try:
152
+ notify(db, payload)
153
+ finally:
154
+ db.close()
155
+ return 0
156
+
157
+ def _cmd_log(self, limit: int) -> int:
158
+ from deeptrade.core import paths
159
+ from deeptrade.core.db import Database
160
+
161
+ db = Database(paths.db_path())
162
+ try:
163
+ rows = db.fetchall(
164
+ "SELECT delivered_at, plugin_id, run_id, status, title, "
165
+ "section_count, item_count, metric_count "
166
+ "FROM stdout_channel_log ORDER BY delivered_at DESC LIMIT ?",
167
+ (limit,),
168
+ )
169
+ finally:
170
+ db.close()
171
+
172
+ if not rows:
173
+ sys.stdout.write("(no deliveries yet)\n")
174
+ return 0
175
+ for r in rows:
176
+ sys.stdout.write(
177
+ f"{r[0]} {r[1]:<20} {r[3]:<8} sec={r[5]} item={r[6]} "
178
+ f"metric={r[7]} {r[4]}\n"
179
+ )
180
+ return 0
deeptrade/cli.py ADDED
@@ -0,0 +1,214 @@
1
+ """DeepTrade CLI entry point.
2
+
3
+ Framework command surface (closed):
4
+
5
+ deeptrade --version | -V
6
+ deeptrade --help | -h
7
+ deeptrade init [--no-prompts]
8
+ deeptrade config {show, set, set-tushare, set-llm, list-llm, test-llm}
9
+ deeptrade plugin {install, list, info, enable, disable, uninstall, upgrade}
10
+ deeptrade data sync ... (stub — pending refactor)
11
+
12
+ Plugin commands are dispatched through pure pass-through:
13
+
14
+ deeptrade <plugin_id> <argv...> → plugin.dispatch(argv)
15
+
16
+ The framework knows nothing about a plugin's subcommand tree. ``--help`` for a
17
+ plugin is the plugin's own responsibility.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import sys
23
+
24
+ import click
25
+ import typer
26
+ from typer.core import TyperGroup
27
+
28
+ from deeptrade import __version__
29
+ from deeptrade.cli_config import app as config_app
30
+ from deeptrade.cli_data import app as data_app
31
+ from deeptrade.cli_plugin import app as plugin_app
32
+ from deeptrade.core import paths
33
+ from deeptrade.core.db import Database, apply_core_migrations
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Custom click.Group implementing pure plugin pass-through
37
+ # ---------------------------------------------------------------------------
38
+
39
+
40
+ class _DeepTradeGroup(TyperGroup):
41
+ """Top-level group that falls back to plugin dispatch for unknown commands.
42
+
43
+ Resolution order on ``deeptrade <token> ...``:
44
+
45
+ 1. If ``<token>`` is a registered framework command → click handles it.
46
+ 2. Otherwise look it up in the ``plugins`` table:
47
+ - found + enabled → load entrypoint, call ``plugin.dispatch(argv)``
48
+ - found + disabled → exit 2 with "enable first" hint
49
+ - not found → exit 2 with "unknown" + framework cmd list
50
+ """
51
+
52
+ # Disable click's "no such command" so we can route to plugins instead.
53
+ def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None:
54
+ registered = super().get_command(ctx, cmd_name)
55
+ if registered is not None:
56
+ return registered
57
+ return _build_plugin_command(cmd_name)
58
+
59
+ def resolve_command(
60
+ self, ctx: click.Context, args: list[str]
61
+ ) -> tuple[str | None, click.Command | None, list[str]]:
62
+ # Standard resolution: returns (cmd_name, cmd, remaining_args).
63
+ cmd_name = args[0] if args else None
64
+ cmd = self.get_command(ctx, cmd_name) if cmd_name else None
65
+ if cmd is None and cmd_name is not None:
66
+ # Synthesize a helpful error including framework cmd list.
67
+ framework = sorted(super().list_commands(ctx))
68
+ ctx.fail(
69
+ f"unknown command or plugin: {cmd_name!r}\n"
70
+ f" framework commands: {framework}\n"
71
+ f" use `deeptrade plugin list` to see installed plugins"
72
+ )
73
+ return super().resolve_command(ctx, args)
74
+
75
+
76
+ def _build_plugin_command(plugin_id: str) -> click.Command | None:
77
+ """Resolve ``plugin_id`` against the installed plugins; return a click
78
+ Command that dispatches to ``plugin.dispatch(remaining_argv)``."""
79
+ from pathlib import Path
80
+
81
+ from deeptrade.core.plugin_manager import (
82
+ PluginManager,
83
+ PluginNotFoundError,
84
+ _load_entrypoint,
85
+ )
86
+
87
+ db = Database(paths.db_path())
88
+ try:
89
+ mgr = PluginManager(db)
90
+ try:
91
+ rec = mgr.info(plugin_id)
92
+ except PluginNotFoundError:
93
+ return None
94
+ finally:
95
+ db.close()
96
+
97
+ if not rec.enabled:
98
+
99
+ @click.command(
100
+ name=plugin_id,
101
+ help=f"(disabled) {rec.name}",
102
+ context_settings={"ignore_unknown_options": True, "allow_extra_args": True},
103
+ )
104
+ def _disabled() -> None:
105
+ typer.echo(
106
+ f"✘ plugin {plugin_id!r} is disabled; "
107
+ f"run `deeptrade plugin enable {plugin_id}`"
108
+ )
109
+ raise typer.Exit(2)
110
+
111
+ return _disabled
112
+
113
+ # Enabled: hand the remaining argv straight to the plugin.
114
+ @click.command(
115
+ name=plugin_id,
116
+ # Plugin owns its own --help; let everything through unparsed.
117
+ help=f"{rec.name} (v{rec.version}) — plugin-managed CLI; try `--help`",
118
+ context_settings={
119
+ "ignore_unknown_options": True,
120
+ "allow_extra_args": True,
121
+ "help_option_names": [], # don't intercept --help
122
+ },
123
+ )
124
+ @click.pass_context
125
+ def _dispatch(ctx: click.Context) -> None:
126
+ plugin = _load_entrypoint(Path(rec.install_path), rec.entrypoint, rec.metadata)
127
+ if not hasattr(plugin, "dispatch"):
128
+ typer.echo(f"✘ plugin {plugin_id!r} does not implement dispatch()")
129
+ raise typer.Exit(2)
130
+ rc = plugin.dispatch(list(ctx.args))
131
+ raise typer.Exit(rc or 0)
132
+
133
+ return _dispatch
134
+
135
+
136
+ # ---------------------------------------------------------------------------
137
+ # Typer application (framework commands only)
138
+ # ---------------------------------------------------------------------------
139
+
140
+
141
+ app = typer.Typer(
142
+ name="deeptrade",
143
+ help="DeepTrade — LLM-driven A-share stock screening CLI",
144
+ no_args_is_help=True,
145
+ add_completion=True,
146
+ cls=_DeepTradeGroup,
147
+ )
148
+ app.add_typer(config_app, name="config")
149
+ app.add_typer(plugin_app, name="plugin")
150
+ app.add_typer(data_app, name="data")
151
+
152
+
153
+ def _version_callback(value: bool) -> None:
154
+ if value:
155
+ typer.echo(f"DeepTrade {__version__}")
156
+ raise typer.Exit()
157
+
158
+
159
+ @app.callback()
160
+ def main(
161
+ version: bool = typer.Option( # noqa: ARG001 — consumed by Typer via callback
162
+ False,
163
+ "--version",
164
+ "-V",
165
+ callback=_version_callback,
166
+ is_eager=True,
167
+ help="Show version and exit.",
168
+ ),
169
+ ) -> None:
170
+ """DeepTrade — LLM-driven A-share stock screening CLI."""
171
+
172
+
173
+ @app.command()
174
+ def init(
175
+ no_prompts: bool = typer.Option(
176
+ False,
177
+ "--no-prompts",
178
+ help="Skip post-init tushare/deepseek configuration prompts.",
179
+ ),
180
+ ) -> None:
181
+ """Initialize ~/.deeptrade layout and apply core schema migrations (idempotent)."""
182
+ paths.ensure_layout()
183
+ db_file = paths.db_path()
184
+ fresh = not db_file.exists()
185
+ db = Database(db_file)
186
+ try:
187
+ applied = apply_core_migrations(db)
188
+ if fresh:
189
+ typer.echo(f"✔ Database created: {db_file}")
190
+ if applied:
191
+ for v in applied:
192
+ typer.echo(f"✔ Schema applied: {v}")
193
+ else:
194
+ typer.echo("✔ Database already initialized; schema up-to-date")
195
+ finally:
196
+ db.close()
197
+
198
+ if no_prompts or not sys.stdin.isatty():
199
+ return
200
+
201
+ import questionary
202
+
203
+ if questionary.confirm("Configure tushare now?", default=True).ask():
204
+ from deeptrade.cli_config import cmd_set_tushare
205
+
206
+ cmd_set_tushare()
207
+ if questionary.confirm("Configure an LLM provider now?", default=True).ask():
208
+ from deeptrade.cli_config import cmd_set_llm
209
+
210
+ cmd_set_llm()
211
+
212
+
213
+ if __name__ == "__main__":
214
+ app()