falyx 0.1.60__tar.gz → 0.1.62__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 (71) hide show
  1. {falyx-0.1.60 → falyx-0.1.62}/PKG-INFO +1 -1
  2. {falyx-0.1.60 → falyx-0.1.62}/falyx/action/action_factory.py +10 -1
  3. {falyx-0.1.60 → falyx-0.1.62}/falyx/action/action_group.py +11 -3
  4. {falyx-0.1.60 → falyx-0.1.62}/falyx/action/action_types.py +30 -0
  5. {falyx-0.1.60 → falyx-0.1.62}/falyx/action/chained_action.py +21 -10
  6. {falyx-0.1.60 → falyx-0.1.62}/falyx/action/confirm_action.py +22 -29
  7. {falyx-0.1.60 → falyx-0.1.62}/falyx/action/process_pool_action.py +0 -2
  8. {falyx-0.1.60 → falyx-0.1.62}/falyx/parser/command_argument_parser.py +5 -1
  9. falyx-0.1.62/falyx/version.py +1 -0
  10. {falyx-0.1.60 → falyx-0.1.62}/pyproject.toml +1 -1
  11. falyx-0.1.60/falyx/version.py +0 -1
  12. {falyx-0.1.60 → falyx-0.1.62}/LICENSE +0 -0
  13. {falyx-0.1.60 → falyx-0.1.62}/README.md +0 -0
  14. {falyx-0.1.60 → falyx-0.1.62}/falyx/.pytyped +0 -0
  15. {falyx-0.1.60 → falyx-0.1.62}/falyx/__init__.py +0 -0
  16. {falyx-0.1.60 → falyx-0.1.62}/falyx/__main__.py +0 -0
  17. {falyx-0.1.60 → falyx-0.1.62}/falyx/action/.pytyped +0 -0
  18. {falyx-0.1.60 → falyx-0.1.62}/falyx/action/__init__.py +0 -0
  19. {falyx-0.1.60 → falyx-0.1.62}/falyx/action/action.py +0 -0
  20. {falyx-0.1.60 → falyx-0.1.62}/falyx/action/action_mixins.py +0 -0
  21. {falyx-0.1.60 → falyx-0.1.62}/falyx/action/base_action.py +0 -0
  22. {falyx-0.1.60 → falyx-0.1.62}/falyx/action/fallback_action.py +0 -0
  23. {falyx-0.1.60 → falyx-0.1.62}/falyx/action/http_action.py +0 -0
  24. {falyx-0.1.60 → falyx-0.1.62}/falyx/action/io_action.py +0 -0
  25. {falyx-0.1.60 → falyx-0.1.62}/falyx/action/literal_input_action.py +0 -0
  26. {falyx-0.1.60 → falyx-0.1.62}/falyx/action/load_file_action.py +0 -0
  27. {falyx-0.1.60 → falyx-0.1.62}/falyx/action/menu_action.py +0 -0
  28. {falyx-0.1.60 → falyx-0.1.62}/falyx/action/process_action.py +0 -0
  29. {falyx-0.1.60 → falyx-0.1.62}/falyx/action/prompt_menu_action.py +0 -0
  30. {falyx-0.1.60 → falyx-0.1.62}/falyx/action/save_file_action.py +0 -0
  31. {falyx-0.1.60 → falyx-0.1.62}/falyx/action/select_file_action.py +0 -0
  32. {falyx-0.1.60 → falyx-0.1.62}/falyx/action/selection_action.py +0 -0
  33. {falyx-0.1.60 → falyx-0.1.62}/falyx/action/shell_action.py +0 -0
  34. {falyx-0.1.60 → falyx-0.1.62}/falyx/action/signal_action.py +0 -0
  35. {falyx-0.1.60 → falyx-0.1.62}/falyx/action/user_input_action.py +0 -0
  36. {falyx-0.1.60 → falyx-0.1.62}/falyx/bottom_bar.py +0 -0
  37. {falyx-0.1.60 → falyx-0.1.62}/falyx/command.py +0 -0
  38. {falyx-0.1.60 → falyx-0.1.62}/falyx/completer.py +0 -0
  39. {falyx-0.1.60 → falyx-0.1.62}/falyx/config.py +0 -0
  40. {falyx-0.1.60 → falyx-0.1.62}/falyx/console.py +0 -0
  41. {falyx-0.1.60 → falyx-0.1.62}/falyx/context.py +0 -0
  42. {falyx-0.1.60 → falyx-0.1.62}/falyx/debug.py +0 -0
  43. {falyx-0.1.60 → falyx-0.1.62}/falyx/exceptions.py +0 -0
  44. {falyx-0.1.60 → falyx-0.1.62}/falyx/execution_registry.py +0 -0
  45. {falyx-0.1.60 → falyx-0.1.62}/falyx/falyx.py +0 -0
  46. {falyx-0.1.60 → falyx-0.1.62}/falyx/falyx_completer.py +0 -0
  47. {falyx-0.1.60 → falyx-0.1.62}/falyx/hook_manager.py +0 -0
  48. {falyx-0.1.60 → falyx-0.1.62}/falyx/hooks.py +0 -0
  49. {falyx-0.1.60 → falyx-0.1.62}/falyx/init.py +0 -0
  50. {falyx-0.1.60 → falyx-0.1.62}/falyx/logger.py +0 -0
  51. {falyx-0.1.60 → falyx-0.1.62}/falyx/menu.py +0 -0
  52. {falyx-0.1.60 → falyx-0.1.62}/falyx/options_manager.py +0 -0
  53. {falyx-0.1.60 → falyx-0.1.62}/falyx/parser/.pytyped +0 -0
  54. {falyx-0.1.60 → falyx-0.1.62}/falyx/parser/__init__.py +0 -0
  55. {falyx-0.1.60 → falyx-0.1.62}/falyx/parser/argument.py +0 -0
  56. {falyx-0.1.60 → falyx-0.1.62}/falyx/parser/argument_action.py +0 -0
  57. {falyx-0.1.60 → falyx-0.1.62}/falyx/parser/parser_types.py +0 -0
  58. {falyx-0.1.60 → falyx-0.1.62}/falyx/parser/parsers.py +0 -0
  59. {falyx-0.1.60 → falyx-0.1.62}/falyx/parser/signature.py +0 -0
  60. {falyx-0.1.60 → falyx-0.1.62}/falyx/parser/utils.py +0 -0
  61. {falyx-0.1.60 → falyx-0.1.62}/falyx/prompt_utils.py +0 -0
  62. {falyx-0.1.60 → falyx-0.1.62}/falyx/protocols.py +0 -0
  63. {falyx-0.1.60 → falyx-0.1.62}/falyx/retry.py +0 -0
  64. {falyx-0.1.60 → falyx-0.1.62}/falyx/retry_utils.py +0 -0
  65. {falyx-0.1.60 → falyx-0.1.62}/falyx/selection.py +0 -0
  66. {falyx-0.1.60 → falyx-0.1.62}/falyx/signals.py +0 -0
  67. {falyx-0.1.60 → falyx-0.1.62}/falyx/tagged_table.py +0 -0
  68. {falyx-0.1.60 → falyx-0.1.62}/falyx/themes/__init__.py +0 -0
  69. {falyx-0.1.60 → falyx-0.1.62}/falyx/themes/colors.py +0 -0
  70. {falyx-0.1.60 → falyx-0.1.62}/falyx/utils.py +0 -0
  71. {falyx-0.1.60 → falyx-0.1.62}/falyx/validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: falyx
