Skip to content

Commit 7e8b35d

Browse files
authored
Keep track of commands (#13)
1 parent 27d377f commit 7e8b35d

File tree

6 files changed

+96
-55
lines changed

6 files changed

+96
-55
lines changed

quantflow/cli/app.py

Lines changed: 31 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,38 +5,23 @@
55

66
import click
77
from prompt_toolkit import PromptSession
8+
from prompt_toolkit.completion import NestedCompleter
89
from prompt_toolkit.history import FileHistory
910
from rich.console import Console
1011
from rich.text import Text
1112

12-
from quantflow.data.fmp import FMP
13-
from quantflow.data.fred import Fred
1413
from quantflow.data.vault import Vault
1514

1615
from . import settings
17-
from .commands import fred, stocks, vault
18-
19-
20-
@click.group()
21-
def qf() -> None:
22-
pass
23-
24-
25-
@qf.command()
26-
def exit() -> None:
27-
"""Exit the program"""
28-
raise click.Abort()
29-
30-
31-
qf.add_command(vault.vault)
32-
qf.add_command(stocks.stocks)
33-
qf.add_command(fred.fred)
16+
from .commands import quantflow
17+
from .commands.base import QuantGroup
3418

3519

3620
@dataclass
3721
class QfApp:
3822
console: Console = field(default_factory=Console)
3923
vault: Vault = field(default_factory=partial(Vault, settings.VAULT_FILE_PATH))
24+
sections: list[QuantGroup] = field(default_factory=lambda: [quantflow])
4025

4126
def __call__(self) -> None:
4227
os.makedirs(settings.SETTINGS_DIRECTORY, exist_ok=True)
@@ -49,14 +34,33 @@ def __call__(self) -> None:
4934
try:
5035
while True:
5136
try:
52-
text = session.prompt("quantflow> ")
37+
text = session.prompt(
38+
self.prompt_message(),
39+
completer=self.prompt_completer(),
40+
complete_while_typing=True,
41+
)
5342
except KeyboardInterrupt:
5443
break
5544
else:
5645
self.handle_command(text)
5746
except click.Abort:
5847
self.console.print(Text("Bye!", style="bold magenta"))
5948

49+
def prompt_message(self) -> str:
50+
name = ":".join([str(section.name) for section in self.sections])
51+
return f"{name} > "
52+
53+
def prompt_completer(self) -> NestedCompleter:
54+
return NestedCompleter.from_nested_dict(
55+
{command: None for command in self.sections[-1].commands}
56+
)
57+
58+
def set_section(self, section: QuantGroup) -> None:
59+
self.sections.append(section)
60+
61+
def back(self) -> None:
62+
self.sections.pop()
63+
6064
def print(self, text_alike: Any, style: str = "") -> None:
6165
if isinstance(text_alike, str):
6266
style = style or "cyan"
@@ -67,29 +71,14 @@ def error(self, err: str | Exception) -> None:
6771
self.console.print(Text(f"\n{err}\n", style="bold red"))
6872

6973
def handle_command(self, text: str) -> None:
70-
self.current_command = text.split(" ")[0].strip()
7174
if not text:
7275
return
73-
elif text == "help":
74-
return qf.main(["--help"], standalone_mode=False, obj=self)
75-
76+
command = self.sections[-1]
7677
try:
77-
qf.main(text.split(), standalone_mode=False, obj=self)
78-
except click.exceptions.MissingParameter as e:
79-
self.error(e)
80-
except click.exceptions.NoSuchOption as e:
78+
command.main(text.split(), standalone_mode=False, obj=self)
79+
except (
80+
click.exceptions.MissingParameter,
81+
click.exceptions.NoSuchOption,
82+
click.exceptions.UsageError,
83+
) as e:
8184
self.error(e)
82-
except click.exceptions.UsageError as e:
83-
self.error(e)
84-
85-
def fmp(self) -> FMP:
86-
if key := self.vault.get("fmp"):
87-
return FMP(key=key)
88-
else:
89-
raise click.UsageError("No FMP API key found")
90-
91-
def fred(self) -> Fred:
92-
if key := self.vault.get("fred"):
93-
return Fred(key=key)
94-
else:
95-
raise click.UsageError("No FRED API key found")

quantflow/cli/commands/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from .base import QuantContext, quant_group
2+
from .fred import fred
3+
from .stocks import stocks
4+
from .vault import vault
5+
6+
7+
@quant_group()
8+
def quantflow() -> None:
9+
ctx = QuantContext.current()
10+
if ctx.invoked_subcommand is None:
11+
ctx.qf.print(ctx.get_help())
12+
13+
14+
quantflow.add_command(vault)
15+
quantflow.add_command(stocks)
16+
quantflow.add_command(fred)

quantflow/cli/commands/base.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING, Self, cast
3+
from typing import TYPE_CHECKING, Any, Self, cast
44

55
import click
66

@@ -21,6 +21,12 @@ def current(cls) -> Self:
2121
def qf(self) -> QfApp:
2222
return self.obj # type: ignore
2323

24+
def set_as_section(self) -> None:
25+
group = cast(QuantGroup, self.command)
26+
group.add_command(back)
27+
self.qf.set_section(group)
28+
self.qf.print(self.get_help())
29+
2430
def fmp(self) -> FMP:
2531
if key := self.qf.vault.get("fmp"):
2632
return FMP(key=key)
@@ -41,3 +47,33 @@ class QuantCommand(click.Command):
4147
class QuantGroup(click.Group):
4248
context_class = QuantContext
4349
command_class = QuantCommand
50+
51+
52+
@click.command(cls=QuantCommand)
53+
def exit() -> None:
54+
"""Exit the program"""
55+
raise click.Abort()
56+
57+
58+
@click.command(cls=QuantCommand)
59+
def help() -> None:
60+
"""display the commands"""
61+
if ctx := QuantContext.current().parent:
62+
cast(QuantContext, ctx).qf.print(ctx.get_help())
63+
64+
65+
@click.command(cls=QuantCommand)
66+
def back() -> None:
67+
"""Exit the current section"""
68+
ctx = QuantContext.current()
69+
ctx.qf.back()
70+
ctx.qf.handle_command("help")
71+
72+
73+
def quant_group() -> Any:
74+
return click.group(
75+
cls=QuantGroup,
76+
commands=[exit, help],
77+
invoke_without_command=True,
78+
add_help_option=False,
79+
)

quantflow/cli/commands/fred.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,20 @@
1212

1313
from quantflow.data.fred import Fred
1414

15-
from .base import QuantContext, QuantGroup
15+
from .base import QuantContext, quant_group
1616

1717
FREQUENCIES = tuple(Fred.freq)
1818

1919
if TYPE_CHECKING:
2020
pass
2121

2222

23-
@click.group(invoke_without_command=True, cls=QuantGroup)
23+
@quant_group()
2424
def fred() -> None:
25-
"""Federal Reserve of St. Louis data"""
25+
"""Federal Reserve of St. Louis data commands"""
2626
ctx = QuantContext.current()
2727
if ctx.invoked_subcommand is None:
28-
ctx.qf.print("Welcome to FRED data commands!")
29-
ctx.qf.print(ctx.get_help())
28+
ctx.set_as_section()
3029

3130

3231
@fred.command()

quantflow/cli/commands/stocks.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,17 @@
99

1010
from quantflow.data.fmp import FMP
1111

12-
from .base import QuantContext, QuantGroup
12+
from .base import QuantContext, quant_group
1313

1414
FREQUENCIES = tuple(FMP().historical_frequencies())
1515

1616

17-
@click.group(invoke_without_command=True, cls=QuantGroup)
17+
@quant_group()
1818
def stocks() -> None:
1919
"""Stocks commands"""
2020
ctx = QuantContext.current()
2121
if ctx.invoked_subcommand is None:
22-
ctx.qf.print("Welcome to the stocks commands!")
23-
ctx.qf.print(ctx.get_help())
22+
ctx.set_as_section()
2423

2524

2625
@stocks.command()

quantflow/cli/commands/vault.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import click
22

3-
from .base import QuantContext, QuantGroup
3+
from .base import QuantContext, quant_group
44

55
API_KEYS = ("fmp", "fred")
66

77

8-
@click.group(invoke_without_command=True, cls=QuantGroup)
8+
@quant_group()
99
def vault() -> None:
1010
"""Manage vault secrets"""
11-
pass
11+
ctx = QuantContext.current()
12+
if ctx.invoked_subcommand is None:
13+
ctx.set_as_section()
1214

1315

1416
@vault.command()

0 commit comments

Comments
 (0)