falyx 0.1.16__py3-none-any.whl → 0.1.18__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/.coverage ADDED
Binary file
falyx/command.py CHANGED
@@ -29,14 +29,16 @@ from rich.tree import Tree
29
29
  from falyx.action import Action, ActionGroup, BaseAction, ChainedAction
30
30
  from falyx.context import ExecutionContext
31
31
  from falyx.debug import register_debug_hooks
32
+ from falyx.exceptions import FalyxError
32
33
  from falyx.execution_registry import ExecutionRegistry as er
33
34
  from falyx.hook_manager import HookManager, HookType
34
35
  from falyx.io_action import BaseIOAction
35
36
  from falyx.options_manager import OptionsManager
37
+ from falyx.prompt_utils import should_prompt_user
36
38
  from falyx.retry import RetryPolicy
37
39
  from falyx.retry_utils import enable_retries_recursively
38
40
  from falyx.themes.colors import OneColors
39
- from falyx.utils import _noop, ensure_async, logger
41
+ from falyx.utils import _noop, confirm_async, ensure_async, logger
40
42
 
41
43
  console = Console()
42
44
 
@@ -180,7 +182,10 @@ class Command(BaseModel):
180
182
  self.action.set_options_manager(self.options_manager)
181
183
 
182
184
  async def __call__(self, *args, **kwargs) -> Any:
183
- """Run the action with full hook lifecycle, timing, and error handling."""
185
+ """
186
+ Run the action with full hook lifecycle, timing, error handling,
187
+ confirmation prompts, preview, and spinner integration.
188
+ """
184
189
  self._inject_options_manager()
185
190
  combined_args = args + self.args
186
191
  combined_kwargs = {**self.kwargs, **kwargs}
@@ -191,11 +196,29 @@ class Command(BaseModel):
191
196
  action=self,
192
197
  )
193
198
  self._context = context
199
+
200
+ if should_prompt_user(confirm=self.confirm, options=self.options_manager):
201
+ if self.preview_before_confirm:
202
+ await self.preview()
203
+ if not await confirm_async(self.confirmation_prompt):
204
+ logger.info(f"[Command:{self.key}] ❌ Cancelled by user.")
205
+ raise FalyxError(f"[Command:{self.key}] Cancelled by confirmation.")
206
+
194
207
  context.start_timer()
195
208
 
196
209
  try:
197
210
  await self.hooks.trigger(HookType.BEFORE, context)
198
- result = await self.action(*combined_args, **combined_kwargs)
211
+ if self.spinner:
212
+ with console.status(
213
+ self.spinner_message,
214
+ spinner=self.spinner_type,
215
+ spinner_style=self.spinner_style,
216
+ **self.spinner_kwargs,
217
+ ):
218
+ result = await self.action(*combined_args, **combined_kwargs)
219
+ else:
220
+ result = await self.action(*combined_args, **combined_kwargs)
221
+
199
222
  context.result = result
200
223
  await self.hooks.trigger(HookType.ON_SUCCESS, context)
201
224
  return context.result
falyx/context.py CHANGED
@@ -129,7 +129,7 @@ class ExecutionContext(BaseModel):
129
129
  if self.start_wall:
130
130
  message.append(f"Start: {self.start_wall.strftime('%H:%M:%S')} | ")
131
131
 
132
- if self.end_time:
132
+ if self.end_wall:
133
133
  message.append(f"End: {self.end_wall.strftime('%H:%M:%S')} | ")
134
134
 
135
135
  message.append(f"Duration: {summary['duration']:.3f}s | ")
falyx/falyx.py CHANGED
@@ -55,13 +55,7 @@ from falyx.parsers import get_arg_parsers
55
55
  from falyx.retry import RetryPolicy
56
56
  from falyx.signals import BackSignal, QuitSignal
57
57
  from falyx.themes.colors import OneColors, get_nord_theme
58
- from falyx.utils import (
59
- CaseInsensitiveDict,
60
- chunks,
61
- confirm_async,
62
- get_program_invocation,
63
- logger,
64
- )
58
+ from falyx.utils import CaseInsensitiveDict, chunks, get_program_invocation, logger
65
59
  from falyx.version import __version__
