kash-shell 0.3.9__py3-none-any.whl → 0.3.11__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 (151) hide show
  1. kash/actions/__init__.py +4 -4
  2. kash/actions/core/format_markdown_template.py +2 -5
  3. kash/actions/core/markdownify.py +7 -6
  4. kash/actions/core/readability.py +7 -6
  5. kash/actions/core/render_as_html.py +37 -0
  6. kash/actions/core/show_webpage.py +6 -11
  7. kash/actions/core/strip_html.py +2 -6
  8. kash/actions/core/tabbed_webpage_config.py +31 -0
  9. kash/actions/core/{webpage_generate.py → tabbed_webpage_generate.py} +5 -4
  10. kash/commands/__init__.py +8 -20
  11. kash/commands/base/basic_file_commands.py +15 -0
  12. kash/commands/base/debug_commands.py +13 -0
  13. kash/commands/base/files_command.py +28 -10
  14. kash/commands/base/general_commands.py +21 -16
  15. kash/commands/base/logs_commands.py +4 -2
  16. kash/commands/base/model_commands.py +8 -8
  17. kash/commands/base/search_command.py +3 -2
  18. kash/commands/base/show_command.py +5 -3
  19. kash/commands/extras/parse_uv_lock.py +186 -0
  20. kash/commands/help/doc_commands.py +2 -31
  21. kash/commands/help/welcome.py +33 -0
  22. kash/commands/workspace/selection_commands.py +11 -6
  23. kash/commands/workspace/workspace_commands.py +19 -17
  24. kash/config/colors.py +3 -1
  25. kash/config/env_settings.py +14 -1
  26. kash/config/init.py +2 -2
  27. kash/config/logger.py +59 -56
  28. kash/config/logger_basic.py +3 -3
  29. kash/config/settings.py +116 -57
  30. kash/config/setup.py +28 -12
  31. kash/config/text_styles.py +3 -13
  32. kash/docs/load_api_docs.py +2 -1
  33. kash/docs/markdown/topics/a3_getting_started.md +3 -2
  34. kash/{concepts → embeddings}/text_similarity.py +2 -2
  35. kash/exec/__init__.py +20 -3
  36. kash/exec/action_decorators.py +24 -10
  37. kash/exec/action_exec.py +41 -23
  38. kash/exec/action_registry.py +13 -48
  39. kash/exec/command_registry.py +2 -1
  40. kash/exec/fetch_url_metadata.py +4 -6
  41. kash/exec/importing.py +56 -0
  42. kash/exec/llm_transforms.py +12 -10
  43. kash/exec/precondition_registry.py +2 -1
  44. kash/exec/preconditions.py +22 -1
  45. kash/exec/resolve_args.py +4 -0
  46. kash/exec/shell_callable_action.py +33 -19
  47. kash/file_storage/file_store.py +42 -27
  48. kash/file_storage/item_file_format.py +5 -2
  49. kash/file_storage/metadata_dirs.py +11 -2
  50. kash/help/assistant.py +1 -1
  51. kash/help/assistant_instructions.py +2 -1
  52. kash/help/function_param_info.py +1 -1
  53. kash/help/help_embeddings.py +2 -2
  54. kash/help/help_printing.py +7 -11
  55. kash/llm_utils/clean_headings.py +1 -1
  56. kash/llm_utils/llm_api_keys.py +4 -4
  57. kash/llm_utils/llm_features.py +68 -0
  58. kash/llm_utils/llm_messages.py +1 -2
  59. kash/llm_utils/llm_names.py +1 -1
  60. kash/llm_utils/llms.py +8 -3
  61. kash/local_server/__init__.py +5 -2
  62. kash/local_server/local_server.py +8 -5
  63. kash/local_server/local_server_commands.py +2 -2
  64. kash/local_server/local_server_routes.py +1 -7
  65. kash/local_server/local_url_formatters.py +1 -1
  66. kash/mcp/__init__.py +5 -2
  67. kash/mcp/mcp_cli.py +5 -5
  68. kash/mcp/mcp_server_commands.py +5 -5
  69. kash/mcp/mcp_server_routes.py +5 -5
  70. kash/mcp/mcp_server_sse.py +4 -2
  71. kash/media_base/media_cache.py +8 -8
  72. kash/media_base/media_services.py +1 -1
  73. kash/media_base/media_tools.py +6 -6
  74. kash/media_base/services/local_file_media.py +2 -2
  75. kash/media_base/{speech_transcription.py → transcription_deepgram.py} +25 -110
  76. kash/media_base/transcription_format.py +73 -0
  77. kash/media_base/transcription_whisper.py +38 -0
  78. kash/model/__init__.py +73 -5
  79. kash/model/actions_model.py +38 -4
  80. kash/model/concept_model.py +30 -0
  81. kash/model/items_model.py +115 -32
  82. kash/model/params_model.py +24 -0
  83. kash/shell/completions/completion_scoring.py +37 -5
  84. kash/shell/output/kerm_codes.py +1 -2
  85. kash/shell/output/shell_formatting.py +14 -4
  86. kash/shell/shell_main.py +2 -2
  87. kash/shell/utils/exception_printing.py +6 -0
  88. kash/shell/utils/native_utils.py +26 -20
  89. kash/shell/utils/shell_function_wrapper.py +15 -15
  90. kash/text_handling/custom_sliding_transforms.py +12 -4
  91. kash/text_handling/doc_normalization.py +6 -2
  92. kash/text_handling/markdown_render.py +118 -0
  93. kash/text_handling/markdown_utils.py +226 -0
  94. kash/utils/common/function_inspect.py +360 -110
  95. kash/utils/common/import_utils.py +12 -3
  96. kash/utils/common/type_utils.py +0 -29
  97. kash/utils/common/url.py +27 -3
  98. kash/utils/errors.py +6 -0
  99. kash/utils/file_utils/file_ext.py +4 -0
  100. kash/utils/file_utils/file_formats.py +2 -2
  101. kash/utils/file_utils/file_formats_model.py +20 -1
  102. kash/web_content/dir_store.py +1 -2
  103. kash/web_content/file_cache_utils.py +37 -10
  104. kash/web_content/file_processing.py +68 -0
  105. kash/web_content/local_file_cache.py +12 -9
  106. kash/web_content/web_extract.py +8 -3
  107. kash/web_content/web_fetch.py +12 -4
  108. kash/web_gen/__init__.py +0 -4
  109. kash/web_gen/simple_webpage.py +52 -0
  110. kash/web_gen/tabbed_webpage.py +24 -14
  111. kash/web_gen/template_render.py +37 -2
  112. kash/web_gen/templates/base_styles.css.jinja +169 -43
  113. kash/web_gen/templates/base_webpage.html.jinja +110 -45
  114. kash/web_gen/templates/content_styles.css.jinja +4 -2
  115. kash/web_gen/templates/item_view.html.jinja +49 -39
  116. kash/web_gen/templates/simple_webpage.html.jinja +24 -0
  117. kash/web_gen/templates/tabbed_webpage.html.jinja +42 -33
  118. kash/workspaces/__init__.py +15 -2
  119. kash/workspaces/selections.py +18 -3
  120. kash/workspaces/source_items.py +0 -1
  121. kash/workspaces/workspaces.py +5 -11
  122. kash/xonsh_custom/command_nl_utils.py +40 -19
  123. kash/xonsh_custom/custom_shell.py +43 -11
  124. kash/xonsh_custom/customize_prompt.py +39 -21
  125. kash/xonsh_custom/load_into_xonsh.py +22 -25
  126. kash/xonsh_custom/shell_load_commands.py +2 -2
  127. kash/xonsh_custom/xonsh_completers.py +2 -249
  128. kash/xonsh_custom/xonsh_keybindings.py +282 -0
  129. kash/xonsh_custom/xonsh_modern_tools.py +3 -3
  130. kash/xontrib/kash_extension.py +5 -6
  131. {kash_shell-0.3.9.dist-info → kash_shell-0.3.11.dist-info}/METADATA +10 -8
  132. {kash_shell-0.3.9.dist-info → kash_shell-0.3.11.dist-info}/RECORD +137 -136
  133. kash/actions/core/webpage_config.py +0 -21
  134. kash/concepts/concept_formats.py +0 -23
  135. kash/shell/clideps/api_keys.py +0 -100
  136. kash/shell/clideps/dotenv_setup.py +0 -115
  137. kash/shell/clideps/dotenv_utils.py +0 -98
  138. kash/shell/clideps/pkg_deps.py +0 -257
  139. kash/shell/clideps/platforms.py +0 -11
  140. kash/shell/clideps/terminal_features.py +0 -56
  141. kash/shell/utils/osc_utils.py +0 -95
  142. kash/shell/utils/terminal_images.py +0 -133
  143. kash/text_handling/markdown_util.py +0 -167
  144. kash/utils/common/atomic_var.py +0 -171
  145. kash/utils/common/string_replace.py +0 -93
  146. kash/utils/common/string_template.py +0 -101
  147. /kash/{concepts → embeddings}/cosine.py +0 -0
  148. /kash/{concepts → embeddings}/embeddings.py +0 -0
  149. {kash_shell-0.3.9.dist-info → kash_shell-0.3.11.dist-info}/WHEEL +0 -0
  150. {kash_shell-0.3.9.dist-info → kash_shell-0.3.11.dist-info}/entry_points.txt +0 -0
  151. {kash_shell-0.3.9.dist-info → kash_shell-0.3.11.dist-info}/licenses/LICENSE +0 -0
