falyx 0.1.63__tar.gz → 0.1.64__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 (70) hide show
  1. {falyx-0.1.63 → falyx-0.1.64}/PKG-INFO +1 -1
  2. {falyx-0.1.63 → falyx-0.1.64}/falyx/action/base_action.py +5 -3
  3. {falyx-0.1.63 → falyx-0.1.64}/falyx/action/select_file_action.py +6 -3
  4. {falyx-0.1.63 → falyx-0.1.64}/falyx/action/selection_action.py +187 -33
  5. {falyx-0.1.63 → falyx-0.1.64}/falyx/execution_registry.py +3 -3
  6. falyx-0.1.64/falyx/version.py +1 -0
  7. {falyx-0.1.63 → falyx-0.1.64}/pyproject.toml +1 -1
  8. falyx-0.1.63/falyx/version.py +0 -1
  9. {falyx-0.1.63 → falyx-0.1.64}/LICENSE +0 -0
  10. {falyx-0.1.63 → falyx-0.1.64}/README.md +0 -0
  11. {falyx-0.1.63 → falyx-0.1.64}/falyx/.pytyped +0 -0
  12. {falyx-0.1.63 → falyx-0.1.64}/falyx/__init__.py +0 -0
  13. {falyx-0.1.63 → falyx-0.1.64}/falyx/__main__.py +0 -0
  14. {falyx-0.1.63 → falyx-0.1.64}/falyx/action/.pytyped +0 -0
  15. {falyx-0.1.63 → falyx-0.1.64}/falyx/action/__init__.py +0 -0
  16. {falyx-0.1.63 → falyx-0.1.64}/falyx/action/action.py +0 -0
  17. {falyx-0.1.63 → falyx-0.1.64}/falyx/action/action_factory.py +0 -0
  18. {falyx-0.1.63 → falyx-0.1.64}/falyx/action/action_group.py +0 -0
  19. {falyx-0.1.63 → falyx-0.1.64}/falyx/action/action_mixins.py +0 -0
  20. {falyx-0.1.63 → falyx-0.1.64}/falyx/action/action_types.py +0 -0
  21. {falyx-0.1.63 → falyx-0.1.64}/falyx/action/chained_action.py +0 -0
  22. {falyx-0.1.63 → falyx-0.1.64}/falyx/action/confirm_action.py +0 -0
  23. {falyx-0.1.63 → falyx-0.1.64}/falyx/action/fallback_action.py +0 -0
  24. {falyx-0.1.63 → falyx-0.1.64}/falyx/action/http_action.py +0 -0
  25. {falyx-0.1.63 → falyx-0.1.64}/falyx/action/io_action.py +0 -0
  26. {falyx-0.1.63 → falyx-0.1.64}/falyx/action/literal_input_action.py +0 -0
  27. {falyx-0.1.63 → falyx-0.1.64}/falyx/action/load_file_action.py +0 -0
  28. {falyx-0.1.63 → falyx-0.1.64}/falyx/action/menu_action.py +0 -0
  29. {falyx-0.1.63 → falyx-0.1.64}/falyx/action/process_action.py +0 -0
  30. {falyx-0.1.63 → falyx-0.1.64}/falyx/action/process_pool_action.py +0 -0
  31. {falyx-0.1.63 → falyx-0.1.64}/falyx/action/prompt_menu_action.py +0 -0
  32. {falyx-0.1.63 → falyx-0.1.64}/falyx/action/save_file_action.py +0 -0
  33. {falyx-0.1.63 → falyx-0.1.64}/falyx/action/shell_action.py +0 -0
  34. {falyx-0.1.63 → falyx-0.1.64}/falyx/action/signal_action.py +0 -0
  35. {falyx-0.1.63 → falyx-0.1.64}/falyx/action/user_input_action.py +0 -0
  36. {falyx-0.1.63 → falyx-0.1.64}/falyx/bottom_bar.py +0 -0
  37. {falyx-0.1.63 → falyx-0.1.64}/falyx/command.py +0 -0
  38. {falyx-0.1.63 → falyx-0.1.64}/falyx/completer.py +0 -0
  39. {falyx-0.1.63 → falyx-0.1.64}/falyx/config.py +0 -0
  40. {falyx-0.1.63 → falyx-0.1.64}/falyx/console.py +0 -0
  41. {falyx-0.1.63 → falyx-0.1.64}/falyx/context.py +0 -0
  42. {falyx-0.1.63 → falyx-0.1.64}/falyx/debug.py +0 -0
  43. {falyx-0.1.63 → falyx-0.1.64}/falyx/exceptions.py +0 -0
  44. {falyx-0.1.63 → falyx-0.1.64}/falyx/falyx.py +0 -0
  45. {falyx-0.1.63 → falyx-0.1.64}/falyx/hook_manager.py +0 -0
  46. {falyx-0.1.63 → falyx-0.1.64}/falyx/hooks.py +0 -0
  47. {falyx-0.1.63 → falyx-0.1.64}/falyx/init.py +0 -0
  48. {falyx-0.1.63 → falyx-0.1.64}/falyx/logger.py +0 -0
  49. {falyx-0.1.63 → falyx-0.1.64}/falyx/menu.py +0 -0
  50. {falyx-0.1.63 → falyx-0.1.64}/falyx/options_manager.py +0 -0
  51. {falyx-0.1.63 → falyx-0.1.64}/falyx/parser/.pytyped +0 -0
  52. {falyx-0.1.63 → falyx-0.1.64}/falyx/parser/__init__.py +0 -0
  53. {falyx-0.1.63 → falyx-0.1.64}/falyx/parser/argument.py +0 -0
  54. {falyx-0.1.63 → falyx-0.1.64}/falyx/parser/argument_action.py +0 -0
  55. {falyx-0.1.63 → falyx-0.1.64}/falyx/parser/command_argument_parser.py +0 -0
  56. {falyx-0.1.63 → falyx-0.1.64}/falyx/parser/parser_types.py +0 -0
  57. {falyx-0.1.63 → falyx-0.1.64}/falyx/parser/parsers.py +0 -0
  58. {falyx-0.1.63 → falyx-0.1.64}/falyx/parser/signature.py +0 -0
  59. {falyx-0.1.63 → falyx-0.1.64}/falyx/parser/utils.py +0 -0
  60. {falyx-0.1.63 → falyx-0.1.64}/falyx/prompt_utils.py +0 -0
  61. {falyx-0.1.63 → falyx-0.1.64}/falyx/protocols.py +0 -0
  62. {falyx-0.1.63 → falyx-0.1.64}/falyx/retry.py +0 -0
  63. {falyx-0.1.63 → falyx-0.1.64}/falyx/retry_utils.py +0 -0
  64. {falyx-0.1.63 → falyx-0.1.64}/falyx/selection.py +0 -0
  65. {falyx-0.1.63 → falyx-0.1.64}/falyx/signals.py +0 -0
  66. {falyx-0.1.63 → falyx-0.1.64}/falyx/tagged_table.py +0 -0
  67. {falyx-0.1.63 → falyx-0.1.64}/falyx/themes/__init__.py +0 -0
  68. {falyx-0.1.63 → falyx-0.1.64}/falyx/themes/colors.py +0 -0
  69. {falyx-0.1.63 → falyx-0.1.64}/falyx/utils.py +0 -0
  70. {falyx-0.1.63 → falyx-0.1.64}/falyx/validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: falyx
