falyx 0.1.22__py3-none-any.whl → 0.1.24__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
falyx/__main__.py CHANGED
@@ -6,6 +6,7 @@ Licensed under the MIT License. See LICENSE file for details.
6
6
  """
7
7
 
8
8
  import asyncio
9
+ import os
9
10
  import sys
10
11
  from argparse import Namespace
11
12
  from pathlib import Path
@@ -14,7 +15,6 @@ from typing import Any
14
15
  from falyx.config import loader
15
16
  from falyx.falyx import Falyx
16
17
  from falyx.parsers import FalyxParsers, get_arg_parsers
17
- from falyx.themes.colors import OneColors
18
18
 
19
19
 
20
20
  def find_falyx_config() -> Path | None:
@@ -23,6 +23,7 @@ def find_falyx_config() -> Path | None:
23
23
  Path.cwd() / "falyx.toml",
24
24
  Path.cwd() / ".falyx.yaml",
25
25
  Path.cwd() / ".falyx.toml",
26
+ Path(os.environ.get("FALYX_CONFIG", "falyx.yaml")),
26
27
  Path.home() / ".config" / "falyx" / "falyx.yaml",
27
28
  Path.home() / ".config" / "falyx" / "falyx.toml",
28
29
  Path.home() / ".falyx.yaml",
@@ -68,13 +69,7 @@ def run(args: Namespace) -> Any:
68
69
  print("No Falyx config file found. Exiting.")
69
70
  return None
70
71
 
71
- flx = Falyx(
72
- title="🛠️ Config-Driven CLI",
73
- cli_args=args,
74
- columns=4,
75
- prompt=[(OneColors.BLUE_b, "FALYX > ")],
76
- )
77
- flx.add_commands(loader(bootstrap_path))
72
+ flx: Falyx = loader(bootstrap_path)
78
73
  return asyncio.run(flx.run())
79
74
 
80
75
 
falyx/action.py CHANGED
@@ -4,7 +4,8 @@
4
4
  Core action system for Falyx.
5
5
 
6
6
  This module defines the building blocks for executable actions and workflows,
7
- providing a structured way to compose, execute, recover, and manage sequences of operations.
7
+ providing a structured way to compose, execute, recover, and manage sequences of
8
+ operations.
8
9
 
9
10
  All actions are callable and follow a unified signature:
10
11
  result = action(*args, **kwargs)
@@ -14,7 +15,8 @@ Core guarantees:
14
15
  - Consistent timing and execution context tracking for each run.
15
16
  - Unified, predictable result handling and error propagation.
16
17
  - Optional last_result injection to enable flexible, data-driven workflows.
17
- - Built-in support for retries, rollbacks, parallel groups, chaining, and fallback recovery.
18
+ - Built-in support for retries, rollbacks, parallel groups, chaining, and fallback
19
+ recovery.
18
20
 
19
21
  Key components:
20
22
  - Action: wraps a function or coroutine into a standard executable unit.
@@ -43,10 +45,11 @@ from falyx.debug import register_debug_hooks
43
45
  from falyx.exceptions import EmptyChainError
44
46
  from falyx.execution_registry import ExecutionRegistry as er
45
47
  from falyx.hook_manager import Hook, HookManager, HookType
48
+ from falyx.logger import logger
46
49
  from falyx.options_manager import OptionsManager
47
50
  from falyx.retry import RetryHandler, RetryPolicy
48
51
  from falyx.themes.colors import OneColors
49
- from falyx.utils import ensure_async, logger
52
+ from falyx.utils import ensure_async
50
53
 
51
54
 
52
55
  class BaseAction(ABC):
@@ -55,7 +58,8 @@ class BaseAction(ABC):
55
58
  complex actions like `ChainedAction` or `ActionGroup`. They can also
56
59
  be run independently or as part of Falyx.
57
60
 
58
- inject_last_result (bool): Whether to inject the previous action's result into kwargs.
61
+ inject_last_result (bool): Whether to inject the previous action's result
62
+ into kwargs.
59
63
  inject_into (str): The name of the kwarg key to inject the result as
60
64
  (default: 'last_result').
61
65
  _requires_injection (bool): Whether the action requires input injection.
@@ -104,7 +108,9 @@ class BaseAction(ABC):
104
108
  self.shared_context = shared_context
105
109
 
106
110
  def get_option(self, option_name: str, default: Any = None) -> Any:
107
- """Resolve an option from the OptionsManager if present, otherwise use the fallback."""
111
+ """
112
+ Resolve an option from the OptionsManager if present, otherwise use the fallback.
113
+ """
108
114
  if self.options_manager:
109
115
  return self.options_manager.get(option_name, default)
110
116
  return default
@@ -288,8 +294,10 @@ class Action(BaseAction):
288
294
 
289
295
  def __str__(self):
290
296
  return (
291
- f"Action(name={self.name!r}, action={getattr(self._action, '__name__', repr(self._action))}, "
292
- f"args={self.args!r}, kwargs={self.kwargs!r}, retry={self.retry_policy.enabled})"
297
+ f"Action(name={self.name!r}, action="
298
+ f"{getattr(self._action, '__name__', repr(self._action))}, "
299
+ f"args={self.args!r}, kwargs={self.kwargs!r}, "
300
+ f"retry={self.retry_policy.enabled})"
293
301
  )
294
302
 
295
303
 
@@ -309,7 +317,7 @@ class LiteralInputAction(Action):
309
317
  def __init__(self, value: Any):
310
318
  self._value = value
311
319
 
312
- async def literal(*args, **kwargs):
320
+ async def literal(*_, **__):
313
321
  return value
314
322
 
315
323
  super().__init__("Input", literal)
@@ -333,14 +341,16 @@ class LiteralInputAction(Action):
333
341
 
334
342
  class FallbackAction(Action):
335
343
  """