@@ -2,111 +2,172 @@ import inspect
2
2
  import types
3
3
  from collections.abc import Callable
4
4
  from dataclasses import dataclass
5
+ from enum import Enum
5
6
  from inspect import Parameter
6
- from typing import Any, Union, cast, get_args, get_origin # pyright: ignore
7
+ from typing import Any, Union, cast, get_args, get_origin # pyright: ignore[reportDeprecated]
7
8
 
8
- NO_DEFAULT = Parameter.empty
9
+ NO_DEFAULT = Parameter.empty # Alias for clarity
10
+
11
+ ParameterKind = Parameter.POSITIONAL_ONLY.__class__
9
12
 
10
13
 
11
14
  @dataclass(frozen=True)
12
15
  class FuncParam:
16
+ """
17
+ Hold structured information about a single function parameter.
18
+ """
19
+
13
20
  name: str
14
- type: type | None
15
- default: Any
16
- position: int | None
17
- is_varargs: bool
21
+ kind: ParameterKind # e.g., POSITIONAL_OR_KEYWORD
22
+ annotation: Any # The raw type annotation (can be Parameter.empty)
23
+ default: Any # The raw default value (can be Parameter.empty)
24
+ position: int | None # Position index for positional args, None for keyword-only
18
25
 
19
- @property
20
- def is_positional(self) -> bool:
21
- """Is this a purely positional parameter, with no default value?"""
22
- return self.position is not None and not self.has_default
26
+ # Resolved type information
27
+ # This is the concrete, simplified main type (e.g., str from str | None, list from list[int])
28
+ effective_type: type | None
29
+ inner_type: type | None # For collections, the type of elements (e.g., str from list[str])
30
+ is_explicitly_optional: bool # True if original annotation was Optional[X] or X | None
23
31
 
