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,941 @@
1
+ """Base controller for the CLI."""
2
+
3
+ import argparse
4
+ import difflib
5
+ import os
6
+ import re
7
+ from abc import ABCMeta, abstractmethod
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Any, Dict, List, Literal, Optional, Union
11
+
12
+ import pandas as pd
13
+ from openbb_cli.config.completer import NestedCompleter
14
+ from openbb_cli.config.constants import SCRIPT_TAGS
15
+ from openbb_cli.controllers.choices import build_controller_choice_map
16
+ from openbb_cli.controllers.hub_service import upload_routine
17
+ from openbb_cli.controllers.utils import (
18
+ check_file_type_saved,
19
+ check_positive,
20
+ get_flair_and_username,
21
+ parse_and_split_input,
22
+ print_guest_block_msg,
23
+ print_rich_table,
24
+ remove_file,
25
+ system_clear,
26
+ )
27
+ from openbb_cli.session import Session
28
+ from prompt_toolkit.formatted_text import HTML
29
+ from prompt_toolkit.styles import Style
30
+
31
+ # pylint: disable=C0301,C0302,R0902,global-statement,too-many-boolean-expressions
32
+ # pylint: disable=R0912
33
+
34
+ controllers: Dict[str, Any] = {}
35
+ session = Session()
36
+
37
+
38
+ # TODO: We should try to avoid these global variables
39
+ RECORD_SESSION = False
40
+ RECORD_SESSION_LOCAL_ONLY = False
41
+ SESSION_RECORDED = list()
42
+ SESSION_RECORDED_NAME = ""
43
+ SESSION_RECORDED_DESCRIPTION = ""
44
+ SESSION_RECORDED_TAGS = ""
45
+ SESSION_RECORDED_PUBLIC = False
46
+
47
+
48
+ class BaseController(metaclass=ABCMeta):
49
+ """Base class for a cli controller."""
50
+
51
+ CHOICES_COMMON = [
52
+ "cls",
53
+ "home",
54
+ "h",
55
+ "?",
56
+ "help",
57
+ "q",
58
+ "quit",
59
+ "..",
60
+ "e",
61
+ "exit",
62
+ "r",
63
+ "reset",
64
+ "stop",
65
+ "whoami",
66
+ "results",
67
+ ]
68
+
69
+ CHOICES_COMMANDS: List[str] = []
70
+ CHOICES_MENUS: List[str] = []
71
+ NEWS_CHOICES: dict = {}
72
+ COMMAND_SEPARATOR = "/"
73
+ KEYS_MENU = "keys" + COMMAND_SEPARATOR
74
+ PATH: str = ""
75
+ FILE_PATH: str = ""
76
+ CHOICES_GENERATION = False
77
+
78
+ @property
79
+ def choices_default(self):
80
+ """Return the default choices."""
81
+ choices = (
82
+ build_controller_choice_map(controller=self)
83
+ if self.CHOICES_GENERATION
84
+ else {}
85
+ )
86
+
87
+ return choices
88
+
89
+ def __init__(self, queue: Optional[List[str]] = None) -> None:
90
+ """Create the base class for any controller in the codebase.
91
+
92
+ Used to simplify the creation of menus.
93
+
94
+ queue: List[str]
95
+ The current queue of jobs to process separated by "/"
96
+ E.g. /stocks/load gme/dps/sidtc/../exit
97
+ """
98
+ self.check_path()
99
+ self.path = [x for x in self.PATH.split("/") if x != ""]
100
+ self.queue = (
101
+ self.parse_input(an_input="/".join(queue))
102
+ if (queue and self.PATH != "/")
103
+ else list()
104
+ )
105
+
106
+ controller_choices = self.CHOICES_COMMANDS + self.CHOICES_MENUS
107
+ if controller_choices:
108
+ self.controller_choices = controller_choices + self.CHOICES_COMMON
109
+ else:
110
+ self.controller_choices = self.CHOICES_COMMON
111
+
112
+ self.completer: Union[None, NestedCompleter] = None
113
+
114
+ self.parser = argparse.ArgumentParser(
115
+ add_help=False,
116
+ prog=self.path[-1] if self.PATH != "/" else "cli",
117
+ )
118
+ self.parser.exit_on_error = False # type: ignore
119
+ self.parser.add_argument("cmd", choices=self.controller_choices)
120
+
121
+ def update_completer(self, choices) -> None:
122
+ """Update the completer with new choices."""
123
+ if session.prompt_session and session.settings.USE_PROMPT_TOOLKIT:
124
+ self.completer = NestedCompleter.from_nested_dict(choices)
125
+
126
+ def check_path(self) -> None:
127
+ """Check if command path is valid."""
128
+ path = self.PATH
129
+ if path[0] != "/":
130
+ raise ValueError("Path must begin with a '/' character.")
131
+ if path[-1] != "/":
132
+ raise ValueError("Path must end with a '/' character.")
133
+ if not re.match("^[a-z/]*$", path):
134
+ raise ValueError(
135
+ "Path must only contain lowercase letters and '/' characters."
136
+ )
137
+
138
+ def load_class(self, class_ins, *args, **kwargs):
139
+ """Check for an existing instance of the controller before creating a new one."""
140
+ settings = session.settings
141
+ self.save_class()
142
+ arguments = len(args) + len(kwargs)
143
+ # Due to the 'arguments == 1' condition, we actually NEVER load a class
144
+ # that has arguments (The 1 argument corresponds to self.queue)
145
+ # Advantage: If the user changes something on one controller and then goes to the
146
+ # controller below, it will create such class from scratch bringing all new variables
147
+ # in and considering latest changes.
148
+ # Disadvantage: If the user goes on a controller below and we have been there before
149
+ # it will not load that previous class, but create a new one from scratch.
150
+ # SCENARIO: If the user is in stocks and does load AAPL/ta the TA menu will get AAPL,
151
+ # and if then the user goes back to the stocks menu using .. that menu will have AAPL
152
+ # Now, if "arguments == 1" condition exists, if the user does "load TSLA" and then
153
+ # goes into "TA", the "TSLA" ticker will appear. If that condition doesn't exist
154
+ # the previous class will be loaded and even if the user changes the ticker on
155
+ # the stocks context it will not impact the one of TA menu - unless changes are done.
156
+ if (
157
+ class_ins.PATH in controllers
158
+ and arguments == 1
159
+ and settings.REMEMBER_CONTEXTS
160
+ ):
161
+ old_class = controllers[class_ins.PATH]
162
+ old_class.queue = self.queue
163
+ return old_class.menu()
164
+ return class_ins(*args, **kwargs).menu()
165
+
166
+ def save_class(self) -> None:
167
+ """Save the current instance of the class to be loaded later."""
168
+ if session.settings.REMEMBER_CONTEXTS:
169
+ controllers[self.PATH] = self
170
+
171
+ def custom_reset(self) -> List[str]:
172
+ """Implement custom reset.
173
+
174
+ This will be replaced by any children with custom_reset functions.
175
+ """
176
+ return []
177
+
178
+ @abstractmethod
179
+ def print_help(self) -> None:
180
+ """Print help placeholder."""
181
+ raise NotImplementedError("Must override print_help.")
182
+
183
+ def parse_input(self, an_input: str) -> list:
184
+ """Parse controller input.
185
+
186
+ Splits the command chain from user input into a list of individual commands
187
+ while respecting the forward slash in the command arguments.
188
+
189
+ In the default scenario only unix-like paths are handles by the parser.
190
+ Override this function in the controller classes that inherit from this one to
191
+ resolve edge cases specific to command arguments on those controllers.
192
+
193
+ When handling edge cases add additional regular expressions to the list.
194
+
195
+ Parameters
196
+ ----------
197
+ an_input : str
198
+ User input string
199
+
200
+ Returns
201
+ ----------
202
+ list
203
+ Command queue as list
204
+ """
205
+ custom_filters: list = []
206
+ commands = parse_and_split_input(
207
+ an_input=an_input, custom_filters=custom_filters
208
+ )
209
+ return commands
210
+
211
+ def switch(self, an_input: str) -> List[str]:
212
+ """Process and dispatch input.
213
+
214
+ Returns
215
+ ----------
216
+ List[str]
217
+ list of commands in the queue to execute
218
+ """
219
+ actions = self.parse_input(an_input)
220
+
221
+ if an_input and an_input != "reset":
222
+ session.console.print()
223
+
224
+ # Empty command
225
+ if len(actions) == 0:
226
+ pass
227
+
228
+ # Navigation slash is being used first split commands
229
+ elif len(actions) > 1:
230
+ # Absolute path is specified
231
+ if not actions[0]:
232
+ actions[0] = "home"
233
+
234
+ # Add all instructions to the queue
235
+ for cmd in actions[::-1]:
236
+ if cmd:
237
+ self.queue.insert(0, cmd)
238
+
239
+ # Single command fed, process
240
+ else:
241
+ try:
242
+ (known_args, other_args) = self.parser.parse_known_args(
243
+ an_input.split()
244
+ )
245
+ except Exception as exc:
246
+ raise SystemExit from exc
247
+
248
+ if RECORD_SESSION:
249
+ SESSION_RECORDED.append(an_input)
250
+
251
+ # Redirect commands to their correct functions
252
+ if known_args.cmd:
253
+ if known_args.cmd in ("..", "q"):
254
+ known_args.cmd = "quit"
255
+ elif known_args.cmd in ("e"):
256
+ known_args.cmd = "exit"
257
+ elif known_args.cmd in ("?", "h"):
258
+ known_args.cmd = "help"
259
+ elif known_args.cmd == "r":
260
+ known_args.cmd = "reset"
261
+
262
+ getattr(
263
+ self,
264
+ "call_" + known_args.cmd,
265
+ lambda _: "Command not recognized!",
266
+ )(other_args)
267
+
268
+ if (
269
+ an_input
270
+ and an_input != "reset"
271
+ and (
272
+ not self.queue or (self.queue and self.queue[0] not in ("quit", "help"))
273
+ )
274
+ ):
275
+ session.console.print()
276
+
277
+ return self.queue
278
+
279
+ def call_cls(self, _) -> None:
280
+ """Process cls command."""
281
+ system_clear()
282
+
283
+ def call_home(self, _) -> None:
284
+ """Process home command."""
285
+ self.save_class()
286
+ if self.PATH.count("/") == 1 and session.settings.ENABLE_EXIT_AUTO_HELP:
287
+ self.print_help()
288
+ for _ in range(self.PATH.count("/") - 1):
289
+ self.queue.insert(0, "quit")
290
+
291
+ def call_help(self, _) -> None:
292
+ """Process help command."""
293
+ self.print_help()
294
+
295
+ def call_quit(self, _) -> None:
296
+ """Process quit menu command."""
297
+ self.save_class()
298
+ self.queue.insert(0, "quit")
299
+
300
+ def call_exit(self, _) -> None:
301
+ # Not sure how to handle controller loading here
302
+ """Process exit cli command."""
303
+ self.save_class()
304
+ for _ in range(self.PATH.count("/")):
305
+ self.queue.insert(0, "quit")
306
+
307
+ if not session.is_local():
308
+ remove_file(
309
+ Path(session.user.preferences.export_directory, "routines", "hub")
310
+ )
311
+
312
+ def call_reset(self, _) -> None:
313
+ """Process reset command.
314
+
315
+ If you would like to have customization in the reset process define a method
316
+ `custom_reset` in the child class.
317
+ """
318
+ self.save_class()
319
+ if self.PATH != "/":
320
+ if self.custom_reset():
321
+ self.queue = self.custom_reset() + self.queue
322
+ else:
323
+ for val in self.path[::-1]:
324
+ self.queue.insert(0, val)
325
+ self.queue.insert(0, "reset")
326
+ for _ in range(len(self.path)):
327
+ self.queue.insert(0, "quit")
328
+
329
+ def call_record(self, other_args) -> None:
330
+ """Process record command."""
331
+ parser = argparse.ArgumentParser(
332
+ add_help=False,
333
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
334
+ prog="record",
335
+ description="Start recording session into .openbb routine file",
336
+ )
337
+ parser.add_argument(
338
+ "-n",
339
+ "--name",
340
+ action="store",
341
+ dest="name",
342
+ type=str,
343
+ default="",
344
+ help="Routine title name to be saved - only use characters, digits and whitespaces.",
345
+ nargs="+",
346
+ )
347
+ parser.add_argument(
348
+ "-d",
349
+ "--description",
350
+ type=str,
351
+ dest="description",
352
+ help="The description of the routine",
353
+ default=f"Routine recorded at {datetime.now().strftime('%H:%M')} from the OpenBB Platform CLI",
354
+ nargs="+",
355
+ )
356
+ parser.add_argument(
357
+ "--tag1",
358
+ type=str,
359
+ dest="tag1",
360
+ help=f"The tag associated with the routine. Select from: {', '.join(SCRIPT_TAGS)}",
361
+ default="",
362
+ nargs="+",
363
+ )
364
+ parser.add_argument(
365
+ "--tag2",
366
+ type=str,
367
+ dest="tag2",
368
+ help=f"The tag associated with the routine. Select from: {', '.join(SCRIPT_TAGS)}",
369
+ default="",
370
+ nargs="+",
371
+ )
372
+ parser.add_argument(
373
+ "--tag3",
374
+ type=str,
375
+ dest="tag3",
376
+ help=f"The tag associated with the routine. Select from: {', '.join(SCRIPT_TAGS)}",
377
+ default="",
378
+ nargs="+",
379
+ )
380
+ parser.add_argument(
381
+ "-p",
382
+ "--public",
383
+ dest="public",
384
+ action="store_true",
385
+ help="Whether the routine should be public or not",
386
+ default=False,
387
+ )
388
+
389
+ if other_args and "-" not in other_args[0][0]:
390
+ other_args.insert(0, "-n")
391
+
392
+ ns_parser = self.parse_simple_args(parser, other_args)
393
+
394
+ if ns_parser:
395
+ if not ns_parser.name:
396
+ session.console.print(
397
+ "[red]Set a routine title by using the '-n' flag. E.g. 'record -n Morning routine'[/red]"
398
+ )
399
+ return
400
+
401
+ tag1 = (
402
+ " ".join(ns_parser.tag1)
403
+ if isinstance(ns_parser.tag1, list)
404
+ else ns_parser.tag1
405
+ )
406
+ if tag1 and tag1 not in SCRIPT_TAGS:
407
+ session.console.print(
408
+ f"[red]The parameter 'tag1' needs to be one of the following {', '.join(SCRIPT_TAGS)}[/red]"
409
+ )
410
+ return
411
+
412
+ tag2 = (
413
+ " ".join(ns_parser.tag2)
414
+ if isinstance(ns_parser.tag2, list)
415
+ else ns_parser.tag2
416
+ )
417
+ if tag2 and tag2 not in SCRIPT_TAGS:
418
+ session.console.print(
419
+ f"[red]The parameter 'tag2' needs to be one of the following {', '.join(SCRIPT_TAGS)}[/red]"
420
+ )
421
+ return
422
+
423
+ tag3 = (
424
+ " ".join(ns_parser.tag3)
425
+ if isinstance(ns_parser.tag3, list)
426
+ else ns_parser.tag3
427
+ )
428
+ if tag3 and tag3 not in SCRIPT_TAGS:
429
+ session.console.print(
430
+ f"[red]The parameter 'tag3' needs to be one of the following {', '.join(SCRIPT_TAGS)}[/red]"
431
+ )
432
+ return
433
+
434
+ if session.is_local():
435
+ session.console.print(
436
+ "[red]Recording session to the OpenBB Hub is not supported in guest mode.[/red]"
437
+ )
438
+ session.console.print(
439
+ "\n[yellow]Visit the OpenBB Hub to register: http://my.openbb.co[/yellow]"
440
+ )
441
+ session.console.print(
442
+ "\n[yellow]Your routine will be saved locally.[/yellow]\n"
443
+ )
444
+
445
+ # Check if title has a valid format
446
+ title = " ".join(ns_parser.name) if ns_parser.name else ""
447
+ pattern = re.compile(r"^[a-zA-Z0-9\s]+$")
448
+ if not pattern.match(title):
449
+ session.console.print(
450
+ f"[red]Title '{title}' has invalid format. Please use only digits, characters and whitespaces.[/]"
451
+ )
452
+ return
453
+
454
+ global RECORD_SESSION # noqa: PLW0603
455
+ global RECORD_SESSION_LOCAL_ONLY # noqa: PLW0603
456
+ global SESSION_RECORDED_NAME # noqa: PLW0603
457
+ global SESSION_RECORDED_DESCRIPTION # noqa: PLW0603
458
+ global SESSION_RECORDED_TAGS # noqa: PLW0603
459
+ global SESSION_RECORDED_PUBLIC # noqa: PLW0603
460
+
461
+ RECORD_SESSION_LOCAL_ONLY = session.is_local()
462
+ RECORD_SESSION = True
463
+ SESSION_RECORDED_NAME = title
464
+ SESSION_RECORDED_DESCRIPTION = (
465
+ " ".join(ns_parser.description)
466
+ if isinstance(ns_parser.description, list)
467
+ else ns_parser.description
468
+ )
469
+ SESSION_RECORDED_TAGS = tag1 if tag1 else ""
470
+ SESSION_RECORDED_TAGS += "," + tag2 if tag2 else ""
471
+ SESSION_RECORDED_TAGS += "," + tag3 if tag3 else ""
472
+
473
+ SESSION_RECORDED_PUBLIC = ns_parser.public
474
+
475
+ session.console.print(
476
+ f"[green]The routine '{title}' is successfully being recorded.[/green]"
477
+ )
478
+ session.console.print(
479
+ "\n[yellow]Remember to run 'stop' command when you are done!\n[/yellow]"
480
+ )
481
+
482
+ def call_stop(self, other_args) -> None:
483
+ """Process stop command."""
484
+ parser = argparse.ArgumentParser(
485
+ add_help=False,
486
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
487
+ prog="stop",
488
+ description="Stop recording session into .openbb routine file",
489
+ )
490
+ # This is only for auto-completion purposes
491
+ _ = self.parse_simple_args(parser, other_args)
492
+
493
+ if "-h" not in other_args and "--help" not in other_args:
494
+ global RECORD_SESSION # noqa: PLW0603
495
+ global SESSION_RECORDED # noqa: PLW0603
496
+
497
+ if not RECORD_SESSION:
498
+ session.console.print(
499
+ "[red]There is no session being recorded. Start one using the command 'record'[/red]\n"
500
+ )
501
+ elif len(SESSION_RECORDED) < 5:
502
+ session.console.print(
503
+ "[red]Run at least 4 commands before stopping recording a session.[/red]\n"
504
+ )
505
+ else:
506
+ current_user = session.user
507
+
508
+ # Check if the user just wants to store routine locally
509
+ # This works regardless of whether they are logged in or not
510
+ if RECORD_SESSION_LOCAL_ONLY:
511
+ # Whitespaces are replaced by underscores and an .openbb extension is added
512
+ title_for_local_storage = (
513
+ SESSION_RECORDED_NAME.replace(" ", "_") + ".openbb"
514
+ )
515
+
516
+ routine_file = os.path.join(
517
+ f"{current_user.preferences.export_directory}/routines",
518
+ title_for_local_storage,
519
+ )
520
+
521
+ # If file already exists, add a timestamp to the name
522
+ if os.path.isfile(routine_file):
523
+ i = session.console.input(
524
+ "A local routine with the same name already exists, "
525
+ "do you want to override it? (y/n): "
526
+ )
527
+ session.console.print("")
528
+ while i.lower() not in ["y", "yes", "n", "no"]:
529
+ i = session.console.input("Select 'y' or 'n' to proceed: ")
530
+ session.console.print("")
531
+
532
+ if i.lower() in ["n", "no"]:
533
+ new_name = (
534
+ datetime.now().strftime("%Y%m%d_%H%M%S_")
535
+ + title_for_local_storage
536
+ )
537
+ routine_file = os.path.join(
538
+ current_user.preferences.export_directory,
539
+ "routines",
540
+ new_name,
541
+ )
542
+ session.console.print(
543
+ f"[yellow]The routine name has been updated to '{new_name}'[/yellow]\n"
544
+ )
545
+
546
+ # Writing to file
547
+ Path(os.path.dirname(routine_file)).mkdir(
548
+ parents=True, exist_ok=True
549
+ )
550
+
551
+ with open(routine_file, "w") as file1:
552
+ lines = ["# OpenBB Platform CLI - Routine", "\n"]
553
+
554
+ username = getattr(
555
+ session.user.profile.hub_session, "username", "local"
556
+ )
557
+
558
+ lines += (
559
+ [f"# Author: {username}", "\n\n"] if username else ["\n"]
560
+ )
561
+ lines += [
562
+ f"# Title: {SESSION_RECORDED_NAME}",
563
+ "\n",
564
+ f"# Tags: {SESSION_RECORDED_TAGS}",
565
+ "\n\n",
566
+ f"# Description: {SESSION_RECORDED_DESCRIPTION}",
567
+ "\n\n",
568
+ ]
569
+ lines += [c + "\n" for c in SESSION_RECORDED[:-1]]
570
+ # Writing data to a file
571
+ file1.writelines(lines)
572
+
573
+ session.console.print(
574
+ f"[green]Your routine has been recorded and saved here: {routine_file}[/green]\n"
575
+ )
576
+
577
+ # If user doesn't specify they want to store routine locally
578
+ # Confirm that the user is logged in
579
+ elif not session.is_local():
580
+ routine = "\n".join(SESSION_RECORDED[:-1])
581
+ hub_session = current_user.profile.hub_session
582
+
583
+ if routine is not None:
584
+ auth_header = (
585
+ f"{hub_session.token_type} {hub_session.access_token.get_secret_value()}"
586
+ if hub_session
587
+ else None
588
+ )
589
+ kwargs = {
590
+ "auth_header": auth_header,
591
+ "name": SESSION_RECORDED_NAME,
592
+ "description": SESSION_RECORDED_DESCRIPTION,
593
+ "routine": routine,
594
+ "tags": SESSION_RECORDED_TAGS,
595
+ "public": SESSION_RECORDED_PUBLIC,
596
+ }
597
+ response = upload_routine(**kwargs) # type: ignore
598
+ if response is not None and response.status_code == 409:
599
+ i = session.console.input(
600
+ "A routine with the same name already exists, "
601
+ "do you want to replace it? (y/n): "
602
+ )
603
+ session.console.print("")
604
+ if i.lower() in ["y", "yes"]:
605
+ kwargs["override"] = True # type: ignore
606
+ response = upload_routine(**kwargs) # type: ignore
607
+ else:
608
+ session.console.print("[info]Aborted.[/info]")
609
+
610
+ # Clear session to be recorded again
611
+ RECORD_SESSION = False
612
+ SESSION_RECORDED = list()
613
+
614
+ def call_whoami(self, other_args: List[str]) -> None:
615
+ """Process whoami command."""
616
+ parser = argparse.ArgumentParser(
617
+ add_help=False,
618
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
619
+ prog="whoami",
620
+ description="Show current user",
621
+ )
622
+ ns_parser = self.parse_simple_args(parser, other_args)
623
+
624
+ if ns_parser:
625
+ current_user = session.user
626
+ local_user = session.is_local()
627
+ if not local_user:
628
+ hub_session = current_user.profile.hub_session
629
+ session.console.print(
630
+ f"[info]email:[/info] {hub_session.email if hub_session else 'N/A'}"
631
+ )
632
+ session.console.print(
633
+ f"[info]uuid:[/info] {hub_session.user_uuid if hub_session else 'N/A'}"
634
+ )
635
+ else:
636
+ print_guest_block_msg()
637
+
638
+ def call_results(self, other_args: List[str]):
639
+ """Process results command."""
640
+ parser = argparse.ArgumentParser(
641
+ add_help=False,
642
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
643
+ prog="results",
644
+ description="Process results command. This command displays a registry of "
645
+ "'OBBjects' where all execution results are stored. "
646
+ "It is organized as a stack, with the most recent result at index 0.",
647
+ )
648
+ ns_parser = self.parse_simple_args(parser, other_args)
649
+ if ns_parser:
650
+ results = session.obbject_registry.all
651
+ if results:
652
+ df = pd.DataFrame.from_dict(results, orient="index")
653
+ print_rich_table(
654
+ df,
655
+ show_index=True,
656
+ index_name="stack index",
657
+ title="OBBject Results",
658
+ )
659
+ else:
660
+ session.console.print("[info]No results found.[/info]")
661
+
662
+ @staticmethod
663
+ def parse_simple_args(parser: argparse.ArgumentParser, other_args: List[str]):
664
+ """Parse list of arguments into the supplied parser.
665
+
666
+ Parameters
667
+ ----------
668
+ parser: argparse.ArgumentParser
669
+ Parser with predefined arguments
670
+ other_args: List[str]
671
+ List of arguments to parse
672
+
673
+ Returns
674
+ -------
675
+ ns_parser:
676
+ Namespace with parsed arguments
677
+ """
678
+ parser.add_argument(
679
+ "-h", "--help", action="store_true", help="show this help message"
680
+ )
681
+
682
+ if session.settings.USE_CLEAR_AFTER_CMD:
683
+ system_clear()
684
+
685
+ try:
686
+ (ns_parser, l_unknown_args) = parser.parse_known_args(other_args)
687
+ except SystemExit:
688
+ # In case the command has required argument that isn't specified
689
+ session.console.print("\n")
690
+ return None
691
+
692
+ if ns_parser.help:
693
+ txt_help = parser.format_help()
694
+ session.console.print(f"[help]{txt_help}[/help]")
695
+ return None
696
+
697
+ if l_unknown_args:
698
+ session.console.print(
699
+ f"The following args couldn't be interpreted: {l_unknown_args}\n"
700
+ )
701
+
702
+ return ns_parser
703
+
704
+ @classmethod
705
+ def parse_known_args_and_warn(
706
+ cls,
707
+ parser: argparse.ArgumentParser,
708
+ other_args: List[str],
709
+ export_allowed: Literal[
710
+ "no_export", "raw_data_only", "figures_only", "raw_data_and_figures"
711
+ ] = "no_export",
712
+ raw: bool = False,
713
+ limit: int = 0,
714
+ ):
715
+ """Parse list of arguments into the supplied parser.
716
+
717
+ Parameters
718
+ ----------
719
+ parser: argparse.ArgumentParser
720
+ Parser with predefined arguments
721
+ other_args: List[str]
722
+ list of arguments to parse
723
+ export_allowed: Literal["no_export", "raw_data_only", "figures_only", "raw_data_and_figures"]
724
+ Export options
725
+ raw: bool
726
+ Add the --raw flag
727
+ limit: int
728
+ Add a --limit flag with this number default
729
+
730
+ Returns
731
+ ----------
732
+ ns_parser:
733
+ Namespace with parsed arguments
734
+ """
735
+ parser.add_argument(
736
+ "-h", "--help", action="store_true", help="show this help message"
737
+ )
738
+
739
+ if export_allowed != "no_export":
740
+ choices_export = []
741
+ help_export = "Does not export!"
742
+
743
+ if export_allowed == "raw_data_only":
744
+ choices_export = ["csv", "json", "xlsx"]
745
+ help_export = "Export raw data into csv, json, xlsx"
746
+ elif export_allowed == "figures_only":
747
+ choices_export = ["png", "jpg", "pdf", "svg"]
748
+ help_export = "Export figure into png, jpg, pdf, svg "
749
+ else:
750
+ choices_export = ["csv", "json", "xlsx", "png", "jpg", "pdf", "svg"]
751
+ help_export = "Export raw data into csv, json, xlsx and figure into png, jpg, pdf, svg "
752
+
753
+ parser.add_argument(
754
+ "--export",
755
+ default="",
756
+ type=check_file_type_saved(choices_export),
757
+ dest="export",
758
+ help=help_export,
759
+ nargs="+",
760
+ )
761
+
762
+ # If excel is an option, add the sheet name
763
+ if export_allowed in [
764
+ "raw_data_only",
765
+ "raw_data_and_figures",
766
+ ]:
767
+ parser.add_argument(
768
+ "--sheet-name",
769
+ dest="sheet_name",
770
+ default=None,
771
+ nargs="+",
772
+ help="Name of excel sheet to save data to. Only valid for .xlsx files.",
773
+ )
774
+
775
+ if raw:
776
+ parser.add_argument(
777
+ "--raw",
778
+ dest="raw",
779
+ action="store_true",
780
+ default=False,
781
+ help="Flag to display raw data",
782
+ )
783
+ if limit > 0:
784
+ parser.add_argument(
785
+ "-l",
786
+ "--limit",
787
+ dest="limit",
788
+ default=limit,
789
+ help="Number of entries to show in data.",
790
+ type=check_positive,
791
+ )
792
+ if session.settings.USE_CLEAR_AFTER_CMD:
793
+ system_clear()
794
+
795
+ if "--help" in other_args or "-h" in other_args:
796
+ txt_help = parser.format_help() + "\n"
797
+ session.console.print(f"[help]{txt_help}[/help]")
798
+ return None
799
+
800
+ try:
801
+ # If the user uses a comma separated list of arguments, split them
802
+ for index, arg in enumerate(other_args):
803
+ if "," in arg:
804
+ parts = arg.split(",")
805
+ other_args[index : index + 1] = parts
806
+
807
+ (ns_parser, l_unknown_args) = parser.parse_known_args(other_args)
808
+
809
+ if export_allowed in [
810
+ "raw_data_only",
811
+ "raw_data_and_figures",
812
+ ]:
813
+ ns_parser.is_image = any(
814
+ ext in ns_parser.export for ext in ["png", "svg", "jpg", "pdf"]
815
+ )
816
+
817
+ except SystemExit:
818
+ # In case the command has required argument that isn't specified
819
+
820
+ return None
821
+
822
+ if l_unknown_args:
823
+ session.console.print(
824
+ f"The following args couldn't be interpreted: {l_unknown_args}"
825
+ )
826
+ return ns_parser
827
+
828
+ def menu(self, custom_path_menu_above: str = ""):
829
+ """Enter controller menu."""
830
+ settings = session.settings
831
+ an_input = "HELP_ME"
832
+
833
+ while True:
834
+ # There is a command in the queue
835
+ if self.queue and len(self.queue) > 0:
836
+ if self.queue[0] in ("q", "..", "quit"):
837
+ self.save_class()
838
+ # Go back to the root in order to go to the right directory because
839
+ # there was a jump between indirect menus
840
+ if custom_path_menu_above:
841
+ self.queue.insert(1, custom_path_menu_above)
842
+
843
+ if len(self.queue) > 1:
844
+ return self.queue[1:]
845
+
846
+ if settings.ENABLE_EXIT_AUTO_HELP:
847
+ return ["help"]
848
+ return []
849
+
850
+ # Consume 1 element from the queue
851
+ an_input = self.queue[0]
852
+ self.queue = self.queue[1:]
853
+
854
+ # Print location because this was an instruction and we want user to know the action
855
+ if (
856
+ an_input
857
+ and an_input != "home"
858
+ and an_input != "help"
859
+ and an_input.split(" ")[0] in self.controller_choices
860
+ ):
861
+ session.console.print(
862
+ f"{get_flair_and_username()} {self.PATH} $ {an_input}"
863
+ )
864
+
865
+ # Get input command from user
866
+ else:
867
+ # Display help menu when entering on this menu from a level above
868
+ if an_input == "HELP_ME":
869
+ self.print_help()
870
+
871
+ try:
872
+ prompt_session = session.prompt_session
873
+ if prompt_session and settings.USE_PROMPT_TOOLKIT:
874
+ # Check if toolbar hint was enabled
875
+ if settings.TOOLBAR_HINT:
876
+ an_input = prompt_session.prompt(
877
+ f"{get_flair_and_username()} {self.PATH} $ ",
878
+ completer=self.completer,
879
+ search_ignore_case=True,
880
+ bottom_toolbar=HTML(
881
+ '<style bg="ansiblack" fg="ansiwhite">[h]</style> help menu '
882
+ '<style bg="ansiblack" fg="ansiwhite">[q]</style> return to previous menu '
883
+ '<style bg="ansiblack" fg="ansiwhite">[e]</style> exit the program '
884
+ '<style bg="ansiblack" fg="ansiwhite">[cmd -h]</style> '
885
+ "see usage and available options "
886
+ f"{self.path[-1].capitalize()} (cmd/menu) Documentation"
887
+ ),
888
+ style=Style.from_dict(
889
+ {"bottom-toolbar": "#ffffff bg:#333333"}
890
+ ),
891
+ )
892
+ else:
893
+ an_input = prompt_session.prompt(
894
+ f"{get_flair_and_username()} {self.PATH} $ ",
895
+ completer=self.completer,
896
+ search_ignore_case=True,
897
+ )
898
+ # Get input from user without auto-completion
899
+ else:
900
+ an_input = input(f"{get_flair_and_username()} {self.PATH} $ ")
901
+
902
+ except (KeyboardInterrupt, EOFError):
903
+ # Exit in case of keyboard interrupt
904
+ an_input = "exit"
905
+
906
+ try:
907
+ # Allow user to go back to root
908
+ an_input = "home" if an_input == "/" else an_input
909
+
910
+ # Process the input command
911
+ self.queue = self.switch(an_input)
912
+
913
+ except SystemExit:
914
+ session.console.print(
915
+ f"[red]The command '{an_input}' doesn't exist on the {self.PATH} menu.[/red]\n",
916
+ )
917
+ similar_cmd = difflib.get_close_matches(
918
+ an_input.split(" ")[0] if " " in an_input else an_input,
919
+ self.controller_choices,
920
+ n=1,
921
+ cutoff=0.7,
922
+ )
923
+ if similar_cmd:
924
+ if " " in an_input:
925
+ candidate_input = (
926
+ f"{similar_cmd[0]} {' '.join(an_input.split(' ')[1:])}"
927
+ )
928
+ if candidate_input == an_input:
929
+ an_input = ""
930
+ self.queue = []
931
+ session.console.print("\n")
932
+ continue
933
+
934
+ an_input = candidate_input
935
+ else:
936
+ an_input = similar_cmd[0]
937
+
938
+ session.console.print(
939
+ f"[green]Replacing by '{an_input}'.[/green]\n"
940
+ )
941
+ self.queue.insert(0, an_input)