falyx 0.1.49__py3-none-any.whl → 0.1.51__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
@@ -14,7 +14,7 @@ from typing import Any
14
14
 
15
15
  from falyx.config import loader
16
16
  from falyx.falyx import Falyx
17
- from falyx.parsers import CommandArgumentParser, get_root_parser, get_subparsers
17
+ from falyx.parser import CommandArgumentParser, get_root_parser, get_subparsers
18
18
 
19
19
 
20
20
  def find_falyx_config() -> Path | None:
falyx/action/action.py CHANGED
@@ -157,6 +157,6 @@ class Action(BaseAction):
157
157
  return (
158
158
  f"Action(name={self.name!r}, action="
159
159
  f"{getattr(self._action, '__name__', repr(self._action))}, "
160
- f"args={self.args!r}, kwargs={self.kwargs!r}, "
161
- f"retry={self.retry_policy.enabled})"
160
+ f"retry={self.retry_policy.enabled}, "
161
+ f"rollback={self.rollback is not None})"
162
162
  )
@@ -11,7 +11,7 @@ from falyx.context import ExecutionContext, SharedContext
11
11
  from falyx.execution_registry import ExecutionRegistry as er
12
12
  from falyx.hook_manager import Hook, HookManager, HookType
13
13
  from falyx.logger import logger
14
- from falyx.parsers.utils import same_argument_definitions
14
+ from falyx.parser.utils import same_argument_definitions
15
15
  from falyx.themes.colors import OneColors
16
16
 
17
17
 
falyx/action/io_action.py CHANGED
@@ -93,10 +93,7 @@ class BaseIOAction(BaseAction):
93
93
  if self.inject_last_result and self.shared_context:
94
94
  return self.shared_context.last_result()
95
95
 
96
- logger.debug(
97
- "[%s] No input provided and no last result found for injection.", self.name
98
- )
99
- raise FalyxError("No input provided and no last result to inject.")
96
+ return ""
100
97
 
101
98
  def get_infer_target(self) -> tuple[Callable[..., Any] | None, dict[str, Any] | None]:
102
99
  return None, None
@@ -111,7 +111,7 @@ class MenuAction(BaseAction):
111
111
  key = effective_default
112
112
  if not self.never_prompt:
113
113
  table = self._build_table()
114
- key = await prompt_for_selection(
114
+ key_ = await prompt_for_selection(
115
115
  self.menu_options.keys(),
116
116
  table,
117
117
  default_selection=self.default_selection,
@@ -120,6 +120,10 @@ class MenuAction(BaseAction):
120
120
  prompt_message=self.prompt_message,
121
121
  show_table=self.show_table,
122
122
  )
123
+ if isinstance(key_, str):
124
+ key = key_
125
+ else:
126
+ assert False, "Unreachable, MenuAction only supports single selection"
123
127
  option = self.menu_options[key]
124
128
  result = await option.action(*args, **kwargs)
125
129
  context.result = result
@@ -14,7 +14,7 @@ from falyx.context import ExecutionContext, SharedContext
14
14
  from falyx.execution_registry import ExecutionRegistry as er
15
15
  from falyx.hook_manager import HookManager, HookType
16
16
  from falyx.logger import logger
17
- from falyx.parsers.utils import same_argument_definitions
17
+ from falyx.parser.utils import same_argument_definitions
18
18
  from falyx.themes import OneColors
19
19
 
20
20
 
@@ -66,6 +66,9 @@ class SelectFileAction(BaseAction):
66
66
  style: str = OneColors.WHITE,
67
67
  suffix_filter: str | None = None,
68
68
  return_type: FileReturnType | str = FileReturnType.PATH,
69
+ number_selections: int | str = 1,
70
+ separator: str = ",",
71
+ allow_duplicates: bool = False,
69
72
  console: Console | None = None,
70
73
  prompt_session: PromptSession | None = None,
71
74
  ):
@@ -76,6 +79,9 @@ class SelectFileAction(BaseAction):
76
79
  self.prompt_message = prompt_message
77
80
  self.suffix_filter = suffix_filter
78
81
  self.style = style
82
+ self.number_selections = number_selections
83
+ self.separator = separator
84
+ self.allow_duplicates = allow_duplicates
79
85
  if isinstance(console, Console):
80
86
  self.console = console
81
87
  elif console:
@@ -83,6 +89,21 @@ class SelectFileAction(BaseAction):
83
89
  self.prompt_session = prompt_session or PromptSession()
84
90
  self.return_type = self._coerce_return_type(return_type)
85
91
 
92
+ @property
93
+ def number_selections(self) -> int | str:
94
+ return self._number_selections
95
+
96
+ @number_selections.setter
97
+ def number_selections(self, value: int | str):
98
+ if isinstance(value, int) and value > 0:
99
+ self._number_selections: int | str = value
100
+ elif isinstance(value, str):
101
+ if value not in ("*"):
102
+ raise ValueError("number_selections string must be one of '*'")
103
+ self._number_selections = value
104
+ else:
105
+ raise ValueError("number_selections must be a positive integer or one of '*'")
106
+
86
107
  def _coerce_return_type(self, return_type: FileReturnType | str) -> FileReturnType:
87
108
  if isinstance(return_type, FileReturnType):
88
109
  return return_type
@@ -163,18 +184,25 @@ class SelectFileAction(BaseAction):
163
184
  title=self.title, selections=options | cancel_option, columns=self.columns
164
185
  )
165
186
 