24
32
  @property
25
33
  def has_default(self) -> bool:
26
34
  """Does this parameter have a default value?"""
27
35
  return self.default != NO_DEFAULT
28
36
 
37
+ @property
38
+ def is_pure_positional(self) -> bool:
39
+ """Is this a plain or varargs positional parameter, with no default value?"""
40
+ return self.position is not None and not self.has_default
41
+
42
+ @property
43
+ def is_positional_only(self) -> bool:
44
+ """Is a true positional parameter, with no default value."""
45
+ return self.kind == Parameter.POSITIONAL_ONLY
29
46
 
30
- def inspect_function_params(func: Callable[..., Any], unwrap: bool = True) -> list[FuncParam]:
47
+ @property
48
+ def is_positional_or_keyword(self) -> bool:
49
+ return self.kind == Parameter.POSITIONAL_OR_KEYWORD
50
+
51
+ @property
52
+ def is_varargs(self) -> bool:
53
+ return self.kind == Parameter.VAR_POSITIONAL or self.kind == Parameter.VAR_KEYWORD
54
+
55
+ @property
56
+ def is_keyword_only(self) -> bool:
57
+ return self.kind == Parameter.KEYWORD_ONLY
58
+
59
+
60
+ def _resolve_type_details(annotation: Any) -> tuple[type | None, type | None, bool]:
31
61
  """
32
- Get names and types of parameters on a Python function. Just a convenience wrapper
33
- around `inspect.signature` to give parameter names, types, and default values.
34
-
35
- It also parses `Optional` types to return the underlying type and unwraps
36
- decorated functions to get the underlying parameters. Returns a list of
37
- `FuncParam` values.
38
-
39
- Note that technically, parameters are of 5 types, positional-only,
40
- positional-or-keyword, keyword-only, varargs, or varargs-keyword, which is what
41
- `inspect` tells you, but in practice you typically just want to know
42
- `FuncParam.is_positional()`, i.e. if it is a pure positional parameter (no default)
43
- and not a keyword (with a default).
62
+ Resolves an annotation into (effective_type, inner_type, is_explicitly_optional).
63
+
64
+ - effective_type: The main type (e.g., str from Optional[str], list from list[int]).
65
+ - inner_type: The type of elements if effective_type is a collection
66
+ (e.g., int from list[int]). For non-collection or unparameterized
67
+ collection, this is None.
68
+ - is_explicitly_optional: True if the annotation was X | None or Optional[X].
44
69
  """
45
- unwrapped = inspect.unwrap(func) if unwrap else func
46
- signature = inspect.signature(unwrapped)
47
- params: list[FuncParam] = []
70
+ if annotation is Parameter.empty or annotation is Any:
71
+ return (None, None, False)
48
72
 
