falyx 0.1.52__tar.gz → 0.1.54__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. {falyx-0.1.52 → falyx-0.1.54}/PKG-INFO +1 -1
  2. {falyx-0.1.52 → falyx-0.1.54}/falyx/action/__init__.py +5 -3
  3. {falyx-0.1.52 → falyx-0.1.54}/falyx/action/action.py +8 -8
  4. {falyx-0.1.52 → falyx-0.1.54}/falyx/action/action_factory.py +3 -3
  5. {falyx-0.1.52 → falyx-0.1.54}/falyx/action/action_group.py +17 -6
  6. falyx-0.1.52/falyx/action/mixins.py → falyx-0.1.54/falyx/action/action_mixins.py +5 -3
  7. falyx-0.1.52/falyx/action/types.py → falyx-0.1.54/falyx/action/action_types.py +3 -3
  8. falyx-0.1.52/falyx/action/base.py → falyx-0.1.54/falyx/action/base_action.py +1 -2
  9. {falyx-0.1.52 → falyx-0.1.54}/falyx/action/chained_action.py +15 -6
  10. {falyx-0.1.52 → falyx-0.1.54}/falyx/action/io_action.py +1 -1
  11. falyx-0.1.54/falyx/action/load_file_action.py +196 -0
  12. {falyx-0.1.52 → falyx-0.1.54}/falyx/action/menu_action.py +1 -1
  13. {falyx-0.1.52 → falyx-0.1.54}/falyx/action/process_action.py +1 -1
  14. {falyx-0.1.52 → falyx-0.1.54}/falyx/action/process_pool_action.py +7 -4
  15. {falyx-0.1.52 → falyx-0.1.54}/falyx/action/prompt_menu_action.py +1 -1
  16. falyx-0.1.54/falyx/action/save_file_action.py +44 -0
  17. {falyx-0.1.52 → falyx-0.1.54}/falyx/action/select_file_action.py +18 -15
  18. {falyx-0.1.52 → falyx-0.1.54}/falyx/action/selection_action.py +2 -2
  19. {falyx-0.1.52 → falyx-0.1.54}/falyx/action/user_input_action.py +1 -1
  20. {falyx-0.1.52 → falyx-0.1.54}/falyx/command.py +3 -3
  21. {falyx-0.1.52 → falyx-0.1.54}/falyx/config.py +1 -1
  22. {falyx-0.1.52 → falyx-0.1.54}/falyx/exceptions.py +8 -0
  23. {falyx-0.1.52 → falyx-0.1.54}/falyx/falyx.py +2 -3
  24. {falyx-0.1.52 → falyx-0.1.54}/falyx/logger.py +1 -1
  25. {falyx-0.1.52 → falyx-0.1.54}/falyx/menu.py +1 -1
  26. {falyx-0.1.52 → falyx-0.1.54}/falyx/parser/argument.py +3 -1
  27. {falyx-0.1.52 → falyx-0.1.54}/falyx/parser/command_argument_parser.py +171 -153
  28. {falyx-0.1.52 → falyx-0.1.54}/falyx/parser/parsers.py +15 -7
  29. {falyx-0.1.52 → falyx-0.1.54}/falyx/parser/utils.py +1 -2
  30. {falyx-0.1.52 → falyx-0.1.54}/falyx/protocols.py +5 -3
  31. {falyx-0.1.52 → falyx-0.1.54}/falyx/retry_utils.py +1 -1
  32. falyx-0.1.54/falyx/version.py +1 -0
  33. {falyx-0.1.52 → falyx-0.1.54}/pyproject.toml +5 -1
  34. falyx-0.1.52/falyx/version.py +0 -1
  35. {falyx-0.1.52 → falyx-0.1.54}/LICENSE +0 -0
  36. {falyx-0.1.52 → falyx-0.1.54}/README.md +0 -0
  37. {falyx-0.1.52 → falyx-0.1.54}/falyx/.pytyped +0 -0
  38. {falyx-0.1.52 → falyx-0.1.54}/falyx/__init__.py +0 -0
  39. {falyx-0.1.52 → falyx-0.1.54}/falyx/__main__.py +0 -0
  40. {falyx-0.1.52 → falyx-0.1.54}/falyx/action/.pytyped +0 -0
  41. {falyx-0.1.52 → falyx-0.1.54}/falyx/action/fallback_action.py +0 -0
  42. {falyx-0.1.52 → falyx-0.1.54}/falyx/action/http_action.py +0 -0
  43. {falyx-0.1.52 → falyx-0.1.54}/falyx/action/literal_input_action.py +0 -0
  44. {falyx-0.1.52 → falyx-0.1.54}/falyx/action/shell_action.py +0 -0
  45. {falyx-0.1.52 → falyx-0.1.54}/falyx/action/signal_action.py +0 -0
  46. {falyx-0.1.52 → falyx-0.1.54}/falyx/bottom_bar.py +0 -0
  47. {falyx-0.1.52 → falyx-0.1.54}/falyx/context.py +0 -0
  48. {falyx-0.1.52 → falyx-0.1.54}/falyx/debug.py +0 -0
  49. {falyx-0.1.52 → falyx-0.1.54}/falyx/execution_registry.py +0 -0
  50. {falyx-0.1.52 → falyx-0.1.54}/falyx/hook_manager.py +0 -0
  51. {falyx-0.1.52 → falyx-0.1.54}/falyx/hooks.py +0 -0
  52. {falyx-0.1.52 → falyx-0.1.54}/falyx/init.py +0 -0
  53. {falyx-0.1.52 → falyx-0.1.54}/falyx/options_manager.py +0 -0
  54. {falyx-0.1.52 → falyx-0.1.54}/falyx/parser/.pytyped +0 -0
  55. {falyx-0.1.52 → falyx-0.1.54}/falyx/parser/__init__.py +0 -0
  56. {falyx-0.1.52 → falyx-0.1.54}/falyx/parser/argument_action.py +0 -0
  57. {falyx-0.1.52 → falyx-0.1.54}/falyx/parser/signature.py +0 -0
  58. {falyx-0.1.52 → falyx-0.1.54}/falyx/prompt_utils.py +0 -0
  59. {falyx-0.1.52 → falyx-0.1.54}/falyx/retry.py +0 -0
  60. {falyx-0.1.52 → falyx-0.1.54}/falyx/selection.py +0 -0
  61. {falyx-0.1.52 → falyx-0.1.54}/falyx/signals.py +0 -0
  62. {falyx-0.1.52 → falyx-0.1.54}/falyx/tagged_table.py +0 -0
  63. {falyx-0.1.52 → falyx-0.1.54}/falyx/themes/__init__.py +0 -0
  64. {falyx-0.1.52 → falyx-0.1.54}/falyx/themes/colors.py +0 -0
  65. {falyx-0.1.52 → falyx-0.1.54}/falyx/utils.py +0 -0
  66. {falyx-0.1.52 → falyx-0.1.54}/falyx/validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: falyx
