kash-shell 0.3.11__py3-none-any.whl → 0.3.13__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.
Files changed (95) hide show
  1. kash/actions/core/markdownify.py +5 -4
  2. kash/actions/core/readability.py +4 -4
  3. kash/actions/core/render_as_html.py +8 -6
  4. kash/actions/core/show_webpage.py +2 -2
  5. kash/actions/core/strip_html.py +2 -2
  6. kash/commands/base/basic_file_commands.py +24 -3
  7. kash/commands/base/diff_commands.py +38 -3
  8. kash/commands/base/files_command.py +5 -4
  9. kash/commands/base/reformat_command.py +1 -1
  10. kash/commands/base/show_command.py +1 -1
  11. kash/commands/extras/parse_uv_lock.py +12 -3
  12. kash/commands/workspace/selection_commands.py +1 -1
  13. kash/commands/workspace/workspace_commands.py +62 -16
  14. kash/config/env_settings.py +2 -42
  15. kash/config/logger.py +30 -25
  16. kash/config/logger_basic.py +6 -6
  17. kash/config/settings.py +23 -7
  18. kash/config/setup.py +33 -5
  19. kash/config/text_styles.py +25 -22
  20. kash/docs/load_source_code.py +1 -1
  21. kash/embeddings/cosine.py +12 -4
  22. kash/embeddings/embeddings.py +16 -6
  23. kash/embeddings/text_similarity.py +10 -4
  24. kash/exec/__init__.py +3 -0
  25. kash/exec/action_decorators.py +4 -19
  26. kash/exec/action_exec.py +46 -27
  27. kash/exec/fetch_url_metadata.py +8 -5
  28. kash/exec/importing.py +4 -4
  29. kash/exec/llm_transforms.py +2 -2
  30. kash/exec/preconditions.py +11 -19
  31. kash/exec/runtime_settings.py +134 -0
  32. kash/exec/shell_callable_action.py +5 -3
  33. kash/file_storage/file_store.py +91 -53
  34. kash/file_storage/item_file_format.py +6 -3
  35. kash/file_storage/store_filenames.py +7 -3
  36. kash/help/help_embeddings.py +2 -2
  37. kash/llm_utils/clean_headings.py +1 -1
  38. kash/{text_handling → llm_utils}/custom_sliding_transforms.py +0 -3
  39. kash/llm_utils/init_litellm.py +16 -0
  40. kash/llm_utils/llm_api_keys.py +6 -2
  41. kash/llm_utils/llm_completion.py +12 -5
  42. kash/local_server/__init__.py +1 -1
  43. kash/local_server/local_server_commands.py +2 -1
  44. kash/mcp/__init__.py +1 -1
  45. kash/mcp/mcp_cli.py +3 -2
  46. kash/mcp/mcp_server_commands.py +8 -2
  47. kash/mcp/mcp_server_routes.py +11 -12
  48. kash/media_base/media_cache.py +10 -3
  49. kash/media_base/transcription_deepgram.py +15 -2
  50. kash/model/__init__.py +1 -1
  51. kash/model/actions_model.py +9 -54
  52. kash/model/exec_model.py +79 -0
  53. kash/model/items_model.py +131 -81
  54. kash/model/operations_model.py +38 -15
  55. kash/model/paths_model.py +2 -0
  56. kash/shell/output/shell_output.py +10 -8
  57. kash/shell/shell_main.py +2 -2
  58. kash/shell/ui/shell_results.py +2 -1
  59. kash/shell/utils/exception_printing.py +2 -2
  60. kash/utils/common/format_utils.py +0 -14
  61. kash/utils/common/import_utils.py +46 -18
  62. kash/utils/common/task_stack.py +4 -15
  63. kash/utils/errors.py +14 -9
  64. kash/utils/file_utils/file_formats_model.py +61 -26
  65. kash/utils/file_utils/file_sort_filter.py +10 -3
  66. kash/utils/file_utils/filename_parsing.py +41 -16
  67. kash/{text_handling → utils/text_handling}/doc_normalization.py +23 -13
  68. kash/utils/text_handling/escape_html_tags.py +156 -0
  69. kash/{text_handling → utils/text_handling}/markdown_utils.py +82 -4
  70. kash/utils/text_handling/markdownify_utils.py +87 -0
  71. kash/{text_handling → utils/text_handling}/unified_diffs.py +1 -44
  72. kash/web_content/file_cache_utils.py +42 -34
  73. kash/web_content/local_file_cache.py +29 -12
  74. kash/web_content/web_extract.py +1 -1
  75. kash/web_content/web_extract_readabilipy.py +4 -2
  76. kash/web_content/web_fetch.py +42 -7
  77. kash/web_content/web_page_model.py +2 -1
  78. kash/web_gen/simple_webpage.py +1 -1
  79. kash/web_gen/templates/base_styles.css.jinja +139 -16
  80. kash/web_gen/templates/simple_webpage.html.jinja +1 -1
  81. kash/workspaces/__init__.py +12 -3
  82. kash/workspaces/selections.py +2 -2
  83. kash/workspaces/workspace_dirs.py +58 -0
  84. kash/workspaces/workspace_importing.py +2 -2
  85. kash/workspaces/workspace_output.py +2 -2
  86. kash/workspaces/workspaces.py +26 -90
  87. kash/xonsh_custom/load_into_xonsh.py +4 -2
  88. {kash_shell-0.3.11.dist-info → kash_shell-0.3.13.dist-info}/METADATA +4 -4
  89. {kash_shell-0.3.11.dist-info → kash_shell-0.3.13.dist-info}/RECORD +93 -89
  90. kash/shell/utils/argparse_utils.py +0 -20
  91. kash/utils/lang_utils/inflection.py +0 -18
  92. /kash/{text_handling → utils/text_handling}/markdown_render.py +0 -0
  93. {kash_shell-0.3.11.dist-info → kash_shell-0.3.13.dist-info}/WHEEL +0 -0
  94. {kash_shell-0.3.11.dist-info → kash_shell-0.3.13.dist-info}/entry_points.txt +0 -0
  95. {kash_shell-0.3.11.dist-info → kash_shell-0.3.13.dist-info}/licenses/LICENSE +0 -0
kash/exec/action_exec.py CHANGED
@@ -1,11 +1,16 @@
1
1
  import time
2
2
  from dataclasses import replace
3
3
 
4
- from prettyfmt import fmt_lines
4
+ from prettyfmt import fmt_lines, plural
5
5
 
6
6
  from kash.config.logger import get_logger
7
- from kash.config.text_styles import EMOJI_SKIP, EMOJI_SUCCESS, EMOJI_TIMING
8
- from kash.exec.preconditions import is_url_item
7
+ from kash.config.text_styles import (
8
+ EMOJI_SKIP,
9
+ EMOJI_START,
10
+ EMOJI_SUCCESS,
11
+ EMOJI_TIMING,
12
+ )
13
+ from kash.exec.preconditions import is_url_resource
9
14
  from kash.exec.resolve_args import assemble_action_args
10
15
  from kash.exec_model.args_model import CommandArg
11
16
  from kash.file_storage.file_store import FileStore
@@ -17,15 +22,15 @@ from kash.model.actions_model import (
17
22
  ExecContext,
18
23
  PathOpType,
19
24
  )
25
+ from kash.model.exec_model import RuntimeSettings
20
26
  from kash.model.items_model import Item, State
21
27
  from kash.model.operations_model import Input, Operation, Source
22
28
  from kash.model.params_model import ALL_COMMON_PARAMS, GLOBAL_PARAMS, RawParamValues
23
29
  from kash.model.paths_model import StorePath
24
- from kash.shell.output.shell_output import PrintHooks, print_h3
30
+ from kash.shell.output.shell_output import PrintHooks
25
31
  from kash.utils.common.task_stack import task_stack
26
32
  from kash.utils.common.type_utils import not_none
27
- from kash.utils.errors import NONFATAL_EXCEPTIONS, ContentError, InvalidOutput
28
- from kash.utils.lang_utils.inflection import plural
33
+ from kash.utils.errors import ContentError, InvalidOutput, get_nonfatal_exceptions
29
34
  from kash.workspaces import Selection, current_ws
30
35
  from kash.workspaces.workspace_importing import import_and_load
31
36
 
@@ -50,7 +55,7 @@ def prepare_action_input(*input_args: CommandArg, refetch: bool = False) -> Acti
50
55
  if input_items:
51
56
  log.message("Assembling metadata for input items:\n%s", fmt_lines(input_items))
52
57
  input_items = [
53
- fetch_url_item_metadata(item, refetch=refetch) if is_url_item(item) else item
58
+ fetch_url_item_metadata(item, refetch=refetch) if is_url_resource(item) else item
54
59
  for item in input_items
55
60
  ]
56
61
 
@@ -63,6 +68,7 @@ def validate_action_input(
63
68
  """
64
69
  Validate an action input, ensuring the right number of args, all explicit params are filled,
65
70
  and the precondition holds and return an `Operation` that describes what will happen.
71
+ For flexibility, we don't require the items to be saved (have a store path).
66
72
  """
67
73
  input_items = action_input.items
68
74
  # Validations:
@@ -75,10 +81,16 @@ def validate_action_input(
75
81
 
76
82
  # Now make a note of the the operation we will perform.
77
83
  # If the inputs are paths, record the input paths, including hashes.
78
- store_paths = [StorePath(not_none(item.store_path)) for item in input_items if item.store_path]
79
- inputs = [Input(store_path, ws.hash(store_path)) for store_path in store_paths]
84
+ def input_for(item: Item) -> Input:
85
+ if item.store_path:
86
+ return Input(StorePath(item.store_path), ws.hash(StorePath(item.store_path)))
87
+ else:
88
+ return Input(path=None, source_info="unsaved")
89
+
90
+ inputs = [input_for(item) for item in input_items]
91
+
80
92
  # Add any non-default runtime options into the options summary.
81
- options = {**action.param_value_summary(), **context.runtime_options}
93
+ options = {**action.param_value_summary(), **context.settings.non_default_options}
82
94
  operation = Operation(action.name, inputs, options)
83
95
 
84
96
  return operation
@@ -89,7 +101,7 @@ def log_action(action: Action, action_input: ActionInput, operation: Operation):
89
101
  Log the action and the operation we are about to run.
90
102
  """
91
103
  PrintHooks.before_log_action_run()
92
- print_h3(f"Action `{action.name}`")
104
+ log.message("%s Action: `%s`", EMOJI_START, action.name)
93
105
  log.message("Running: `%s`", operation.command_line(with_options=True))
94
106
  if len(action.param_value_summary()) > 0:
95
107
  log.message("Parameters:\n%s", action.param_value_summary_str())
@@ -106,8 +118,9 @@ def check_for_existing_result(
106
118
  already exist.
107
119
  """
108
120
  action = context.action
109
- ws = context.workspace
110
- rerun = context.rerun
121
+ settings = context.settings
122
+ ws = settings.workspace
123
+ rerun = settings.rerun
111
124
 
112
125
  existing_result = None
113
126
 
@@ -154,6 +167,7 @@ def run_action_operation(
154
167
 
155
168
  # Run the action.
156
169
  action = context.action
170
+ settings = context.settings
157
171
  if action.run_per_item:
158
172
  result = _run_for_each_item(context, action_input)
159
173
  else:
@@ -172,9 +186,9 @@ def run_action_operation(
172
186
  item.update_history(Source(operation=this_op, output_num=i, cacheable=action.cacheable))
173
187
 
174
188
  # Override the state if appropriate (this handles marking items as transient).
175
- if context.override_state:
189
+ if settings.override_state:
176
190
  for item in result.items:
177
- item.state = context.override_state
191
+ item.state = settings.override_state
178
192
 
179
193
  log.info("Action `%s` result: %s", action.name, result)
180
194
 
@@ -233,7 +247,7 @@ def _run_for_each_item(context: ExecContext, input: ActionInput) -> ActionResult
233
247
  log.info("Caught SkipItem exception, skipping run on this item")
234
248
  result_items.append(item)
235
249
  continue
236
- except NONFATAL_EXCEPTIONS as e:
250
+ except get_nonfatal_exceptions() as e:
237
251
  errors.append(e)
238
252
  had_error = True
239
253
 
@@ -284,7 +298,7 @@ def save_action_result(
284
298
  skipped_paths.append(store_path)
285
299
  continue
286
300
 
287
- ws.save(item, as_tmp=as_tmp, no_format=no_format)
301
+ ws.save(item, overwrite=result.overwrite, as_tmp=as_tmp, no_format=no_format)
288
302
 
289
303
  if skipped_paths:
290
304
  log.message(
@@ -293,14 +307,18 @@ def save_action_result(
293
307
  fmt_lines(skipped_paths),
294
308
  )
295
309
 
296
- input_store_paths = [StorePath(not_none(item.store_path)) for item in input_items]
310
+ unsaved_items = [item for item in input_items if not item.store_path]
311
+ input_store_paths = [StorePath(item.store_path) for item in input_items if item.store_path]
297
312
  result_store_paths = [StorePath(item.store_path) for item in result.items if item.store_path]
298
313
  old_inputs = sorted(set(input_store_paths) - set(result_store_paths))
314
+ if unsaved_items:
315
+ log.info("unsaved_items:\n%s", fmt_lines(unsaved_items))
299
316
  log.info("result_store_paths:\n%s", fmt_lines(result_store_paths))
300
- log.info("old_inputs:\n%s", fmt_lines(old_inputs))
317
+ if old_inputs:
318
+ log.info("old_inputs:\n%s", fmt_lines(old_inputs))
301
319
 
302
320
  # If there is a hint that the action replaces the input, archive any inputs that are not in the result.
303
- archived_store_paths = []
321
+ archived_store_paths: list[StorePath] = []
304
322
  if result.replaces_input and input_items:
305
323
  for input_store_path in old_inputs:
306
324
  # Note some outputs may be missing if replace_input was used.
@@ -325,7 +343,8 @@ def run_action_with_caching(
325
343
  Note: Mutates the input but only to add `context` to each item.
326
344
  """
327
345
  action = context.action
328
- ws = context.workspace
346
+ settings = context.settings
347
+ ws = settings.workspace
329
348
 
330
349
  # For convenience, we include the context to each item too (this helps so per-item
331
350
  # functions don't have to take context args everywhere).
@@ -341,7 +360,7 @@ def run_action_with_caching(
341
360
  # Check if a previous run already produced the result.
342
361
  existing_result = check_for_existing_result(context, action_input, operation)
343
362
 
344
- if existing_result and not context.rerun:
363
+ if existing_result and not settings.rerun:
345
364
  # Use the cached result.
346
365
  result = existing_result
347
366
  result_store_paths = [StorePath(not_none(item.store_path)) for item in result.items]
@@ -349,7 +368,7 @@ def run_action_with_caching(
349
368
 
350
369
  PrintHooks.before_done_message()
351
370
  log.message(
352
- "%s Action skipped: `%s` completed with %s %s",
371
+ "%s Skipped: `%s` completed with %s %s",
353
372
  EMOJI_SKIP,
354
373
  action.name,
355
374
  len(result.items),
@@ -359,12 +378,12 @@ def run_action_with_caching(
359
378
  # Run it!
360
379
  result = run_action_operation(context, action_input, operation)
361
380
  result_store_paths, archived_store_paths = save_action_result(
362
- ws, result, action_input, as_tmp=context.tmp_output, no_format=context.no_format
381
+ ws, result, action_input, as_tmp=settings.tmp_output, no_format=settings.no_format
363
382
  )
364
383
 
365
384
  PrintHooks.before_done_message()
366
385
  log.message(
367
- "%s Action done: `%s` completed with %s %s",
386
+ "%s Done: `%s` completed with %s %s",
368
387
  EMOJI_SUCCESS,
369
388
  action.name,
370
389
  len(result.items),
@@ -415,8 +434,7 @@ def run_action_with_shell_context(
415
434
  action_name = action.name
416
435
 
417
436
  # Execution context. This is fixed for the duration of the action.
418
- context = ExecContext(
419
- action=action,
437
+ settings = RuntimeSettings(
420
438
  workspace_dir=ws.base_dir,
421
439
  rerun=rerun,
422
440
  refetch=refetch,
@@ -424,6 +442,7 @@ def run_action_with_shell_context(
424
442
  tmp_output=tmp_output,
425
443
  no_format=no_format,
426
444
  )
445
+ context = ExecContext(action, settings)
427
446
 
428
447
  # Collect args from the provided args or otherwise the current selection.
429
448
  args, from_selection = assemble_action_args(*provided_args, use_selection=action.uses_selection)
@@ -1,19 +1,18 @@
1
1
  from kash.config.logger import get_logger
2
- from kash.exec.preconditions import is_url_item
2
+ from kash.exec.preconditions import is_url_resource
3
3
  from kash.media_base.media_services import get_media_metadata
4
4
  from kash.model.items_model import Item, ItemType
5
5
  from kash.model.paths_model import StorePath
6
6
  from kash.utils.common.format_utils import fmt_loc
7
7
  from kash.utils.common.url import Url, is_url
8
8
  from kash.utils.errors import InvalidInput
9
- from kash.web_content.canon_url import canonicalize_url
10
- from kash.web_content.web_extract import fetch_extract
11
- from kash.workspaces import current_ws
12
9
 
13
10
  log = get_logger(__name__)
14
11
 
15
12
 
16
13
  def fetch_url_metadata(locator: Url | StorePath, refetch: bool = False) -> Item:
14
+ from kash.workspaces import current_ws
15
+
17
16
  ws = current_ws()
18
17
  if is_url(locator):
19
18
  # Import or find URL as a resource in the current workspace.
@@ -21,7 +20,7 @@ def fetch_url_metadata(locator: Url | StorePath, refetch: bool = False) -> Item:
21
20
  item = ws.load(store_path)
22
21
  elif isinstance(locator, StorePath):
23
22
  item = ws.load(locator)
24
- if not is_url_item(item):
23
+ if not is_url_resource(item):
25
24
  raise InvalidInput(f"Not a URL resource: {fmt_loc(locator)}")
26
25
  else:
27
26
  raise InvalidInput(f"Not a URL or URL resource: {fmt_loc(locator)}")
@@ -34,6 +33,10 @@ def fetch_url_item_metadata(item: Item, refetch: bool = False) -> Item:
34
33
  Fetch metadata for a URL using a media service if we recognize the URL,
35
34
  and otherwise fetching and extracting it from the web page HTML.
36
35
  """
36
+ from kash.web_content.canon_url import canonicalize_url
37
+ from kash.web_content.web_extract import fetch_extract
38
+ from kash.workspaces import current_ws
39
+
37
40
  ws = current_ws()
38
41
  if not refetch and item.title and item.description:
39
42
  log.message(
kash/exec/importing.py CHANGED
@@ -5,7 +5,7 @@ from prettyfmt import fmt_lines, fmt_path
5
5
  from kash.config.logger import get_logger
6
6
  from kash.exec.action_registry import action_classes, refresh_action_classes
7
7
  from kash.exec.command_registry import get_all_commands
8
- from kash.utils.common.import_utils import Tallies, import_subdirs
8
+ from kash.utils.common.import_utils import Tallies, import_recursive
9
9
 
10
10
  log = get_logger(__name__)
11
11
 
@@ -13,12 +13,12 @@ log = get_logger(__name__)
13
13
  def import_and_register(
14
14
  package_name: str | None,
15
15
  parent_dir: Path,
16
- subdir_names: list[str] | None = None,
16
+ resource_names: list[str] | None = None,
17
17
  tallies: Tallies | None = None,
18
18
  ):
19
19
  """
20
20
  This hook can be used for auto-registering commands and actions from any
21
- subdirectory of a given package.
21
+ module or subdirectory of a given package.
22
22
 
23
23
  Useful to call from `__init__.py` files to import a directory of code,
24
24
  auto-registering annotated commands and actions and also handles refreshing the
@@ -38,7 +38,7 @@ def import_and_register(
38
38
  prev_command_count = len(get_all_commands())
39
39
  prev_action_count = len(ac)
40
40
 
41
- import_subdirs(package_name, parent_dir, subdir_names, tallies)
41
+ import_recursive(package_name, parent_dir, resource_names, tallies)
42
42
 
43
43
  new_command_count = len(get_all_commands()) - prev_command_count
44
44
  new_action_count = len(ac) - prev_action_count
@@ -13,9 +13,9 @@ from kash.llm_utils.llm_completion import llm_template_completion
13
13
  from kash.llm_utils.llm_messages import Message, MessageTemplate
14
14
  from kash.model.actions_model import LLMOptions
15
15
  from kash.model.items_model import Item
16
- from kash.text_handling.doc_normalization import normalize_formatting_ansi
17
16
  from kash.utils.errors import InvalidInput
18
17
  from kash.utils.file_utils.file_formats_model import Format
18
+ from kash.utils.text_handling.doc_normalization import normalize_formatting
19
19
 
20
20
  log = get_logger(__name__)
21
21
 
@@ -118,7 +118,7 @@ def llm_transform_item(
118
118
  if strip_fence:
119
119
  result_str = strip_markdown_fence(result_str)
120
120
  if normalize:
121
- result_str = normalize_formatting_ansi(result_str, format=format)
121
+ result_str = normalize_formatting(result_str, format=format)
122
122
 
123
123
  result_item.body = result_str
124
124
  return result_item
@@ -7,9 +7,9 @@ from chopdiff.html import has_timestamp
7
7
 
8
8
  from kash.exec.precondition_registry import kash_precondition
9
9
  from kash.model.items_model import Item, ItemType
10
- from kash.text_handling.markdown_utils import extract_bullet_points
11
10
  from kash.utils.file_utils.file_formats import is_full_html_page
12
11
  from kash.utils.file_utils.file_formats_model import Format
12
+ from kash.utils.text_handling.markdown_utils import extract_bullet_points
13
13
 
14
14
 
15
15
  @kash_precondition
@@ -32,6 +32,11 @@ def is_docx_resource(item: Item) -> bool:
32
32
  return bool(is_resource(item) and item.format and item.format == Format.docx)
33
33
 
34
34
 
35
+ @kash_precondition
36
+ def is_pdf_resource(item: Item) -> bool:
37
+ return bool(is_resource(item) and item.format and item.format == Format.pdf)
38
+
39
+
35
40
  @kash_precondition
36
41
  def is_concept(item: Item) -> bool:
37
42
  return item.type == ItemType.concept
@@ -53,7 +58,7 @@ def is_instructions(item: Item) -> bool:
53
58
 
54
59
 
55
60
  @kash_precondition
56
- def is_url_item(item: Item) -> bool:
61
+ def is_url_resource(item: Item) -> bool:
57
62
  return bool(item.type == ItemType.resource and item.url)
58
63
 
59
64
 
@@ -85,13 +90,13 @@ def contains_curly_vars(item: Item) -> bool:
85
90
 
86
91
 
87
92
  @kash_precondition
88
- def has_text_body(item: Item) -> bool:
89
- return has_body(item) and item.format in (Format.plaintext, Format.markdown, Format.md_html)
93
+ def has_simple_text_body(item: Item) -> bool:
94
+ return bool(has_body(item) and item.format and item.format.is_simple_text)
90
95
 
91
96
 
92
97
  @kash_precondition
93
98
  def has_html_body(item: Item) -> bool:
94
- return has_body(item) and item.format in (Format.html, Format.md_html)
99
+ return bool(has_body(item) and item.format and item.format.is_html)
95
100
 
96
101
 
97
102
  @kash_precondition
@@ -106,7 +111,7 @@ def is_plaintext(item: Item) -> bool:
106
111
 
107
112
  @kash_precondition
108
113
  def is_markdown(item: Item) -> bool:
109
- return has_body(item) and item.format in (Format.markdown, Format.md_html)
114
+ return bool(has_body(item) and item.format and item.format.is_markdown)
110
115
 
111
116
 
112
117
  @kash_precondition
@@ -114,19 +119,6 @@ def is_markdown_template(item: Item) -> bool:
114
119
  return is_markdown(item) and contains_curly_vars(item)
115
120
 
116
121
 
117
- @kash_precondition
118
- def is_html(item: Item) -> bool:
119
- return has_body(item) and item.format == Format.html
120
-
121
-
122
- @kash_precondition
123
- def is_text_doc(item: Item) -> bool:
124
- """
125
- A document that can be processed by LLMs and other plaintext tools.
126
- """
127
- return (is_plaintext(item) or is_markdown(item)) and has_body(item)
128
-
129
-
130
122
  @kash_precondition
131
123
  def is_markdown_list(item: Item) -> bool:
132
124
  try:
@@ -0,0 +1,134 @@
1
+ import contextvars
2
+ from dataclasses import dataclass
3
+ from pathlib import Path
4
+
5
+ from kash.config.logger import get_logger
6
+ from kash.model.exec_model import RuntimeSettings
7
+ from kash.model.items_model import State
8
+ from kash.workspaces.workspace_dirs import enclosing_ws_dir, global_ws_dir
9
+
10
+ log = get_logger(__name__)
11
+
12
+ _current_settings: contextvars.ContextVar[RuntimeSettings | None] = contextvars.ContextVar(
13
+ "current_runtime_settings", default=None
14
+ )
15
+ """
16
+ Context variable that tracks the current runtime settings. Only used if it is
17
+ explicitly set with a `with runtime_settings(...):` block.
18
+ """
19
+
20
+
21
+ def current_runtime_settings() -> RuntimeSettings:
22
+ """
23
+ Get the current runtime settings. Uses the ambient context var settings if
24
+ set and otherwise infers the workspace from the current working directory
25
+ with default runtime settings.
26
+ """
27
+
28
+ ambient_settings = _current_settings.get()
29
+ if ambient_settings:
30
+ return ambient_settings
31
+
32
+ default_ws_dir = enclosing_ws_dir() or global_ws_dir()
33
+ return RuntimeSettings(default_ws_dir)
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class WsContext:
38
+ global_ws_dir: Path
39
+ enclosing_ws_dir: Path | None
40
+ override_dir: Path | None
41
+
42
+ @property
43
+ def current_ws_dir(self) -> Path:
44
+ if self.override_dir:
45
+ return self.override_dir
46
+ elif self.enclosing_ws_dir:
47
+ return self.enclosing_ws_dir
48
+ else:
49
+ return self.global_ws_dir
50
+
51
+
52
+ def current_ws_context() -> WsContext:
53
+ """
54
+ Context path info about the current workspace, including the global workspace
55
+ directory, any workspace directory that encloses the current working directory,
56
+ and override set via runtime settings.
57
+ """
58
+
59
+ override_dir = None
60
+ ambient_settings = _current_settings.get()
61
+ if ambient_settings:
62
+ override_dir = ambient_settings.workspace_dir
63
+
64
+ return WsContext(global_ws_dir(), enclosing_ws_dir(), override_dir)
65
+
66
+
67
+ @dataclass
68
+ class RuntimeSettingsManager:
69
+ """
70
+ Manage the context for executing actions, including `RuntimeSettings` and
71
+ `Workspace`.
72
+
73
+ This is a minimal base class for use as a context manager. Most functionality
74
+ is still in `FileStore`.
75
+
76
+ Workspaces may be detected based on the current working directory or explicitly
77
+ set using a `with` block:
78
+ ```
79
+ ws = get_ws("my_workspace")
80
+ with ws:
81
+ # code that calls current_ws() will use this workspace
82
+ ```
83
+ """
84
+
85
+ settings: RuntimeSettings
86
+
87
+ def __enter__(self):
88
+ self._token = _current_settings.set(self.settings)
89
+ log.info("New runtime context: %s", self.settings)
90
+ return self.settings
91
+
92
+ def __exit__(self, exc_type, exc_val, exc_tb):
93
+ _current_settings.reset(self._token)
94
+ log.info("Exiting runtime context: %s", self.settings)
95
+
96
+
97
+ def kash_runtime(
98
+ workspace_dir: Path | None,
99
+ *,
100
+ rerun: bool = False,
101
+ refetch: bool = False,
102
+ override_state: State | None = None,
103
+ tmp_output: bool = False,
104
+ no_format: bool = False,
105
+ ) -> RuntimeSettingsManager:
106
+ """
107
+ Set a specific kash execution context for a with block.
108
+ This allows defining a workspace and other execution settings as the ambient
109
+ context within the block.
110
+
111
+ If `workspace_dir` is not provided, the current workspace will be inferred
112
+ from the working directory or fall back to the global workspace.
113
+
114
+ Example usage:
115
+ ```
116
+ with kash_runtime(ws_path, rerun=args.rerun) as runtime:
117
+ runtime.workspace.log_workspace_info()
118
+ # Perform actions.
119
+ ```
120
+ """
121
+ from kash.workspaces.workspaces import current_ws
122
+
123
+ if workspace_dir is None:
124
+ workspace_dir = current_ws().base_dir
125
+
126
+ settings = RuntimeSettings(
127
+ workspace_dir=workspace_dir,
128
+ rerun=rerun,
129
+ refetch=refetch,
130
+ override_state=override_state,
131
+ tmp_output=tmp_output,
132
+ no_format=no_format,
133
+ )
134
+ return RuntimeSettingsManager(settings=settings)
@@ -13,7 +13,7 @@ from kash.model.params_model import RawParamValues
13
13
  from kash.shell.output.shell_output import PrintHooks
14
14
  from kash.shell.utils.exception_printing import summarize_traceback
15
15
  from kash.utils.common.parse_shell_args import parse_shell_args
16
- from kash.utils.errors import NONFATAL_EXCEPTIONS
16
+ from kash.utils.errors import get_nonfatal_exceptions
17
17
 
18
18
  log = get_logger(__name__)
19
19
 
@@ -71,14 +71,16 @@ class ShellCallableAction:
71
71
  action_cls, explicit_values, *shell_args.args, rerun=rerun, refetch=refetch
72
72
  )
73
73
  # We don't return the result to keep the xonsh shell output clean.
74
- except NONFATAL_EXCEPTIONS as e:
74
+ except get_nonfatal_exceptions() as e:
75
75
  PrintHooks.nonfatal_exception()
76
76
  log.error(f"[{COLOR_ERROR}]Action error:[/{COLOR_ERROR}] %s", summarize_traceback(e))
77
77
  log.info("Action error details: %s", e, exc_info=True)
78
78
  return ShellResult(exception=e)
79
79
  except Exception as e:
80
80
  # Log here while we are in the true call stack (not inside the xonsh call stack).
81
- log.error("Action error: %s", e, exc_info=KashEnv.KASH_SHOW_TRACEBACK.read_bool(True))
81
+ log.error(
82
+ "Action error: %s", e, exc_info=KashEnv.KASH_SHOW_TRACEBACK.read_bool(default=True)
83
+ )
82
84
  raise
83
85
  finally:
84
86
  log_tallies(level="warning", if_slower_than=10.0)