49
- for i, param in enumerate(signature.parameters.values()):
50
- is_positional = param.kind in (
51
- Parameter.POSITIONAL_ONLY,
52
- Parameter.POSITIONAL_OR_KEYWORD,
53
- Parameter.VAR_POSITIONAL,
54
- )
55
- is_varargs = param.kind in (Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD)
56
- has_default = param.default != NO_DEFAULT
57
-
58
- # Get type from type annotation or default value.
59
- param_type: type | None = None
60
- if param.annotation != Parameter.empty:
61
- param_type = _extract_simple_type(param.annotation)
62
- elif param.default is not Parameter.empty:
63
- param_type = type(param.default)
64
-
65
- func_param = FuncParam(
66
- name=param.name,
67
- type=param_type,
68
- default=param.default if has_default else NO_DEFAULT,
69
- position=i + 1 if is_positional else None,
70
- is_varargs=is_varargs,
71
- )
72
- params.append(func_param)
73
+ current_annotation = annotation
74
+ is_optional_flag = False
73
75
 
74
- return params
76
+ # Unwrap Optional[T] or T | None (from Python 3.10+ UnionType)
77
+ origin: type | None = get_origin(current_annotation)
78
+ args = get_args(current_annotation)
79
+
80
+ if origin is Union or ( # pyright: ignore[reportDeprecated]
81
+ hasattr(types, "UnionType") and isinstance(current_annotation, types.UnionType)
82
+ ):
83
+ non_none_args = [arg for arg in args if arg is not type(None)]
84
+ if len(non_none_args) == 1:
85
+ is_optional_flag = True
86
+ current_annotation = non_none_args[0]
87
+ origin = get_origin(current_annotation) # Re-evaluate for the unwrapped type
88
+ args = get_args(current_annotation)
89
+ elif not non_none_args: # Handles Union[NoneType] or just NoneType
90
+ return (type(None), None, True)
91
+ # If multiple non_none_args (e.g., int | str), current_annotation remains the Union for now.
92
+
93
+ # Determine effective_type and inner_type from (potentially unwrapped) current_annotation
94
+ final_effective_type: type | None = None
95
+ final_inner_type: type | None = None
96
+
97
+ if isinstance(
98
+ current_annotation, type
99
+ ): # Covers simple types (int, str) and resolved Union types (int | str)
100
+ final_effective_type = current_annotation
101
+ elif origin and isinstance(origin, type): # Generics like list, dict, tuple (e.g., list[str])
102
+ final_effective_type = cast(type, origin) # This would be `list` for `list[str]`
103
+ if args and _is_type_tuple(args) and args[0] is not Any:
104
+ # For simplicity, take the first type argument as inner_type.
105
+ # E.g., for list[str], inner_type is str. For dict[str, int], inner_type is str.
106
+ final_inner_type = args[0]
107
+ # A more sophisticated approach might handle all args for tuples like tuple[str, int]
108
+
109
+ return final_effective_type, final_inner_type, is_optional_flag
110
+
111
+
112
+ def _is_type_tuple(args: tuple[Any, ...]) -> bool:
113
+ """Are all args types?"""
114
+ if not args:
115
+ return False
116
+ return all(isinstance(arg, type) for arg in args)
75
117
 
76
118
 
77
- def _extract_simple_type(annotation: Any) -> type | None:
119
+ def inspect_function_params(func: Callable[..., Any], unwrap: bool = True) -> list[FuncParam]:
78
120
  """
79
- Extract a single Type from an annotation that is an explicit simple type (like `str` or
80
- an enum) or a simple Union (such as `str` from `Optional[str]`). Return None if it's not
81
- clear.
121
+ Inspects a Python function's signature and returns a list of `ParamInfo` objects.
122
+ A convenience wrapper for `inspect.signature` that provides a detailed, structured
123
+ representation of each parameter, making it easier to build tools like CLI argument
124
+ parsers. By default, it unwraps decorated functions to get to the original signature.
82
125
  """
83
- if isinstance(annotation, type):
84
- return annotation
126
+ unwrapped_func = inspect.unwrap(func) if unwrap else func
127
+ signature = inspect.signature(unwrapped_func)
85
128
 
86
- # Handle pipe syntax (str | None) from Python 3.10+
87
- if hasattr(types, "UnionType") and isinstance(annotation, types.UnionType):
88
- args = get_args(annotation)
89
- non_none_args = [arg for arg in args if arg is not type(None)]
90
- if len(non_none_args) == 1 and isinstance(non_none_args[0], type):
91
- return non_none_args[0]
129
+ param_infos: list[FuncParam] = []
92
130
 