3
- Version: 0.1.60
3
+ Version: 0.1.62
4
4
  Summary: Reliable and introspectable async CLI action framework.
5
5
  License: MIT
6
6
  Author: Roland Thomas Jr
@@ -112,7 +112,16 @@ class ActionFactory(BaseAction):
112
112
  tree = parent.add(label) if parent else Tree(label)
113
113
 
114
114
  try:
115
- generated = await self.factory(*self.preview_args, **self.preview_kwargs)
115
+ generated = None
116
+ if self.args or self.kwargs:
117
+ try:
118
+ generated = await self.factory(*self.args, **self.kwargs)
119
+ except TypeError:
120
+ ...
121
+
122
+ if not generated:
123
+ generated = await self.factory(*self.preview_args, **self.preview_kwargs)
124
+
116
125
  if isinstance(generated, BaseAction):
117
126
  await generated.preview(parent=tree)
118
127
  else:
@@ -60,6 +60,8 @@ class ActionGroup(BaseAction, ActionListMixin):
60
60
  Sequence[BaseAction | Callable[..., Any] | Callable[..., Awaitable]] | None
61
61
  ) = None,
62
62
  *,
63
+ args: tuple[Any, ...] = (),
64
+ kwargs: dict[str, Any] | None = None,
63
65
  hooks: HookManager | None = None,