3
- Version: 0.1.52
3
+ Version: 0.1.54
4
4
  Summary: Reliable and introspectable async CLI action framework.
5
5
  License: MIT
6
6
  Author: Roland Thomas Jr
@@ -6,14 +6,15 @@ Licensed under the MIT License. See LICENSE file for details.
6
6
  """
7
7
 
8
8
  from .action import Action
9
- from .action_factory import ActionFactoryAction
9
+ from .action_factory import ActionFactory
10
10
  from .action_group import ActionGroup
11
- from .base import BaseAction
11
+ from .base_action import BaseAction
12
12
  from .chained_action import ChainedAction
13
13
  from .fallback_action import FallbackAction
14
14
  from .http_action import HTTPAction
15
15
  from .io_action import BaseIOAction
16
16
  from .literal_input_action import LiteralInputAction
17
+ from .load_file_action import LoadFileAction
17
18
  from .menu_action import MenuAction
18
19
  from .process_action import ProcessAction
19
20
  from .process_pool_action import ProcessPoolAction
@@ -30,7 +31,7 @@ __all__ = [
30
31
  "BaseAction",
31
32
  "ChainedAction",
32
33
  "ProcessAction",
33
- "ActionFactoryAction",
34
+ "ActionFactory",
34
35
  "HTTPAction",
35
36
  "BaseIOAction",
36
37
  "ShellAction",
@@ -43,4 +44,5 @@ __all__ = [
43
44
  "UserInputAction",
44
45
  "PromptMenuAction",
45
46
  "ProcessPoolAction",
47
+ "LoadFileAction",
46
48
  ]
@@ -2,11 +2,11 @@
2
2
  """action.py"""
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Any, Callable
5
+ from typing import Any, Awaitable, Callable
6
6
 
7
7
  from rich.tree import Tree
8
8
 
9
- from falyx.action.base import BaseAction
9
+ from falyx.action.base_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 HookManager, HookType
@@ -42,9 +42,9 @@ class Action(BaseAction):
42
42
  def __init__(
43
43
  self,
44
44
  name: str,
45
- action: Callable[..., Any],
45
+ action: Callable[..., Any] | Callable[..., Awaitable[Any]],
46
46
  *,
47
- rollback: Callable[..., Any] | None = None,
47
+ rollback: Callable[..., Any] | Callable[..., Awaitable[Any]] | None = None,
48
48
  args: tuple[Any, ...] = (),
49
49
  kwargs: dict[str, Any] | None = None,
50
50
  hooks: HookManager | None = None,
@@ -69,19 +69,19 @@ class Action(BaseAction):
69
69
  self.enable_retry()
70
70
 
71
71
  @property
72
- def action(self) -> Callable[..., Any]:
72
+ def action(self) -> Callable[..., Awaitable[Any]]:
73
73
  return self._action
74
74
 
75
75
  @action.setter
76
- def action(self, value: Callable[..., Any]):
76
+ def action(self, value: Callable[..., Awaitable[Any]]):
77
77
  self._action = ensure_async(value)
78
78
 
79
79
  @property
80
- def rollback(self) -> Callable[..., Any] | None:
80
+ def rollback(self) -> Callable[..., Awaitable[Any]] | None:
81
81
  return self._rollback
82
82
 
83
83
  @rollback.setter
84
- def rollback(self, value: Callable[..., Any] | None):
84
+ def rollback(self, value: Callable[..., Awaitable[Any]] | None):
85
85
  if value is None:
86
86
  self._rollback = None
87
87
  else:
@@ -1,10 +1,10 @@
1
1
  # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
- """action_factory.py"""
2
+ """action_factory_action.py"""
3
3
  from typing import Any, Callable
4
4
 
5
5
  from rich.tree import Tree
6
6
 
7
- from falyx.action.base import BaseAction
7
+ from falyx.action.base_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
@@ -14,7 +14,7 @@ from falyx.themes import OneColors
14
14
  from falyx.utils import ensure_async
15
15
 
16
16
 
17
- class ActionFactoryAction(BaseAction):
17
+ class ActionFactory(BaseAction):
18
18
  """