93
- origin = get_origin(annotation)
94
- if origin is Union: # pyright: ignore
95
- args = get_args(annotation)
96
- non_none_args = [arg for arg in args if arg is not type(None)]
97
- if len(non_none_args) == 1 and isinstance(non_none_args[0], type):
98
- return non_none_args[0]
99
- elif origin and isinstance(origin, type):
100
- # Cast origin to type to satisfy the type checker
101
- return cast(type, origin) # pyright: ignore
131
+ for i, (param_name, param_obj) in enumerate(signature.parameters.items()):
132
+ effective_type, inner_type, is_optional = _resolve_type_details(param_obj.annotation)
133
+
134
+ # Fallback: if type is not resolved from annotation, try to infer from default value.
135
+ if (
136
+ effective_type is None
137
+ and param_obj.default is not NO_DEFAULT
138
+ and param_obj.default is not None
139
+ ):
140
+ if not is_optional: # Avoid setting NoneType if it was Optional[SomethingElse]
141
+ effective_type = type(param_obj.default)
142
+
143
+ # Determine position
144
+ is_positional = param_obj.kind in (
145
+ Parameter.POSITIONAL_ONLY,
146
+ Parameter.POSITIONAL_OR_KEYWORD,
147
+ Parameter.VAR_POSITIONAL,
148
+ )
149
+ position = i + 1 if is_positional else None
150
+
151
+ info = FuncParam(
152
+ name=param_name,
153
+ kind=param_obj.kind,
154
+ annotation=param_obj.annotation, # Store raw annotation
155
+ default=param_obj.default, # Store raw default
156
+ position=position,
157
+ effective_type=effective_type,
158
+ inner_type=inner_type,
159
+ is_explicitly_optional=is_optional,
160
+ )
161
+ param_infos.append(info)
102
162
 
103
- return None
163
+ return param_infos
104
164
 
105
165
 
106
166
  ## Tests
107
167
 
108
168
 
109
- def test_inspect_function_params():
169
+ def test_inspect_function_parameters_updated():
170
+ # Test functions from your original example
110
171
  def func0(path: str | None = None) -> list:
111
172
  return [path]
112
173
 
@@ -124,55 +185,244 @@ def test_inspect_function_params():
124
185
  def func4() -> list:
125
186
  return []
126
187
 
127
- def func5(x: int, y: int = 3, *, z: int = 4, **kwargs): # pyright: ignore[reportUnusedParameter]
128
- pass
188
+ def func5(x: int, y: int = 3, *, z: int = 4, **kwargs):
189
+ return [x, y, z, kwargs]
129
190
 
130
- params0 = inspect_function_params(func0)
131
- params1 = inspect_function_params(func1)
132
- params2 = inspect_function_params(func2)
133
- params3 = inspect_function_params(func3)
134
- params4 = inspect_function_params(func4)
135
- params5 = inspect_function_params(func5)
191
+ class MyEnum(Enum):
192
+ ITEM1 = "item1"
193
+ ITEM2 = "item2"
194
+
195
+ def func6(opt_enum: MyEnum | None = MyEnum.ITEM1):
196
+ return [opt_enum]
136
197
 
137
- print("\ninspect:")
138
- print(repr(params0))
139
- print()
140
- print(repr(params1))
141
- print()
142
- print(repr(params2))
143
- print()
144
- print(repr(params3))
145
- print()
146
- print(repr(params4))
147
- print()
148
- print(repr(params5))
149
-
150
- assert params0 == [FuncParam(name="path", type=str, default=None, position=1, is_varargs=False)]
198
+ def func7(numbers: list[int]):
199
+ return [numbers]
151
200
 