336
- FallbackAction provides a default value if the previous action failed or returned None.
344
+ FallbackAction provides a default value if the previous action failed or
345
+ returned None.
337
346
 
338
347
  It injects the last result and checks:
339
348
  - If last_result is not None, it passes it through unchanged.
340
349
  - If last_result is None (e.g., due to failure), it replaces it with a fallback value.
341
350
 
342
351
  Used in ChainedAction pipelines to gracefully recover from errors or missing data.
343
- When activated, it consumes the preceding error and allows the chain to continue normally.
352
+ When activated, it consumes the preceding error and allows the chain to continue
353
+ normally.
344
354
 
345
355
  Args:
346
356
  fallback (Any): The fallback value to use if last_result is None.
@@ -413,16 +423,19 @@ class ChainedAction(BaseAction, ActionListMixin):
413
423
  - Rolls back all previously executed actions if a failure occurs.
414
424
  - Handles literal values with LiteralInputAction.
415
425
 
416
- Best used for defining robust, ordered workflows where each step can depend on previous results.
426
+ Best used for defining robust, ordered workflows where each step can depend on
427
+ previous results.
417
428
 
418
429
  Args:
419
430
  name (str): Name of the chain.
420
431
  actions (list): List of actions or literals to execute.
421
432
  hooks (HookManager, optional): Hooks for lifecycle events.
422
- inject_last_result (bool, optional): Whether to inject last results into kwargs by default.
433
+ inject_last_result (bool, optional): Whether to inject last results into kwargs
434
+ by default.
423
435
  inject_into (str, optional): Key name for injection.
424
436
  auto_inject (bool, optional): Auto-enable injection for subsequent actions.