3
- Version: 0.1.63
3
+ Version: 0.1.64
4
4
  Summary: Reliable and introspectable async CLI action framework.
5
5
  License: MIT
6
6
  Author: Roland Thomas Jr
@@ -63,7 +63,7 @@ class BaseAction(ABC):
63
63
  hooks: HookManager | None = None,
64
64
  inject_last_result: bool = False,
65
65
  inject_into: str = "last_result",
66
- never_prompt: bool = False,
66
+ never_prompt: bool | None = None,
67
67
  logging_hooks: bool = False,
68
68
  ) -> None:
69
69
  self.name = name
@@ -72,7 +72,7 @@ class BaseAction(ABC):
72
72
  self.shared_context: SharedContext | None = None
73
73
  self.inject_last_result: bool = inject_last_result
74
74
  self.inject_into: str = inject_into
75
- self._never_prompt: bool = never_prompt
75
+ self._never_prompt: bool | None = never_prompt
76
76
  self._skip_in_chain: bool = False
77
77
  self.console: Console = console
78
78
  self.options_manager: OptionsManager | None = None
@@ -122,7 +122,9 @@ class BaseAction(ABC):
122
122
 
123
123
  @property
124
124
  def never_prompt(self) -> bool:
125
- return self.get_option("never_prompt", self._never_prompt)
125
+ if self._never_prompt is not None:
126
+ return self._never_prompt
127
+ return self.get_option("never_prompt", False)
126
128
 
