openbb-cli 1.0.0__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 (42) hide show
  1. openbb_cli/__init__.py +1 -0
  2. openbb_cli/argparse_translator/__init__.py +0 -0
  3. openbb_cli/argparse_translator/argparse_class_processor.py +148 -0
  4. openbb_cli/argparse_translator/argparse_translator.py +627 -0
  5. openbb_cli/argparse_translator/obbject_registry.py +94 -0
  6. openbb_cli/assets/routines/routine_example.openbb +21 -0
  7. openbb_cli/assets/styles/default/Consolas.ttf +0 -0
  8. openbb_cli/assets/styles/default/dark.mpfstyle.json +47 -0
  9. openbb_cli/assets/styles/default/dark.mplrc.json +7 -0
  10. openbb_cli/assets/styles/default/dark.mplstyle +96 -0
  11. openbb_cli/assets/styles/default/dark.pltstyle.json +132 -0
  12. openbb_cli/assets/styles/default/dark.richstyle.json +9 -0
  13. openbb_cli/assets/styles/default/light.mpfstyle.json +47 -0
  14. openbb_cli/assets/styles/default/light.mplrc.json +7 -0
  15. openbb_cli/assets/styles/default/light.mplstyle +95 -0
  16. openbb_cli/assets/styles/default/light.pltstyle.json +871 -0
  17. openbb_cli/assets/styles/default/light.richstyle.json +9 -0
  18. openbb_cli/assets/styles/default/tables.pltstyle.json +102 -0
  19. openbb_cli/assets/styles/user/openbb.richstyle.json +9 -0
  20. openbb_cli/cli.py +20 -0
  21. openbb_cli/config/__init__.py +1 -0
  22. openbb_cli/config/completer.py +427 -0
  23. openbb_cli/config/console.py +93 -0
  24. openbb_cli/config/constants.py +80 -0
  25. openbb_cli/config/menu_text.py +165 -0
  26. openbb_cli/config/setup.py +11 -0
  27. openbb_cli/config/style.py +108 -0
  28. openbb_cli/controllers/base_controller.py +941 -0
  29. openbb_cli/controllers/base_platform_controller.py +349 -0
  30. openbb_cli/controllers/choices.py +324 -0
  31. openbb_cli/controllers/cli_controller.py +946 -0
  32. openbb_cli/controllers/hub_service.py +107 -0
  33. openbb_cli/controllers/platform_controller_factory.py +58 -0
  34. openbb_cli/controllers/script_parser.py +488 -0
  35. openbb_cli/controllers/settings_controller.py +373 -0
  36. openbb_cli/controllers/utils.py +967 -0
  37. openbb_cli/models/settings.py +73 -0
  38. openbb_cli/session.py +92 -0
  39. openbb_cli-1.0.0.dist-info/METADATA +91 -0
  40. openbb_cli-1.0.0.dist-info/RECORD +42 -0
  41. openbb_cli-1.0.0.dist-info/WHEEL +4 -0
  42. openbb_cli-1.0.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,946 @@