425
- return_list (bool, optional): Whether to return a list of all results. False returns the last result.
437
+ return_list (bool, optional): Whether to return a list of all results. False
438
+ returns the last result.
426
439
  """
427
440
 
428
441
  def __init__(
@@ -468,7 +481,7 @@ class ChainedAction(BaseAction, ActionListMixin):
468
481
  if not self.actions:
469
482
  raise EmptyChainError(f"[{self.name}] No actions to execute.")
470
483
 
471
- shared_context = SharedContext(name=self.name)
484
+ shared_context = SharedContext(name=self.name, action=self)
472
485
  if self.shared_context:
473
486
  shared_context.add_result(self.shared_context.last_result())
474
487
  updated_kwargs = self._maybe_inject_last_result(kwargs)
@@ -503,7 +516,8 @@ class ChainedAction(BaseAction, ActionListMixin):
503
516
  self.actions[index + 1], FallbackAction
504
517
  ):
505
518
  logger.warning(
506
- "[%s] ⚠️ Fallback triggered: %s, recovering with fallback '%s'.",
519
+ "[%s] ⚠️ Fallback triggered: %s, recovering with fallback "
520
+ "'%s'.",
507
521
  self.name,
508
522
  error,
509
523
  self.actions[index + 1].name,
@@ -579,7 +593,8 @@ class ChainedAction(BaseAction, ActionListMixin):
579
593
 
580
594
  def __str__(self):
581
595
  return (
582
- f"ChainedAction(name={self.name!r}, actions={[a.name for a in self.actions]!r}, "
596
+ f"ChainedAction(name={self.name!r}, "
597
+ f"actions={[a.name for a in self.actions]!r}, "
583
598
  f"auto_inject={self.auto_inject}, return_list={self.return_list})"
584
599
  )
585
600
 
@@ -613,7 +628,8 @@ class ActionGroup(BaseAction, ActionListMixin):
613
628
  name (str): Name of the chain.
614
629
  actions (list): List of actions or literals to execute.
615
630
  hooks (HookManager, optional): Hooks for lifecycle events.
616
- inject_last_result (bool, optional): Whether to inject last results into kwargs by default.
631
+ inject_last_result (bool, optional): Whether to inject last results into kwargs
632
+ by default.
617
633
  inject_into (str, optional): Key name for injection.
618
634
  """
619
635
 
@@ -643,7 +659,8 @@ class ActionGroup(BaseAction, ActionListMixin):
643
659
  return Action(name=action.__name__, action=action)
644
660
  else:
645
661
  raise TypeError(
646
- f"ActionGroup only accepts BaseAction or callable, got {type(action).__name__}"
662
+ "ActionGroup only accepts BaseAction or callable, got "
663
+ f"{type(action).__name__}"
647
664
  )
648
665
 
649
666
  def add_action(self, action: BaseAction | Any) -> None:
@@ -653,7 +670,7 @@ class ActionGroup(BaseAction, ActionListMixin):
653
670
  action.register_teardown(self.hooks)
654
671
 
655
672
  async def _run(self, *args, **kwargs) -> list[tuple[str, Any]]:
656
- shared_context = SharedContext(name=self.name, is_parallel=True)
673
+ shared_context = SharedContext(name=self.name, action=self, is_parallel=True)
657
674
  if self.shared_context:
658
675
  shared_context.set_shared_result(self.shared_context.last_result())
659
676
  updated_kwargs = self._maybe_inject_last_result(kwargs)
@@ -721,8 +738,8 @@ class ActionGroup(BaseAction, ActionListMixin):
721
738
 
722
739
  def __str__(self):
723
740
  return (
724
- f"ActionGroup(name={self.name!r}, actions={[a.name for a in self.actions]!r}, "
725
- f"inject_last_result={self.inject_last_result})"
741
+ f"ActionGroup(name={self.name!r}, actions={[a.name for a in self.actions]!r},"
742
+ f" inject_last_result={self.inject_last_result})"
726
743
  )
727
744
 
728
745
 
@@ -831,6 +848,7 @@ class ProcessAction(BaseAction):
831
848
 
832
849
  def __str__(self) -> str:
833
850
  return (
834
- f"ProcessAction(name={self.name!r}, action={getattr(self.action, '__name__', repr(self.action))}, "
851
+ f"ProcessAction(name={self.name!r}, "
852
+ f"action={getattr(self.action, '__name__', repr(self.action))}, "
835
853
  f"args={self.args!r}, kwargs={self.kwargs!r})"
836
854
  )
falyx/action_factory.py CHANGED
@@ -1,3 +1,5 @@
1
+ # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
+ """action_factory.py"""
1
3
  from typing import Any
2
4
 
3
5
  from rich.tree import Tree
@@ -6,6 +8,7 @@ from falyx.action import BaseAction
6
8
  from falyx.context import ExecutionContext
7
9
  from falyx.execution_registry import ExecutionRegistry as er
8
10
  from falyx.hook_manager import HookType
11
+ from falyx.logger import logger
9
12
  from falyx.protocols import ActionFactoryProtocol
10
13
  from falyx.themes.colors import OneColors
11
14
 
