kash-shell 0.3.28__py3-none-any.whl → 0.3.33__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/chat.py +1 -0
- kash/actions/core/markdownify_html.py +4 -5
- kash/actions/core/minify_html.py +4 -5
- kash/actions/core/readability.py +1 -4
- kash/actions/core/render_as_html.py +10 -7
- kash/actions/core/save_sidematter_meta.py +47 -0
- kash/actions/core/show_webpage.py +2 -0
- kash/actions/core/zip_sidematter.py +47 -0
- kash/commands/base/basic_file_commands.py +7 -4
- kash/commands/base/diff_commands.py +6 -4
- kash/commands/base/files_command.py +31 -30
- kash/commands/base/general_commands.py +3 -2
- kash/commands/base/logs_commands.py +6 -4
- kash/commands/base/reformat_command.py +3 -2
- kash/commands/base/search_command.py +4 -3
- kash/commands/base/show_command.py +9 -7
- kash/commands/help/assistant_commands.py +6 -4
- kash/commands/help/help_commands.py +7 -4
- kash/commands/workspace/selection_commands.py +18 -16
- kash/commands/workspace/workspace_commands.py +39 -26
- kash/config/logger.py +1 -1
- kash/config/setup.py +2 -27
- kash/config/text_styles.py +1 -1
- kash/docs/markdown/topics/a1_what_is_kash.md +26 -18
- kash/docs/markdown/topics/a2_installation.md +3 -2
- kash/exec/action_decorators.py +7 -5
- kash/exec/action_exec.py +104 -53
- kash/exec/fetch_url_items.py +40 -11
- kash/exec/llm_transforms.py +14 -5
- kash/exec/preconditions.py +2 -2
- kash/exec/resolve_args.py +4 -1
- kash/exec/runtime_settings.py +3 -0
- kash/file_storage/file_store.py +108 -114
- kash/file_storage/item_file_format.py +91 -26
- kash/file_storage/item_id_index.py +128 -0
- kash/help/help_types.py +1 -1
- kash/llm_utils/llms.py +6 -1
- kash/local_server/local_server_commands.py +2 -1
- kash/mcp/mcp_server_commands.py +3 -2
- kash/mcp/mcp_server_routes.py +42 -12
- kash/model/actions_model.py +44 -32
- kash/model/compound_actions_model.py +4 -3
- kash/model/exec_model.py +33 -3
- kash/model/items_model.py +150 -60
- kash/model/params_model.py +4 -4
- kash/shell/output/shell_output.py +1 -2
- kash/utils/api_utils/gather_limited.py +2 -0
- kash/utils/api_utils/multitask_gather.py +74 -0
- kash/utils/common/s3_utils.py +108 -0
- kash/utils/common/url.py +16 -4
- kash/utils/file_formats/chat_format.py +7 -4
- kash/utils/file_utils/file_ext.py +1 -0
- kash/utils/file_utils/file_formats.py +4 -2
- kash/utils/file_utils/file_formats_model.py +12 -0
- kash/utils/text_handling/doc_normalization.py +1 -1
- kash/utils/text_handling/markdown_footnotes.py +224 -0
- kash/utils/text_handling/markdown_utils.py +532 -41
- kash/utils/text_handling/markdownify_utils.py +2 -1
- kash/web_content/web_fetch.py +2 -1
- kash/web_gen/templates/components/tooltip_scripts.js.jinja +186 -1
- kash/web_gen/templates/components/youtube_popover_scripts.js.jinja +223 -0
- kash/web_gen/templates/components/youtube_popover_styles.css.jinja +150 -0
- kash/web_gen/templates/content_styles.css.jinja +53 -1
- kash/web_gen/templates/youtube_webpage.html.jinja +47 -0
- kash/web_gen/webpage_render.py +103 -0
- kash/workspaces/workspaces.py +0 -5
- kash/xonsh_custom/custom_shell.py +4 -3
- {kash_shell-0.3.28.dist-info → kash_shell-0.3.33.dist-info}/METADATA +35 -26
- {kash_shell-0.3.28.dist-info → kash_shell-0.3.33.dist-info}/RECORD +72 -64
- kash/llm_utils/llm_features.py +0 -72
- kash/web_gen/simple_webpage.py +0 -55
- {kash_shell-0.3.28.dist-info → kash_shell-0.3.33.dist-info}/WHEEL +0 -0
- {kash_shell-0.3.28.dist-info → kash_shell-0.3.33.dist-info}/entry_points.txt +0 -0
- {kash_shell-0.3.28.dist-info → kash_shell-0.3.33.dist-info}/licenses/LICENSE +0 -0
|
@@ -100,8 +100,9 @@ def cache_list(media: bool = False, content: bool = False, workspace: str | None
|
|
|
100
100
|
"""
|
|
101
101
|
List the contents of the workspace media and content caches. By default lists both caches.
|
|
102
102
|
|
|
103
|
-
:
|
|
104
|
-
|
|
103
|
+
Args:
|
|
104
|
+
media: List media cache only.
|
|
105
|
+
content: List content cache only.
|
|
105
106
|
"""
|
|
106
107
|
if not media and not content:
|
|
107
108
|
media = True
|
|
@@ -131,9 +132,10 @@ def clear_cache(media: bool = False, content: bool = False, workspace: str | Non
|
|
|
131
132
|
"""
|
|
132
133
|
Clear the media and content caches. By default clears both caches.
|
|
133
134
|
|
|
134
|
-
:
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
Args:
|
|
136
|
+
media: Clear media cache only.
|
|
137
|
+
content: Clear content cache only.
|
|
138
|
+
global_cache: If in a workspace, clear the global caches, not the workspace caches.
|
|
137
139
|
"""
|
|
138
140
|
if not media and not content:
|
|
139
141
|
media = True
|
|
@@ -193,8 +195,9 @@ def history(max: int = 30, raw: bool = False) -> None:
|
|
|
193
195
|
|
|
194
196
|
For xonsh's built-in history, use `xhistory`.
|
|
195
197
|
|
|
196
|
-
:
|
|
197
|
-
|
|
198
|
+
Args:
|
|
199
|
+
max: Show at most the last `max` commands.
|
|
200
|
+
raw: Show raw command history by tailing the history file directly.
|
|
198
201
|
"""
|
|
199
202
|
# TODO: Customize this by time frame.
|
|
200
203
|
ws = current_ws()
|
|
@@ -395,15 +398,21 @@ def params(full: bool = False) -> None:
|
|
|
395
398
|
|
|
396
399
|
@kash_command
|
|
397
400
|
def import_item(
|
|
398
|
-
*files_or_urls: str,
|
|
401
|
+
*files_or_urls: str,
|
|
402
|
+
type: ItemType | None = None,
|
|
403
|
+
inplace: bool = False,
|
|
404
|
+
with_sidematter: bool = False,
|
|
399
405
|
) -> ShellResult:
|
|
400
406
|
"""
|
|
401
407
|
Add a file or URL resource to the workspace as an item.
|
|
402
408
|
|
|
403
|
-
:
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
409
|
+
Args:
|
|
410
|
+
inplace: If set and the item is already in the store, reimport the item,
|
|
411
|
+
adding or rewriting metadata frontmatter.
|
|
412
|
+
type: Change the item type. Usually items are auto-detected from the file
|
|
413
|
+
format (typically doc or resource), but you can override this with this option.
|
|
414
|
+
with_sidematter: If set, will copy any sidematter-format files (metadata/assets)
|
|
415
|
+
to the destination.
|
|
407
416
|
"""
|
|
408
417
|
if not files_or_urls:
|
|
409
418
|
raise InvalidInput("No files or URLs provided")
|
|
@@ -412,7 +421,9 @@ def import_item(
|
|
|
412
421
|
store_paths = []
|
|
413
422
|
|
|
414
423
|
locators = [resolve_locator_arg(r) for r in files_or_urls]
|
|
415
|
-
store_paths = ws.import_items(
|
|
424
|
+
store_paths = ws.import_items(
|
|
425
|
+
*locators, as_type=type, reimport=inplace, with_sidematter=with_sidematter
|
|
426
|
+
)
|
|
416
427
|
|
|
417
428
|
print_status(
|
|
418
429
|
"Imported %s %s:\n%s",
|
|
@@ -434,9 +445,10 @@ def save_clipboard(
|
|
|
434
445
|
"""
|
|
435
446
|
Import the contents of the OS-native clipboard as a new item in the workspace.
|
|
436
447
|
|
|
437
|
-
:
|
|
438
|
-
|
|
439
|
-
|
|
448
|
+
Args:
|
|
449
|
+
title: The title of the new item (default: "pasted_text").
|
|
450
|
+
type: The type of the new item (default: resource).
|
|
451
|
+
format: The format of the new item (default: plaintext).
|
|
440
452
|
"""
|
|
441
453
|
import pyperclip
|
|
442
454
|
|
|
@@ -537,8 +549,9 @@ def applicable_actions(*paths: str, brief: bool = False, all: bool = False) -> N
|
|
|
537
549
|
Show the actions that are applicable to the current selection.
|
|
538
550
|
This is a great command to use at any point to see what actions are available!
|
|
539
551
|
|
|
540
|
-
:
|
|
541
|
-
|
|
552
|
+
Args:
|
|
553
|
+
brief: Show only action names. Otherwise show actions and descriptions.
|
|
554
|
+
all: Include actions with no preconditions.
|
|
542
555
|
"""
|
|
543
556
|
store_paths = assemble_store_path_args(*paths)
|
|
544
557
|
ws = current_ws()
|
|
@@ -592,19 +605,19 @@ def applicable_actions(*paths: str, brief: bool = False, all: bool = False) -> N
|
|
|
592
605
|
|
|
593
606
|
|
|
594
607
|
@kash_command
|
|
595
|
-
def preconditions() -> None:
|
|
608
|
+
def preconditions(path: str | None = None) -> None:
|
|
596
609
|
"""
|
|
597
|
-
List all preconditions and if the current selection meets them.
|
|
610
|
+
List all preconditions and if the current selection or specified path meets them.
|
|
598
611
|
"""
|
|
599
612
|
|
|
600
613
|
ws = current_ws()
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
raise InvalidInput("No selection")
|
|
604
|
-
|
|
605
|
-
items = [ws.load(item) for item in selection]
|
|
614
|
+
input_paths = assemble_path_args(path)
|
|
615
|
+
items = [ws.load(item) for item in input_paths]
|
|
606
616
|
|
|
607
|
-
|
|
617
|
+
if path:
|
|
618
|
+
print_status("Precondition check for path:\n%s", fmt_lines([fmt_loc(path)]))
|
|
619
|
+
else:
|
|
620
|
+
print_status("Precondition check for selection:\n%s", fmt_lines(input_paths))
|
|
608
621
|
|
|
609
622
|
for precondition in get_all_preconditions().values():
|
|
610
623
|
satisfied = all(precondition(item) for item in items)
|
kash/config/logger.py
CHANGED
|
@@ -281,7 +281,7 @@ def _do_logging_setup(log_settings: LogSettings):
|
|
|
281
281
|
def prefix(line: str, emoji: str = "", warn_emoji: str = "") -> str:
|
|
282
282
|
prefix = task_stack_prefix_str()
|
|
283
283
|
emojis = f"{warn_emoji}{emoji}".strip()
|
|
284
|
-
return "
|
|
284
|
+
return "".join(filter(None, [prefix, emojis, line]))
|
|
285
285
|
|
|
286
286
|
|
|
287
287
|
def prefix_args(
|
kash/config/setup.py
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
from enum import Enum
|
|
2
1
|
from functools import cache
|
|
3
2
|
from pathlib import Path
|
|
4
|
-
from typing import Any
|
|
5
3
|
|
|
6
4
|
from clideps.env_vars.dotenv_utils import load_dotenv_paths
|
|
7
5
|
|
|
@@ -77,29 +75,6 @@ def kash_setup(
|
|
|
77
75
|
|
|
78
76
|
|
|
79
77
|
def _lib_setup():
|
|
80
|
-
from
|
|
81
|
-
from ruamel.yaml import Representer
|
|
78
|
+
from sidematter_format import register_default_yaml_representers
|
|
82
79
|
|
|
83
|
-
|
|
84
|
-
"""
|
|
85
|
-
Represent Enums as their values.
|
|
86
|
-
Helps make it easy to serialize enums to YAML everywhere.
|
|
87
|
-
We use the convention of storing enum values as readable strings.
|
|
88
|
-
"""
|
|
89
|
-
return dumper.represent_str(data.value)
|
|
90
|
-
|
|
91
|
-
add_default_yaml_customizer(
|
|
92
|
-
lambda yaml: yaml.representer.add_multi_representer(Enum, represent_enum)
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
# Maybe useful?
|
|
96
|
-
|
|
97
|
-
# from pydantic import BaseModel
|
|
98
|
-
|
|
99
|
-
# def represent_pydantic(dumper: Representer, data: BaseModel) -> Any:
|
|
100
|
-
# """Represent Pydantic models as YAML dictionaries."""
|
|
101
|
-
# return dumper.represent_dict(data.model_dump())
|
|
102
|
-
|
|
103
|
-
# add_default_yaml_customizer(
|
|
104
|
-
# lambda yaml: yaml.representer.add_multi_representer(BaseModel, represent_pydantic)
|
|
105
|
-
# )
|
|
80
|
+
register_default_yaml_representers()
|
kash/config/text_styles.py
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
## Hello!
|
|
2
|
+
|
|
3
|
+
If you’re seeing this, you there’s a good chance I shared it with you for feedback.
|
|
4
|
+
Thank you for checking out Kash.
|
|
5
|
+
|
|
6
|
+
It’s new, the result of some experimentation over the past few months.
|
|
7
|
+
I like a lot of things about it but it isn’t mature and I’d love your help to make it
|
|
8
|
+
more usable. If you try it please **let me know** what works and what doesn’t work.
|
|
9
|
+
Or if you just don’t get it, where you lost interest or got stuck.
|
|
10
|
+
My contact info is at [github.com/jlevy](https://github.com/jlevy) or [follow or DM
|
|
11
|
+
me](https://x.com/ojoshe) (I’m fastest on Twitter DMs).
|
|
12
|
+
Thank you. :)
|
|
13
|
+
|
|
1
14
|
## What is Kash?
|
|
2
15
|
|
|
3
16
|
> “*Simple should be simple.
|
|
@@ -12,9 +25,15 @@ It operates on “items” such as URLs, files, or Markdown notes within a works
|
|
|
12
25
|
directory.
|
|
13
26
|
|
|
14
27
|
You can use Kash as an **interactive, AI-native command-line** shell for practical
|
|
15
|
-
knowledge tasks.
|
|
16
|
-
|
|
17
|
-
|
|
28
|
+
knowledge tasks.
|
|
29
|
+
|
|
30
|
+
But it’s actually not just a shell, and you can skip the shell entirely.
|
|
31
|
+
It’s really simply **a Python library** that lets you convert a simple Python function
|
|
32
|
+
into “actions” that work in a clean way on plain files in a workspace.
|
|
33
|
+
An action is also an MCP tool, so it integrates with other tools like Anthropic Desktop
|
|
34
|
+
or Cursor.
|
|
35
|
+
|
|
36
|
+
So basically, it gives a unified way to use the shell, Python functions, and MCP tools.
|
|
18
37
|
|
|
19
38
|
It’s new and still has some rough edges, but it’s now working well enough it is feeling
|
|
20
39
|
quite powerful. It now serves as a replacement for my usual shell (previously bash or
|
|
@@ -73,10 +92,10 @@ quick to install via uv.
|
|
|
73
92
|
- **Support for any API:** Kash is tool agnostic and runs locally, on file inputs in
|
|
74
93
|
simple formats, so you own and manage your data and workspaces however you like.
|
|
75
94
|
You can use it with any models or APIs you like, and is already set up to use the APIs
|
|
76
|
-
of **OpenAI GPT-
|
|
77
|
-
Grok**, **Mistral**, **Groq (Llama, Qwen, Deepseek)** (via
|
|
78
|
-
**Perplexity**, **Firecrawl**, **Exa**, and any Python
|
|
79
|
-
There is also some experimental support for **LlamaIndex** and **ChromaDB**.
|
|
95
|
+
of **OpenAI** (GPT-5 is now the default model), **Anthropic Claude**, **Google
|
|
96
|
+
Gemini**, **xAI Grok**, **Mistral**, **Groq (Llama, Qwen, Deepseek)** (via
|
|
97
|
+
**LiteLLM**), **Deepgram**, **Perplexity**, **Firecrawl**, **Exa**, and any Python
|
|
98
|
+
libraries. There is also some experimental support for **LlamaIndex** and **ChromaDB**.
|
|
80
99
|
|
|
81
100
|
- **MCP support:** Finally, an action is also an **MCP tool server** so you can use it
|
|
82
101
|
in any MCP client, like Anthropic Desktop or Cursor.
|
|
@@ -110,14 +129,3 @@ I’ve separately built a new desktop terminal app, Kerm, which adds support for
|
|
|
110
129
|
“Kerm codes” protocol for such visual components, encoded as OSC codes then rendered in
|
|
111
130
|
the terminal. Because Kash supports these codes, as this develops you will get the
|
|
112
131
|
visuals of a web app layered on the flexibility of a text-based terminal.
|
|
113
|
-
|
|
114
|
-
### Is Kash Mature?
|
|
115
|
-
|
|
116
|
-
It’s the result of a couple months of coding and experimentation, and it’s still in
|
|
117
|
-
progress and has rough edges.
|
|
118
|
-
Please help me make it better by sharing your ideas and feedback!
|
|
119
|
-
It’s easiest to DM me at [twitter.com/ojoshe](https://x.com/ojoshe).
|
|
120
|
-
My contact info is at [github.com/jlevy](https://github.com/jlevy).
|
|
121
|
-
|
|
122
|
-
[**Please follow or DM me**](https://x.com/ojoshe) for future updates or if you have
|
|
123
|
-
ideas, feedback, or use cases for Kash!
|
|
@@ -60,8 +60,9 @@ These are for `kash-media` but you can use a `kash-shell` for a more basic setup
|
|
|
60
60
|
|
|
61
61
|
```shell
|
|
62
62
|
sudo apt-get update
|
|
63
|
-
sudo apt-get install -y libgl1 ffmpeg libmagic-dev
|
|
64
|
-
|
|
63
|
+
sudo apt-get install -y libgl1 ffmpeg libmagic-dev imagemagick bat ripgrep hexyl
|
|
64
|
+
|
|
65
|
+
# Or for additional command-line tools, pixi is better on Ubuntu:
|
|
65
66
|
curl -fsSL https://pixi.sh/install.sh | sh
|
|
66
67
|
. ~/.bashrc
|
|
67
68
|
pixi global install ripgrep bat eza hexyl imagemagick zoxide
|
kash/exec/action_decorators.py
CHANGED
|
@@ -31,12 +31,13 @@ from kash.model.actions_model import (
|
|
|
31
31
|
ParamSource,
|
|
32
32
|
TitleTemplate,
|
|
33
33
|
)
|
|
34
|
-
from kash.model.exec_model import ExecContext
|
|
34
|
+
from kash.model.exec_model import ActionContext, ExecContext
|
|
35
35
|
from kash.model.items_model import Item, ItemType
|
|
36
36
|
from kash.model.params_model import Param, ParamDeclarations, TypedParamValues
|
|
37
37
|
from kash.model.preconditions_model import Precondition
|
|
38
38
|
from kash.utils.common.function_inspect import FuncParam, inspect_function_params
|
|
39
39
|
from kash.utils.errors import InvalidDefinition
|
|
40
|
+
from kash.utils.file_utils.file_formats_model import Format
|
|
40
41
|
|
|
41
42
|
log = get_logger(__name__)
|
|
42
43
|
|
|
@@ -204,6 +205,7 @@ def kash_action(
|
|
|
204
205
|
arg_type: ArgType = ArgType.Locator,
|
|
205
206
|
expected_args: ArgCount = ONE_ARG,
|
|
206
207
|
output_type: ItemType = ItemType.doc,
|
|
208
|
+
output_format: Format | None = None,
|
|
207
209
|
expected_outputs: ArgCount = ONE_ARG,
|
|
208
210
|
params: ParamDeclarations = (),
|
|
209
211
|
run_per_item: bool | None = None,
|
|
@@ -318,6 +320,7 @@ def kash_action(
|
|
|
318
320
|
self.arg_type = arg_type
|
|
319
321
|
self.uses_selection = uses_selection
|
|
320
322
|
self.output_type = output_type
|
|
323
|
+
self.output_format = output_format
|
|
321
324
|
self.interactive_input = interactive_input
|
|
322
325
|
self.live_output = live_output
|
|
323
326
|
self.mcp_tool = mcp_tool
|
|
@@ -328,7 +331,7 @@ def kash_action(
|
|
|
328
331
|
super().__post_init__()
|
|
329
332
|
|
|
330
333
|
@override
|
|
331
|
-
def run(self, input: ActionInput, context:
|
|
334
|
+
def run(self, input: ActionInput, context: ActionContext) -> ActionResult:
|
|
332
335
|
# Map the final, current actions param values back to the function parameters.
|
|
333
336
|
pos_args: list[Any] = []
|
|
334
337
|
kw_args: dict[str, Any] = {}
|
|
@@ -397,9 +400,8 @@ def kash_action(
|
|
|
397
400
|
context = ExecContext(action, current_runtime_settings())
|
|
398
401
|
|
|
399
402
|
# Run the action.
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
return result
|
|
403
|
+
result_with_paths = run_action_with_caching(context, action_input)
|
|
404
|
+
return result_with_paths.result
|
|
403
405
|
|
|
404
406
|
if is_simple_func:
|
|
405
407
|
# Need to convert back to a SimpleActionFunction.
|
kash/exec/action_exec.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import time
|
|
2
|
-
from dataclasses import replace
|
|
2
|
+
from dataclasses import dataclass, replace
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
5
|
from prettyfmt import fmt_lines, fmt_path, plural
|
|
@@ -23,14 +23,16 @@ from kash.model.actions_model import (
|
|
|
23
23
|
ExecContext,
|
|
24
24
|
PathOpType,
|
|
25
25
|
)
|
|
26
|
-
from kash.model.exec_model import RuntimeSettings
|
|
26
|
+
from kash.model.exec_model import ActionContext, RuntimeSettings
|
|
27
27
|
from kash.model.items_model import Item, State
|
|
28
28
|
from kash.model.operations_model import Input, Operation, Source
|
|
29
29
|
from kash.model.params_model import ALL_COMMON_PARAMS, GLOBAL_PARAMS, RawParamValues
|
|
30
30
|
from kash.model.paths_model import StorePath
|
|
31
31
|
from kash.shell.output.shell_output import PrintHooks
|
|
32
|
+
from kash.utils.common.s3_utils import get_s3_parent_folder, s3_sync_to_folder
|
|
32
33
|
from kash.utils.common.task_stack import task_stack
|
|
33
34
|
from kash.utils.common.type_utils import not_none
|
|
35
|
+
from kash.utils.common.url import Url, is_s3_url
|
|
34
36
|
from kash.utils.errors import ContentError, InvalidOutput, get_nonfatal_exceptions
|
|
35
37
|
from kash.workspaces import Selection, current_ws
|
|
36
38
|
|
|
@@ -42,6 +44,7 @@ def prepare_action_input(*input_args: CommandArg, refetch: bool = False) -> Acti
|
|
|
42
44
|
Prepare input args, which may be URLs or paths, into items that correspond to
|
|
43
45
|
URL or file resources, either finding them in the workspace or importing them.
|
|
44
46
|
Also fetches metadata for URLs if they don't already have title and description.
|
|
47
|
+
Automatically imports any URLs and copies any sidematter-format files (metadata/assets).
|
|
45
48
|
"""
|
|
46
49
|
from kash.exec.fetch_url_items import fetch_url_item_content
|
|
47
50
|
|
|
@@ -49,7 +52,7 @@ def prepare_action_input(*input_args: CommandArg, refetch: bool = False) -> Acti
|
|
|
49
52
|
|
|
50
53
|
# Ensure input items are already saved in the workspace and load the corresponding items.
|
|
51
54
|
# This also imports any URLs.
|
|
52
|
-
input_items = [ws.import_and_load(arg) for arg in input_args]
|
|
55
|
+
input_items = [ws.import_and_load(arg, with_sidematter=True) for arg in input_args]
|
|
53
56
|
|
|
54
57
|
# URLs should have metadata like a title and be valid, so we fetch them.
|
|
55
58
|
if input_items:
|
|
@@ -109,9 +112,7 @@ def log_action(action: Action, action_input: ActionInput, operation: Operation):
|
|
|
109
112
|
log.info("Input items are:\n%s", fmt_lines(action_input.items))
|
|
110
113
|
|
|
111
114
|
|
|
112
|
-
def check_for_existing_result(
|
|
113
|
-
context: ExecContext, action_input: ActionInput, operation: Operation
|
|
114
|
-
) -> ActionResult | None:
|
|
115
|
+
def check_for_existing_result(context: ActionContext) -> ActionResult | None:
|
|
115
116
|
"""
|
|
116
117
|
Check if we already have the results for this operation (same action and inputs)
|
|
117
118
|
If so return it, unless rerun is requested, in which case we just log that the results
|
|
@@ -126,7 +127,7 @@ def check_for_existing_result(
|
|
|
126
127
|
|
|
127
128
|
# Check if a previous run already produced the result.
|
|
128
129
|
# To do this we preassemble outputs.
|
|
129
|
-
preassembled_result = action.
|
|
130
|
+
preassembled_result = action.preassemble_result(context)
|
|
130
131
|
if preassembled_result:
|
|
131
132
|
# Check if these items already exist, with last_operation matching action and input fingerprints.
|
|
132
133
|
already_present = [ws.find_by_id(item) for item in preassembled_result.items]
|
|
@@ -155,7 +156,7 @@ def check_for_existing_result(
|
|
|
155
156
|
|
|
156
157
|
|
|
157
158
|
def run_action_operation(
|
|
158
|
-
context:
|
|
159
|
+
context: ActionContext,
|
|
159
160
|
action_input: ActionInput,
|
|
160
161
|
operation: Operation,
|
|
161
162
|
) -> ActionResult:
|
|
@@ -183,7 +184,7 @@ def run_action_operation(
|
|
|
183
184
|
this_op = replace(operation, arguments=[operation.arguments[i]])
|
|
184
185
|
else:
|
|
185
186
|
this_op = operation
|
|
186
|
-
item.
|
|
187
|
+
item.update_source(Source(operation=this_op, output_num=i, cacheable=action.cacheable))
|
|
187
188
|
|
|
188
189
|
# Override the state if appropriate (this handles marking items as transient).
|
|
189
190
|
if settings.override_state:
|
|
@@ -205,7 +206,7 @@ class SkipItem(Exception):
|
|
|
205
206
|
"""
|
|
206
207
|
|
|
207
208
|
|
|
208
|
-
def _run_for_each_item(context:
|
|
209
|
+
def _run_for_each_item(context: ActionContext, input: ActionInput) -> ActionResult:
|
|
209
210
|
"""
|
|
210
211
|
Helper to process each input item. If non-fatal errors are encountered on any item,
|
|
211
212
|
they are reported and processing continues with the next item.
|
|
@@ -340,69 +341,116 @@ def save_action_result(
|
|
|
340
341
|
return result_store_paths, archived_store_paths
|
|
341
342
|
|
|
342
343
|
|
|
344
|
+
@dataclass(frozen=True)
|
|
345
|
+
class ResultWithPaths:
|
|
346
|
+
"""
|
|
347
|
+
Result of an action, including the store paths of any S3 items created.
|
|
348
|
+
"""
|
|
349
|
+
|
|
350
|
+
result: ActionResult
|
|
351
|
+
result_paths: list[StorePath]
|
|
352
|
+
archived_paths: list[StorePath]
|
|
353
|
+
s3_paths: list[Url]
|
|
354
|
+
|
|
355
|
+
|
|
343
356
|
def run_action_with_caching(
|
|
344
|
-
|
|
345
|
-
) ->
|
|
357
|
+
exec_context: ExecContext, action_input: ActionInput
|
|
358
|
+
) -> ResultWithPaths:
|
|
346
359
|
"""
|
|
347
360
|
Run an action, including validation, only rerunning if `rerun` requested or
|
|
348
361
|
result is not already present. Returns the result, the store paths of the
|
|
349
362
|
result items, and the store paths of any archived items.
|
|
350
363
|
|
|
364
|
+
Also handles optional S3 syncing if the input was from S3.
|
|
365
|
+
|
|
351
366
|
Note: Mutates the input but only to add `context` to each item.
|
|
352
367
|
"""
|
|
353
|
-
action =
|
|
354
|
-
settings =
|
|
368
|
+
action = exec_context.action
|
|
369
|
+
settings = exec_context.settings
|
|
355
370
|
ws = settings.workspace
|
|
356
371
|
|
|
357
|
-
#
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
372
|
+
# If the input is from S3, we note the parent folder to copy the output back to.
|
|
373
|
+
s3_parent_folder = None
|
|
374
|
+
if exec_context.settings.sync_to_s3 and action_input.items and action_input.items[0].url:
|
|
375
|
+
url = action_input.items[0].url
|
|
376
|
+
if url and is_s3_url(url):
|
|
377
|
+
s3_parent_folder = get_s3_parent_folder(url)
|
|
361
378
|
|
|
362
379
|
# Assemble the operation and validate the action input.
|
|
363
|
-
operation = validate_action_input(
|
|
380
|
+
operation = validate_action_input(exec_context, ws, action, action_input)
|
|
364
381
|
|
|
365
382
|
# Log what we're about to run.
|
|
366
383
|
log_action(action, action_input, operation)
|
|
367
384
|
|
|
368
|
-
#
|
|
369
|
-
|
|
385
|
+
# Consolidate all the context.
|
|
386
|
+
context = ActionContext(
|
|
387
|
+
exec_context=exec_context, operation=operation, action_input=action_input
|
|
388
|
+
)
|
|
370
389
|
|
|
371
|
-
|
|
372
|
-
#
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
archived_store_paths = []
|
|
390
|
+
try:
|
|
391
|
+
# Hack to add the context to each item.
|
|
392
|
+
# We do this before cache preassemble check.
|
|
393
|
+
action_input.set_context(context)
|
|
376
394
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
"%s Skipped: `%s` completed with %s %s",
|
|
380
|
-
EMOJI_SKIP,
|
|
381
|
-
action.name,
|
|
382
|
-
len(result.items),
|
|
383
|
-
plural("item", len(result.items)),
|
|
384
|
-
)
|
|
385
|
-
else:
|
|
386
|
-
# Run it!
|
|
387
|
-
result = run_action_operation(context, action_input, operation)
|
|
388
|
-
result_store_paths, archived_store_paths = save_action_result(
|
|
389
|
-
ws,
|
|
390
|
-
result,
|
|
391
|
-
action_input,
|
|
392
|
-
as_tmp=settings.tmp_output,
|
|
393
|
-
no_format=settings.no_format,
|
|
394
|
-
)
|
|
395
|
+
# Check if a previous run already produced the result.
|
|
396
|
+
existing_result = check_for_existing_result(context)
|
|
395
397
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
398
|
+
if existing_result and not settings.rerun:
|
|
399
|
+
# Use the cached result.
|
|
400
|
+
result = existing_result
|
|
401
|
+
result_store_paths = [StorePath(not_none(item.store_path)) for item in result.items]
|
|
402
|
+
archived_store_paths = []
|
|
403
|
+
|
|
404
|
+
PrintHooks.before_done_message()
|
|
405
|
+
log.message(
|
|
406
|
+
"%s Skipped: `%s` completed with %s %s",
|
|
407
|
+
EMOJI_SKIP,
|
|
408
|
+
action.name,
|
|
409
|
+
len(result.items),
|
|
410
|
+
plural("item", len(result.items)),
|
|
411
|
+
)
|
|
412
|
+
else:
|
|
413
|
+
# Run it!
|
|
414
|
+
result = run_action_operation(context, action_input, operation)
|
|
415
|
+
result_store_paths, archived_store_paths = save_action_result(
|
|
416
|
+
ws,
|
|
417
|
+
result,
|
|
418
|
+
action_input,
|
|
419
|
+
as_tmp=settings.tmp_output,
|
|
420
|
+
no_format=settings.no_format,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
PrintHooks.before_done_message()
|
|
424
|
+
log.message(
|
|
425
|
+
"%s Action: `%s` completed with %s %s",
|
|
426
|
+
EMOJI_SUCCESS,
|
|
427
|
+
action.name,
|
|
428
|
+
len(result.items),
|
|
429
|
+
plural("item", len(result.items)),
|
|
430
|
+
)
|
|
431
|
+
finally:
|
|
432
|
+
action_input.clear_context()
|
|
433
|
+
|
|
434
|
+
# If the action created an S3 item, we copy it back to the same S3 parent folder.
|
|
435
|
+
# Only do this for the first result, for simplicity.
|
|
436
|
+
s3_urls: list[Url] = []
|
|
437
|
+
if s3_parent_folder and len(result_store_paths) > 0:
|
|
438
|
+
log.warning(
|
|
439
|
+
"Source was an S3 path so syncing result S3: %s -> %s",
|
|
440
|
+
result_store_paths[0],
|
|
441
|
+
s3_parent_folder,
|
|
442
|
+
)
|
|
443
|
+
s3_urls = s3_sync_to_folder(
|
|
444
|
+
result_store_paths[0], s3_parent_folder, include_sidematter=True
|
|
403
445
|
)
|
|
446
|
+
log.message("Synced result to S3:\n%s", fmt_lines(s3_urls))
|
|
404
447
|
|
|
405
|
-
return
|
|
448
|
+
return ResultWithPaths(
|
|
449
|
+
result=result,
|
|
450
|
+
result_paths=result_store_paths,
|
|
451
|
+
archived_paths=archived_store_paths,
|
|
452
|
+
s3_paths=s3_urls,
|
|
453
|
+
)
|
|
406
454
|
|
|
407
455
|
|
|
408
456
|
def run_action_with_shell_context(
|
|
@@ -480,7 +528,10 @@ def run_action_with_shell_context(
|
|
|
480
528
|
input = prepare_action_input(*args, refetch=refetch)
|
|
481
529
|
|
|
482
530
|
# Finally, run the action.
|
|
483
|
-
|
|
531
|
+
result_with_paths = run_action_with_caching(context, input)
|
|
532
|
+
result = result_with_paths.result
|
|
533
|
+
result_store_paths = result_with_paths.result_paths
|
|
534
|
+
archived_store_paths = result_with_paths.archived_paths
|
|
484
535
|
|
|
485
536
|
# Implement any path operations from the output and/or select the final output
|
|
486
537
|
if not internal_call:
|