66
60
 
67
61
 
@@ -93,9 +87,8 @@ class Falyx:
93
87
  key_bindings (KeyBindings | None): Custom Prompt Toolkit key bindings.
94
88
  include_history_command (bool): Whether to add a built-in history viewer command.
95
89
  include_help_command (bool): Whether to add a built-in help viewer command.
96
- confirm_on_error (bool): Whether to prompt the user after errors.
97
- never_prompt (bool): Whether to skip confirmation prompts entirely.
98
- always_confirm (bool): Whether to force confirmation prompts for all actions.
90
+ never_prompt (bool): Seed default for `OptionsManager["never_prompt"]`
91
+ force_confirm (bool): Seed default for `OptionsManager["force_confirm"]`
99
92
  cli_args (Namespace | None): Parsed CLI arguments, usually from argparse.
100
93
  options (OptionsManager | None): Declarative option mappings.
101
94
  custom_table (Callable[[Falyx], Table] | Table | None): Custom menu table generator.
@@ -123,9 +116,8 @@ class Falyx:
123
116
  key_bindings: KeyBindings | None = None,
124
117
  include_history_command: bool = True,
125
118
  include_help_command: bool = True,
126
- confirm_on_error: bool = True,
127
119
  never_prompt: bool = False,
128
- always_confirm: bool = False,
120
+ force_confirm: bool = False,
129
121
  cli_args: Namespace | None = None,
130
122
  options: OptionsManager | None = None,
131
123
  render_menu: Callable[["Falyx"], None] | None = None,
@@ -150,16 +142,15 @@ class Falyx:
150
142
  self.last_run_command: Command | None = None
151
143
  self.key_bindings: KeyBindings = key_bindings or KeyBindings()
152
144
  self.bottom_bar: BottomBar | str | Callable[[], None] = bottom_bar
153
- self.confirm_on_error: bool = confirm_on_error
154
145
  self._never_prompt: bool = never_prompt
155
- self._always_confirm: bool = always_confirm
146
+ self._force_confirm: bool = force_confirm
156
147
  self.cli_args: Namespace | None = cli_args
157
148
  self.render_menu: Callable[["Falyx"], None] | None = render_menu
158
149
  self.custom_table: Callable[["Falyx"], Table] | Table | None = custom_table
159
- self.set_options(cli_args, options)
150
+ self.validate_options(cli_args, options)
160
151
  self._session: PromptSession | None = None
161
152
 