201
+ def func8(maybe_list: list[str] | None = None):
202
+ return [maybe_list]
203
+
204
+ params0 = inspect_function_params(func0)
205
+ print("\ninspect_function_parameters results:")
206
+ print(f"func0: {params0}")
207
+ assert params0 == [
208
+ FuncParam(
209
+ name="path",
210
+ kind=Parameter.POSITIONAL_OR_KEYWORD,
211
+ annotation=(str | None),
212
+ default=None,
213
+ position=1,
214
+ effective_type=str,
215
+ inner_type=None,
216
+ is_explicitly_optional=True,
217
+ )
218
+ ]
219
+
220
+ params1 = inspect_function_params(func1)
221
+ print(f"func1: {params1}")
152
222
  assert params1 == [
153
- FuncParam(name="arg1", type=str, default=NO_DEFAULT, position=1, is_varargs=False),
154
- FuncParam(name="arg2", type=str, default=NO_DEFAULT, position=2, is_varargs=False),
155
- FuncParam(name="arg3", type=int, default=NO_DEFAULT, position=3, is_varargs=False),
156
- FuncParam(name="option_one", type=bool, default=False, position=4, is_varargs=False),
157
- FuncParam(name="option_two", type=str, default=None, position=5, is_varargs=False),
223
+ FuncParam(
224
+ name="arg1",
225
+ kind=Parameter.POSITIONAL_OR_KEYWORD,
226
+ annotation=str,
227
+ default=NO_DEFAULT,
228
+ position=1,
229
+ effective_type=str,
230
+ inner_type=None,
231
+ is_explicitly_optional=False,
232
+ ),
233
+ FuncParam(
234
+ name="arg2",
235
+ kind=Parameter.POSITIONAL_OR_KEYWORD,
236
+ annotation=str,
237
+ default=NO_DEFAULT,
238
+ position=2,
239
+ effective_type=str,
240
+ inner_type=None,
241
+ is_explicitly_optional=False,
242
+ ),
243
+ FuncParam(
244
+ name="arg3",
245
+ kind=Parameter.POSITIONAL_OR_KEYWORD,
246
+ annotation=int,
247
+ default=NO_DEFAULT,
248
+ position=3,
249
+ effective_type=int,
250
+ inner_type=None,
251
+ is_explicitly_optional=False,
252
+ ),
253
+ FuncParam(
254
+ name="option_one",
255
+ kind=Parameter.POSITIONAL_OR_KEYWORD,
256
+ annotation=bool,
257
+ default=False,
258
+ position=4,
259
+ effective_type=bool,
260
+ inner_type=None,
261
+ is_explicitly_optional=False,
262
+ ), # bool default makes it not explicitly Optional from type
263
+ FuncParam(
264
+ name="option_two",
265
+ kind=Parameter.POSITIONAL_OR_KEYWORD,
266
+ annotation=(str | None),
267
+ default=None,
268
+ position=5,
269
+ effective_type=str,
270
+ inner_type=None,
271
+ is_explicitly_optional=True,
272
+ ),
158
273
  ]
159
274
 
275
+ params2 = inspect_function_params(func2)
276
+ print(f"func2: {params2}")
160
277
  assert params2 == [
161
- FuncParam(name="paths", type=str, default=NO_DEFAULT, position=1, is_varargs=True),
162
- FuncParam(name="summary", type=bool, default=False, position=None, is_varargs=False),
163
- FuncParam(name="iso_time", type=bool, default=False, position=None, is_varargs=False),
278
+ FuncParam(
279
+ name="paths",
280
+ kind=Parameter.VAR_POSITIONAL,
281
+ annotation=str,
282
+ default=NO_DEFAULT,
283
+ position=1,
284
+ effective_type=str,
285
+ inner_type=None,
286
+ is_explicitly_optional=False,
287
+ ), # For *args: T, effective_type is T
288
+ FuncParam(
289
+ name="summary",
290
+ kind=Parameter.KEYWORD_ONLY,
291
+ annotation=(bool | None),
292
+ default=False,
293
+ position=None,
294
+ effective_type=bool,
295
+ inner_type=None,
296
+ is_explicitly_optional=True,
297
+ ),
298
+ FuncParam(
299
+ name="iso_time",
300
+ kind=Parameter.KEYWORD_ONLY,
301
+ annotation=bool,
302
+ default=False,
303
+ position=None,
304
+ effective_type=bool,
305
+ inner_type=None,
306
+ is_explicitly_optional=False,
307
+ ),
164
308
  ]
165
309
 
310
+ params3 = inspect_function_params(func3)
311
+ print(f"func3: {params3}")
166
312
  assert params3 == [
167
- FuncParam(name="arg1", type=str, default=NO_DEFAULT, position=1, is_varargs=False),
168
- FuncParam(name="keywords", type=None, default=NO_DEFAULT, position=None, is_varargs=True),
313
+ FuncParam(
314
+ name="arg1",
315
+ kind=Parameter.POSITIONAL_OR_KEYWORD,
316
+ annotation=str,
317
+ default=NO_DEFAULT,
318
+ position=1,
319
+ effective_type=str,
320
+ inner_type=None,
321
+ is_explicitly_optional=False,
322
+ ),
323
+ FuncParam(
324
+ name="keywords",
325
+ kind=Parameter.VAR_KEYWORD,
326
+ annotation=Parameter.empty,
327
+ default=NO_DEFAULT,
328
+ position=None,
329
+ effective_type=None,
330
+ inner_type=None,
331
+ is_explicitly_optional=False,
332
+ ),
169
333
  ]
170
334
 
335
+ params4 = inspect_function_params(func4)
336
+ print(f"func4: {params4}")
171
337
  assert params4 == []
172
338
 
