kash-shell 0.3.11__py3-none-any.whl → 0.3.12__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.
- kash/actions/core/render_as_html.py +2 -2
- kash/actions/core/show_webpage.py +2 -2
- kash/actions/core/strip_html.py +2 -2
- kash/commands/base/basic_file_commands.py +21 -3
- kash/commands/base/files_command.py +5 -4
- kash/commands/extras/parse_uv_lock.py +12 -3
- kash/commands/workspace/selection_commands.py +1 -1
- kash/commands/workspace/workspace_commands.py +1 -1
- kash/config/env_settings.py +2 -42
- kash/config/logger.py +30 -25
- kash/config/logger_basic.py +6 -6
- kash/config/settings.py +23 -7
- kash/config/setup.py +33 -5
- kash/config/text_styles.py +25 -22
- kash/embeddings/cosine.py +12 -4
- kash/embeddings/embeddings.py +16 -6
- kash/embeddings/text_similarity.py +10 -4
- kash/exec/__init__.py +3 -0
- kash/exec/action_decorators.py +4 -19
- kash/exec/action_exec.py +43 -23
- kash/exec/llm_transforms.py +2 -2
- kash/exec/preconditions.py +4 -12
- kash/exec/runtime_settings.py +134 -0
- kash/exec/shell_callable_action.py +5 -3
- kash/file_storage/file_store.py +18 -21
- kash/file_storage/item_file_format.py +6 -3
- kash/file_storage/store_filenames.py +6 -3
- kash/llm_utils/init_litellm.py +16 -0
- kash/llm_utils/llm_api_keys.py +6 -2
- kash/llm_utils/llm_completion.py +11 -4
- kash/mcp/mcp_cli.py +3 -2
- kash/mcp/mcp_server_routes.py +11 -12
- kash/media_base/transcription_deepgram.py +15 -2
- kash/model/__init__.py +1 -1
- kash/model/actions_model.py +6 -54
- kash/model/exec_model.py +79 -0
- kash/model/items_model.py +71 -50
- kash/model/operations_model.py +38 -15
- kash/model/paths_model.py +2 -0
- kash/shell/output/shell_output.py +10 -8
- kash/shell/shell_main.py +2 -2
- kash/shell/utils/exception_printing.py +2 -2
- kash/text_handling/doc_normalization.py +16 -8
- kash/text_handling/markdown_utils.py +83 -2
- kash/utils/common/format_utils.py +2 -8
- kash/utils/common/inflection.py +22 -0
- kash/utils/common/task_stack.py +4 -15
- kash/utils/errors.py +14 -9
- kash/utils/file_utils/file_formats_model.py +15 -0
- kash/utils/file_utils/file_sort_filter.py +10 -3
- kash/web_gen/templates/base_styles.css.jinja +8 -3
- kash/workspaces/__init__.py +12 -3
- kash/workspaces/workspace_dirs.py +58 -0
- kash/workspaces/workspace_importing.py +1 -1
- kash/workspaces/workspaces.py +26 -90
- {kash_shell-0.3.11.dist-info → kash_shell-0.3.12.dist-info}/METADATA +4 -4
- {kash_shell-0.3.11.dist-info → kash_shell-0.3.12.dist-info}/RECORD +60 -57
- kash/shell/utils/argparse_utils.py +0 -20
- kash/utils/lang_utils/inflection.py +0 -18
- {kash_shell-0.3.11.dist-info → kash_shell-0.3.12.dist-info}/WHEEL +0 -0
- {kash_shell-0.3.11.dist-info → kash_shell-0.3.12.dist-info}/entry_points.txt +0 -0
- {kash_shell-0.3.11.dist-info → kash_shell-0.3.12.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from kash.actions.core.tabbed_webpage_config import tabbed_webpage_config
|
|
2
2
|
from kash.actions.core.tabbed_webpage_generate import tabbed_webpage_generate
|
|
3
3
|
from kash.exec import kash_action
|
|
4
|
-
from kash.exec.preconditions import has_full_html_page_body,
|
|
4
|
+
from kash.exec.preconditions import has_full_html_page_body, has_html_body, has_simple_text_body
|
|
5
5
|
from kash.exec_model.args_model import ONE_OR_MORE_ARGS
|
|
6
6
|
from kash.model import ActionInput, ActionResult, Param
|
|
7
7
|
from kash.model.items_model import ItemType
|
|
@@ -11,7 +11,7 @@ from kash.web_gen.simple_webpage import simple_webpage_render
|
|
|
11
11
|
|
|
12
12
|
@kash_action(
|
|
13
13
|
expected_args=ONE_OR_MORE_ARGS,
|
|
14
|
-
precondition=(
|
|
14
|
+
precondition=(has_html_body | has_simple_text_body) & ~has_full_html_page_body,
|
|
15
15
|
params=(Param("add_title", "Add a title to the page body.", type=bool),),
|
|
16
16
|
)
|
|
17
17
|
def render_as_html(input: ActionInput, add_title: bool = False) -> ActionResult:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from kash.actions.core.render_as_html import render_as_html
|
|
2
2
|
from kash.commands.base.show_command import show
|
|
3
3
|
from kash.exec import kash_action
|
|
4
|
-
from kash.exec.preconditions import has_full_html_page_body,
|
|
4
|
+
from kash.exec.preconditions import has_full_html_page_body, has_html_body, has_simple_text_body
|
|
5
5
|
from kash.exec_model.args_model import ONE_OR_MORE_ARGS
|
|
6
6
|
from kash.exec_model.commands_model import Command
|
|
7
7
|
from kash.exec_model.shell_model import ShellResult
|
|
@@ -10,7 +10,7 @@ from kash.model import ActionInput, ActionResult
|
|
|
10
10
|
|
|
11
11
|
@kash_action(
|
|
12
12
|
expected_args=ONE_OR_MORE_ARGS,
|
|
13
|
-
precondition=(
|
|
13
|
+
precondition=(has_html_body | has_simple_text_body) & ~has_full_html_page_body,
|
|
14
14
|
)
|
|
15
15
|
def show_webpage(input: ActionInput) -> ActionResult:
|
|
16
16
|
"""
|
kash/actions/core/strip_html.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from kash.config.logger import get_logger
|
|
2
2
|
from kash.exec import kash_action
|
|
3
|
-
from kash.exec.preconditions import has_html_body,
|
|
3
|
+
from kash.exec.preconditions import has_html_body, has_simple_text_body
|
|
4
4
|
from kash.model import Format, Item
|
|
5
5
|
from kash.utils.common.format_utils import html_to_plaintext
|
|
6
6
|
from kash.utils.errors import InvalidInput
|
|
@@ -8,7 +8,7 @@ from kash.utils.errors import InvalidInput
|
|
|
8
8
|
log = get_logger(__name__)
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
@kash_action(precondition=has_html_body |
|
|
11
|
+
@kash_action(precondition=has_html_body | has_simple_text_body)
|
|
12
12
|
def strip_html(item: Item) -> Item:
|
|
13
13
|
"""
|
|
14
14
|
Strip HTML tags from HTML or Markdown. This is a simple filter, simply searching
|
|
@@ -2,7 +2,7 @@ import os
|
|
|
2
2
|
|
|
3
3
|
from frontmatter_format import fmf_read_raw, fmf_strip_frontmatter
|
|
4
4
|
from prettyfmt import fmt_lines
|
|
5
|
-
from strif import copyfile_atomic
|
|
5
|
+
from strif import atomic_output_file, copyfile_atomic
|
|
6
6
|
|
|
7
7
|
from kash.config.logger import get_logger
|
|
8
8
|
from kash.config.text_styles import STYLE_EMPH
|
|
@@ -28,10 +28,10 @@ log = get_logger(__name__)
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
@kash_command
|
|
31
|
-
def
|
|
31
|
+
def clipboard_copy(path: str | None = None, raw: bool = False) -> None:
|
|
32
32
|
"""
|
|
33
33
|
Copy the contents of a file (or the first file in the selection) to the OS-native
|
|
34
|
-
clipboard.
|
|
34
|
+
clipboard. Similar to `pbcopy` on macOS.
|
|
35
35
|
|
|
36
36
|
:param raw: Copy the full exact contents of the file. Otherwise frontmatter is omitted.
|
|
37
37
|
"""
|
|
@@ -39,6 +39,8 @@ def cbcopy(path: str | None = None, raw: bool = False) -> None:
|
|
|
39
39
|
import pyperclip
|
|
40
40
|
|
|
41
41
|
input_paths = assemble_path_args(path)
|
|
42
|
+
if not input_paths:
|
|
43
|
+
raise InvalidInput("No path provided")
|
|
42
44
|
input_path = input_paths[0]
|
|
43
45
|
|
|
44
46
|
format = detect_file_format(input_path)
|
|
@@ -69,6 +71,22 @@ def cbcopy(path: str | None = None, raw: bool = False) -> None:
|
|
|
69
71
|
)
|
|
70
72
|
|
|
71
73
|
|
|
74
|
+
@kash_command
|
|
75
|
+
def clipboard_paste(path: str = "untitled_paste.txt") -> None:
|
|
76
|
+
"""
|
|
77
|
+
Paste the contents of the OS-native clipboard into a new file.
|
|
78
|
+
"""
|
|
79
|
+
# TODO: Get this to work for images!
|
|
80
|
+
# And can we convert rich text to Markdown?
|
|
81
|
+
import pyperclip
|
|
82
|
+
|
|
83
|
+
contents = pyperclip.paste()
|
|
84
|
+
with atomic_output_file(path, backup_suffix=".{timestamp}.bak") as f:
|
|
85
|
+
f.write_text(contents)
|
|
86
|
+
|
|
87
|
+
print_status("Pasted clipboard contents to:\n%s", fmt_lines([fmt_loc(path)]))
|
|
88
|
+
|
|
89
|
+
|
|
72
90
|
@kash_command
|
|
73
91
|
def edit(path: str | None = None, all: bool = False) -> None:
|
|
74
92
|
"""
|
|
@@ -75,7 +75,8 @@ def _print_listing_tallies(
|
|
|
75
75
|
cprint("(use --no_max to remove cutoff)", style=STYLE_HINT)
|
|
76
76
|
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
DEFAULT_MAX_PER_GROUP = 50
|
|
79
|
+
"""Default maximum number of files to display per group."""
|
|
79
80
|
|
|
80
81
|
|
|
81
82
|
@kash_command
|
|
@@ -301,13 +302,13 @@ def files(
|
|
|
301
302
|
# there are lots of groups and lots of files per group.
|
|
302
303
|
# Default is max 100 per group but if we have 4 * 100 items, cut to 25.
|
|
303
304
|
# If we have 2 * 100 items, cut to 50.
|
|
304
|
-
final_max_pg =
|
|
305
|
+
final_max_pg = DEFAULT_MAX_PER_GROUP if cap_per_group else max_per_group
|
|
305
306
|
max_pg_explicit = max_per_group > 0
|
|
306
307
|
if not max_pg_explicit:
|
|
307
308
|
group_lens = [len(group_df) for group_df in grouped]
|
|
308
309
|
for ratio in [2, 4]:
|
|
309
|
-
if sum(group_lens) > ratio *
|
|
310
|
-
final_max_pg = int(
|
|
310
|
+
if sum(group_lens) > ratio * DEFAULT_MAX_PER_GROUP:
|
|
311
|
+
final_max_pg = int(DEFAULT_MAX_PER_GROUP / ratio)
|
|
311
312
|
|
|
312
313
|
total_displayed = 0
|
|
313
314
|
total_displayed_size = 0
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import subprocess
|
|
2
4
|
import tomllib
|
|
3
5
|
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
4
7
|
|
|
5
|
-
import pandas as pd
|
|
6
8
|
from packaging.tags import Tag, sys_tags
|
|
7
9
|
from packaging.utils import parse_wheel_filename
|
|
8
10
|
from prettyfmt import fmt_size_dual
|
|
@@ -13,6 +15,9 @@ from kash.config.text_styles import COLOR_STATUS
|
|
|
13
15
|
from kash.exec import kash_command
|
|
14
16
|
from kash.shell.output.shell_output import cprint
|
|
15
17
|
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from pandas import DataFrame
|
|
20
|
+
|
|
16
21
|
log = get_logger(__name__)
|
|
17
22
|
|
|
18
23
|
|
|
@@ -49,13 +54,15 @@ def get_platform() -> str:
|
|
|
49
54
|
return next(sys_tags()).platform
|
|
50
55
|
|
|
51
56
|
|
|
52
|
-
def parse_uv_lock(lock_path: Path) ->
|
|
57
|
+
def parse_uv_lock(lock_path: Path) -> DataFrame:
|
|
53
58
|
"""
|
|
54
59
|
Return one row per package from a uv.lock file, selecting the best
|
|
55
60
|
matching wheel for the current interpreter or falling back to the sdist.
|
|
56
61
|
|
|
57
62
|
Columns: name, version, registry, file_type, url, hash, size, filename.
|
|
58
63
|
"""
|
|
64
|
+
from pandas import DataFrame
|
|
65
|
+
|
|
59
66
|
with open(lock_path, "rb") as f:
|
|
60
67
|
data = tomllib.load(f)
|
|
61
68
|
|
|
@@ -88,7 +95,7 @@ def parse_uv_lock(lock_path: Path) -> pd.DataFrame:
|
|
|
88
95
|
}
|
|
89
96
|
)
|
|
90
97
|
|
|
91
|
-
return
|
|
98
|
+
return DataFrame(rows)
|
|
92
99
|
|
|
93
100
|
|
|
94
101
|
def uv_runtime_packages(
|
|
@@ -141,6 +148,8 @@ def uv_dep_info(
|
|
|
141
148
|
By default, filters to show only 'main' dependencies from pyproject.toml.
|
|
142
149
|
Helpful for looking at sizes of dependencies.
|
|
143
150
|
"""
|
|
151
|
+
import pandas as pd
|
|
152
|
+
|
|
144
153
|
uv_lock_path = Path(uv_lock)
|
|
145
154
|
pyproject_path = Path(pyproject)
|
|
146
155
|
|
|
@@ -10,8 +10,8 @@ from kash.exec_model.shell_model import ShellResult
|
|
|
10
10
|
from kash.model.paths_model import StorePath
|
|
11
11
|
from kash.shell.ui.shell_results import shell_print_selection_history
|
|
12
12
|
from kash.utils.common.format_utils import fmt_loc
|
|
13
|
+
from kash.utils.common.inflection import plural
|
|
13
14
|
from kash.utils.errors import InvalidInput
|
|
14
|
-
from kash.utils.lang_utils.inflection import plural
|
|
15
15
|
from kash.workspaces import Selection, current_ws
|
|
16
16
|
|
|
17
17
|
log = get_logger(__name__)
|
|
@@ -51,6 +51,7 @@ from kash.shell.output.shell_output import (
|
|
|
51
51
|
)
|
|
52
52
|
from kash.shell.utils.native_utils import tail_file
|
|
53
53
|
from kash.utils.common.format_utils import fmt_loc
|
|
54
|
+
from kash.utils.common.inflection import plural
|
|
54
55
|
from kash.utils.common.obj_replace import remove_values
|
|
55
56
|
from kash.utils.common.parse_key_vals import parse_key_value
|
|
56
57
|
from kash.utils.common.type_utils import not_none
|
|
@@ -58,7 +59,6 @@ from kash.utils.common.url import Url, is_url
|
|
|
58
59
|
from kash.utils.errors import InvalidInput
|
|
59
60
|
from kash.utils.file_formats.chat_format import tail_chat_history
|
|
60
61
|
from kash.utils.file_utils.dir_info import is_nonempty_dir
|
|
61
|
-
from kash.utils.lang_utils.inflection import plural
|
|
62
62
|
from kash.web_content.file_cache_utils import cache_file
|
|
63
63
|
from kash.workspaces import (
|
|
64
64
|
current_ws,
|
kash/config/env_settings.py
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
from enum import Enum
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from typing import overload
|
|
1
|
+
from clideps.env_vars.env_enum import EnvEnum
|
|
5
2
|
|
|
6
3
|
|
|
7
|
-
class KashEnv(
|
|
4
|
+
class KashEnv(EnvEnum):
|
|
8
5
|
"""
|
|
9
6
|
Environment variable settings for kash. None are required, but these may be
|
|
10
7
|
used to override default values.
|
|
@@ -33,40 +30,3 @@ class KashEnv(str, Enum):
|
|
|
33
30
|
|
|
34
31
|
KASH_USER_AGENT = "KASH_USER_AGENT"
|
|
35
32
|
"""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/logger.py
CHANGED
|
@@ -55,6 +55,10 @@ class LogSettings:
|
|
|
55
55
|
log_objects_dir: Path
|
|
56
56
|
log_file_path: Path
|
|
57
57
|
|
|
58
|
+
@property
|
|
59
|
+
def is_quiet(self) -> bool:
|
|
60
|
+
return self.log_console_level >= LogLevel.error
|
|
61
|
+
|
|
58
62
|
|
|
59
63
|
LOG_NAME_GLOBAL = "workspace"
|
|
60
64
|
|
|
@@ -83,6 +87,13 @@ def get_log_settings() -> LogSettings:
|
|
|
83
87
|
return _log_settings.copy()
|
|
84
88
|
|
|
85
89
|
|
|
90
|
+
def is_console_quiet() -> bool:
|
|
91
|
+
"""
|
|
92
|
+
Whether to suppress non-logging console output.
|
|
93
|
+
"""
|
|
94
|
+
return global_settings().console_quiet
|
|
95
|
+
|
|
96
|
+
|
|
86
97
|
def make_valid_log_name(name: str) -> str:
|
|
87
98
|
name = str(name).strip().rstrip("/").removesuffix(".log")
|
|
88
99
|
name = re.sub(r"[^\w-]", "_", name)
|
|
@@ -244,14 +255,6 @@ def _do_logging_setup(log_settings: LogSettings):
|
|
|
244
255
|
|
|
245
256
|
# Manually adjust logging for a few packages, removing previous verbose default handlers.
|
|
246
257
|
|
|
247
|
-
try:
|
|
248
|
-
import litellm
|
|
249
|
-
from litellm import _logging # noqa: F401
|
|
250
|
-
|
|
251
|
-
litellm.suppress_debug_info = True # Suppress overly prominent exception messages.
|
|
252
|
-
except ImportError:
|
|
253
|
-
pass
|
|
254
|
-
|
|
255
258
|
log_levels = {
|
|
256
259
|
None: INFO,
|
|
257
260
|
"LiteLLM": INFO,
|
|
@@ -276,10 +279,12 @@ def prefix(line: str, emoji: str = "", warn_emoji: str = "") -> str:
|
|
|
276
279
|
return " ".join(filter(None, [prefix, emojis, line]))
|
|
277
280
|
|
|
278
281
|
|
|
279
|
-
def prefix_args(
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
282
|
+
def prefix_args(
|
|
283
|
+
msg: object, *other_args: object, emoji: str = "", warn_emoji: str = ""
|
|
284
|
+
) -> tuple[str, *tuple[object, ...]]:
|
|
285
|
+
"""Prefixes the string representation of msg and returns it with other_args."""
|
|
286
|
+
prefixed_msg = prefix(str(msg), emoji, warn_emoji)
|
|
287
|
+
return (prefixed_msg,) + other_args
|
|
283
288
|
|
|
284
289
|
|
|
285
290
|
class CustomLogger(logging.Logger):
|
|
@@ -290,29 +295,29 @@ class CustomLogger(logging.Logger):
|
|
|
290
295
|
"""
|
|
291
296
|
|
|
292
297
|
@override
|
|
293
|
-
def debug(self, *args, **kwargs):
|
|
294
|
-
super().debug(*prefix_args(args), **kwargs)
|
|
298
|
+
def debug(self, msg: object, *args: object, **kwargs: Any) -> None:
|
|
299
|
+
super().debug(*prefix_args(msg, *args), **kwargs)
|
|
295
300
|
|
|
296
301
|
@override
|
|
297
|
-
def info(self, *args, **kwargs):
|
|
298
|
-
super().info(*prefix_args(args), **kwargs)
|
|
302
|
+
def info(self, msg: object, *args: object, **kwargs: Any) -> None:
|
|
303
|
+
super().info(*prefix_args(msg, *args), **kwargs)
|
|
299
304
|
|
|
300
305
|
@override
|
|
301
|
-
def warning(self, *args, **kwargs):
|
|
302
|
-
super().warning(*prefix_args(args, warn_emoji=EMOJI_WARN), **kwargs)
|
|
306
|
+
def warning(self, msg: object, *args: object, **kwargs: Any) -> None:
|
|
307
|
+
super().warning(*prefix_args(msg, *args, warn_emoji=EMOJI_WARN), **kwargs)
|
|
303
308
|
|
|
304
309
|
@override
|
|
305
|
-
def error(self, *args, **kwargs):
|
|
306
|
-
super().error(*prefix_args(args, warn_emoji=EMOJI_ERROR), **kwargs)
|
|
310
|
+
def error(self, msg: object, *args: object, **kwargs: Any) -> None:
|
|
311
|
+
super().error(*prefix_args(msg, *args, warn_emoji=EMOJI_ERROR), **kwargs)
|
|
307
312
|
|
|
308
|
-
def log_at(self, level: LogLevel, *args, **kwargs):
|
|
313
|
+
def log_at(self, level: LogLevel, *args: object, **kwargs: Any) -> None:
|
|
309
314
|
getattr(self, level.name)(*args, **kwargs)
|
|
310
315
|
|
|
311
|
-
def message(self, *args, **kwargs):
|
|
316
|
+
def message(self, msg: object, *args: object, **kwargs: Any) -> None:
|
|
312
317
|
"""
|
|
313
318
|
An informative message that should appear even if log level is set to warning.
|
|
314
319
|
"""
|
|
315
|
-
super().warning(*prefix_args(args), **kwargs)
|
|
320
|
+
super().warning(*prefix_args(msg, *args), **kwargs)
|
|
316
321
|
|
|
317
322
|
def save_object(
|
|
318
323
|
self,
|
|
@@ -321,7 +326,7 @@ class CustomLogger(logging.Logger):
|
|
|
321
326
|
obj: Any,
|
|
322
327
|
level: LogLevel = LogLevel.info,
|
|
323
328
|
file_ext: str = "txt",
|
|
324
|
-
):
|
|
329
|
+
) -> None:
|
|
325
330
|
"""
|
|
326
331
|
Save an object to a file in the log directory. Useful for details too large to
|
|
327
332
|
log normally but useful for debugging.
|
|
@@ -342,7 +347,7 @@ class CustomLogger(logging.Logger):
|
|
|
342
347
|
|
|
343
348
|
self.log_at(level, "%s %s saved: %s", EMOJI_SAVED, description, path)
|
|
344
349
|
|
|
345
|
-
def dump_stack(self, all_threads: bool = True, level: LogLevel = LogLevel.info):
|
|
350
|
+
def dump_stack(self, all_threads: bool = True, level: LogLevel = LogLevel.info) -> None:
|
|
346
351
|
self.log_at(level, "Stack trace dump:\n%s", current_stack_traces(all_threads))
|
|
347
352
|
|
|
348
353
|
def __repr__(self):
|
kash/config/logger_basic.py
CHANGED
|
@@ -3,7 +3,7 @@ import sys
|
|
|
3
3
|
from logging import FileHandler, Formatter, LogRecord
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
-
from kash.config.settings import LogLevel
|
|
6
|
+
from kash.config.settings import LogLevel, LogLevelStr
|
|
7
7
|
from kash.config.suppress_warnings import demote_warnings
|
|
8
8
|
|
|
9
9
|
# Basic logging setup for non-interactive logging, like on a server.
|
|
@@ -16,21 +16,21 @@ class SuppressedWarningsStreamHandler(logging.StreamHandler):
|
|
|
16
16
|
super().emit(record)
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
def basic_file_handler(path: Path, level: LogLevel) -> logging.FileHandler:
|
|
19
|
+
def basic_file_handler(path: Path, level: LogLevel | LogLevelStr) -> logging.FileHandler:
|
|
20
20
|
handler = logging.FileHandler(path)
|
|
21
|
-
handler.setLevel(level.value)
|
|
21
|
+
handler.setLevel(LogLevel.parse(level).value)
|
|
22
22
|
handler.setFormatter(Formatter("%(asctime)s %(levelname).1s %(name)s - %(message)s"))
|
|
23
23
|
return handler
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
def basic_stderr_handler(level: LogLevel) -> logging.StreamHandler:
|
|
26
|
+
def basic_stderr_handler(level: LogLevel | LogLevelStr) -> logging.StreamHandler:
|
|
27
27
|
handler = SuppressedWarningsStreamHandler(stream=sys.stderr)
|
|
28
|
-
handler.setLevel(level.value)
|
|
28
|
+
handler.setLevel(LogLevel.parse(level).value)
|
|
29
29
|
handler.setFormatter(Formatter("%(asctime)s %(levelname).1s %(name)s - %(message)s"))
|
|
30
30
|
return handler
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
def basic_logging_setup(log_path: Path | None, level: LogLevel):
|
|
33
|
+
def basic_logging_setup(log_path: Path | None, level: LogLevel | LogLevelStr):
|
|
34
34
|
"""
|
|
35
35
|
Set up basic logging to a file and to stderr.
|
|
36
36
|
"""
|
kash/config/settings.py
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import os
|
|
2
|
-
from
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import IntEnum
|
|
3
6
|
from functools import cache
|
|
4
7
|
from logging import DEBUG, ERROR, INFO, WARNING
|
|
5
8
|
from pathlib import Path
|
|
9
|
+
from typing import Literal
|
|
6
10
|
|
|
7
|
-
from pydantic.dataclasses import dataclass
|
|
8
11
|
from strif import AtomicVar
|
|
9
12
|
|
|
10
13
|
from kash.config.env_settings import KashEnv
|
|
@@ -64,7 +67,14 @@ LOCAL_SERVER_PORTS_MAX = 30
|
|
|
64
67
|
LOCAL_SERVER_LOG_NAME = "local_server"
|
|
65
68
|
|
|
66
69
|
|
|
67
|
-
|
|
70
|
+
LogLevelStr = Literal["debug", "info", "message", "warning", "error"]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class LogLevel(IntEnum):
|
|
74
|
+
"""
|
|
75
|
+
Convenience enum for log levels with parsing and ordering.
|
|
76
|
+
"""
|
|
77
|
+
|
|
68
78
|
debug = DEBUG
|
|
69
79
|
info = INFO
|
|
70
80
|
warning = WARNING
|
|
@@ -72,7 +82,9 @@ class LogLevel(Enum):
|
|
|
72
82
|
error = ERROR
|
|
73
83
|
|
|
74
84
|
@classmethod
|
|
75
|
-
def parse(cls, level_str: str):
|
|
85
|
+
def parse(cls, level_str: str | LogLevelStr | LogLevel) -> LogLevel:
|
|
86
|
+
if isinstance(level_str, LogLevel):
|
|
87
|
+
return level_str
|
|
76
88
|
canon_name = level_str.strip().lower()
|
|
77
89
|
if canon_name == "warn":
|
|
78
90
|
canon_name = "warning"
|
|
@@ -87,7 +99,7 @@ class LogLevel(Enum):
|
|
|
87
99
|
return self.name
|
|
88
100
|
|
|
89
101
|
|
|
90
|
-
DEFAULT_LOG_LEVEL = LogLevel.parse(KashEnv.KASH_LOG_LEVEL.read_str("warning"))
|
|
102
|
+
DEFAULT_LOG_LEVEL = LogLevel.parse(KashEnv.KASH_LOG_LEVEL.read_str(default="warning"))
|
|
91
103
|
|
|
92
104
|
|
|
93
105
|
def resolve_and_create_dirs(path: Path | str, is_dir: bool = False) -> Path:
|
|
@@ -174,6 +186,9 @@ class Settings:
|
|
|
174
186
|
console_log_level: LogLevel
|
|
175
187
|
"""The log level for console-based logging."""
|
|
176
188
|
|
|
189
|
+
console_quiet: bool
|
|
190
|
+
"""If true, suppress non-logging console output."""
|
|
191
|
+
|
|
177
192
|
file_log_level: LogLevel
|
|
178
193
|
"""The log level for file-based logging."""
|
|
179
194
|
|
|
@@ -205,7 +220,7 @@ def _get_ws_root_dir() -> Path:
|
|
|
205
220
|
|
|
206
221
|
|
|
207
222
|
def _get_global_ws_dir() -> Path:
|
|
208
|
-
kash_ws_dir = KashEnv.KASH_GLOBAL_WS.read_path()
|
|
223
|
+
kash_ws_dir = KashEnv.KASH_GLOBAL_WS.read_path(default=None)
|
|
209
224
|
if kash_ws_dir:
|
|
210
225
|
return kash_ws_dir
|
|
211
226
|
else:
|
|
@@ -225,7 +240,7 @@ def _get_system_cache_dir() -> Path:
|
|
|
225
240
|
|
|
226
241
|
|
|
227
242
|
def _get_mcp_ws_dir() -> Path | None:
|
|
228
|
-
mcp_dir = KashEnv.KASH_MCP_WS.read_str()
|
|
243
|
+
mcp_dir = KashEnv.KASH_MCP_WS.read_str(default=None)
|
|
229
244
|
if mcp_dir:
|
|
230
245
|
return Path(mcp_dir).expanduser().resolve()
|
|
231
246
|
else:
|
|
@@ -254,6 +269,7 @@ def _read_settings():
|
|
|
254
269
|
default_editor="nano",
|
|
255
270
|
file_log_level=LogLevel.info,
|
|
256
271
|
console_log_level=DEFAULT_LOG_LEVEL,
|
|
272
|
+
console_quiet=False,
|
|
257
273
|
local_server_ports_start=LOCAL_SERVER_PORT_START,
|
|
258
274
|
local_server_ports_max=LOCAL_SERVER_PORTS_MAX,
|
|
259
275
|
local_server_port=0,
|
kash/config/setup.py
CHANGED
|
@@ -7,7 +7,13 @@ from clideps.env_vars.dotenv_utils import load_dotenv_paths
|
|
|
7
7
|
|
|
8
8
|
from kash.config.logger import reset_rich_logging
|
|
9
9
|
from kash.config.logger_basic import basic_logging_setup
|
|
10
|
-
from kash.config.settings import
|
|
10
|
+
from kash.config.settings import (
|
|
11
|
+
LogLevel,
|
|
12
|
+
LogLevelStr,
|
|
13
|
+
atomic_global_settings,
|
|
14
|
+
configure_ws_and_settings,
|
|
15
|
+
global_settings,
|
|
16
|
+
)
|
|
11
17
|
|
|
12
18
|
|
|
13
19
|
@cache
|
|
@@ -16,7 +22,9 @@ def kash_setup(
|
|
|
16
22
|
rich_logging: bool,
|
|
17
23
|
kash_ws_root: Path | None = None,
|
|
18
24
|
log_path: Path | None = None,
|
|
19
|
-
|
|
25
|
+
log_level: LogLevel | LogLevelStr | None = None,
|
|
26
|
+
console_log_level: LogLevel | LogLevelStr | None = None,
|
|
27
|
+
console_quiet: bool | None = None,
|
|
20
28
|
):
|
|
21
29
|
"""
|
|
22
30
|
One-time top-level setup of essential logging, keys, directories, and configs.
|
|
@@ -25,8 +33,14 @@ def kash_setup(
|
|
|
25
33
|
Can call this if embedding kash in another app.
|
|
26
34
|
Can be used to set the global default workspace and logs directory
|
|
27
35
|
and/or the default log file.
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
|
|
37
|
+
Basic logging is to the specified log file.
|
|
38
|
+
If enabled, rich logging is to the console as well.
|
|
39
|
+
|
|
40
|
+
By default console is "warning" level but can be controlled with
|
|
41
|
+
the `console_log_level` parameter.
|
|
42
|
+
All console/shell output can be suppressed with `console_quiet`. By default
|
|
43
|
+
console is quiet if `console_log_level` is "error" or higher.
|
|
30
44
|
"""
|
|
31
45
|
from kash.utils.common.stack_traces import add_stacktrace_handler
|
|
32
46
|
|
|
@@ -40,10 +54,24 @@ def kash_setup(
|
|
|
40
54
|
configure_ws_and_settings(kash_ws_root)
|
|
41
55
|
|
|
42
56
|
# Now set up logging, as it might depend on workspace root.
|
|
57
|
+
log_level = LogLevel.parse(log_level) if log_level else LogLevel.info
|
|
58
|
+
|
|
43
59
|
if rich_logging:
|
|
60
|
+
# These settings are only used for rich logging.
|
|
61
|
+
console_log_level = (
|
|
62
|
+
LogLevel.parse(console_log_level) if console_log_level else LogLevel.warning
|
|
63
|
+
)
|
|
64
|
+
console_quiet = (
|
|
65
|
+
console_quiet if console_quiet is not None else console_log_level >= LogLevel.error
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
with atomic_global_settings().updates() as settings:
|
|
69
|
+
settings.console_log_level = console_log_level
|
|
70
|
+
settings.file_log_level = log_level
|
|
71
|
+
settings.console_quiet = console_quiet
|
|
44
72
|
reset_rich_logging(log_path=log_path)
|
|
45
73
|
else:
|
|
46
|
-
basic_logging_setup(log_path=log_path, level=
|
|
74
|
+
basic_logging_setup(log_path=log_path, level=log_level)
|
|
47
75
|
|
|
48
76
|
_lib_setup()
|
|
49
77
|
|