162
- def set_options(
153
+ def validate_options(
163
154
  self,
164
155
  cli_args: Namespace | None,
165
156
  options: OptionsManager | None = None,
@@ -175,8 +166,6 @@ class Falyx:
175
166
  assert isinstance(
176
167
  cli_args, Namespace
177
168
  ), "CLI arguments must be a Namespace object."
178
- if options is None:
179
- self.options.from_namespace(cli_args, "cli_args")
180
169
 
181
170
  if not isinstance(self.options, OptionsManager):
182
171
  raise FalyxError("Options must be an instance of OptionsManager.")
@@ -705,33 +694,8 @@ class Falyx:
705
694
  self.console.print(
706
695
  f"[{OneColors.LIGHT_YELLOW}]⚠️ Unknown command '{choice}'[/]"
707
696
  )
708
- logger.warning(f"⚠️ Command '{choice}' not found.")
709
697
  return None
710
698
 
711
- async def _should_run_action(self, selected_command: Command) -> bool:
712
- if self._never_prompt:
713
- return True
714
-
715
- if self.cli_args and getattr(self.cli_args, "skip_confirm", False):
716
- return True
717
-
718
- if (
719
- self._always_confirm
720
- or selected_command.confirm
721
- or self.cli_args
722
- and getattr(self.cli_args, "force_confirm", False)
723
- ):
724
- if selected_command.preview_before_confirm:
725
- await selected_command.preview()
726
- confirm_answer = await confirm_async(selected_command.confirmation_prompt)
727
-
728
- if confirm_answer:
729
- logger.info(f"[{selected_command.description}]🔐 confirmed.")
730
- else:
731
- logger.info(f"[{selected_command.description}]❌ cancelled.")
732
- return confirm_answer
733
- return True
734
-
735
699
  def _create_context(self, selected_command: Command) -> ExecutionContext:
736
700
  """Creates a context dictionary for the selected command."""
737
701
  return ExecutionContext(
@@ -741,16 +705,6 @@ class Falyx:
741
705
  action=selected_command,
742
706
  )
743
707
 
744
- async def _run_action_with_spinner(self, command: Command) -> Any:
745
- """Runs the action of the selected command with a spinner."""
746
- with self.console.status(
747
- command.spinner_message,
748
- spinner=command.spinner_type,
749
- spinner_style=command.spinner_style,
750
- **command.spinner_kwargs,
751
- ):
752
- return await command()
753
-
754
708
  async def _handle_action_error(
755
709
  self, selected_command: Command, error: Exception
756
710
  ) -> None:
@@ -784,19 +738,12 @@ class Falyx:
784
738
  logger.info(f"🔙 Back selected: exiting {self.get_title()}")
785
739
  return False
786
740
 
787
- if not await self._should_run_action(selected_command):
788
- logger.info(f"{selected_command.description} cancelled.")
789
- return True
790
-
791
741
  context = self._create_context(selected_command)
792
742
  context.start_timer()
793
743
  try:
794
744
  await self.hooks.trigger(HookType.BEFORE, context)
795
745
 
796
- if selected_command.spinner:
797
- result = await self._run_action_with_spinner(selected_command)
798
- else:
799
- result = await selected_command()
746
+ result = await selected_command()
800
747
  context.result = result
801
748
  await self.hooks.trigger(HookType.ON_SUCCESS, context)
802
749
  except Exception as error:
@@ -824,22 +771,11 @@ class Falyx:
824
771
  selected_command.description,
825
772
  )
826
773
 
827
- if not await self._should_run_action(selected_command):
828
- logger.info("[run_key] ❌ Cancelled: %s", selected_command.description)
829
- raise FalyxError(
830
- f"[run_key] '{selected_command.description}' "
831
- "cancelled by confirmation."
832
- )
833
-
834
774
  context = self._create_context(selected_command)
835
775
  context.start_timer()
836
776
  try:
837
777
  await self.hooks.trigger(HookType.BEFORE, context)
838
-
839
- if selected_command.spinner:
840
- result = await self._run_action_with_spinner(selected_command)
841
- else:
842
- result = await selected_command()
778
+ result = await selected_command()
843
779
  context.result = result
844
780
 
845
781
  await self.hooks.trigger(HookType.ON_SUCCESS, context)
@@ -939,6 +875,13 @@ class Falyx:
939
875
  """Run Falyx CLI with structured subcommands."""
940
876
  if not self.cli_args:
941
877
  self.cli_args = get_arg_parsers().root.parse_args()
878
+ self.options.from_namespace(self.cli_args, "cli_args")
879
+
880
+ if not self.options.get("never_prompt"):
881
+ self.options.set("never_prompt", self._never_prompt)
882
+
883
+ if not self.options.get("force_confirm"):
884
+ self.options.set("force_confirm", self._force_confirm)
942
885
 
943
886
  if self.cli_args.verbose:
944
887
  logging.getLogger("falyx").setLevel(logging.DEBUG)
@@ -947,9 +890,6 @@ class Falyx:
947
890
  logger.debug("✅ Enabling global debug hooks for all commands")
948
891
  self.register_all_with_debug_hooks()
949
892
 
950
- if self.cli_args.never_prompt:
951
- self._never_prompt = True
952
-
953
893
  if self.cli_args.command == "list":
954
894
  await self._show_help()
955
895
  sys.exit(0)
falyx/http_action.py CHANGED
@@ -97,30 +97,36 @@ class HTTPAction(Action):
97
97
  )
98
98
 
99
99
  async def _request(self, *args, **kwargs) -> dict[str, Any]:
100
- assert self.shared_context is not None, "SharedContext is not set"
101
- context: SharedContext = self.shared_context
102
-
103
- session = context.get("http_session")
104
- if session is None:
100
+ # TODO: Add check for HOOK registration
101
+ if self.shared_context:
102
+ context: SharedContext = self.shared_context
103
+ session = context.get("http_session")
104
+ if session is None:
105
+ session = aiohttp.ClientSession()
106
+ context.set("http_session", session)
107
+ context.set("_session_should_close", True)
108
+ else:
105
109
  session = aiohttp.ClientSession()
106
- context.set("http_session", session)
107
- context.set("_session_should_close", True)
108
-
109
- async with session.request(
110
- self.method,
111
- self.url,
112
- headers=self.headers,
113
- params=self.params,
114
- json=self.json,
115
- data=self.data,
116
- ) as response:
117
- body = await response.text()
118
- return {
119
- "status": response.status,
120
- "url": str(response.url),
121
- "headers": dict(response.headers),
122
- "body": body,
123
- }
110
+
111
+ try:
112
+ async with session.request(
113
+ self.method,
114
+ self.url,
115
+ headers=self.headers,
116
+ params=self.params,
117
+ json=self.json,
118
+ data=self.data,
119
+ ) as response:
120
+ body = await response.text()
121
+ return {
122
+ "status": response.status,
123
+ "url": str(response.url),
124
+ "headers": dict(response.headers),
125
+ "body": body,
126
+ }
127
+ finally:
128
+ if not self.shared_context:
129
+ await session.close()
124
130
 
125
131
  async def preview(self, parent: Tree | None = None):