64
66
  inject_last_result: bool = False,
65
67
  inject_into: str = "last_result",
@@ -71,6 +73,8 @@ class ActionGroup(BaseAction, ActionListMixin):
71
73
  inject_into=inject_into,
72
74
  )
73
75
  ActionListMixin.__init__(self)
76
+ self.args = args
77
+ self.kwargs = kwargs or {}
74
78
  if actions:
75
79
  self.set_actions(actions)
76
80
 
@@ -115,13 +119,17 @@ class ActionGroup(BaseAction, ActionListMixin):
115
119
  async def _run(self, *args, **kwargs) -> list[tuple[str, Any]]:
116
120
  if not self.actions:
117
121
  raise EmptyGroupError(f"[{self.name}] No actions to execute.")
122
+
123
+ combined_args = args + self.args
124
+ combined_kwargs = {**self.kwargs, **kwargs}
125
+
118
126
  shared_context = SharedContext(name=self.name, action=self, is_parallel=True)
119
127
  if self.shared_context:
120
128
  shared_context.set_shared_result(self.shared_context.last_result())
121
- updated_kwargs = self._maybe_inject_last_result(kwargs)
129
+ updated_kwargs = self._maybe_inject_last_result(combined_kwargs)
122
130
  context = ExecutionContext(
123
131
  name=self.name,
124
- args=args,
132
+ args=combined_args,
125
133
  kwargs=updated_kwargs,
126
134
  action=self,
127
135
  extra={"results": [], "errors": []},
@@ -131,7 +139,7 @@ class ActionGroup(BaseAction, ActionListMixin):
131
139
  async def run_one(action: BaseAction):
132
140
  try:
133
141
  prepared = action.prepare(shared_context, self.options_manager)
134
- result = await prepared(*args, **updated_kwargs)
142
+ result = await prepared(*combined_args, **updated_kwargs)
135
143
  shared_context.add_result((action.name, result))
136
144
  context.extra["results"].append((action.name, result))
137
145
  except Exception as error:
@@ -52,3 +52,33 @@ class SelectionReturnType(Enum):
52
52
  def _missing_(cls, value: object) -> SelectionReturnType:
53
53
  valid = ", ".join(member.value for member in cls)
54
54
  raise ValueError(f"Invalid DictReturnType: '{value}'. Must be one of: {valid}")
55
+
56
+
57
+ class ConfirmType(Enum):
58
+ """Enum for different confirmation types."""
59
+
60
+ YES_NO = "yes_no"
61
+ YES_CANCEL = "yes_cancel"
62
+ YES_NO_CANCEL = "yes_no_cancel"
63
+ TYPE_WORD = "type_word"
64
+ TYPE_WORD_CANCEL = "type_word_cancel"
65
+ OK_CANCEL = "ok_cancel"
66
+ ACKNOWLEDGE = "acknowledge"
67
+
68
+ @classmethod
69
+ def choices(cls) -> list[ConfirmType]:
70
+ """Return a list of all hook type choices."""
71
+ return list(cls)
72
+
73
+ def __str__(self) -> str:
74
+ """Return the string representation of the confirm type."""
75
+ return self.value
76
+
77
+ @classmethod
78
+ def _missing_(cls, value: object) -> ConfirmType:
79
+ if isinstance(value, str):
80
+ for member in cls:
81
+ if member.value == value.lower():
82
+ return member
83
+ valid = ", ".join(member.value for member in cls)
84
+ raise ValueError(f"Invalid ConfirmType: '{value}'. Must be one of: {valid}")
@@ -54,6 +54,8 @@ class ChainedAction(BaseAction, ActionListMixin):
54
54
  | None
55
55
  ) = None,
