openbb-cli 1.2.0__tar.gz → 1.3.0__tar.gz

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 (46) hide show
  1. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/PKG-INFO +5 -31
  2. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/README.md +2 -28
  3. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/argparse_translator/argparse_argument.py +4 -7
  4. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/argparse_translator/argparse_class_processor.py +11 -11
  5. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/argparse_translator/argparse_translator.py +180 -114
  6. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/argparse_translator/obbject_registry.py +8 -9
  7. openbb_cli-1.3.0/openbb_cli/argparse_translator/reference_processor.py +122 -0
  8. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/argparse_translator/utils.py +3 -6
  9. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/cli.py +2 -1
  10. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/config/completer.py +13 -20
  11. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/config/console.py +3 -3
  12. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/config/menu_text.py +3 -4
  13. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/config/style.py +13 -15
  14. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/controllers/base_controller.py +68 -184
  15. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/controllers/base_platform_controller.py +12 -18
  16. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/controllers/choices.py +5 -4
  17. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/controllers/cli_controller.py +51 -79
  18. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/controllers/platform_controller_factory.py +3 -5
  19. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/controllers/script_parser.py +11 -18
  20. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/controllers/settings_controller.py +5 -5
  21. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/controllers/utils.py +33 -78
  22. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/session.py +5 -9
  23. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/pyproject.toml +3 -3
  24. openbb_cli-1.2.0/openbb_cli/argparse_translator/reference_processor.py +0 -142
  25. openbb_cli-1.2.0/openbb_cli/controllers/hub_service.py +0 -107
  26. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/__init__.py +0 -0
  27. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/argparse_translator/__init__.py +0 -0
  28. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/assets/routines/routine_example.openbb +0 -0
  29. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/assets/styles/default/Consolas.ttf +0 -0
  30. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/assets/styles/default/dark.mpfstyle.json +0 -0
  31. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/assets/styles/default/dark.mplrc.json +0 -0
  32. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/assets/styles/default/dark.mplstyle +0 -0
  33. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/assets/styles/default/dark.pltstyle.json +0 -0
  34. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/assets/styles/default/dark.richstyle.json +0 -0
  35. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/assets/styles/default/light.mpfstyle.json +0 -0
  36. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/assets/styles/default/light.mplrc.json +0 -0
  37. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/assets/styles/default/light.mplstyle +0 -0
  38. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/assets/styles/default/light.pltstyle.json +0 -0
  39. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/assets/styles/default/light.richstyle.json +0 -0
  40. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/assets/styles/default/tables.pltstyle.json +0 -0
  41. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/assets/styles/user/openbb.richstyle.json +0 -0
  42. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/config/__init__.py +0 -0
  43. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/config/constants.py +0 -0
  44. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/config/setup.py +0 -0
  45. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/models/settings.py +0 -0
  46. {openbb_cli-1.2.0 → openbb_cli-1.3.0}/openbb_cli/utils/utils.py +0 -0
@@ -1,18 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openbb-cli
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: Investment Research for Everyone, Anywhere.
5
5
  License: AGPL-3.0-only
6
6
  Author: OpenBB
7
7
  Author-email: hello@openbb.co
8
- Requires-Python: >=3.9.21,<3.14
8
+ Requires-Python: >=3.10,<3.14
9
9
  Classifier: License :: OSI Approved :: GNU Affero General Public License v3
10
10
  Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.10
12
12
  Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
- Requires-Dist: openbb[all] (>=4.5.0,<5.0.0)
15
+ Requires-Dist: openbb[all] (>=4.6.0,<5.0.0)
16
16
  Requires-Dist: openpyxl (>=3.1.5,<4.0.0)
17
17
  Requires-Dist: prompt-toolkit (>=3.0.50,<4.0.0)
18
18
  Requires-Dist: python-dotenv (>=1.0.1,<2.0.0)
@@ -61,33 +61,7 @@ Which should result in the following output:
61
61
 