126
132
  label = [
falyx/prompt_utils.py ADDED
@@ -0,0 +1,19 @@
1
+ from falyx.options_manager import OptionsManager
2
+
3
+
4
+ def should_prompt_user(
5
+ *,
6
+ confirm: bool,
7
+ options: OptionsManager,
8
+ namespace: str = "cli_args",
9
+ ):
10
+ """Determine whether to prompt the user for confirmation based on command and global options."""
11
+ never_prompt = options.get("never_prompt", False, namespace)
12
+ always_confirm = options.get("always_confirm", False, namespace)
13
+ force_confirm = options.get("force_confirm", False, namespace)
14
+ skip_confirm = options.get("skip_confirm", False, namespace)
15
+
16
+ if never_prompt or skip_confirm:
17
+ return False
18
+
19
+ return confirm or always_confirm or force_confirm
falyx/selection.py CHANGED
@@ -145,7 +145,7 @@ def render_selection_indexed_table(
145
145
  chunks(range(len(selections)), columns), chunks(selections, columns)
146
146
  ):
147
147
  row = [
148
- formatter(index, selection) if formatter else f"{index}: {selection}"
148
+ formatter(index, selection) if formatter else f"[{index}] {selection}"
149
149
  for index, selection in zip(indexes, chunk)
150
150
  ]
151
151
  table.add_row(*row)
falyx/selection_action.py CHANGED
@@ -87,17 +87,23 @@ class SelectionAction(BaseAction):
87
87
  if isinstance(self.selections, dict):
88
88
  if maybe_result in self.selections:
89
89
  effective_default = maybe_result
90
+ elif self.inject_last_result:
91
+ logger.warning(
92
+ "[%s] Injected last result '%s' not found in selections",
93
+ self.name,
94
+ maybe_result,
95
+ )
90
96
  elif isinstance(self.selections, list):
91
97
  if maybe_result.isdigit() and int(maybe_result) in range(
92
98
  len(self.selections)
93
99
  ):
94
100
  effective_default = maybe_result
95
- elif self.inject_last_result:
96
- logger.warning(
97
- "[%s] Injected last result '%s' not found in selections",
98
- self.name,
99
- maybe_result,
100
- )
101
+ elif self.inject_last_result:
102
+ logger.warning(
103
+ "[%s] Injected last result '%s' not found in selections",
104
+ self.name,
105
+ maybe_result,
106
+ )
101
107
 
102
108
  if self.never_prompt and not effective_default:
103
109
  raise ValueError(
falyx/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.1.16"
1
+ __version__ = "0.1.18"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: falyx
3
- Version: 0.1.16
3
+ Version: 0.1.18
4
4
  Summary: Reliable and introspectable async CLI action framework.
5
5
  License: MIT
6
6
  Author: Roland Thomas Jr
@@ -1,37 +1,39 @@
1
+ falyx/.coverage,sha256=DNx1Ew1vSvuIcKko7httsyL62erJxVQ6CKtuJKxRVj4,53248
1
2
  falyx/.pytyped,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
3
  falyx/__init__.py,sha256=dYRamQJlT1Zoy5Uu1uG4NCV05Xk98nN1LAQrSR1CT2A,643
3
4
  falyx/__main__.py,sha256=pXxXLlDot33dc4mR11Njpr4M_xbSTdKEqKWMS2aUqfk,2195
4
5
  falyx/action.py,sha256=7WuiJyRlhXqnGxfkXWIcAppzsIZ38OyZ9SKs-4hdhAg,31170
5
6
  falyx/bottom_bar.py,sha256=83KSElBU7sFJqUhKyfef6gYfPnNDM8V7bph60cY5ARQ,7384
6
- falyx/command.py,sha256=kS4VvK4C7c3AO4fldt4QTS6yzKQAyWtvgWGNoKBoxNA,10939
7
+ falyx/command.py,sha256=8Db_A3WDxmFNuZUb4_PO0UlpnfZFAUUEFkgC7B8v1Jk,11891
7
8
  falyx/config.py,sha256=czt_EjOCT0lzWcoRE2F-oS2k1AVBFdiNuTcQXHlUVD0,4534
8
- falyx/context.py,sha256=M8hpYpTlY5Lg2QxgiTcp2RIajemav8mVmGr11VKM07g,10070
9
+ falyx/context.py,sha256=Dm7HV-eigU-aTv5ERah6Ow9fIRdrOsB1G6ETPIu42Gw,10070
9
10
  falyx/debug.py,sha256=-jbTti29UC5zP9qQlWs3TbkOQR2f3zKSuNluh-r56wY,1551
10
11
  falyx/exceptions.py,sha256=YVbhPp2BNvZoO_xqeGSRKHVQ2rdLOLf1HCjH4JTj9w8,776
11
12
  falyx/execution_registry.py,sha256=xB2SJuEoDxxfwUmKXLAZQSrVoNPXwnVML98sTgwBqRI,2869
12
- falyx/falyx.py,sha256=tDjs78Y0Ifv8YgAR8ABKRW_Q50I8jkuWdt3-Rta4BNY,40672
13
+ falyx/falyx.py,sha256=NdQdaQcG3V4D9lgMOrgH_8_w3T-MqUiGNeyBZXKpTbQ,38522
13
14
  falyx/hook_manager.py,sha256=E9Vk4bdoUTeXPQ_BQEvY2Jt-jUAusc40LI8JDy3NLUw,2381
14
15
  falyx/hooks.py,sha256=9zXk62DsJLJrmwTdyeNy5s-rVRvl8feuYRrfMmz6cVQ,2802
15
- falyx/http_action.py,sha256=PZhzoAsOWCT1K1IGYwagh_NP7MTsSqkkAXm9NnI2MJE,5459
16
+ falyx/http_action.py,sha256=thZXnypT7ZUshBg4c9F-c8Pk7-yCjvHmMwHR9cqg_Xw,5715
16
17
  falyx/init.py,sha256=-xly0VkOo0lTDaAZyRGEPD8YSP0ySxJtxFs79faZhO4,1799
17
18
  falyx/io_action.py,sha256=MsvKj63VmABqdtDURgfGOpiudpTPOL0N_hlmTbYVtcA,10662
18
19
  falyx/menu_action.py,sha256=sHKcLYHCl3yzyVjL6cu97NtGVJhPZO4oXkQ4Lb5z3C8,8060
19
20
  falyx/options_manager.py,sha256=yYpn-moYN-bRYgMLccmi_de4mUzhTT7cv_bR2FFWZ8c,2798
20
21
  falyx/parsers.py,sha256=Ki0rn2wryPDmMI9WUNBLQ5J5Y64BNten0POMZM8wPKU,5189
22
+ falyx/prompt_utils.py,sha256=QgPJpGZfXJlX9SoZNYPnHcUKQ54fZ-Xz5ahNpTZkjcU,647
21
23
  falyx/retry.py,sha256=GncBUiDDfDHUvLsWsWQw2Nq2XYL0TR0Fne3iXPzvQ48,3551
22
24
  falyx/retry_utils.py,sha256=SN5apcsg71IG2-KylysqdJd-PkPBLoCVwsgrSTF9wrQ,666
23
25
  falyx/select_files_action.py,sha256=dJYAnwTBLHIWs-4h1sN2pWBBYykoTO717FA4InrJgeA,2409
24
- falyx/selection.py,sha256=0uSs6Qz5rZBrQACTco4tLVbR3_VvyYU9Dkp4PAg7pVk,9683
25
- falyx/selection_action.py,sha256=oSRSs3RpxBhd9s_lVIRnCfRqGt-FBLMNwSHf0x1ucqs,7888
26
+ falyx/selection.py,sha256=JGb8KeU6SH9qep9AIgBclRfYSds2tqucgyHcJ5bdZcw,9684
27
+ falyx/selection_action.py,sha256=G1QjyTrZKtAupOobqwa2sG-7zjSN71HgSlJLiZrpp_8,8147
26
28
  falyx/signal_action.py,sha256=wfhW9miSUj9MUoc1WOyk4tU9CtYKAXusHxQdBPYLoyQ,829
27
29
  falyx/signals.py,sha256=tlUbz3x6z3rYlUggan_Ntoy4bU5RbOd8UfR4cNcV6kQ,694
28
30
  falyx/tagged_table.py,sha256=sn2kosRRpcpeMB8vKk47c9yjpffSz_9FXH_e6kw15mA,1019
29
31
  falyx/themes/colors.py,sha256=4aaeAHJetmeNInI0Zytg4E3YqKfPFelpf04vtjSvsS8,19776
30
32
  falyx/utils.py,sha256=b1GQ3ooz4Io3zPE7MsoDm7j42AioTG-ZcWH-N2TRpbI,7710
31
33
  falyx/validators.py,sha256=NMxqCk8Fr8HQGVDYpg8B_JRk5SKR41E_G9gj1YfQnxg,1316
32
- falyx/version.py,sha256=yF88-8vL8keLe6gCTumymw0UoMkWkSrJnzLru4zBCLQ,23
33
- falyx-0.1.16.dist-info/LICENSE,sha256=B0yqgaHuSdhN7T3OBmgQSiDTy8HqT5Oe_dLypRe4Ra4,1073
34
- falyx-0.1.16.dist-info/METADATA,sha256=eWtPMP6LJCKeMZtZwHc-M5DRLdYzrVq0TGhCD1kQUwQ,5484
35
- falyx-0.1.16.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
36
- falyx-0.1.16.dist-info/entry_points.txt,sha256=j8owOSl2j1Ss8DtGMnKfgehKaolqnIPhVFHaUBLUnMs,45
37
- falyx-0.1.16.dist-info/RECORD,,
34
+ falyx/version.py,sha256=6BiuMUkhwQp6bzUZSF8np8F1NwCltEtK0sPBF__tepU,23
35
+ falyx-0.1.18.dist-info/LICENSE,sha256=B0yqgaHuSdhN7T3OBmgQSiDTy8HqT5Oe_dLypRe4Ra4,1073
36
+ falyx-0.1.18.dist-info/METADATA,sha256=tq_ZdtoqSKsn3klMc-ykqVkm2GxiVC96OJOvLKy028s,5484
37
+ falyx-0.1.18.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
38
+ falyx-0.1.18.dist-info/entry_points.txt,sha256=j8owOSl2j1Ss8DtGMnKfgehKaolqnIPhVFHaUBLUnMs,45
39
+ falyx-0.1.18.dist-info/RECORD,,
File without changes