kash-shell 0.3.12__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 +6 -4
- kash/commands/base/basic_file_commands.py +3 -0
- kash/commands/base/diff_commands.py +38 -3
- kash/commands/base/reformat_command.py +1 -1
- kash/commands/base/show_command.py +1 -1
- kash/commands/workspace/selection_commands.py +1 -1
- kash/commands/workspace/workspace_commands.py +62 -16
- kash/docs/load_source_code.py +1 -1
- kash/exec/action_exec.py +4 -5
- kash/exec/fetch_url_metadata.py +8 -5
- kash/exec/importing.py +4 -4
- kash/exec/llm_transforms.py +1 -1
- kash/exec/preconditions.py +7 -7
- kash/file_storage/file_store.py +73 -32
- kash/file_storage/item_file_format.py +1 -1
- kash/file_storage/store_filenames.py +2 -1
- 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/llm_completion.py +1 -1
- 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_server_commands.py +8 -2
- kash/media_base/media_cache.py +10 -3
- kash/model/actions_model.py +3 -0
- kash/model/items_model.py +71 -42
- kash/shell/ui/shell_results.py +2 -1
- kash/utils/common/format_utils.py +0 -8
- kash/utils/common/import_utils.py +46 -18
- kash/utils/file_utils/file_formats_model.py +46 -26
- kash/utils/file_utils/filename_parsing.py +41 -16
- kash/{text_handling → utils/text_handling}/doc_normalization.py +10 -8
- kash/utils/text_handling/escape_html_tags.py +156 -0
- kash/{text_handling → utils/text_handling}/markdown_utils.py +0 -3
- 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 +134 -16
- kash/web_gen/templates/simple_webpage.html.jinja +1 -1
- kash/workspaces/selections.py +2 -2
- kash/workspaces/workspace_importing.py +1 -1
- kash/workspaces/workspace_output.py +2 -2
- kash/xonsh_custom/load_into_xonsh.py +4 -2
- {kash_shell-0.3.12.dist-info → kash_shell-0.3.13.dist-info}/METADATA +1 -1
- {kash_shell-0.3.12.dist-info → kash_shell-0.3.13.dist-info}/RECORD +58 -57
- kash/utils/common/inflection.py +0 -22
- /kash/{text_handling → utils/text_handling}/markdown_render.py +0 -0
- {kash_shell-0.3.12.dist-info → kash_shell-0.3.13.dist-info}/WHEEL +0 -0
- {kash_shell-0.3.12.dist-info → kash_shell-0.3.13.dist-info}/entry_points.txt +0 -0
- {kash_shell-0.3.12.dist-info → kash_shell-0.3.13.dist-info}/licenses/LICENSE +0 -0
kash/file_storage/file_store.py
CHANGED
|
@@ -182,12 +182,37 @@ class FileStore(Workspace):
|
|
|
182
182
|
except (FileNotFoundError, InvalidFilename):
|
|
183
183
|
pass
|
|
184
184
|
|
|
185
|
+
def resolve_path(self, path: Path | StorePath) -> StorePath | None:
|
|
186
|
+
"""
|
|
187
|
+
Return a StorePath if the given path is within the store, otherwise None.
|
|
188
|
+
If it is already a StorePath, return it unchanged.
|
|
189
|
+
"""
|
|
190
|
+
if isinstance(path, StorePath):
|
|
191
|
+
return path
|
|
192
|
+
resolved = path.resolve()
|
|
193
|
+
if resolved.is_relative_to(self.base_dir):
|
|
194
|
+
return StorePath(resolved.relative_to(self.base_dir))
|
|
195
|
+
else:
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
def exists(self, store_path: StorePath) -> bool:
|
|
199
|
+
"""
|
|
200
|
+
Check given store path refers to an existing file.
|
|
201
|
+
"""
|
|
202
|
+
return (self.base_dir / store_path).exists()
|
|
203
|
+
|
|
185
204
|
@synchronized
|
|
186
|
-
def
|
|
205
|
+
def _pick_filename_for(self, item: Item, *, overwrite: bool = False) -> tuple[str, str | None]:
|
|
187
206
|
"""
|
|
188
|
-
Get a suitable filename for this item
|
|
189
|
-
|
|
207
|
+
Get a suitable filename for this item.
|
|
208
|
+
If `overwrite` is true, use the the slugified title.
|
|
209
|
+
If it is false, use the slugified title with a suffix to make it unique
|
|
210
|
+
and in this case returns the old filename for this item, if it is different.
|
|
190
211
|
"""
|
|
212
|
+
if overwrite:
|
|
213
|
+
log.info("Picked default filename: %s for item: %s", item.default_filename(), item)
|
|
214
|
+
return item.default_filename(), None
|
|
215
|
+
|
|
191
216
|
slug = item.slug_name()
|
|
192
217
|
full_suffix = item.get_full_suffix()
|
|
193
218
|
# Get a unique name per item type.
|
|
@@ -198,29 +223,15 @@ class FileStore(Workspace):
|
|
|
198
223
|
|
|
199
224
|
old_filename = join_suffix(old_slugs[0], full_suffix) if old_slugs else None
|
|
200
225
|
|
|
226
|
+
log.info("Picked new unique filename: %s for item: %s", new_unique_filename, item)
|
|
201
227
|
return new_unique_filename, old_filename
|
|
202
228
|
|
|
203
|
-
def
|
|
204
|
-
folder_path = folder_for_type(item.type)
|
|
205
|
-
slug = item.slug_name()
|
|
206
|
-
suffix = item.get_full_suffix()
|
|
207
|
-
return StorePath(folder_path / join_suffix(slug, suffix))
|
|
208
|
-
|
|
209
|
-
def exists(self, store_path: StorePath) -> bool:
|
|
210
|
-
return (self.base_dir / store_path).exists()
|
|
211
|
-
|
|
212
|
-
def resolve_path(self, path: Path | StorePath) -> StorePath | None:
|
|
229
|
+
def default_path_for(self, item: Item) -> StorePath:
|
|
213
230
|
"""
|
|
214
|
-
|
|
215
|
-
If it is already a StorePath, return it unchanged.
|
|
231
|
+
Get the default store path for an item based on slugifying its title or other metadata.
|
|
216
232
|
"""
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
resolved = path.resolve()
|
|
220
|
-
if resolved.is_relative_to(self.base_dir):
|
|
221
|
-
return StorePath(resolved.relative_to(self.base_dir))
|
|
222
|
-
else:
|
|
223
|
-
return None
|
|
233
|
+
folder_path = folder_for_type(item.type)
|
|
234
|
+
return StorePath(folder_path / item.default_filename())
|
|
224
235
|
|
|
225
236
|
@synchronized
|
|
226
237
|
def find_by_id(self, item: Item) -> StorePath | None:
|
|
@@ -235,7 +246,7 @@ class FileStore(Workspace):
|
|
|
235
246
|
store_path = self.id_map.get(item_id)
|
|
236
247
|
if not store_path:
|
|
237
248
|
# Just in case the id_map is not complete, check the default path too.
|
|
238
|
-
default_path = self.
|
|
249
|
+
default_path = self.default_path_for(item)
|
|
239
250
|
if self.exists(default_path):
|
|
240
251
|
old_item = self.load(default_path)
|
|
241
252
|
if old_item.item_id() == item_id:
|
|
@@ -256,7 +267,7 @@ class FileStore(Workspace):
|
|
|
256
267
|
|
|
257
268
|
@synchronized
|
|
258
269
|
def store_path_for(
|
|
259
|
-
self, item: Item, as_tmp: bool = False
|
|
270
|
+
self, item: Item, *, as_tmp: bool = False, overwrite: bool = False
|
|
260
271
|
) -> tuple[StorePath, bool, StorePath | None]:
|
|
261
272
|
"""
|
|
262
273
|
Return the store path for an item. If the item already has a `store_path`, we use that.
|
|
@@ -265,6 +276,10 @@ class FileStore(Workspace):
|
|
|
265
276
|
Returns `store_path, found, old_store_path` where `found` indicates whether the path was
|
|
266
277
|
already found (in the item or in the store by checking for identity) and `old_store_path`
|
|
267
278
|
is the previous similarly named item with a different identity (or None there is none).
|
|
279
|
+
|
|
280
|
+
If `as_tmp` is true, will return a path from the temporary directory in the store.
|
|
281
|
+
Normally an item is always saved to a unique store path but if `overwrite` is true,
|
|
282
|
+
will always save to the same path
|
|
268
283
|
"""
|
|
269
284
|
item_id = item.item_id()
|
|
270
285
|
old_filename = None
|
|
@@ -281,9 +296,9 @@ class FileStore(Workspace):
|
|
|
281
296
|
)
|
|
282
297
|
return store_path, True, None
|
|
283
298
|
else:
|
|
284
|
-
# We need to
|
|
299
|
+
# We need to pick the path and filename.
|
|
285
300
|
folder_path = folder_for_type(item.type)
|
|
286
|
-
filename, old_filename = self.
|
|
301
|
+
filename, old_filename = self._pick_filename_for(item, overwrite=overwrite)
|
|
287
302
|
store_path = folder_path / filename
|
|
288
303
|
|
|
289
304
|
old_store_path = None
|
|
@@ -312,7 +327,13 @@ class FileStore(Workspace):
|
|
|
312
327
|
|
|
313
328
|
@log_calls()
|
|
314
329
|
def save(
|
|
315
|
-
self,
|
|
330
|
+
self,
|
|
331
|
+
item: Item,
|
|
332
|
+
*,
|
|
333
|
+
overwrite: bool = False,
|
|
334
|
+
skip_dup_names: bool = False,
|
|
335
|
+
as_tmp: bool = False,
|
|
336
|
+
no_format: bool = False,
|
|
316
337
|
) -> StorePath:
|
|
317
338
|
"""
|
|
318
339
|
Save the item. Uses the `store_path` if it's already set or generates a new one.
|
|
@@ -321,9 +342,18 @@ class FileStore(Workspace):
|
|
|
321
342
|
Unless `no_format` is true, also normalizes body text formatting (for Markdown)
|
|
322
343
|
and updates the item's body to match.
|
|
323
344
|
|
|
345
|
+
If `overwrite` is true, will overwrite a file that has the same path.
|
|
346
|
+
|
|
324
347
|
If `as_tmp` is true, will save the item to a temporary file.
|
|
325
|
-
|
|
348
|
+
|
|
349
|
+
If `skip_dup_names` is true, will skip saving if an item if an item with a
|
|
350
|
+
matching path (based on its title) already exists.
|
|
326
351
|
"""
|
|
352
|
+
if overwrite and skip_dup_names:
|
|
353
|
+
raise ValueError("Cannot both overwrite and skip duplicate names.")
|
|
354
|
+
if overwrite and as_tmp:
|
|
355
|
+
raise ValueError("Cannot both overwrite and save to a temporary file.")
|
|
356
|
+
|
|
327
357
|
# If external file already exists within the workspace, the file is already saved (without metadata).
|
|
328
358
|
external_path = item.external_path and Path(item.external_path).resolve()
|
|
329
359
|
if external_path and self._is_in_store(external_path):
|
|
@@ -335,10 +365,15 @@ class FileStore(Workspace):
|
|
|
335
365
|
return StorePath(rel_path)
|
|
336
366
|
else:
|
|
337
367
|
# Otherwise it's still in memory or in a file outside the workspace and we need to save it.
|
|
338
|
-
store_path, found, old_store_path = self.store_path_for(
|
|
368
|
+
store_path, found, old_store_path = self.store_path_for(
|
|
369
|
+
item, as_tmp=as_tmp, overwrite=overwrite
|
|
370
|
+
)
|
|
339
371
|
|
|
340
|
-
if
|
|
341
|
-
log.message(
|
|
372
|
+
if skip_dup_names and found:
|
|
373
|
+
log.message(
|
|
374
|
+
"Skipping save because an item of the same name already exists: %s",
|
|
375
|
+
fmt_loc(store_path),
|
|
376
|
+
)
|
|
342
377
|
item.store_path = str(store_path)
|
|
343
378
|
return store_path
|
|
344
379
|
|
|
@@ -356,8 +391,14 @@ class FileStore(Workspace):
|
|
|
356
391
|
# Now save the new item.
|
|
357
392
|
try:
|
|
358
393
|
if item.external_path:
|
|
359
|
-
copyfile_atomic(item.external_path, full_path)
|
|
394
|
+
copyfile_atomic(item.external_path, full_path, make_parents=True)
|
|
360
395
|
else:
|
|
396
|
+
if overwrite and full_path.exists():
|
|
397
|
+
log.info(
|
|
398
|
+
"Overwrite is enabled and a previous file exists so will archive it: %s",
|
|
399
|
+
fmt_loc(store_path),
|
|
400
|
+
)
|
|
401
|
+
self.archive(store_path, quiet=True)
|
|
361
402
|
write_item(item, full_path, normalize=not no_format)
|
|
362
403
|
except OSError as e:
|
|
363
404
|
log.error("Error saving item: %s", e)
|
|
@@ -7,10 +7,10 @@ from prettyfmt import custom_key_sort, fmt_size_human
|
|
|
7
7
|
from kash.config.logger import get_logger
|
|
8
8
|
from kash.model.items_model import ITEM_FIELDS, Item
|
|
9
9
|
from kash.model.operations_model import OPERATION_FIELDS
|
|
10
|
-
from kash.text_handling.doc_normalization import normalize_formatting
|
|
11
10
|
from kash.utils.common.format_utils import fmt_loc
|
|
12
11
|
from kash.utils.file_utils.file_formats_model import Format
|
|
13
12
|
from kash.utils.file_utils.mtime_cache import MtimeCache
|
|
13
|
+
from kash.utils.text_handling.doc_normalization import normalize_formatting
|
|
14
14
|
|
|
15
15
|
log = get_logger(__name__)
|
|
16
16
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from functools import cache
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
|
|
4
|
+
from prettyfmt import plural
|
|
5
|
+
|
|
4
6
|
from kash.config.logger import get_logger
|
|
5
7
|
from kash.model.items_model import ItemType
|
|
6
|
-
from kash.utils.common.inflection import plural
|
|
7
8
|
from kash.utils.file_utils.file_formats_model import FileExt, Format
|
|
8
9
|
from kash.utils.file_utils.filename_parsing import split_filename
|
|
9
10
|
|
kash/help/help_embeddings.py
CHANGED
|
@@ -62,9 +62,9 @@ class HelpIndex:
|
|
|
62
62
|
embeddings.to_npz(target_path)
|
|
63
63
|
|
|
64
64
|
key = f"kash_help_embeddings_v1_{len(self.docs)}.npz"
|
|
65
|
-
path
|
|
65
|
+
path = cache_file(
|
|
66
66
|
Loadable(key, save=calculate_and_save_help_embeddings), global_cache=True
|
|
67
|
-
)
|
|
67
|
+
).content.path
|
|
68
68
|
log.info("Loaded help doc embeddings from: %s", path)
|
|
69
69
|
return Embeddings.read_from_npz(path)
|
|
70
70
|
|
kash/llm_utils/clean_headings.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from kash.llm_utils import Message, MessageTemplate, llm_template_completion
|
|
2
2
|
from kash.llm_utils.llms import LLM
|
|
3
|
-
from kash.text_handling.markdown_utils import as_bullet_points
|
|
3
|
+
from kash.utils.text_handling.markdown_utils import as_bullet_points
|
|
4
4
|
|
|
5
5
|
# TODO: Enforce that the edits below doesn't contain anything extraneous.
|
|
6
6
|
|
kash/llm_utils/llm_completion.py
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import time
|
|
4
|
+
from dataclasses import dataclass
|
|
4
5
|
from typing import TYPE_CHECKING, cast
|
|
5
6
|
|
|
6
7
|
from flowmark import Wrap, fill_text
|
|
7
8
|
from funlog import format_duration, log_calls
|
|
8
9
|
from prettyfmt import slugify_snake
|
|
9
10
|
from pydantic import BaseModel
|
|
10
|
-
from pydantic.dataclasses import dataclass
|
|
11
11
|
|
|
12
12
|
from kash.config.logger import get_logger
|
|
13
13
|
from kash.config.text_styles import EMOJI_TIMING
|
kash/local_server/__init__.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from kash.config.logger import get_logger
|
|
2
2
|
from kash.config.settings import global_settings
|
|
3
3
|
from kash.exec import kash_command
|
|
4
|
-
from kash.local_server.local_url_formatters import enable_local_urls
|
|
5
4
|
from kash.shell.utils.native_utils import tail_file
|
|
6
5
|
from kash.utils.errors import InvalidState
|
|
7
6
|
|
|
@@ -17,6 +16,7 @@ def start_ui_server() -> None:
|
|
|
17
16
|
tooltips.
|
|
18
17
|
"""
|
|
19
18
|
from kash.local_server.local_server import start_ui_server
|
|
19
|
+
from kash.local_server.local_url_formatters import enable_local_urls
|
|
20
20
|
|
|
21
21
|
start_ui_server()
|
|
22
22
|
enable_local_urls(True)
|
|
@@ -28,6 +28,7 @@ def stop_ui_server() -> None:
|
|
|
28
28
|
Stop the kash local server.
|
|
29
29
|
"""
|
|
30
30
|
from kash.local_server.local_server import stop_ui_server
|
|
31
|
+
from kash.local_server.local_url_formatters import enable_local_urls
|
|
31
32
|
|
|
32
33
|
stop_ui_server()
|
|
33
34
|
enable_local_urls(False)
|
kash/mcp/__init__.py
CHANGED
kash/mcp/mcp_server_commands.py
CHANGED
|
@@ -6,8 +6,6 @@ from kash.config.settings import (
|
|
|
6
6
|
global_settings,
|
|
7
7
|
)
|
|
8
8
|
from kash.exec import kash_command
|
|
9
|
-
from kash.mcp import mcp_server_routes
|
|
10
|
-
from kash.mcp.mcp_cli import MCP_CLI_LOG_PATH
|
|
11
9
|
from kash.shell.output.shell_formatting import format_name_and_value
|
|
12
10
|
from kash.shell.output.shell_output import cprint, print_h2
|
|
13
11
|
from kash.shell.utils.native_utils import tail_file
|
|
@@ -21,6 +19,7 @@ def start_mcp_server() -> None:
|
|
|
21
19
|
"""
|
|
22
20
|
Start the MCP server, using all currently known actions marked as MCP tools.
|
|
23
21
|
"""
|
|
22
|
+
from kash.mcp import mcp_server_routes
|
|
24
23
|
from kash.mcp.mcp_server_sse import start_mcp_server_sse
|
|
25
24
|
|
|
26
25
|
mcp_server_routes.publish_mcp_tools()
|
|
@@ -42,6 +41,7 @@ def restart_mcp_server() -> None:
|
|
|
42
41
|
"""
|
|
43
42
|
Restart the MCP server, republishing all actions marked as MCP tools.
|
|
44
43
|
"""
|
|
44
|
+
from kash.mcp import mcp_server_routes
|
|
45
45
|
from kash.mcp.mcp_server_sse import restart_mcp_server_sse
|
|
46
46
|
|
|
47
47
|
mcp_server_routes.unpublish_mcp_tools(None)
|
|
@@ -57,6 +57,8 @@ def mcp_logs(follow: bool = False, all: bool = False) -> None:
|
|
|
57
57
|
:param follow: Follow the file as it grows.
|
|
58
58
|
:param all: Show all logs, not just the server logs, including Claude Desktop logs if found.
|
|
59
59
|
"""
|
|
60
|
+
from kash.mcp.mcp_cli import MCP_CLI_LOG_PATH
|
|
61
|
+
|
|
60
62
|
settings = global_settings()
|
|
61
63
|
if all:
|
|
62
64
|
global_log_base = settings.system_logs_dir
|
|
@@ -96,6 +98,7 @@ def list_mcp_tools() -> None:
|
|
|
96
98
|
"""
|
|
97
99
|
List published MCP tools.
|
|
98
100
|
"""
|
|
101
|
+
from kash.mcp import mcp_server_routes
|
|
99
102
|
|
|
100
103
|
tools = mcp_server_routes.get_published_tools()
|
|
101
104
|
|
|
@@ -118,6 +121,8 @@ def publish_mcp_tool(*action_names: str) -> None:
|
|
|
118
121
|
Publish one or more actions as local MCP tools. With no arguments, publish all
|
|
119
122
|
actions marked as MCP tools.
|
|
120
123
|
"""
|
|
124
|
+
from kash.mcp import mcp_server_routes
|
|
125
|
+
|
|
121
126
|
if not action_names:
|
|
122
127
|
log.message("Publishing all actions marked as MCP tools.")
|
|
123
128
|
mcp_server_routes.publish_mcp_tools()
|
|
@@ -131,6 +136,7 @@ def unpublish_mcp_tool(*action_names: str) -> None:
|
|
|
131
136
|
Un-publish one or more actions as local MCP tools. With no arguments,
|
|
132
137
|
un-publish all published actions.
|
|
133
138
|
"""
|
|
139
|
+
from kash.mcp import mcp_server_routes
|
|
134
140
|
|
|
135
141
|
if not action_names:
|
|
136
142
|
log.message("Un-publishing all actions marked as MCP tools.")
|
kash/media_base/media_cache.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
from functools import cache
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
|
|
4
5
|
from prettyfmt import fmt_lines, fmt_path
|
|
@@ -11,7 +12,6 @@ from kash.media_base.media_services import (
|
|
|
11
12
|
download_media_by_service,
|
|
12
13
|
get_media_services,
|
|
13
14
|
)
|
|
14
|
-
from kash.media_base.transcription_deepgram import deepgram_transcribe_audio
|
|
15
15
|
from kash.utils.common.format_utils import fmt_loc
|
|
16
16
|
from kash.utils.common.url import Url, as_file_url, is_url
|
|
17
17
|
from kash.utils.errors import FileNotFound, InvalidInput, UnexpectedError
|
|
@@ -22,7 +22,14 @@ log = get_logger(__name__)
|
|
|
22
22
|
|
|
23
23
|
# FIXME: Hard-coded dependency for now. Would be better to make it settable.
|
|
24
24
|
# transcribe_audio = whisper_transcribe_audio_small
|
|
25
|
-
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@cache
|
|
28
|
+
def get_transcriber():
|
|
29
|
+
from kash.media_base.transcription_deepgram import deepgram_transcribe_audio
|
|
30
|
+
|
|
31
|
+
transcribe_audio = deepgram_transcribe_audio
|
|
32
|
+
return transcribe_audio
|
|
26
33
|
|
|
27
34
|
|
|
28
35
|
# For simplicity we assume all audio is converted to mp3.
|
|
@@ -83,7 +90,7 @@ class MediaCache(DirStore):
|
|
|
83
90
|
url,
|
|
84
91
|
fmt_path(downsampled_audio_file),
|
|
85
92
|
)
|
|
86
|
-
transcript =
|
|
93
|
+
transcript = get_transcriber()(downsampled_audio_file, language=language)
|
|
87
94
|
self._write_transcript(url, transcript)
|
|
88
95
|
return transcript
|
|
89
96
|
|
kash/model/actions_model.py
CHANGED
|
@@ -90,6 +90,9 @@ class ActionResult:
|
|
|
90
90
|
replaces_input: bool = False
|
|
91
91
|
"""If True, a hint to archive the input items."""
|
|
92
92
|
|
|
93
|
+
overwrite: bool = False
|
|
94
|
+
"""If True, will not pick unique output paths to save to, overwriting existing files of the same name."""
|
|
95
|
+
|
|
93
96
|
skip_duplicates: bool = False
|
|
94
97
|
"""If True, do not save duplicate items (based on identity)."""
|
|
95
98
|
|
kash/model/items_model.py
CHANGED
|
@@ -24,13 +24,14 @@ from kash.model.concept_model import canonicalize_concept
|
|
|
24
24
|
from kash.model.media_model import MediaMetadata
|
|
25
25
|
from kash.model.operations_model import OperationSummary, Source
|
|
26
26
|
from kash.model.paths_model import StorePath, fmt_store_path
|
|
27
|
-
from kash.text_handling.markdown_render import markdown_to_html
|
|
28
|
-
from kash.text_handling.markdown_utils import first_heading
|
|
29
27
|
from kash.utils.common.format_utils import fmt_loc, html_to_plaintext, plaintext_to_html
|
|
30
28
|
from kash.utils.common.url import Locator, Url
|
|
31
29
|
from kash.utils.errors import FileFormatError
|
|
32
30
|
from kash.utils.file_formats.chat_format import ChatHistory
|
|
31
|
+
from kash.utils.file_utils.file_formats import MimeType
|
|
33
32
|
from kash.utils.file_utils.file_formats_model import FileExt, Format
|
|
33
|
+
from kash.utils.text_handling.markdown_render import markdown_to_html
|
|
34
|
+
from kash.utils.text_handling.markdown_utils import first_heading
|
|
34
35
|
|
|
35
36
|
if TYPE_CHECKING:
|
|
36
37
|
from kash.model.exec_model import ExecContext
|
|
@@ -359,15 +360,19 @@ class Item:
|
|
|
359
360
|
cls,
|
|
360
361
|
path: Path | str,
|
|
361
362
|
item_type: ItemType | None = None,
|
|
363
|
+
*,
|
|
362
364
|
title: str | None = None,
|
|
365
|
+
original_filename: str | None = None,
|
|
366
|
+
mime_type: MimeType | None = None,
|
|
363
367
|
) -> Item:
|
|
364
368
|
"""
|
|
365
369
|
Create a resource Item for a file with a format inferred from the file extension
|
|
366
370
|
or the content. Only sets basic metadata. Does not read the content. Will set
|
|
367
371
|
`format` and `file_ext` if possible but will leave them as None if unrecognized.
|
|
372
|
+
If `mime_type` is provided, it can help determine the file extension.
|
|
368
373
|
"""
|
|
369
374
|
from kash.file_storage.store_filenames import parse_item_filename
|
|
370
|
-
from kash.utils.file_utils.file_formats_model import detect_file_format
|
|
375
|
+
from kash.utils.file_utils.file_formats_model import choose_file_ext, detect_file_format
|
|
371
376
|
|
|
372
377
|
# Will raise error for unrecognized file ext.
|
|
373
378
|
_name, filename_item_type, format, file_ext = parse_item_filename(path)
|
|
@@ -380,12 +385,17 @@ class Item:
|
|
|
380
385
|
item_type = (
|
|
381
386
|
ItemType.doc if format and format.supports_frontmatter else ItemType.resource
|
|
382
387
|
)
|
|
388
|
+
# Do our best to determine a good file extension if it's not already on the filename.
|
|
389
|
+
if not file_ext and mime_type:
|
|
390
|
+
file_ext = choose_file_ext(path, mime_type)
|
|
391
|
+
|
|
383
392
|
item = cls(
|
|
384
393
|
type=item_type,
|
|
385
394
|
title=title,
|
|
386
395
|
file_ext=file_ext,
|
|
387
396
|
format=format,
|
|
388
397
|
external_path=str(path),
|
|
398
|
+
original_filename=original_filename,
|
|
389
399
|
)
|
|
390
400
|
|
|
391
401
|
# Update modified time from the file system.
|
|
@@ -507,17 +517,43 @@ class Item:
|
|
|
507
517
|
|
|
508
518
|
return item_dict
|
|
509
519
|
|
|
510
|
-
def
|
|
520
|
+
def filename_stem(self) -> str | None:
|
|
511
521
|
"""
|
|
512
|
-
|
|
513
|
-
|
|
522
|
+
If the item has an existing or previous filename, return its stem,
|
|
523
|
+
for use in picking new filenames.
|
|
514
524
|
"""
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
525
|
+
from kash.file_storage.store_filenames import parse_item_filename
|
|
526
|
+
|
|
527
|
+
# Prefer original to external, e.g. if we know the original but the external might
|
|
528
|
+
# be a cache filename.
|
|
529
|
+
path = self.store_path or self.original_filename or self.external_path
|
|
530
|
+
if path:
|
|
531
|
+
path_name, _item_type, _format, _file_ext = parse_item_filename(Path(path).name)
|
|
532
|
+
else:
|
|
533
|
+
path_name = None
|
|
534
|
+
return path_name
|
|
535
|
+
|
|
536
|
+
def slug_name(self, max_len: int = SLUG_MAX_LEN, prefer_title: bool = False) -> str:
|
|
537
|
+
"""
|
|
538
|
+
Get a readable slugified name for this item, either from a previous filename
|
|
539
|
+
or from slugifying the title or content. May not be unique.
|
|
540
|
+
"""
|
|
541
|
+
filename_stem = self.filename_stem()
|
|
542
|
+
if filename_stem and not prefer_title:
|
|
543
|
+
return slugify_snake(filename_stem)
|
|
544
|
+
else:
|
|
545
|
+
return slugify_snake(self.abbrev_title(max_len=max_len, add_ops_suffix=True))
|
|
546
|
+
|
|
547
|
+
def default_filename(self) -> str:
|
|
548
|
+
"""
|
|
549
|
+
Get the default filename for an item based on slugifying its title or other
|
|
550
|
+
metadata. May not be unique.
|
|
551
|
+
"""
|
|
552
|
+
from kash.file_storage.store_filenames import join_suffix
|
|
553
|
+
|
|
554
|
+
slug = self.slug_name()
|
|
555
|
+
full_suffix = self.get_full_suffix()
|
|
556
|
+
return join_suffix(slug, full_suffix)
|
|
521
557
|
|
|
522
558
|
def abbrev_title(
|
|
523
559
|
self,
|
|
@@ -527,12 +563,10 @@ class Item:
|
|
|
527
563
|
pull_body_heading: bool = False,
|
|
528
564
|
) -> str:
|
|
529
565
|
"""
|
|
530
|
-
Get or infer a title for this item, falling back to the filename, URL,
|
|
531
|
-
|
|
532
|
-
|
|
566
|
+
Get or infer a title for this item, falling back to the filename, URL, description, or
|
|
567
|
+
finally body text. Optionally, include the last operation as a parenthetical at the end
|
|
568
|
+
of the title. Will use "Untitled" if all else fails.
|
|
533
569
|
"""
|
|
534
|
-
from kash.file_storage.store_filenames import parse_item_filename
|
|
535
|
-
|
|
536
570
|
# First special case: if we are pulling the title from the body header, check
|
|
537
571
|
# that.
|
|
538
572
|
if not self.title and pull_body_heading:
|
|
@@ -544,18 +578,12 @@ class Item:
|
|
|
544
578
|
if not self.title and self.url:
|
|
545
579
|
return abbrev_str(self.url, max_len)
|
|
546
580
|
|
|
547
|
-
|
|
548
|
-
# Use stem to drop suffix like .resource.docx etc in a title.
|
|
549
|
-
path = self.store_path or self.external_path or self.original_filename
|
|
550
|
-
if path:
|
|
551
|
-
path_name, _item_type, _format, _file_ext = parse_item_filename(Path(path).name)
|
|
552
|
-
else:
|
|
553
|
-
path_name = None
|
|
581
|
+
filename_stem = self.filename_stem()
|
|
554
582
|
|
|
555
583
|
# Use the title or the path if possible, falling back to description or even body text.
|
|
556
584
|
title_raw_text = (
|
|
557
585
|
self.title
|
|
558
|
-
or
|
|
586
|
+
or filename_stem
|
|
559
587
|
or self.description
|
|
560
588
|
or (not self.is_binary and self.abbrev_body(max_len))
|
|
561
589
|
or UNTITLED
|
|
@@ -586,6 +614,24 @@ class Item:
|
|
|
586
614
|
|
|
587
615
|
return final_text
|
|
588
616
|
|
|
617
|
+
def display_title(self) -> str:
|
|
618
|
+
"""
|
|
619
|
+
A display title for this item. Same as abbrev_title() but will fall back
|
|
620
|
+
to the filename if it is available.
|
|
621
|
+
"""
|
|
622
|
+
display_title = self.title
|
|
623
|
+
if not display_title and self.store_path:
|
|
624
|
+
display_title = Path(self.store_path).name
|
|
625
|
+
if not display_title:
|
|
626
|
+
display_title = self.abbrev_title()
|
|
627
|
+
return display_title
|
|
628
|
+
|
|
629
|
+
def abbrev_description(self, max_len: int = 1000) -> str:
|
|
630
|
+
"""
|
|
631
|
+
Get or infer description.
|
|
632
|
+
"""
|
|
633
|
+
return abbrev_on_words(html_to_plaintext(self.description or self.body or ""), max_len)
|
|
634
|
+
|
|
589
635
|
def body_heading(self) -> str | None:
|
|
590
636
|
"""
|
|
591
637
|
Get the first h1 or h2 heading from the body text, if present.
|
|
@@ -620,21 +666,6 @@ class Item:
|
|
|
620
666
|
"""
|
|
621
667
|
return bool(self.body and self.body.strip())
|
|
622
668
|
|
|
623
|
-
def slug_name(self, max_len: int = SLUG_MAX_LEN) -> str:
|
|
624
|
-
"""
|
|
625
|
-
Get a readable slugified version of the title or filename or content
|
|
626
|
-
appropriate for this item. May not be unique.
|
|
627
|
-
"""
|
|
628
|
-
title = self.abbrev_title(max_len=max_len, add_ops_suffix=True)
|
|
629
|
-
slug = slugify_snake(title)
|
|
630
|
-
return slug
|
|
631
|
-
|
|
632
|
-
def abbrev_description(self, max_len: int = 1000) -> str:
|
|
633
|
-
"""
|
|
634
|
-
Get or infer description.
|
|
635
|
-
"""
|
|
636
|
-
return abbrev_on_words(html_to_plaintext(self.description or self.body or ""), max_len)
|
|
637
|
-
|
|
638
669
|
def read_as_config(self) -> Any:
|
|
639
670
|
"""
|
|
640
671
|
If it is a config Item, return the parsed YAML.
|
|
@@ -653,8 +684,6 @@ class Item:
|
|
|
653
684
|
"""
|
|
654
685
|
if self.file_ext:
|
|
655
686
|
return self.file_ext
|
|
656
|
-
if self.is_binary and not self.file_ext:
|
|
657
|
-
raise ValueError(f"Binary Items must have a file extension: {self}")
|
|
658
687
|
inferred_ext = self.format and self.format.file_ext
|
|
659
688
|
if not inferred_ext:
|
|
660
689
|
raise ValueError(f"Cannot infer file extension for Item: {self}")
|
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
|
|
|
@@ -4,7 +4,6 @@ from pathlib import Path
|
|
|
4
4
|
|
|
5
5
|
from prettyfmt import fmt_path
|
|
6
6
|
|
|
7
|
-
from kash.utils.common.inflection import plural
|
|
8
7
|
from kash.utils.common.url import Locator, is_url
|
|
9
8
|
|
|
10
9
|
|
|
@@ -44,13 +43,6 @@ def fmt_loc(locator: str | Locator, resolve: bool = True) -> str:
|
|
|
44
43
|
return fmt_path(locator, resolve=resolve)
|
|
45
44
|
|
|
46
45
|
|
|
47
|
-
def fmt_count_items(count: int, name: str = "item") -> str:
|
|
48
|
-
"""
|
|
49
|
-
Format a count and a name as a pluralized phrase, e.g. "1 item" or "2 items".
|
|
50
|
-
"""
|
|
51
|
-
return f"{count} {plural(name, count)}" # pyright: ignore
|
|
52
|
-
|
|
53
|
-
|
|
54
46
|
## Tests
|
|
55
47
|
|
|
56
48
|
|