kash-shell 0.3.10__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/format_markdown_template.py +2 -5
- kash/actions/core/markdownify.py +2 -4
- kash/actions/core/readability.py +2 -4
- kash/actions/core/render_as_html.py +30 -11
- kash/actions/core/show_webpage.py +6 -11
- kash/actions/core/strip_html.py +4 -8
- kash/actions/core/{webpage_config.py → tabbed_webpage_config.py} +5 -3
- kash/actions/core/{webpage_generate.py → tabbed_webpage_generate.py} +5 -4
- kash/commands/base/basic_file_commands.py +21 -3
- kash/commands/base/files_command.py +29 -10
- kash/commands/extras/parse_uv_lock.py +12 -3
- kash/commands/workspace/selection_commands.py +1 -1
- kash/commands/workspace/workspace_commands.py +2 -3
- kash/config/colors.py +2 -2
- 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 +10 -25
- kash/exec/action_exec.py +43 -23
- kash/exec/llm_transforms.py +6 -3
- kash/exec/preconditions.py +10 -12
- kash/exec/resolve_args.py +4 -0
- kash/exec/runtime_settings.py +134 -0
- kash/exec/shell_callable_action.py +5 -3
- kash/file_storage/file_store.py +37 -38
- kash/file_storage/item_file_format.py +6 -3
- kash/file_storage/store_filenames.py +6 -3
- kash/help/function_param_info.py +1 -1
- 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/local_server/local_server_routes.py +1 -7
- 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 +102 -35
- 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/shell/utils/shell_function_wrapper.py +15 -15
- kash/text_handling/doc_normalization.py +16 -8
- kash/text_handling/markdown_render.py +1 -0
- kash/text_handling/markdown_utils.py +105 -2
- kash/utils/common/format_utils.py +2 -8
- kash/utils/common/function_inspect.py +360 -110
- 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_ext.py +4 -0
- kash/utils/file_utils/file_formats_model.py +32 -1
- kash/utils/file_utils/file_sort_filter.py +10 -3
- kash/web_gen/__init__.py +0 -4
- kash/web_gen/simple_webpage.py +52 -0
- kash/web_gen/tabbed_webpage.py +23 -16
- kash/web_gen/template_render.py +37 -2
- kash/web_gen/templates/base_styles.css.jinja +84 -59
- kash/web_gen/templates/base_webpage.html.jinja +85 -67
- kash/web_gen/templates/item_view.html.jinja +47 -37
- kash/web_gen/templates/simple_webpage.html.jinja +24 -0
- kash/web_gen/templates/tabbed_webpage.html.jinja +42 -32
- 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.10.dist-info → kash_shell-0.3.12.dist-info}/METADATA +7 -7
- {kash_shell-0.3.10.dist-info → kash_shell-0.3.12.dist-info}/RECORD +81 -76
- kash/shell/utils/argparse_utils.py +0 -20
- kash/utils/lang_utils/inflection.py +0 -18
- {kash_shell-0.3.10.dist-info → kash_shell-0.3.12.dist-info}/WHEEL +0 -0
- {kash_shell-0.3.10.dist-info → kash_shell-0.3.12.dist-info}/entry_points.txt +0 -0
- {kash_shell-0.3.10.dist-info → kash_shell-0.3.12.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import re
|
|
2
|
-
from
|
|
2
|
+
from textwrap import dedent
|
|
3
|
+
from typing import Any, TypeAlias
|
|
3
4
|
|
|
4
5
|
import marko
|
|
5
6
|
import regex
|
|
@@ -11,6 +12,8 @@ from kash.utils.common.url import Url
|
|
|
11
12
|
|
|
12
13
|
log = get_logger(__name__)
|
|
13
14
|
|
|
15
|
+
HTag: TypeAlias = str
|
|
16
|
+
|
|
14
17
|
# Characters that commonly need escaping in Markdown inline text.
|
|
15
18
|
MARKDOWN_ESCAPE_CHARS = r"([\\`*_{}\[\]()#+.!-])"
|
|
16
19
|
MARKDOWN_ESCAPE_RE = re.compile(MARKDOWN_ESCAPE_CHARS)
|
|
@@ -83,6 +86,19 @@ def extract_links(file_path: str, include_internal=False) -> list[str]:
|
|
|
83
86
|
return _tree_links(document, include_internal)
|
|
84
87
|
|
|
85
88
|
|
|
89
|
+
def extract_first_header(content: str) -> str | None:
|
|
90
|
+
"""
|
|
91
|
+
Extract the first header from markdown content if present.
|
|
92
|
+
Also drops any formatting, so the result can be used as a document title.
|
|
93
|
+
"""
|
|
94
|
+
document = marko.parse(content)
|
|
95
|
+
|
|
96
|
+
if document.children and isinstance(document.children[0], Heading):
|
|
97
|
+
return _extract_text(document.children[0]).strip()
|
|
98
|
+
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
86
102
|
def _extract_text(element: Any) -> str:
|
|
87
103
|
if isinstance(element, str):
|
|
88
104
|
return element
|
|
@@ -115,7 +131,7 @@ def extract_bullet_points(content: str) -> list[str]:
|
|
|
115
131
|
return _tree_bullet_points(document)
|
|
116
132
|
|
|
117
133
|
|
|
118
|
-
def _type_from_heading(heading: Heading) ->
|
|
134
|
+
def _type_from_heading(heading: Heading) -> HTag:
|
|
119
135
|
if heading.level in [1, 2, 3, 4, 5, 6]:
|
|
120
136
|
return f"h{heading.level}"
|
|
121
137
|
else:
|
|
@@ -161,6 +177,43 @@ def find_markdown_text(
|
|
|
161
177
|
pos = match.end()
|
|
162
178
|
|
|
163
179
|
|
|
180
|
+
def extract_headings(text: str) -> list[tuple[HTag, str]]:
|
|
181
|
+
"""
|
|
182
|
+
Extract all Markdown headings from the given content.
|
|
183
|
+
Returns a list of (tag, text) tuples:
|
|
184
|
+
[("h1", "Main Title"), ("h2", "Subtitle")]
|
|
185
|
+
where `#` corresponds to `h1`, `##` to `h2`, etc.
|
|
186
|
+
"""
|
|
187
|
+
document = marko.parse(text)
|
|
188
|
+
headings_list: list[tuple[HTag, str]] = []
|
|
189
|
+
|
|
190
|
+
def _collect_headings_recursive(element: Any) -> None:
|
|
191
|
+
if isinstance(element, Heading):
|
|
192
|
+
tag = _type_from_heading(element)
|
|
193
|
+
content = _extract_text(element).strip()
|
|
194
|
+
headings_list.append((tag, content))
|
|
195
|
+
|
|
196
|
+
if hasattr(element, "children"):
|
|
197
|
+
for child in element.children:
|
|
198
|
+
_collect_headings_recursive(child)
|
|
199
|
+
|
|
200
|
+
_collect_headings_recursive(document)
|
|
201
|
+
|
|
202
|
+
return headings_list
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def first_heading(text: str, *, allowed_tags: tuple[HTag, ...] = ("h1", "h2")) -> str | None:
|
|
206
|
+
"""
|
|
207
|
+
Find the text of the first heading. Returns first h1 if present, otherwise first h2, etc.
|
|
208
|
+
"""
|
|
209
|
+
headings = extract_headings(text)
|
|
210
|
+
for goal_tag in allowed_tags:
|
|
211
|
+
for h_tag, h_text in headings:
|
|
212
|
+
if h_tag == goal_tag:
|
|
213
|
+
return h_text
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
|
|
164
217
|
## Tests
|
|
165
218
|
|
|
166
219
|
|
|
@@ -182,6 +235,15 @@ def test_escape_markdown() -> None:
|
|
|
182
235
|
)
|
|
183
236
|
|
|
184
237
|
|
|
238
|
+
def test_extract_first_header() -> None:
|
|
239
|
+
assert extract_first_header("# Header 1") == "Header 1"
|
|
240
|
+
assert extract_first_header("Not a header\n# Header later") is None
|
|
241
|
+
assert extract_first_header("") is None
|
|
242
|
+
assert (
|
|
243
|
+
extract_first_header("## *Formatted* _Header_ [link](#anchor)") == "Formatted Header link"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
185
247
|
def test_find_markdown_text() -> None: # pragma: no cover
|
|
186
248
|
# Match is returned when the term is not inside a link.
|
|
187
249
|
text = "Foo bar baz"
|
|
@@ -202,3 +264,44 @@ def test_find_markdown_text() -> None: # pragma: no cover
|
|
|
202
264
|
pattern = re.compile("bar", re.IGNORECASE)
|
|
203
265
|
match = find_markdown_text(pattern, text)
|
|
204
266
|
assert match is None
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def test_extract_headings_and_first_header() -> None:
|
|
270
|
+
markdown_content = dedent("""
|
|
271
|
+
# Title 1
|
|
272
|
+
Some text.
|
|
273
|
+
## Subtitle 1.1
|
|
274
|
+
More text.
|
|
275
|
+
### Sub-subtitle 1.1.1
|
|
276
|
+
Even more text.
|
|
277
|
+
# Title 2 *with formatting*
|
|
278
|
+
And final text.
|
|
279
|
+
## Subtitle 2.1
|
|
280
|
+
""")
|
|
281
|
+
expected_headings = [
|
|
282
|
+
("h1", "Title 1"),
|
|
283
|
+
("h2", "Subtitle 1.1"),
|
|
284
|
+
("h3", "Sub-subtitle 1.1.1"),
|
|
285
|
+
("h1", "Title 2 with formatting"),
|
|
286
|
+
("h2", "Subtitle 2.1"),
|
|
287
|
+
]
|
|
288
|
+
assert extract_headings(markdown_content) == expected_headings
|
|
289
|
+
|
|
290
|
+
assert first_heading(markdown_content) == "Title 1"
|
|
291
|
+
assert first_heading(markdown_content) == "Title 1"
|
|
292
|
+
assert first_heading(markdown_content, allowed_tags=("h2",)) == "Subtitle 1.1"
|
|
293
|
+
assert first_heading(markdown_content, allowed_tags=("h3",)) == "Sub-subtitle 1.1.1"
|
|
294
|
+
assert first_heading(markdown_content, allowed_tags=("h4",)) is None
|
|
295
|
+
|
|
296
|
+
assert extract_headings("") == []
|
|
297
|
+
assert first_heading("") is None
|
|
298
|
+
assert first_heading("Just text, no headers.") is None
|
|
299
|
+
|
|
300
|
+
markdown_h2_only = "## Only H2 Here"
|
|
301
|
+
assert extract_headings(markdown_h2_only) == [("h2", "Only H2 Here")]
|
|
302
|
+
assert first_heading(markdown_h2_only) == "Only H2 Here"
|
|
303
|
+
assert first_heading(markdown_h2_only, allowed_tags=("h2",)) == "Only H2 Here"
|
|
304
|
+
|
|
305
|
+
formatted_header_md = "## *Formatted* _Header_ [link](#anchor)"
|
|
306
|
+
assert extract_headings(formatted_header_md) == [("h2", "Formatted Header link")]
|
|
307
|
+
assert first_heading(formatted_header_md, allowed_tags=("h2",)) == "Formatted Header link"
|
|
@@ -2,10 +2,9 @@ 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.
|
|
7
|
+
from kash.utils.common.inflection import plural
|
|
9
8
|
from kash.utils.common.url import Locator, is_url
|
|
10
9
|
|
|
11
10
|
|
|
@@ -45,16 +44,11 @@ def fmt_loc(locator: str | Locator, resolve: bool = True) -> str:
|
|
|
45
44
|
return fmt_path(locator, resolve=resolve)
|
|
46
45
|
|
|
47
46
|
|
|
48
|
-
@lazyobject
|
|
49
|
-
def inflect():
|
|
50
|
-
return engine()
|
|
51
|
-
|
|
52
|
-
|
|
53
47
|
def fmt_count_items(count: int, name: str = "item") -> str:
|
|
54
48
|
"""
|
|
55
49
|
Format a count and a name as a pluralized phrase, e.g. "1 item" or "2 items".
|
|
56
50
|
"""
|
|
57
|
-
return f"{count} {
|
|
51
|
+
return f"{count} {plural(name, count)}" # pyright: ignore
|
|
58
52
|
|
|
59
53
|
|
|
60
54
|
## Tests
|