Create action submodule, add various examples
This commit is contained in:
parent
87a56ac40b
commit
2bdca72e04
|
@ -0,0 +1,48 @@
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from falyx import Falyx
|
||||||
|
from falyx.action import ActionFactoryAction, ChainedAction, HTTPAction, SelectionAction
|
||||||
|
|
||||||
|
# Selection of a post ID to fetch (just an example set)
|
||||||
|
post_selector = SelectionAction(
|
||||||
|
name="Pick Post ID",
|
||||||
|
selections=["1", "2", "3", "4", "5"],
|
||||||
|
title="Choose a Post ID to submit",
|
||||||
|
prompt_message="Post ID > ",
|
||||||
|
show_table=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Factory that builds and executes the actual HTTP POST request
|
||||||
|
def build_post_action(post_id) -> HTTPAction:
|
||||||
|
print(f"Building HTTPAction for Post ID: {post_id}")
|
||||||
|
return HTTPAction(
|
||||||
|
name=f"POST to /posts (id={post_id})",
|
||||||
|
method="POST",
|
||||||
|
url="https://jsonplaceholder.typicode.com/posts",
|
||||||
|
json={"title": "foo", "body": "bar", "userId": int(post_id)},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
post_factory = ActionFactoryAction(
|
||||||
|
name="Build HTTPAction from Post ID",
|
||||||
|
factory=build_post_action,
|
||||||
|
inject_last_result=True,
|
||||||
|
inject_into="post_id",
|
||||||
|
preview_kwargs={"post_id": "100"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Wrap in a ChainedAction
|
||||||
|
chain = ChainedAction(
|
||||||
|
name="Submit Post Flow",
|
||||||
|
actions=[post_selector, post_factory],
|
||||||
|
auto_inject=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
flx = Falyx()
|
||||||
|
flx.add_command(
|
||||||
|
key="S",
|
||||||
|
description="Submit a Post",
|
||||||
|
action=chain,
|
||||||
|
)
|
||||||
|
asyncio.run(flx.run())
|
|
@ -0,0 +1,10 @@
|
||||||
|
"""config_loading.py"""
|
||||||
|
|
||||||
|
from falyx.config import loader
|
||||||
|
|
||||||
|
flx = loader("falyx.yaml")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
asyncio.run(flx.run())
|
|
@ -0,0 +1,22 @@
|
||||||
|
commands:
|
||||||
|
- key: P
|
||||||
|
description: Pipeline Demo
|
||||||
|
action: pipeline_demo.pipeline
|
||||||
|
tags: [pipeline, demo]
|
||||||
|
help_text: Run Demployment Pipeline with retries.
|
||||||
|
|
||||||
|
- key: G
|
||||||
|
description: Run HTTP Action Group
|
||||||
|
action: http_demo.action_group
|
||||||
|
tags: [http, demo]
|
||||||
|
|
||||||
|
- key: S
|
||||||
|
description: Select a file
|
||||||
|
action: file_select.sf
|
||||||
|
tags: [file, select, demo]
|
||||||
|
|
||||||
|
- key: M
|
||||||
|
description: Menu Demo
|
||||||
|
action: menu_demo.menu
|
||||||
|
tags: [menu, demo]
|
||||||
|
help_text: Run a menu demo with multiple options.
|
|
@ -0,0 +1,171 @@
|
||||||
|
"""
|
||||||
|
Falyx CLI Framework
|
||||||
|
|
||||||
|
Copyright (c) 2025 rtj.dev LLC.
|
||||||
|
Licensed under the MIT License. See LICENSE file for details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import random
|
||||||
|
from argparse import Namespace
|
||||||
|
|
||||||
|
from falyx.action import Action, ActionGroup, ChainedAction
|
||||||
|
from falyx.falyx import Falyx
|
||||||
|
from falyx.parsers import FalyxParsers, get_arg_parsers
|
||||||
|
from falyx.version import __version__
|
||||||
|
|
||||||
|
|
||||||
|
class Foo:
|
||||||
|
def __init__(self, flx: Falyx) -> None:
|
||||||
|
self.flx = flx
|
||||||
|
|
||||||
|
async def build(self):
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
print("✅ Build complete!")
|
||||||
|
return "Build complete!"
|
||||||
|
|
||||||
|
async def test(self):
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
print("✅ Tests passed!")
|
||||||
|
return "Tests passed!"
|
||||||
|
|
||||||
|
async def deploy(self):
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
print("✅ Deployment complete!")
|
||||||
|
return "Deployment complete!"
|
||||||
|
|
||||||
|
async def clean(self):
|
||||||
|
print("🧹 Cleaning...")
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
print("✅ Clean complete!")
|
||||||
|
return "Clean complete!"
|
||||||
|
|
||||||
|
async def build_package(self):
|
||||||
|
print("🔨 Building...")
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
print("✅ Build finished!")
|
||||||
|
return "Build finished!"
|
||||||
|
|
||||||
|
async def package(self):
|
||||||
|
print("📦 Packaging...")
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
print("✅ Package complete!")
|
||||||
|
return "Package complete!"
|
||||||
|
|
||||||
|
async def run_tests(self):
|
||||||
|
print("🧪 Running tests...")
|
||||||
|
await asyncio.sleep(random.randint(1, 3))
|
||||||
|
print("✅ Tests passed!")
|
||||||
|
return "Tests passed!"
|
||||||
|
|
||||||
|
async def run_integration_tests(self):
|
||||||
|
print("🔗 Running integration tests...")
|
||||||
|
await asyncio.sleep(random.randint(1, 3))
|
||||||
|
print("✅ Integration tests passed!")
|
||||||
|
return "Integration tests passed!"
|
||||||
|
|
||||||
|
async def run_linter(self):
|
||||||
|
print("🧹 Running linter...")
|
||||||
|
await asyncio.sleep(random.randint(1, 3))
|
||||||
|
print("✅ Linter passed!")
|
||||||
|
return "Linter passed!"
|
||||||
|
|
||||||
|
async def run(self):
|
||||||
|
await self.flx.run()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> Namespace:
|
||||||
|
parsers: FalyxParsers = get_arg_parsers()
|
||||||
|
return parsers.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
"""Build and return a Falyx instance with all your commands."""
|
||||||
|
args = parse_args()
|
||||||
|
flx = Falyx(
|
||||||
|
title="🚀 Falyx CLI",
|
||||||
|
cli_args=args,
|
||||||
|
columns=5,
|
||||||
|
welcome_message="Welcome to Falyx CLI!",
|
||||||
|
exit_message="Goodbye!",
|
||||||
|
)
|
||||||
|
foo = Foo(flx)
|
||||||
|
|
||||||
|
# --- Bottom bar info ---
|
||||||
|
flx.bottom_bar.columns = 3
|
||||||
|
flx.bottom_bar.add_toggle_from_option("V", "Verbose", flx.options, "verbose")
|
||||||
|
flx.bottom_bar.add_toggle_from_option("U", "Debug Hooks", flx.options, "debug_hooks")
|
||||||
|
flx.bottom_bar.add_static("Version", f"Falyx v{__version__}")
|
||||||
|
|
||||||
|
# --- Command actions ---
|
||||||
|
|
||||||
|
# --- Single Actions ---
|
||||||
|
flx.add_command(
|
||||||
|
key="B",
|
||||||
|
description="Build project",
|
||||||
|
action=Action("Build", foo.build),
|
||||||
|
tags=["build"],
|
||||||
|
spinner=True,
|
||||||
|
spinner_message="📦 Building...",
|
||||||
|
)
|
||||||
|
flx.add_command(
|
||||||
|
key="T",
|
||||||
|
description="Run tests",
|
||||||
|
action=Action("Test", foo.test),
|
||||||
|
tags=["test"],
|
||||||
|
spinner=True,
|
||||||
|
spinner_message="🧪 Running tests...",
|
||||||
|
)
|
||||||
|
flx.add_command(
|
||||||
|
key="D",
|
||||||
|
description="Deploy project",
|
||||||
|
action=Action("Deploy", foo.deploy),
|
||||||
|
tags=["deploy"],
|
||||||
|
spinner=True,
|
||||||
|
spinner_message="🚀 Deploying...",
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Build pipeline (ChainedAction) ---
|
||||||
|
pipeline = ChainedAction(
|
||||||
|
name="Full Build Pipeline",
|
||||||
|
actions=[
|
||||||
|
Action("Clean", foo.clean),
|
||||||
|
Action("Build", foo.build_package),
|
||||||
|
Action("Package", foo.package),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
flx.add_command(
|
||||||
|
key="P",
|
||||||
|
description="Run Build Pipeline",
|
||||||
|
action=pipeline,
|
||||||
|
tags=["build", "pipeline"],
|
||||||
|
spinner=True,
|
||||||
|
spinner_message="🔨 Running build pipeline...",
|
||||||
|
spinner_type="line",
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Test suite (ActionGroup) ---
|
||||||
|
test_suite = ActionGroup(
|
||||||
|
name="Test Suite",
|
||||||
|
actions=[
|
||||||
|
Action("Unit Tests", foo.run_tests),
|
||||||
|
Action("Integration Tests", foo.run_integration_tests),
|
||||||
|
Action("Lint", foo.run_linter),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
flx.add_command(
|
||||||
|
key="G",
|
||||||
|
description="Run All Tests",
|
||||||
|
action=test_suite,
|
||||||
|
tags=["test", "parallel"],
|
||||||
|
spinner=True,
|
||||||
|
spinner_type="line",
|
||||||
|
)
|
||||||
|
await foo.run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
asyncio.run(main())
|
||||||
|
except (KeyboardInterrupt, EOFError):
|
||||||
|
pass
|
|
@ -0,0 +1,26 @@
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from falyx import Falyx
|
||||||
|
from falyx.action import SelectFileAction
|
||||||
|
from falyx.action.types import FileReturnType
|
||||||
|
|
||||||
|
sf = SelectFileAction(
|
||||||
|
name="select_file",
|
||||||
|
suffix_filter=".py",
|
||||||
|
title="Select a YAML file",
|
||||||
|
prompt_message="Choose > ",
|
||||||
|
return_type=FileReturnType.TEXT,
|
||||||
|
columns=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
flx = Falyx()
|
||||||
|
|
||||||
|
flx.add_command(
|
||||||
|
key="S",
|
||||||
|
description="Select a file",
|
||||||
|
action=sf,
|
||||||
|
help_text="Select a file from the current directory",
|
||||||
|
)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(flx.run())
|
|
@ -0,0 +1,67 @@
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
from falyx import ActionGroup, Falyx
|
||||||
|
from falyx.action import HTTPAction
|
||||||
|
from falyx.hook_manager import HookType
|
||||||
|
from falyx.hooks import ResultReporter
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
|
action_group = ActionGroup(
|
||||||
|
"HTTP Group",
|
||||||
|
actions=[
|
||||||
|
HTTPAction(
|
||||||
|
name="Get Example",
|
||||||
|
method="GET",
|
||||||
|
url="https://jsonplaceholder.typicode.com/posts/1",
|
||||||
|
headers={"Accept": "application/json"},
|
||||||
|
retry=True,
|
||||||
|
),
|
||||||
|
HTTPAction(
|
||||||
|
name="Post Example",
|
||||||
|
method="POST",
|
||||||
|
url="https://jsonplaceholder.typicode.com/posts",
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
json={"title": "foo", "body": "bar", "userId": 1},
|
||||||
|
retry=True,
|
||||||
|
),
|
||||||
|
HTTPAction(
|
||||||
|
name="Put Example",
|
||||||
|
method="PUT",
|
||||||
|
url="https://jsonplaceholder.typicode.com/posts/1",
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
json={"id": 1, "title": "foo", "body": "bar", "userId": 1},
|
||||||
|
retry=True,
|
||||||
|
),
|
||||||
|
HTTPAction(
|
||||||
|
name="Delete Example",
|
||||||
|
method="DELETE",
|
||||||
|
url="https://jsonplaceholder.typicode.com/posts/1",
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
retry=True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
reporter = ResultReporter()
|
||||||
|
|
||||||
|
action_group.hooks.register(
|
||||||
|
HookType.ON_SUCCESS,
|
||||||
|
reporter.report,
|
||||||
|
)
|
||||||
|
|
||||||
|
flx = Falyx("HTTP Demo")
|
||||||
|
|
||||||
|
flx.add_command(
|
||||||
|
key="G",
|
||||||
|
description="Run HTTP Action Group",
|
||||||
|
action=action_group,
|
||||||
|
spinner=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(flx.run())
|
|
@ -0,0 +1,113 @@
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
|
||||||
|
from falyx import Falyx
|
||||||
|
from falyx.action import Action, ActionGroup, ChainedAction, MenuAction, ProcessAction
|
||||||
|
from falyx.menu import MenuOption, MenuOptionMap
|
||||||
|
|
||||||
|
|
||||||
|
# Basic coroutine for Action
|
||||||
|
async def greet_user():
|
||||||
|
print("👋 Hello from a regular Action!")
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
return "Greeted user."
|
||||||
|
|
||||||
|
|
||||||
|
# Chain of tasks
|
||||||
|
async def fetch_data():
|
||||||
|
print("📡 Fetching data...")
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
return "data123"
|
||||||
|
|
||||||
|
|
||||||
|
async def process_data(last_result):
|
||||||
|
print(f"⚙️ Processing: {last_result}")
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
return f"processed({last_result})"
|
||||||
|
|
||||||
|
|
||||||
|
async def save_data(last_result):
|
||||||
|
print(f"💾 Saving: {last_result}")
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
return f"saved({last_result})"
|
||||||
|
|
||||||
|
|
||||||
|
# Parallel tasks
|
||||||
|
async def fetch_users():
|
||||||
|
print("👥 Fetching users...")
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
return ["alice", "bob", "carol"]
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_logs():
|
||||||
|
print("📝 Fetching logs...")
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
return ["log1", "log2"]
|
||||||
|
|
||||||
|
|
||||||
|
# CPU-bound task (simulate via blocking sleep)
|
||||||
|
def heavy_computation():
|
||||||
|
print("🧠 Starting heavy computation...")
|
||||||
|
time.sleep(3)
|
||||||
|
print("✅ Finished computation.")
|
||||||
|
return 42
|
||||||
|
|
||||||
|
|
||||||
|
# Define actions
|
||||||
|
|
||||||
|
basic_action = Action("greet", greet_user)
|
||||||
|
|
||||||
|
chained = ChainedAction(
|
||||||
|
name="data-pipeline",
|
||||||
|
actions=[
|
||||||
|
Action("fetch", fetch_data),
|
||||||
|
Action("process", process_data, inject_last_result=True),
|
||||||
|
Action("save", save_data, inject_last_result=True),
|
||||||
|
],
|
||||||
|
auto_inject=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
parallel = ActionGroup(
|
||||||
|
name="parallel-fetch",
|
||||||
|
actions=[
|
||||||
|
Action("fetch-users", fetch_users),
|
||||||
|
Action("fetch-logs", fetch_logs),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
process = ProcessAction(name="compute", action=heavy_computation)
|
||||||
|
|
||||||
|
|
||||||
|
# Menu setup
|
||||||
|
|
||||||
|
menu = MenuAction(
|
||||||
|
name="main-menu",
|
||||||
|
title="Choose a task to run",
|
||||||
|
menu_options=MenuOptionMap(
|
||||||
|
{
|
||||||
|
"1": MenuOption("Run basic Action", basic_action),
|
||||||
|
"2": MenuOption("Run ChainedAction", chained),
|
||||||
|
"3": MenuOption("Run ActionGroup (parallel)", parallel),
|
||||||
|
"4": MenuOption("Run ProcessAction (heavy task)", process),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
flx = Falyx(
|
||||||
|
title="🚀 Falyx Menu Demo",
|
||||||
|
welcome_message="Welcome to the Menu Demo!",
|
||||||
|
exit_message="Goodbye!",
|
||||||
|
columns=2,
|
||||||
|
never_prompt=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
flx.add_command(
|
||||||
|
key="M",
|
||||||
|
description="Show Menu",
|
||||||
|
action=menu,
|
||||||
|
logging_hooks=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(flx.run())
|
|
@ -0,0 +1,78 @@
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from falyx import Action, ActionGroup, ChainedAction
|
||||||
|
from falyx import ExecutionRegistry as er
|
||||||
|
from falyx import ProcessAction
|
||||||
|
from falyx.hook_manager import HookType
|
||||||
|
from falyx.retry import RetryHandler, RetryPolicy
|
||||||
|
|
||||||
|
|
||||||
|
# Step 1: Fast I/O-bound setup (standard Action)
|
||||||
|
async def checkout_code():
|
||||||
|
print("📥 Checking out code...")
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
|
|
||||||
|
# Step 2: CPU-bound task (ProcessAction)
|
||||||
|
def run_static_analysis():
|
||||||
|
print("🧠 Running static analysis (CPU-bound)...")
|
||||||
|
total = 0
|
||||||
|
for i in range(10_000_000):
|
||||||
|
total += i % 3
|
||||||
|
return total
|
||||||
|
|
||||||
|
|
||||||
|
# Step 3: Simulated flaky test with retry
|
||||||
|
async def flaky_tests():
|
||||||
|
import random
|
||||||
|
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
if random.random() < 0.3:
|
||||||
|
raise RuntimeError("❌ Random test failure!")
|
||||||
|
print("🧪 Tests passed.")
|
||||||
|
return "ok"
|
||||||
|
|
||||||
|
|
||||||
|
# Step 4: Multiple deploy targets (parallel ActionGroup)
|
||||||
|
async def deploy_to(target: str):
|
||||||
|
print(f"🚀 Deploying to {target}...")
|
||||||
|
await asyncio.sleep(0.2)
|
||||||
|
return f"{target} complete"
|
||||||
|
|
||||||
|
|
||||||
|
def build_pipeline():
|
||||||
|
retry_handler = RetryHandler(RetryPolicy(max_retries=3, delay=0.5))
|
||||||
|
|
||||||
|
# Base actions
|
||||||
|
checkout = Action("Checkout", checkout_code)
|
||||||
|
analysis = ProcessAction("Static Analysis", run_static_analysis)
|
||||||
|
tests = Action("Run Tests", flaky_tests)
|
||||||
|
tests.hooks.register(HookType.ON_ERROR, retry_handler.retry_on_error)
|
||||||
|
|
||||||
|
# Parallel deploys
|
||||||
|
deploy_group = ActionGroup(
|
||||||
|
"Deploy to All",
|
||||||
|
[
|
||||||
|
Action("Deploy US", deploy_to, args=("us-west",)),
|
||||||
|
Action("Deploy EU", deploy_to, args=("eu-central",)),
|
||||||
|
Action("Deploy Asia", deploy_to, args=("asia-east",)),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Full pipeline
|
||||||
|
return ChainedAction("CI/CD Pipeline", [checkout, analysis, tests, deploy_group])
|
||||||
|
|
||||||
|
|
||||||
|
pipeline = build_pipeline()
|
||||||
|
|
||||||
|
|
||||||
|
# Run the pipeline
|
||||||
|
async def main():
|
||||||
|
pipeline = build_pipeline()
|
||||||
|
await pipeline()
|
||||||
|
er.summary()
|
||||||
|
await pipeline.preview()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
|
@ -1,7 +1,7 @@
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
|
||||||
from falyx import Falyx, ProcessAction
|
from falyx import Falyx, ProcessAction
|
||||||
from falyx.themes.colors import NordColors as nc
|
from falyx.themes import NordColors as nc
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
falyx = Falyx(title="🚀 Process Pool Demo")
|
falyx = Falyx(title="🚀 Process Pool Demo")
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from falyx.selection import (
|
||||||
|
SelectionOption,
|
||||||
|
prompt_for_selection,
|
||||||
|
render_selection_dict_table,
|
||||||
|
)
|
||||||
|
|
||||||
|
menu = {
|
||||||
|
"A": SelectionOption("Run diagnostics", lambda: print("Running diagnostics...")),
|
||||||
|
"B": SelectionOption("Deploy to staging", lambda: print("Deploying...")),
|
||||||
|
}
|
||||||
|
|
||||||
|
table = render_selection_dict_table(
|
||||||
|
title="Main Menu",
|
||||||
|
selections=menu,
|
||||||
|
)
|
||||||
|
|
||||||
|
key = asyncio.run(prompt_for_selection(menu.keys(), table))
|
||||||
|
print(f"You selected: {key}")
|
||||||
|
|
||||||
|
menu[key.upper()].value()
|
|
@ -0,0 +1,90 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from falyx import Action, ChainedAction, Falyx
|
||||||
|
from falyx.action import ShellAction
|
||||||
|
from falyx.hook_manager import HookType
|
||||||
|
from falyx.hooks import ResultReporter
|
||||||
|
from falyx.utils import setup_logging
|
||||||
|
|
||||||
|
# Setup logging
|
||||||
|
setup_logging()
|
||||||
|
|
||||||
|
|
||||||
|
fx = Falyx("🚀 Falyx Demo")
|
||||||
|
|
||||||
|
e = ShellAction("Shell", "echo Hello, {}!")
|
||||||
|
|
||||||
|
fx.add_command(
|
||||||
|
key="R",
|
||||||
|
description="Echo a message",
|
||||||
|
action=e,
|
||||||
|
)
|
||||||
|
|
||||||
|
s = ShellAction("Ping", "ping -c 1 {}")
|
||||||
|
|
||||||
|
fx.add_command(
|
||||||
|
key="P",
|
||||||
|
description="Ping a host",
|
||||||
|
action=s,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def a1(last_result):
|
||||||
|
return f"Hello, {last_result}"
|
||||||
|
|
||||||
|
|
||||||
|
async def a2(last_result):
|
||||||
|
return f"World! {last_result}"
|
||||||
|
|
||||||
|
|
||||||
|
reporter = ResultReporter()
|
||||||
|
|
||||||
|
a1 = Action("a1", a1, inject_last_result=True)
|
||||||
|
a1.hooks.register(
|
||||||
|
HookType.ON_SUCCESS,
|
||||||
|
reporter.report,
|
||||||
|
)
|
||||||
|
a2 = Action("a2", a2, inject_last_result=True)
|
||||||
|
a2.hooks.register(
|
||||||
|
HookType.ON_SUCCESS,
|
||||||
|
reporter.report,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def normal():
|
||||||
|
print("Normal")
|
||||||
|
return "Normal"
|
||||||
|
|
||||||
|
|
||||||
|
async def annotate(last_result):
|
||||||
|
return f"Annotated: {last_result}"
|
||||||
|
|
||||||
|
|
||||||
|
async def whisper(last_result):
|
||||||
|
return last_result.lower()
|
||||||
|
|
||||||
|
|
||||||
|
c1 = ChainedAction(
|
||||||
|
name="ShellDemo",
|
||||||
|
actions=[
|
||||||
|
# host,
|
||||||
|
ShellAction("Ping", "ping -c 1 {}"),
|
||||||
|
Action("Annotate", annotate),
|
||||||
|
Action("Whisper", whisper),
|
||||||
|
],
|
||||||
|
auto_inject=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
fx.add_command(
|
||||||
|
key="C",
|
||||||
|
description="Run a chain of actions",
|
||||||
|
action=c1,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
await fx.run()
|
||||||
|
|
||||||
|
|
||||||
|
asyncio.run(main())
|
|
@ -0,0 +1,52 @@
|
||||||
|
import asyncio
|
||||||
|
import random
|
||||||
|
|
||||||
|
from falyx import Action, ChainedAction, Falyx
|
||||||
|
from falyx.utils import setup_logging
|
||||||
|
|
||||||
|
setup_logging()
|
||||||
|
|
||||||
|
|
||||||
|
# A flaky async step that fails randomly
|
||||||
|
async def flaky_step():
|
||||||
|
await asyncio.sleep(0.2)
|
||||||
|
if random.random() < 0.5:
|
||||||
|
raise RuntimeError("Random failure!")
|
||||||
|
return "ok"
|
||||||
|
|
||||||
|
|
||||||
|
step1 = Action(name="step_1", action=flaky_step, retry=True)
|
||||||
|
step2 = Action(name="step_2", action=flaky_step, retry=True)
|
||||||
|
|
||||||
|
# Chain the actions
|
||||||
|
chain = ChainedAction(name="my_pipeline", actions=[step1, step2])
|
||||||
|
|
||||||
|
# Create the CLI menu
|
||||||
|
falyx = Falyx("🚀 Falyx Demo")
|
||||||
|
falyx.add_command(
|
||||||
|
key="R",
|
||||||
|
description="Run My Pipeline",
|
||||||
|
action=chain,
|
||||||
|
logging_hooks=True,
|
||||||
|
preview_before_confirm=True,
|
||||||
|
confirm=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a submenu
|
||||||
|
submenu = Falyx("Submenu")
|
||||||
|
submenu.add_command(
|
||||||
|
key="T",
|
||||||
|
description="Test",
|
||||||
|
action=lambda: "test",
|
||||||
|
logging_hooks=True,
|
||||||
|
preview_before_confirm=True,
|
||||||
|
confirm=True,
|
||||||
|
)
|
||||||
|
falyx.add_submenu(
|
||||||
|
key="S",
|
||||||
|
description="Submenu",
|
||||||
|
submenu=submenu,
|
||||||
|
)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(falyx.run())
|
|
@ -7,7 +7,7 @@ Licensed under the MIT License. See LICENSE file for details.
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from .action import Action, ActionGroup, ChainedAction, ProcessAction
|
from .action.action import Action, ActionGroup, ChainedAction, ProcessAction
|
||||||
from .command import Command
|
from .command import Command
|
||||||
from .context import ExecutionContext, SharedContext
|
from .context import ExecutionContext, SharedContext
|
||||||
from .execution_registry import ExecutionRegistry
|
from .execution_registry import ExecutionRegistry
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
"""
|
||||||
|
Falyx CLI Framework
|
||||||
|
|
||||||
|
Copyright (c) 2025 rtj.dev LLC.
|
||||||
|
Licensed under the MIT License. See LICENSE file for details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .action import (
|
||||||
|
Action,
|
||||||
|
ActionGroup,
|
||||||
|
BaseAction,
|
||||||
|
ChainedAction,
|
||||||
|
FallbackAction,
|
||||||
|
LiteralInputAction,
|
||||||
|
ProcessAction,
|
||||||
|
)
|
||||||
|
from .action_factory import ActionFactoryAction
|
||||||
|
from .http_action import HTTPAction
|
||||||
|
from .io_action import BaseIOAction, ShellAction
|
||||||
|
from .menu_action import MenuAction
|
||||||
|
from .select_file_action import SelectFileAction
|
||||||
|
from .selection_action import SelectionAction
|
||||||
|
from .signal_action import SignalAction
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Action",
|
||||||
|
"ActionGroup",
|
||||||
|
"BaseAction",
|
||||||
|
"ChainedAction",
|
||||||
|
"ProcessAction",
|
||||||
|
"ActionFactoryAction",
|
||||||
|
"HTTPAction",
|
||||||
|
"BaseIOAction",
|
||||||
|
"ShellAction",
|
||||||
|
"SelectionAction",
|
||||||
|
"SelectFileAction",
|
||||||
|
"MenuAction",
|
||||||
|
"SignalAction",
|
||||||
|
"FallbackAction",
|
||||||
|
"LiteralInputAction",
|
||||||
|
]
|
|
@ -48,7 +48,7 @@ from falyx.hook_manager import Hook, HookManager, HookType
|
||||||
from falyx.logger import logger
|
from falyx.logger import logger
|
||||||
from falyx.options_manager import OptionsManager
|
from falyx.options_manager import OptionsManager
|
||||||
from falyx.retry import RetryHandler, RetryPolicy
|
from falyx.retry import RetryHandler, RetryPolicy
|
||||||
from falyx.themes.colors import OneColors
|
from falyx.themes import OneColors
|
||||||
from falyx.utils import ensure_async
|
from falyx.utils import ensure_async
|
||||||
|
|
||||||
|
|
|
@ -4,13 +4,13 @@ from typing import Any
|
||||||
|
|
||||||
from rich.tree import Tree
|
from rich.tree import Tree
|
||||||
|
|
||||||
from falyx.action import BaseAction
|
from falyx.action.action import BaseAction
|
||||||
from falyx.context import ExecutionContext
|
from falyx.context import ExecutionContext
|
||||||
from falyx.execution_registry import ExecutionRegistry as er
|
from falyx.execution_registry import ExecutionRegistry as er
|
||||||
from falyx.hook_manager import HookType
|
from falyx.hook_manager import HookType
|
||||||
from falyx.logger import logger
|
from falyx.logger import logger
|
||||||
from falyx.protocols import ActionFactoryProtocol
|
from falyx.protocols import ActionFactoryProtocol
|
||||||
from falyx.themes.colors import OneColors
|
from falyx.themes import OneColors
|
||||||
|
|
||||||
|
|
||||||
class ActionFactoryAction(BaseAction):
|
class ActionFactoryAction(BaseAction):
|
|
@ -13,11 +13,11 @@ from typing import Any
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from rich.tree import Tree
|
from rich.tree import Tree
|
||||||
|
|
||||||
from falyx.action import Action
|
from falyx.action.action import Action
|
||||||
from falyx.context import ExecutionContext, SharedContext
|
from falyx.context import ExecutionContext, SharedContext
|
||||||
from falyx.hook_manager import HookManager, HookType
|
from falyx.hook_manager import HookManager, HookType
|
||||||
from falyx.logger import logger
|
from falyx.logger import logger
|
||||||
from falyx.themes.colors import OneColors
|
from falyx.themes import OneColors
|
||||||
|
|
||||||
|
|
||||||
async def close_shared_http_session(context: ExecutionContext) -> None:
|
async def close_shared_http_session(context: ExecutionContext) -> None:
|
|
@ -23,13 +23,13 @@ from typing import Any
|
||||||
|
|
||||||
from rich.tree import Tree
|
from rich.tree import Tree
|
||||||
|
|
||||||
from falyx.action import BaseAction
|
from falyx.action.action import BaseAction
|
||||||
from falyx.context import ExecutionContext
|
from falyx.context import ExecutionContext
|
||||||
from falyx.exceptions import FalyxError
|
from falyx.exceptions import FalyxError
|
||||||
from falyx.execution_registry import ExecutionRegistry as er
|
from falyx.execution_registry import ExecutionRegistry as er
|
||||||
from falyx.hook_manager import HookManager, HookType
|
from falyx.hook_manager import HookManager, HookType
|
||||||
from falyx.logger import logger
|
from falyx.logger import logger
|
||||||
from falyx.themes.colors import OneColors
|
from falyx.themes import OneColors
|
||||||
|
|
||||||
|
|
||||||
class BaseIOAction(BaseAction):
|
class BaseIOAction(BaseAction):
|
|
@ -1,6 +1,5 @@
|
||||||
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
||||||
"""menu_action.py"""
|
"""menu_action.py"""
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from prompt_toolkit import PromptSession
|
from prompt_toolkit import PromptSession
|
||||||
|
@ -8,91 +7,16 @@ from rich.console import Console
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
from rich.tree import Tree
|
from rich.tree import Tree
|
||||||
|
|
||||||
from falyx.action import BaseAction
|
from falyx.action.action import BaseAction
|
||||||
from falyx.context import ExecutionContext
|
from falyx.context import ExecutionContext
|
||||||
from falyx.execution_registry import ExecutionRegistry as er
|
from falyx.execution_registry import ExecutionRegistry as er
|
||||||
from falyx.hook_manager import HookType
|
from falyx.hook_manager import HookType
|
||||||
from falyx.logger import logger
|
from falyx.logger import logger
|
||||||
|
from falyx.menu import MenuOptionMap
|
||||||
from falyx.selection import prompt_for_selection, render_table_base
|
from falyx.selection import prompt_for_selection, render_table_base
|
||||||
from falyx.signal_action import SignalAction
|
|
||||||
from falyx.signals import BackSignal, QuitSignal
|
from falyx.signals import BackSignal, QuitSignal
|
||||||
from falyx.themes.colors import OneColors
|
from falyx.themes import OneColors
|
||||||
from falyx.utils import CaseInsensitiveDict, chunks
|
from falyx.utils import chunks
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class MenuOption:
|
|
||||||
"""Represents a single menu option with a description and an action to execute."""
|
|
||||||
|
|
||||||
description: str
|
|
||||||
action: BaseAction
|
|
||||||
style: str = OneColors.WHITE
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
if not isinstance(self.description, str):
|
|
||||||
raise TypeError("MenuOption description must be a string.")
|
|
||||||
if not isinstance(self.action, BaseAction):
|
|
||||||
raise TypeError("MenuOption action must be a BaseAction instance.")
|
|
||||||
|
|
||||||
def render(self, key: str) -> str:
|
|
||||||
"""Render the menu option for display."""
|
|
||||||
return f"[{OneColors.WHITE}][{key}][/] [{self.style}]{self.description}[/]"
|
|
||||||
|
|
||||||
|
|
||||||
class MenuOptionMap(CaseInsensitiveDict):
|
|
||||||
"""
|
|
||||||
Manages menu options including validation, reserved key protection,
|
|
||||||
and special signal entries like Quit and Back.
|
|
||||||
"""
|
|
||||||
|
|
||||||
RESERVED_KEYS = {"Q", "B"}
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
options: dict[str, MenuOption] | None = None,
|
|
||||||
allow_reserved: bool = False,
|
|
||||||
):
|
|
||||||
super().__init__()
|
|
||||||
self.allow_reserved = allow_reserved
|
|
||||||
if options:
|
|
||||||
self.update(options)
|
|
||||||
self._inject_reserved_defaults()
|
|
||||||
|
|
||||||
def _inject_reserved_defaults(self):
|
|
||||||
self._add_reserved(
|
|
||||||
"Q",
|
|
||||||
MenuOption("Exit", SignalAction("Quit", QuitSignal()), OneColors.DARK_RED),
|
|
||||||
)
|
|
||||||
self._add_reserved(
|
|
||||||
"B",
|
|
||||||
MenuOption("Back", SignalAction("Back", BackSignal()), OneColors.DARK_YELLOW),
|
|
||||||
)
|
|
||||||
|
|
||||||
def _add_reserved(self, key: str, option: MenuOption) -> None:
|
|
||||||
"""Add a reserved key, bypassing validation."""
|
|
||||||
norm_key = key.upper()
|
|
||||||
super().__setitem__(norm_key, option)
|
|
||||||
|
|
||||||
def __setitem__(self, key: str, option: MenuOption) -> None:
|
|
||||||
if not isinstance(option, MenuOption):
|
|
||||||
raise TypeError(f"Value for key '{key}' must be a MenuOption.")
|
|
||||||
norm_key = key.upper()
|
|
||||||
if norm_key in self.RESERVED_KEYS and not self.allow_reserved:
|
|
||||||
raise ValueError(
|
|
||||||
f"Key '{key}' is reserved and cannot be used in MenuOptionMap."
|
|
||||||
)
|
|
||||||
super().__setitem__(norm_key, option)
|
|
||||||
|
|
||||||
def __delitem__(self, key: str) -> None:
|
|
||||||
if key.upper() in self.RESERVED_KEYS and not self.allow_reserved:
|
|
||||||
raise ValueError(f"Cannot delete reserved option '{key}'.")
|
|
||||||
super().__delitem__(key)
|
|
||||||
|
|
||||||
def items(self, include_reserved: bool = True):
|
|
||||||
for k, v in super().items():
|
|
||||||
if not include_reserved and k in self.RESERVED_KEYS:
|
|
||||||
continue
|
|
||||||
yield k, v
|
|
||||||
|
|
||||||
|
|
||||||
class MenuAction(BaseAction):
|
class MenuAction(BaseAction):
|
|
@ -5,7 +5,6 @@ from __future__ import annotations
|
||||||
import csv
|
import csv
|
||||||
import json
|
import json
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from enum import Enum
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
@ -15,7 +14,8 @@ from prompt_toolkit import PromptSession
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.tree import Tree
|
from rich.tree import Tree
|
||||||
|
|
||||||
from falyx.action import BaseAction
|
from falyx.action.action import BaseAction
|
||||||
|
from falyx.action.types import FileReturnType
|
||||||
from falyx.context import ExecutionContext
|
from falyx.context import ExecutionContext
|
||||||
from falyx.execution_registry import ExecutionRegistry as er
|
from falyx.execution_registry import ExecutionRegistry as er
|
||||||
from falyx.hook_manager import HookType
|
from falyx.hook_manager import HookType
|
||||||
|
@ -25,41 +25,7 @@ from falyx.selection import (
|
||||||
prompt_for_selection,
|
prompt_for_selection,
|
||||||
render_selection_dict_table,
|
render_selection_dict_table,
|
||||||
)
|
)
|
||||||
from falyx.themes.colors import OneColors
|
from falyx.themes import OneColors
|
||||||
|
|
||||||
|
|
||||||
class FileReturnType(Enum):
|
|
||||||
"""Enum for file return types."""
|
|
||||||
|
|
||||||
TEXT = "text"
|
|
||||||
PATH = "path"
|
|
||||||
JSON = "json"
|
|
||||||
TOML = "toml"
|
|
||||||
YAML = "yaml"
|
|
||||||
CSV = "csv"
|
|
||||||
TSV = "tsv"
|
|
||||||
XML = "xml"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _get_alias(cls, value: str) -> str:
|
|
||||||
aliases = {
|
|
||||||
"yml": "yaml",
|
|
||||||
"txt": "text",
|
|
||||||
"file": "path",
|
|
||||||
"filepath": "path",
|
|
||||||
}
|
|
||||||
return aliases.get(value, value)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _missing_(cls, value: object) -> FileReturnType:
|
|
||||||
if isinstance(value, str):
|
|
||||||
normalized = value.lower()
|
|
||||||
alias = cls._get_alias(normalized)
|
|
||||||
for member in cls:
|
|
||||||
if member.value == alias:
|
|
||||||
return member
|
|
||||||
valid = ", ".join(member.value for member in cls)
|
|
||||||
raise ValueError(f"Invalid FileReturnType: '{value}'. Must be one of: {valid}")
|
|
||||||
|
|
||||||
|
|
||||||
class SelectFileAction(BaseAction):
|
class SelectFileAction(BaseAction):
|
|
@ -6,7 +6,7 @@ from prompt_toolkit import PromptSession
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.tree import Tree
|
from rich.tree import Tree
|
||||||
|
|
||||||
from falyx.action import BaseAction
|
from falyx.action.action import BaseAction
|
||||||
from falyx.context import ExecutionContext
|
from falyx.context import ExecutionContext
|
||||||
from falyx.execution_registry import ExecutionRegistry as er
|
from falyx.execution_registry import ExecutionRegistry as er
|
||||||
from falyx.hook_manager import HookType
|
from falyx.hook_manager import HookType
|
||||||
|
@ -18,7 +18,7 @@ from falyx.selection import (
|
||||||
render_selection_dict_table,
|
render_selection_dict_table,
|
||||||
render_selection_indexed_table,
|
render_selection_indexed_table,
|
||||||
)
|
)
|
||||||
from falyx.themes.colors import OneColors
|
from falyx.themes import OneColors
|
||||||
from falyx.utils import CaseInsensitiveDict
|
from falyx.utils import CaseInsensitiveDict
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
||||||
|
"""signal_action.py"""
|
||||||
|
from rich.tree import Tree
|
||||||
|
|
||||||
|
from falyx.action.action import Action
|
||||||
|
from falyx.signals import FlowSignal
|
||||||
|
from falyx.themes import OneColors
|
||||||
|
|
||||||
|
|
||||||
|
class SignalAction(Action):
|
||||||
|
"""
|
||||||
|
An action that raises a control flow signal when executed.
|
||||||
|
|
||||||
|
Useful for exiting a menu, going back, or halting execution gracefully.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name: str, signal: Exception):
|
||||||
|
self.signal = signal
|
||||||
|
super().__init__(name, action=self.raise_signal)
|
||||||
|
|
||||||
|
async def raise_signal(self, *args, **kwargs):
|
||||||
|
raise self.signal
|
||||||
|
|
||||||
|
@property
|
||||||
|
def signal(self):
|
||||||
|
return self._signal
|
||||||
|
|
||||||
|
@signal.setter
|
||||||
|
def signal(self, value: FlowSignal):
|
||||||
|
if not isinstance(value, FlowSignal):
|
||||||
|
raise TypeError(
|
||||||
|
f"Signal must be an FlowSignal instance, got {type(value).__name__}"
|
||||||
|
)
|
||||||
|
self._signal = value
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"SignalAction(name={self.name}, signal={self._signal.__class__.__name__})"
|
||||||
|
|
||||||
|
async def preview(self, parent: Tree | None = None):
|
||||||
|
label = f"[{OneColors.LIGHT_RED}]⚡ SignalAction[/] '{self.signal.__class__.__name__}'"
|
||||||
|
tree = parent.add(label) if parent else Tree(label)
|
||||||
|
if not parent:
|
||||||
|
self.console.print(tree)
|
|
@ -0,0 +1,37 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class FileReturnType(Enum):
|
||||||
|
"""Enum for file return types."""
|
||||||
|
|
||||||
|
TEXT = "text"
|
||||||
|
PATH = "path"
|
||||||
|
JSON = "json"
|
||||||
|
TOML = "toml"
|
||||||
|
YAML = "yaml"
|
||||||
|
CSV = "csv"
|
||||||
|
TSV = "tsv"
|
||||||
|
XML = "xml"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_alias(cls, value: str) -> str:
|
||||||
|
aliases = {
|
||||||
|
"yml": "yaml",
|
||||||
|
"txt": "text",
|
||||||
|
"file": "path",
|
||||||
|
"filepath": "path",
|
||||||
|
}
|
||||||
|
return aliases.get(value, value)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _missing_(cls, value: object) -> FileReturnType:
|
||||||
|
if isinstance(value, str):
|
||||||
|
normalized = value.lower()
|
||||||
|
alias = cls._get_alias(normalized)
|
||||||
|
for member in cls:
|
||||||
|
if member.value == alias:
|
||||||
|
return member
|
||||||
|
valid = ", ".join(member.value for member in cls)
|
||||||
|
raise ValueError(f"Invalid FileReturnType: '{value}'. Must be one of: {valid}")
|
|
@ -8,7 +8,7 @@ from prompt_toolkit.key_binding import KeyBindings
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
|
||||||
from falyx.options_manager import OptionsManager
|
from falyx.options_manager import OptionsManager
|
||||||
from falyx.themes.colors import OneColors
|
from falyx.themes import OneColors
|
||||||
from falyx.utils import CaseInsensitiveDict, chunks
|
from falyx.utils import CaseInsensitiveDict, chunks
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -26,19 +26,19 @@ from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.tree import Tree
|
from rich.tree import Tree
|
||||||
|
|
||||||
from falyx.action import Action, ActionGroup, BaseAction, ChainedAction
|
from falyx.action.action import Action, ActionGroup, BaseAction, ChainedAction
|
||||||
|
from falyx.action.io_action import BaseIOAction
|
||||||
from falyx.context import ExecutionContext
|
from falyx.context import ExecutionContext
|
||||||
from falyx.debug import register_debug_hooks
|
from falyx.debug import register_debug_hooks
|
||||||
from falyx.exceptions import FalyxError
|
from falyx.exceptions import FalyxError
|
||||||
from falyx.execution_registry import ExecutionRegistry as er
|
from falyx.execution_registry import ExecutionRegistry as er
|
||||||
from falyx.hook_manager import HookManager, HookType
|
from falyx.hook_manager import HookManager, HookType
|
||||||
from falyx.io_action import BaseIOAction
|
|
||||||
from falyx.logger import logger
|
from falyx.logger import logger
|
||||||
from falyx.options_manager import OptionsManager
|
from falyx.options_manager import OptionsManager
|
||||||
from falyx.prompt_utils import confirm_async, should_prompt_user
|
from falyx.prompt_utils import confirm_async, should_prompt_user
|
||||||
from falyx.retry import RetryPolicy
|
from falyx.retry import RetryPolicy
|
||||||
from falyx.retry_utils import enable_retries_recursively
|
from falyx.retry_utils import enable_retries_recursively
|
||||||
from falyx.themes.colors import OneColors
|
from falyx.themes import OneColors
|
||||||
from falyx.utils import _noop, ensure_async
|
from falyx.utils import _noop, ensure_async
|
||||||
|
|
||||||
console = Console(color_system="auto")
|
console = Console(color_system="auto")
|
||||||
|
|
|
@ -13,12 +13,12 @@ import yaml
|
||||||
from pydantic import BaseModel, Field, field_validator, model_validator
|
from pydantic import BaseModel, Field, field_validator, model_validator
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
|
||||||
from falyx.action import Action, BaseAction
|
from falyx.action.action import Action, BaseAction
|
||||||
from falyx.command import Command
|
from falyx.command import Command
|
||||||
from falyx.falyx import Falyx
|
from falyx.falyx import Falyx
|
||||||
from falyx.logger import logger
|
from falyx.logger import logger
|
||||||
from falyx.retry import RetryPolicy
|
from falyx.retry import RetryPolicy
|
||||||
from falyx.themes.colors import OneColors
|
from falyx.themes import OneColors
|
||||||
|
|
||||||
console = Console(color_system="auto")
|
console = Console(color_system="auto")
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ from rich.table import Table
|
||||||
|
|
||||||
from falyx.context import ExecutionContext
|
from falyx.context import ExecutionContext
|
||||||
from falyx.logger import logger
|
from falyx.logger import logger
|
||||||
from falyx.themes.colors import OneColors
|
from falyx.themes import OneColors
|
||||||
|
|
||||||
|
|
||||||
class ExecutionRegistry:
|
class ExecutionRegistry:
|
||||||
|
|
|
@ -38,7 +38,7 @@ from rich.console import Console
|
||||||
from rich.markdown import Markdown
|
from rich.markdown import Markdown
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
|
|
||||||
from falyx.action import Action, BaseAction
|
from falyx.action.action import Action, BaseAction
|
||||||
from falyx.bottom_bar import BottomBar
|
from falyx.bottom_bar import BottomBar
|
||||||
from falyx.command import Command
|
from falyx.command import Command
|
||||||
from falyx.context import ExecutionContext
|
from falyx.context import ExecutionContext
|
||||||
|
@ -56,7 +56,7 @@ from falyx.options_manager import OptionsManager
|
||||||
from falyx.parsers import get_arg_parsers
|
from falyx.parsers import get_arg_parsers
|
||||||
from falyx.retry import RetryPolicy
|
from falyx.retry import RetryPolicy
|
||||||
from falyx.signals import BackSignal, QuitSignal
|
from falyx.signals import BackSignal, QuitSignal
|
||||||
from falyx.themes.colors import OneColors, get_nord_theme
|
from falyx.themes import OneColors, get_nord_theme
|
||||||
from falyx.utils import CaseInsensitiveDict, chunks, get_program_invocation
|
from falyx.utils import CaseInsensitiveDict, chunks, get_program_invocation
|
||||||
from falyx.version import __version__
|
from falyx.version import __version__
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ from typing import Any, Callable
|
||||||
from falyx.context import ExecutionContext
|
from falyx.context import ExecutionContext
|
||||||
from falyx.exceptions import CircuitBreakerOpen
|
from falyx.exceptions import CircuitBreakerOpen
|
||||||
from falyx.logger import logger
|
from falyx.logger import logger
|
||||||
from falyx.themes.colors import OneColors
|
from falyx.themes import OneColors
|
||||||
|
|
||||||
|
|
||||||
class ResultReporter:
|
class ResultReporter:
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from falyx.action import BaseAction
|
||||||
|
from falyx.signals import BackSignal, QuitSignal
|
||||||
|
from falyx.themes import OneColors
|
||||||
|
from falyx.utils import CaseInsensitiveDict
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MenuOption:
|
||||||
|
"""Represents a single menu option with a description and an action to execute."""
|
||||||
|
|
||||||
|
description: str
|
||||||
|
action: BaseAction
|
||||||
|
style: str = OneColors.WHITE
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
if not isinstance(self.description, str):
|
||||||
|
raise TypeError("MenuOption description must be a string.")
|
||||||
|
if not isinstance(self.action, BaseAction):
|
||||||
|
raise TypeError("MenuOption action must be a BaseAction instance.")
|
||||||
|
|
||||||
|
def render(self, key: str) -> str:
|
||||||
|
"""Render the menu option for display."""
|
||||||
|
return f"[{OneColors.WHITE}][{key}][/] [{self.style}]{self.description}[/]"
|
||||||
|
|
||||||
|
|
||||||
|
class MenuOptionMap(CaseInsensitiveDict):
|
||||||
|
"""
|
||||||
|
Manages menu options including validation, reserved key protection,
|
||||||
|
and special signal entries like Quit and Back.
|
||||||
|
"""
|
||||||
|
|
||||||
|
RESERVED_KEYS = {"Q", "B"}
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
options: dict[str, MenuOption] | None = None,
|
||||||
|
allow_reserved: bool = False,
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
self.allow_reserved = allow_reserved
|
||||||
|
if options:
|
||||||
|
self.update(options)
|
||||||
|
self._inject_reserved_defaults()
|
||||||
|
|
||||||
|
def _inject_reserved_defaults(self):
|
||||||
|
from falyx.action import SignalAction
|
||||||
|
|
||||||
|
self._add_reserved(
|
||||||
|
"Q",
|
||||||
|
MenuOption("Exit", SignalAction("Quit", QuitSignal()), OneColors.DARK_RED),
|
||||||
|
)
|
||||||
|
self._add_reserved(
|
||||||
|
"B",
|
||||||
|
MenuOption("Back", SignalAction("Back", BackSignal()), OneColors.DARK_YELLOW),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _add_reserved(self, key: str, option: MenuOption) -> None:
|
||||||
|
"""Add a reserved key, bypassing validation."""
|
||||||
|
norm_key = key.upper()
|
||||||
|
super().__setitem__(norm_key, option)
|
||||||
|
|
||||||
|
def __setitem__(self, key: str, option: MenuOption) -> None:
|
||||||
|
if not isinstance(option, MenuOption):
|
||||||
|
raise TypeError(f"Value for key '{key}' must be a MenuOption.")
|
||||||
|
norm_key = key.upper()
|
||||||
|
if norm_key in self.RESERVED_KEYS and not self.allow_reserved:
|
||||||
|
raise ValueError(
|
||||||
|
f"Key '{key}' is reserved and cannot be used in MenuOptionMap."
|
||||||
|
)
|
||||||
|
super().__setitem__(norm_key, option)
|
||||||
|
|
||||||
|
def __delitem__(self, key: str) -> None:
|
||||||
|
if key.upper() in self.RESERVED_KEYS and not self.allow_reserved:
|
||||||
|
raise ValueError(f"Cannot delete reserved option '{key}'.")
|
||||||
|
super().__delitem__(key)
|
||||||
|
|
||||||
|
def items(self, include_reserved: bool = True):
|
||||||
|
for k, v in super().items():
|
||||||
|
if not include_reserved and k in self.RESERVED_KEYS:
|
||||||
|
continue
|
||||||
|
yield k, v
|
|
@ -8,7 +8,7 @@ from prompt_toolkit.formatted_text import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from falyx.options_manager import OptionsManager
|
from falyx.options_manager import OptionsManager
|
||||||
from falyx.themes.colors import OneColors
|
from falyx.themes import OneColors
|
||||||
from falyx.validators import yes_no_validator
|
from falyx.validators import yes_no_validator
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Protocol
|
from typing import Any, Protocol
|
||||||
|
|
||||||
from falyx.action import BaseAction
|
from falyx.action.action import BaseAction
|
||||||
|
|
||||||
|
|
||||||
class ActionFactoryProtocol(Protocol):
|
class ActionFactoryProtocol(Protocol):
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
||||||
"""retry_utils.py"""
|
"""retry_utils.py"""
|
||||||
from falyx.action import Action, BaseAction
|
from falyx.action.action import Action, BaseAction
|
||||||
from falyx.hook_manager import HookType
|
from falyx.hook_manager import HookType
|
||||||
from falyx.retry import RetryHandler, RetryPolicy
|
from falyx.retry import RetryHandler, RetryPolicy
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ from rich.console import Console
|
||||||
from rich.markup import escape
|
from rich.markup import escape
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
|
|
||||||
from falyx.themes.colors import OneColors
|
from falyx.themes import OneColors
|
||||||
from falyx.utils import chunks
|
from falyx.utils import chunks
|
||||||
from falyx.validators import int_range_validator, key_validator
|
from falyx.validators import int_range_validator, key_validator
|
||||||
|
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
|
||||||
"""signal_action.py"""
|
|
||||||
from falyx.action import Action
|
|
||||||
from falyx.signals import FlowSignal
|
|
||||||
|
|
||||||
|
|
||||||
class SignalAction(Action):
|
|
||||||
"""
|
|
||||||
An action that raises a control flow signal when executed.
|
|
||||||
|
|
||||||
Useful for exiting a menu, going back, or halting execution gracefully.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, name: str, signal: Exception):
|
|
||||||
if not isinstance(signal, FlowSignal):
|
|
||||||
raise TypeError(
|
|
||||||
f"Signal must be an FlowSignal instance, got {type(signal).__name__}"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def raise_signal(*args, **kwargs):
|
|
||||||
raise signal
|
|
||||||
|
|
||||||
super().__init__(name=name, action=raise_signal)
|
|
||||||
self._signal = signal
|
|
||||||
|
|
||||||
@property
|
|
||||||
def signal(self):
|
|
||||||
return self._signal
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"SignalAction(name={self.name}, signal={self._signal.__class__.__name__})"
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
"""
|
||||||
|
Falyx CLI Framework
|
||||||
|
|
||||||
|
Copyright (c) 2025 rtj.dev LLC.
|
||||||
|
Licensed under the MIT License. See LICENSE file for details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .colors import ColorsMeta, NordColors, OneColors, get_nord_theme
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"OneColors",
|
||||||
|
"NordColors",
|
||||||
|
"get_nord_theme",
|
||||||
|
"ColorsMeta",
|
||||||
|
]
|
|
@ -1 +1 @@
|
||||||
__version__ = "0.1.24"
|
__version__ = "0.1.25"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "falyx"
|
name = "falyx"
|
||||||
version = "0.1.24"
|
version = "0.1.25"
|
||||||
description = "Reliable and introspectable async CLI action framework."
|
description = "Reliable and introspectable async CLI action framework."
|
||||||
authors = ["Roland Thomas Jr <roland@rtj.dev>"]
|
authors = ["Roland Thomas Jr <roland@rtj.dev>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
@ -15,6 +15,7 @@ pydantic = "^2.0"
|
||||||
python-json-logger = "^3.3.0"
|
python-json-logger = "^3.3.0"
|
||||||
toml = "^0.10"
|
toml = "^0.10"
|
||||||
pyyaml = "^6.0"
|
pyyaml = "^6.0"
|
||||||
|
aiohttp = "^3.11"
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
pytest = "^8.3.5"
|
pytest = "^8.3.5"
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
# test_command.py
|
# test_command.py
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from falyx.action import Action, ActionGroup, ChainedAction
|
from falyx.action import Action, ActionGroup, BaseIOAction, ChainedAction
|
||||||
from falyx.command import Command
|
from falyx.command import Command
|
||||||
from falyx.execution_registry import ExecutionRegistry as er
|
from falyx.execution_registry import ExecutionRegistry as er
|
||||||
from falyx.io_action import BaseIOAction
|
|
||||||
from falyx.retry import RetryPolicy
|
from falyx.retry import RetryPolicy
|
||||||
|
|
||||||
asyncio_default_fixture_loop_scope = "function"
|
asyncio_default_fixture_loop_scope = "function"
|
||||||
|
|
Loading…
Reference in New Issue