166
- key = await prompt_for_selection(
187
+ keys = await prompt_for_selection(
167
188
  (options | cancel_option).keys(),
168
189
  table,
169
190
  console=self.console,
170
191
  prompt_session=self.prompt_session,
171
192
  prompt_message=self.prompt_message,
193
+ number_selections=self.number_selections,
194
+ separator=self.separator,
195
+ allow_duplicates=self.allow_duplicates,
196
+ cancel_key=cancel_key,
172
197
  )
173
198
 
174
- if key == cancel_key:
175
- raise CancelSignal("User canceled the selection.")
199
+ if isinstance(keys, str):
200
+ if keys == cancel_key:
201
+ raise CancelSignal("User canceled the selection.")
202
+ result = options[keys].value
203
+ elif isinstance(keys, list):
204
+ result = [options[key].value for key in keys]
176
205
 
177
- result = options[key].value
178
206
  context.result = result
179
207
  await self.hooks.trigger(HookType.ON_SUCCESS, context)
180
208
  return result
@@ -48,6 +48,9 @@ class SelectionAction(BaseAction):
48
48
  columns: int = 5,
49
49
  prompt_message: str = "Select > ",
50
50
  default_selection: str = "",
51
+ number_selections: int | str = 1,
52
+ separator: str = ",",
53
+ allow_duplicates: bool = False,
51
54
  inject_last_result: bool = False,
52
55
  inject_into: str = "last_result",
53
56
  return_type: SelectionReturnType | str = "value",
@@ -73,9 +76,26 @@ class SelectionAction(BaseAction):
73
76
  raise ValueError("`console` must be an instance of `rich.console.Console`")
74
77
  self.prompt_session = prompt_session or PromptSession()
75
78
  self.default_selection = default_selection
79
+ self.number_selections = number_selections
80
+ self.separator = separator
81
+ self.allow_duplicates = allow_duplicates
76
82
  self.prompt_message = prompt_message
77
83
  self.show_table = show_table
78
- self.cancel_key = self._find_cancel_key()
84
+
85
+ @property
86
+ def number_selections(self) -> int | str:
87
+ return self._number_selections
88
+
89
+ @number_selections.setter
90
+ def number_selections(self, value: int | str):
91
+ if isinstance(value, int) and value > 0:
92
+ self._number_selections: int | str = value
93
+ elif isinstance(value, str):
94
+ if value not in ("*"):
95
+ raise ValueError("number_selections string must be '*'")
96
+ self._number_selections = value
97
+ else:
98
+ raise ValueError("number_selections must be a positive integer or '*'")
79
99
 
80
100
  def _coerce_return_type(
81
101
  self, return_type: SelectionReturnType | str
@@ -156,6 +176,38 @@ class SelectionAction(BaseAction):
156
176
  def get_infer_target(self) -> tuple[None, None]:
157
177
  return None, None
158
178
 
179
+ def _get_result_from_keys(self, keys: str | list[str]) -> Any:
180
+ if not isinstance(self.selections, dict):
181
+ raise TypeError("Selections must be a dictionary to get result by keys.")
182
+ if self.return_type == SelectionReturnType.KEY:
183
+ result: Any = keys
184
+ elif self.return_type == SelectionReturnType.VALUE:
185
+ if isinstance(keys, list):
186
+ result = [self.selections[key].value for key in keys]
187
+ elif isinstance(keys, str):
188
+ result = self.selections[keys].value
189
+ elif self.return_type == SelectionReturnType.ITEMS:
190
+ if isinstance(keys, list):
191
+ result = {key: self.selections[key] for key in keys}
192
+ elif isinstance(keys, str):
193
+ result = {keys: self.selections[keys]}
194
+ elif self.return_type == SelectionReturnType.DESCRIPTION:
195
+ if isinstance(keys, list):
196
+ result = [self.selections[key].description for key in keys]
197
+ elif isinstance(keys, str):
198
+ result = self.selections[keys].description
199
+ elif self.return_type == SelectionReturnType.DESCRIPTION_VALUE:
200
+ if isinstance(keys, list):
201
+ result = {
202
+ self.selections[key].description: self.selections[key].value
203
+ for key in keys
204
+ }
205
+ elif isinstance(keys, str):
206
+ result = {self.selections[keys].description: self.selections[keys].value}
207
+ else:
208
+ raise ValueError(f"Unsupported return type: {self.return_type}")
209
+ return result
210
+
159
211
  async def _run(self, *args, **kwargs) -> Any:
160
212
  kwargs = self._maybe_inject_last_result(kwargs)
161
213
  context = ExecutionContext(
@@ -191,7 +243,7 @@ class SelectionAction(BaseAction):
191
243
  if self.never_prompt and not effective_default:
192
244
  raise ValueError(
193
245
  f"[{self.name}] 'never_prompt' is True but no valid default_selection "
194
- "was provided."
246
+ "or usable last_result was available."
195
247
  )
196
248
 
197
249
  context.start_timer()
@@ -206,7 +258,7 @@ class SelectionAction(BaseAction):
206
258
  formatter=self.cancel_formatter,
207
259
  )
208
260
  if not self.never_prompt:
209
- index: int | str = await prompt_for_index(
261
+ indices: int | list[int] = await prompt_for_index(
210
262
  len(self.selections),
211
263
  table,
212
264
  default_selection=effective_default,
@@ -214,12 +266,30 @@ class SelectionAction(BaseAction):
214
266
  prompt_session=self.prompt_session,
215
267
  prompt_message=self.prompt_message,
216
268
  show_table=self.show_table,
269
+ number_selections=self.number_selections,
270
+ separator=self.separator,
271
+ allow_duplicates=self.allow_duplicates,
272
+ cancel_key=self.cancel_key,
217
273
  )
218
274
  else:
219
- index = effective_default
220
- if int(index) == int(self.cancel_key):
275
+ if effective_default:
276
+ indices = int(effective_default)
277
+ else:
278
+ raise ValueError(
279
+ f"[{self.name}] 'never_prompt' is True but no valid "
280
+ "default_selection was provided."
281
+ )
282
+
283
+ if indices == int(self.cancel_key):
221
284
  raise CancelSignal("User cancelled the selection.")
222
- result: Any = self.selections[int(index)]
285
+ if isinstance(indices, list):
286
+ result: str | list[str] = [
287
+ self.selections[index] for index in indices
288
+ ]
289
+ elif isinstance(indices, int):
290
+ result = self.selections[indices]
291
+ else:
292
+ assert False, "unreachable"
223
293
  elif isinstance(self.selections, dict):
224
294
  cancel_option = {
225
295
  self.cancel_key: SelectionOption(
@@ -232,7 +302,7 @@ class SelectionAction(BaseAction):
232
302
  columns=self.columns,
233
303
  )
234
304
  if not self.never_prompt:
235
- key = await prompt_for_selection(
305
+ keys = await prompt_for_selection(
236
306
  (self.selections | cancel_option).keys(),
237
307
  table,
238
308
  default_selection=effective_default,
@@ -240,25 +310,17 @@ class SelectionAction(BaseAction):
240
310
  prompt_session=self.prompt_session,
241
311
  prompt_message=self.prompt_message,
242
312
  show_table=self.show_table,
313
+ number_selections=self.number_selections,
314
+ separator=self.separator,
315
+ allow_duplicates=self.allow_duplicates,
316
+ cancel_key=self.cancel_key,
243
317
  )
244
318
  else:
245
- key = effective_default
246
- if key == self.cancel_key:
319
+ keys = effective_default
320
+ if keys == self.cancel_key:
247
321
  raise CancelSignal("User cancelled the selection.")
248
- if self.return_type == SelectionReturnType.KEY:
249
- result = key
250
- elif self.return_type == SelectionReturnType.VALUE:
251
- result = self.selections[key].value
252
- elif self.return_type == SelectionReturnType.ITEMS:
253
- result = {key: self.selections[key]}
254
- elif self.return_type == SelectionReturnType.DESCRIPTION:
255
- result = self.selections[key].description
256
- elif self.return_type == SelectionReturnType.DESCRIPTION_VALUE:
257
- result = {
258
- self.selections[key].description: self.selections[key].value
259
- }
260
- else:
261
- raise ValueError(f"Unsupported return type: {self.return_type}")
322
+
323
+ result = self._get_result_from_keys(keys)
262
324
  else:
263
325
  raise TypeError(
264
326
  "'selections' must be a list[str] or dict[str, Any], "
@@ -29,6 +29,7 @@ class UserInputAction(BaseAction):
29
29
  name: str,
30
30
  *,
31
31
  prompt_text: str = "Input > ",
32
+ default_text: str = "",
32
33
  validator: Validator | None = None,
33
34
  console: Console | None = None,
34
35
  prompt_session: PromptSession | None = None,
@@ -45,6 +46,7 @@ class UserInputAction(BaseAction):
45
46
  elif console:
46
47
  raise ValueError("`console` must be an instance of `rich.console.Console`")
47
48
  self.prompt_session = prompt_session or PromptSession()
49
+ self.default_text = default_text
48
50
 
49
51
  def get_infer_target(self) -> tuple[None, None]:
50
52
  return None, None
@@ -67,6 +69,7 @@ class UserInputAction(BaseAction):
67
69
  answer = await self.prompt_session.prompt_async(
68
70
  prompt_text,
69
71
  validator=self.validator,
72
+ default=kwargs.get("default_text", self.default_text),
70
73
  )
71
74
  context.result = answer
72
75
  await self.hooks.trigger(HookType.ON_SUCCESS, context)
falyx/command.py CHANGED
@@ -34,8 +34,8 @@ from falyx.execution_registry import ExecutionRegistry as er
34
34
  from falyx.hook_manager import HookManager, HookType
35
35
  from falyx.logger import logger
36
36
  from falyx.options_manager import OptionsManager
37
- from falyx.parsers.argparse import CommandArgumentParser
38
- from falyx.parsers.signature import infer_args_from_func
37
+ from falyx.parser.argparse import CommandArgumentParser
38
+ from falyx.parser.signature import infer_args_from_func
39
39
  from falyx.prompt_utils import confirm_async, should_prompt_user
40
40
  from falyx.protocols import ArgParserProtocol
41
41
  from falyx.retry import RetryPolicy
falyx/context.py CHANGED
@@ -70,7 +70,7 @@ class ExecutionContext(BaseModel):
70
70
 
71
71
  name: str
72
72
  args: tuple = ()
73
- kwargs: dict = {}
73
+ kwargs: dict = Field(default_factory=dict)
74
74
  action: Any
75
75
  result: Any | None = None
76
76
  exception: Exception | None = None
@@ -120,6 +120,17 @@ class ExecutionContext(BaseModel):
120
120
  def status(self) -> str:
121
121
  return "OK" if self.success else "ERROR"
122
122
 
123
+ @property
124
+ def signature(self) -> str:
125
+ """
126
+ Returns a string representation of the action signature, including
127
+ its name and arguments.
128
+ """
129
+ args = ", ".join(map(repr, self.args))
130
+ kwargs = ", ".join(f"{key}={value!r}" for key, value in self.kwargs.items())
131
+ signature = ", ".join(filter(None, [args, kwargs]))
132
+ return f"{self.action} ({signature})"
133
+
123
134
  def as_dict(self) -> dict:
124
135
  return {
125
136
  "name": self.name,
falyx/debug.py CHANGED
@@ -8,7 +8,7 @@ from falyx.logger import logger
8
8
  def log_before(context: ExecutionContext):
9
9
  """Log the start of an action."""
10
10
  args = ", ".join(map(repr, context.args))
11
- kwargs = ", ".join(f"{k}={v!r}" for k, v in context.kwargs.items())
11
+ kwargs = ", ".join(f"{key}={value!r}" for key, value in context.kwargs.items())
12
12
  signature = ", ".join(filter(None, [args, kwargs]))
13
13
  logger.info("[%s] Starting -> %s(%s)", context.name, context.action, signature)
14
14
 
@@ -30,7 +30,7 @@ from __future__ import annotations
30
30
  from collections import defaultdict
31
31
  from datetime import datetime
32
32
  from threading import Lock
33
- from typing import Any, Literal
33
+ from typing import Literal
34
34
 
35
35
  from rich import box
36
36
  from rich.console import Console
@@ -111,8 +111,8 @@ class ExecutionRegistry:
111
111
  def summary(
112
112
  cls,
113
113
  name: str = "",
114
- index: int = -1,
115
- result: int = -1,
114
+ index: int | None = None,
115
+ result_index: int | None = None,
116
116
  clear: bool = False,
117
117
  last_result: bool = False,
118
118
  status: Literal["all", "success", "error"] = "all",
@@ -138,15 +138,19 @@ class ExecutionRegistry:
138
138
  )
139
139
  return
140
140
 
141
- if result and result >= 0:
141
+ if result_index is not None and result_index >= 0:
142
142
  try:
143
- result_context = cls._store_by_index[result]
143
+ result_context = cls._store_by_index[result_index]
144
144
  except KeyError:
145
145
  cls._console.print(
146
- f"[{OneColors.DARK_RED}]❌ No execution found for index {index}."
146
+ f"[{OneColors.DARK_RED}]❌ No execution found for index {result_index}."
147
147
  )
148
148
  return
149
- cls._console.print(result_context.result)
149
+ cls._console.print(f"{result_context.signature}:")
150
+ if result_context.exception:
151
+ cls._console.print(result_context.exception)
152
+ else:
153
+ cls._console.print(result_context.result)
150
154
  return
151
155
 
152
156
  if name:
@@ -157,9 +161,10 @@ class ExecutionRegistry:
157
161
  )
158
162
  return
159
163
  title = f"📊 Execution History for '{contexts[0].name}'"
160
- elif index and index >= 0:
164
+ elif index is not None and index >= 0:
161
165
  try:
162
166
  contexts = [cls._store_by_index[index]]
167
+ print(contexts)
163
168
  except KeyError:
164
169
  cls._console.print(
165
170
  f"[{OneColors.DARK_RED}]❌ No execution found for index {index}."
falyx/falyx.py CHANGED
@@ -59,7 +59,7 @@ from falyx.execution_registry import ExecutionRegistry as er
59
59
  from falyx.hook_manager import Hook, HookManager, HookType
60
60
  from falyx.logger import logger
61
61
  from falyx.options_manager import OptionsManager
62
- from falyx.parsers import CommandArgumentParser, FalyxParsers, get_arg_parsers
62
+ from falyx.parser import CommandArgumentParser, FalyxParsers, get_arg_parsers
63
63
  from falyx.protocols import ArgParserProtocol
64
64
  from falyx.retry import RetryPolicy
65
65
  from falyx.signals import BackSignal, CancelSignal, HelpSignal, QuitSignal
@@ -330,7 +330,13 @@ class Falyx:
330
330
  action="store_true",
331
331
  help="Clear the Execution History.",
332
332
  )
333
- parser.add_argument("-r", "--result", type=int, help="Get the result by index")
333
+ parser.add_argument(
334
+ "-r",
335
+ "--result",
336
+ type=int,
337
+ dest="result_index",
338
+ help="Get the result by index",
339
+ )
334
340
  parser.add_argument(
335
341
  "-l", "--last-result", action="store_true", help="Get the last result"
336
342
  )
@@ -796,7 +802,12 @@ class Falyx:
796
802
  def table(self) -> Table:
797
803
  """Creates or returns a custom table to display the menu commands."""
798
804
  if callable(self.custom_table):
799
- return self.custom_table(self)
805
+ custom_table = self.custom_table(self)
806
+ if not isinstance(custom_table, Table):
807
+ raise FalyxError(
808
+ "custom_table must return an instance of rich.table.Table."
809
+ )
810
+ return custom_table
800
811
  elif isinstance(self.custom_table, Table):
801
812
  return self.custom_table
802
813
  else:
@@ -834,21 +845,31 @@ class Falyx:
834
845
 
835
846
  choice = choice.upper()
836
847
  name_map = self._name_map
848
+ run_command = None
837
849
  if name_map.get(choice):
850
+ run_command = name_map[choice]
851
+ else:
852
+ prefix_matches = [
853
+ cmd for key, cmd in name_map.items() if key.startswith(choice)
854
+ ]
855
+ if len(prefix_matches) == 1:
856
+ run_command = prefix_matches[0]
857
+
858
+ if run_command:
838
859
  if not from_validate:
839
- logger.info("Command '%s' selected.", choice)
860
+ logger.info("Command '%s' selected.", run_command.key)
840
861
  if is_preview:
841
- return True, name_map[choice], args, kwargs
862
+ return True, run_command, args, kwargs
842
863
  elif self.mode in {FalyxMode.RUN, FalyxMode.RUN_ALL, FalyxMode.PREVIEW}:
843
- return False, name_map[choice], args, kwargs
864
+ return False, run_command, args, kwargs
844
865
  try:
845
- args, kwargs = await name_map[choice].parse_args(
846
- input_args, from_validate
847
- )
866
+ args, kwargs = await run_command.parse_args(input_args, from_validate)
848
867
  except (CommandArgumentError, Exception) as error:
849
868
  if not from_validate:
850
- name_map[choice].show_help()
851
- self.console.print(f"[{OneColors.DARK_RED}]❌ [{choice}]: {error}")
869
+ run_command.show_help()
870
+ self.console.print(
871
+ f"[{OneColors.DARK_RED}]❌ [{run_command.key}]: {error}"
872
+ )
852
873
  else:
853
874
  raise ValidationError(
854
875
  message=str(error), cursor_position=len(raw_choices)
@@ -856,11 +877,7 @@ class Falyx:
856
877
  return is_preview, None, args, kwargs
857
878
  except HelpSignal:
858
879
  return True, None, args, kwargs
859
- return is_preview, name_map[choice], args, kwargs
860
-
861
- prefix_matches = [cmd for key, cmd in name_map.items() if key.startswith(choice)]
862
- if len(prefix_matches) == 1:
863
- return is_preview, prefix_matches[0], args, kwargs
880
+ return is_preview, run_command, args, kwargs
864
881
 
865
882
  fuzzy_matches = get_close_matches(choice, list(name_map.keys()), n=3, cutoff=0.7)
866
883
  if fuzzy_matches:
@@ -890,12 +907,14 @@ class Falyx:
890
907
  )
891
908
  return is_preview, None, args, kwargs
892
909
 
893
- def _create_context(self, selected_command: Command) -> ExecutionContext:
894
- """Creates a context dictionary for the selected command."""
910
+ def _create_context(
911
+ self, selected_command: Command, args: tuple, kwargs: dict[str, Any]
912
+ ) -> ExecutionContext:
913
+ """Creates an ExecutionContext object for the selected command."""
895
914
  return ExecutionContext(
896
915
  name=selected_command.description,
897
- args=tuple(),
898
- kwargs={},
916
+ args=args,
917
+ kwargs=kwargs,
899
918
  action=selected_command,
900
919
  )
901
920
 
@@ -929,7 +948,7 @@ class Falyx:
929
948
  logger.info("Back selected: exiting %s", self.get_title())
930
949
  return False
931
950
 
932
- context = self._create_context(selected_command)
951
+ context = self._create_context(selected_command, args, kwargs)
933
952
  context.start_timer()
934
953
  try:
935
954
  await self.hooks.trigger(HookType.BEFORE, context)
@@ -974,7 +993,7 @@ class Falyx:
974
993
  selected_command.description,
975
994
  )
976
995
 
977
- context = self._create_context(selected_command)
996
+ context = self._create_context(selected_command, args, kwargs)
978
997
  context.start_timer()
979
998
  try:
980
999
  await self.hooks.trigger(HookType.BEFORE, context)
@@ -12,7 +12,7 @@ from rich.text import Text
12
12
 
13
13
  from falyx.action.base import BaseAction
14
14
  from falyx.exceptions import CommandArgumentError
15
- from falyx.parsers.utils import coerce_value
15
+ from falyx.parser.utils import coerce_value
16
16
  from falyx.signals import HelpSignal
17
17
 
18
18
 
@@ -629,7 +629,10 @@ class CommandArgumentParser:
629
629
  consumed_positional_indicies.add(j)
630
630
 
631
631
  if i < len(args):
632
- raise CommandArgumentError(f"Unexpected positional argument: {args[i:]}")
632
+ plural = "s" if len(args[i:]) > 1 else ""
633
+ raise CommandArgumentError(
634
+ f"Unexpected positional argument{plural}: {', '.join(args[i:])}"
635
+ )
633
636
 
634
637
  return i
635
638
 
@@ -7,7 +7,7 @@ from dateutil import parser as date_parser
7
7
 
8
8
  from falyx.action.base import BaseAction
9
9
  from falyx.logger import logger
10
- from falyx.parsers.signature import infer_args_from_func
10
+ from falyx.parser.signature import infer_args_from_func
11
11
 
12
12
 
13
13
  def coerce_bool(value: str) -> bool:
falyx/selection.py CHANGED
@@ -11,7 +11,7 @@ from rich.table import Table
11
11
 
12
12
  from falyx.themes import OneColors
13
13
  from falyx.utils import CaseInsensitiveDict, chunks
14
- from falyx.validators import int_range_validator, key_validator
14
+ from falyx.validators import MultiIndexValidator, MultiKeyValidator
15
15
 
16
16
 
17
17
  @dataclass
@@ -271,7 +271,11 @@ async def prompt_for_index(
271
271
  prompt_session: PromptSession | None = None,
272
272
  prompt_message: str = "Select an option > ",
273
273
  show_table: bool = True,
274
- ) -> int:
274
+ number_selections: int | str = 1,
275
+ separator: str = ",",
276
+ allow_duplicates: bool = False,
277
+ cancel_key: str = "",
278
+ ) -> int | list[int]:
275
279
  prompt_session = prompt_session or PromptSession()
276
280
  console = console or Console(color_system="truecolor")
277
281
 
@@ -280,10 +284,22 @@ async def prompt_for_index(
280
284
 
281
285
  selection = await prompt_session.prompt_async(
282
286
  message=prompt_message,
283
- validator=int_range_validator(min_index, max_index),
287
+ validator=MultiIndexValidator(
288
+ min_index,
289
+ max_index,
290
+ number_selections,
291
+ separator,
292
+ allow_duplicates,
293
+ cancel_key,
294
+ ),
284
295
  default=default_selection,
285
296
  )
286
- return int(selection)
297
+
298
+ if selection.strip() == cancel_key:
299
+ return int(cancel_key)
300
+ if isinstance(number_selections, int) and number_selections == 1:
301
+ return int(selection.strip())
302
+ return [int(index.strip()) for index in selection.strip().split(separator)]
287
303
 
288
304
 
289
305
  async def prompt_for_selection(
@@ -295,7 +311,11 @@ async def prompt_for_selection(
295
311
  prompt_session: PromptSession | None = None,
296
312
  prompt_message: str = "Select an option > ",
297
313
  show_table: bool = True,
298
- ) -> str:
314
+ number_selections: int | str = 1,
315
+ separator: str = ",",
316
+ allow_duplicates: bool = False,
317
+ cancel_key: str = "",
318
+ ) -> str | list[str]:
299
319
  """Prompt the user to select a key from a set of options. Return the selected key."""
300
320
  prompt_session = prompt_session or PromptSession()
301
321
  console = console or Console(color_system="truecolor")
@@ -305,11 +325,17 @@ async def prompt_for_selection(
305
325
 
306
326
  selected = await prompt_session.prompt_async(
307
327
  message=prompt_message,
308
- validator=key_validator(keys),
328
+ validator=MultiKeyValidator(
329
+ keys, number_selections, separator, allow_duplicates, cancel_key
330
+ ),
309
331
  default=default_selection,
310
332
  )
311
333
 
312
- return selected
334
+ if selected.strip() == cancel_key:
335
+ return cancel_key
336
+ if isinstance(number_selections, int) and number_selections == 1:
337
+ return selected.strip()
338
+ return [key.strip() for key in selected.strip().split(separator)]
313
339
 
314
340
 
315
341
  async def select_value_from_list(
@@ -320,6 +346,10 @@ async def select_value_from_list(
320
346
  prompt_session: PromptSession | None = None,
321
347
  prompt_message: str = "Select an option > ",
322
348
  default_selection: str = "",
349
+ number_selections: int | str = 1,
350
+ separator: str = ",",
351
+ allow_duplicates: bool = False,
352
+ cancel_key: str = "",
323
353
  columns: int = 4,
324
354
  caption: str = "",
325
355
  box_style: box.Box = box.SIMPLE,
@@ -332,7 +362,7 @@ async def select_value_from_list(
332
362
  title_style: str = "",
333
363
  caption_style: str = "",
334
364
  highlight: bool = False,
335
- ):
365
+ ) -> str | list[str]:
336
366
  """Prompt for a selection. Return the selected item."""
337
367
  table = render_selection_indexed_table(
338
368
  title=title,
@@ -360,8 +390,14 @@ async def select_value_from_list(
360
390
  console=console,
361
391
  prompt_session=prompt_session,
362
392
  prompt_message=prompt_message,
393
+ number_selections=number_selections,
394
+ separator=separator,
395
+ allow_duplicates=allow_duplicates,
396
+ cancel_key=cancel_key,
363
397
  )
364
398
 
399
+ if isinstance(selection_index, list):
400
+ return [selections[i] for i in selection_index]
365
401
  return selections[selection_index]
366
402
 
367
403
 
@@ -373,7 +409,11 @@ async def select_key_from_dict(
373
409
  prompt_session: PromptSession | None = None,
374
410
  prompt_message: str = "Select an option > ",
375
411
  default_selection: str = "",
376
- ) -> Any:
412
+ number_selections: int | str = 1,
413
+ separator: str = ",",
414
+ allow_duplicates: bool = False,
415
+ cancel_key: str = "",
416
+ ) -> str | list[str]:
377
417
  """Prompt for a key from a dict, returns the key."""
378
418
  prompt_session = prompt_session or PromptSession()
379
419
  console = console or Console(color_system="truecolor")
@@ -387,6 +427,10 @@ async def select_key_from_dict(
387
427
  console=console,
388
428
  prompt_session=prompt_session,
389
429
  prompt_message=prompt_message,
430
+ number_selections=number_selections,
431
+ separator=separator,
432
+ allow_duplicates=allow_duplicates,
433
+ cancel_key=cancel_key,
390
434
  )
391
435
 
392
436
 
@@ -398,7 +442,11 @@ async def select_value_from_dict(
398
442
  prompt_session: PromptSession | None = None,
399
443
  prompt_message: str = "Select an option > ",
400
444
  default_selection: str = "",
401
- ) -> Any:
445
+ number_selections: int | str = 1,
446
+ separator: str = ",",
447
+ allow_duplicates: bool = False,
448
+ cancel_key: str = "",
449
+ ) -> Any | list[Any]:
402
450
  """Prompt for a key from a dict, but return the value."""
403
451
  prompt_session = prompt_session or PromptSession()
404
452
  console = console or Console(color_system="truecolor")
@@ -412,8 +460,14 @@ async def select_value_from_dict(
412
460
  console=console,
413
461
  prompt_session=prompt_session,
414
462
  prompt_message=prompt_message,
463
+ number_selections=number_selections,
464
+ separator=separator,
465
+ allow_duplicates=allow_duplicates,
466
+ cancel_key=cancel_key,
415
467
  )
416
468
 
469
+ if isinstance(selection_key, list):
470
+ return [selections[key].value for key in selection_key]
417
471
  return selections[selection_key].value
418
472
 
419
473
 
@@ -425,7 +479,11 @@ async def get_selection_from_dict_menu(
425
479
  prompt_session: PromptSession | None = None,
426
480
  prompt_message: str = "Select an option > ",
427
481
  default_selection: str = "",
428
- ):
482
+ number_selections: int | str = 1,
483
+ separator: str = ",",
484
+ allow_duplicates: bool = False,
485
+ cancel_key: str = "",
486
+ ) -> Any | list[Any]:
429
487
  """Prompt for a key from a dict, but return the value."""
430
488
  table = render_selection_dict_table(
431
489
  title,
@@ -439,4 +497,8 @@ async def get_selection_from_dict_menu(
439
497
  prompt_session=prompt_session,
440
498
  prompt_message=prompt_message,
441
499
  default_selection=default_selection,
500
+ number_selections=number_selections,
501
+ separator=separator,
502
+ allow_duplicates=allow_duplicates,
503
+ cancel_key=cancel_key,
442
504
  )
falyx/validators.py CHANGED
@@ -2,7 +2,7 @@
2
2
  """validators.py"""
3
3
  from typing import KeysView, Sequence
4
4
 
5
- from prompt_toolkit.validation import Validator
5
+ from prompt_toolkit.validation import ValidationError, Validator
6
6
 
7
7
 
8
8
  def int_range_validator(minimum: int, maximum: int) -> Validator:
@@ -45,3 +45,91 @@ def yes_no_validator() -> Validator:
45
45
  return True
46
46
 
47
47
  return Validator.from_callable(validate, error_message="Enter 'Y' or 'n'.")
48
+
49
+
50
+ class MultiIndexValidator(Validator):
51
+ def __init__(
52
+ self,
53
+ minimum: int,
54
+ maximum: int,
55
+ number_selections: int | str,
56
+ separator: str,
57
+ allow_duplicates: bool,
58
+ cancel_key: str,
59
+ ) -> None:
60
+ self.minimum = minimum
61
+ self.maximum = maximum
62
+ self.number_selections = number_selections
63
+ self.separator = separator
64
+ self.allow_duplicates = allow_duplicates
65
+ self.cancel_key = cancel_key
66
+ super().__init__()
67
+
68
+ def validate(self, document):
69
+ selections = [
70
+ index.strip() for index in document.text.strip().split(self.separator)
71
+ ]
72
+ if not selections or selections == [""]:
73
+ raise ValidationError(message="Select at least 1 item.")
74
+ if self.cancel_key in selections and len(selections) == 1:
75
+ return
76
+ elif self.cancel_key in selections:
77
+ raise ValidationError(message="Cancel key must be selected alone.")
78
+ for selection in selections:
79
+ try:
80
+ index = int(selection)
81
+ if not self.minimum <= index <= self.maximum:
82
+ raise ValidationError(
83
+ message=f"Invalid selection: {selection}. Select a number between {self.minimum} and {self.maximum}."
84
+ )
85
+ except ValueError:
86
+ raise ValidationError(
87
+ message=f"Invalid selection: {selection}. Select a number between {self.minimum} and {self.maximum}."
88
+ )
89
+ if not self.allow_duplicates and selections.count(selection) > 1:
90
+ raise ValidationError(message=f"Duplicate selection: {selection}")
91
+ if isinstance(self.number_selections, int):
92
+ if self.number_selections == 1 and len(selections) > 1:
93
+ raise ValidationError(message="Invalid selection. Select only 1 item.")
94
+ if len(selections) != self.number_selections:
95
+ raise ValidationError(
96
+ message=f"Select exactly {self.number_selections} items separated by '{self.separator}'"
97
+ )
98
+
99
+
100
+ class MultiKeyValidator(Validator):
101
+ def __init__(
102
+ self,
103
+ keys: Sequence[str] | KeysView[str],
104
+ number_selections: int | str,
105
+ separator: str,
106
+ allow_duplicates: bool,
107
+ cancel_key: str,
108
+ ) -> None:
109
+ self.keys = keys
110
+ self.separator = separator
111
+ self.number_selections = number_selections
112
+ self.allow_duplicates = allow_duplicates
113
+ self.cancel_key = cancel_key
114
+ super().__init__()
115
+
116
+ def validate(self, document):
117
+ selections = [key.strip() for key in document.text.strip().split(self.separator)]
118
+ if not selections or selections == [""]:
119
+ raise ValidationError(message="Select at least 1 item.")
120
+ if self.cancel_key in selections and len(selections) == 1:
121
+ return
122
+ elif self.cancel_key in selections:
123
+ raise ValidationError(message="Cancel key must be selected alone.")
124
+ for selection in selections:
125
+ if selection.upper() not in [key.upper() for key in self.keys]:
126
+ raise ValidationError(message=f"Invalid selection: {selection}")
127
+ if not self.allow_duplicates and selections.count(selection) > 1:
128
+ raise ValidationError(message=f"Duplicate selection: {selection}")
129
+ if isinstance(self.number_selections, int):
130
+ if self.number_selections == 1 and len(selections) > 1:
131
+ raise ValidationError(message="Invalid selection. Select only 1 item.")
132
+ if len(selections) != self.number_selections:
133
+ raise ValidationError(
134
+ message=f"Select exactly {self.number_selections} items separated by '{self.separator}'"
135
+ )
falyx/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.1.49"
1
+ __version__ = "0.1.51"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: falyx
3
- Version: 0.1.49
3
+ Version: 0.1.51
4
4
  Summary: Reliable and introspectable async CLI action framework.
5
5
  License: MIT
6
6
  Author: Roland Thomas Jr
@@ -1,61 +1,61 @@
1
1
  falyx/.pytyped,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  falyx/__init__.py,sha256=Gh88lQ5pbD7xbGWrBgslE2kSTZKY9TkvKSa53rZ3l8U,305
3
- falyx/__main__.py,sha256=186MGZUMtx-hCH7QsBqcN-68hYg747LOSk3rlc_23_c,3421
3
+ falyx/__main__.py,sha256=xHO4pB45rccixo-ougF84QJeB36ef8mEZXWVK_CJL9M,3420
4
4
  falyx/action/.pytyped,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  falyx/action/__init__.py,sha256=4E3Rb0GgGcmggrPJh0YFiwbVgN_PQjIzL06-Z3qMReo,1247
6
- falyx/action/action.py,sha256=w6xDbsB1SlMPSvpo2Dh0e11lRGP6a4E3K6AdfjlEqGY,5759
6
+ falyx/action/action.py,sha256=j9ANO0xRfWoiNK6z-46T04EKAJ7GhjVrzb8_U8afEAA,5753
7
7
  falyx/action/action_factory.py,sha256=br-P7Oip-4tZkO8qVT_ECwLe6idYjJa_GuBi5QR7vS4,4832
8
- falyx/action/action_group.py,sha256=dfCEJM0RfdopuLFtfaxpvNzbZT6hVMrUuRBAFz--Uss,6835
8
+ falyx/action/action_group.py,sha256=_u64s81kgLAQFlTic3OJJoa8DwYJ2kqBh2U-_mpuPhs,6834
9
9
  falyx/action/base.py,sha256=B7mt66oznmhv2qpSOwOuScgMckVXrxjRMU2buzZkRD8,5866
10
10
  falyx/action/chained_action.py,sha256=aV_plUdDVdc1o-oU57anbWkw33jgRIh4W29QwEA_1Mw,8501
11
11
  falyx/action/fallback_action.py,sha256=0z5l0s_LKnhIwgMdykm8lJqs246DKSpyYs-p7PnsKok,1619
12
12
  falyx/action/http_action.py,sha256=DNeSBWh58UTFGlfFyTk2GnhS54hpLAJLC0QNbq2cYic,5799
13
- falyx/action/io_action.py,sha256=8x9HpvLhqJF7lI8PUo7Hs9F2NdJ1WfGs_wP5Myyoor8,10059
13
+ falyx/action/io_action.py,sha256=9vx3mX9lBUBmwl1Xtzr5-idEwlMKlK1ZQuZkzTmORJc,9881
14
14
  falyx/action/literal_input_action.py,sha256=7H2VX_L5VaytVdV2uis-VTGi782kQtwKTB8T04c7J1k,1293
15
- falyx/action/menu_action.py,sha256=Nvvz7XsVnKgx6flW1ARaQOmSIZZtYmWTTFl2XNyUp7k,5882
15
+ falyx/action/menu_action.py,sha256=SLqwmQ1TOt8kl_cgIWogBYfx8lYPLZa4E-Yy6M2cX_w,6069
16
16
  falyx/action/mixins.py,sha256=eni8_PwzMnuwh0ZqOdzCdAyWlOphoiqL7z27xnFsg5s,1117
17
17
  falyx/action/process_action.py,sha256=HsDqlKy1PkG3HHC6mHa4O6ayY_oKVY2qj5nDRJuSn24,4571
18
- falyx/action/process_pool_action.py,sha256=1fFVEKpe-_XiMJxo4xF-j749Yd1noXjf76EOCPeX9xA,5940
18
+ falyx/action/process_pool_action.py,sha256=8RQSjJaU0nGY_ObpcO31uI-HfNY7krqMN2wSzTnJ8jw,5939
19
19
  falyx/action/prompt_menu_action.py,sha256=corzjpPNVMYKncfueeRUWwklnlZHN-Fc61psOzbZELg,5286
20
- falyx/action/select_file_action.py,sha256=zhkBIJw6OZ-AYShAzcmAkbZPPF3pXGmLUDZRx4N0jzE,8726
21
- falyx/action/selection_action.py,sha256=UxAFoKqkeEA84V_JqsuTlzlfCZdQHDspSXig71iM_J0,13017
20
+ falyx/action/select_file_action.py,sha256=2T4I1CLvHLAAqNUD2rFBIpdi74BP5amU4yTHUOGnd64,9911
21
+ falyx/action/selection_action.py,sha256=Mav39iTkVIJPDvmDek8R2bSF18f-mII56l5sSzZSPII,15735
22
22
  falyx/action/signal_action.py,sha256=5UMqvzy7fBnLANGwYUWoe1VRhrr7e-yOVeLdOnCBiJo,1350
23
23
  falyx/action/types.py,sha256=NfZz1ufZuvCgp-he2JIItbnjX7LjOUadjtKbjpRlSIY,1399
24
- falyx/action/user_input_action.py,sha256=w9QTjKbdPmhXleX_XxUKS9VvNyKpwTtcuXBX7067seA,3606
24
+ falyx/action/user_input_action.py,sha256=EnwSk-ZW0bqSlEnWpUE9_0jmoFCoTGMQc5PqP49cSyg,3750
25
25
  falyx/bottom_bar.py,sha256=KPACb9VC0I3dv_pYZLqy7e4uA_KT5dSfwnvuknyV0FI,7388
26
- falyx/command.py,sha256=LmtoI4NbW_zEy_f1AlZ-obl83K5l07Sac0zXJt6xQYc,16419
26
+ falyx/command.py,sha256=7BM4JPK36dsWm2JPmRwarLJiDRk1GgfX5HESbeysuMY,16417
27
27
  falyx/config.py,sha256=Cm1F9SfNSbugPALxaEz7NRqp1wrk-g2jYq35bQzN2uE,9658
28
- falyx/context.py,sha256=EJIWQxU3SeAcufq6_WrAVIAs6abpY4-ttcemH3RVFnQ,10365
29
- falyx/debug.py,sha256=IRpYtdH8yeXJEsfP5rASALmBQb2U_EwrTudF2GIDdZY,1545
28
+ falyx/context.py,sha256=b9PGkIfhc1BbFUmaqmr4AojzONfKG1c9WP2uixzCJGQ,10806
29
+ falyx/debug.py,sha256=pguI0XQcZ-7jte5YUPexAufa1oxxalYO1JgmO6GU3rI,1557
30
30
  falyx/exceptions.py,sha256=kK9k1v7LVNjJSwYztRa9Krhr3ZOI-6Htq2ZjlYICPKg,922
31
- falyx/execution_registry.py,sha256=Ke3DlvG1pYvjwEG7fgb4KPNqCTj1MyYC9m9yd1oxmWk,7341
32
- falyx/falyx.py,sha256=LL7KzQW1O9eFtU_SGVvG-3RHlMErVsQ3l4V7H678Z7s,49272
31
+ falyx/execution_registry.py,sha256=7t_96-Q7R7MAJBvWwAt5IAERp0TjbGZPGeeJ1s24ey8,7628
32
+ falyx/falyx.py,sha256=zOAah7OYHZgMRI60zwXjNgfeEHs8y87SDs3Meslood0,49805
33
33
  falyx/hook_manager.py,sha256=TFuHQnAncS_rk6vuw-VSx8bnAppLuHfrZCrzLwqcO9o,2979
34
34
  falyx/hooks.py,sha256=xMfQROib0BNsaQF4AXJpmCiGePoE1f1xpcdibgnVZWM,2913
35
35
  falyx/init.py,sha256=F9jg7mLPoBWXdJnc_fyWG7zVQSnrAO8ueDiP8AJxDWE,3331
36
36
  falyx/logger.py,sha256=1Mfb_vJFJ1tQwziuyU2p-cSMi2Js8N2byniFEnI6vOQ,132
37
37
  falyx/menu.py,sha256=E580qZsx08bnWcqRVjJuD2Fy8Zh_1zIexp5f0lC7L2c,3745
38
38
  falyx/options_manager.py,sha256=dFAnQw543tQ6Xupvh1PwBrhiSWlSACHw8K-sHP_lUh4,2842
39
- falyx/parsers/.pytyped,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
- falyx/parsers/__init__.py,sha256=ZfPmbtEUechDvgl99-lWhTXmFnXS_FMXJ_xb8KGEJLo,448
41
- falyx/parsers/argparse.py,sha256=P_MS5YjtHJjEgAR8pm41SLndZhvCOCYfygv6svkMFu8,37401
42
- falyx/parsers/parsers.py,sha256=MXWC8OQ3apDaeKfY0O4J8NnkxofWVOCRnKatC00lGm0,8796
43
- falyx/parsers/signature.py,sha256=cCa-yKUcbbET0Ho45oFZWWHFGCX5a_LaAOWRP7b87po,2465
44
- falyx/parsers/utils.py,sha256=w_UzvvP62EDKXWSf3jslEsJfd45usGyFqXKNziQhLRI,2893
39
+ falyx/parser/.pytyped,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
+ falyx/parser/__init__.py,sha256=ZfPmbtEUechDvgl99-lWhTXmFnXS_FMXJ_xb8KGEJLo,448
41
+ falyx/parser/argparse.py,sha256=izJDlhHxyP1y0-NjiuLcNpHYMIO6zS4nY_-v-Xg-O94,37503
42
+ falyx/parser/parsers.py,sha256=MXWC8OQ3apDaeKfY0O4J8NnkxofWVOCRnKatC00lGm0,8796
43
+ falyx/parser/signature.py,sha256=cCa-yKUcbbET0Ho45oFZWWHFGCX5a_LaAOWRP7b87po,2465
44
+ falyx/parser/utils.py,sha256=VX4C58pJdHQihkaLIrYmcwqHJrFjbNjb5blEU3IqSAE,2892
45
45
  falyx/prompt_utils.py,sha256=qgk0bXs7mwzflqzWyFhEOTpKQ_ZtMIqGhKeg-ocwNnE,1542
46
46
  falyx/protocols.py,sha256=-9GbCBUzzsEgw2_KOCYqxxzWJuez0eHmwnZp_ShY0jc,493
47
47
  falyx/retry.py,sha256=sGRE9QhdZK98M99G8F15WUsJ_fYLNyLlCgu3UANaSQs,3744
48
48
  falyx/retry_utils.py,sha256=vwoZmFVCGVqZ13BX_xi3qZZVsmSxkp-jfaf6kJtBV9c,723
49
- falyx/selection.py,sha256=PxjsO2qqN-_7algR-rUnkjC-WAPTkCY8khMVr-BswrU,12919
49
+ falyx/selection.py,sha256=TPSM_KKGHedJblWI0AzxTZR2haZjRF3k-gQoQeR3L28,15239
50
50
  falyx/signals.py,sha256=Y_neFXpfHs7qY0syw9XcfR9WeAGRcRw1nG_2L1JJqKE,1083
51
51
  falyx/tagged_table.py,sha256=4SV-SdXFrAhy1JNToeBCvyxT-iWVf6cWY7XETTys4n8,1067
52
52
  falyx/themes/__init__.py,sha256=1CZhEUCin9cUk8IGYBUFkVvdHRNNJBEFXccHwpUKZCA,284
53
53
  falyx/themes/colors.py,sha256=4aaeAHJetmeNInI0Zytg4E3YqKfPFelpf04vtjSvsS8,19776
54
54
  falyx/utils.py,sha256=U45xnZFUdoFC4xiji_9S1jHS5V7MvxSDtufP8EgB0SM,6732
55
- falyx/validators.py,sha256=t5iyzVpY8tdC4rfhr4isEfWpD5gNTzjeX_Hbi_Uq6sA,1328
56
- falyx/version.py,sha256=6ak6J_3GLVYH79iSCY8wWsCV4clQHUuDdcBlOnUh9K8,23
57
- falyx-0.1.49.dist-info/LICENSE,sha256=B0yqgaHuSdhN7T3OBmgQSiDTy8HqT5Oe_dLypRe4Ra4,1073
58
- falyx-0.1.49.dist-info/METADATA,sha256=uIl8tUOOoQzWV810xzwogyxWfNCRYYTZR684_8SXFcw,5561
59
- falyx-0.1.49.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
60
- falyx-0.1.49.dist-info/entry_points.txt,sha256=j8owOSl2j1Ss8DtGMnKfgehKaolqnIPhVFHaUBLUnMs,45
61
- falyx-0.1.49.dist-info/RECORD,,
55
+ falyx/validators.py,sha256=Pbdxh5777Y03HxyArAh2ApeVSx23in4w4K38G43Vt98,5197
56
+ falyx/version.py,sha256=J5DqQAfJrmJsVwP_F1QEYvB7YXVaXR6y-BTblSWQs-k,23
57
+ falyx-0.1.51.dist-info/LICENSE,sha256=B0yqgaHuSdhN7T3OBmgQSiDTy8HqT5Oe_dLypRe4Ra4,1073
58
+ falyx-0.1.51.dist-info/METADATA,sha256=1pOPjynlq0g3vfgJaV2VqedsHH4O-3Egjo1WEengCns,5561
59
+ falyx-0.1.51.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
60
+ falyx-0.1.51.dist-info/entry_points.txt,sha256=j8owOSl2j1Ss8DtGMnKfgehKaolqnIPhVFHaUBLUnMs,45
61
+ falyx-0.1.51.dist-info/RECORD,,
File without changes
File without changes
File without changes
File without changes
File without changes