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.
- kash/actions/core/markdownify.py +5 -4
- kash/actions/core/readability.py +4 -4
- kash/actions/core/render_as_html.py +8 -6
- kash/actions/core/show_webpage.py +2 -2
- kash/actions/core/strip_html.py +2 -2
- kash/commands/base/basic_file_commands.py +24 -3
- kash/commands/base/diff_commands.py +38 -3
- kash/commands/base/files_command.py +5 -4
- kash/commands/base/reformat_command.py +1 -1
- kash/commands/base/show_command.py +1 -1
- kash/commands/extras/parse_uv_lock.py +12 -3
- kash/commands/workspace/selection_commands.py +1 -1
- kash/commands/workspace/workspace_commands.py +62 -16
- 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/docs/load_source_code.py +1 -1
- 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 +46 -27
- kash/exec/fetch_url_metadata.py +8 -5
- kash/exec/importing.py +4 -4
- kash/exec/llm_transforms.py +2 -2
- kash/exec/preconditions.py +11 -19
- kash/exec/runtime_settings.py +134 -0
- kash/exec/shell_callable_action.py +5 -3
- kash/file_storage/file_store.py +91 -53
- kash/file_storage/item_file_format.py +6 -3
- kash/file_storage/store_filenames.py +7 -3
- kash/help/help_embeddings.py +2 -2
- kash/llm_utils/clean_headings.py +1 -1
- kash/{text_handling → llm_utils}/custom_sliding_transforms.py +0 -3
- kash/llm_utils/init_litellm.py +16 -0
- kash/llm_utils/llm_api_keys.py +6 -2
- kash/llm_utils/llm_completion.py +12 -5
- kash/local_server/__init__.py +1 -1
- kash/local_server/local_server_commands.py +2 -1
- kash/mcp/__init__.py +1 -1
- kash/mcp/mcp_cli.py +3 -2
- kash/mcp/mcp_server_commands.py +8 -2
- kash/mcp/mcp_server_routes.py +11 -12
- kash/media_base/media_cache.py +10 -3
- kash/media_base/transcription_deepgram.py +15 -2
- kash/model/__init__.py +1 -1
- kash/model/actions_model.py +9 -54
- kash/model/exec_model.py +79 -0
- kash/model/items_model.py +131 -81
- 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/ui/shell_results.py +2 -1
- kash/shell/utils/exception_printing.py +2 -2
- kash/utils/common/format_utils.py +0 -14
- kash/utils/common/import_utils.py +46 -18
- kash/utils/common/task_stack.py +4 -15
- kash/utils/errors.py +14 -9
- kash/utils/file_utils/file_formats_model.py +61 -26
- kash/utils/file_utils/file_sort_filter.py +10 -3
- kash/utils/file_utils/filename_parsing.py +41 -16
- kash/{text_handling → utils/text_handling}/doc_normalization.py +23 -13
- kash/utils/text_handling/escape_html_tags.py +156 -0
- kash/{text_handling → utils/text_handling}/markdown_utils.py +82 -4
- kash/utils/text_handling/markdownify_utils.py +87 -0
- kash/{text_handling → utils/text_handling}/unified_diffs.py +1 -44
- kash/web_content/file_cache_utils.py +42 -34
- kash/web_content/local_file_cache.py +29 -12
- kash/web_content/web_extract.py +1 -1
- kash/web_content/web_extract_readabilipy.py +4 -2
- kash/web_content/web_fetch.py +42 -7
- kash/web_content/web_page_model.py +2 -1
- kash/web_gen/simple_webpage.py +1 -1
- kash/web_gen/templates/base_styles.css.jinja +139 -16
- kash/web_gen/templates/simple_webpage.html.jinja +1 -1
- kash/workspaces/__init__.py +12 -3
- kash/workspaces/selections.py +2 -2
- kash/workspaces/workspace_dirs.py +58 -0
- kash/workspaces/workspace_importing.py +2 -2
- kash/workspaces/workspace_output.py +2 -2
- kash/workspaces/workspaces.py +26 -90
- kash/xonsh_custom/load_into_xonsh.py +4 -2
- {kash_shell-0.3.11.dist-info → kash_shell-0.3.13.dist-info}/METADATA +4 -4
- {kash_shell-0.3.11.dist-info → kash_shell-0.3.13.dist-info}/RECORD +93 -89
- kash/shell/utils/argparse_utils.py +0 -20
- kash/utils/lang_utils/inflection.py +0 -18
- /kash/{text_handling → utils/text_handling}/markdown_render.py +0 -0
- {kash_shell-0.3.11.dist-info → kash_shell-0.3.13.dist-info}/WHEEL +0 -0
- {kash_shell-0.3.11.dist-info → kash_shell-0.3.13.dist-info}/entry_points.txt +0 -0
- {kash_shell-0.3.11.dist-info → kash_shell-0.3.13.dist-info}/licenses/LICENSE +0 -0
kash/model/operations_model.py
CHANGED
|
@@ -25,27 +25,46 @@ class OperationSummary:
|
|
|
25
25
|
class Input:
|
|
26
26
|
"""
|
|
27
27
|
An input to an operation, which may include a hash fingerprint.
|
|
28
|
+
Typically an input is a StorePath, but it could be something else like an in-memory
|
|
29
|
+
item that hasn't been saved yet.
|
|
28
30
|
"""
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
path: StorePath
|
|
32
|
+
path: StorePath | None
|
|
32
33
|
hash: str | None = None
|
|
34
|
+
source_info: str | None = None
|
|
33
35
|
|
|
34
36
|
@classmethod
|
|
35
37
|
def parse(cls, input_str: str) -> Input:
|
|
36
38
|
"""
|
|
37
|
-
Parse an Input string in the format `
|
|
38
|
-
`@some/path/filename.ext@sha1:hash`, with a store path and a hash.
|
|
39
|
+
Parse an Input string in the format printed by `Input.parseable_str()`.
|
|
39
40
|
"""
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
path, hash = parts
|
|
43
|
-
return cls(path=StorePath(path), hash=hash)
|
|
41
|
+
if input_str.startswith("[") and input_str.endswith("]"):
|
|
42
|
+
return cls(path=None, hash=None, source_info=input_str[1:-1])
|
|
44
43
|
else:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
parts = input_str.rsplit("@", 1)
|
|
45
|
+
if len(parts) == 2:
|
|
46
|
+
path, hash = parts
|
|
47
|
+
return cls(path=StorePath(path), hash=hash)
|
|
48
|
+
else:
|
|
49
|
+
return cls(path=StorePath(input_str), hash=None)
|
|
50
|
+
|
|
51
|
+
def parseable_str(self):
|
|
52
|
+
"""
|
|
53
|
+
A readable and parseable string describing the input, typically a hash and a path but
|
|
54
|
+
could be a path without a hash or another info in brackets. Paths may have an `@` at the
|
|
55
|
+
front.
|
|
56
|
+
|
|
57
|
+
some/path.txt@sha1:1234567890
|
|
58
|
+
@some/path.txt@sha1:1234567890
|
|
59
|
+
some/path.txt
|
|
60
|
+
[unsaved]
|
|
61
|
+
"""
|
|
62
|
+
if self.path and self.hash:
|
|
63
|
+
return f"{fmt_loc(self.path)}@{self.hash}"
|
|
64
|
+
elif self.source_info:
|
|
65
|
+
return f"[{self.source_info}]"
|
|
66
|
+
else:
|
|
67
|
+
return "[input info missing]"
|
|
49
68
|
|
|
50
69
|
# Inputs are equal if the hashes match (even if the paths have changed).
|
|
51
70
|
|
|
@@ -53,6 +72,10 @@ class Input:
|
|
|
53
72
|
return hash(self.hash) if self.hash else object.__hash__(self)
|
|
54
73
|
|
|
55
74
|
def __eq__(self, other: Any) -> bool:
|
|
75
|
+
"""
|
|
76
|
+
Inputs are equal if the hashes match (even if the paths have changed) or if the paths
|
|
77
|
+
are the same. They are *not* equal otherwise, even if the source_info is the same.
|
|
78
|
+
"""
|
|
56
79
|
if not isinstance(other, Input):
|
|
57
80
|
return NotImplemented
|
|
58
81
|
if self.hash and other.hash:
|
|
@@ -62,7 +85,7 @@ class Input:
|
|
|
62
85
|
return False
|
|
63
86
|
|
|
64
87
|
def __str__(self):
|
|
65
|
-
return self.
|
|
88
|
+
return self.parseable_str()
|
|
66
89
|
|
|
67
90
|
|
|
68
91
|
@dataclass(frozen=True)
|
|
@@ -88,7 +111,7 @@ class Operation:
|
|
|
88
111
|
}
|
|
89
112
|
|
|
90
113
|
if self.arguments:
|
|
91
|
-
d["arguments"] = [arg.
|
|
114
|
+
d["arguments"] = [arg.parseable_str() for arg in self.arguments]
|
|
92
115
|
if self.options:
|
|
93
116
|
d["options"] = self.options
|
|
94
117
|
|
|
@@ -101,7 +124,7 @@ class Operation:
|
|
|
101
124
|
return [shell_quote(str(arg.path)) for arg in self.arguments]
|
|
102
125
|
|
|
103
126
|
def hashed_args(self):
|
|
104
|
-
return [arg.
|
|
127
|
+
return [arg.parseable_str() for arg in self.arguments]
|
|
105
128
|
|
|
106
129
|
def quoted_options(self):
|
|
107
130
|
return [f"--{k}={shell_quote(str(v))}" for k, v in self.options.items()]
|
kash/model/paths_model.py
CHANGED
|
@@ -16,7 +16,7 @@ from rich.rule import Rule
|
|
|
16
16
|
from rich.style import Style
|
|
17
17
|
from rich.text import Text
|
|
18
18
|
|
|
19
|
-
from kash.config.logger import get_console
|
|
19
|
+
from kash.config.logger import get_console, is_console_quiet
|
|
20
20
|
from kash.config.text_styles import (
|
|
21
21
|
COLOR_HINT_DIM,
|
|
22
22
|
COLOR_RESPONSE,
|
|
@@ -31,9 +31,6 @@ from kash.shell.output.kmarkdown import KMarkdown
|
|
|
31
31
|
from kash.utils.rich_custom.rich_indent import Indent
|
|
32
32
|
from kash.utils.rich_custom.rich_markdown_fork import Markdown
|
|
33
33
|
|
|
34
|
-
console = get_console()
|
|
35
|
-
|
|
36
|
-
|
|
37
34
|
print_context_var: contextvars.ContextVar[str] = contextvars.ContextVar("print_prefix", default="")
|
|
38
35
|
"""
|
|
39
36
|
Context variable override for print prefix.
|
|
@@ -99,7 +96,12 @@ def rich_print(
|
|
|
99
96
|
Print to the Rich console, either the global console or a thread-local
|
|
100
97
|
override, if one is active. With `raw` true, we bypass rich formatting
|
|
101
98
|
entirely and simply write to the console stream.
|
|
99
|
+
|
|
100
|
+
Output is suppressed by the global `console_quiet` setting.
|
|
102
101
|
"""
|
|
102
|
+
if is_console_quiet():
|
|
103
|
+
return
|
|
104
|
+
|
|
103
105
|
console = get_console()
|
|
104
106
|
if raw:
|
|
105
107
|
# TODO: Indent not supported in raw mode.
|
|
@@ -136,7 +138,7 @@ def cprint(
|
|
|
136
138
|
raw: bool = False,
|
|
137
139
|
):
|
|
138
140
|
"""
|
|
139
|
-
Main way to print to the shell. Wraps `
|
|
141
|
+
Main way to print to the shell. Wraps `rich_print` with our additional
|
|
140
142
|
formatting options for text fill and prefix.
|
|
141
143
|
"""
|
|
142
144
|
empty_indent = extra_indent.strip()
|
|
@@ -323,8 +325,8 @@ class PrintHooks(Enum):
|
|
|
323
325
|
after_command_run = "after_command_run"
|
|
324
326
|
before_status = "before_status"
|
|
325
327
|
after_status = "after_status"
|
|
326
|
-
before_shell_action_run = "
|
|
327
|
-
after_shell_action_run = "
|
|
328
|
+
before_shell_action_run = "before_shell_action_run"
|
|
329
|
+
after_shell_action_run = "after_shell_action_run"
|
|
328
330
|
before_log_action_run = "before_log_action_run"
|
|
329
331
|
before_assistance = "before_assistance"
|
|
330
332
|
after_assistance = "after_assistance"
|
|
@@ -376,7 +378,7 @@ class PrintHooks(Enum):
|
|
|
376
378
|
elif self == PrintHooks.nonfatal_exception:
|
|
377
379
|
self.nl()
|
|
378
380
|
elif self == PrintHooks.before_done_message:
|
|
379
|
-
|
|
381
|
+
pass
|
|
380
382
|
elif self == PrintHooks.before_output:
|
|
381
383
|
self.nl()
|
|
382
384
|
elif self == PrintHooks.after_output:
|
kash/shell/shell_main.py
CHANGED
|
@@ -14,10 +14,10 @@ import argparse
|
|
|
14
14
|
import threading
|
|
15
15
|
|
|
16
16
|
import xonsh.main
|
|
17
|
+
from clideps.utils.readable_argparse import ReadableColorFormatter
|
|
17
18
|
from strif import quote_if_needed
|
|
18
19
|
|
|
19
20
|
from kash.config.setup import kash_setup
|
|
20
|
-
from kash.shell.utils.argparse_utils import WrappedColorFormatter
|
|
21
21
|
from kash.shell.version import get_full_version_name, get_version
|
|
22
22
|
from kash.xonsh_custom.custom_shell import install_to_xonshrc, start_shell
|
|
23
23
|
|
|
@@ -51,7 +51,7 @@ def run_shell(single_command: str | None = None):
|
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
def build_parser() -> argparse.ArgumentParser:
|
|
54
|
-
parser = argparse.ArgumentParser(description=__doc__, formatter_class=
|
|
54
|
+
parser = argparse.ArgumentParser(description=__doc__, formatter_class=ReadableColorFormatter)
|
|
55
55
|
|
|
56
56
|
parser.add_argument("--version", action="version", version=get_full_version_name())
|
|
57
57
|
|
kash/shell/ui/shell_results.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
|
|
3
|
+
from prettyfmt import fmt_count_items
|
|
3
4
|
from rich.box import SQUARE
|
|
4
5
|
from rich.panel import Panel
|
|
5
6
|
from rich.table import Table
|
|
@@ -10,7 +11,7 @@ from kash.config.text_styles import COLOR_SELECTION, STYLE_HINT
|
|
|
10
11
|
from kash.exec.command_exec import run_command_or_action
|
|
11
12
|
from kash.exec_model.shell_model import ShellResult
|
|
12
13
|
from kash.shell.output.shell_output import PrintHooks, console_pager, cprint, print_result
|
|
13
|
-
from kash.utils.common.format_utils import
|
|
14
|
+
from kash.utils.common.format_utils import fmt_loc
|
|
14
15
|
from kash.utils.errors import is_fatal
|
|
15
16
|
from kash.workspaces import SelectionHistory
|
|
16
17
|
|
|
@@ -5,7 +5,7 @@ from typing import TypeVar
|
|
|
5
5
|
from kash.config.logger import get_logger
|
|
6
6
|
from kash.config.text_styles import COLOR_ERROR
|
|
7
7
|
from kash.shell.output.shell_output import PrintHooks
|
|
8
|
-
from kash.utils.errors import
|
|
8
|
+
from kash.utils.errors import get_nonfatal_exceptions
|
|
9
9
|
|
|
10
10
|
log = get_logger(__name__)
|
|
11
11
|
|
|
@@ -41,7 +41,7 @@ def wrap_with_exception_printing(func: Callable[..., R]) -> Callable[[list[str]]
|
|
|
41
41
|
(", ".join(str(arg) for arg in args)),
|
|
42
42
|
)
|
|
43
43
|
return func(*args)
|
|
44
|
-
except
|
|
44
|
+
except get_nonfatal_exceptions() as e:
|
|
45
45
|
PrintHooks.nonfatal_exception()
|
|
46
46
|
log.error(f"[{COLOR_ERROR}]Command error:[/{COLOR_ERROR}] %s", summarize_traceback(e))
|
|
47
47
|
log.info("Command error details: %s", e, exc_info=True)
|
|
@@ -2,10 +2,8 @@ import html
|
|
|
2
2
|
import re
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
|
-
from inflect import engine
|
|
6
5
|
from prettyfmt import fmt_path
|
|
7
6
|
|
|
8
|
-
from kash.utils.common.lazyobject import lazyobject
|
|
9
7
|
from kash.utils.common.url import Locator, is_url
|
|
10
8
|
|
|
11
9
|
|
|
@@ -45,18 +43,6 @@ def fmt_loc(locator: str | Locator, resolve: bool = True) -> str:
|
|
|
45
43
|
return fmt_path(locator, resolve=resolve)
|
|
46
44
|
|
|
47
45
|
|
|
48
|
-
@lazyobject
|
|
49
|
-
def inflect():
|
|
50
|
-
return engine()
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def fmt_count_items(count: int, name: str = "item") -> str:
|
|
54
|
-
"""
|
|
55
|
-
Format a count and a name as a pluralized phrase, e.g. "1 item" or "2 items".
|
|
56
|
-
"""
|
|
57
|
-
return f"{count} {inflect.plural(name, count)}" # pyright: ignore
|
|
58
|
-
|
|
59
|
-
|
|
60
46
|
## Tests
|
|
61
47
|
|
|
62
48
|
|
|
@@ -12,36 +12,64 @@ log = logging.getLogger(__name__)
|
|
|
12
12
|
Tallies: TypeAlias = dict[str, int]
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
def
|
|
15
|
+
def import_recursive(
|
|
16
16
|
parent_package_name: str,
|
|
17
17
|
parent_dir: Path,
|
|
18
|
-
|
|
18
|
+
resource_names: list[str] | None = None,
|
|
19
19
|
tallies: Tallies | None = None,
|
|
20
20
|
):
|
|
21
21
|
"""
|
|
22
|
-
Import
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
Import modules from subdirectories or individual Python modules within a parent package.
|
|
23
|
+
|
|
24
|
+
Each resource in `resource_names` can be:
|
|
25
|
+
- A directory name (all modules within it will be imported)
|
|
26
|
+
- A module name with or without '.py' extension (a single module will be imported)
|
|
27
|
+
- "." to import all modules in the parent_dir
|
|
28
|
+
|
|
29
|
+
If `resource_names` is `None`, imports all modules directly in parent_dir.
|
|
30
|
+
|
|
31
|
+
Simply a convenience wrapper for `importlib.import_module` and
|
|
32
|
+
`pkgutil.iter_modules` to iterate over all modules in the subdirectories.
|
|
33
|
+
|
|
34
|
+
If `tallies` is provided, it will be updated with the number of modules imported
|
|
35
|
+
for each package.
|
|
25
36
|
"""
|
|
26
37
|
if tallies is None:
|
|
27
38
|
tallies = {}
|
|
28
|
-
if not
|
|
29
|
-
|
|
39
|
+
if not resource_names:
|
|
40
|
+
resource_names = ["."]
|
|
30
41
|
|
|
31
|
-
for
|
|
32
|
-
if
|
|
42
|
+
for name in resource_names:
|
|
43
|
+
if name == ".":
|
|
33
44
|
full_path = parent_dir
|
|
34
45
|
package_name = parent_package_name
|
|
35
46
|
else:
|
|
36
|
-
full_path = parent_dir /
|
|
37
|
-
package_name = f"{parent_package_name}.{
|
|
38
|
-
|
|
39
|
-
if
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
full_path = parent_dir / name
|
|
48
|
+
package_name = f"{parent_package_name}.{name}"
|
|
49
|
+
|
|
50
|
+
# Check if it's a directory
|
|
51
|
+
if full_path.is_dir():
|
|
52
|
+
# Import all modules in the directory
|
|
53
|
+
for _, module_name, _ in pkgutil.iter_modules(path=[str(full_path)]):
|
|
54
|
+
importlib.import_module(f"{package_name}.{module_name}")
|
|
55
|
+
tallies[package_name] = tallies.get(package_name, 0) + 1
|
|
56
|
+
else:
|
|
57
|
+
# Not a directory, try as a module file
|
|
58
|
+
module_path = full_path
|
|
59
|
+
module_name = name
|
|
60
|
+
|
|
61
|
+
# Handle with or without .py extension
|
|
62
|
+
if not module_path.is_file() and module_path.suffix != ".py":
|
|
63
|
+
module_path = parent_dir / f"{name}.py"
|
|
64
|
+
module_name = name
|
|
65
|
+
elif module_path.suffix == ".py":
|
|
66
|
+
module_name = module_path.stem
|
|
67
|
+
|
|
68
|
+
if module_path.is_file() and module_name != "__init__":
|
|
69
|
+
importlib.import_module(f"{parent_package_name}.{module_name}")
|
|
70
|
+
tallies[parent_package_name] = tallies.get(parent_package_name, 0) + 1
|
|
71
|
+
else:
|
|
72
|
+
raise FileNotFoundError(f"Path not found or not importable: {full_path}")
|
|
45
73
|
|
|
46
74
|
return tallies
|
|
47
75
|
|
kash/utils/common/task_stack.py
CHANGED
|
@@ -3,7 +3,6 @@ from contextlib import contextmanager
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
|
|
5
5
|
from kash.config.text_styles import (
|
|
6
|
-
EMOJI_ACTION,
|
|
7
6
|
EMOJI_BREADCRUMB_SEP,
|
|
8
7
|
EMOJI_MSG_INDENT,
|
|
9
8
|
TASK_STACK_HEADER,
|
|
@@ -93,9 +92,8 @@ class TaskStack:
|
|
|
93
92
|
if not self.stack:
|
|
94
93
|
return ""
|
|
95
94
|
else:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return prefix + sep.join(state.full_str() for state in self.stack)
|
|
95
|
+
sep = f" {EMOJI_BREADCRUMB_SEP} "
|
|
96
|
+
return f"{EMOJI_BREADCRUMB_SEP} " + sep.join(state.full_str() for state in self.stack)
|
|
99
97
|
|
|
100
98
|
def prefix_str(self) -> str:
|
|
101
99
|
if not self.stack:
|
|
@@ -107,8 +105,7 @@ class TaskStack:
|
|
|
107
105
|
return f"TaskStack({self.full_str()})"
|
|
108
106
|
|
|
109
107
|
def log_stack(self):
|
|
110
|
-
self.
|
|
111
|
-
self._log.message(f"{EMOJI_ACTION} {TASK_STACK_HEADER}\n%s", self.full_str())
|
|
108
|
+
self._log.message(f"{TASK_STACK_HEADER} %s", self.full_str())
|
|
112
109
|
|
|
113
110
|
@contextmanager
|
|
114
111
|
def context(self, name: str, total_parts: int = 1, unit: str = ""):
|
|
@@ -123,9 +120,7 @@ class TaskStack:
|
|
|
123
120
|
except Exception as e:
|
|
124
121
|
# Log immediately where the exception occurred, but don't double-log.
|
|
125
122
|
if e not in self.exceptions_logged:
|
|
126
|
-
self._log.
|
|
127
|
-
"Exception in task context: %s: %s", type(e).__name__, e, exc_info=True
|
|
128
|
-
)
|
|
123
|
+
self._log.info("Exception in task context: %s: %s", type(e).__name__, e)
|
|
129
124
|
self.exceptions_logged.add(e)
|
|
130
125
|
self.next(last_had_error=True)
|
|
131
126
|
raise
|
|
@@ -139,12 +134,6 @@ class TaskStack:
|
|
|
139
134
|
|
|
140
135
|
return get_logger(__name__)
|
|
141
136
|
|
|
142
|
-
@property
|
|
143
|
-
def _print(self):
|
|
144
|
-
from kash.shell.output.shell_output import cprint
|
|
145
|
-
|
|
146
|
-
return cprint
|
|
147
|
-
|
|
148
137
|
|
|
149
138
|
task_stack_var: contextvars.ContextVar[TaskStack | None] = contextvars.ContextVar(
|
|
150
139
|
"task_stack", default=None
|
kash/utils/errors.py
CHANGED
|
@@ -3,6 +3,8 @@ Common hierarchy of error types. These inherit from standard errors like
|
|
|
3
3
|
ValueError and FileExistsError but are more fine-grained.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
from functools import cache
|
|
7
|
+
|
|
6
8
|
|
|
7
9
|
class KashRuntimeError(ValueError):
|
|
8
10
|
"""Base class for kash runtime errors."""
|
|
@@ -145,8 +147,14 @@ class ApiError(KashRuntimeError):
|
|
|
145
147
|
pass
|
|
146
148
|
|
|
147
149
|
|
|
148
|
-
|
|
150
|
+
@cache
|
|
151
|
+
def get_nonfatal_exceptions() -> tuple[type[Exception], ...]:
|
|
152
|
+
"""
|
|
153
|
+
Exceptions that are not fatal and usually don't merit a full stack trace.
|
|
154
|
+
"""
|
|
149
155
|
exceptions: list[type[Exception]] = [SelfExplanatoryError, FileNotFoundError, IOError]
|
|
156
|
+
|
|
157
|
+
# Slow imports, do lazily.
|
|
150
158
|
try:
|
|
151
159
|
from xonsh.tools import XonshError
|
|
152
160
|
|
|
@@ -155,14 +163,15 @@ def _nonfatal_exceptions() -> tuple[type[Exception], ...]:
|
|
|
155
163
|
pass
|
|
156
164
|
|
|
157
165
|
try:
|
|
158
|
-
import
|
|
166
|
+
import openai
|
|
159
167
|
|
|
160
|
-
exceptions.
|
|
168
|
+
# LiteLLM exceptions subclass openai.APIError
|
|
169
|
+
exceptions.append(openai.APIError)
|
|
161
170
|
except ImportError:
|
|
162
171
|
pass
|
|
163
172
|
|
|
164
173
|
try:
|
|
165
|
-
import yt_dlp
|
|
174
|
+
import yt_dlp.utils
|
|
166
175
|
|
|
167
176
|
exceptions.append(yt_dlp.utils.DownloadError)
|
|
168
177
|
except ImportError:
|
|
@@ -171,12 +180,8 @@ def _nonfatal_exceptions() -> tuple[type[Exception], ...]:
|
|
|
171
180
|
return tuple(exceptions)
|
|
172
181
|
|
|
173
182
|
|
|
174
|
-
NONFATAL_EXCEPTIONS = _nonfatal_exceptions()
|
|
175
|
-
"""Exceptions that are not fatal and usually don't merit a full stack trace."""
|
|
176
|
-
|
|
177
|
-
|
|
178
183
|
def is_fatal(exception: Exception) -> bool:
|
|
179
|
-
for e in
|
|
184
|
+
for e in get_nonfatal_exceptions():
|
|
180
185
|
if isinstance(exception, e):
|
|
181
186
|
return False
|
|
182
187
|
return True
|
|
@@ -4,7 +4,7 @@ from dataclasses import dataclass
|
|
|
4
4
|
from enum import Enum
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
-
from kash.utils.common.url import Url, is_file_url, parse_file_url
|
|
7
|
+
from kash.utils.common.url import Url, is_file_url, is_url, parse_file_url
|
|
8
8
|
from kash.utils.file_utils.file_ext import FileExt
|
|
9
9
|
from kash.utils.file_utils.file_formats import (
|
|
10
10
|
MIME_EMPTY,
|
|
@@ -103,8 +103,18 @@ class Format(Enum):
|
|
|
103
103
|
self.log,
|
|
104
104
|
]
|
|
105
105
|
|
|
106
|
+
@property
|
|
107
|
+
def is_simple_text(self) -> bool:
|
|
108
|
+
"""
|
|
109
|
+
Is this plaintext or close to it, like Markdown?
|
|
110
|
+
"""
|
|
111
|
+
return self in [self.plaintext, self.markdown, self.md_html]
|
|
112
|
+
|
|
106
113
|
@property
|
|
107
114
|
def is_doc(self) -> bool:
|
|
115
|
+
"""
|
|
116
|
+
Is this a textual document of some kind?
|
|
117
|
+
"""
|
|
108
118
|
return self in [
|
|
109
119
|
self.markdown,
|
|
110
120
|
self.md_html,
|
|
@@ -112,6 +122,7 @@ class Format(Enum):
|
|
|
112
122
|
self.pdf,
|
|
113
123
|
self.docx,
|
|
114
124
|
self.pptx,
|
|
125
|
+
self.epub,
|
|
115
126
|
]
|
|
116
127
|
|
|
117
128
|
@property
|
|
@@ -130,6 +141,14 @@ class Format(Enum):
|
|
|
130
141
|
def is_code(self) -> bool:
|
|
131
142
|
return self in [self.python, self.shellscript, self.xonsh, self.json, self.yaml]
|
|
132
143
|
|
|
144
|
+
@property
|
|
145
|
+
def is_markdown(self) -> bool:
|
|
146
|
+
return self in [self.markdown, self.md_html]
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def is_html(self) -> bool:
|
|
150
|
+
return self in [self.html, self.md_html]
|
|
151
|
+
|
|
133
152
|
@property
|
|
134
153
|
def is_data(self) -> bool:
|
|
135
154
|
return self in [self.csv, self.xlsx, self.npz]
|
|
@@ -325,8 +344,8 @@ Format._init_mime_type_map()
|
|
|
325
344
|
|
|
326
345
|
@dataclass(frozen=True)
|
|
327
346
|
class FileFormatInfo:
|
|
328
|
-
|
|
329
|
-
"""File extension, if recognized."""
|
|
347
|
+
current_file_ext: FileExt | None
|
|
348
|
+
"""File extension, if recognized and in the current filename."""
|
|
330
349
|
|
|
331
350
|
format: Format | None
|
|
332
351
|
"""Format, if recognized."""
|
|
@@ -334,11 +353,18 @@ class FileFormatInfo:
|
|
|
334
353
|
mime_type: MimeType | None
|
|
335
354
|
"""Raw mime type, which may include more formats than the ones above."""
|
|
336
355
|
|
|
356
|
+
@property
|
|
357
|
+
def suggested_file_ext(self) -> FileExt | None:
|
|
358
|
+
"""
|
|
359
|
+
Suggested file extension based on detected format.
|
|
360
|
+
"""
|
|
361
|
+
return self.format.file_ext if self.format else self.current_file_ext
|
|
362
|
+
|
|
337
363
|
@property
|
|
338
364
|
def is_text(self) -> bool:
|
|
339
365
|
return bool(
|
|
340
|
-
self.
|
|
341
|
-
and self.
|
|
366
|
+
self.current_file_ext
|
|
367
|
+
and self.current_file_ext.is_text
|
|
342
368
|
or self.format
|
|
343
369
|
and self.format.is_text
|
|
344
370
|
or self.mime_type
|
|
@@ -358,8 +384,8 @@ class FileFormatInfo:
|
|
|
358
384
|
@property
|
|
359
385
|
def is_image(self) -> bool:
|
|
360
386
|
return bool(
|
|
361
|
-
self.
|
|
362
|
-
and self.
|
|
387
|
+
self.current_file_ext
|
|
388
|
+
and self.current_file_ext.is_image
|
|
363
389
|
or self.format
|
|
364
390
|
and self.format.is_image
|
|
365
391
|
or self.mime_type
|
|
@@ -432,24 +458,33 @@ def detect_media_type(filename: str | Path) -> MediaType:
|
|
|
432
458
|
return media_type
|
|
433
459
|
|
|
434
460
|
|
|
435
|
-
def choose_file_ext(
|
|
461
|
+
def choose_file_ext(
|
|
462
|
+
url_or_path: Url | Path | str, mime_type: MimeType | None = None
|
|
463
|
+
) -> FileExt | None:
|
|
436
464
|
"""
|
|
437
|
-
Pick a
|
|
438
|
-
|
|
465
|
+
Pick a file extension to reflect the type of the content. First tries from any
|
|
466
|
+
provided content type (e.g. if this item was just downloaded). Then
|
|
467
|
+
recognizes known file extensions on the filename or URL, then tries looking
|
|
468
|
+
at the content with libmagic and heuristics, then gives up.
|
|
439
469
|
"""
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
fmt
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
470
|
+
if mime_type:
|
|
471
|
+
fmt = Format.from_mime_type(mime_type)
|
|
472
|
+
if fmt:
|
|
473
|
+
return fmt.file_ext
|
|
474
|
+
|
|
475
|
+
# First check if it's a known standard extension.
|
|
476
|
+
filename_ext = parse_file_ext(url_or_path)
|
|
477
|
+
if filename_ext:
|
|
478
|
+
return filename_ext
|
|
479
|
+
|
|
480
|
+
local_path = None
|
|
481
|
+
if isinstance(url_or_path, str) and is_file_url(url_or_path):
|
|
482
|
+
local_path = parse_file_url(url_or_path)
|
|
483
|
+
elif not is_url(url_or_path):
|
|
484
|
+
local_path = Path(url_or_path)
|
|
485
|
+
|
|
486
|
+
# If it's local based the extension on the file content.
|
|
487
|
+
if local_path:
|
|
488
|
+
return file_format_info(local_path).suggested_file_ext
|
|
489
|
+
|
|
490
|
+
return None
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from datetime import UTC, datetime
|
|
2
4
|
from enum import Enum
|
|
3
5
|
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
4
7
|
|
|
5
8
|
import humanfriendly
|
|
6
|
-
import pandas as pd
|
|
7
9
|
from funlog import log_calls
|
|
8
10
|
from prettyfmt import fmt_path
|
|
9
11
|
from pydantic.dataclasses import dataclass
|
|
@@ -12,6 +14,9 @@ from kash.config.logger import get_logger
|
|
|
12
14
|
from kash.utils.errors import FileNotFound, InvalidInput
|
|
13
15
|
from kash.utils.file_utils.file_walk import IgnoreFilter, walk_by_dir
|
|
14
16
|
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from pandas import DataFrame
|
|
19
|
+
|
|
15
20
|
log = get_logger(__name__)
|
|
16
21
|
|
|
17
22
|
|
|
@@ -122,8 +127,10 @@ class FileListing:
|
|
|
122
127
|
size_matching: int
|
|
123
128
|
since_timestamp: float
|
|
124
129
|
|
|
125
|
-
def as_dataframe(self) ->
|
|
126
|
-
|
|
130
|
+
def as_dataframe(self) -> DataFrame:
|
|
131
|
+
from pandas import DataFrame
|
|
132
|
+
|
|
133
|
+
df = DataFrame([file.__dict__ for file in self.files])
|
|
127
134
|
return df
|
|
128
135
|
|
|
129
136
|
@property
|