@@ -32,7 +35,7 @@ class ActionFactoryAction(BaseAction):
32
35
  inject_last_result: bool = False,
33
36
  inject_into: str = "last_result",
34
37
  preview_args: tuple[Any, ...] = (),
35
- preview_kwargs: dict[str, Any] = {},
38
+ preview_kwargs: dict[str, Any] | None = None,
36
39
  ):
37
40
  super().__init__(
38
41
  name=name,
@@ -41,7 +44,7 @@ class ActionFactoryAction(BaseAction):
41
44
  )
42
45
  self.factory = factory
43
46
  self.preview_args = preview_args
44
- self.preview_kwargs = preview_kwargs
47
+ self.preview_kwargs = preview_kwargs or {}
45
48
 
46
49
  async def _run(self, *args, **kwargs) -> Any:
47
50
  updated_kwargs = self._maybe_inject_last_result(kwargs)
@@ -57,10 +60,20 @@ class ActionFactoryAction(BaseAction):
57
60
  generated_action = self.factory(*args, **updated_kwargs)
58
61
  if not isinstance(generated_action, BaseAction):
59
62
  raise TypeError(
60
- f"[{self.name}] Factory must return a BaseAction, got {type(generated_action).__name__}"
63
+ f"[{self.name}] Factory must return a BaseAction, got "
64
+ f"{type(generated_action).__name__}"
61
65
  )
62
66
  if self.shared_context:
63
67
  generated_action.set_shared_context(self.shared_context)
68
+ if hasattr(generated_action, "register_teardown") and callable(
69
+ generated_action.register_teardown
70
+ ):
71
+ generated_action.register_teardown(self.shared_context.action.hooks)
72
+ logger.debug(
73
+ "[%s] Registered teardown for %s",
74
+ self.name,
75
+ generated_action.name,
76
+ )
64
77
  if self.options_manager:
65
78
  generated_action.set_options_manager(self.options_manager)
66
79
  context.result = await generated_action(*args, **kwargs)
falyx/bottom_bar.py CHANGED
@@ -146,7 +146,7 @@ class BottomBar:
146
146
  for k in (key.upper(), key.lower()):
147
147
 
148
148
  @self.key_bindings.add(k)
149
- def _(event):
149
+ def _(_):
150
150
  toggle_state()
151
151
 