339
+ params5 = inspect_function_params(func5)
340
+ print(f"func5: {params5}")
173
341
  assert params5 == [
174
- FuncParam(name="x", type=int, default=NO_DEFAULT, position=1, is_varargs=False),
175
- FuncParam(name="y", type=int, default=3, position=2, is_varargs=False),
176
- FuncParam(name="z", type=int, default=4, position=None, is_varargs=False),
177
- FuncParam(name="kwargs", type=None, default=NO_DEFAULT, position=None, is_varargs=True),
342
+ FuncParam(
343
+ name="x",
344
+ kind=Parameter.POSITIONAL_OR_KEYWORD,
345
+ annotation=int,
346
+ default=NO_DEFAULT,
347
+ position=1,
348
+ effective_type=int,
349
+ inner_type=None,
350
+ is_explicitly_optional=False,
351
+ ),
352
+ FuncParam(
353
+ name="y",
354
+ kind=Parameter.POSITIONAL_OR_KEYWORD,
355
+ annotation=int,
356
+ default=3,
357
+ position=2,
358
+ effective_type=int,
359
+ inner_type=None,
360
+ is_explicitly_optional=False,
361
+ ),
362
+ FuncParam(
363
+ name="z",
364
+ kind=Parameter.KEYWORD_ONLY,
365
+ annotation=int,
366
+ default=4,
367
+ position=None,
368
+ effective_type=int,
369
+ inner_type=None,
370
+ is_explicitly_optional=False,
371
+ ),
372
+ FuncParam(
373
+ name="kwargs",
374
+ kind=Parameter.VAR_KEYWORD,
375
+ annotation=Parameter.empty,
376
+ default=NO_DEFAULT,
377
+ position=None,
378
+ effective_type=None,
379
+ inner_type=None,
380
+ is_explicitly_optional=False,
381
+ ),
382
+ ]
383
+
384
+ params6 = inspect_function_params(func6)
385
+ print(f"func6: {params6}")
386
+
387
+ assert params6 == [
388
+ FuncParam(
389
+ name="opt_enum",
390
+ kind=Parameter.POSITIONAL_OR_KEYWORD,
391
+ annotation=(MyEnum | None),
392
+ default=MyEnum.ITEM1,
393
+ position=1,
394
+ effective_type=MyEnum,
395
+ inner_type=None,
396
+ is_explicitly_optional=True,
397
+ )
398
+ ]
399
+
400
+ params7 = inspect_function_params(func7)
401
+ print(f"func7: {params7}")
402
+ assert params7 == [
403
+ FuncParam(
404
+ name="numbers",
405
+ kind=Parameter.POSITIONAL_OR_KEYWORD,
406
+ annotation=list[int],
407
+ default=NO_DEFAULT,
408
+ position=1,
409
+ effective_type=list,
410
+ inner_type=int,
411
+ is_explicitly_optional=False,
412
+ )
413
+ ]
414
+
415
+ params8 = inspect_function_params(func8)
416
+ print(f"func8: {params8}")
417
+ assert params8 == [
418
+ FuncParam(
419
+ name="maybe_list",
420
+ kind=Parameter.POSITIONAL_OR_KEYWORD,
421
+ annotation=(list[str] | None),
422
+ default=None,
423
+ position=1,
424
+ effective_type=list,
425
+ inner_type=str,
426
+ is_explicitly_optional=True,
427
+ )
178
428
  ]
@@ -15,21 +15,30 @@ Tallies: TypeAlias = dict[str, int]
15
15
  def import_subdirs(
16
16
  parent_package_name: str,
17
17
  parent_dir: Path,
18
- subdir_names: list[str],
18
+ subdir_names: list[str] | None = None,
19
19
  tallies: Tallies | None = None,
20
20
  ):
21
21
  """
22
22
  Import all files in the given subdirectories of a single parent directory.
23
+ Wraps `pkgutil.iter_modules` to iterate over all modules in the subdirectories.
24
+ If `subdir_names` is `None`, will import all subdirectories.
23
25
  """
24
26
  if tallies is None:
25
27
  tallies = {}
28
+ if not subdir_names:
29
+ subdir_names = ["."]
26
30
 
27
31
  for subdir_name in subdir_names:
28
- full_path = parent_dir / subdir_name
32
+ if subdir_name == ".":
33
+ full_path = parent_dir
34
+ package_name = parent_package_name
35
+ else:
36
+ full_path = parent_dir / subdir_name
37
+ package_name = f"{parent_package_name}.{subdir_name}"
38
+
29
39
  if not full_path.is_dir():
30
40
  raise FileNotFoundError(f"Subdirectory not found: {full_path}")
31
41
 
32
- package_name = f"{parent_package_name}.{subdir_name}"
33
42
  for _module_finder, module_name, _is_pkg in pkgutil.iter_modules(path=[str(full_path)]):
