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.
Files changed (83) hide show
  1. kash/actions/core/format_markdown_template.py +2 -5
  2. kash/actions/core/markdownify.py +2 -4
  3. kash/actions/core/readability.py +2 -4
  4. kash/actions/core/render_as_html.py +30 -11
  5. kash/actions/core/show_webpage.py +6 -11
  6. kash/actions/core/strip_html.py +4 -8
  7. kash/actions/core/{webpage_config.py → tabbed_webpage_config.py} +5 -3
  8. kash/actions/core/{webpage_generate.py → tabbed_webpage_generate.py} +5 -4
  9. kash/commands/base/basic_file_commands.py +21 -3
  10. kash/commands/base/files_command.py +29 -10
  11. kash/commands/extras/parse_uv_lock.py +12 -3
  12. kash/commands/workspace/selection_commands.py +1 -1
  13. kash/commands/workspace/workspace_commands.py +2 -3
  14. kash/config/colors.py +2 -2
  15. kash/config/env_settings.py +2 -42
  16. kash/config/logger.py +30 -25
  17. kash/config/logger_basic.py +6 -6
  18. kash/config/settings.py +23 -7
  19. kash/config/setup.py +33 -5
  20. kash/config/text_styles.py +25 -22
  21. kash/embeddings/cosine.py +12 -4
  22. kash/embeddings/embeddings.py +16 -6
  23. kash/embeddings/text_similarity.py +10 -4
  24. kash/exec/__init__.py +3 -0
  25. kash/exec/action_decorators.py +10 -25
  26. kash/exec/action_exec.py +43 -23
  27. kash/exec/llm_transforms.py +6 -3
  28. kash/exec/preconditions.py +10 -12
  29. kash/exec/resolve_args.py +4 -0
  30. kash/exec/runtime_settings.py +134 -0
  31. kash/exec/shell_callable_action.py +5 -3
  32. kash/file_storage/file_store.py +37 -38
  33. kash/file_storage/item_file_format.py +6 -3
  34. kash/file_storage/store_filenames.py +6 -3
  35. kash/help/function_param_info.py +1 -1
  36. kash/llm_utils/init_litellm.py +16 -0
  37. kash/llm_utils/llm_api_keys.py +6 -2
  38. kash/llm_utils/llm_completion.py +11 -4
  39. kash/local_server/local_server_routes.py +1 -7
  40. kash/mcp/mcp_cli.py +3 -2
  41. kash/mcp/mcp_server_routes.py +11 -12
  42. kash/media_base/transcription_deepgram.py +15 -2
  43. kash/model/__init__.py +1 -1
  44. kash/model/actions_model.py +6 -54
  45. kash/model/exec_model.py +79 -0
  46. kash/model/items_model.py +102 -35
  47. kash/model/operations_model.py +38 -15
  48. kash/model/paths_model.py +2 -0
  49. kash/shell/output/shell_output.py +10 -8
  50. kash/shell/shell_main.py +2 -2
  51. kash/shell/utils/exception_printing.py +2 -2
  52. kash/shell/utils/shell_function_wrapper.py +15 -15
  53. kash/text_handling/doc_normalization.py +16 -8
  54. kash/text_handling/markdown_render.py +1 -0
  55. kash/text_handling/markdown_utils.py +105 -2
  56. kash/utils/common/format_utils.py +2 -8
  57. kash/utils/common/function_inspect.py +360 -110
  58. kash/utils/common/inflection.py +22 -0
  59. kash/utils/common/task_stack.py +4 -15
  60. kash/utils/errors.py +14 -9
  61. kash/utils/file_utils/file_ext.py +4 -0
  62. kash/utils/file_utils/file_formats_model.py +32 -1
  63. kash/utils/file_utils/file_sort_filter.py +10 -3
  64. kash/web_gen/__init__.py +0 -4
  65. kash/web_gen/simple_webpage.py +52 -0
  66. kash/web_gen/tabbed_webpage.py +23 -16
  67. kash/web_gen/template_render.py +37 -2
  68. kash/web_gen/templates/base_styles.css.jinja +84 -59
  69. kash/web_gen/templates/base_webpage.html.jinja +85 -67
  70. kash/web_gen/templates/item_view.html.jinja +47 -37
  71. kash/web_gen/templates/simple_webpage.html.jinja +24 -0
  72. kash/web_gen/templates/tabbed_webpage.html.jinja +42 -32
  73. kash/workspaces/__init__.py +12 -3
  74. kash/workspaces/workspace_dirs.py +58 -0
  75. kash/workspaces/workspace_importing.py +1 -1
  76. kash/workspaces/workspaces.py +26 -90
  77. {kash_shell-0.3.10.dist-info → kash_shell-0.3.12.dist-info}/METADATA +7 -7
  78. {kash_shell-0.3.10.dist-info → kash_shell-0.3.12.dist-info}/RECORD +81 -76
  79. kash/shell/utils/argparse_utils.py +0 -20
  80. kash/utils/lang_utils/inflection.py +0 -18
  81. {kash_shell-0.3.10.dist-info → kash_shell-0.3.12.dist-info}/WHEEL +0 -0
  82. {kash_shell-0.3.10.dist-info → kash_shell-0.3.12.dist-info}/entry_points.txt +0 -0
  83. {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 typing import Any
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) -> str:
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.lazyobject import lazyobject
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} {inflect.plural(name, count)}" # pyright: ignore
51
+ return f"{count} {plural(name, count)}" # pyright: ignore
58
52
 
59
53
 
60
54
  ## Tests