152
152
  def add_toggle_from_option(
@@ -204,6 +204,6 @@ class BottomBar:
204
204
  """Render the bottom bar."""
205
205
  lines = []
206
206
  for chunk in chunks(self._named_items.values(), self.columns):
207
- lines.extend([fn for fn in chunk])
207
+ lines.extend(list(chunk))
208
208
  lines.append(lambda: HTML("\n"))
209
209
  return merge_formatted_text([fn() for fn in lines[:-1]])
falyx/command.py CHANGED
@@ -33,12 +33,13 @@ from falyx.exceptions import FalyxError
33
33
  from falyx.execution_registry import ExecutionRegistry as er
34
34
  from falyx.hook_manager import HookManager, HookType
35
35
  from falyx.io_action import BaseIOAction
36
+ from falyx.logger import logger
36
37
  from falyx.options_manager import OptionsManager
37
- from falyx.prompt_utils import should_prompt_user
38
+ from falyx.prompt_utils import confirm_async, should_prompt_user
38
39
  from falyx.retry import RetryPolicy
39
40
  from falyx.retry_utils import enable_retries_recursively
40
41
  from falyx.themes.colors import OneColors
41
- from falyx.utils import _noop, confirm_async, ensure_async, logger
42
+ from falyx.utils import _noop, ensure_async
42
43
 
43
44
  console = Console(color_system="auto")
44
45
 
@@ -134,7 +135,7 @@ class Command(BaseModel):
134
135
  return ensure_async(action)
135
136
  raise TypeError("Action must be a callable or an instance of BaseAction")
136
137
 
137
- def model_post_init(self, __context: Any) -> None:
138
+ def model_post_init(self, _: Any) -> None:
138
139
  """Post-initialization to set up the action and hooks."""
139
140
  if self.retry and isinstance(self.action, Action):
140
141
  self.action.enable_retry()
@@ -142,14 +143,16 @@ class Command(BaseModel):
142
143
  self.action.set_retry_policy(self.retry_policy)
143
144
  elif self.retry:
144
145
  logger.warning(
145
- f"[Command:{self.key}] Retry requested, but action is not an Action instance."
146
+ "[Command:%s] Retry requested, but action is not an Action instance.",
147
+ self.key,
146
148
  )
147
149
  if self.retry_all and isinstance(self.action, BaseAction):
148
150
  self.retry_policy.enabled = True
149
151
  enable_retries_recursively(self.action, self.retry_policy)
150
152
  elif self.retry_all:
151
153
  logger.warning(
152
- f"[Command:{self.key}] Retry all requested, but action is not a BaseAction instance."
154
+ "[Command:%s] Retry all requested, but action is not a BaseAction.",
155
+ self.key,
153
156
  )
154
157
 
155
158
  if self.logging_hooks and isinstance(self.action, BaseAction):
@@ -201,7 +204,7 @@ class Command(BaseModel):
201
204
  if self.preview_before_confirm:
202
205
  await self.preview()
203
206
  if not await confirm_async(self.confirmation_prompt):
204
- logger.info(f"[Command:{self.key}] ❌ Cancelled by user.")
207
+ logger.info("[Command:%s] ❌ Cancelled by user.", self.key)
205
208
  raise FalyxError(f"[Command:{self.key}] Cancelled by confirmation.")
206
209
 
207
210
  context.start_timer()
@@ -288,7 +291,7 @@ class Command(BaseModel):
288
291
  if self.help_text:
289
292
  console.print(f"[dim]💡 {self.help_text}[/dim]")
290
293
  console.print(
291
- f"[{OneColors.DARK_RED}]⚠️ Action is not callable or lacks a preview method.[/]"
294
+ f"[{OneColors.DARK_RED}]⚠️ No preview available for this action.[/]"
292
295
  )
293
296
 
294
297
  def __str__(self) -> str:
falyx/config.py CHANGED
@@ -1,21 +1,24 @@
1
1
  # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
2
  """config.py
3
3
  Configuration loader for Falyx CLI commands."""
4
+ from __future__ import annotations
4
5
 
5
6
  import importlib
6
7
  import sys
7
8
  from pathlib import Path
8
- from typing import Any
9
+ from typing import Any, Callable
9
10
 
10
11
  import toml
11
12
  import yaml
13
+ from pydantic import BaseModel, Field, field_validator, model_validator
12
14
  from rich.console import Console
13
15
 
14
16
  from falyx.action import Action, BaseAction
15
17
  from falyx.command import Command
18
+ from falyx.falyx import Falyx
19
+ from falyx.logger import logger
16
20
  from falyx.retry import RetryPolicy
17
21
  from falyx.themes.colors import OneColors
18
- from falyx.utils import logger
19
22
 
20
23
  console = Console(color_system="auto")
21
24
 
@@ -27,8 +30,8 @@ def wrap_if_needed(obj: Any, name=None) -> BaseAction | Command:
27
30
  return Action(name=name or getattr(obj, "__name__", "unnamed"), action=obj)
28
31
  else:
29
32
  raise TypeError(
30
- f"Cannot wrap object of type '{type(obj).__name__}' as a BaseAction or Command. "
31
- "It must be a callable or an instance of BaseAction."
33
+ f"Cannot wrap object of type '{type(obj).__name__}'. "
34
+ "Expected a function or BaseAction."
32
35
  )
33
36
 
34
37
 
@@ -44,7 +47,8 @@ def import_action(dotted_path: str) -> Any:
44
47
  logger.error("Failed to import module '%s': %s", module_path, error)
45
48
  console.print(
46
49
  f"[{OneColors.DARK_RED}]❌ Could not import '{dotted_path}': {error}[/]\n"
47
- f"[{OneColors.COMMENT_GREY}]Ensure the module is installed and discoverable via PYTHONPATH."
50
+ f"[{OneColors.COMMENT_GREY}]Ensure the module is installed and discoverable "
51
+ "via PYTHONPATH."
48
52
  )
49
53
  sys.exit(1)
50
54
  try:
@@ -54,15 +58,116 @@ def import_action(dotted_path: str) -> Any:
54
58
  "Module '%s' does not have attribute '%s': %s", module_path, attr, error
55
59
  )