19
19
  Dynamically creates and runs another Action at runtime using a factory function.
20
20
 
@@ -2,14 +2,15 @@
2
2
  """action_group.py"""
3
3
  import asyncio
4
4
  import random
5
- from typing import Any, Callable
5
+ from typing import Any, Awaitable, Callable, Sequence
6
6
 
7
7
  from rich.tree import Tree
8
8
 
9
9
  from falyx.action.action import Action
10
- from falyx.action.base import BaseAction
11
- from falyx.action.mixins import ActionListMixin
10
+ from falyx.action.action_mixins import ActionListMixin
11
+ from falyx.action.base_action import BaseAction
12
12
  from falyx.context import ExecutionContext, SharedContext
13
+ from falyx.exceptions import EmptyGroupError
13
14
  from falyx.execution_registry import ExecutionRegistry as er
14
15
  from falyx.hook_manager import Hook, HookManager, HookType
15
16
  from falyx.logger import logger
@@ -54,7 +55,9 @@ class ActionGroup(BaseAction, ActionListMixin):
54
55
  def __init__(
55
56
  self,
56
57
  name: str,
57
- actions: list[BaseAction] | None = None,
58
+ actions: (
59
+ Sequence[BaseAction | Callable[..., Any] | Callable[..., Awaitable]] | None
60
+ ) = None,
58
61
  *,
59
62
  hooks: HookManager | None = None,
60
63
  inject_last_result: bool = False,
@@ -70,7 +73,7 @@ class ActionGroup(BaseAction, ActionListMixin):
70
73
  if actions:
71
74
  self.set_actions(actions)
72
75
 
73
- def _wrap_if_needed(self, action: BaseAction | Any) -> BaseAction:
76
+ def _wrap_if_needed(self, action: BaseAction | Callable[..., Any]) -> BaseAction:
74
77
  if isinstance(action, BaseAction):
75
78
  return action
76
79
  elif callable(action):
@@ -81,12 +84,18 @@ class ActionGroup(BaseAction, ActionListMixin):
81
84
  f"{type(action).__name__}"
82
85
  )
