falyx 0.1.24__tar.gz → 0.1.26__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.
- {falyx-0.1.24 → falyx-0.1.26}/PKG-INFO +2 -1
- {falyx-0.1.24 → falyx-0.1.26}/falyx/__init__.py +1 -1
- falyx-0.1.26/falyx/action/__init__.py +41 -0
- {falyx-0.1.24/falyx → falyx-0.1.26/falyx/action}/action.py +1 -1
- {falyx-0.1.24/falyx → falyx-0.1.26/falyx/action}/action_factory.py +2 -2
- {falyx-0.1.24/falyx → falyx-0.1.26/falyx/action}/http_action.py +2 -2
- {falyx-0.1.24/falyx → falyx-0.1.26/falyx/action}/io_action.py +2 -2
- {falyx-0.1.24/falyx → falyx-0.1.26/falyx/action}/menu_action.py +4 -80
- {falyx-0.1.24/falyx → falyx-0.1.26/falyx/action}/select_file_action.py +3 -37
- {falyx-0.1.24/falyx → falyx-0.1.26/falyx/action}/selection_action.py +2 -2
- falyx-0.1.26/falyx/action/signal_action.py +43 -0
- falyx-0.1.26/falyx/action/types.py +37 -0
- {falyx-0.1.24 → falyx-0.1.26}/falyx/bottom_bar.py +1 -1
- {falyx-0.1.24 → falyx-0.1.26}/falyx/command.py +3 -3
- {falyx-0.1.24 → falyx-0.1.26}/falyx/config.py +75 -13
- {falyx-0.1.24 → falyx-0.1.26}/falyx/execution_registry.py +1 -1
- {falyx-0.1.24 → falyx-0.1.26}/falyx/falyx.py +7 -4
- {falyx-0.1.24 → falyx-0.1.26}/falyx/hooks.py +1 -1
- falyx-0.1.26/falyx/menu.py +85 -0
- {falyx-0.1.24 → falyx-0.1.26}/falyx/prompt_utils.py +1 -1
- {falyx-0.1.24 → falyx-0.1.26}/falyx/protocols.py +1 -1
- {falyx-0.1.24 → falyx-0.1.26}/falyx/retry_utils.py +1 -1
- {falyx-0.1.24 → falyx-0.1.26}/falyx/selection.py +1 -1
- falyx-0.1.26/falyx/themes/__init__.py +15 -0
- falyx-0.1.26/falyx/version.py +1 -0
- {falyx-0.1.24 → falyx-0.1.26}/pyproject.toml +2 -1
- falyx-0.1.24/falyx/signal_action.py +0 -31
- falyx-0.1.24/falyx/version.py +0 -1
- {falyx-0.1.24 → falyx-0.1.26}/LICENSE +0 -0
- {falyx-0.1.24 → falyx-0.1.26}/README.md +0 -0
- {falyx-0.1.24 → falyx-0.1.26}/falyx/.pytyped +0 -0
- {falyx-0.1.24 → falyx-0.1.26}/falyx/__main__.py +0 -0
- {falyx-0.1.24 → falyx-0.1.26}/falyx/config_schema.py +0 -0
- {falyx-0.1.24 → falyx-0.1.26}/falyx/context.py +0 -0
- {falyx-0.1.24 → falyx-0.1.26}/falyx/debug.py +0 -0
- {falyx-0.1.24 → falyx-0.1.26}/falyx/exceptions.py +0 -0
- {falyx-0.1.24 → falyx-0.1.26}/falyx/hook_manager.py +0 -0
- {falyx-0.1.24 → falyx-0.1.26}/falyx/init.py +0 -0
- {falyx-0.1.24 → falyx-0.1.26}/falyx/logger.py +0 -0
- {falyx-0.1.24 → falyx-0.1.26}/falyx/options_manager.py +0 -0
- {falyx-0.1.24 → falyx-0.1.26}/falyx/parsers.py +0 -0
- {falyx-0.1.24 → falyx-0.1.26}/falyx/retry.py +0 -0
- {falyx-0.1.24 → falyx-0.1.26}/falyx/signals.py +0 -0
- {falyx-0.1.24 → falyx-0.1.26}/falyx/tagged_table.py +0 -0
- {falyx-0.1.24 → falyx-0.1.26}/falyx/themes/colors.py +0 -0
- {falyx-0.1.24 → falyx-0.1.26}/falyx/utils.py +0 -0
- {falyx-0.1.24 → falyx-0.1.26}/falyx/validators.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: falyx
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.26
|
4
4
|
Summary: Reliable and introspectable async CLI action framework.
|
5
5
|
License: MIT
|
6
6
|
Author: Roland Thomas Jr
|
@@ -12,6 +12,7 @@ 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: aiohttp (>=3.11,<4.0)
|
15
16
|
Requires-Dist: prompt_toolkit (>=3.0,<4.0)
|
16
17
|
Requires-Dist: pydantic (>=2.0,<3.0)
|
17
18
|
Requires-Dist: python-json-logger (>=3.3.0,<4.0.0)
|
@@ -7,7 +7,7 @@ Licensed under the MIT License. See LICENSE file for details.
|
|
7
7
|
|
8
8
|
import logging
|
9
9
|
|
10
|
-
from .action import Action, ActionGroup, ChainedAction, ProcessAction
|
10
|
+
from .action.action import Action, ActionGroup, ChainedAction, ProcessAction
|
11
11
|
from .command import Command
|
12
12
|
from .context import ExecutionContext, SharedContext
|
13
13
|
from .execution_registry import ExecutionRegistry
|
@@ -0,0 +1,41 @@
|
|
1
|
+
"""
|
2
|
+
Falyx CLI Framework
|
3
|
+
|
4
|
+
Copyright (c) 2025 rtj.dev LLC.
|
5
|
+
Licensed under the MIT License. See LICENSE file for details.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from .action import (
|
9
|
+
Action,
|
10
|
+
ActionGroup,
|
11
|
+
BaseAction,
|
12
|
+
ChainedAction,
|
13
|
+
FallbackAction,
|
14
|
+
LiteralInputAction,
|
15
|
+
ProcessAction,
|
16
|
+
)
|
17
|
+
from .action_factory import ActionFactoryAction
|
18
|
+
from .http_action import HTTPAction
|
19
|
+
from .io_action import BaseIOAction, ShellAction
|
20
|
+
from .menu_action import MenuAction
|
21
|
+
from .select_file_action import SelectFileAction
|
22
|
+
from .selection_action import SelectionAction
|
23
|
+
from .signal_action import SignalAction
|
24
|
+
|
25
|
+
__all__ = [
|
26
|
+
"Action",
|
27
|
+
"ActionGroup",
|
28
|
+
"BaseAction",
|
29
|
+
"ChainedAction",
|
30
|
+
"ProcessAction",
|
31
|
+
"ActionFactoryAction",
|
32
|
+
"HTTPAction",
|
33
|
+
"BaseIOAction",
|
34
|
+
"ShellAction",
|
35
|
+
"SelectionAction",
|
36
|
+
"SelectFileAction",
|
37
|
+
"MenuAction",
|
38
|
+
"SignalAction",
|
39
|
+
"FallbackAction",
|
40
|
+
"LiteralInputAction",
|
41
|
+
]
|
@@ -48,7 +48,7 @@ from falyx.hook_manager import Hook, HookManager, HookType
|
|
48
48
|
from falyx.logger import logger
|
49
49
|
from falyx.options_manager import OptionsManager
|
50
50
|
from falyx.retry import RetryHandler, RetryPolicy
|
51
|
-
from falyx.themes
|
51
|
+
from falyx.themes import OneColors
|
52
52
|
from falyx.utils import ensure_async
|
53
53
|
|
54
54
|
|
@@ -4,13 +4,13 @@ from typing import Any
|
|
4
4
|
|
5
5
|
from rich.tree import Tree
|
6
6
|
|
7
|
-
from falyx.action import BaseAction
|
7
|
+
from falyx.action.action import BaseAction
|
8
8
|
from falyx.context import ExecutionContext
|
9
9
|
from falyx.execution_registry import ExecutionRegistry as er
|
10
10
|
from falyx.hook_manager import HookType
|
11
11
|
from falyx.logger import logger
|
12
12
|
from falyx.protocols import ActionFactoryProtocol
|
13
|
-
from falyx.themes
|
13
|
+
from falyx.themes import OneColors
|
14
14
|
|
15
15
|
|
16
16
|
class ActionFactoryAction(BaseAction):
|
@@ -13,11 +13,11 @@ from typing import Any
|
|
13
13
|
import aiohttp
|
14
14
|
from rich.tree import Tree
|
15
15
|
|
16
|
-
from falyx.action import Action
|
16
|
+
from falyx.action.action import Action
|
17
17
|
from falyx.context import ExecutionContext, SharedContext
|
18
18
|
from falyx.hook_manager import HookManager, HookType
|
19
19
|
from falyx.logger import logger
|
20
|
-
from falyx.themes
|
20
|
+
from falyx.themes import OneColors
|
21
21
|
|
22
22
|
|
23
23
|
async def close_shared_http_session(context: ExecutionContext) -> None:
|
@@ -23,13 +23,13 @@ from typing import Any
|
|
23
23
|
|
24
24
|
from rich.tree import Tree
|
25
25
|
|
26
|
-
from falyx.action import BaseAction
|
26
|
+
from falyx.action.action import BaseAction
|
27
27
|
from falyx.context import ExecutionContext
|
28
28
|
from falyx.exceptions import FalyxError
|
29
29
|
from falyx.execution_registry import ExecutionRegistry as er
|
30
30
|
from falyx.hook_manager import HookManager, HookType
|
31
31
|
from falyx.logger import logger
|
32
|
-
from falyx.themes
|
32
|
+
from falyx.themes import OneColors
|
33
33
|
|
34
34
|
|
35
35
|
class BaseIOAction(BaseAction):
|
@@ -1,6 +1,5 @@
|
|
1
1
|
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
2
2
|
"""menu_action.py"""
|
3
|
-
from dataclasses import dataclass
|
4
3
|
from typing import Any
|
5
4
|
|
6
5
|
from prompt_toolkit import PromptSession
|
@@ -8,91 +7,16 @@ from rich.console import Console
|
|
8
7
|
from rich.table import Table
|
9
8
|
from rich.tree import Tree
|
10
9
|
|
11
|
-
from falyx.action import BaseAction
|
10
|
+
from falyx.action.action import BaseAction
|
12
11
|
from falyx.context import ExecutionContext
|
13
12
|
from falyx.execution_registry import ExecutionRegistry as er
|
14
13
|
from falyx.hook_manager import HookType
|
15
14
|
from falyx.logger import logger
|
15
|
+
from falyx.menu import MenuOptionMap
|
16
16
|
from falyx.selection import prompt_for_selection, render_table_base
|
17
|
-
from falyx.signal_action import SignalAction
|
18
17
|
from falyx.signals import BackSignal, QuitSignal
|
19
|
-
from falyx.themes
|
20
|
-
from falyx.utils import
|
21
|
-
|
22
|
-
|
23
|
-
@dataclass
|
24
|
-
class MenuOption:
|
25
|
-
"""Represents a single menu option with a description and an action to execute."""
|
26
|
-
|
27
|
-
description: str
|
28
|
-
action: BaseAction
|
29
|
-
style: str = OneColors.WHITE
|
30
|
-
|
31
|
-
def __post_init__(self):
|
32
|
-
if not isinstance(self.description, str):
|
33
|
-
raise TypeError("MenuOption description must be a string.")
|
34
|
-
if not isinstance(self.action, BaseAction):
|
35
|
-
raise TypeError("MenuOption action must be a BaseAction instance.")
|
36
|
-
|
37
|
-
def render(self, key: str) -> str:
|
38
|
-
"""Render the menu option for display."""
|
39
|
-
return f"[{OneColors.WHITE}][{key}][/] [{self.style}]{self.description}[/]"
|
40
|
-
|
41
|
-
|
42
|
-
class MenuOptionMap(CaseInsensitiveDict):
|
43
|
-
"""
|
44
|
-
Manages menu options including validation, reserved key protection,
|
45
|
-
and special signal entries like Quit and Back.
|
46
|
-
"""
|
47
|
-
|
48
|
-
RESERVED_KEYS = {"Q", "B"}
|
49
|
-
|
50
|
-
def __init__(
|
51
|
-
self,
|
52
|
-
options: dict[str, MenuOption] | None = None,
|
53
|
-
allow_reserved: bool = False,
|
54
|
-
):
|
55
|
-
super().__init__()
|
56
|
-
self.allow_reserved = allow_reserved
|
57
|
-
if options:
|
58
|
-
self.update(options)
|
59
|
-
self._inject_reserved_defaults()
|
60
|
-
|
61
|
-
def _inject_reserved_defaults(self):
|
62
|
-
self._add_reserved(
|
63
|
-
"Q",
|
64
|
-
MenuOption("Exit", SignalAction("Quit", QuitSignal()), OneColors.DARK_RED),
|
65
|
-
)
|
66
|
-
self._add_reserved(
|
67
|
-
"B",
|
68
|
-
MenuOption("Back", SignalAction("Back", BackSignal()), OneColors.DARK_YELLOW),
|
69
|
-
)
|
70
|
-
|
71
|
-
def _add_reserved(self, key: str, option: MenuOption) -> None:
|
72
|
-
"""Add a reserved key, bypassing validation."""
|
73
|
-
norm_key = key.upper()
|
74
|
-
super().__setitem__(norm_key, option)
|
75
|
-
|
76
|
-
def __setitem__(self, key: str, option: MenuOption) -> None:
|
77
|
-
if not isinstance(option, MenuOption):
|
78
|
-
raise TypeError(f"Value for key '{key}' must be a MenuOption.")
|
79
|
-
norm_key = key.upper()
|
80
|
-
if norm_key in self.RESERVED_KEYS and not self.allow_reserved:
|
81
|
-
raise ValueError(
|
82
|
-
f"Key '{key}' is reserved and cannot be used in MenuOptionMap."
|
83
|
-
)
|
84
|
-
super().__setitem__(norm_key, option)
|
85
|
-
|
86
|
-
def __delitem__(self, key: str) -> None:
|
87
|
-
if key.upper() in self.RESERVED_KEYS and not self.allow_reserved:
|
88
|
-
raise ValueError(f"Cannot delete reserved option '{key}'.")
|
89
|
-
super().__delitem__(key)
|
90
|
-
|
91
|
-
def items(self, include_reserved: bool = True):
|
92
|
-
for k, v in super().items():
|
93
|
-
if not include_reserved and k in self.RESERVED_KEYS:
|
94
|
-
continue
|
95
|
-
yield k, v
|
18
|
+
from falyx.themes import OneColors
|
19
|
+
from falyx.utils import chunks
|
96
20
|
|
97
21
|
|
98
22
|
class MenuAction(BaseAction):
|
@@ -5,7 +5,6 @@ from __future__ import annotations
|
|
5
5
|
import csv
|
6
6
|
import json
|
7
7
|
import xml.etree.ElementTree as ET
|
8
|
-
from enum import Enum
|
9
8
|
from pathlib import Path
|
10
9
|
from typing import Any
|
11
10
|
|
@@ -15,7 +14,8 @@ from prompt_toolkit import PromptSession
|
|
15
14
|
from rich.console import Console
|
16
15
|
from rich.tree import Tree
|
17
16
|
|
18
|
-
from falyx.action import BaseAction
|
17
|
+
from falyx.action.action import BaseAction
|
18
|
+
from falyx.action.types import FileReturnType
|
19
19
|
from falyx.context import ExecutionContext
|
20
20
|
from falyx.execution_registry import ExecutionRegistry as er
|
21
21
|
from falyx.hook_manager import HookType
|
@@ -25,41 +25,7 @@ from falyx.selection import (
|
|
25
25
|
prompt_for_selection,
|
26
26
|
render_selection_dict_table,
|
27
27
|
)
|
28
|
-
from falyx.themes
|
29
|
-
|
30
|
-
|
31
|
-
class FileReturnType(Enum):
|
32
|
-
"""Enum for file return types."""
|
33
|
-
|
34
|
-
TEXT = "text"
|
35
|
-
PATH = "path"
|
36
|
-
JSON = "json"
|
37
|
-
TOML = "toml"
|
38
|
-
YAML = "yaml"
|
39
|
-
CSV = "csv"
|
40
|
-
TSV = "tsv"
|
41
|
-
XML = "xml"
|
42
|
-
|
43
|
-
@classmethod
|
44
|
-
def _get_alias(cls, value: str) -> str:
|
45
|
-
aliases = {
|
46
|
-
"yml": "yaml",
|
47
|
-
"txt": "text",
|
48
|
-
"file": "path",
|
49
|
-
"filepath": "path",
|
50
|
-
}
|
51
|
-
return aliases.get(value, value)
|
52
|
-
|
53
|
-
@classmethod
|
54
|
-
def _missing_(cls, value: object) -> FileReturnType:
|
55
|
-
if isinstance(value, str):
|
56
|
-
normalized = value.lower()
|
57
|
-
alias = cls._get_alias(normalized)
|
58
|
-
for member in cls:
|
59
|
-
if member.value == alias:
|
60
|
-
return member
|
61
|
-
valid = ", ".join(member.value for member in cls)
|
62
|
-
raise ValueError(f"Invalid FileReturnType: '{value}'. Must be one of: {valid}")
|
28
|
+
from falyx.themes import OneColors
|
63
29
|
|
64
30
|
|
65
31
|
class SelectFileAction(BaseAction):
|
@@ -6,7 +6,7 @@ from prompt_toolkit import PromptSession
|
|
6
6
|
from rich.console import Console
|
7
7
|
from rich.tree import Tree
|
8
8
|
|
9
|
-
from falyx.action import BaseAction
|
9
|
+
from falyx.action.action import BaseAction
|
10
10
|
from falyx.context import ExecutionContext
|
11
11
|
from falyx.execution_registry import ExecutionRegistry as er
|
12
12
|
from falyx.hook_manager import HookType
|
@@ -18,7 +18,7 @@ from falyx.selection import (
|
|
18
18
|
render_selection_dict_table,
|
19
19
|
render_selection_indexed_table,
|
20
20
|
)
|
21
|
-
from falyx.themes
|
21
|
+
from falyx.themes import OneColors
|
22
22
|
from falyx.utils import CaseInsensitiveDict
|
23
23
|
|
24
24
|
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
2
|
+
"""signal_action.py"""
|
3
|
+
from rich.tree import Tree
|
4
|
+
|
5
|
+
from falyx.action.action import Action
|
6
|
+
from falyx.signals import FlowSignal
|
7
|
+
from falyx.themes import OneColors
|
8
|
+
|
9
|
+
|
10
|
+
class SignalAction(Action):
|
11
|
+
"""
|
12
|
+
An action that raises a control flow signal when executed.
|
13
|
+
|
14
|
+
Useful for exiting a menu, going back, or halting execution gracefully.
|
15
|
+
"""
|
16
|
+
|
17
|
+
def __init__(self, name: str, signal: Exception):
|
18
|
+
self.signal = signal
|
19
|
+
super().__init__(name, action=self.raise_signal)
|
20
|
+
|
21
|
+
async def raise_signal(self, *args, **kwargs):
|
22
|
+
raise self.signal
|
23
|
+
|
24
|
+
@property
|
25
|
+
def signal(self):
|
26
|
+
return self._signal
|
27
|
+
|
28
|
+
@signal.setter
|
29
|
+
def signal(self, value: FlowSignal):
|
30
|
+
if not isinstance(value, FlowSignal):
|
31
|
+
raise TypeError(
|
32
|
+
f"Signal must be an FlowSignal instance, got {type(value).__name__}"
|
33
|
+
)
|
34
|
+
self._signal = value
|
35
|
+
|
36
|
+
def __str__(self):
|
37
|
+
return f"SignalAction(name={self.name}, signal={self._signal.__class__.__name__})"
|
38
|
+
|
39
|
+
async def preview(self, parent: Tree | None = None):
|
40
|
+
label = f"[{OneColors.LIGHT_RED}]⚡ SignalAction[/] '{self.signal.__class__.__name__}'"
|
41
|
+
tree = parent.add(label) if parent else Tree(label)
|
42
|
+
if not parent:
|
43
|
+
self.console.print(tree)
|
@@ -0,0 +1,37 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from enum import Enum
|
4
|
+
|
5
|
+
|
6
|
+
class FileReturnType(Enum):
|
7
|
+
"""Enum for file return types."""
|
8
|
+
|
9
|
+
TEXT = "text"
|
10
|
+
PATH = "path"
|
11
|
+
JSON = "json"
|
12
|
+
TOML = "toml"
|
13
|
+
YAML = "yaml"
|
14
|
+
CSV = "csv"
|
15
|
+
TSV = "tsv"
|
16
|
+
XML = "xml"
|
17
|
+
|
18
|
+
@classmethod
|
19
|
+
def _get_alias(cls, value: str) -> str:
|
20
|
+
aliases = {
|
21
|
+
"yml": "yaml",
|
22
|
+
"txt": "text",
|
23
|
+
"file": "path",
|
24
|
+
"filepath": "path",
|
25
|
+
}
|
26
|
+
return aliases.get(value, value)
|
27
|
+
|
28
|
+
@classmethod
|
29
|
+
def _missing_(cls, value: object) -> FileReturnType:
|
30
|
+
if isinstance(value, str):
|
31
|
+
normalized = value.lower()
|
32
|
+
alias = cls._get_alias(normalized)
|
33
|
+
for member in cls:
|
34
|
+
if member.value == alias:
|
35
|
+
return member
|
36
|
+
valid = ", ".join(member.value for member in cls)
|
37
|
+
raise ValueError(f"Invalid FileReturnType: '{value}'. Must be one of: {valid}")
|
@@ -8,7 +8,7 @@ from prompt_toolkit.key_binding import KeyBindings
|
|
8
8
|
from rich.console import Console
|
9
9
|
|
10
10
|
from falyx.options_manager import OptionsManager
|
11
|
-
from falyx.themes
|
11
|
+
from falyx.themes import OneColors
|
12
12
|
from falyx.utils import CaseInsensitiveDict, chunks
|
13
13
|
|
14
14
|
|
@@ -26,19 +26,19 @@ from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator
|
|
26
26
|
from rich.console import Console
|
27
27
|
from rich.tree import Tree
|
28
28
|
|
29
|
-
from falyx.action import Action, ActionGroup, BaseAction, ChainedAction
|
29
|
+
from falyx.action.action import Action, ActionGroup, BaseAction, ChainedAction
|
30
|
+
from falyx.action.io_action import BaseIOAction
|
30
31
|
from falyx.context import ExecutionContext
|
31
32
|
from falyx.debug import register_debug_hooks
|
32
33
|
from falyx.exceptions import FalyxError
|
33
34
|
from falyx.execution_registry import ExecutionRegistry as er
|
34
35
|
from falyx.hook_manager import HookManager, HookType
|
35
|
-
from falyx.io_action import BaseIOAction
|
36
36
|
from falyx.logger import logger
|
37
37
|
from falyx.options_manager import OptionsManager
|
38
38
|
from falyx.prompt_utils import confirm_async, should_prompt_user
|
39
39
|
from falyx.retry import RetryPolicy
|
40
40
|
from falyx.retry_utils import enable_retries_recursively
|
41
|
-
from falyx.themes
|
41
|
+
from falyx.themes import OneColors
|
42
42
|
from falyx.utils import _noop, ensure_async
|
43
43
|
|
44
44
|
console = Console(color_system="auto")
|
@@ -13,12 +13,12 @@ import yaml
|
|
13
13
|
from pydantic import BaseModel, Field, field_validator, model_validator
|
14
14
|
from rich.console import Console
|
15
15
|
|
16
|
-
from falyx.action import Action, BaseAction
|
16
|
+
from falyx.action.action import Action, BaseAction
|
17
17
|
from falyx.command import Command
|
18
18
|
from falyx.falyx import Falyx
|
19
19
|
from falyx.logger import logger
|
20
20
|
from falyx.retry import RetryPolicy
|
21
|
-
from falyx.themes
|
21
|
+
from falyx.themes import OneColors
|
22
22
|
|
23
23
|
console = Console(color_system="auto")
|
24
24
|
|
@@ -72,10 +72,10 @@ class RawCommand(BaseModel):
|
|
72
72
|
description: str
|
73
73
|
action: str
|
74
74
|
|
75
|
-
args: tuple[Any, ...] = ()
|
76
|
-
kwargs: dict[str, Any] =
|
77
|
-
aliases: list[str] =
|
78
|
-
tags: list[str] =
|
75
|
+
args: tuple[Any, ...] = Field(default_factory=tuple)
|
76
|
+
kwargs: dict[str, Any] = Field(default_factory=dict)
|
77
|
+
aliases: list[str] = Field(default_factory=list)
|
78
|
+
tags: list[str] = Field(default_factory=list)
|
79
79
|
style: str = OneColors.WHITE
|
80
80
|
|
81
81
|
confirm: bool = False
|
@@ -86,13 +86,13 @@ class RawCommand(BaseModel):
|
|
86
86
|
spinner_message: str = "Processing..."
|
87
87
|
spinner_type: str = "dots"
|
88
88
|
spinner_style: str = OneColors.CYAN
|
89
|
-
spinner_kwargs: dict[str, Any] =
|
89
|
+
spinner_kwargs: dict[str, Any] = Field(default_factory=dict)
|
90
90
|
|
91
|
-
before_hooks: list[Callable] =
|
92
|
-
success_hooks: list[Callable] =
|
93
|
-
error_hooks: list[Callable] =
|
94
|
-
after_hooks: list[Callable] =
|
95
|
-
teardown_hooks: list[Callable] =
|
91
|
+
before_hooks: list[Callable] = Field(default_factory=list)
|
92
|
+
success_hooks: list[Callable] = Field(default_factory=list)
|
93
|
+
error_hooks: list[Callable] = Field(default_factory=list)
|
94
|
+
after_hooks: list[Callable] = Field(default_factory=list)
|
95
|
+
teardown_hooks: list[Callable] = Field(default_factory=list)
|
96
96
|
|
97
97
|
logging_hooks: bool = False
|
98
98
|
retry: bool = False
|
@@ -129,6 +129,60 @@ def convert_commands(raw_commands: list[dict[str, Any]]) -> list[Command]:
|
|
129
129
|
return commands
|
130
130
|
|
131
131
|
|
132
|
+
def convert_submenus(
|
133
|
+
raw_submenus: list[dict[str, Any]], *, parent_path: Path | None = None, depth: int = 0
|
134
|
+
) -> list[dict[str, Any]]:
|
135
|
+
submenus: list[dict[str, Any]] = []
|
136
|
+
for raw_submenu in raw_submenus:
|
137
|
+
if raw_submenu.get("config"):
|
138
|
+
config_path = Path(raw_submenu["config"])
|
139
|
+
if parent_path:
|
140
|
+
config_path = (parent_path.parent / config_path).resolve()
|
141
|
+
submenu = loader(config_path, _depth=depth + 1)
|
142
|
+
else:
|
143
|
+
submenu_module_path = raw_submenu.get("submenu")
|
144
|
+
if not isinstance(submenu_module_path, str):
|
145
|
+
console.print(
|
146
|
+
f"[{OneColors.DARK_RED}]❌ Invalid submenu path:[/] {submenu_module_path}"
|
147
|
+
)
|
148
|
+
sys.exit(1)
|
149
|
+
submenu = import_action(submenu_module_path)
|
150
|
+
if not isinstance(submenu, Falyx):
|
151
|
+
console.print(f"[{OneColors.DARK_RED}]❌ Invalid submenu:[/] {submenu}")
|
152
|
+
sys.exit(1)
|
153
|
+
|
154
|
+
key = raw_submenu.get("key")
|
155
|
+
if not isinstance(key, str):
|
156
|
+
console.print(f"[{OneColors.DARK_RED}]❌ Invalid submenu key:[/] {key}")
|
157
|
+
sys.exit(1)
|
158
|
+
|
159
|
+
description = raw_submenu.get("description")
|
160
|
+
if not isinstance(description, str):
|
161
|
+
console.print(
|
162
|
+
f"[{OneColors.DARK_RED}]❌ Invalid submenu description:[/] {description}"
|
163
|
+
)
|
164
|
+
sys.exit(1)
|
165
|
+
|
166
|
+
submenus.append(
|
167
|
+
Submenu(
|
168
|
+
key=key,
|
169
|
+
description=description,
|
170
|
+
submenu=submenu,
|
171
|
+
style=raw_submenu.get("style", OneColors.CYAN),
|
172
|
+
).model_dump()
|
173
|
+
)
|
174
|
+
return submenus
|
175
|
+
|
176
|
+
|
177
|
+
class Submenu(BaseModel):
|
178
|
+
"""Submenu model for Falyx CLI configuration."""
|
179
|
+
|
180
|
+
key: str
|
181
|
+
description: str
|
182
|
+
submenu: Any
|
183
|
+
style: str = OneColors.CYAN
|
184
|
+
|
185
|
+
|
132
186
|
class FalyxConfig(BaseModel):
|
133
187
|
"""Falyx CLI configuration model."""
|
134
188
|
|
@@ -140,6 +194,7 @@ class FalyxConfig(BaseModel):
|
|
140
194
|
welcome_message: str = ""
|
141
195
|
exit_message: str = ""
|
142
196
|
commands: list[Command] | list[dict] = []
|
197
|
+
submenus: list[dict[str, Any]] = []
|
143
198
|
|
144
199
|
@model_validator(mode="after")
|
145
200
|
def validate_prompt_format(self) -> FalyxConfig:
|
@@ -160,10 +215,12 @@ class FalyxConfig(BaseModel):
|
|
160
215
|
exit_message=self.exit_message,
|
161
216
|
)
|
162
217
|
flx.add_commands(self.commands)
|
218
|
+
for submenu in self.submenus:
|
219
|
+
flx.add_submenu(**submenu)
|
163
220
|
return flx
|
164
221
|
|
165
222
|
|
166
|
-
def loader(file_path: Path | str) -> Falyx:
|
223
|
+
def loader(file_path: Path | str, _depth: int = 0) -> Falyx:
|
167
224
|
"""
|
168
225
|
Load Falyx CLI configuration from a YAML or TOML file.
|
169
226
|
|
@@ -183,6 +240,9 @@ def loader(file_path: Path | str) -> Falyx:
|
|
183
240
|
Raises:
|
184
241
|
ValueError: If the file format is unsupported or file cannot be parsed.
|
185
242
|
"""
|
243
|
+
if _depth > 5:
|
244
|
+
raise ValueError("Maximum submenu depth exceeded (5 levels deep)")
|
245
|
+
|
186
246
|
if isinstance(file_path, (str, Path)):
|
187
247
|
path = Path(file_path)
|
188
248
|
else:
|
@@ -212,6 +272,7 @@ def loader(file_path: Path | str) -> Falyx:
|
|
212
272
|
)
|
213
273
|
|
214
274
|
commands = convert_commands(raw_config["commands"])
|
275
|
+
submenus = convert_submenus(raw_config.get("submenus", []))
|
215
276
|
return FalyxConfig(
|
216
277
|
title=raw_config.get("title", f"[{OneColors.BLUE_b}]Falyx CLI"),
|
217
278
|
prompt=raw_config.get("prompt", [(OneColors.BLUE_b, "FALYX > ")]),
|
@@ -219,4 +280,5 @@ def loader(file_path: Path | str) -> Falyx:
|
|
219
280
|
welcome_message=raw_config.get("welcome_message", ""),
|
220
281
|
exit_message=raw_config.get("exit_message", ""),
|
221
282
|
commands=commands,
|
283
|
+
submenus=submenus,
|
222
284
|
).to_falyx()
|
@@ -19,6 +19,8 @@ for running commands, actions, and workflows. It supports:
|
|
19
19
|
Falyx enables building flexible, robust, and user-friendly
|
20
20
|
terminal applications with minimal boilerplate.
|
21
21
|
"""
|
22
|
+
from __future__ import annotations
|
23
|
+
|
22
24
|
import asyncio
|
23
25
|
import logging
|
24
26
|
import sys
|
@@ -38,7 +40,7 @@ from rich.console import Console
|
|
38
40
|
from rich.markdown import Markdown
|
39
41
|
from rich.table import Table
|
40
42
|
|
41
|
-
from falyx.action import Action, BaseAction
|
43
|
+
from falyx.action.action import Action, BaseAction
|
42
44
|
from falyx.bottom_bar import BottomBar
|
43
45
|
from falyx.command import Command
|
44
46
|
from falyx.context import ExecutionContext
|
@@ -56,7 +58,7 @@ from falyx.options_manager import OptionsManager
|
|
56
58
|
from falyx.parsers import get_arg_parsers
|
57
59
|
from falyx.retry import RetryPolicy
|
58
60
|
from falyx.signals import BackSignal, QuitSignal
|
59
|
-
from falyx.themes
|
61
|
+
from falyx.themes import OneColors, get_nord_theme
|
60
62
|
from falyx.utils import CaseInsensitiveDict, chunks, get_program_invocation
|
61
63
|
from falyx.version import __version__
|
62
64
|
|
@@ -528,14 +530,15 @@ class Falyx:
|
|
528
530
|
)
|
529
531
|
|
530
532
|
def add_submenu(
|
531
|
-
self, key: str, description: str, submenu:
|
533
|
+
self, key: str, description: str, submenu: Falyx, *, style: str = OneColors.CYAN
|
532
534
|
) -> None:
|
533
535
|
"""Adds a submenu to the menu."""
|
534
536
|
if not isinstance(submenu, Falyx):
|
535
537
|
raise NotAFalyxError("submenu must be an instance of Falyx.")
|
536
538
|
self._validate_command_key(key)
|
537
539
|
self.add_command(key, description, submenu.menu, style=style)
|
538
|
-
submenu.
|
540
|
+
if submenu.exit_command.key == "Q":
|
541
|
+
submenu.update_exit_command(key="B", description="Back", aliases=["BACK"])
|
539
542
|
|
540
543
|
def add_commands(self, commands: list[Command] | list[dict]) -> None:
|
541
544
|
"""Adds a list of Command instances or config dicts."""
|
@@ -6,7 +6,7 @@ from typing import Any, Callable
|
|
6
6
|
from falyx.context import ExecutionContext
|
7
7
|
from falyx.exceptions import CircuitBreakerOpen
|
8
8
|
from falyx.logger import logger
|
9
|
-
from falyx.themes
|
9
|
+
from falyx.themes import OneColors
|
10
10
|
|
11
11
|
|
12
12
|
class ResultReporter:
|
@@ -0,0 +1,85 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from dataclasses import dataclass
|
4
|
+
|
5
|
+
from falyx.action import BaseAction
|
6
|
+
from falyx.signals import BackSignal, QuitSignal
|
7
|
+
from falyx.themes import OneColors
|
8
|
+
from falyx.utils import CaseInsensitiveDict
|
9
|
+
|
10
|
+
|
11
|
+
@dataclass
|
12
|
+
class MenuOption:
|
13
|
+
"""Represents a single menu option with a description and an action to execute."""
|
14
|
+
|
15
|
+
description: str
|
16
|
+
action: BaseAction
|
17
|
+
style: str = OneColors.WHITE
|
18
|
+
|
19
|
+
def __post_init__(self):
|
20
|
+
if not isinstance(self.description, str):
|
21
|
+
raise TypeError("MenuOption description must be a string.")
|
22
|
+
if not isinstance(self.action, BaseAction):
|
23
|
+
raise TypeError("MenuOption action must be a BaseAction instance.")
|
24
|
+
|
25
|
+
def render(self, key: str) -> str:
|
26
|
+
"""Render the menu option for display."""
|
27
|
+
return f"[{OneColors.WHITE}][{key}][/] [{self.style}]{self.description}[/]"
|
28
|
+
|
29
|
+
|
30
|
+
class MenuOptionMap(CaseInsensitiveDict):
|
31
|
+
"""
|
32
|
+
Manages menu options including validation, reserved key protection,
|
33
|
+
and special signal entries like Quit and Back.
|
34
|
+
"""
|
35
|
+
|
36
|
+
RESERVED_KEYS = {"Q", "B"}
|
37
|
+
|
38
|
+
def __init__(
|
39
|
+
self,
|
40
|
+
options: dict[str, MenuOption] | None = None,
|
41
|
+
allow_reserved: bool = False,
|
42
|
+
):
|
43
|
+
super().__init__()
|
44
|
+
self.allow_reserved = allow_reserved
|
45
|
+
if options:
|
46
|
+
self.update(options)
|
47
|
+
self._inject_reserved_defaults()
|
48
|
+
|
49
|
+
def _inject_reserved_defaults(self):
|
50
|
+
from falyx.action import SignalAction
|
51
|
+
|
52
|
+
self._add_reserved(
|
53
|
+
"Q",
|
54
|
+
MenuOption("Exit", SignalAction("Quit", QuitSignal()), OneColors.DARK_RED),
|
55
|
+
)
|
56
|
+
self._add_reserved(
|
57
|
+
"B",
|
58
|
+
MenuOption("Back", SignalAction("Back", BackSignal()), OneColors.DARK_YELLOW),
|
59
|
+
)
|
60
|
+
|
61
|
+
def _add_reserved(self, key: str, option: MenuOption) -> None:
|
62
|
+
"""Add a reserved key, bypassing validation."""
|
63
|
+
norm_key = key.upper()
|
64
|
+
super().__setitem__(norm_key, option)
|
65
|
+
|
66
|
+
def __setitem__(self, key: str, option: MenuOption) -> None:
|
67
|
+
if not isinstance(option, MenuOption):
|
68
|
+
raise TypeError(f"Value for key '{key}' must be a MenuOption.")
|
69
|
+
norm_key = key.upper()
|
70
|
+
if norm_key in self.RESERVED_KEYS and not self.allow_reserved:
|
71
|
+
raise ValueError(
|
72
|
+
f"Key '{key}' is reserved and cannot be used in MenuOptionMap."
|
73
|
+
)
|
74
|
+
super().__setitem__(norm_key, option)
|
75
|
+
|
76
|
+
def __delitem__(self, key: str) -> None:
|
77
|
+
if key.upper() in self.RESERVED_KEYS and not self.allow_reserved:
|
78
|
+
raise ValueError(f"Cannot delete reserved option '{key}'.")
|
79
|
+
super().__delitem__(key)
|
80
|
+
|
81
|
+
def items(self, include_reserved: bool = True):
|
82
|
+
for k, v in super().items():
|
83
|
+
if not include_reserved and k in self.RESERVED_KEYS:
|
84
|
+
continue
|
85
|
+
yield k, v
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
2
2
|
"""retry_utils.py"""
|
3
|
-
from falyx.action import Action, BaseAction
|
3
|
+
from falyx.action.action import Action, BaseAction
|
4
4
|
from falyx.hook_manager import HookType
|
5
5
|
from falyx.retry import RetryHandler, RetryPolicy
|
6
6
|
|
@@ -9,7 +9,7 @@ from rich.console import Console
|
|
9
9
|
from rich.markup import escape
|
10
10
|
from rich.table import Table
|
11
11
|
|
12
|
-
from falyx.themes
|
12
|
+
from falyx.themes import OneColors
|
13
13
|
from falyx.utils import chunks
|
14
14
|
from falyx.validators import int_range_validator, key_validator
|
15
15
|
|
@@ -0,0 +1,15 @@
|
|
1
|
+
"""
|
2
|
+
Falyx CLI Framework
|
3
|
+
|
4
|
+
Copyright (c) 2025 rtj.dev LLC.
|
5
|
+
Licensed under the MIT License. See LICENSE file for details.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from .colors import ColorsMeta, NordColors, OneColors, get_nord_theme
|
9
|
+
|
10
|
+
__all__ = [
|
11
|
+
"OneColors",
|
12
|
+
"NordColors",
|
13
|
+
"get_nord_theme",
|
14
|
+
"ColorsMeta",
|
15
|
+
]
|
@@ -0,0 +1 @@
|
|
1
|
+
__version__ = "0.1.26"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "falyx"
|
3
|
-
version = "0.1.
|
3
|
+
version = "0.1.26"
|
4
4
|
description = "Reliable and introspectable async CLI action framework."
|
5
5
|
authors = ["Roland Thomas Jr <roland@rtj.dev>"]
|
6
6
|
license = "MIT"
|
@@ -15,6 +15,7 @@ pydantic = "^2.0"
|
|
15
15
|
python-json-logger = "^3.3.0"
|
16
16
|
toml = "^0.10"
|
17
17
|
pyyaml = "^6.0"
|
18
|
+
aiohttp = "^3.11"
|
18
19
|
|
19
20
|
[tool.poetry.group.dev.dependencies]
|
20
21
|
pytest = "^8.3.5"
|
@@ -1,31 +0,0 @@
|
|
1
|
-
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
2
|
-
"""signal_action.py"""
|
3
|
-
from falyx.action import Action
|
4
|
-
from falyx.signals import FlowSignal
|
5
|
-
|
6
|
-
|
7
|
-
class SignalAction(Action):
|
8
|
-
"""
|
9
|
-
An action that raises a control flow signal when executed.
|
10
|
-
|
11
|
-
Useful for exiting a menu, going back, or halting execution gracefully.
|
12
|
-
"""
|
13
|
-
|
14
|
-
def __init__(self, name: str, signal: Exception):
|
15
|
-
if not isinstance(signal, FlowSignal):
|
16
|
-
raise TypeError(
|
17
|
-
f"Signal must be an FlowSignal instance, got {type(signal).__name__}"
|
18
|
-
)
|
19
|
-
|
20
|
-
async def raise_signal(*args, **kwargs):
|
21
|
-
raise signal
|
22
|
-
|
23
|
-
super().__init__(name=name, action=raise_signal)
|
24
|
-
self._signal = signal
|
25
|
-
|
26
|
-
@property
|
27
|
-
def signal(self):
|
28
|
-
return self._signal
|
29
|
-
|
30
|
-
def __str__(self):
|
31
|
-
return f"SignalAction(name={self.name}, signal={self._signal.__class__.__name__})"
|
falyx-0.1.24/falyx/version.py
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
__version__ = "0.1.24"
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|