56
60
  console.print(
57
- f"[{OneColors.DARK_RED}]❌ Module '{module_path}' has no attribute '{attr}': {error}[/]"
61
+ f"[{OneColors.DARK_RED}]❌ Module '{module_path}' has no attribute "
62
+ f"'{attr}': {error}[/]"
58
63
  )
59
64
  sys.exit(1)
60
65
  return action
61
66
 
62
67
 
63
- def loader(file_path: Path | str) -> list[dict[str, Any]]:
68
+ class RawCommand(BaseModel):
69
+ """Raw command model for Falyx CLI configuration."""
70
+
71
+ key: str
72
+ description: str
73
+ action: str
74
+
75
+ args: tuple[Any, ...] = ()
76
+ kwargs: dict[str, Any] = {}
77
+ aliases: list[str] = []
78
+ tags: list[str] = []
79
+ style: str = OneColors.WHITE
80
+
81
+ confirm: bool = False
82
+ confirm_message: str = "Are you sure?"
83
+ preview_before_confirm: bool = True
84
+
85
+ spinner: bool = False
86
+ spinner_message: str = "Processing..."
87
+ spinner_type: str = "dots"
88
+ spinner_style: str = OneColors.CYAN
89
+ spinner_kwargs: dict[str, Any] = {}
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] = []
96
+
97
+ logging_hooks: bool = False
98
+ retry: bool = False
99
+ retry_all: bool = False
100
+ retry_policy: RetryPolicy = Field(default_factory=RetryPolicy)
101
+ requires_input: bool | None = None
102
+ hidden: bool = False
103
+ help_text: str = ""
104
+
105
+ @field_validator("retry_policy")
106
+ @classmethod
107
+ def validate_retry_policy(cls, value: dict | RetryPolicy) -> RetryPolicy:
108
+ if isinstance(value, RetryPolicy):
109
+ return value
110
+ if not isinstance(value, dict):
111
+ raise ValueError("retry_policy must be a dictionary.")
112
+ return RetryPolicy(**value)
113
+
114
+
115
+ def convert_commands(raw_commands: list[dict[str, Any]]) -> list[Command]:
116
+ commands = []
117
+ for entry in raw_commands:
118
+ raw_command = RawCommand(**entry)
119
+ commands.append(
120
+ Command.model_validate(
121
+ {
122
+ **raw_command.model_dump(exclude={"action"}),
123
+ "action": wrap_if_needed(
124
+ import_action(raw_command.action), name=raw_command.description
125
+ ),
126
+ }
127
+ )
128
+ )
129
+ return commands
130
+
131
+
132
+ class FalyxConfig(BaseModel):
133
+ """Falyx CLI configuration model."""
134
+
135
+ title: str = "Falyx CLI"
136
+ prompt: str | list[tuple[str, str]] | list[list[str]] = [
137
+ (OneColors.BLUE_b, "FALYX > ")
138
+ ]
139
+ columns: int = 4
140
+ welcome_message: str = ""
141
+ exit_message: str = ""
142
+ commands: list[Command] | list[dict] = []
143
+
144
+ @model_validator(mode="after")
145
+ def validate_prompt_format(self) -> FalyxConfig:
146
+ if isinstance(self.prompt, list):
147
+ for pair in self.prompt:
148
+ if not isinstance(pair, (list, tuple)) or len(pair) != 2:
149
+ raise ValueError(
150
+ "Prompt list must contain 2-element (style, text) pairs"
151
+ )
152
+ return self
153
+
154
+ def to_falyx(self) -> Falyx:
155
+ flx = Falyx(
156
+ title=self.title,
157
+ prompt=self.prompt, # type: ignore[arg-type]
158
+ columns=self.columns,
159
+ welcome_message=self.welcome_message,
160
+ exit_message=self.exit_message,
161
+ )
162
+ flx.add_commands(self.commands)
163
+ return flx
164
+
165
+
166
+ def loader(file_path: Path | str) -> Falyx:
64
167
  """
65
- Load command definitions from a YAML or TOML file.
168
+ Load Falyx CLI configuration from a YAML or TOML file.
169
+
170
+ The file should contain a dictionary with a list of commands.
66
171
 
67
172
  Each command should be defined as a dictionary with at least:
68
173
  - key: a unique single-character key
@@ -73,15 +178,13 @@ def loader(file_path: Path | str) -> list[dict[str, Any]]:
73
178
  file_path (str): Path to the config file (YAML or TOML).
74
179
 
75
180
  Returns:
76
- list[dict[str, Any]]: A list of command configuration dictionaries.
181
+ Falyx: An instance of the Falyx CLI with loaded commands.
77
182
 
78
183
  Raises:
79
184
  ValueError: If the file format is unsupported or file cannot be parsed.
80
185
  """