83
86
 
84
- def add_action(self, action: BaseAction | Any) -> None:
87
+ def add_action(self, action: BaseAction | Callable[..., Any]) -> None:
85
88
  action = self._wrap_if_needed(action)
86
89
  super().add_action(action)
87
90
  if hasattr(action, "register_teardown") and callable(action.register_teardown):
88
91
  action.register_teardown(self.hooks)
89
92
 
93
+ def set_actions(self, actions: Sequence[BaseAction | Callable[..., Any]]) -> None:
94
+ """Replaces the current action list with a new one."""
95
+ self.actions.clear()
96
+ for action in actions:
97
+ self.add_action(action)
98
+
90
99
  def get_infer_target(self) -> tuple[Callable[..., Any] | None, dict[str, Any] | None]:
91
100
  arg_defs = same_argument_definitions(self.actions)
92
101
  if arg_defs:
@@ -98,6 +107,8 @@ class ActionGroup(BaseAction, ActionListMixin):
98
107
  return None, None
99
108
 
100
109
  async def _run(self, *args, **kwargs) -> list[tuple[str, Any]]:
110
+ if not self.actions:
111
+ raise EmptyGroupError(f"[{self.name}] No actions to execute.")
101
112
  shared_context = SharedContext(name=self.name, action=self, is_parallel=True)
102
113
  if self.shared_context:
103
114
  shared_context.set_shared_result(self.shared_context.last_result())
@@ -1,6 +1,8 @@
1
1
  # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
- """mixins.py"""
3
- from falyx.action.base import BaseAction
2
+ """action_mixins.py"""
3
+ from typing import Sequence
4
+
5
+ from falyx.action.base_action import BaseAction
4
6
 
5
7
 
6
8
  class ActionListMixin:
@@ -9,7 +11,7 @@ class ActionListMixin:
9
11
  def __init__(self) -> None:
10
12
  self.actions: list[BaseAction] = []
11
13
 
12
- def set_actions(self, actions: list[BaseAction]) -> None:
14
+ def set_actions(self, actions: Sequence[BaseAction]) -> None:
13
15
  """Replaces the current action list with a new one."""
14
16
  self.actions.clear()
15
17
  for action in actions:
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  from enum import Enum
6
6
 
7
7
 
8
- class FileReturnType(Enum):
8
+ class FileType(Enum):
9
9
  """Enum for file return types."""
10
10
 
11
11
  TEXT = "text"
@@ -28,7 +28,7 @@ class FileReturnType(Enum):
28
28
  return aliases.get(value, value)
29
29
 
30
30
  @classmethod
31
- def _missing_(cls, value: object) -> FileReturnType:
31
+ def _missing_(cls, value: object) -> FileType:
32
32
  if isinstance(value, str):
33
33
  normalized = value.lower()
34
34
  alias = cls._get_alias(normalized)
@@ -36,7 +36,7 @@ class FileReturnType(Enum):
36
36
  if member.value == alias:
37
37
  return member
38
38
  valid = ", ".join(member.value for member in cls)
39
- raise ValueError(f"Invalid FileReturnType: '{value}'. Must be one of: {valid}")
39
+ raise ValueError(f"Invalid FileType: '{value}'. Must be one of: {valid}")
40
40
 
41
41
 
42
42
  class SelectionReturnType(Enum):
@@ -1,5 +1,5 @@
1
1
  # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
- """base.py
2
+ """base_action.py
3
3
 
4
4
  Core action system for Falyx.
5
5
 
@@ -38,7 +38,6 @@ from rich.tree import Tree
38
38
 
39
39
  from falyx.context import SharedContext
40
40
  from falyx.debug import register_debug_hooks
41
- from falyx.execution_registry import ExecutionRegistry as er
42
41
  from falyx.hook_manager import Hook, HookManager, HookType
43
42
  from falyx.logger import logger
44
43
  from falyx.options_manager import OptionsManager