127
129
  def prepare(
128
130
  self, shared_context: SharedContext, options_manager: OptionsManager | None = None
@@ -30,7 +30,7 @@ from falyx.themes import OneColors
30
30
 
31
31
  class SelectFileAction(BaseAction):
32
32
  """
33
- SelectFileAction allows users to select a file from a directory and return:
33
+ SelectFileAction allows users to select a file(s) from a directory and return:
34
34
  - file content (as text, JSON, CSV, etc.)
35
35
  - or the file path itself.
36
36
 
@@ -50,6 +50,9 @@ class SelectFileAction(BaseAction):
50
50
  style (str): Style for the selection options.
51
51
  suffix_filter (str | None): Restrict to certain file types.
52
52
  return_type (FileType): What to return (path, content, parsed).
53
+ number_selections (int | str): How many files to select (1, 2, '*').
54
+ separator (str): Separator for multiple selections.
55
+ allow_duplicates (bool): Allow selecting the same file multiple times.
53
56
  prompt_session (PromptSession | None): Prompt session for user input.
54
57
  """
55
58
 
@@ -217,7 +220,7 @@ class SelectFileAction(BaseAction):
217
220
  er.record(context)
218
221
 
219
222
  async def preview(self, parent: Tree | None = None):
220
- label = f"[{OneColors.GREEN}]📁 SelectFilesAction[/] '{self.name}'"
223
+ label = f"[{OneColors.GREEN}]📁 SelectFileAction[/] '{self.name}'"
221
224
  tree = parent.add(label) if parent else Tree(label)
222
225
 
223
226
  tree.add(f"[dim]Directory:[/] {str(self.directory)}")
@@ -243,6 +246,6 @@ class SelectFileAction(BaseAction):
243
246
 
244
247
  def __str__(self) -> str:
245
248
  return (
246
- f"SelectFilesAction(name={self.name!r}, dir={str(self.directory)!r}, "
249
+ f"SelectFileAction(name={self.name!r}, dir={str(self.directory)!r}, "
247
250
  f"suffix_filter={self.suffix_filter!r}, return_type={self.return_type})"
248
251
  )
@@ -25,11 +25,60 @@ from falyx.themes import OneColors
25
25
 
26
26
  class SelectionAction(BaseAction):
27
27
  """
28
- A selection action that prompts the user to select an option from a list or
29
- dictionary. The selected option is then returned as the result of the action.
28
+ A Falyx Action for interactively or programmatically selecting one or more items
29
+ from a list or dictionary of options.
30
30
 
31
- If return_key is True, the key of the selected option is returned instead of
32
- the value.
31
+ `SelectionAction` supports both `list[str]` and `dict[str, SelectionOption]`
32
+ inputs. It renders a prompt (unless `never_prompt=True`), validates user input
33
+ or injected defaults, and returns a structured result based on the specified
34
+ `return_type`.
35
+
36
+ It is commonly used for item pickers, confirmation flows, dynamic parameterization,
37
+ or guided workflows in interactive or headless CLI pipelines.
38
+
39
+ Features:
40
+ - Supports single or multiple selections (`number_selections`)
41
+ - Dictionary mode allows rich metadata (description, value, style)
42
+ - Flexible return values: key(s), value(s), item(s), description(s), or mappings
43
+ - Fully hookable lifecycle (`before`, `on_success`, `on_error`, `after`, `on_teardown`)
44
+ - Default selection logic supports previous results (`last_result`)
45
+ - Can run in headless mode using `never_prompt` and fallback defaults
46
+
47
+ Args:
48
+ name (str): Action name for tracking and logging.
49
+ selections (list[str] | dict[str, SelectionOption] | dict[str, Any]):
50
+ The available choices. If a plain dict is passed, values are converted
51
+ into `SelectionOption` instances.
52
+ title (str): Title shown in the selection UI (default: "Select an option").
53
+ columns (int): Number of columns in the selection table.
54
+ prompt_message (str): Input prompt for the user (default: "Select > ").
55
+ default_selection (str | list[str]): Key(s) or index(es) used as fallback selection.
56
+ number_selections (int | str): Max number of choices allowed (or "*" for unlimited).
57
+ separator (str): Character used to separate multi-selections (default: ",").
58
+ allow_duplicates (bool): Whether duplicate selections are allowed.
59
+ inject_last_result (bool): If True, attempts to inject the last result as default.
60
+ inject_into (str): The keyword name for injected value (default: "last_result").
61
+ return_type (SelectionReturnType | str): The type of result to return.
62
+ prompt_session (PromptSession | None): Reused or customized prompt_toolkit session.
63
+ never_prompt (bool): If True, skips prompting and uses default_selection or last_result.
64
+ show_table (bool): Whether to render the selection table before prompting.
65
+
66
+ Returns:
67
+ Any: The selected result(s), shaped according to `return_type`.
68
+
69
+ Raises:
70
+ CancelSignal: If the user chooses the cancel option.
71
+ ValueError: If configuration is invalid or no selection can be resolved.
72
+ TypeError: If `selections` is not a supported type.
73
+
74
+ Example:
75
+ SelectionAction(
76
+ name="PickEnv",
77
+ selections={"dev": "Development", "prod": "Production"},
78
+ return_type="key",
79
+ )
80
+
81
+ This Action supports use in both interactive menus and chained, non-interactive CLI flows.
33
82
  """
34
83
 
35
84
  def __init__(
@@ -46,7 +95,7 @@ class SelectionAction(BaseAction):
46
95
  title: str = "Select an option",
47
96
  columns: int = 5,
48
97
  prompt_message: str = "Select > ",
49
- default_selection: str = "",
98
+ default_selection: str | list[str] = "",
50
99
  number_selections: int | str = 1,
51
100
  separator: str = ",",
52
101
  allow_duplicates: bool = False,
@@ -202,37 +251,105 @@ class SelectionAction(BaseAction):
202
251
  raise ValueError(f"Unsupported return type: {self.return_type}")
203
252
  return result
204
253
 
205
- async def _run(self, *args, **kwargs) -> Any:
206
- kwargs = self._maybe_inject_last_result(kwargs)
207
- context = ExecutionContext(
208
- name=self.name,
209
- args=args,
210
- kwargs=kwargs,
211
- action=self,
212
- )
213
-
214
- effective_default = str(self.default_selection)
215
- maybe_result = str(self.last_result)
216
- if isinstance(self.selections, dict):
217
- if maybe_result in self.selections:
218
- effective_default = maybe_result
219
- elif self.inject_last_result:
254
+ async def _resolve_effective_default(self) -> str:
255
+ effective_default: str | list[str] = self.default_selection
256
+ maybe_result = self.last_result
257
+ if self.number_selections == 1:
258
+ if isinstance(effective_default, list):
259
+ effective_default = effective_default[0] if effective_default else ""
260
+ elif isinstance(maybe_result, list):
261
+ maybe_result = maybe_result[0] if maybe_result else ""
262
+ default = await self._resolve_single_default(maybe_result)
263
+ if not default:
264
+ default = await self._resolve_single_default(effective_default)
265
+ if not default and self.inject_last_result:
220
266
  logger.warning(
221
267
  "[%s] Injected last result '%s' not found in selections",
222
268
  self.name,
223
269
  maybe_result,
224
270
  )
271
+ return default
272
+
273
+ if maybe_result and isinstance(maybe_result, list):
274
+ maybe_result = [
275
+ await self._resolve_single_default(item) for item in maybe_result
276
+ ]
277
+ if (
278
+ maybe_result
279
+ and self.number_selections != "*"
280
+ and len(maybe_result) != self.number_selections
281
+ ):
282
+ raise ValueError(
283
+ f"[{self.name}] 'number_selections' is {self.number_selections}, "
284
+ f"but last_result has a different length: {len(maybe_result)}."
285
+ )
286
+ return self.separator.join(maybe_result)
287
+ elif effective_default and isinstance(effective_default, list):
288
+ effective_default = [
289
+ await self._resolve_single_default(item) for item in effective_default
290
+ ]
291
+ if (
292
+ effective_default
293
+ and self.number_selections != "*"
294
+ and len(effective_default) != self.number_selections
295
+ ):
296
+ raise ValueError(
297
+ f"[{self.name}] 'number_selections' is {self.number_selections}, "
298
+ f"but default_selection has a different length: {len(effective_default)}."
299
+ )
300
+ return self.separator.join(effective_default)
301
+ if self.inject_last_result:
302
+ logger.warning(
303
+ "[%s] Injected last result '%s' not found in selections",
304
+ self.name,
305
+ maybe_result,
306
+ )
307
+ return ""
308
+
309
+ async def _resolve_single_default(self, maybe_result: str) -> str:
310
+ effective_default = ""
311
+ if isinstance(self.selections, dict):
312
+ if str(maybe_result) in self.selections:
313
+ effective_default = str(maybe_result)
314
+ elif maybe_result in (
315
+ selection.value for selection in self.selections.values()
316
+ ):
317
+ selection = [
318
+ key
319
+ for key, sel in self.selections.items()
320
+ if sel.value == maybe_result
321
+ ]
322
+ if selection:
323
+ effective_default = selection[0]
324
+ elif maybe_result in (
325
+ selection.description for selection in self.selections.values()
326
+ ):
327
+ selection = [
328
+ key
329
+ for key, sel in self.selections.items()
330
+ if sel.description == maybe_result
331
+ ]
332
+ if selection:
333
+ effective_default = selection[0]
225
334
  elif isinstance(self.selections, list):
226
- if maybe_result.isdigit() and int(maybe_result) in range(
335
+ if str(maybe_result).isdigit() and int(maybe_result) in range(
227
336
  len(self.selections)
228
337
  ):
229
338
  effective_default = maybe_result
230
- elif self.inject_last_result:
231
- logger.warning(
232
- "[%s] Injected last result '%s' not found in selections",
233
- self.name,
234
- maybe_result,
235
- )
339
+ elif maybe_result in self.selections:
340
+ effective_default = str(self.selections.index(maybe_result))
341
+ return effective_default
342
+
343
+ async def _run(self, *args, **kwargs) -> Any:
344
+ kwargs = self._maybe_inject_last_result(kwargs)
345
+ context = ExecutionContext(
346
+ name=self.name,
347
+ args=args,
348
+ kwargs=kwargs,
349
+ action=self,
350
+ )
351
+
352
+ effective_default = await self._resolve_effective_default()
236
353
 
237
354
  if self.never_prompt and not effective_default:
238
355
  raise ValueError(
@@ -251,6 +368,9 @@ class SelectionAction(BaseAction):
251
368
  columns=self.columns,
252
369
  formatter=self.cancel_formatter,
253
370
  )
371
+ if effective_default is None or isinstance(effective_default, int):
372
+ effective_default = ""
373
+
254
374
  if not self.never_prompt:
255
375
  indices: int | list[int] = await prompt_for_index(
256
376
  len(self.selections),
@@ -265,8 +385,13 @@ class SelectionAction(BaseAction):
265
385
  cancel_key=self.cancel_key,
266
386
  )
267
387
  else:
268
- if effective_default:
388
+ if effective_default and self.number_selections == 1:
269
389
  indices = int(effective_default)
390
+ elif effective_default:
391
+ indices = [
392
+ int(index)
393
+ for index in effective_default.split(self.separator)
394
+ ]
270
395
  else:
271
396
  raise ValueError(
272
397
  f"[{self.name}] 'never_prompt' is True but no valid "
@@ -308,7 +433,15 @@ class SelectionAction(BaseAction):
308
433
  cancel_key=self.cancel_key,
309
434
  )
310
435
  else:
311
- keys = effective_default
436
+ if effective_default and self.number_selections == 1:
437
+ keys = effective_default
438
+ elif effective_default:
439
+ keys = effective_default.split(self.separator)
440
+ else:
441
+ raise ValueError(
442
+ f"[{self.name}] 'never_prompt' is True but no valid "
443
+ "default_selection was provided."
444
+ )
312
445
  if keys == self.cancel_key:
313
446
  raise CancelSignal("User cancelled the selection.")
314
447
 
@@ -337,13 +470,13 @@ class SelectionAction(BaseAction):
337
470
 
338
471
  if isinstance(self.selections, list):
339
472
  sub = tree.add(f"[dim]Type:[/] List[str] ({len(self.selections)} items)")
340
- for i, item in enumerate(self.selections[:10]): # limit to 10
473
+ for i, item in enumerate(self.selections[:10]):
341
474
  sub.add(f"[dim]{i}[/]: {item}")
342
475
  if len(self.selections) > 10:
343
476
  sub.add(f"[dim]... ({len(self.selections) - 10} more)[/]")
344
477
  elif isinstance(self.selections, dict):
345
478
  sub = tree.add(
346
- f"[dim]Type:[/] Dict[str, (str, Any)] ({len(self.selections)} items)"
479
+ f"[dim]Type:[/] Dict[str, SelectionOption] ({len(self.selections)} items)"
347
480
  )
348
481
  for i, (key, option) in enumerate(list(self.selections.items())[:10]):
349
482
  sub.add(f"[dim]{key}[/]: {option.description}")
@@ -353,9 +486,30 @@ class SelectionAction(BaseAction):
353
486
  tree.add(f"[{OneColors.DARK_RED_b}]Invalid selections type[/]")
354
487
  return
355
488
 
356
- tree.add(f"[dim]Default:[/] '{self.default_selection or self.last_result}'")
357
- tree.add(f"[dim]Return:[/] {self.return_type.name.capitalize()}")
489
+ default = self.default_selection or self.last_result
490
+ if isinstance(default, list):
491
+ default_display = self.separator.join(str(d) for d in default)
492
+ else:
493
+ default_display = str(default or "")
494
+
495
+ tree.add(f"[dim]Default:[/] '{default_display}'")
496
+
497
+ return_behavior = {
498
+ "KEY": "selected key(s)",
499
+ "VALUE": "mapped value(s)",
500
+ "DESCRIPTION": "description(s)",
501
+ "ITEMS": "SelectionOption object(s)",
502
+ "DESCRIPTION_VALUE": "{description: value}",
503
+ }.get(self.return_type.name, self.return_type.name)
504
+
505
+ tree.add(
506
+ f"[dim]Return:[/] {self.return_type.name.capitalize()} → {return_behavior}"
507
+ )
358
508
  tree.add(f"[dim]Prompt:[/] {'Disabled' if self.never_prompt else 'Enabled'}")
509
+ tree.add(f"[dim]Columns:[/] {self.columns}")
510
+ tree.add(
511
+ f"[dim]Multi-select:[/] {'Yes' if self.number_selections != 1 else 'No'}"
512
+ )
359
513
 
360
514
  if not parent:
361
515
  self.console.print(tree)
@@ -75,7 +75,7 @@ class ExecutionRegistry:
75
75
  _store_by_name: dict[str, list[ExecutionContext]] = defaultdict(list)
76
76
  _store_by_index: dict[int, ExecutionContext] = {}
77
77
  _store_all: list[ExecutionContext] = []
78
- _console = Console(color_system="truecolor")
78
+ _console: Console = console
79
79
  _index = 0
80
80
  _lock = Lock()
81
81
 
@@ -205,8 +205,8 @@ class ExecutionRegistry:
205
205
  elif status.lower() in ["all", "success"]:
206
206
  final_status = f"[{OneColors.GREEN}]✅ Success"
207
207
  final_result = repr(ctx.result)
208
- if len(final_result) > 1000:
209
- final_result = f"{final_result[:1000]}..."
208
+ if len(final_result) > 50:
209
+ final_result = f"{final_result[:50]}..."
210
210
  else:
211
211
  continue
212
212
 
@@ -0,0 +1 @@
1
+ __version__ = "0.1.64"
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "falyx"
3
- version = "0.1.63"
3
+ version = "0.1.64"
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.63"
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