62
62
  ![image](https://github.com/OpenBB-finance/OpenBB/assets/48914296/f606bb6e-fa00-4fc8-bad2-8269bb4fc38e)
63
63
 
64
- ## API keys
64
+ ## Documentation
65
65
 
66
- To fully leverage the OpenBB Platform you need to get some API keys to connect with data providers. Here are the 3 options on where to set them:
67
-
68
- 1. OpenBB Hub
69
- 2. Local file
70
-
71
- ### 1. OpenBB Hub
72
-
73
- Set your keys at [OpenBB Hub](https://my.openbb.co/app/platform/credentials) and get your personal access token from <https://my.openbb.co/app/platform/pat> to connect with your account.
74
-
75
- > Once you log in, on the Platform CLI (through the `/account` menu, all your credentials will be in sync with the OpenBB Hub.)
76
-
77
- ### 2. Local file
78
-
79
- You can specify the keys directly in the `~/.openbb_platform/user_settings.json` file.
80
-
81
- Populate this file with the following template and replace the values with your keys:
82
-
83
- ```json
84
- {
85
- "credentials": {
86
- "fmp_api_key": "REPLACE_ME",
87
- "polygon_api_key": "REPLACE_ME",
88
- "benzinga_api_key": "REPLACE_ME",
89
- "fred_api_key": "REPLACE_ME"
90
- }
91
- }
92
- ```
66
+ View the user documentation for this package [here](https://docs.openbb.co/cli)
93
67
 
@@ -36,32 +36,6 @@ Which should result in the following output:
36
36
 
37
37
  ![image](https://github.com/OpenBB-finance/OpenBB/assets/48914296/f606bb6e-fa00-4fc8-bad2-8269bb4fc38e)
38
38
 
39
- ## API keys
39
+ ## Documentation
40
40
 
41
- To fully leverage the OpenBB Platform you need to get some API keys to connect with data providers. Here are the 3 options on where to set them:
42
-
43
- 1. OpenBB Hub
44
- 2. Local file
45
-
46
- ### 1. OpenBB Hub
47
-
48
- Set your keys at [OpenBB Hub](https://my.openbb.co/app/platform/credentials) and get your personal access token from <https://my.openbb.co/app/platform/pat> to connect with your account.
49
-
50
- > Once you log in, on the Platform CLI (through the `/account` menu, all your credentials will be in sync with the OpenBB Hub.)
51
-
52
- ### 2. Local file
53
-
54
- You can specify the keys directly in the `~/.openbb_platform/user_settings.json` file.
55
-
56
- Populate this file with the following template and replace the values with your keys:
57
-
58
- ```json
59
- {
60
- "credentials": {
61
- "fmp_api_key": "REPLACE_ME",
62
- "polygon_api_key": "REPLACE_ME",
63
- "benzinga_api_key": "REPLACE_ME",
64
- "fred_api_key": "REPLACE_ME"
65
- }
66
- }
67
- ```
41
+ View the user documentation for this package [here](https://docs.openbb.co/cli)
@@ -2,10 +2,7 @@
2
2
 
3
3
  from typing import (
4
4
  Any,
5
- List,
6
5
  Literal,
7
- Optional,
8
- Tuple,
9
6
  )
10
7
 
11
8
  from pydantic import BaseModel, model_validator
@@ -22,9 +19,9 @@ class ArgparseArgumentModel(BaseModel):
22
19
  default: Any
23
20
  required: bool
24
21
  action: Literal["store_true", "store"]
25
- help: Optional[str]
26
- nargs: Optional[Literal["+"]]
27
- choices: Optional[Tuple]
22
+ help: str | None
23
+ nargs: Literal["+"] | None
24
+ choices: tuple | None
28
25
 
29
26
  @model_validator(mode="after") # type: ignore
30
27
  @classmethod
@@ -60,4 +57,4 @@ class ArgparseArgumentGroupModel(BaseModel):
60
57
  """Pydantic model for a custom argument group."""
61
58
 
62
59
  name: str
63
- arguments: List[ArgparseArgumentModel]
60
+ arguments: list[ArgparseArgumentModel]
@@ -1,7 +1,7 @@
1
1
  """Module for the ArgparseClassProcessor class."""
2
2
 
3
3
  import inspect
4
- from typing import Any, Dict, Optional, Type
4
+ from typing import Any
5
5
 
6
6
  # TODO: this needs to be done differently
7
7
  from openbb_core.app.static.container import Container
@@ -16,13 +16,13 @@ class ArgparseClassProcessor:
16
16
  """Process a target class to create ArgparseTranslators for its methods."""
17
17
 
18
18
  # reference variable used to create custom groups for the ArgpaseTranslators
19
- _reference: Dict[str, Any] = {}
19
+ _reference: dict[str, Any] = {}
20
20
 
21
21
  def __init__(
22
22
  self,
23
- target_class: Type,
23
+ target_class: type,
24
24
  add_help: bool = False,
25
- reference: Optional[Dict[str, Any]] = None,
25
+ reference: dict[str, Any] | None = None,
26
26
  ):
27
27
  """
28
28
  Initialize the ArgparseClassProcessor.
@@ -34,10 +34,10 @@ class ArgparseClassProcessor:
34
34
  add_help : Optional[bool]
35
35
  Whether to add help to the ArgparseTranslators.
36
36
  """
37
- self._target_class: Type = target_class
37
+ self._target_class: type = target_class
38
38
  self._add_help: bool = add_help
39
- self._translators: Dict[str, ArgparseTranslator] = {}
40
- self._paths: Dict[str, str] = {}
39
+ self._translators: dict[str, ArgparseTranslator] = {}
40
+ self._paths: dict[str, str] = {}
41
41
 
42
42
  ArgparseClassProcessor._reference = reference or {}
43
43
 
@@ -48,7 +48,7 @@ class ArgparseClassProcessor:
48
48
  self._build_paths(target=self._target_class)
49
49
 
50
50
  @property
51
- def translators(self) -> Dict[str, ArgparseTranslator]:
51
+ def translators(self) -> dict[str, ArgparseTranslator]:
52
52
  """
53
53
  Get the ArgparseTranslators associated with the target class.
54
54
 
@@ -60,7 +60,7 @@ class ArgparseClassProcessor:
60
60
  return self._translators
61
61
 
62
62
  @property
63
- def paths(self) -> Dict[str, str]:
63
+ def paths(self) -> dict[str, str]:
64
64
  """
65
65
  Get the paths associated with the target class.
66
66
 
@@ -72,7 +72,7 @@ class ArgparseClassProcessor:
72
72
  return self._paths
73
73
 
74
74
  @classmethod
75
- def _custom_groups_from_reference(cls, class_name: str, function_name: str) -> Dict:
75
+ def _custom_groups_from_reference(cls, class_name: str, function_name: str) -> dict:
76
76
  route = f"/{class_name.replace('_', '/')}/{function_name}"
77
77
  reference = {route: cls._reference[route]} if route in cls._reference else {}
78
78
  if not reference:
@@ -85,7 +85,7 @@ class ArgparseClassProcessor:
85
85
  cls,
86
86
  target: type,
87
87
  add_help: bool = False,
88
- ) -> Dict[str, ArgparseTranslator]:
88
+ ) -> dict[str, ArgparseTranslator]:
89
89
  methods = {}
90
90
 
91
91
  for name, member in inspect.getmembers(target):
@@ -3,16 +3,12 @@
3
3
  import argparse
4
4
  import inspect
5
5
  import re
6
+ from collections.abc import Callable
6
7
  from copy import deepcopy
7
8
  from typing import (
9
+ Annotated,
8
10
  Any,
9
- Callable,
10
- Dict,
11
- List,
12
11
  Literal,
13
- Optional,
14
- Tuple,
15
- Type,
16
12
  Union,
17
13
  get_args,
18
14
  get_origin,
@@ -21,7 +17,6 @@ from typing import (
21
17
 
22
18
  from openbb_core.app.model.field import OpenBBField
23
19
  from pydantic import BaseModel
24
- from typing_extensions import Annotated
25
20
 
26
21
  from openbb_cli.argparse_translator.argparse_argument import (
27
22
  ArgparseArgumentGroupModel,
@@ -46,8 +41,8 @@ class ArgparseTranslator:
46
41
  def __init__(
47
42
  self,
48
43
  func: Callable,
49
- custom_argument_groups: Optional[List[ArgparseArgumentGroupModel]] = None,
50
- add_help: Optional[bool] = True,
44
+ custom_argument_groups: list[ArgparseArgumentGroupModel] | None = None,
45
+ add_help: bool | None = True,
51
46
  ):
52
47
  """
53
48
  Initialize the ArgparseTranslator.
@@ -59,7 +54,7 @@ class ArgparseTranslator:
59
54
  self.func = func
60
55
  self.signature = inspect.signature(func)
61
56
  self.type_hints = get_type_hints(func)
62
- self.provider_parameters: Dict[str, List[str]] = {}
57
+ self.provider_parameters: dict[str, list[str]] = {}
63
58
 
64
59
  self._parser = argparse.ArgumentParser(
65
60
  prog=func.__name__,
@@ -82,9 +77,7 @@ class ArgparseTranslator:
82
77
  def _handle_argument_in_groups(self, argument, group):
83
78
  """Handle the argument and add it to the parser."""
84
79
 
85
- def _update_providers(
86
- input_string: str, new_provider: List[Optional[str]]
87
- ) -> str:
80
+ def _update_providers(input_string: str, new_provider: list[str | None]) -> str:
88
81
  pattern = r"\(provider:\s*(.*?)\)"
89
82
  providers = re.findall(pattern, input_string)
90
83
  providers.extend(new_provider)
@@ -159,119 +152,192 @@ class ArgparseTranslator:
159
152
  @staticmethod
160
153
  def _build_description(func_doc: str) -> str:
161
154
  """Build the description of the argparse program from the function docstring."""
162
- patterns = ["openbb\n ======", "Parameters\n ----------"]
155
+ # Remove only the Examples section and the openbb header if present
156
+ patterns_to_remove = [
157
+ (r"openbb\n\s+={3,}\n", ""), # Remove openbb header
158
+ (r"\n\s*Examples\n\s*-{3,}\n.*", ""), # Remove Examples section to end
159
+ ]
160
+
161
+ for pattern, replacement in patterns_to_remove:
162
+ func_doc = re.sub(pattern, replacement, func_doc, flags=re.DOTALL)
163
+
164
+ # Clean up type annotations in docstrings
165
+ def clean_type_annotation(type_str: str) -> str:
166
+ """Clean up a single type annotation."""
167
+ # First, handle the pipe union syntax: type1 | type2 -> type1 or type2
168
+ # Do this FIRST before other transformations
169
+ type_str = re.sub(r"\s*\|\s*", " or ", type_str)
163
170
 
164
- if func_doc:
165
- for pattern in patterns:
166
- if pattern in func_doc:
167
- func_doc = func_doc[: func_doc.index(pattern)].strip()
168
- break
171
+ # Handle Annotated[type, ...] -> type
172
+ type_str = re.sub(r"Annotated\[([^,\]]+)(?:,\s*[^\]]+)?\]", r"\1", type_str)
169
173
 
170
- return func_doc
174
+ # Handle Union[type1, type2, ...] -> type1 or type2 or ...
175
+ type_str = re.sub(
176
+ r"Union\[([^\]]+)\]",
177
+ lambda m: " or ".join(m.group(1).split(", ")),
178
+ type_str,
179
+ )
180
+
181
+ # Handle Optional[type] -> type or None
182
+ type_str = re.sub(r"Optional\[([^\]]+)\]", r"\1 or None", type_str)
183
+
184
+ # Deduplicate "or" separated types
185
+ parts = [p.strip() for p in type_str.split(" or ")]
186
+
187
+ # Remove duplicates while preserving order (case-insensitive)
188
+ seen = set()
189
+ unique_parts = []
190
+ for part in parts:
191
+ part_lower = part.lower()
192
+ if part_lower not in seen:
193
+ seen.add(part_lower)
194
+ unique_parts.append(part)
195
+
196
+ # If None is present, move it to the end
197
+ if "None" in unique_parts:
198
+ unique_parts.remove("None")
199
+ unique_parts.append("None")
200
+
201
+ type_str = " or ".join(unique_parts)
202
+
203
+ return type_str
204
+
205
+ # Process each line that contains a type annotation (format: "name : type")
206
+ lines = func_doc.split("\n")
207
+ cleaned_lines = []
208
+
209
+ for line in lines:
210
+ # Match lines with type annotations (parameter_name : type_annotation)
211
+ if ":" in line and not line.strip().startswith("#"):
212
+ # Split only on the first colon to preserve any colons in the description
213
+ parts = line.split(":", 1)
214
+ if len(parts) == 2:
215
+ param_name = parts[0]
216
+ rest = parts[1].strip()
217
+
218
+ # Extract the type part (everything before the first real description line)
219
+ # Type annotations typically don't contain spaces at the start of continuation lines
220
+ type_match = re.match(r"^([^\n]+?)(?:\n\s{4,}|\s{2,}|$)", rest)
221
+ if type_match:
222
+ type_part = type_match.group(1).strip()
223
+ description_part = rest[len(type_match.group(1)) :]
224
+
225
+ # Clean the type annotation
226
+ cleaned_type = clean_type_annotation(type_part)
227
+
228
+ # Reconstruct the line
229
+ if description_part:
230
+ cleaned_lines.append(
231
+ f"{param_name}: {cleaned_type}{description_part}"
232
+ )
233
+ else:
234
+ cleaned_lines.append(f"{param_name}: {cleaned_type}")
235
+ continue
236
+
237
+ cleaned_lines.append(line)
238
+
239
+ func_doc = "\n".join(cleaned_lines)
240
+
241
+ return func_doc.strip()
171
242
 
172
243
  @staticmethod
173
244
  def _param_is_default(param: inspect.Parameter) -> bool:
174
245
  """Return True if the parameter has a default value."""
175
246
  return param.default != inspect.Parameter.empty
176
247
 
177
- def _get_action_type(self, param: inspect.Parameter) -> str:
248
+ def _get_action_type(
249
+ self, param: inspect.Parameter
250
+ ) -> Literal["store_true", "store"]:
178
251
  """Return the argparse action type for the given parameter."""
179
252
  param_type = self.type_hints[param.name]
180
- type_origin = get_origin(param_type)
181
- if param_type is bool or (
182
- type_origin is Union and bool in get_args(param_type)
183
- ):
253
+ origin = get_origin(param_type)
254
+ args = get_args(param_type)
255
+
256
+ if param_type is bool:
257
+ return "store_true"
258
+
259
+ if origin is Union and bool in args:
260
+ return "store_true"
261
+
262
+ # Special case for Optional[bool] which is Union[bool, None]
263
+ if origin is Union and bool in args and type(None) in args:
184
264
  return "store_true"
265
+
185
266
  return "store"
186
267
 
187
- def _get_type_and_choices( # noqa: PLR0912 # pylint: disable=too-many-return-statements, too-many-branches, too-many-statements
268
+ def _get_type_and_choices(
188
269
  self, param: inspect.Parameter
189
- ) -> Tuple[Type[Any], Tuple[Any, ...]]:
270
+ ) -> tuple[type[Any], tuple[Any, ...]]:
190
271
  """Return the type and choices for the given parameter."""
191
- param_type = self.type_hints[param.name]
192
- type_origin = get_origin(param_type)
193
-
194
- choices: tuple[Any, ...] = ()
195
-
196
- if type_origin is Literal:
197
- choices = get_args(param_type)
198
- # Special handling for boolean literals
199
- if all(isinstance(choice, bool) for choice in choices):
200
- return bool, ()
201
- param_type = type(choices[0]) # type: ignore
202
-
203
- if type_origin is list:
204
- param_type = get_args(param_type)[0]
205
-
206
- if get_origin(param_type) is Literal:
207
- choices = get_args(param_type)
208
- # Special handling for boolean literals in lists
209
- if all(isinstance(choice, bool) for choice in choices):
210
- return bool, ()
211
- param_type = type(choices[0]) # type: ignore
212
-
213
- if type_origin is Union:
214
- union_args = get_args(param_type)
215
- # Check if Union contains bool type
216
- if bool in union_args and (str in union_args or len(union_args) == 2):
217
- return bool, ()
218
-
219
- # Check if Union contains Literal types and extract all choices
220
- literal_choices: list = []
221
- for arg in union_args:
222
- if get_origin(arg) is Literal:
223
- literal_choices.extend(get_args(arg))
224
-
225
- if literal_choices:
226
- # Check if all choices are boolean
227
- if all(isinstance(choice, bool) for choice in literal_choices):
228
- return bool, ()
229
- # We have Literal types in the Union, use their choices
230
- choices = tuple(literal_choices)
231
- param_type = type(choices[0]) # type: ignore
232
- elif str in union_args:
233
- param_type = str
234
-
235
- # check if it's an Optional, which would be a Union with NoneType
236
- if type(None) in get_args(param_type):
237
- # remove NoneType from the args
238
- args = [arg for arg in get_args(param_type) if arg is not None]
239
- # if there is only one arg left, use it
240
- if len(args) == 1:
241
- param_type = args[0]
242
-
243
- if get_origin(param_type) is Literal:
244
- choices = get_args(param_type)
245
- # Special handling for boolean literals
246
- if all(isinstance(choice, bool) for choice in choices):
247
- return bool, ()
248
- param_type = type(choices[0]) # type: ignore
249
- elif len(args) > 1:
250
- # Handle Union with multiple types (not just Optional)
251
- # Try to extract Literal types again from the filtered args
252
- literal_choices = []
253
- for arg in args:
254
- if get_origin(arg) is Literal:
255
- literal_choices.extend(get_args(arg))
256
-
257
- if literal_choices:
258
- # Check if all choices are boolean
259
- if all(isinstance(choice, bool) for choice in literal_choices):
260
- return bool, ()
261
- choices = tuple(set(literal_choices))
262
- param_type = type(choices[0]) # type: ignore
263
-
264
- # if there are custom choices, override
272
+
273
+ def get_base_type( # pylint: disable=R0911 # noqa:PLR0911
274
+ t: Any,
275
+ ) -> type:
276
+ """Recursively find the base type for argparse."""
277
+ origin = get_origin(t)
278
+ args = get_args(t)
279
+
280
+ if origin is Union or "types.UnionType" in str(type(t)):
281
+ non_none_args = [a for a in args if a is not type(None)]
282
+ if len(non_none_args) == 1:
283
+ return get_base_type(non_none_args[0])
284
+ # For Union[A, B, C], check for bool first, then default to str
285
+ if bool in non_none_args:
286
+ return bool
287
+ # If we have multiple types including str, prefer str as it's most flexible
288
+ if str in non_none_args:
289
+ return str
290
+ # Otherwise, try to get the first concrete type
291
+ for arg in non_none_args:
292
+ if arg not in (type(None), Any):
293
+ return get_base_type(arg)
294
+ return str
295
+ if origin is Literal:
296
+ return type(args[0]) if args else str
297
+ if origin is list:
298
+ return get_base_type(args[0]) if args else Any # type: ignore
299
+ if t is Any:
300
+ return str
301
+ # Handle actual type objects (like datetime.date)
302
+ if isinstance(t, type):
303
+ return t
304
+ return str
305
+
306
+ def get_choices(t: Any) -> tuple:
307
+ """Recursively find the choices for argparse."""
308
+ origin = get_origin(t)
309
+ args = get_args(t)
310
+
311
+ if origin is Union or "types.UnionType" in str(type(t)):
312
+ non_none_args = [a for a in args if a is not type(None)]
313
+ all_choices: list = []
314
+ for arg in non_none_args:
315
+ all_choices.extend(get_choices(arg))
316
+ return tuple(set(all_choices))
317
+ if origin is Literal:
318
+ return args
319
+ if origin is list and args:
320
+ return get_choices(args[0])
321
+ return ()
322
+
323
+ param_type_hint = self.type_hints[param.name]
324
+
325
+ base_type = get_base_type(param_type_hint)
326
+ choices = get_choices(param_type_hint)
327
+
265
328
  custom_choices = self._get_argument_custom_choices(param)
266
- if custom_choices and param_type is not bool:
329
+ if custom_choices:
267
330
  choices = tuple(custom_choices)
268
331
 
269
- return param_type, choices
332
+ if base_type is bool:
333
+ choices = ()
334
+
335
+ return base_type, choices
270
336
 
271
337
  @staticmethod
272
338
  def _split_annotation(
273
- base_annotation: Type[Any], custom_annotation_type: Type
274
- ) -> Tuple[Type[Any], List[Any]]:
339
+ base_annotation: type[Any], custom_annotation_type: type
340
+ ) -> tuple[type[Any], list[Any]]:
275
341
  """Find the base annotation and the custom annotations, namely the OpenBBField."""
276
342
  if get_origin(base_annotation) is not Annotated:
277
343
  return base_annotation, []
@@ -283,7 +349,7 @@ class ArgparseTranslator:
283
349
  ]
284
350
 
285
351
  @classmethod
286
- def _get_argument_custom_help(cls, param: inspect.Parameter) -> Optional[str]:
352
+ def _get_argument_custom_help(cls, param: inspect.Parameter) -> str | None:
287
353
  """Return the help annotation for the given parameter."""
288
354
  base_annotation = param.annotation
289
355
  _, custom_annotations = cls._split_annotation(base_annotation, OpenBBField)
@@ -293,7 +359,7 @@ class ArgparseTranslator:
293
359
  return help_annotation
294
360
 
295
361
  @classmethod
296
- def _get_argument_custom_choices(cls, param: inspect.Parameter) -> Optional[str]:
362
+ def _get_argument_custom_choices(cls, param: inspect.Parameter) -> str | None:
297
363
  """Return the help annotation for the given parameter."""
298
364
  base_annotation = param.annotation
299
365
  _, custom_annotations = cls._split_annotation(base_annotation, OpenBBField)
@@ -302,7 +368,7 @@ class ArgparseTranslator:
302
368
  )
303
369
  return choices_annotation
304
370
 
305
- def _get_nargs(self, param: inspect.Parameter) -> Optional[str]:
371
+ def _get_nargs(self, param: inspect.Parameter) -> Literal["+"] | None:
306
372
  """Return the nargs annotation for the given parameter."""
307
373
  param_type = self.type_hints[param.name]
308
374
  origin = get_origin(param_type)
@@ -339,7 +405,7 @@ class ArgparseTranslator:
339
405
  sig = inspect.signature(param_type)
340
406
 
341
407
  # add help to the annotation
342
- annotated_parameters: List[inspect.Parameter] = []
408
+ annotated_parameters: list[inspect.Parameter] = []
343
409
  for child_param in sig.parameters.values():
344
410
  new_child_param = child_param.replace(
345
411
  name=f"{param.name}{SEP}{child_param.name}",
@@ -400,9 +466,9 @@ class ArgparseTranslator:
400
466
  )
401
467
 
402
468
  @staticmethod
403
- def _unflatten_args(args: dict) -> Dict[str, Any]:
469
+ def _unflatten_args(args: dict) -> dict[str, Any]:
404
470
  """Unflatten the args that were flattened by the custom types."""
405
- result: Dict[str, Any] = {}
471
+ result: dict[str, Any] = {}
406
472
  for key, value in args.items():
407
473
  if SEP in key:
408
474
  parts = key.split(SEP)
@@ -416,7 +482,7 @@ class ArgparseTranslator:
416
482
  result[key] = value
417
483
  return result
418
484
 
419
- def _update_with_custom_types(self, kwargs: Dict[str, Any]) -> Dict[str, Any]:
485
+ def _update_with_custom_types(self, kwargs: dict[str, Any]) -> dict[str, Any]:
420
486
  """Update the kwargs with the custom types."""
421
487
  # for each argument in the signature that is a custom type, we need to
422
488
  # update the kwargs with the custom type kwargs
@@ -432,7 +498,7 @@ class ArgparseTranslator:
432
498
 
433
499
  def execute_func(
434
500
  self,
435
- parsed_args: Optional[argparse.Namespace] = None,
501
+ parsed_args: argparse.Namespace | None = None,
436
502
  ) -> Any:
437
503
  """
438
504
  Execute the original function with the parsed arguments.
@@ -447,7 +513,7 @@ class ArgparseTranslator:
447
513
  kwargs = self._unflatten_args(vars(parsed_args))
448
514
  kwargs = self._update_with_custom_types(kwargs)
449
515
  provider = kwargs.get("provider")
450
- provider_args: List = []
516
+ provider_args: list = []
451
517
  if provider and provider in self.provider_parameters:
452
518
  provider_args = self.provider_parameters[provider]
453
519
  else: