kash-shell 0.3.7__py3-none-any.whl → 0.3.9__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 (75) hide show
  1. kash/commands/base/debug_commands.py +4 -3
  2. kash/commands/base/general_commands.py +33 -41
  3. kash/commands/base/logs_commands.py +4 -8
  4. kash/commands/base/model_commands.py +16 -14
  5. kash/commands/base/search_command.py +2 -2
  6. kash/commands/workspace/workspace_commands.py +2 -3
  7. kash/concepts/cosine.py +2 -1
  8. kash/concepts/text_similarity.py +3 -58
  9. kash/config/env_settings.py +59 -0
  10. kash/config/logger.py +46 -38
  11. kash/config/logger_basic.py +10 -2
  12. kash/config/server_config.py +6 -6
  13. kash/config/settings.py +42 -47
  14. kash/config/setup.py +14 -4
  15. kash/config/suppress_warnings.py +30 -12
  16. kash/docs/markdown/topics/a2_installation.md +7 -3
  17. kash/docs/markdown/warning.md +3 -8
  18. kash/docs/markdown/welcome.md +4 -0
  19. kash/docs_base/load_recipe_snippets.py +1 -1
  20. kash/docs_base/recipes/{general_system_commands.ksh → general_system_commands.sh} +1 -1
  21. kash/exec/llm_transforms.py +3 -2
  22. kash/file_storage/file_store.py +0 -4
  23. kash/file_storage/item_file_format.py +8 -1
  24. kash/file_storage/metadata_dirs.py +2 -2
  25. kash/help/assistant.py +1 -1
  26. kash/help/help_pages.py +1 -1
  27. kash/help/help_printing.py +16 -9
  28. kash/help/tldr_help.py +5 -3
  29. kash/llm_utils/__init__.py +1 -0
  30. kash/llm_utils/llm_api_keys.py +39 -0
  31. kash/llm_utils/llm_completion.py +2 -2
  32. kash/llm_utils/llms.py +9 -9
  33. kash/local_server/local_server.py +50 -43
  34. kash/local_server/local_server_commands.py +15 -15
  35. kash/local_server/local_server_routes.py +2 -2
  36. kash/mcp/mcp_cli.py +53 -16
  37. kash/mcp/mcp_server_commands.py +10 -6
  38. kash/mcp/mcp_server_routes.py +9 -6
  39. kash/mcp/mcp_server_sse.py +59 -34
  40. kash/mcp/mcp_server_stdio.py +0 -8
  41. kash/media_base/audio_processing.py +81 -7
  42. kash/media_base/media_cache.py +10 -10
  43. kash/media_base/services/local_file_media.py +2 -2
  44. kash/media_base/speech_transcription.py +3 -2
  45. kash/model/items_model.py +12 -6
  46. kash/shell/clideps/api_keys.py +100 -0
  47. kash/shell/{input/collect_dotenv.py → clideps/dotenv_setup.py} +41 -6
  48. kash/{config → shell/clideps}/dotenv_utils.py +16 -6
  49. kash/shell/{utils/sys_tool_deps.py → clideps/pkg_deps.py} +63 -99
  50. kash/shell/clideps/platforms.py +11 -0
  51. kash/shell/clideps/terminal_features.py +56 -0
  52. kash/shell/output/shell_formatting.py +106 -0
  53. kash/shell/output/shell_output.py +1 -94
  54. kash/shell/utils/native_utils.py +11 -10
  55. kash/utils/common/atomic_var.py +16 -3
  56. kash/utils/common/url.py +53 -25
  57. kash/utils/file_utils/{dir_size.py → dir_info.py} +25 -4
  58. kash/utils/file_utils/file_ext.py +2 -3
  59. kash/utils/file_utils/file_formats.py +28 -2
  60. kash/utils/file_utils/file_formats_model.py +47 -19
  61. kash/utils/file_utils/filename_parsing.py +10 -4
  62. kash/workspaces/source_items.py +4 -1
  63. kash/workspaces/workspace_output.py +13 -5
  64. kash/workspaces/workspaces.py +9 -10
  65. kash/xonsh_custom/custom_shell.py +1 -1
  66. kash/xonsh_custom/load_into_xonsh.py +7 -5
  67. kash/xonsh_custom/xonsh_modern_tools.py +5 -5
  68. {kash_shell-0.3.7.dist-info → kash_shell-0.3.9.dist-info}/METADATA +19 -7
  69. {kash_shell-0.3.7.dist-info → kash_shell-0.3.9.dist-info}/RECORD +74 -69
  70. {kash_shell-0.3.7.dist-info → kash_shell-0.3.9.dist-info}/entry_points.txt +1 -1
  71. kash/config/api_keys.py +0 -109
  72. /kash/docs_base/recipes/{python_dev_commands.ksh → python_dev_commands.sh} +0 -0
  73. /kash/docs_base/recipes/{tldr_standard_commands.ksh → tldr_standard_commands.sh} +0 -0
  74. {kash_shell-0.3.7.dist-info → kash_shell-0.3.9.dist-info}/WHEEL +0 -0
  75. {kash_shell-0.3.7.dist-info → kash_shell-0.3.9.dist-info}/licenses/LICENSE +0 -0
@@ -12,7 +12,8 @@ from kash.help.function_param_info import annotate_param_info
12
12
  from kash.help.recommended_commands import RECOMMENDED_TLDR_COMMANDS
13
13
  from kash.model.params_model import Param
14
14
  from kash.shell.output.kerm_codes import IframePopover, TextTooltip
15
- from kash.shell.output.shell_output import PrintHooks, console_pager, cprint, format_name_and_value
15
+ from kash.shell.output.shell_formatting import format_name_and_value
16
+ from kash.shell.output.shell_output import PrintHooks, console_pager, cprint
16
17
  from kash.utils.errors import InvalidInput
17
18
 
18
19
  log = get_logger(__name__)
@@ -178,7 +179,7 @@ def reload_system() -> None:
178
179
  the local the local server. Not perfect! But sometimes useful for development.
179
180
  """
180
181
  import kash
181
- from kash.local_server.local_server import restart_local_server
182
+ from kash.local_server.local_server import restart_ui_server
182
183
  from kash.utils.common.import_utils import recursive_reload
183
184
 
184
185
  module = kash
@@ -196,7 +197,7 @@ def reload_system() -> None:
196
197
  log.info("Reloaded modules: %s", ", ".join(package_names))
197
198
  log.message("Reloaded %s modules from %s.", len(package_names), module.__name__)
198
199
 
199
- restart_local_server()
200
+ restart_ui_server()
200
201
 
201
202
  # TODO Re-register commands and actions.
202
203
 
@@ -1,34 +1,34 @@
1
+ from flowmark import Wrap
2
+
1
3
  from kash.commands.base.model_commands import list_apis, list_models
2
4
  from kash.commands.workspace.workspace_commands import list_params
3
- from kash.config.api_keys import (
4
- RECOMMENDED_APIS,
5
- Api,
6
- get_all_configured_models,
7
- load_dotenv_paths,
8
- print_api_key_setup,
9
- )
10
- from kash.config.dotenv_utils import env_var_is_set
11
5
  from kash.config.logger import get_logger
6
+ from kash.config.settings import RECOMMENDED_API_KEYS, get_system_config_dir
12
7
  from kash.docs.all_docs import all_docs
13
8
  from kash.exec import kash_command
14
9
  from kash.help.tldr_help import tldr_refresh_cache
10
+ from kash.llm_utils.llm_api_keys import get_all_configured_models
15
11
  from kash.model.params_model import (
16
12
  DEFAULT_CAREFUL_LLM,
17
13
  DEFAULT_FAST_LLM,
18
14
  DEFAULT_STANDARD_LLM,
19
15
  DEFAULT_STRUCTURED_LLM,
20
16
  )
21
- from kash.shell.input.collect_dotenv import fill_missing_dotenv
17
+ from kash.shell.clideps.api_keys import (
18
+ ApiEnvKey,
19
+ load_dotenv_paths,
20
+ print_api_key_setup,
21
+ )
22
+ from kash.shell.clideps.dotenv_setup import interactive_dotenv_setup
23
+ from kash.shell.clideps.pkg_deps import pkg_check
24
+ from kash.shell.clideps.terminal_features import terminal_check
22
25
  from kash.shell.input.input_prompts import input_choice
26
+ from kash.shell.output.shell_formatting import format_name_and_value
23
27
  from kash.shell.output.shell_output import (
24
28
  PrintHooks,
25
29
  cprint,
26
- format_failure,
27
- format_name_and_value,
28
- format_success,
29
30
  print_h2,
30
31
  )
31
- from kash.shell.utils.sys_tool_deps import sys_tool_check, terminal_feature_check
32
32
  from kash.shell.version import get_full_version_name
33
33
  from kash.utils.errors import InvalidState
34
34
  from kash.workspaces.workspaces import current_ws
@@ -50,8 +50,8 @@ def self_check(brief: bool = False) -> None:
50
50
  Self-check kash setup, including termal settings, tools, and API keys.
51
51
  """
52
52
  if brief:
53
- terminal_feature_check().print_term_info()
54
- print_api_key_setup(once=False)
53
+ terminal_check().print_term_info()
54
+ print_api_key_setup(recommended_keys=RECOMMENDED_API_KEYS, once=False)
55
55
  check_system_tools(brief=brief)
56
56
  tldr_refresh_cache()
57
57
  try:
@@ -63,7 +63,7 @@ def self_check(brief: bool = False) -> None:
63
63
  else:
64
64
  version()
65
65
  cprint()
66
- terminal_feature_check().print_term_info()
66
+ terminal_check().print_term_info()
67
67
  cprint()
68
68
  list_apis()
69
69
  cprint()
@@ -86,28 +86,18 @@ def self_check(brief: bool = False) -> None:
86
86
  @kash_command
87
87
  def self_configure(all: bool = False, update: bool = False) -> None:
88
88
  """
89
- Interactively configure your .env file with recommended API keys.
90
-
91
- :param all: Configure all known API keys (instead of just recommended ones).
92
- :param update: Update values even if they are already set.
89
+ Interactively configure API keys and preferred models.
93
90
  """
94
91
 
92
+ if all:
93
+ api_keys = [key.value for key in ApiEnvKey]
94
+ else:
95
+ api_keys = RECOMMENDED_API_KEYS
95
96
  # Show APIs before starting.
96
97
  list_apis()
97
98
 
98
- apis = Api if all else RECOMMENDED_APIS
99
- keys = [api.value for api in apis]
100
- if not update:
101
- keys = [key for key in keys if not env_var_is_set(key)]
102
-
103
- cprint()
104
- print_h2("Configuring .env file")
105
- if keys:
106
- cprint(format_failure(f"API keys needed: {', '.join(keys)}"))
107
- fill_missing_dotenv(keys)
108
- reload_env()
109
- else:
110
- cprint(format_success("All requested API keys are set!"))
99
+ interactive_dotenv_setup(api_keys, update=update)
100
+ reload_env()
111
101
 
112
102
  cprint()
113
103
  ws = current_ws()
@@ -170,15 +160,15 @@ def check_system_tools(warn_only: bool = False, brief: bool = False) -> None:
170
160
  :param brief: Print summary as a single line.
171
161
  """
172
162
  if warn_only:
173
- sys_tool_check().warn_if_missing()
163
+ pkg_check().warn_if_missing()
174
164
  else:
175
165
  if brief:
176
- cprint(sys_tool_check().status())
166
+ cprint(pkg_check().status())
177
167
  else:
178
168
  print_h2("Installed System Tools")
179
- cprint(sys_tool_check().formatted())
169
+ cprint(pkg_check().formatted())
180
170
  cprint()
181
- sys_tool_check().warn_if_missing()
171
+ pkg_check().warn_if_missing()
182
172
 
183
173
 
184
174
  @kash_command
@@ -187,10 +177,10 @@ def reload_env() -> None:
187
177
  Reload the environment variables from the .env file.
188
178
  """
189
179
 
190
- env_paths = load_dotenv_paths()
180
+ env_paths = load_dotenv_paths(True, True, get_system_config_dir())
191
181
  if env_paths:
192
182
  cprint("Reloaded environment variables")
193
- print_api_key_setup()
183
+ print_api_key_setup(RECOMMENDED_API_KEYS)
194
184
  else:
195
185
  raise InvalidState("No .env file found")
196
186
 
@@ -209,7 +199,9 @@ def kits() -> None:
209
199
  else:
210
200
  cprint("Currently imported kits:")
211
201
  for kit in get_loaded_kits().values():
212
- cprint(format_name_and_value(f"{kit.name} kit", str(kit.path or "")))
202
+ cprint(
203
+ format_name_and_value(f"{kit.name} kit", str(kit.path or ""), text_wrap=Wrap.NONE)
204
+ )
213
205
 
214
206
 
215
207
  @kash_command
@@ -222,5 +214,5 @@ def settings() -> None:
222
214
  settings = global_settings()
223
215
  print_h2("Global Settings")
224
216
  for field, value in settings.__dict__.items():
225
- cprint(format_name_and_value(field, str(value)))
217
+ cprint(format_name_and_value(field, str(value), text_wrap=Wrap.NONE))
226
218
  PrintHooks.spacer()
@@ -7,11 +7,11 @@ from kash.config.logger import get_log_settings, get_logger, reload_rich_logging
7
7
  from kash.config.settings import (
8
8
  LogLevel,
9
9
  atomic_global_settings,
10
- global_settings,
11
- server_log_file_path,
10
+ local_server_log_path,
12
11
  )
13
12
  from kash.exec import kash_command
14
- from kash.shell.output.shell_output import cprint, format_name_and_value, print_status
13
+ from kash.shell.output.shell_formatting import format_name_and_value
14
+ from kash.shell.output.shell_output import cprint, print_status
15
15
  from kash.shell.utils.native_utils import tail_file
16
16
  from kash.utils.common.format_utils import fmt_loc
17
17
 
@@ -85,8 +85,4 @@ def log_settings() -> None:
85
85
  cprint(format_name_and_value("log_objects_dir", str(settings.log_objects_dir)))
86
86
  cprint(format_name_and_value("log_file_level", settings.log_file_level.name))
87
87
  cprint(format_name_and_value("log_console_level", settings.log_console_level.name))
88
- cprint(
89
- format_name_and_value(
90
- "server_log_file", str(server_log_file_path(global_settings().local_server_port))
91
- )
92
- )
88
+ cprint(format_name_and_value("server_log_file_path", str(local_server_log_path())))
@@ -1,17 +1,19 @@
1
1
  from flowmark import Wrap
2
2
  from rich.text import Text
3
3
 
4
- from kash.config.api_keys import Api
5
- from kash.config.dotenv_utils import env_var_is_set
6
- from kash.config.text_styles import format_success_emoji
7
4
  from kash.exec.command_registry import kash_command
8
- from kash.llm_utils import LLM
9
- from kash.shell.output.shell_output import (
10
- cprint,
5
+ from kash.llm_utils import LLM, api_for_model
6
+ from kash.shell.clideps.api_keys import ApiEnvKey
7
+ from kash.shell.clideps.dotenv_utils import env_var_is_set
8
+ from kash.shell.output.shell_formatting import (
11
9
  format_failure,
12
10
  format_name_and_value,
13
11
  format_success,
12
+ format_success_emoji,
14
13
  format_success_or_failure,
14
+ )
15
+ from kash.shell.output.shell_output import (
16
+ cprint,
15
17
  print_h2,
16
18
  )
17
19
 
@@ -23,11 +25,11 @@ def list_models() -> None:
23
25
  """
24
26
  print_h2("Models")
25
27
  for model in LLM.all_names():
26
- api = Api.for_model(model)
27
- have_key = bool(api and env_var_is_set(api.env_var))
28
+ api = api_for_model(model)
29
+ have_key = bool(api and env_var_is_set(api.value))
28
30
  if api:
29
31
  provider_msg = format_success(f"provider {api.name}")
30
- key_message = format_success_or_failure(have_key, f"{api.env_var}")
32
+ key_message = format_success_or_failure(have_key, f"{api.value}")
31
33
  else:
32
34
  provider_msg = format_failure("provider not recognized")
33
35
  key_message = format_failure("unknown API key")
@@ -46,11 +48,11 @@ def list_apis() -> None:
46
48
  List and check configuration for all APIs.
47
49
  """
48
50
  print_h2("API keys")
49
- for api in Api:
50
- emoji = format_success_emoji(env_var_is_set(api.env_var))
51
+ for api in ApiEnvKey:
52
+ emoji = format_success_emoji(env_var_is_set(api.value))
51
53
  message = (
52
- f"API key {api.env_var} found"
53
- if env_var_is_set(api.env_var)
54
- else f"API key {api.env_var} not found"
54
+ f"API key {api.value} found"
55
+ if env_var_is_set(api.value)
56
+ else f"API key {api.value} not found"
55
57
  )
56
58
  cprint(Text.assemble(emoji, format_name_and_value(api.name, message)))
@@ -3,8 +3,8 @@ from pathlib import Path
3
3
  from kash.config.logger import get_logger
4
4
  from kash.exec import assemble_path_args, kash_command
5
5
  from kash.exec_model.shell_model import ShellResult
6
+ from kash.shell.clideps.pkg_deps import Pkg, pkg_check
6
7
  from kash.shell.output.shell_output import cprint
7
- from kash.shell.utils.sys_tool_deps import SysTool, sys_tool_check
8
8
  from kash.utils.common.parse_shell_args import shell_quote
9
9
  from kash.utils.errors import InvalidState
10
10
 
@@ -33,7 +33,7 @@ def search(
33
33
  :param ignore_case: Ignore case when searching.
34
34
  :param verbose: Also print the ripgrep command line.
35
35
  """
36
- sys_tool_check().require(SysTool.ripgrep)
36
+ pkg_check().require(Pkg.ripgrep)
37
37
  from ripgrepy import RipGrepNotFound, Ripgrepy
38
38
 
39
39
  resolved_paths = assemble_path_args(*paths)
@@ -36,13 +36,12 @@ from kash.model.items_model import Item, ItemType
36
36
  from kash.model.params_model import GLOBAL_PARAMS
37
37
  from kash.model.paths_model import StorePath, fmt_store_path
38
38
  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
39
40
  from kash.shell.output.shell_output import (
40
41
  PrintHooks,
41
42
  Wrap,
42
43
  console_pager,
43
44
  cprint,
44
- format_name_and_description,
45
- format_name_and_value,
46
45
  print_h2,
47
46
  print_h3,
48
47
  print_status,
@@ -55,7 +54,7 @@ from kash.utils.common.type_utils import not_none
55
54
  from kash.utils.common.url import Url, is_url
56
55
  from kash.utils.errors import InvalidInput
57
56
  from kash.utils.file_formats.chat_format import tail_chat_history
58
- from kash.utils.file_utils.dir_size import is_nonempty_dir
57
+ from kash.utils.file_utils.dir_info import is_nonempty_dir
59
58
  from kash.utils.lang_utils.inflection import plural
60
59
  from kash.web_content.file_cache_utils import cache_file
61
60
  from kash.workspaces import (
kash/concepts/cosine.py CHANGED
@@ -1,9 +1,10 @@
1
1
  from collections.abc import Sequence
2
+ from typing import Any, TypeAlias
2
3
 
3
4
  import numpy as np
4
5
 
5
6
  # Type aliases for clarity
6
- ArrayLike = Sequence[float] | np.ndarray
7
+ ArrayLike: TypeAlias = Sequence[float] | np.ndarray[Any, Any]
7
8
 
8
9
 
9
10
  def cosine(u: ArrayLike, v: ArrayLike) -> float:
@@ -1,12 +1,11 @@
1
1
  from typing import cast
2
2
 
3
3
  import litellm
4
- import pandas as pd
5
- from funlog import log_calls, tally_calls
4
+ from funlog import log_calls
6
5
  from litellm import embedding
7
6
  from litellm.types.utils import EmbeddingResponse
8
7
 
9
- from kash.concepts.cosine import cosine
8
+ from kash.concepts.cosine import ArrayLike, cosine
10
9
  from kash.concepts.embeddings import Embeddings
11
10
  from kash.config.logger import get_logger
12
11
  from kash.llm_utils.llms import DEFAULT_EMBEDDING_MODEL, EmbeddingModel
@@ -15,11 +14,7 @@ from kash.utils.errors import ApiResultError
15
14
  log = get_logger(__name__)
16
15
 
17
16
 
18
- def sort_by_length(values: list[str]) -> list[str]:
19
- return sorted(values, key=lambda x: (len(x), x))
20
-
21
-
22
- def cosine_relatedness(x, y):
17
+ def cosine_relatedness(x: ArrayLike, y: ArrayLike) -> float:
23
18
  return 1 - cosine(x, y)
24
19
 
25
20
 
@@ -60,53 +55,3 @@ def rank_by_relatedness(
60
55
  scored_strings.sort(key=lambda x: x[2], reverse=True)
61
56
 
62
57
  return scored_strings[:top_n]
63
-
64
-
65
- @tally_calls(level="warning", min_total_runtime=5, if_slower_than=10)
66
- def relate_texts_by_embedding(
67
- embeddings: Embeddings, relatedness_fn=cosine_relatedness
68
- ) -> pd.DataFrame:
69
- log.message("Computing relatedness matrix of %d text embeddings…", len(embeddings.data))
70
-
71
- keys = [key for key, _, _ in embeddings.as_iterable()]
72
- relatedness_matrix = pd.DataFrame(index=keys, columns=keys) # pyright: ignore
73
-
74
- for i, (key1, _, emb1) in enumerate(embeddings.as_iterable()):
75
- for j, (key2, _, emb2) in enumerate(embeddings.as_iterable()):
76
- if i <= j:
77
- score = relatedness_fn(emb1, emb2)
78
- relatedness_matrix.at[key1, key2] = score
79
- relatedness_matrix.at[key2, key1] = score
80
-
81
- # Fill diagonal (self-relatedness).
82
- for key in keys:
83
- relatedness_matrix.at[key, key] = 1.0
84
-
85
- return relatedness_matrix
86
-
87
-
88
- def find_related_pairs(
89
- relatedness_matrix: pd.DataFrame, threshold: float = 0.9
90
- ) -> list[tuple[str, str, float]]:
91
- log.message(
92
- "Finding near duplicates among %s items (threshold %s)",
93
- relatedness_matrix.shape[0],
94
- threshold,
95
- )
96
-
97
- pairs: list[tuple[str, str, float]] = []
98
- keys = relatedness_matrix.index.tolist()
99
-
100
- for i, key1 in enumerate(keys):
101
- for j, key2 in enumerate(keys):
102
- if i < j:
103
- relatedness = relatedness_matrix.at[key1, key2]
104
- if relatedness >= threshold:
105
- # Put shortest one first.
106
- [short_key, long_key] = sort_by_length([key1, key2])
107
- pairs.append((short_key, long_key, relatedness))
108
-
109
- # Sort with highest relatedness first.
110
- pairs.sort(key=lambda x: x[2], reverse=True)
111
-
112
- return pairs
@@ -0,0 +1,59 @@
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
+ @overload
32
+ def read_str(self) -> str | None: ...
33
+
34
+ @overload
35
+ def read_str(self, default: str) -> str: ...
36
+
37
+ def read_str(self, default: str | None = None) -> str | None:
38
+ """
39
+ Get the value of the environment variable from the environment (with
40
+ optional default).
41
+ """
42
+ return os.environ.get(self.value, default)
43
+
44
+ @overload
45
+ def read_path(self, default: None) -> None: ...
46
+
47
+ @overload
48
+ def read_path(self, default: Path) -> Path: ...
49
+
50
+ def read_path(self, default: Path | None = None) -> Path | None:
51
+ """
52
+ Get the value of the environment variable as a resolved path (with
53
+ optional default).
54
+ """
55
+ value = os.environ.get(self.value)
56
+ if value:
57
+ return Path(value).expanduser().resolve()
58
+ else:
59
+ return default.expanduser().resolve() if default else None
kash/config/logger.py CHANGED
@@ -12,18 +12,21 @@ from pathlib import Path
12
12
  from typing import IO, Any, cast
13
13
 
14
14
  import rich
15
- from rich import reconfigure
15
+ from prettyfmt import slugify_snake
16
16
  from rich._null_file import NULL_FILE
17
17
  from rich.console import Console
18
18
  from rich.logging import RichHandler
19
19
  from rich.theme import Theme
20
- from slugify import slugify
21
20
  from strif import atomic_output_file, new_timestamped_uid
22
21
  from typing_extensions import override
23
22
 
24
23
  import kash.config.suppress_warnings # noqa: F401
25
- from kash.config.logger_basic import basic_file_handler
26
- from kash.config.settings import GLOBAL_LOGS_DIR, LogLevel, get_global_kash_dir, global_settings
24
+ from kash.config.logger_basic import basic_file_handler, basic_stderr_handler
25
+ from kash.config.settings import (
26
+ LogLevel,
27
+ get_system_logs_dir,
28
+ global_settings,
29
+ )
27
30
  from kash.config.text_styles import (
28
31
  EMOJI_ERROR,
29
32
  EMOJI_SAVED,
@@ -48,9 +51,9 @@ class LogSettings:
48
51
  log_file_path: Path
49
52
 
50
53
 
51
- _log_base = get_global_kash_dir()
54
+ _log_dir = get_system_logs_dir()
52
55
  """
53
- Parent of the "logs" directory. Initially the global kash data root.
56
+ Parent of the "logs" directory. Initially the global kash workspace.
54
57
  """
55
58
 
56
59
 
@@ -72,14 +75,14 @@ def make_valid_log_name(name: str) -> str:
72
75
 
73
76
 
74
77
  def _read_log_settings() -> LogSettings:
75
- global _log_base, _log_name
78
+ global _log_dir, _log_name
76
79
  return LogSettings(
77
80
  log_console_level=global_settings().console_log_level,
78
81
  log_file_level=global_settings().file_log_level,
79
- global_log_dir=GLOBAL_LOGS_DIR,
80
- log_dir=_log_base / "logs",
81
- log_objects_dir=_log_base / "logs" / "objects" / _log_name,
82
- log_file_path=_log_base / "logs" / f"{_log_name}.log",
82
+ 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",
83
86
  )
84
87
 
85
88
 
@@ -102,7 +105,7 @@ def reset_log_root(log_root: Path | None = None, log_name: str | None = None):
102
105
  """
103
106
  global _log_lock, _log_base, _log_name
104
107
  with _log_lock:
105
- _log_base = log_root or get_global_kash_dir()
108
+ _log_base = log_root or get_system_logs_dir()
106
109
  _log_name = make_valid_log_name(log_name or LOG_NAME_GLOBAL)
107
110
  reload_rich_logging_setup()
108
111
 
@@ -125,9 +128,6 @@ def get_theme():
125
128
  return Theme(RICH_STYLES)
126
129
 
127
130
 
128
- reconfigure(theme=get_theme(), highlighter=get_highlighter())
129
-
130
-
131
131
  def get_console() -> Console:
132
132
  """
133
133
  Return the Rich global console, unless it is overridden by a
@@ -166,7 +166,7 @@ def record_console() -> Generator[Console, None, None]:
166
166
 
167
167
 
168
168
  _file_handler: logging.FileHandler
169
- _console_handler: RichHandler
169
+ _console_handler: logging.Handler
170
170
 
171
171
 
172
172
  def reload_rich_logging_setup():
@@ -210,20 +210,25 @@ def _do_logging_setup(log_settings: LogSettings):
210
210
  super().emit(record)
211
211
 
212
212
  global _console_handler
213
- _console_handler = PrefixedRichHandler(
214
- # For now we use the fixed global console for logging.
215
- # In the future we may want to add a way to have thread-local capture
216
- # of all system logs.
217
- console=rich.get_console(),
218
- level=log_settings.log_console_level.value,
219
- show_time=False,
220
- show_path=False,
221
- show_level=False,
222
- highlighter=get_highlighter(),
223
- markup=True,
224
- )
225
- _console_handler.setLevel(log_settings.log_console_level.value)
226
- _console_handler.setFormatter(Formatter("%(message)s"))
213
+
214
+ # Use the Rich stdout handler only on terminals, stderr for servers or non-interactive use.
215
+ if get_console().is_terminal:
216
+ _console_handler = PrefixedRichHandler(
217
+ # For now we use the fixed global console for logging.
218
+ # In the future we may want to add a way to have thread-local capture
219
+ # of all system logs.
220
+ console=rich.get_console(),
221
+ level=log_settings.log_console_level.value,
222
+ show_time=False,
223
+ show_path=False,
224
+ show_level=False,
225
+ highlighter=get_highlighter(),
226
+ markup=True,
227
+ )
228
+ _console_handler.setLevel(log_settings.log_console_level.value)
229
+ _console_handler.setFormatter(Formatter("%(message)s"))
230
+ else:
231
+ _console_handler = basic_stderr_handler(log_settings.log_console_level)
227
232
 
228
233
  # Manually adjust logging for a few packages, removing previous verbose default handlers.
229
234
 
@@ -312,8 +317,7 @@ class CustomLogger(logging.Logger):
312
317
  global _log_settings
313
318
  prefix = prefix_slug + "." if prefix_slug else ""
314
319
  filename = (
315
- f"{prefix}{slugify(description, separator='_')}."
316
- f"{new_timestamped_uid()}.{file_ext.lstrip('.')}"
320
+ f"{prefix}{slugify_snake(description)}.{new_timestamped_uid()}.{file_ext.lstrip('.')}"
317
321
  )
318
322
  path = _log_settings.log_objects_dir / filename
319
323
  with atomic_output_file(path, make_parents=True) as tmp_filename:
@@ -337,17 +341,12 @@ class CustomLogger(logging.Logger):
337
341
  )
338
342
 
339
343
 
340
- logging.setLoggerClass(CustomLogger)
341
-
342
-
343
- reload_rich_logging_setup()
344
-
345
-
346
344
  def get_logger(name: str) -> CustomLogger:
347
345
  """
348
346
  Get a logger that's compatible with system logging but has our additional custom
349
347
  methods.
350
348
  """
349
+ init_rich_logging()
351
350
  logger = logging.getLogger(name)
352
351
  # print("Logger is", logger)
353
352
  return cast(CustomLogger, logger)
@@ -355,3 +354,12 @@ def get_logger(name: str) -> CustomLogger:
355
354
 
356
355
  def get_log_file_stream():
357
356
  return _file_handler.stream
357
+
358
+
359
+ @cache
360
+ def init_rich_logging():
361
+ rich.reconfigure(theme=get_theme(), highlighter=get_highlighter())
362
+
363
+ logging.setLoggerClass(CustomLogger)
364
+
365
+ reload_rich_logging_setup()
@@ -1,13 +1,21 @@
1
1
  import logging
2
- from logging import FileHandler, Formatter
2
+ import sys
3
+ from logging import FileHandler, Formatter, LogRecord
3
4
  from pathlib import Path
4
5
 
5
6
  from kash.config.settings import LogLevel
7
+ from kash.config.suppress_warnings import demote_warnings
6
8
 
7
9
  # Basic logging setup for non-interactive logging, like on a server.
8
10
  # For richer logging, see logger.py.
9
11
 
10
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
+
11
19
  def basic_file_handler(path: Path, level: LogLevel) -> logging.FileHandler:
12
20
  handler = logging.FileHandler(path)
13
21
  handler.setLevel(level.value)
@@ -16,7 +24,7 @@ def basic_file_handler(path: Path, level: LogLevel) -> logging.FileHandler:
16
24
 
17
25
 
18
26
  def basic_stderr_handler(level: LogLevel) -> logging.StreamHandler:
19
- handler = logging.StreamHandler()
27
+ handler = SuppressedWarningsStreamHandler(stream=sys.stderr)
20
28
  handler.setLevel(level.value)
21
29
  handler.setFormatter(Formatter("%(asctime)s %(levelname).1s %(name)s - %(message)s"))
22
30
  return handler