34
43
  importlib.import_module(f"{package_name}.{module_name}") # Propagate import errors
35
44
  tallies[package_name] = tallies.get(package_name, 0) + 1
@@ -15,35 +15,6 @@ def not_none(value: T | None, message: str | None = None) -> T:
15
15
  return value
16
16
 
17
17
 
18
- def is_truthy(value: Any, strict: bool = True) -> bool:
19
- """
20
- True for all common string and non-string values for true. Useful for parsing
21
- string values or command line arguments.
22
- """
23
- truthy_values = {"true", "1", "yes", "on", "y"}
24
- falsy_values = {"false", "0", "no", "off", "n", ""}
25
-
26
- if value is None:
27
- return False
28
- elif isinstance(value, str):
29
- value = value.strip().lower()
30
- if value in truthy_values:
31
- return True
32
- elif value in falsy_values:
33
- return False
34
- elif isinstance(value, (int, float)):
35
- return value != 0
36
- elif isinstance(value, bool):
37
- return value
38
- elif isinstance(value, (list, tuple, set, dict)):
39
- return len(value) > 0
40
-
41
- if strict:
42
- raise ValueError(f"Could not convert type {type(value)} to boolean: {repr(value)}")
43
-
44
- return bool(value)
45
-
46
-
47
18
  def as_dataclass(dict_data: dict[str, Any], dataclass_type: type[T]) -> T:
48
19
  """
49
20
  Convert a dict recursively to dataclass object, raising an error if the data does
kash/utils/common/url.py CHANGED
@@ -67,15 +67,39 @@ def is_file_url(url: str | Url) -> bool:
67
67
  return url.startswith("file://")
68
68
 
69
69
 
70
- def parse_file_url(url: str | Url) -> Path | None:
70
+ def parse_http_url(url: str | Url) -> ParseResult:
71
71
  """
72
- Parse a file URL and return the path, or None if not a file URL.
72
+ Parse an http/https URL and return the parsed result, raising ValueError if
73
+ not an http/https URL.
74
+ """
75
+ parsed_url = urlparse(url)
76
+ if parsed_url.scheme in ("http", "https"):
77
+ return parsed_url
78
+ else:
79
+ raise ValueError(f"Not an http/https URL: {url}")
80
+
81
+
82
+ def parse_file_url(url: str | Url) -> Path:
83
+ """
84
+ Parse a file URL and return the path, raising ValueError if not a file URL.
73
85
  """
74
86
  parsed_url = urlparse(url)
75
87
  if parsed_url.scheme == "file":
76
88
  return Path(parsed_url.path)
77
89
  else:
78
- return None
90
+ raise ValueError(f"Not a file URL: {url}")
91
+
92
+
93
+ def parse_s3_url(url: str | Url) -> tuple[str, str]:
94
+ """
95
+ Parse an S3 URL and return the bucket and key, raising ValueError if not an
96
+ S3 URL.
97
+ """
98
+ parsed_url = urlparse(url)
99
+ if parsed_url.scheme == "s3":
100
+ return parsed_url.netloc, parsed_url.path.lstrip("/")
101
+ else:
102
+ raise ValueError(f"Not an S3 URL: {url}")
79
103
 
80
104
 
81
105
  def as_file_url(path: str | Path) -> Url:
kash/utils/errors.py CHANGED
@@ -139,6 +139,12 @@ class FileFormatError(ContentError):
139
139
  pass
140
140
 
141
141
 
142
+ class ApiError(KashRuntimeError):
143
+ """Raised when an API call returns something unexpected."""
144
+
145
+ pass
146
+
147
+
142
148
  def _nonfatal_exceptions() -> tuple[type[Exception], ...]:
143
149
  exceptions: list[type[Exception]] = [SelfExplanatoryError, FileNotFoundError, IOError]
144
150
  try:
@@ -20,6 +20,7 @@ class FileExt(Enum):
20
20
  diff = "diff"
21
21
  json = "json"
22
22
  csv = "csv"
23
+ xlsx = "xlsx"
23
24
  npz = "npz"
24
25
  log = "log"
25
26
  py = "py"
@@ -34,6 +35,8 @@ class FileExt(Enum):
34
35
  mp3 = "mp3"
35
36
  m4a = "m4a"
36
37
  mp4 = "mp4"
38
+ pptx = "pptx"
39
+ epub = "epub"
37
40
 
38
41
  @property
39
42
  def dot_ext(self) -> str:
@@ -50,6 +53,7 @@ class FileExt(Enum):
50
53
  self.py,
51
54
  self.sh,
52
55
  self.xsh,
56
+ self.epub,
53
57
  ]
54
58
 
55
59
  @property