@@ -2,15 +2,15 @@
2
2
  """chained_action.py"""
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Any, Callable
5
+ from typing import Any, Awaitable, Callable, Sequence
6
6
 
7
7
  from rich.tree import Tree
8
8
 
9
9
  from falyx.action.action import Action
10
- from falyx.action.base import BaseAction
10
+ from falyx.action.action_mixins import ActionListMixin
11
+ from falyx.action.base_action import BaseAction
11
12
  from falyx.action.fallback_action import FallbackAction
12
13
  from falyx.action.literal_input_action import LiteralInputAction
13
- from falyx.action.mixins import ActionListMixin
14
14
  from falyx.context import ExecutionContext, SharedContext
15
15
  from falyx.exceptions import EmptyChainError
16
16
  from falyx.execution_registry import ExecutionRegistry as er
@@ -47,7 +47,10 @@ class ChainedAction(BaseAction, ActionListMixin):
47
47
  def __init__(
48
48
  self,
49
49
  name: str,
50
- actions: list[BaseAction | Any] | None = None,
50
+ actions: (
51
+ Sequence[BaseAction | Callable[..., Any] | Callable[..., Awaitable[Any]]]
52
+ | None
53
+ ) = None,
51
54
  *,
52
55
  hooks: HookManager | None = None,
53
56
  inject_last_result: bool = False,
@@ -67,7 +70,7 @@ class ChainedAction(BaseAction, ActionListMixin):
67
70
  if actions:
68
71
  self.set_actions(actions)
69
72
 
70
- def _wrap_if_needed(self, action: BaseAction | Any) -> BaseAction:
73
+ def _wrap_if_needed(self, action: BaseAction | Callable[..., Any]) -> BaseAction:
71
74
  if isinstance(action, BaseAction):
72
75
  return action
73
76
  elif callable(action):
@@ -75,7 +78,7 @@ class ChainedAction(BaseAction, ActionListMixin):
75
78
  else:
76
79
  return LiteralInputAction(action)
77
80
 
78
- def add_action(self, action: BaseAction | Any) -> None:
81
+ def add_action(self, action: BaseAction | Callable[..., Any]) -> None:
79
82
  action = self._wrap_if_needed(action)
80
83
  if self.actions and self.auto_inject and not action.inject_last_result:
81
84
  action.inject_last_result = True
@@ -83,6 +86,12 @@ class ChainedAction(BaseAction, ActionListMixin):
83
86
  if hasattr(action, "register_teardown") and callable(action.register_teardown):
84
87
  action.register_teardown(self.hooks)
85
88
 
89
+ def set_actions(self, actions: Sequence[BaseAction | Callable[..., Any]]) -> None:
90
+ """Replaces the current action list with a new one."""
91
+ self.actions.clear()
92
+ for action in actions:
93
+ self.add_action(action)
94
+
86
95
  def get_infer_target(self) -> tuple[Callable[..., Any] | None, dict[str, Any] | None]:
87
96
  if self.actions:
88
97
  return self.actions[0].get_infer_target()
@@ -21,7 +21,7 @@ from typing import Any, Callable
21
21
 
22
22
  from rich.tree import Tree
23
23
 
24
- from falyx.action.base import BaseAction
24
+ from falyx.action.base_action import BaseAction
25
25
  from falyx.context import ExecutionContext
26
26
  from falyx.execution_registry import ExecutionRegistry as er
27
27
  from falyx.hook_manager import HookManager, HookType
@@ -0,0 +1,196 @@
1
+ # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
+ """load_file_action.py"""
3
+ import csv
4
+ import json
5
+ import xml.etree.ElementTree as ET
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import toml
11
+ import yaml
12
+ from rich.tree import Tree
13
+
14
+ from falyx.action.action_types import FileType
15
+ from falyx.action.base_action import BaseAction
16
+ from falyx.context import ExecutionContext
17
+ from falyx.execution_registry import ExecutionRegistry as er
18
+ from falyx.hook_manager import HookType
19
+ from falyx.logger import logger
20
+ from falyx.themes import OneColors
21
+
22
+
23
+ class LoadFileAction(BaseAction):
24
+ """LoadFileAction allows loading and parsing files of various types."""
25
+
26
+ def __init__(
27
+ self,
28
+ name: str,
29
+ file_path: str | Path | None = None,
30
+ file_type: FileType | str = FileType.TEXT,
31
+ inject_last_result: bool = False,
32
+ inject_into: str = "file_path",
33
+ ):
34
+ super().__init__(
35
+ name=name, inject_last_result=inject_last_result, inject_into=inject_into
36
+ )
37
+ self._file_path = self._coerce_file_path(file_path)
38
+ self._file_type = self._coerce_file_type(file_type)
39
+
40
+ @property
41
+ def file_path(self) -> Path | None:
42
+ """Get the file path as a Path object."""
43
+ return self._file_path
44
+
45
+ @file_path.setter
46
+ def file_path(self, value: str | Path):
47
+ """Set the file path, converting to Path if necessary."""
48
+ self._file_path = self._coerce_file_path(value)
49
+
50
+ def _coerce_file_path(self, file_path: str | Path | None) -> Path | None:
51
+ """Coerce the file path to a Path object."""
52
+ if isinstance(file_path, Path):
53
+ return file_path
54
+ elif isinstance(file_path, str):
55
+ return Path(file_path)
56
+ elif file_path is None:
57
+ return None
58
+ else:
59
+ raise TypeError("file_path must be a string or Path object")
60
+
61
+ @property
62
+ def file_type(self) -> FileType:
63
+ """Get the file type."""
64
+ return self._file_type
65
+
66
+ @file_type.setter
67
+ def file_type(self, value: FileType | str):
68
+ """Set the file type, converting to FileType if necessary."""
69
+ self._file_type = self._coerce_file_type(value)
70
+
71
+ def _coerce_file_type(self, file_type: FileType | str) -> FileType:
72
+ """Coerce the file type to a FileType enum."""
73
+ if isinstance(file_type, FileType):
74
+ return file_type
75
+ elif isinstance(file_type, str):
76
+ return FileType(file_type)
77
+ else:
78
+ raise TypeError("file_type must be a FileType enum or string")
79
+
80
+ def get_infer_target(self) -> tuple[None, None]:
81
+ return None, None
82
+
83
+ def load_file(self) -> Any:
84
+ if self.file_path is None:
85
+ raise ValueError("file_path must be set before loading a file")
86
+ value: Any = None
87
+ try:
88
+ if self.file_type == FileType.TEXT:
89
+ value = self.file_path.read_text(encoding="UTF-8")
90
+ elif self.file_type == FileType.PATH:
91
+ value = self.file_path
92
+ elif self.file_type == FileType.JSON:
93
+ value = json.loads(self.file_path.read_text(encoding="UTF-8"))
94
+ elif self.file_type == FileType.TOML:
95
+ value = toml.loads(self.file_path.read_text(encoding="UTF-8"))
96
+ elif self.file_type == FileType.YAML:
97
+ value = yaml.safe_load(self.file_path.read_text(encoding="UTF-8"))
98
+ elif self.file_type == FileType.CSV:
99
+ with open(self.file_path, newline="", encoding="UTF-8") as csvfile:
100
+ reader = csv.reader(csvfile)
101
+ value = list(reader)
102
+ elif self.file_type == FileType.TSV:
103
+ with open(self.file_path, newline="", encoding="UTF-8") as tsvfile:
104
+ reader = csv.reader(tsvfile, delimiter="\t")
105
+ value = list(reader)
106
+ elif self.file_type == FileType.XML:
107
+ tree = ET.parse(self.file_path, parser=ET.XMLParser(encoding="UTF-8"))
108
+ root = tree.getroot()
109
+ value = ET.tostring(root, encoding="unicode")
110
+ else:
111
+ raise ValueError(f"Unsupported return type: {self.file_type}")
112
+
113
+ except Exception as error:
114
+ logger.error("Failed to parse %s: %s", self.file_path.name, error)
115
+ return value
116
+
117
+ async def _run(self, *args, **kwargs) -> Any:
118
+ context = ExecutionContext(name=self.name, args=args, kwargs=kwargs, action=self)
119
+ context.start_timer()
120
+ try:
121
+ await self.hooks.trigger(HookType.BEFORE, context)
122
+
123
+ if "file_path" in kwargs:
124
+ self.file_path = kwargs["file_path"]
125
+ elif self.inject_last_result and self.last_result:
126
+ self.file_path = self.last_result
127
+
128
+ if self.file_path is None:
129
+ raise ValueError("file_path must be set before loading a file")
130
+ elif not self.file_path.exists():
131
+ raise FileNotFoundError(f"File not found: {self.file_path}")
132
+ elif not self.file_path.is_file():
133
+ raise ValueError(f"Path is not a regular file: {self.file_path}")
134
+
135
+ result = self.load_file()
136
+ await self.hooks.trigger(HookType.ON_SUCCESS, context)
137
+ return result
138
+ except Exception as error:
139
+ context.exception = error
140
+ await self.hooks.trigger(HookType.ON_ERROR, context)
141
+ raise
142
+ finally:
143
+ context.stop_timer()
144
+ await self.hooks.trigger(HookType.AFTER, context)
145
+ await self.hooks.trigger(HookType.ON_TEARDOWN, context)
146
+ er.record(context)
147
+
148
+ async def preview(self, parent: Tree | None = None):
149
+ label = f"[{OneColors.GREEN}]📄 LoadFileAction[/] '{self.name}'"
150
+ tree = parent.add(label) if parent else Tree(label)
151
+
152
+ tree.add(f"[dim]Path:[/] {self.file_path}")
153
+ tree.add(f"[dim]Type:[/] {self.file_type.name if self.file_type else 'None'}")
154
+ if self.file_path is None:
155
+ tree.add(f"[{OneColors.DARK_RED_b}]❌ File path is not set[/]")
156
+ elif not self.file_path.exists():
157
+ tree.add(f"[{OneColors.DARK_RED_b}]❌ File does not exist[/]")
158
+ elif not self.file_path.is_file():
159
+ tree.add(f"[{OneColors.LIGHT_YELLOW_b}]⚠️ Not a regular file[/]")
160
+ else:
161
+ try:
162
+ stat = self.file_path.stat()
163
+ tree.add(f"[dim]Size:[/] {stat.st_size:,} bytes")
164
+ tree.add(
165
+ f"[dim]Modified:[/] {datetime.fromtimestamp(stat.st_mtime):%Y-%m-%d %H:%M:%S}"
166
+ )
167
+ tree.add(
168
+ f"[dim]Created:[/] {datetime.fromtimestamp(stat.st_ctime):%Y-%m-%d %H:%M:%S}"
169
+ )
170
+ if self.file_type == FileType.TEXT:
171
+ preview_lines = self.file_path.read_text(
172
+ encoding="UTF-8"
173
+ ).splitlines()[:10]
174
+ content_tree = tree.add("[dim]Preview (first 10 lines):[/]")
175
+ for line in preview_lines:
176
+ content_tree.add(f"[dim]{line}[/]")
177
+ elif self.file_type in {FileType.JSON, FileType.YAML, FileType.TOML}:
178
+ raw = self.load_file()
179
+ if raw is not None:
180
+ preview_str = (
181
+ json.dumps(raw, indent=2)
182
+ if isinstance(raw, dict)
183
+ else str(raw)
184
+ )
185
+ preview_lines = preview_str.splitlines()[:10]
186
+ content_tree = tree.add("[dim]Parsed preview:[/]")
187
+ for line in preview_lines:
188
+ content_tree.add(f"[dim]{line}[/]")
189
+ except Exception as e:
190
+ tree.add(f"[{OneColors.DARK_RED_b}]❌ Error reading file:[/] {e}")
191
+
192
+ if not parent:
193
+ self.console.print(tree)
194
+
195
+ def __str__(self) -> str:
196
+ return f"LoadFileAction(file_path={self.file_path}, file_type={self.file_type})"
@@ -7,7 +7,7 @@ from rich.console import Console
7
7
  from rich.table import Table
8
8
  from rich.tree import Tree
9
9
 
10
- from falyx.action.base import BaseAction
10
+ from falyx.action.base_action import BaseAction
11
11
  from falyx.context import ExecutionContext
12
12
  from falyx.execution_registry import ExecutionRegistry as er
13
13
  from falyx.hook_manager import HookType
@@ -9,7 +9,7 @@ from typing import Any, Callable
9
9
 
10
10
  from rich.tree import Tree
11
11
 
12
- from falyx.action.base import BaseAction
12
+ from falyx.action.base_action import BaseAction
13
13
  from falyx.context import ExecutionContext
14
14
  from falyx.execution_registry import ExecutionRegistry as er
15
15
  from falyx.hook_manager import HookManager, HookType
@@ -7,12 +7,13 @@ import random
7
7
  from concurrent.futures import ProcessPoolExecutor
8
8
  from dataclasses import dataclass, field
9
9
  from functools import partial
10
- from typing import Any, Callable
10
+ from typing import Any, Callable, Sequence
11
11
 
12
12
  from rich.tree import Tree
13
13
 
14
- from falyx.action.base import BaseAction
14
+ from falyx.action.base_action import BaseAction
15
15
  from falyx.context import ExecutionContext, SharedContext
16
+ from falyx.exceptions import EmptyPoolError
16
17
  from falyx.execution_registry import ExecutionRegistry as er
17
18
  from falyx.hook_manager import HookManager, HookType
18
19
  from falyx.logger import logger
@@ -37,7 +38,7 @@ class ProcessPoolAction(BaseAction):
37
38
  def __init__(
38
39
  self,
39
40
  name: str,
40
- actions: list[ProcessTask] | None = None,
41
+ actions: Sequence[ProcessTask] | None = None,
41
42
  *,
42
43
  hooks: HookManager | None = None,
43
44
  executor: ProcessPoolExecutor | None = None,
@@ -56,7 +57,7 @@ class ProcessPoolAction(BaseAction):
56
57
  if actions:
57
58
  self.set_actions(actions)
58
59
 
59
- def set_actions(self, actions: list[ProcessTask]) -> None:
60
+ def set_actions(self, actions: Sequence[ProcessTask]) -> None:
60
61
  """Replaces the current action list with a new one."""
61
62
  self.actions.clear()
62
63
  for action in actions:
@@ -78,6 +79,8 @@ class ProcessPoolAction(BaseAction):
78
79
  return None, None
79
80
 
80
81
  async def _run(self, *args, **kwargs) -> Any:
82
+ if not self.actions:
83
+ raise EmptyPoolError(f"[{self.name}] No actions to execute.")
81
84
  shared_context = SharedContext(name=self.name, action=self, is_parallel=True)
82
85
  if self.shared_context:
83
86
  shared_context.set_shared_result(self.shared_context.last_result())
@@ -7,7 +7,7 @@ from prompt_toolkit.formatted_text import FormattedText, merge_formatted_text
7
7
  from rich.console import Console
8
8
  from rich.tree import Tree
9
9
 
10
- from falyx.action.base import BaseAction
10
+ from falyx.action.base_action import BaseAction
11
11
  from falyx.context import ExecutionContext
12
12
  from falyx.execution_registry import ExecutionRegistry as er
13
13
  from falyx.hook_manager import HookType
@@ -0,0 +1,44 @@
1
+ # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
+ """save_file_action.py"""
3
+ from pathlib import Path
4
+
5
+ from rich.tree import Tree
6
+
7
+ from falyx.action.action_types import FileType
8
+ from falyx.action.base_action import BaseAction
9
+
10
+
11
+ class SaveFileAction(BaseAction):
12
+ """ """
13
+
14
+ def __init__(
15
+ self,
16
+ name: str,
17
+ file_path: str,
18
+ input_type: str | FileType = "text",
19
+ output_type: str | FileType = "text",
20
+ ):
21
+ """
22
+ SaveFileAction allows saving data to a file.
23
+
24
+ Args:
25
+ name (str): Name of the action.
26
+ file_path (str | Path): Path to the file where data will be saved.
27
+ input_type (str | FileType): Type of data being saved (default is "text").
28
+ output_type (str | FileType): Type of data to save to the file (default is "text").
29
+ """
30
+ super().__init__(name=name)
31
+ self.file_path = file_path
32
+
33
+ def get_infer_target(self) -> tuple[None, None]:
34
+ return None, None
35
+
36
+ async def _run(self, *args, **kwargs):
37
+ raise NotImplementedError(
38
+ "SaveFileAction is not finished yet... Use primitives instead..."
39
+ )
40
+
41
+ async def preview(self, parent: Tree | None = None): ...
42
+
43
+ def __str__(self) -> str:
44
+ return f"SaveFileAction(file_path={self.file_path})"