56
56
  *,
57
+ args: tuple[Any, ...] = (),
58
+ kwargs: dict[str, Any] | None = None,
57
59
  hooks: HookManager | None = None,
58
60
  inject_last_result: bool = False,
59
61
  inject_into: str = "last_result",
@@ -67,6 +69,8 @@ class ChainedAction(BaseAction, ActionListMixin):
67
69
  inject_into=inject_into,
68
70
  )
69
71
  ActionListMixin.__init__(self)
72
+ self.args = args
73
+ self.kwargs = kwargs or {}
70
74
  self.auto_inject = auto_inject
71
75
  self.return_list = return_list
72
76
  if actions:
@@ -111,13 +115,16 @@ class ChainedAction(BaseAction, ActionListMixin):
111
115
  if not self.actions:
112
116
  raise EmptyChainError(f"[{self.name}] No actions to execute.")
113
117
 
118
+ combined_args = args + self.args
119
+ combined_kwargs = {**self.kwargs, **kwargs}
120
+
114
121
  shared_context = SharedContext(name=self.name, action=self)
115
122
  if self.shared_context:
116
123
  shared_context.add_result(self.shared_context.last_result())
117
- updated_kwargs = self._maybe_inject_last_result(kwargs)
124
+ updated_kwargs = self._maybe_inject_last_result(combined_kwargs)
118
125
  context = ExecutionContext(
119
126
  name=self.name,
120
- args=args,
127
+ args=combined_args,
121
128
  kwargs=updated_kwargs,
122
129
  action=self,
123
130
  extra={"results": [], "rollback_stack": []},
@@ -136,7 +143,7 @@ class ChainedAction(BaseAction, ActionListMixin):
136
143
  shared_context.current_index = index
137
144
  prepared = action.prepare(shared_context, self.options_manager)
138
145
  try:
139
- result = await prepared(*args, **updated_kwargs)
146
+ result = await prepared(*combined_args, **updated_kwargs)
140
147
  except Exception as error:
141
148
  if index + 1 < len(self.actions) and isinstance(
142
149
  self.actions[index + 1], FallbackAction
@@ -155,10 +162,12 @@ class ChainedAction(BaseAction, ActionListMixin):
155
162
  fallback._skip_in_chain = True
156
163
  else:
157
164
  raise
158
- args, updated_kwargs = self._clear_args()
159
165
  shared_context.add_result(result)
160
166
  context.extra["results"].append(result)
161
- context.extra["rollback_stack"].append(prepared)
167
+ context.extra["rollback_stack"].append(
168
+ (prepared, combined_args, updated_kwargs)
169
+ )
170
+ combined_args, updated_kwargs = self._clear_args()
162
171
 
163
172
  all_results = context.extra["results"]
164
173
  assert (
@@ -171,11 +180,11 @@ class ChainedAction(BaseAction, ActionListMixin):
171
180
  logger.info("[%s] Chain broken: %s", self.name, error)
172
181
  context.exception = error
173
182
  shared_context.add_error(shared_context.current_index, error)
174
- await self._rollback(context.extra["rollback_stack"], *args, **kwargs)
183
+ await self._rollback(context.extra["rollback_stack"])
175
184
  except Exception as error:
176
185
  context.exception = error
177
186
  shared_context.add_error(shared_context.current_index, error)
178
- await self._rollback(context.extra["rollback_stack"], *args, **kwargs)
187
+ await self._rollback(context.extra["rollback_stack"])
179
188
  await self.hooks.trigger(HookType.ON_ERROR, context)
180
189
  raise
181
190
  finally:
@@ -184,7 +193,9 @@ class ChainedAction(BaseAction, ActionListMixin):
184
193
  await self.hooks.trigger(HookType.ON_TEARDOWN, context)
185
194
  er.record(context)
186
195
 
187
- async def _rollback(self, rollback_stack, *args, **kwargs):
196
+ async def _rollback(
197
+ self, rollback_stack: list[tuple[Action, tuple[Any, ...], dict[str, Any]]]
198
+ ):
188
199
  """
189
200
  Roll back all executed actions in reverse order.
190
201
 
@@ -197,12 +208,12 @@ class ChainedAction(BaseAction, ActionListMixin):
197
208
  rollback_stack (list): Actions to roll back.
198
209
  *args, **kwargs: Passed to rollback handlers.
199
210
  """
200
- for action in reversed(rollback_stack):
211
+ for action, args, kwargs in reversed(rollback_stack):
201
212
  rollback = getattr(action, "rollback", None)
202
213
  if rollback:
203
214
  try:
204
215
  logger.warning("[%s] Rolling back...", action.name)
205
- await action.rollback(*args, **kwargs)
216
+ await rollback(*args, **kwargs)
206
217
  except Exception as error:
207
218
  logger.error("[%s] Rollback failed: %s", action.name, error)
208
219
 
@@ -1,11 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
- from enum import Enum
4
3
  from typing import Any
5
4
 
6
5
  from prompt_toolkit import PromptSession
7
6
  from rich.tree import Tree
8
7
 
8
+ from falyx.action.action_types import ConfirmType
9
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
@@ -17,25 +17,6 @@ from falyx.themes import OneColors
17
17
  from falyx.validators import word_validator, words_validator
18
18
 
19
19
 
20
- class ConfirmType(Enum):
21
- """Enum for different confirmation types."""
22
-
23
- YES_NO = "yes_no"
24
- YES_CANCEL = "yes_cancel"
25
- YES_NO_CANCEL = "yes_no_cancel"
26
- TYPE_WORD = "type_word"
27
- OK_CANCEL = "ok_cancel"
28
-
29
- @classmethod
30
- def choices(cls) -> list[ConfirmType]:
31
- """Return a list of all hook type choices."""
32
- return list(cls)
33
-
34
- def __str__(self) -> str:
35
- """Return the string representation of the confirm type."""
36
- return self.value
37
-
38
-
39
20
  class ConfirmAction(BaseAction):
40
21
  """
41
22
  Action to confirm an operation with the user.
@@ -66,7 +47,7 @@ class ConfirmAction(BaseAction):
66
47
  message: str = "Confirm?",
67
48
  confirm_type: ConfirmType | str = ConfirmType.YES_NO,
68
49
  prompt_session: PromptSession | None = None,
69
- confirm: bool = True,
50
+ never_prompt: bool = False,
70
51
  word: str = "CONFIRM",
71
52
  return_last_result: bool = False,
72
53
  inject_last_result: bool = True,
@@ -88,11 +69,11 @@ class ConfirmAction(BaseAction):
88
69
  name=name,
89
70
  inject_last_result=inject_last_result,
90
71
  inject_into=inject_into,
72
+ never_prompt=never_prompt,
91
73
  )
92
74
  self.message = message
93
75
  self.confirm_type = self._coerce_confirm_type(confirm_type)
94
76
  self.prompt_session = prompt_session or PromptSession()
95
- self.confirm = confirm
96
77
  self.word = word
97
78
  self.return_last_result = return_last_result
98
79
 
@@ -131,6 +112,14 @@ class ConfirmAction(BaseAction):
131
112
  validator=word_validator(self.word),
132
113
  )
133
114
  return answer.upper().strip() != "N"
115
+ case ConfirmType.TYPE_WORD_CANCEL:
116
+ answer = await self.prompt_session.prompt_async(
117
+ f"❓ {self.message} [{self.word}] to confirm or [N/n] > ",
118
+ validator=word_validator(self.word),
119
+ )
120
+ if answer.upper().strip() == "N":
121
+ raise CancelSignal(f"Action '{self.name}' was cancelled by the user.")
122
+ return answer.upper().strip() == self.word.upper().strip()
134
123
  case ConfirmType.YES_CANCEL:
135
124
  answer = await confirm_async(
136
125
  self.message,
@@ -150,6 +139,12 @@ class ConfirmAction(BaseAction):
150
139
  if answer.upper() == "C":
151
140
  raise CancelSignal(f"Action '{self.name}' was cancelled by the user.")
152
141
  return answer.upper() == "O"
142
+ case ConfirmType.ACKNOWLEDGE:
143
+ answer = await self.prompt_session.prompt_async(
144
+ f"❓ {self.message} [A]cknowledge > ",
145
+ validator=word_validator("A"),
146
+ )
147
+ return answer.upper().strip() == "A"
153
148
  case _:
154
149
  raise ValueError(f"Unknown confirm_type: {self.confirm_type}")
155
150
 
@@ -165,14 +160,12 @@ class ConfirmAction(BaseAction):
165
160
  try:
166
161
  await self.hooks.trigger(HookType.BEFORE, context)
167
162
  if (
168
- not self.confirm
163
+ self.never_prompt
169
164
  or self.options_manager
170
- and not should_prompt_user(
171
- confirm=self.confirm, options=self.options_manager
172
- )
165
+ and not should_prompt_user(confirm=True, options=self.options_manager)
173
166
  ):
174
167
  logger.debug(
175
- "Skipping confirmation for action '%s' as 'confirm' is False or options manager indicates no prompt.",
168
+ "Skipping confirmation for '%s' due to never_prompt or options_manager settings.",
176
169
  self.name,
177
170
  )
178
171
  if self.return_last_result:
@@ -209,8 +202,8 @@ class ConfirmAction(BaseAction):
209
202
  )
210
203
  tree.add(f"[bold]Message:[/] {self.message}")
211
204
  tree.add(f"[bold]Type:[/] {self.confirm_type.value}")
212
- tree.add(f"[bold]Prompt Required:[/] {'Yes' if self.confirm else 'No'}")
213
- if self.confirm_type == ConfirmType.TYPE_WORD:
205
+ tree.add(f"[bold]Prompt Required:[/] {'No' if self.never_prompt else 'Yes'}")
206
+ if self.confirm_type in (ConfirmType.TYPE_WORD, ConfirmType.TYPE_WORD_CANCEL):
214
207
  tree.add(f"[bold]Confirmation Word:[/] {self.word}")
215
208
  if parent is None:
216
209
  self.console.print(tree)
@@ -91,9 +91,7 @@ class ProcessPoolAction(BaseAction):
91
91
  f"Cannot inject last result into {self.name}: "
92
92
  f"last result is not pickleable."
93
93
  )
94
- print(kwargs)
95
94
  updated_kwargs = self._maybe_inject_last_result(kwargs)
96
- print(updated_kwargs)
97
95
  context = ExecutionContext(
98
96
  name=self.name,
99
97
  args=args,
@@ -395,7 +395,7 @@ class CommandArgumentParser:
395
395
  help: str = "",
396
396
  dest: str | None = None,
397
397
  resolver: BaseAction | None = None,
398
- lazy_resolver: bool = False,
398
+ lazy_resolver: bool = True,
399
399
  ) -> None:
400
400
  """Add an argument to the parser.
401
401
  For `ArgumentAction.ACTION`, `nargs` and `type` determine how many and what kind
@@ -852,6 +852,10 @@ class CommandArgumentParser:
852
852
  and spec.lazy_resolver
853
853
  and from_validate
854
854
  ):
855
+ if not args:
856
+ raise CommandArgumentError(
857
+ f"Missing required argument '{spec.dest}': {spec.get_choice_text()}{help_text}"
858
+ )
855
859
  continue # Lazy resolvers are not validated here
856
860
  raise CommandArgumentError(
857
861
  f"Missing required argument '{spec.dest}': {spec.get_choice_text()}{help_text}"
@@ -0,0 +1 @@
1
+ __version__ = "0.1.62"
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "falyx"
3
- version = "0.1.60"
3
+ version = "0.1.62"
4
4
  description = "Reliable and introspectable async CLI action framework."
5
5
  authors = ["Roland Thomas Jr <roland@rtj.dev>"]
6
6
  license = "MIT"
@@ -1 +0,0 @@
1
- __version__ = "0.1.60"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes