kash-shell 0.3.8__py3-none-any.whl → 0.3.10__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 (154) hide show
  1. kash/actions/__init__.py +4 -4
  2. kash/actions/core/markdownify.py +5 -2
  3. kash/actions/core/readability.py +5 -2
  4. kash/actions/core/render_as_html.py +18 -0
  5. kash/actions/core/webpage_config.py +12 -4
  6. kash/commands/__init__.py +8 -20
  7. kash/commands/base/basic_file_commands.py +15 -0
  8. kash/commands/base/debug_commands.py +15 -2
  9. kash/commands/base/general_commands.py +27 -18
  10. kash/commands/base/logs_commands.py +1 -4
  11. kash/commands/base/model_commands.py +8 -8
  12. kash/commands/base/search_command.py +3 -2
  13. kash/commands/base/show_command.py +5 -3
  14. kash/commands/extras/parse_uv_lock.py +186 -0
  15. kash/commands/help/doc_commands.py +2 -31
  16. kash/commands/help/welcome.py +33 -0
  17. kash/commands/workspace/selection_commands.py +11 -6
  18. kash/commands/workspace/workspace_commands.py +19 -16
  19. kash/config/colors.py +2 -0
  20. kash/config/env_settings.py +72 -0
  21. kash/config/init.py +2 -2
  22. kash/config/logger.py +61 -59
  23. kash/config/logger_basic.py +12 -5
  24. kash/config/server_config.py +6 -6
  25. kash/config/settings.py +117 -67
  26. kash/config/setup.py +35 -9
  27. kash/config/suppress_warnings.py +30 -12
  28. kash/config/text_styles.py +3 -13
  29. kash/docs/load_api_docs.py +2 -1
  30. kash/docs/markdown/topics/a2_installation.md +7 -3
  31. kash/docs/markdown/topics/a3_getting_started.md +3 -2
  32. kash/docs/markdown/warning.md +3 -8
  33. kash/docs/markdown/welcome.md +4 -0
  34. kash/docs_base/load_recipe_snippets.py +1 -1
  35. kash/docs_base/recipes/{general_system_commands.ksh → general_system_commands.sh} +1 -1
  36. kash/{concepts → embeddings}/cosine.py +2 -1
  37. kash/embeddings/text_similarity.py +57 -0
  38. kash/exec/__init__.py +20 -3
  39. kash/exec/action_decorators.py +18 -4
  40. kash/exec/action_exec.py +41 -23
  41. kash/exec/action_registry.py +13 -48
  42. kash/exec/command_registry.py +2 -1
  43. kash/exec/fetch_url_metadata.py +4 -6
  44. kash/exec/importing.py +56 -0
  45. kash/exec/llm_transforms.py +6 -6
  46. kash/exec/precondition_registry.py +2 -1
  47. kash/exec/preconditions.py +16 -1
  48. kash/exec/shell_callable_action.py +33 -19
  49. kash/file_storage/file_store.py +23 -14
  50. kash/file_storage/item_file_format.py +13 -3
  51. kash/file_storage/metadata_dirs.py +11 -2
  52. kash/help/assistant.py +2 -2
  53. kash/help/assistant_instructions.py +2 -1
  54. kash/help/help_embeddings.py +2 -2
  55. kash/help/help_printing.py +14 -10
  56. kash/help/tldr_help.py +5 -3
  57. kash/llm_utils/clean_headings.py +1 -1
  58. kash/llm_utils/llm_api_keys.py +4 -4
  59. kash/llm_utils/llm_completion.py +2 -2
  60. kash/llm_utils/llm_features.py +68 -0
  61. kash/llm_utils/llm_messages.py +1 -2
  62. kash/llm_utils/llm_names.py +1 -1
  63. kash/llm_utils/llms.py +17 -12
  64. kash/local_server/__init__.py +5 -2
  65. kash/local_server/local_server.py +56 -46
  66. kash/local_server/local_server_commands.py +15 -15
  67. kash/local_server/local_server_routes.py +2 -2
  68. kash/local_server/local_url_formatters.py +1 -1
  69. kash/mcp/__init__.py +5 -2
  70. kash/mcp/mcp_cli.py +54 -17
  71. kash/mcp/mcp_server_commands.py +5 -6
  72. kash/mcp/mcp_server_routes.py +14 -11
  73. kash/mcp/mcp_server_sse.py +61 -34
  74. kash/mcp/mcp_server_stdio.py +0 -8
  75. kash/media_base/audio_processing.py +81 -7
  76. kash/media_base/media_cache.py +18 -18
  77. kash/media_base/media_services.py +1 -1
  78. kash/media_base/media_tools.py +6 -6
  79. kash/media_base/services/local_file_media.py +2 -2
  80. kash/media_base/{speech_transcription.py → transcription_deepgram.py} +25 -109
  81. kash/media_base/transcription_format.py +73 -0
  82. kash/media_base/transcription_whisper.py +38 -0
  83. kash/model/__init__.py +73 -5
  84. kash/model/actions_model.py +38 -4
  85. kash/model/concept_model.py +30 -0
  86. kash/model/items_model.py +56 -13
  87. kash/model/params_model.py +24 -0
  88. kash/shell/completions/completion_scoring.py +37 -5
  89. kash/shell/output/kerm_codes.py +1 -2
  90. kash/shell/output/shell_formatting.py +14 -4
  91. kash/shell/shell_main.py +2 -2
  92. kash/shell/utils/exception_printing.py +6 -0
  93. kash/shell/utils/native_utils.py +26 -20
  94. kash/text_handling/custom_sliding_transforms.py +12 -4
  95. kash/text_handling/doc_normalization.py +6 -2
  96. kash/text_handling/markdown_render.py +117 -0
  97. kash/text_handling/markdown_utils.py +204 -0
  98. kash/utils/common/import_utils.py +12 -3
  99. kash/utils/common/type_utils.py +0 -29
  100. kash/utils/common/url.py +80 -28
  101. kash/utils/errors.py +6 -0
  102. kash/utils/file_utils/{dir_size.py → dir_info.py} +25 -4
  103. kash/utils/file_utils/file_ext.py +2 -3
  104. kash/utils/file_utils/file_formats.py +28 -2
  105. kash/utils/file_utils/file_formats_model.py +50 -19
  106. kash/utils/file_utils/filename_parsing.py +10 -4
  107. kash/web_content/dir_store.py +1 -2
  108. kash/web_content/file_cache_utils.py +37 -10
  109. kash/web_content/file_processing.py +68 -0
  110. kash/web_content/local_file_cache.py +12 -9
  111. kash/web_content/web_extract.py +8 -3
  112. kash/web_content/web_fetch.py +12 -4
  113. kash/web_gen/tabbed_webpage.py +5 -2
  114. kash/web_gen/templates/base_styles.css.jinja +120 -14
  115. kash/web_gen/templates/base_webpage.html.jinja +60 -13
  116. kash/web_gen/templates/content_styles.css.jinja +4 -2
  117. kash/web_gen/templates/item_view.html.jinja +2 -2
  118. kash/web_gen/templates/tabbed_webpage.html.jinja +1 -2
  119. kash/workspaces/__init__.py +15 -2
  120. kash/workspaces/selections.py +18 -3
  121. kash/workspaces/source_items.py +4 -2
  122. kash/workspaces/workspace_output.py +11 -4
  123. kash/workspaces/workspaces.py +5 -11
  124. kash/xonsh_custom/command_nl_utils.py +40 -19
  125. kash/xonsh_custom/custom_shell.py +44 -12
  126. kash/xonsh_custom/customize_prompt.py +39 -21
  127. kash/xonsh_custom/load_into_xonsh.py +26 -27
  128. kash/xonsh_custom/shell_load_commands.py +2 -2
  129. kash/xonsh_custom/xonsh_completers.py +2 -249
  130. kash/xonsh_custom/xonsh_keybindings.py +282 -0
  131. kash/xonsh_custom/xonsh_modern_tools.py +3 -3
  132. kash/xontrib/kash_extension.py +5 -6
  133. {kash_shell-0.3.8.dist-info → kash_shell-0.3.10.dist-info}/METADATA +26 -12
  134. {kash_shell-0.3.8.dist-info → kash_shell-0.3.10.dist-info}/RECORD +140 -140
  135. {kash_shell-0.3.8.dist-info → kash_shell-0.3.10.dist-info}/entry_points.txt +1 -1
  136. kash/concepts/concept_formats.py +0 -23
  137. kash/concepts/text_similarity.py +0 -112
  138. kash/shell/clideps/api_keys.py +0 -99
  139. kash/shell/clideps/dotenv_setup.py +0 -114
  140. kash/shell/clideps/dotenv_utils.py +0 -89
  141. kash/shell/clideps/pkg_deps.py +0 -232
  142. kash/shell/clideps/platforms.py +0 -11
  143. kash/shell/clideps/terminal_features.py +0 -56
  144. kash/shell/utils/osc_utils.py +0 -95
  145. kash/shell/utils/terminal_images.py +0 -133
  146. kash/text_handling/markdown_util.py +0 -167
  147. kash/utils/common/atomic_var.py +0 -158
  148. kash/utils/common/string_replace.py +0 -93
  149. kash/utils/common/string_template.py +0 -101
  150. /kash/docs_base/recipes/{python_dev_commands.ksh → python_dev_commands.sh} +0 -0
  151. /kash/docs_base/recipes/{tldr_standard_commands.ksh → tldr_standard_commands.sh} +0 -0
  152. /kash/{concepts → embeddings}/embeddings.py +0 -0
  153. {kash_shell-0.3.8.dist-info → kash_shell-0.3.10.dist-info}/WHEEL +0 -0
  154. {kash_shell-0.3.8.dist-info → kash_shell-0.3.10.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,33 @@
1
+ from rich.box import SQUARE
2
+ from rich.console import Group
3
+ from rich.panel import Panel
4
+
5
+ from kash.commands.help.logo import branded_box
6
+ from kash.config.text_styles import (
7
+ COLOR_HINT,
8
+ )
9
+ from kash.docs.all_docs import all_docs
10
+ from kash.exec import kash_command
11
+ from kash.shell.output.shell_output import PrintHooks, cprint
12
+ from kash.shell.version import get_full_version_name
13
+ from kash.utils.rich_custom.rich_markdown_fork import Markdown
14
+
15
+
16
+ @kash_command
17
+ def welcome() -> None:
18
+ """
19
+ Print a welcome message.
20
+ """
21
+
22
+ help_topics = all_docs.help_topics
23
+ version = get_full_version_name()
24
+ # Create header with logo and right-justified version
25
+
26
+ PrintHooks.before_welcome()
27
+ cprint(
28
+ branded_box(
29
+ Group(Markdown(help_topics.welcome)),
30
+ version,
31
+ )
32
+ )
33
+ cprint(Panel(Markdown(help_topics.warning), box=SQUARE, border_style=COLOR_HINT))
@@ -27,8 +27,9 @@ def select(
27
27
  previous: bool = False,
28
28
  next: bool = False,
29
29
  pop: bool = False,
30
- clear: bool = False,
30
+ clear_all: bool = False,
31
31
  clear_future: bool = False,
32
+ refresh: bool = False,
32
33
  ) -> ShellResult:
33
34
  """
34
35
  Set or show the current selection.
@@ -47,12 +48,13 @@ def select(
47
48
  :param previous: Move back in the selection history to the previous selection.
48
49
  :param next: Move forward in the selection history to the next selection.
49
50
  :param pop: Pop the current selection from the history.
50
- :param clear: Clear the full selection history.
51
+ :param clear_all: Clear the full selection history.
51
52
  :param clear_future: Clear all selections from history after the current one.
53
+ :param refresh: Refresh the current selection to drop any paths that no longer exist.
52
54
  """
53
55
  ws = current_ws()
54
56
 
55
- # TODO: It would be nice to be able to read stdin from a pipe but this isn't working rn.
57
+ # FIXME: It would be nice to be able to read stdin from a pipe but this isn't working rn.
56
58
  # You could then run `... | select --stdin` to select the piped input.
57
59
  # Globally we have THREAD_SUBPROCS=False to avoid hard-to-interrupt subprocesses.
58
60
  # But xonsh seems to hang with stdin unless we modify the spec to be threadable?
@@ -61,7 +63,7 @@ def select(
61
63
  # if stdin:
62
64
  # paths = tuple(sys.stdin.read().splitlines())
63
65
 
64
- exclusive_flags = [history, last, back, forward, previous, next, pop, clear, clear_future]
66
+ exclusive_flags = [history, last, back, forward, previous, next, pop, clear_all, clear_future]
65
67
  if sum(bool(f) for f in exclusive_flags) > 1:
66
68
  raise InvalidInput("Cannot combine multiple flags")
67
69
  if paths and any(exclusive_flags):
@@ -96,12 +98,15 @@ def select(
96
98
  elif pop:
97
99
  ws.selections.pop()
98
100
  return ShellResult(show_selection=True)
99
- elif clear:
100
- ws.selections.clear()
101
+ elif clear_all:
102
+ ws.selections.clear_all()
101
103
  return ShellResult(show_selection=True)
102
104
  elif clear_future:
103
105
  ws.selections.clear_future()
104
106
  return ShellResult(show_selection=True)
107
+ elif refresh:
108
+ ws.selections.refresh_current(ws.base_dir)
109
+ return ShellResult(show_selection=True)
105
110
  else:
106
111
  return ShellResult(show_selection=True)
107
112
 
@@ -15,7 +15,6 @@ from kash.config.text_styles import (
15
15
  EMOJI_WARN,
16
16
  STYLE_EMPH,
17
17
  STYLE_HINT,
18
- format_success_emoji,
19
18
  )
20
19
  from kash.exec import (
21
20
  assemble_path_args,
@@ -36,7 +35,11 @@ from kash.model.items_model import Item, ItemType
36
35
  from kash.model.params_model import GLOBAL_PARAMS
37
36
  from kash.model.paths_model import StorePath, fmt_store_path
38
37
  from kash.shell.input.param_inputs import input_param_name, input_param_value
39
- from kash.shell.output.shell_formatting import format_name_and_description, format_name_and_value
38
+ from kash.shell.output.shell_formatting import (
39
+ format_name_and_description,
40
+ format_name_and_value,
41
+ format_success_emoji,
42
+ )
40
43
  from kash.shell.output.shell_output import (
41
44
  PrintHooks,
42
45
  Wrap,
@@ -54,7 +57,7 @@ from kash.utils.common.type_utils import not_none
54
57
  from kash.utils.common.url import Url, is_url
55
58
  from kash.utils.errors import InvalidInput
56
59
  from kash.utils.file_formats.chat_format import tail_chat_history
57
- from kash.utils.file_utils.dir_size import is_nonempty_dir
60
+ from kash.utils.file_utils.dir_info import is_nonempty_dir
58
61
  from kash.utils.lang_utils.inflection import plural
59
62
  from kash.web_content.file_cache_utils import cache_file
60
63
  from kash.workspaces import (
@@ -170,14 +173,15 @@ def cache_media(*urls: str) -> None:
170
173
 
171
174
 
172
175
  @kash_command
173
- def cache_content(*urls_or_paths: str) -> None:
176
+ def cache_content(*urls_or_paths: str, refetch: bool = False) -> None:
174
177
  """
175
178
  Cache the given file in the content cache. Downloads any URL or copies a local file.
176
179
  """
180
+ expiration_sec = 0 if refetch else None
177
181
  PrintHooks.spacer()
178
182
  for url_or_path in urls_or_paths:
179
183
  locator = resolve_locator_arg(url_or_path)
180
- cache_path, was_cached = cache_file(locator)
184
+ cache_path, was_cached = cache_file(locator, expiration_sec=expiration_sec)
181
185
  cache_str = " (already cached)" if was_cached else ""
182
186
  cprint(f"{fmt_loc(url_or_path)}{cache_str}:", style=STYLE_EMPH, text_wrap=Wrap.NONE)
183
187
  cprint(f"{cache_path}", text_wrap=Wrap.INDENT_ONLY)
@@ -185,10 +189,13 @@ def cache_content(*urls_or_paths: str) -> None:
185
189
 
186
190
 
187
191
  @kash_command
188
- def download(*urls_or_paths: str) -> None:
192
+ def download(*urls_or_paths: str, refetch: bool = False) -> None:
189
193
  """
190
- Download a URL or resource. Inputs can be URLs or paths to URL resources.
194
+ Download a URL or resource. Uses cached content if available, unless `refetch` is true.
195
+ Inputs can be URLs or paths to URL resources.
191
196
  """
197
+ expiration_sec = 0 if refetch else None
198
+
192
199
  # TODO: Add option to include frontmatter metadata for text files.
193
200
  ws = current_ws()
194
201
  for url_or_path in urls_or_paths:
@@ -211,7 +218,7 @@ def download(*urls_or_paths: str) -> None:
211
218
  media_tools.cache_media(url)
212
219
  else:
213
220
  log.message("Will cache file and save to workspace: %s", fmt_loc(url))
214
- cache_path, _was_cached = cache_file(url)
221
+ cache_path, _was_cached = cache_file(url, expiration_sec=expiration_sec)
215
222
  item = Item.from_external_path(cache_path, item_type=ItemType.resource)
216
223
  store_path = ws.save(item)
217
224
 
@@ -402,12 +409,10 @@ def set_params(*key_vals: str) -> None:
402
409
 
403
410
 
404
411
  @kash_command
405
- def list_params(full: bool = False) -> None:
412
+ def params(full: bool = False) -> None:
406
413
  """
407
- Show or set currently set of workspace parameters, which are settings that may be used
414
+ List currently set workspace parameters, which are settings that may be used
408
415
  by commands and actions or to override default parameters.
409
-
410
- Run with no args to interactively set parameters.
411
416
  """
412
417
  ws: Workspace = current_ws()
413
418
  settable_params = GLOBAL_PARAMS
@@ -461,9 +466,7 @@ def import_item(
461
466
 
462
467
 
463
468
  @kash_command
464
- def fetch_metadata(
465
- *files_or_urls: str, no_cache: bool = False, refetch: bool = False
466
- ) -> ShellResult:
469
+ def fetch_metadata(*files_or_urls: str, refetch: bool = False) -> ShellResult:
467
470
  """
468
471
  Fetch metadata for the given URLs or resources. Imports new URLs and saves back
469
472
  the fetched metadata for existing resources.
@@ -483,7 +486,7 @@ def fetch_metadata(
483
486
  try:
484
487
  if isinstance(locator, Path):
485
488
  raise InvalidInput()
486
- fetched_item = fetch_url_metadata(locator, use_cache=not no_cache, refetch=refetch)
489
+ fetched_item = fetch_url_metadata(locator, refetch=refetch)
487
490
  store_paths.append(fetched_item.store_path)
488
491
  except InvalidInput:
489
492
  log.warning("Not a URL or URL resource, will not fetch metadata: %s", fmt_loc(locator))
kash/config/colors.py CHANGED
@@ -134,8 +134,10 @@ web_light_translucent = SimpleNamespace(
134
134
  primary_light=hsl_to_hex("hsl(188, 40%, 62%)"),
135
135
  secondary=hsl_to_hex("hsl(188, 12%, 28%)"),
136
136
  bg=hsl_to_hex("hsla(44, 6%, 100%, 0.75)"),
137
+ bg_solid=hsl_to_hex("hsla(44, 6%, 100%, 1)"),
137
138
  bg_header=hsl_to_hex("hsla(188, 42%, 70%, 0.2)"),
138
139
  bg_alt=hsl_to_hex("hsla(44, 28%, 90%, 0.3)"),
140
+ bg_alt_solid=hsl_to_hex("hsla(44, 28%, 97%, 1)"),
139
141
  text=hsl_to_hex("hsl(188, 39%, 11%)"),
140
142
  border=hsl_to_hex("hsl(188, 8%, 50%)"),
141
143
  border_hint=hsl_to_hex("hsla(188, 8%, 72%, 0.7)"),
@@ -0,0 +1,72 @@
1
+ import os
2
+ from enum import Enum
3
+ from pathlib import Path
4
+ from typing import overload
5
+
6
+
7
+ class KashEnv(str, Enum):
8
+ """
9
+ Environment variable settings for kash. None are required, but these may be
10
+ used to override default values.
11
+ """
12
+
13
+ KASH_LOG_LEVEL = "KASH_LOG_LEVEL"
14
+ """The log level for console-based logging."""
15
+
16
+ KASH_WS_ROOT = "KASH_WS_ROOT"
17
+ """The root directory for kash workspaces."""
18
+
19
+ KASH_GLOBAL_WS = "KASH_GLOBAL_WS"
20
+ """The global workspace directory."""
21
+
22
+ KASH_SYSTEM_LOGS_DIR = "KASH_SYSTEM_LOGS_DIR"
23
+ """The directory for system logs."""
24
+
25
+ KASH_SYSTEM_CACHE_DIR = "KASH_SYSTEM_CACHE_DIR"
26
+ """The directory for system cache (caches separate from workspace caches)."""
27
+
28
+ KASH_MCP_WS = "KASH_MCP_WS"
29
+ """The directory for the workspace for MCP servers."""
30
+
31
+ KASH_SHOW_TRACEBACK = "KASH_SHOW_TRACEBACK"
32
+ """Whether to show tracebacks on actions and commands in the shell."""
33
+
34
+ KASH_USER_AGENT = "KASH_USER_AGENT"
35
+ """The user agent to use for HTTP requests."""
36
+
37
+ @overload
38
+ def read_str(self) -> str | None: ...
39
+
40
+ @overload
41
+ def read_str(self, default: str) -> str: ...
42
+
43
+ def read_str(self, default: str | None = None) -> str | None:
44
+ """
45
+ Get the value of the environment variable from the environment (with
46
+ optional default).
47
+ """
48
+ return os.environ.get(self.value, default)
49
+
50
+ @overload
51
+ def read_path(self) -> Path | None: ...
52
+
53
+ @overload
54
+ def read_path(self, default: Path) -> Path: ...
55
+
56
+ def read_path(self, default: Path | None = None) -> Path | None:
57
+ """
58
+ Get the value of the environment variable as a resolved path (with
59
+ optional default).
60
+ """
61
+ value = os.environ.get(self.value)
62
+ if value:
63
+ return Path(value).expanduser().resolve()
64
+ else:
65
+ return default.expanduser().resolve() if default else None
66
+
67
+ def read_bool(self, default: bool = False) -> bool:
68
+ """
69
+ Get the value of the environment variable as a boolean.
70
+ """
71
+ value = str(os.environ.get(self.value, default) or "").lower()
72
+ return bool(value and value != "0" and value != "false" and value != "no")
kash/config/init.py CHANGED
@@ -9,10 +9,10 @@ def kash_reload_all() -> tuple[dict[str, Callable], dict[str, type["Action"]]]:
9
9
  """
10
10
  Import all kash modules that define actions and commands.
11
11
  """
12
- from kash.exec.action_registry import reload_all_action_classes
12
+ from kash.exec.action_registry import refresh_action_classes
13
13
  from kash.exec.command_registry import get_all_commands
14
14
 
15
15
  commands = get_all_commands()
16
- actions = reload_all_action_classes()
16
+ actions = refresh_action_classes()
17
17
 
18
18
  return commands, actions
kash/config/logger.py CHANGED
@@ -2,7 +2,6 @@ import contextvars
2
2
  import logging
3
3
  import os
4
4
  import re
5
- import threading
6
5
  from collections.abc import Generator
7
6
  from contextlib import contextmanager
8
7
  from dataclasses import dataclass
@@ -12,12 +11,12 @@ from pathlib import Path
12
11
  from typing import IO, Any, cast
13
12
 
14
13
  import rich
14
+ from prettyfmt import slugify_snake
15
15
  from rich._null_file import NULL_FILE
16
16
  from rich.console import Console
17
17
  from rich.logging import RichHandler
18
18
  from rich.theme import Theme
19
- from slugify import slugify
20
- from strif import atomic_output_file, new_timestamped_uid
19
+ from strif import AtomicVar, atomic_output_file, new_timestamped_uid
21
20
  from typing_extensions import override
22
21
 
23
22
  import kash.config.suppress_warnings # noqa: F401
@@ -38,55 +37,41 @@ from kash.utils.common.stack_traces import current_stack_traces
38
37
  from kash.utils.common.task_stack import task_stack_prefix_str
39
38
 
40
39
 
41
- @dataclass(frozen=True)
40
+ @dataclass
42
41
  class LogSettings:
43
42
  log_console_level: LogLevel
44
43
  log_file_level: LogLevel
45
- # Always the same global log directory.
44
+
46
45
  global_log_dir: Path
46
+ """Global directory for log files."""
47
47
 
48
48
  # These directories can change based on the current workspace:
49
49
  log_dir: Path
50
- log_objects_dir: Path
51
- log_file_path: Path
52
-
53
-
54
- _log_dir = get_system_logs_dir()
55
- """
56
- Parent of the "logs" directory. Initially the global kash workspace.
57
- """
58
-
59
-
60
- LOG_NAME_GLOBAL = "global"
50
+ """Parent of the "logs" directory. Initially the global kash workspace."""
61
51
 
62
- _log_name = LOG_NAME_GLOBAL
63
- """
64
- Name of the log file. By default the workspace name or "global" if
65
- for the global workspace.
66
- """
52
+ log_name: str
53
+ """Name of the log file. Typically the workspace name or "workspace" if for the global workspace."""
67
54
 
68
- _log_lock = threading.RLock()
55
+ log_objects_dir: Path
56
+ log_file_path: Path
69
57
 
70
58
 
71
- def make_valid_log_name(name: str) -> str:
72
- name = str(name).strip().rstrip("/").removesuffix(".log")
73
- name = re.sub(r"[^\w-]", "_", name)
74
- return name
59
+ LOG_NAME_GLOBAL = "workspace"
75
60
 
76
61
 
77
62
  def _read_log_settings() -> LogSettings:
78
- global _log_dir, _log_name
79
63
  return LogSettings(
80
64
  log_console_level=global_settings().console_log_level,
81
65
  log_file_level=global_settings().file_log_level,
82
66
  global_log_dir=get_system_logs_dir(),
83
- log_dir=_log_dir,
84
- log_objects_dir=_log_dir / "objects" / _log_name,
85
- log_file_path=_log_dir / f"{_log_name}.log",
67
+ log_dir=get_system_logs_dir(),
68
+ log_name=LOG_NAME_GLOBAL,
69
+ log_objects_dir=get_system_logs_dir() / "objects" / LOG_NAME_GLOBAL,
70
+ log_file_path=get_system_logs_dir() / f"{LOG_NAME_GLOBAL}.log",
86
71
  )
87
72
 
88
73
 
89
- _log_settings: LogSettings = _read_log_settings()
74
+ _log_settings: AtomicVar[LogSettings] = AtomicVar(_read_log_settings())
90
75
 
91
76
  _setup_done = False
92
77
 
@@ -95,19 +80,13 @@ def get_log_settings() -> LogSettings:
95
80
  """
96
81
  Currently active log settings.
97
82
  """
98
- return _log_settings
83
+ return _log_settings.copy()
99
84
 
100
85
 
101
- def reset_log_root(log_root: Path | None = None, log_name: str | None = None):
102
- """
103
- Reset the logging root or log name, if it has changed. None means no change
104
- and global default values.
105
- """
106
- global _log_lock, _log_base, _log_name
107
- with _log_lock:
108
- _log_base = log_root or get_system_logs_dir()
109
- _log_name = make_valid_log_name(log_name or LOG_NAME_GLOBAL)
110
- reload_rich_logging_setup()
86
+ def make_valid_log_name(name: str) -> str:
87
+ name = str(name).strip().rstrip("/").removesuffix(".log")
88
+ name = re.sub(r"[^\w-]", "_", name)
89
+ return name
111
90
 
112
91
 
113
92
  console_context_var: contextvars.ContextVar[Console | None] = contextvars.ContextVar(
@@ -169,6 +148,30 @@ _file_handler: logging.FileHandler
169
148
  _console_handler: logging.Handler
170
149
 
171
150
 
151
+ def reset_rich_logging(
152
+ log_root: Path | None = None,
153
+ log_name: str | None = None,
154
+ log_path: Path | None = None,
155
+ ):
156
+ """
157
+ Set or reset the logging root or log name, if it has changed. None means no change
158
+ and global default values. `log_name` is the name of the log, excluding
159
+ the `.log` extension. If `log_path` is provided, it will be used to infer
160
+ the log root and name.
161
+ """
162
+ if log_path:
163
+ if not log_path.parent.exists():
164
+ log_path.parent.mkdir(parents=True, exist_ok=True)
165
+ log_root = log_path.parent
166
+ log_name = log_path.name
167
+
168
+ global _log_settings
169
+ with _log_settings.updates() as settings:
170
+ settings.log_dir = log_root or get_system_logs_dir()
171
+ settings.log_name = make_valid_log_name(log_name or LOG_NAME_GLOBAL)
172
+ reload_rich_logging_setup()
173
+
174
+
172
175
  def reload_rich_logging_setup():
173
176
  """
174
177
  Set up or reset logging setup. This is for rich/formatted console logging and
@@ -176,12 +179,12 @@ def reload_rich_logging_setup():
176
179
  Call at initial run and again if log directory changes. Replaces all previous
177
180
  loggers and handlers. Can be called to reset with different settings.
178
181
  """
179
- global _log_lock, _log_settings, _setup_done
180
- with _log_lock:
182
+ global _setup_done, _log_settings
183
+ with _log_settings.lock:
181
184
  new_log_settings = _read_log_settings()
182
- if not _setup_done or new_log_settings != _log_settings:
185
+ if not _setup_done or new_log_settings != _log_settings.value:
183
186
  _do_logging_setup(new_log_settings)
184
- _log_settings = new_log_settings
187
+ _log_settings.set(new_log_settings)
185
188
  _setup_done = True
186
189
 
187
190
  # get_console().print(
@@ -190,6 +193,15 @@ def reload_rich_logging_setup():
190
193
  # )
191
194
 
192
195
 
196
+ @cache
197
+ def _init_rich_logging():
198
+ rich.reconfigure(theme=get_theme(), highlighter=get_highlighter())
199
+
200
+ logging.setLoggerClass(CustomLogger)
201
+
202
+ reload_rich_logging_setup()
203
+
204
+
193
205
  def _do_logging_setup(log_settings: LogSettings):
194
206
  from kash.config.suppress_warnings import demote_warnings, filter_warnings
195
207
 
@@ -317,10 +329,9 @@ class CustomLogger(logging.Logger):
317
329
  global _log_settings
318
330
  prefix = prefix_slug + "." if prefix_slug else ""
319
331
  filename = (
320
- f"{prefix}{slugify(description, separator='_')}."
321
- f"{new_timestamped_uid()}.{file_ext.lstrip('.')}"
332
+ f"{prefix}{slugify_snake(description)}.{new_timestamped_uid()}.{file_ext.lstrip('.')}"
322
333
  )
323
- path = _log_settings.log_objects_dir / filename
334
+ path = _log_settings.copy().log_objects_dir / filename
324
335
  with atomic_output_file(path, make_parents=True) as tmp_filename:
325
336
  if isinstance(obj, bytes):
326
337
  with open(tmp_filename, "wb") as f:
@@ -347,7 +358,7 @@ def get_logger(name: str) -> CustomLogger:
347
358
  Get a logger that's compatible with system logging but has our additional custom
348
359
  methods.
349
360
  """
350
- init_rich_logging()
361
+ _init_rich_logging()
351
362
  logger = logging.getLogger(name)
352
363
  # print("Logger is", logger)
353
364
  return cast(CustomLogger, logger)
@@ -355,12 +366,3 @@ def get_logger(name: str) -> CustomLogger:
355
366
 
356
367
  def get_log_file_stream():
357
368
  return _file_handler.stream
358
-
359
-
360
- @cache
361
- def init_rich_logging():
362
- rich.reconfigure(theme=get_theme(), highlighter=get_highlighter())
363
-
364
- logging.setLoggerClass(CustomLogger)
365
-
366
- reload_rich_logging_setup()
@@ -1,14 +1,21 @@
1
1
  import logging
2
2
  import sys
3
- from logging import FileHandler, Formatter
3
+ from logging import FileHandler, Formatter, LogRecord
4
4
  from pathlib import Path
5
5
 
6
6
  from kash.config.settings import LogLevel
7
+ from kash.config.suppress_warnings import demote_warnings
7
8
 
8
9
  # Basic logging setup for non-interactive logging, like on a server.
9
10
  # For richer logging, see logger.py.
10
11
 
11
12
 
13
+ class SuppressedWarningsStreamHandler(logging.StreamHandler):
14
+ def emit(self, record: LogRecord):
15
+ demote_warnings(record, level=logging.DEBUG)
16
+ super().emit(record)
17
+
18
+
12
19
  def basic_file_handler(path: Path, level: LogLevel) -> logging.FileHandler:
13
20
  handler = logging.FileHandler(path)
14
21
  handler.setLevel(level.value)
@@ -17,13 +24,13 @@ def basic_file_handler(path: Path, level: LogLevel) -> logging.FileHandler:
17
24
 
18
25
 
19
26
  def basic_stderr_handler(level: LogLevel) -> logging.StreamHandler:
20
- handler = logging.StreamHandler(stream=sys.stderr)
27
+ handler = SuppressedWarningsStreamHandler(stream=sys.stderr)
21
28
  handler.setLevel(level.value)
22
29
  handler.setFormatter(Formatter("%(asctime)s %(levelname).1s %(name)s - %(message)s"))
23
30
  return handler
24
31
 
25
32
 
26
- def basic_logging_setup(file_log_path: Path | None, level: LogLevel):
33
+ def basic_logging_setup(log_path: Path | None, level: LogLevel):
27
34
  """
28
35
  Set up basic logging to a file and to stderr.
29
36
  """
@@ -31,8 +38,8 @@ def basic_logging_setup(file_log_path: Path | None, level: LogLevel):
31
38
  for h in root_logger.handlers[:]:
32
39
  root_logger.removeHandler(h)
33
40
 
34
- if file_log_path:
35
- file_handler: FileHandler = basic_file_handler(file_log_path, level)
41
+ if log_path:
42
+ file_handler: FileHandler = basic_file_handler(log_path, level)
36
43
  root_logger.addHandler(file_handler)
37
44
 
38
45
  stderr_handler = basic_stderr_handler(level)
@@ -7,7 +7,7 @@ if TYPE_CHECKING:
7
7
 
8
8
 
9
9
  def create_server_config(
10
- app: Callable[..., Any], host: str, port: int, server_name: str, log_path: Path
10
+ app: Callable[..., Any], host: str, port: int, _server_name: str, log_path: Path
11
11
  ) -> "uvicorn.Config":
12
12
  """
13
13
  Create a common server configuration for both local and MCP servers.
@@ -43,11 +43,11 @@ def create_server_config(
43
43
  "uvicorn": {"handlers": ["default"], "level": "INFO", "propagate": False},
44
44
  "uvicorn.error": {"handlers": ["default"], "level": "INFO", "propagate": False},
45
45
  "uvicorn.access": {"handlers": ["default"], "level": "INFO", "propagate": False},
46
- f"kash.{server_name}": {
47
- "handlers": ["default"],
48
- "level": "INFO",
49
- "propagate": False,
50
- },
46
+ # f"kash.{server_name}": {
47
+ # "handlers": ["default"],
48
+ # "level": "INFO",
49
+ # "propagate": False,
50
+ # },
51
51
  },
52
52
  },
53
53
  )