81
- if isinstance(file_path, str):
186
+ if isinstance(file_path, (str, Path)):
82
187
  path = Path(file_path)
83
- elif isinstance(file_path, Path):
84
- path = file_path
85
188
  else:
86
189
  raise TypeError("file_path must be a string or Path object.")
87
190
 
@@ -97,48 +200,23 @@ def loader(file_path: Path | str) -> list[dict[str, Any]]:
97
200
  else:
98
201
  raise ValueError(f"Unsupported config format: {suffix}")
99
202
 
100
- if not isinstance(raw_config, list):
101
- raise ValueError("Configuration file must contain a list of command definitions.")
102
-
103
- required = ["key", "description", "action"]
104
- commands = []
105
- for entry in raw_config:
106
- for field in required:
107
- if field not in entry:
108
- raise ValueError(f"Missing '{field}' in command entry: {entry}")
109
-
110
- command_dict = {
111
- "key": entry["key"],
112
- "description": entry["description"],
113
- "action": wrap_if_needed(
114
- import_action(entry["action"]), name=entry["description"]
115
- ),
116
- "args": tuple(entry.get("args", ())),
117
- "kwargs": entry.get("kwargs", {}),
118
- "hidden": entry.get("hidden", False),
119
- "aliases": entry.get("aliases", []),
120
- "help_text": entry.get("help_text", ""),
121
- "style": entry.get("style", "white"),
122
- "confirm": entry.get("confirm", False),
123
- "confirm_message": entry.get("confirm_message", "Are you sure?"),
124
- "preview_before_confirm": entry.get("preview_before_confirm", True),
125
- "spinner": entry.get("spinner", False),
126
- "spinner_message": entry.get("spinner_message", "Processing..."),
127
- "spinner_type": entry.get("spinner_type", "dots"),
128
- "spinner_style": entry.get("spinner_style", "cyan"),
129
- "spinner_kwargs": entry.get("spinner_kwargs", {}),
130
- "before_hooks": entry.get("before_hooks", []),
131
- "success_hooks": entry.get("success_hooks", []),
132
- "error_hooks": entry.get("error_hooks", []),
133
- "after_hooks": entry.get("after_hooks", []),
134
- "teardown_hooks": entry.get("teardown_hooks", []),
135
- "retry": entry.get("retry", False),
136
- "retry_all": entry.get("retry_all", False),
137
- "retry_policy": RetryPolicy(**entry.get("retry_policy", {})),
138
- "tags": entry.get("tags", []),
139
- "logging_hooks": entry.get("logging_hooks", False),
140
- "requires_input": entry.get("requires_input", None),
141
- }
142
- commands.append(command_dict)
203
+ if not isinstance(raw_config, dict):
204
+ raise ValueError(
205
+ "Configuration file must contain a dictionary with a list of commands.\n"
206
+ "Example:\n"
207
+ "title: 'My CLI'\n"
208
+ "commands:\n"
209
+ " - key: 'a'\n"
210
+ " description: 'Example command'\n"
211
+ " action: 'my_module.my_function'"
212
+ )
143
213
 
144
- return commands
214
+ commands = convert_commands(raw_config["commands"])
215
+ return FalyxConfig(
216
+ title=raw_config.get("title", f"[{OneColors.BLUE_b}]Falyx CLI"),
217
+ prompt=raw_config.get("prompt", [(OneColors.BLUE_b, "FALYX > ")]),
218
+ columns=raw_config.get("columns", 4),
219
+ welcome_message=raw_config.get("welcome_message", ""),
220
+ exit_message=raw_config.get("exit_message", ""),
221
+ commands=commands,
222
+ ).to_falyx()