1
+ #!/usr/bin/env python
2
+ """Main CLI Module."""
3
+
4
+ import argparse
5
+ import contextlib
6
+ import difflib
7
+ import logging
8
+ import os
9
+ import re
10
+ import sys
11
+ import time
12
+ import webbrowser
13
+ from datetime import datetime
14
+ from functools import partial, update_wrapper
15
+ from pathlib import Path
16
+ from types import MethodType
17
+ from typing import Any, Dict, List, Optional
18
+
19
+ import pandas as pd
20
+ import requests
21
+ from openbb import obb
22
+ from openbb_cli.config import constants
23
+ from openbb_cli.config.constants import (
24
+ ASSETS_DIRECTORY,
25
+ ENV_FILE_SETTINGS,
26
+ HOME_DIRECTORY,
27
+ REPOSITORY_DIRECTORY,
28
+ )
29
+ from openbb_cli.config.menu_text import MenuText
30
+ from openbb_cli.controllers.base_controller import BaseController
31
+ from openbb_cli.controllers.platform_controller_factory import (
32
+ PlatformControllerFactory,
33
+ )
34
+ from openbb_cli.controllers.script_parser import is_reset, parse_openbb_script
35
+ from openbb_cli.controllers.utils import (
36
+ bootup,
37
+ first_time_user,
38
+ get_flair_and_username,
39
+ parse_and_split_input,
40
+ print_goodbye,
41
+ print_rich_table,
42
+ reset,
43
+ suppress_stdout,
44
+ welcome_message,
45
+ )
46
+ from openbb_cli.session import Session
47
+ from prompt_toolkit.formatted_text import HTML
48
+ from prompt_toolkit.styles import Style
49
+ from pydantic import BaseModel
50
+
51
+ PLATFORM_ROUTERS = {
52
+ d: "menu" if not isinstance(getattr(obb, d), BaseModel) else "command"
53
+ for d in dir(obb)
54
+ if "_" not in d
55
+ }
56
+ NON_DATA_ROUTERS = ["coverage", "account", "reference", "system", "user"]
57
+ DATA_PROCESSING_ROUTERS = ["technical", "quantitative", "econometrics"]
58
+
59
+ # pylint: disable=too-many-public-methods,import-outside-toplevel, too-many-function-args
60
+ # pylint: disable=too-many-branches,no-member,C0302,too-many-return-statements, inconsistent-return-statements
61
+
62
+ logger = logging.getLogger(__name__)
63
+
64
+ env_file = str(ENV_FILE_SETTINGS)
65
+ session = Session()
66
+
67
+
68
+ class CLIController(BaseController):
69
+ """CLI Controller class."""
70
+
71
+ CHOICES_COMMANDS = ["record", "stop", "exe", "results"]
72
+ CHOICES_MENUS = [
73
+ "settings",
74
+ ]
75
+
76
+ for router, value in PLATFORM_ROUTERS.items():
77
+ if value == "menu":
78
+ CHOICES_MENUS.append(router)
79
+ else:
80
+ CHOICES_COMMANDS.append(router)
81
+
82
+ PATH = "/"
83
+ CHOICES_GENERATION = False
84
+
85
+ def __init__(self, jobs_cmds: Optional[List[str]] = None):
86
+ """Construct CLI controller."""
87
+ self.ROUTINE_FILES: Dict[str, str] = dict()
88
+ self.ROUTINE_DEFAULT_FILES: Dict[str, str] = dict()
89
+ self.ROUTINE_PERSONAL_FILES: Dict[str, str] = dict()
90
+ self.ROUTINE_CHOICES: Dict[str, Any] = dict()
91
+
92
+ super().__init__(jobs_cmds)
93
+
94
+ self.queue: List[str] = list()
95
+
96
+ if jobs_cmds:
97
+ self.queue = parse_and_split_input(
98
+ an_input=" ".join(jobs_cmds), custom_filters=[]
99
+ )
100
+
101
+ self.update_success = False
102
+
103
+ self._generate_platform_commands()
104
+
105
+ self.update_runtime_choices()
106
+
107
+ def _generate_platform_commands(self):
108
+ """Generate Platform based commands/menus."""
109
+
110
+ def method_call_class(self, _, controller, name, parent_path, target):
111
+ self.queue = self.load_class(
112
+ controller, name, parent_path, target, self.queue
113
+ )
114
+
115
+ # pylint: disable=unused-argument
116
+ def method_call_command(self, _, router: str):
117
+ """Call command."""
118
+ mdl = getattr(obb, router)
119
+ df = pd.DataFrame.from_dict(mdl.model_dump(), orient="index")
120
+ if isinstance(df.columns, pd.RangeIndex):
121
+ df.columns = [str(i) for i in df.columns]
122
+ return print_rich_table(df, show_index=True)
123
+
124
+ for router, value in PLATFORM_ROUTERS.items():
125
+ target = getattr(obb, router)
126
+
127
+ if value == "menu":
128
+ pcf = PlatformControllerFactory(
129
+ target, reference=obb.reference["paths"] # type: ignore
130
+ )
131
+ DynamicController = pcf.create()
132
+
133
+ # Bind the method to the class
134
+ bound_method = MethodType(method_call_class, self)
135
+
136
+ # Update the wrapper and set the attribute
137
+ bound_method = update_wrapper( # type: ignore
138
+ partial(
139
+ bound_method,
140
+ controller=DynamicController,
141
+ name=router,
142
+ target=target,
143
+ parent_path=self.path,
144
+ ),
145
+ method_call_class,
146
+ )
147
+ else:
148
+ bound_method = MethodType(method_call_command, self)
149
+ bound_method = update_wrapper( # type: ignore
150
+ partial(bound_method, router=router),
151
+ method_call_command,
152
+ )
153
+
154
+ setattr(self, f"call_{router}", bound_method)
155
+
156
+ def update_runtime_choices(self):
157
+ """Update runtime choices."""
158
+ routines_directory = Path(session.user.preferences.export_directory, "routines")
159
+
160
+ if session.prompt_session and session.settings.USE_PROMPT_TOOLKIT:
161
+ # choices: dict = self.choices_default
162
+ choices: dict = {c: {} for c in self.controller_choices} # type: ignore
163
+
164
+ self.ROUTINE_FILES = {
165
+ filepath.name: filepath # type: ignore
166
+ for filepath in routines_directory.rglob("*.openbb")
167
+ }
168
+ self.ROUTINE_DEFAULT_FILES = {
169
+ filepath.name: filepath # type: ignore
170
+ for filepath in Path(routines_directory / "hub" / "default").rglob(
171
+ "*.openbb"
172
+ )
173
+ }
174
+ self.ROUTINE_PERSONAL_FILES = {
175
+ filepath.name: filepath # type: ignore
176
+ for filepath in Path(routines_directory / "hub" / "personal").rglob(
177
+ "*.openbb"
178
+ )
179
+ }
180
+
181
+ choices["exe"] = {
182
+ "--file": {
183
+ filename: {} for filename in list(self.ROUTINE_FILES.keys())
184
+ },
185
+ "-f": "--file",
186
+ "--example": None,
187
+ "-e": "--example",
188
+ "--input": None,
189
+ "-i": "--input",
190
+ "--url": None,
191
+ "--help": None,
192
+ "-h": "--help",
193
+ }
194
+ choices["record"] = {
195
+ "--name": None,
196
+ "-n": "--name",
197
+ "--description": None,
198
+ "-d": "--description",
199
+ "--public": None,
200
+ "-p": "--public",
201
+ "--tag1": {c: None for c in constants.SCRIPT_TAGS},
202
+ "--tag2": {c: None for c in constants.SCRIPT_TAGS},
203
+ "--tag3": {c: None for c in constants.SCRIPT_TAGS},
204
+ "--help": None,
205
+ "-h": "--help",
206
+ }
207
+ choices["stop"] = {"--help": None, "-h": "--help"}
208
+ choices["results"] = {"--help": None, "-h": "--help"}
209
+
210
+ self.update_completer(choices)
211
+
212
+ def print_help(self):
213
+ """Print help."""
214
+ mt = MenuText("")
215
+
216
+ mt.add_info("Configure the platform and manage your account")
217
+ for router, value in PLATFORM_ROUTERS.items():
218
+ if router not in NON_DATA_ROUTERS or router in ["reference", "coverage"]:
219
+ continue
220
+ if value == "menu":
221
+ menu_description = (
222
+ obb.reference["routers"] # type: ignore
223
+ .get(f"{self.PATH}{router}", {})
224
+ .get("description")
225
+ ) or ""
226
+ mt.add_menu(
227
+ name=router,
228
+ description=menu_description.split(".")[0].lower(),
229
+ )
230
+ else:
231
+ mt.add_cmd(router)
232
+
233
+ mt.add_info("\nConfigure your CLI")
234
+ mt.add_menu(
235
+ "settings",
236
+ description="enable and disable feature flags, preferences and settings",
237
+ )
238
+ mt.add_raw("\n")
239
+ mt.add_info("Record and execute your own .openbb routine scripts")
240
+ mt.add_cmd("record", description="start recording current session")
241
+ mt.add_cmd(
242
+ "stop", description="stop session recording and convert to .openbb routine"
243
+ )
244
+ mt.add_cmd(
245
+ "exe",
246
+ description="execute .openbb routine scripts (use exe --example for an example)",
247
+ )
248
+ mt.add_raw("\n")
249
+ mt.add_info("Retrieve data from different asset classes and providers")
250
+
251
+ for router, value in PLATFORM_ROUTERS.items():
252
+ if router in NON_DATA_ROUTERS or router in DATA_PROCESSING_ROUTERS:
253
+ continue
254
+ if value == "menu":
255
+ menu_description = (
256
+ obb.reference["routers"] # type: ignore
257
+ .get(f"{self.PATH}{router}", {})
258
+ .get("description")
259
+ ) or ""
260
+ mt.add_menu(
261
+ name=router,
262
+ description=menu_description.split(".")[0].lower(),
263
+ )
264
+ else:
265
+ mt.add_cmd(router)
266
+
267
+ if any(router in PLATFORM_ROUTERS for router in DATA_PROCESSING_ROUTERS):
268
+ mt.add_info("\nAnalyze and process previously obtained data")
269
+
270
+ for router, value in PLATFORM_ROUTERS.items():
271
+ if router not in DATA_PROCESSING_ROUTERS:
272
+ continue
273
+ if value == "menu":
274
+ menu_description = (
275
+ obb.reference["routers"] # type: ignore
276
+ .get(f"{self.PATH}{router}", {})
277
+ .get("description")
278
+ ) or ""
279
+ mt.add_menu(
280
+ name=router,
281
+ description=menu_description.split(".")[0].lower(),
282
+ )
283
+ else:
284
+ mt.add_cmd(router)
285
+
286
+ mt.add_raw("\n")
287
+ mt.add_cmd("results")
288
+ if session.obbject_registry.obbjects:
289
+ mt.add_info("\nCached Results")
290
+ for key, value in list(session.obbject_registry.all.items())[ # type: ignore
291
+ : session.settings.N_TO_DISPLAY_OBBJECT_REGISTRY
292
+ ]:
293
+ mt.add_raw(
294
+ f"[yellow]OBB{key}[/yellow]: {value['command']}",
295
+ left_spacing=True,
296
+ )
297
+
298
+ session.console.print(text=mt.menu_text, menu="Home")
299
+ self.update_runtime_choices()
300
+
301
+ def parse_input(self, an_input: str) -> List:
302
+ """Overwrite the BaseController parse_input for `askobb` and 'exe'.
303
+
304
+ This will allow us to search for something like "P/E" ratio.
305
+ """
306
+ # Filtering out sorting parameters with forward slashes like P/E
307
+ sort_filter = r"((\ -q |\ --question|\ ).*?(/))"
308
+ # Filter out urls
309
+ url = r"(exe (--url )?(https?://)?my\.openbb\.(dev|co)/u/.*/routine/.*)"
310
+ custom_filters = [sort_filter, url]
311
+ return parse_and_split_input(an_input=an_input, custom_filters=custom_filters)
312
+
313
+ def call_settings(self, _):
314
+ """Process settings command."""
315
+ from openbb_cli.controllers.settings_controller import (
316
+ SettingsController,
317
+ )
318
+
319
+ self.queue = self.load_class(SettingsController, self.queue)
320
+
321
+ def call_exe(self, other_args: List[str]):
322
+ """Process exe command."""
323
+ # Merge rest of string path to other_args and remove queue since it is a dir
324
+ other_args += self.queue
325
+
326
+ if not other_args:
327
+ session.console.print(
328
+ "[info]Provide a path to the routine you wish to execute. For an example, please use "
329
+ "`exe --example`.\n[/info]"
330
+ )
331
+ return
332
+ parser = argparse.ArgumentParser(
333
+ add_help=False,
334
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
335
+ prog="exe",
336
+ description="Execute automated routine script. For an example, please use "
337
+ "`exe --example`.",
338
+ )
339
+ parser.add_argument(
340
+ "--file",
341
+ "-f",
342
+ help="The path or .openbb file to run.",
343
+ dest="file",
344
+ required="-h" not in other_args
345
+ and "--help" not in other_args
346
+ and "-e" not in other_args
347
+ and "--example" not in other_args
348
+ and "--url" not in other_args
349
+ and "my.openbb" not in other_args[0],
350
+ type=str,
351
+ nargs="+",
352
+ )
353
+ parser.add_argument(
354
+ "-i",
355
+ "--input",
356
+ help="Select multiple inputs to be replaced in the routine and separated by commas. E.g. GME,AMC,BTC-USD",
357
+ dest="routine_args",
358
+ type=lambda s: [str(item) for item in s.split(",")],
359
+ )
360
+ parser.add_argument(
361
+ "-e",
362
+ "--example",
363
+ help="Run an example script to understand how routines can be used.",
364
+ dest="example",
365
+ action="store_true",
366
+ default=False,
367
+ )
368
+ parser.add_argument(
369
+ "--url", help="URL to run openbb script from.", dest="url", type=str
370
+ )
371
+ if other_args and "-" not in other_args[0][0]:
372
+ if other_args[0].startswith("my.") or other_args[0].startswith("http"):
373
+ other_args.insert(0, "--url")
374
+ else:
375
+ other_args.insert(0, "--file")
376
+ ns_parser = self.parse_known_args_and_warn(parser, other_args)
377
+ if ns_parser:
378
+ if ns_parser.example:
379
+ routine_path = ASSETS_DIRECTORY / "routines" / "routine_example.openbb"
380
+ session.console.print( # TODO: Point to docs when ready
381
+ "[info]Executing an example, please visit our docs "
382
+ "to learn how to create your own script.[/info]\n"
383
+ )
384
+ time.sleep(3)
385
+ elif ns_parser.url:
386
+ if not ns_parser.url.startswith(
387
+ "https"
388
+ ) and not ns_parser.url.startswith("http:"):
389
+ url = "https://" + ns_parser.url
390
+ elif ns_parser.url.startswith("http://"):
391
+ url = ns_parser.url.replace("http://", "https://")
392
+ else:
393
+ url = ns_parser.url
394
+ username = url.split("/")[-3]
395
+ script_name = url.split("/")[-1]
396
+ file_name = f"{username}_{script_name}.openbb"
397
+ final_url = f"{url}?raw=true"
398
+ response = requests.get(final_url, timeout=10)
399
+ if response.status_code != 200:
400
+ session.console.print(
401
+ "[red]Could not find the requested script.[/red]"
402
+ )
403
+ return
404
+ routine_text = response.json()["script"]
405
+ file_path = Path(session.user.preferences.export_directory, "routines")
406
+ routine_path = file_path / file_name
407
+ with open(routine_path, "w") as file:
408
+ file.write(routine_text)
409
+ self.update_runtime_choices()
410
+
411
+ elif ns_parser.file:
412
+ file_path = " ".join(ns_parser.file) # type: ignore
413
+ # if string is not in this format "default/file.openbb" then check for files in ROUTINE_FILES
414
+ full_path = file_path
415
+ hub_routine = file_path.split("/") # type: ignore
416
+ # Change with: my.openbb.co
417
+ if hub_routine[0] == "default":
418
+ routine_path = Path(
419
+ self.ROUTINE_DEFAULT_FILES.get(hub_routine[1], full_path)
420
+ )
421
+ elif hub_routine[0] == "personal":
422
+ routine_path = Path(
423
+ self.ROUTINE_PERSONAL_FILES.get(hub_routine[1], full_path)
424
+ )
425
+ else:
426
+ routine_path = Path(self.ROUTINE_FILES.get(file_path, full_path)) # type: ignore
427
+ else:
428
+ return
429
+
430
+ try:
431
+ with open(routine_path) as fp:
432
+ raw_lines = list(fp)
433
+
434
+ # Capture ARGV either as list if args separated by commas or as single value
435
+ if ns_parser.routine_args:
436
+ script_inputs = (
437
+ ns_parser.routine_args
438
+ if "," not in ns_parser.routine_args
439
+ else ns_parser.routine_args.split(",")
440
+ )
441
+
442
+ err, parsed_script = parse_openbb_script(
443
+ raw_lines=raw_lines,
444
+ script_inputs=script_inputs if ns_parser.routine_args else None,
445
+ )
446
+
447
+ # If there err output is not an empty string then it means there was an
448
+ # issue in parsing the routine and therefore we don't want to feed it
449
+ # to the terminal
450
+ if err:
451
+ session.console.print(err)
452
+ return
453
+
454
+ self.queue = [
455
+ val
456
+ for val in parse_and_split_input(
457
+ an_input=parsed_script, custom_filters=[]
458
+ )
459
+ if val
460
+ ]
461
+
462
+ if "export" in self.queue[0]:
463
+ export_path = self.queue[0].split(" ")[1]
464
+ # If the path selected does not start from the user root, give relative location from root
465
+ if export_path[0] == "~":
466
+ export_path = export_path.replace(
467
+ "~", HOME_DIRECTORY.as_posix()
468
+ )
469
+ elif export_path[0] != "/":
470
+ export_path = os.path.join(
471
+ os.path.dirname(os.path.abspath(__file__)), export_path
472
+ )
473
+
474
+ # Check if the directory exists
475
+ if os.path.isdir(export_path):
476
+ session.console.print(
477
+ f"Export data to be saved in the selected folder: '{export_path}'"
478
+ )
479
+ else:
480
+ os.makedirs(export_path)
481
+ session.console.print(
482
+ f"[green]Folder '{export_path}' successfully created.[/green]"
483
+ )
484
+ self.queue = self.queue[1:]
485
+
486
+ except FileNotFoundError:
487
+ session.console.print(
488
+ f"[red]File '{routine_path}' doesn't exist.[/red]"
489
+ )
490
+ return
491
+
492
+
493
+ def handle_job_cmds(jobs_cmds: Optional[List[str]]) -> Optional[List[str]]:
494
+ """Handle job commands."""
495
+ # If the path selected does not start from the user root,
496
+ # give relative location from root
497
+ if jobs_cmds is not None and jobs_cmds:
498
+ logger.info("INPUT: %s", "/".join(jobs_cmds))
499
+
500
+ export_path = ""
501
+ if jobs_cmds and "export" in jobs_cmds[0]:
502
+ commands = jobs_cmds[0].split("/")
503
+ first_split = commands[0].split(" ")
504
+ if len(first_split) > 1:
505
+ export_path = first_split[1]
506
+ jobs_cmds = ["/".join(commands[1:])]
507
+ if not export_path:
508
+ return jobs_cmds
509
+ if export_path[0] == "~":
510
+ export_path = export_path.replace("~", HOME_DIRECTORY.as_posix())
511
+ elif export_path[0] != "/":
512
+ export_path = os.path.join(
513
+ os.path.dirname(os.path.abspath(__file__)), export_path
514
+ )
515
+
516
+ # Check if the directory exists
517
+ if os.path.isdir(export_path):
518
+ session.console.print(
519
+ f"Export data to be saved in the selected folder: '{export_path}'"
520
+ )
521
+ else:
522
+ os.makedirs(export_path)
523
+ session.console.print(
524
+ f"[green]Folder '{export_path}' successfully created.[/green]"
525
+ )
526
+ return jobs_cmds
527
+
528
+
529
+ # pylint: disable=unused-argument
530
+ def run_cli(jobs_cmds: Optional[List[str]] = None, test_mode=False):
531
+ """Run the CLI menu."""
532
+ ret_code = 1
533
+ t_controller = CLIController(jobs_cmds)
534
+ an_input = ""
535
+
536
+ jobs_cmds = handle_job_cmds(jobs_cmds)
537
+
538
+ bootup()
539
+ if not jobs_cmds:
540
+ welcome_message()
541
+
542
+ if first_time_user():
543
+ with contextlib.suppress(EOFError):
544
+ webbrowser.open("https://docs.openbb.co/cli")
545
+
546
+ t_controller.print_help()
547
+
548
+ while ret_code:
549
+
550
+ # There is a command in the queue
551
+ if t_controller.queue and len(t_controller.queue) > 0:
552
+ # If the command is quitting the menu we want to return in here
553
+ if t_controller.queue[0] in ("q", "..", "quit"):
554
+ print_goodbye()
555
+ break
556
+
557
+ # Consume 1 element from the queue
558
+ an_input = t_controller.queue[0]
559
+ t_controller.queue = t_controller.queue[1:]
560
+
561
+ # Print the current location because this was an instruction and we want user to know what was the action
562
+ if an_input and an_input.split(" ")[0] in t_controller.CHOICES_COMMANDS:
563
+ session.console.print(f"{get_flair_and_username()} / $ {an_input}")
564
+
565
+ # Get input command from user
566
+ else:
567
+ try:
568
+ # Get input from user using auto-completion
569
+ if session.prompt_session and session.settings.USE_PROMPT_TOOLKIT:
570
+ # Check if toolbar hint was enabled
571
+ if session.settings.TOOLBAR_HINT:
572
+ an_input = session.prompt_session.prompt( # type: ignore[union-attr]
573
+ f"{get_flair_and_username()} / $ ",
574
+ completer=t_controller.completer,
575
+ search_ignore_case=True,
576
+ bottom_toolbar=HTML(
577
+ '<style bg="ansiblack" fg="ansiwhite">[h]</style> help menu '
578
+ '<style bg="ansiblack" fg="ansiwhite">[q]</style> return to previous menu '
579
+ '<style bg="ansiblack" fg="ansiwhite">[e]</style> exit the program '
580
+ '<style bg="ansiblack" fg="ansiwhite">[cmd -h]</style> '
581
+ "see usage and available options "
582
+ ),
583
+ style=Style.from_dict(
584
+ {
585
+ "bottom-toolbar": "#ffffff bg:#333333",
586
+ }
587
+ ),
588
+ )
589
+ else:
590
+ an_input = session.prompt_session.prompt( # type: ignore[union-attr]
591
+ f"{get_flair_and_username()} / $ ",
592
+ completer=t_controller.completer,
593
+ search_ignore_case=True,
594
+ )
595
+
596
+ # Get input from user without auto-completion
597
+ else:
598
+ an_input = input(f"{get_flair_and_username()} / $ ")
599
+
600
+ except (KeyboardInterrupt, EOFError):
601
+ print_goodbye()
602
+ break
603
+
604
+ try:
605
+ # Process the input command
606
+ t_controller.queue = t_controller.switch(an_input)
607
+
608
+ if an_input in ("q", "quit", "..", "exit", "e"):
609
+ print_goodbye()
610
+ break
611
+
612
+ # Check if the user wants to reset application
613
+ if an_input in ("r", "reset") or t_controller.update_success:
614
+ reset(t_controller.queue if t_controller.queue else [])
615
+ break
616
+
617
+ except SystemExit:
618
+ logger.exception(
619
+ "The command '%s' doesn't exist on the / menu.",
620
+ an_input,
621
+ )
622
+ session.console.print(
623
+ f"[red]The command '{an_input}' doesn't exist on the / menu.[/red]\n",
624
+ )
625
+ similar_cmd = difflib.get_close_matches(
626
+ an_input.split(" ")[0] if " " in an_input else an_input,
627
+ t_controller.controller_choices,
628
+ n=1,
629
+ cutoff=0.7,
630
+ )
631
+ if similar_cmd:
632
+ an_input = similar_cmd[0]
633
+ if " " in an_input:
634
+ candidate_input = (
635
+ f"{similar_cmd[0]} {' '.join(an_input.split(' ')[1:])}"
636
+ )
637
+ if candidate_input == an_input:
638
+ an_input = ""
639
+ t_controller.queue = []
640
+ session.console.print("\n")
641
+ continue
642
+ an_input = candidate_input
643
+
644
+ session.console.print(f"[green]Replacing by '{an_input}'.[/green]")
645
+ t_controller.queue.insert(0, an_input)
646
+
647
+
648
+ def insert_start_slash(cmds: List[str]) -> List[str]:
649
+ """Insert a slash at the beginning of a command sequence."""
650
+ if not cmds[0].startswith("/"):
651
+ cmds[0] = f"/{cmds[0]}"
652
+ if cmds[0].startswith("/home"):
653
+ cmds[0] = f"/{cmds[0][5:]}"
654
+ return cmds
655
+
656
+
657
+ def run_scripts(
658
+ path: Path,
659
+ test_mode: bool = False,
660
+ verbose: bool = False,
661
+ routines_args: Optional[List[str]] = None,
662
+ special_arguments: Optional[Dict[str, str]] = None,
663
+ output: bool = True,
664
+ ):
665
+ """Run given .openbb scripts.
666
+
667
+ Parameters
668
+ ----------
669
+ path : str
670
+ The location of the .openbb file
671
+ test_mode : bool
672
+ Whether the CLI is in test mode
673
+ verbose : bool
674
+ Whether to run tests in verbose mode
675
+ routines_args : List[str]
676
+ One or multiple inputs to be replaced in the routine and separated by commas.
677
+ E.g. GME,AMC,BTC-USD
678
+ special_arguments: Optional[Dict[str, str]]
679
+ Replace `${key=default}` with `value` for every key in the dictionary
680
+ output: bool
681
+ Whether to log tests to txt files
682
+ """
683
+ if not path.exists():
684
+ session.console.print(f"File '{path}' doesn't exist. Launching base CLI.\n")
685
+ if not test_mode:
686
+ run_cli()
687
+
688
+ # THIS NEEDS TO BE REFACTORED!!! - ITS USED FOR TESTING
689
+ with path.open() as fp:
690
+ raw_lines = [x for x in fp if (not is_reset(x)) and ("#" not in x) and x]
691
+ raw_lines = [
692
+ raw_line.strip("\n") for raw_line in raw_lines if raw_line.strip("\n")
693
+ ]
694
+
695
+ if routines_args:
696
+ lines = []
697
+ for rawline in raw_lines:
698
+ templine = rawline
699
+ for i, arg in enumerate(routines_args):
700
+ templine = templine.replace(f"$ARGV[{i}]", arg)
701
+ lines.append(templine)
702
+ # Handle new testing arguments:
703
+ elif special_arguments:
704
+ lines = []
705
+ for line in raw_lines:
706
+ new_line = re.sub(
707
+ r"\${[^{]+=[^{]+}",
708
+ lambda x: replace_dynamic(x, special_arguments), # type: ignore
709
+ line,
710
+ )
711
+ lines.append(new_line)
712
+
713
+ else:
714
+ lines = raw_lines
715
+
716
+ if test_mode and "exit" not in lines[-1]:
717
+ lines.append("exit")
718
+
719
+ # Deals with the export with a path with "/" in it
720
+ export_folder = ""
721
+ if "export" in lines[0]:
722
+ export_folder = lines[0].split("export ")[1].rstrip()
723
+ lines = lines[1:]
724
+
725
+ simulate_argv = f"/{'/'.join([line.rstrip() for line in lines])}"
726
+ file_cmds = simulate_argv.replace("//", "/home/").split()
727
+ file_cmds = insert_start_slash(file_cmds) if file_cmds else file_cmds
728
+ file_cmds = (
729
+ [f"export {export_folder}{' '.join(file_cmds)}"]
730
+ if export_folder
731
+ else [" ".join(file_cmds)]
732
+ )
733
+
734
+ if not test_mode or verbose:
735
+ run_cli(file_cmds, test_mode=True)
736
+ else:
737
+ with suppress_stdout():
738
+ session.console.print(f"To ensure: {output}")
739
+ if output:
740
+ timestamp = datetime.now().timestamp()
741
+ stamp_str = str(timestamp).replace(".", "")
742
+ whole_path = Path(REPOSITORY_DIRECTORY / "integration_test_output")
743
+ whole_path.mkdir(parents=True, exist_ok=True)
744
+ first_cmd = file_cmds[0].split("/")[1]
745
+ with open(
746
+ whole_path / f"{stamp_str}_{first_cmd}_output.txt", "w"
747
+ ) as output_file, contextlib.redirect_stdout(output_file):
748
+ run_cli(file_cmds, test_mode=True)
749
+ else:
750
+ run_cli(file_cmds, test_mode=True)
751
+
752
+
753
+ def replace_dynamic(match: re.Match, special_arguments: Dict[str, str]) -> str:
754
+ """Replace ${key=default} with value in special_arguments if it exists, else with default.
755
+
756
+ Parameters
757
+ ----------
758
+ match: re.Match[str]
759
+ The match object
760
+ special_arguments: Dict[str, str]
761
+ The key value pairs to replace in the scripts
762
+
763
+ Returns
764
+ ----------
765
+ str
766
+ The new string
767
+ """
768
+
769
+ cleaned = match[0].replace("{", "").replace("}", "").replace("$", "")
770
+ key, default = cleaned.split("=")
771
+ dict_value = special_arguments.get(key, default)
772
+ if dict_value:
773
+ return dict_value
774
+ return default
775
+
776
+
777
+ def run_routine(file: str, routines_args=Optional[str]):
778
+ """Execute command routine from .openbb file."""
779
+ user_routine_path = Path(session.user.preferences.export_directory, "routines")
780
+ default_routine_path = ASSETS_DIRECTORY / "routines" / file
781
+
782
+ if user_routine_path.exists():
783
+ run_scripts(path=user_routine_path, routines_args=routines_args)
784
+ elif default_routine_path.exists():
785
+ run_scripts(path=default_routine_path, routines_args=routines_args)
786
+ else:
787
+ session.console.print(
788
+ f"Routine not found, please put your `.openbb` file into : {user_routine_path}."
789
+ )
790
+
791
+
792
+ # pylint: disable=unused-argument
793
+ def main(
794
+ debug: bool,
795
+ dev: bool,
796
+ path_list: List[str],
797
+ routines_args: Optional[List[str]] = None,
798
+ **kwargs,
799
+ ):
800
+ """Run the CLI with various options.
801
+
802
+ Parameters
803
+ ----------
804
+ debug : bool
805
+ Whether to run the CLI in debug mode
806
+ dev:
807
+ Points backend towards development environment instead of production
808
+ test : bool
809
+ Whether to run the CLI in integrated test mode
810
+ filtert : str
811
+ Filter test files with given string in name
812
+ paths : List[str]
813
+ The paths to run for scripts or to test
814
+ verbose : bool
815
+ Whether to show output from tests
816
+ routines_args : List[str]
817
+ One or multiple inputs to be replaced in the routine and separated by commas.
818
+ E.g. GME,AMC,BTC-USD
819
+ """
820
+ if debug:
821
+ session.settings.DEBUG_MODE = True
822
+
823
+ if dev:
824
+ session.settings.DEV_BACKEND = True
825
+ session.settings.BASE_URL = "https://payments.openbb.dev/"
826
+ session.settings.HUB_URL = "https://my.openbb.dev"
827
+
828
+ if isinstance(path_list, list) and path_list[0].endswith(".openbb"):
829
+ run_routine(file=path_list[0], routines_args=routines_args)
830
+ elif path_list:
831
+ argv_cmds = list([" ".join(path_list).replace(" /", "/home/")])
832
+ argv_cmds = insert_start_slash(argv_cmds) if argv_cmds else argv_cmds
833
+ run_cli(argv_cmds)
834
+ else:
835
+ run_cli()
836
+
837
+
838
+ def parse_args_and_run():
839
+ """Parse input arguments and run CLI."""
840
+ parser = argparse.ArgumentParser(
841
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
842
+ prog="cli",
843
+ description="The OpenBB Platform CLI.",
844
+ )
845
+ parser.add_argument(
846
+ "-d",
847
+ "--debug",
848
+ dest="debug",
849
+ action="store_true",
850
+ default=False,
851
+ help="Runs the CLI in debug mode.",
852
+ )
853
+ parser.add_argument(
854
+ "--dev",
855
+ dest="dev",
856
+ action="store_true",
857
+ default=False,
858
+ help="Points backend towards development environment instead of production",
859
+ )
860
+ parser.add_argument(
861
+ "--file",
862
+ help="The path or .openbb file to run.",
863
+ dest="path",
864
+ nargs="+",
865
+ default="",
866
+ type=str,
867
+ )
868
+ parser.add_argument(
869
+ "-i",
870
+ "--input",
871
+ help=(
872
+ "Select multiple inputs to be replaced in the routine and separated by commas."
873
+ "E.g. GME,AMC,BTC-USD"
874
+ ),
875
+ dest="routine_args",
876
+ type=lambda s: [str(item) for item in s.split(",")],
877
+ default=None,
878
+ )
879
+ parser.add_argument(
880
+ "-t",
881
+ "--test",
882
+ action="store_true",
883
+ help=(
884
+ "Run the CLI in testing mode. Also run this option and '-h'"
885
+ " to see testing argument options."
886
+ ),
887
+ )
888
+ # The args -m, -f and --HistoryManager.hist_file are used only in reports menu
889
+ # by papermill and that's why they have suppress help.
890
+ parser.add_argument(
891
+ "-m",
892
+ help=argparse.SUPPRESS,
893
+ dest="module",
894
+ default="",
895
+ type=str,
896
+ )
897
+ parser.add_argument(
898
+ "-f",
899
+ help=argparse.SUPPRESS,
900
+ dest="module_file",
901
+ default="",
902
+ type=str,
903
+ )
904
+ parser.add_argument(
905
+ "--HistoryManager.hist_file",
906
+ help=argparse.SUPPRESS,
907
+ dest="module_hist_file",
908
+ default="",
909
+ type=str,
910
+ )
911
+ if sys.argv[1:] and "-" not in sys.argv[1][0]:
912
+ sys.argv.insert(1, "--file")
913
+ ns_parser, unknown = parser.parse_known_args()
914
+
915
+ # This ensures that if cli.py receives unknown args it will not start.
916
+ # Use -d flag if you want to see the unknown args.
917
+ if unknown:
918
+ if ns_parser.debug:
919
+ session.console.print(unknown)
920
+ else:
921
+ sys.exit(-1)
922
+
923
+ main(
924
+ ns_parser.debug,
925
+ ns_parser.dev,
926
+ ns_parser.path,
927
+ ns_parser.routine_args,
928
+ module=ns_parser.module,
929
+ module_file=ns_parser.module_file,
930
+ module_hist_file=ns_parser.module_hist_file,
931
+ )
932
+
933
+
934
+ def launch(
935
+ debug: bool = False, dev: bool = False, queue: Optional[List[str]] = None
936
+ ) -> None:
937
+ """Launch CLI."""
938
+
939
+ if queue:
940
+ main(debug, dev, queue, module="")
941
+ else:
942
+ parse_args_and_run()
943
+
944
+
945
+ if __name__ == "__main__":
946
+ parse_args_and_run()