update
This commit is contained in:
@@ -98,7 +98,7 @@ from SYS.result_table import Table
|
|||||||
|
|
||||||
from SYS.worker import WorkerManagerRegistry, WorkerStages, WorkerOutputMirror, WorkerStageSession
|
from SYS.worker import WorkerManagerRegistry, WorkerStages, WorkerOutputMirror, WorkerStageSession
|
||||||
from SYS.pipeline import PipelineExecutor
|
from SYS.pipeline import PipelineExecutor
|
||||||
from PluginCore.registry import plugin_inline_query_choices
|
from PluginCore.registry import plugin_inline_query_choices, plugin_query_field_map
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1242,10 +1242,13 @@ class CmdletCompleter(Completer):
|
|||||||
if query_fragment is not None:
|
if query_fragment is not None:
|
||||||
field_choices: Dict[str, List[str]] = {}
|
field_choices: Dict[str, List[str]] = {}
|
||||||
ordered_fields: List[str] = []
|
ordered_fields: List[str] = []
|
||||||
|
query_only_field_names: set = set()
|
||||||
for spec in query_specs:
|
for spec in query_specs:
|
||||||
key = str(spec.get("query_key") or spec.get("name") or "").strip().lower()
|
key = str(spec.get("query_key") or spec.get("name") or "").strip().lower()
|
||||||
if not key:
|
if not key:
|
||||||
continue
|
continue
|
||||||
|
if spec.get("query_only"):
|
||||||
|
query_only_field_names.add(key)
|
||||||
if key not in field_choices:
|
if key not in field_choices:
|
||||||
ordered_fields.append(key)
|
ordered_fields.append(key)
|
||||||
field_choices[key] = [str(choice) for choice in list(spec.get("choices", []) or [])]
|
field_choices[key] = [str(choice) for choice in list(spec.get("choices", []) or [])]
|
||||||
@@ -1255,6 +1258,31 @@ class CmdletCompleter(Completer):
|
|||||||
continue
|
continue
|
||||||
field_choices.setdefault(alias_text, field_choices[key])
|
field_choices.setdefault(alias_text, field_choices[key])
|
||||||
|
|
||||||
|
plugin_field_map: Dict[str, List[str]] = {}
|
||||||
|
if selected_plugin:
|
||||||
|
try:
|
||||||
|
plugin_field_map = plugin_query_field_map(selected_plugin, config)
|
||||||
|
except Exception:
|
||||||
|
plugin_field_map = {}
|
||||||
|
if plugin_field_map:
|
||||||
|
ordered_fields = [f for f in ordered_fields if f not in query_only_field_names]
|
||||||
|
removed_keys = set()
|
||||||
|
for key in query_only_field_names:
|
||||||
|
field_choices.pop(key, None)
|
||||||
|
removed_keys.add(key)
|
||||||
|
aliases_to_remove = set()
|
||||||
|
for alias_key, alias_values in list(field_choices.items()):
|
||||||
|
if alias_key in removed_keys:
|
||||||
|
continue
|
||||||
|
if alias_key not in ordered_fields and alias_key not in plugin_field_map:
|
||||||
|
aliases_to_remove.add(alias_key)
|
||||||
|
for akey in aliases_to_remove:
|
||||||
|
field_choices.pop(akey, None)
|
||||||
|
for field_name, choices in plugin_field_map.items():
|
||||||
|
if field_name not in field_choices:
|
||||||
|
ordered_fields.append(field_name)
|
||||||
|
field_choices[field_name] = choices
|
||||||
|
|
||||||
raw_fragment = str(query_fragment or "")
|
raw_fragment = str(query_fragment or "")
|
||||||
segment = raw_fragment[1:] if raw_fragment[:1] in {"'", '"'} else raw_fragment
|
segment = raw_fragment[1:] if raw_fragment[:1] in {"'", '"'} else raw_fragment
|
||||||
if "," in segment:
|
if "," in segment:
|
||||||
@@ -1267,8 +1295,8 @@ class CmdletCompleter(Completer):
|
|||||||
partial_lower = partial.strip().lower()
|
partial_lower = partial.strip().lower()
|
||||||
|
|
||||||
inline_choices = []
|
inline_choices = []
|
||||||
if effective_cmd == "search-file" and provider_name:
|
if selected_plugin and effective_cmd in {"search-file", "download-file"}:
|
||||||
inline_choices = self._inline_query_choices(provider_name, field, config)
|
inline_choices = self._inline_query_choices(selected_plugin, field, config)
|
||||||
|
|
||||||
choice_pool = inline_choices or field_choices.get(field, [])
|
choice_pool = inline_choices or field_choices.get(field, [])
|
||||||
if choice_pool:
|
if choice_pool:
|
||||||
|
|||||||
@@ -863,6 +863,36 @@ def plugin_inline_query_choices(
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def plugin_query_field_map(
|
||||||
|
plugin_name: str,
|
||||||
|
config: Optional[Dict[str, Any]] = None,
|
||||||
|
) -> Dict[str, List[str]]:
|
||||||
|
pname = str(plugin_name or "").strip().lower()
|
||||||
|
if not pname:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
info = REGISTRY.get(pname)
|
||||||
|
if info is not None:
|
||||||
|
mapping = collect_choice(info.plugin_class)
|
||||||
|
else:
|
||||||
|
plugin = get_plugin(pname, config)
|
||||||
|
if plugin is None:
|
||||||
|
return {}
|
||||||
|
mapping = collect_choice(plugin)
|
||||||
|
result: Dict[str, List[str]] = {}
|
||||||
|
for field, choices in mapping.items():
|
||||||
|
texts = []
|
||||||
|
for c in choices:
|
||||||
|
text = str(c.get("text") or c.get("value") or "")
|
||||||
|
if text:
|
||||||
|
texts.append(text)
|
||||||
|
if texts:
|
||||||
|
result[field] = texts
|
||||||
|
return result
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def get_plugin_for_url(url: str,
|
def get_plugin_for_url(url: str,
|
||||||
config: Optional[Dict[str, Any]] = None) -> Optional[Provider]:
|
config: Optional[Dict[str, Any]] = None) -> Optional[Provider]:
|
||||||
name = match_plugin_name_for_url(url)
|
name = match_plugin_name_for_url(url)
|
||||||
|
|||||||
+2
-2
@@ -2076,8 +2076,8 @@ class PipelineExecutor:
|
|||||||
except Exception:
|
except Exception:
|
||||||
row_action = None
|
row_action = None
|
||||||
if row_action:
|
if row_action:
|
||||||
prefer_row_action = True
|
stages.insert(0, list(row_action))
|
||||||
preferred_row_action = list(row_action)
|
return True, None
|
||||||
# Command expansion via @N:
|
# Command expansion via @N:
|
||||||
# - Default behavior: expand ONLY for single-row selections.
|
# - Default behavior: expand ONLY for single-row selections.
|
||||||
# - Special case: allow multi-row expansion for add-file directory tables by
|
# - Special case: allow multi-row expansion for add-file directory tables by
|
||||||
|
|||||||
+140
-1
@@ -32,6 +32,7 @@ _PREFERENCES_BROWSE_PATH = "__preferences__"
|
|||||||
_PLUGINS_BROWSE_PATH = "__plugins__"
|
_PLUGINS_BROWSE_PATH = "__plugins__"
|
||||||
_PLUGIN_CATEGORY_KEYS = ("plugin",)
|
_PLUGIN_CATEGORY_KEYS = ("plugin",)
|
||||||
_CREATE_INSTANCE_FLAG = "-create-instance"
|
_CREATE_INSTANCE_FLAG = "-create-instance"
|
||||||
|
_CHOOSE_FLAG = "-choose"
|
||||||
_KNOWN_SECTION_LABELS = {
|
_KNOWN_SECTION_LABELS = {
|
||||||
"plugin": "Plugins",
|
"plugin": "Plugins",
|
||||||
}
|
}
|
||||||
@@ -65,6 +66,7 @@ _CONFIG_ITEM_FIELDS = (
|
|||||||
"type",
|
"type",
|
||||||
"display_path",
|
"display_path",
|
||||||
"instance_target",
|
"instance_target",
|
||||||
|
"choices",
|
||||||
)
|
)
|
||||||
|
|
||||||
CMDLET = Cmdlet(
|
CMDLET = Cmdlet(
|
||||||
@@ -576,6 +578,7 @@ def _build_value_item(
|
|||||||
key_path: str,
|
key_path: str,
|
||||||
name: str,
|
name: str,
|
||||||
value: Any,
|
value: Any,
|
||||||
|
choices: Any = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
display_value = "***" if _is_sensitive_config_key(key_path) else _format_config_value(value)
|
display_value = "***" if _is_sensitive_config_key(key_path) else _format_config_value(value)
|
||||||
path_parts = [part for part in str(key_path or "").split(".") if part]
|
path_parts = [part for part in str(key_path or "").split(".") if part]
|
||||||
@@ -583,6 +586,7 @@ def _build_value_item(
|
|||||||
[_format_config_path_label(".".join(path_parts[:-1]))] if len(path_parts) > 1 else []
|
[_format_config_path_label(".".join(path_parts[:-1]))] if len(path_parts) > 1 else []
|
||||||
+ [_format_config_label(path_parts[-1])] if path_parts else [_format_config_label(name)]
|
+ [_format_config_label(path_parts[-1])] if path_parts else [_format_config_label(name)]
|
||||||
)
|
)
|
||||||
|
choice_list = list(choices) if isinstance(choices, (list, tuple)) else []
|
||||||
return {
|
return {
|
||||||
"kind": "value",
|
"kind": "value",
|
||||||
"key": key_path,
|
"key": key_path,
|
||||||
@@ -592,6 +596,7 @@ def _build_value_item(
|
|||||||
"value_display": display_value,
|
"value_display": display_value,
|
||||||
"display_path": display_path,
|
"display_path": display_path,
|
||||||
"type": type(value).__name__,
|
"type": type(value).__name__,
|
||||||
|
"choices": choice_list,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -700,6 +705,25 @@ def _build_root_config_items(config_data: Dict[str, Any]) -> List[Dict[str, Any]
|
|||||||
return items
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
def _lookup_plugin_choices(browse_path: str, key_name: str) -> Optional[List[str]]:
|
||||||
|
parts = _split_config_path(browse_path)
|
||||||
|
if len(parts) < 2 or parts[0] != "plugin":
|
||||||
|
return None
|
||||||
|
plugin_name = parts[1]
|
||||||
|
try:
|
||||||
|
from SYS.plugin_config import get_plugin_schema
|
||||||
|
schema = get_plugin_schema(plugin_name)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
for field in schema:
|
||||||
|
if field.get("key") == key_name:
|
||||||
|
choices = field.get("choices")
|
||||||
|
if isinstance(choices, list):
|
||||||
|
return [str(c) for c in choices]
|
||||||
|
break
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _build_nested_config_items(
|
def _build_nested_config_items(
|
||||||
config_data: Dict[str, Any],
|
config_data: Dict[str, Any],
|
||||||
browse_path: str,
|
browse_path: str,
|
||||||
@@ -737,6 +761,7 @@ def _build_nested_config_items(
|
|||||||
key_path=full_key,
|
key_path=full_key,
|
||||||
name=key,
|
name=key,
|
||||||
value=value,
|
value=value,
|
||||||
|
choices=_lookup_plugin_choices(browse_path, key),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -799,6 +824,21 @@ def _extract_browse_arg(args: Sequence[str]) -> Optional[str]:
|
|||||||
return _extract_arg_value(args, flags={"-browse", "--browse"}, allow_positional=False)
|
return _extract_arg_value(args, flags={"-browse", "--browse"}, allow_positional=False)
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_choose_arg(args: Sequence[str]) -> Optional[str]:
|
||||||
|
return _extract_arg_value(args, flags={_CHOOSE_FLAG}, allow_positional=False)
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_config_value_raw(config: Dict[str, Any], key_path: str) -> Any:
|
||||||
|
parts = [p for p in str(key_path or "").split(".") if p]
|
||||||
|
current: Any = config
|
||||||
|
for part in parts:
|
||||||
|
if isinstance(current, dict):
|
||||||
|
current = current.get(part)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
return current
|
||||||
|
|
||||||
|
|
||||||
def _extract_selected_update_value(args: Sequence[str]) -> Optional[str]:
|
def _extract_selected_update_value(args: Sequence[str]) -> Optional[str]:
|
||||||
explicit = _extract_arg_value(args, flags=VALUE_ARG_FLAGS, allow_positional=False)
|
explicit = _extract_arg_value(args, flags=VALUE_ARG_FLAGS, allow_positional=False)
|
||||||
if explicit is not None:
|
if explicit is not None:
|
||||||
@@ -871,6 +911,44 @@ def _normalize_config_item(candidate: Any) -> Optional[Dict[str, Any]]:
|
|||||||
return normalized or None
|
return normalized or None
|
||||||
|
|
||||||
|
|
||||||
|
def _show_choice_table(
|
||||||
|
target_key: str,
|
||||||
|
choices: List[str],
|
||||||
|
current_value: str,
|
||||||
|
display_name: str,
|
||||||
|
) -> int:
|
||||||
|
items: List[Dict[str, Any]] = []
|
||||||
|
for choice in choices:
|
||||||
|
choice_str = str(choice)
|
||||||
|
marker = " ← current" if choice_str == current_value else ""
|
||||||
|
items.append({
|
||||||
|
"kind": "choice",
|
||||||
|
"key": target_key,
|
||||||
|
"title": choice_str,
|
||||||
|
"value": choice_str,
|
||||||
|
"display": f"{choice_str}{marker}",
|
||||||
|
})
|
||||||
|
|
||||||
|
table = Table(f"Configuration: {display_name}", preserve_order=True)
|
||||||
|
table.set_table("config")
|
||||||
|
table.set_source_command(".config", [target_key])
|
||||||
|
table.set_header_lines([
|
||||||
|
f"Available values for {display_name}. Current: {current_value}",
|
||||||
|
"Use @N to select a value. Use @.. to go back.",
|
||||||
|
])
|
||||||
|
|
||||||
|
for idx, item in enumerate(items):
|
||||||
|
row = table.add_row()
|
||||||
|
row.add_column("Choice", item.get("display", item.get("title", "")))
|
||||||
|
table.set_row_selection_action(idx, [".config", str(item.get("key")), str(item.get("value"))])
|
||||||
|
|
||||||
|
ctx.set_last_result_table(table, items)
|
||||||
|
ctx.set_current_stage_table(table)
|
||||||
|
print()
|
||||||
|
print(table)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def _show_config_table(
|
def _show_config_table(
|
||||||
config_data: Dict[str, Any],
|
config_data: Dict[str, Any],
|
||||||
*,
|
*,
|
||||||
@@ -893,13 +971,23 @@ def _show_config_table(
|
|||||||
for idx, item in enumerate(items):
|
for idx, item in enumerate(items):
|
||||||
row = table.add_row()
|
row = table.add_row()
|
||||||
row.add_column("Name", item.get("title", ""))
|
row.add_column("Name", item.get("title", ""))
|
||||||
row.add_column("Value", item.get("summary") or item.get("value_display", ""))
|
value_text = item.get("summary") or item.get("value_display", "")
|
||||||
|
choices = item.get("choices")
|
||||||
|
if choices and isinstance(choices, list) and len(choices) > 0:
|
||||||
|
choices_repr = ", ".join(str(c) for c in choices)
|
||||||
|
value_text = f"{value_text} [{choices_repr}]"
|
||||||
|
row.add_column("Value", value_text)
|
||||||
row.add_column("Type", item.get("type", ""))
|
row.add_column("Type", item.get("type", ""))
|
||||||
if item.get("kind") == "section" and item.get("browse_path"):
|
if item.get("kind") == "section" and item.get("browse_path"):
|
||||||
table.set_row_selection_action(
|
table.set_row_selection_action(
|
||||||
idx,
|
idx,
|
||||||
[".config", "-browse", str(item.get("browse_path"))],
|
[".config", "-browse", str(item.get("browse_path"))],
|
||||||
)
|
)
|
||||||
|
elif item.get("kind") == "value" and choices and isinstance(choices, list) and len(choices) > 0 and item.get("key"):
|
||||||
|
table.set_row_selection_action(
|
||||||
|
idx,
|
||||||
|
[".config", "-choose", str(item.get("key"))],
|
||||||
|
)
|
||||||
elif item.get("kind") == "create_instance" and item.get("instance_target"):
|
elif item.get("kind") == "create_instance" and item.get("instance_target"):
|
||||||
table.set_row_selection_action(
|
table.set_row_selection_action(
|
||||||
idx,
|
idx,
|
||||||
@@ -966,6 +1054,22 @@ def _run(piped_result: Any, args: List[str], config: Dict[str, Any]) -> int:
|
|||||||
if browse_path:
|
if browse_path:
|
||||||
return _show_config_table(current_config, browse_path=browse_path)
|
return _show_config_table(current_config, browse_path=browse_path)
|
||||||
|
|
||||||
|
choose_key = _extract_choose_arg(args)
|
||||||
|
if choose_key:
|
||||||
|
choices = _lookup_plugin_choices(choose_key, choose_key.split(".")[-1]) or []
|
||||||
|
if not choices:
|
||||||
|
nested_branch = _resolve_config_branch(current_config, choose_key)
|
||||||
|
if isinstance(nested_branch, dict):
|
||||||
|
return _show_config_table(current_config, browse_path=choose_key)
|
||||||
|
print(f"No choices available for '{_format_config_path_label(choose_key)}'")
|
||||||
|
return 0
|
||||||
|
try:
|
||||||
|
current_value = str(_resolve_config_value_raw(current_config, choose_key) or "").strip()
|
||||||
|
except Exception:
|
||||||
|
current_value = ""
|
||||||
|
display_name = _format_config_path_label(choose_key)
|
||||||
|
return _show_choice_table(choose_key, choices, current_value, display_name)
|
||||||
|
|
||||||
selection_item = _get_selected_config_item() or _normalize_config_item(piped_result)
|
selection_item = _get_selected_config_item() or _normalize_config_item(piped_result)
|
||||||
|
|
||||||
create_instance_target = _extract_create_instance_target(args)
|
create_instance_target = _extract_create_instance_target(args)
|
||||||
@@ -1025,6 +1129,31 @@ def _run(piped_result: Any, args: List[str], config: Dict[str, Any]) -> int:
|
|||||||
log(f"Error updating config '{target_key}': {exc}")
|
log(f"Error updating config '{target_key}': {exc}")
|
||||||
print(f"Error updating config: {exc}")
|
print(f"Error updating config: {exc}")
|
||||||
return 1
|
return 1
|
||||||
|
choices = (selection_item or {}).get("choices")
|
||||||
|
if choices and isinstance(choices, list) and len(choices) > 0:
|
||||||
|
current_value = str((selection_item or {}).get("value") or "").strip()
|
||||||
|
display_name = selection_display_path or selection_key
|
||||||
|
return _show_choice_table(selection_key, choices, current_value, display_name)
|
||||||
|
|
||||||
|
if selection_kind == "choice" and selection_key:
|
||||||
|
choice_value = str((selection_item or {}).get("value") or "").strip()
|
||||||
|
if choice_value:
|
||||||
|
try:
|
||||||
|
target_key = _resolve_update_key(current_config, selection_key)
|
||||||
|
set_nested_config(current_config, target_key, choice_value)
|
||||||
|
_save_updated_config(current_config, target_key)
|
||||||
|
print(f"Updated '{_format_config_path_label(selection_key)}' to '{choice_value}'")
|
||||||
|
current_config = load_config()
|
||||||
|
key_parts = [p for p in str(selection_key).split(".") if p]
|
||||||
|
if len(key_parts) >= 2:
|
||||||
|
parent_path = ".".join(key_parts[:-1])
|
||||||
|
if _resolve_config_branch(current_config, parent_path):
|
||||||
|
return _show_config_table(current_config, browse_path=parent_path)
|
||||||
|
return 0
|
||||||
|
except Exception as exc:
|
||||||
|
log(f"Error updating config '{selection_key}': {exc}")
|
||||||
|
print(f"Error updating config: {exc}")
|
||||||
|
return 1
|
||||||
|
|
||||||
if not args:
|
if not args:
|
||||||
if sys.stdin.isatty() and not piped_result:
|
if sys.stdin.isatty() and not piped_result:
|
||||||
@@ -1047,6 +1176,16 @@ def _run(piped_result: Any, args: List[str], config: Dict[str, Any]) -> int:
|
|||||||
set_nested_config(current_config, key, value)
|
set_nested_config(current_config, key, value)
|
||||||
_save_updated_config(current_config, key)
|
_save_updated_config(current_config, key)
|
||||||
print(f"Updated '{key}' to '{value}'")
|
print(f"Updated '{key}' to '{value}'")
|
||||||
|
current_config = load_config()
|
||||||
|
parent_path = _resolve_direct_browse_path(current_config, key)
|
||||||
|
if not parent_path:
|
||||||
|
key_parts = [p for p in str(key).split(".") if p]
|
||||||
|
if len(key_parts) >= 2:
|
||||||
|
candidate = ".".join(key_parts[:-1])
|
||||||
|
if _resolve_config_branch(current_config, candidate):
|
||||||
|
parent_path = candidate
|
||||||
|
if parent_path:
|
||||||
|
return _show_config_table(current_config, browse_path=parent_path)
|
||||||
return 0
|
return 0
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
log(f"Error updating config '{key}': {exc}")
|
log(f"Error updating config '{key}': {exc}")
|
||||||
|
|||||||
@@ -251,6 +251,12 @@ class Local(Provider):
|
|||||||
folder_name = str(kwargs.get("folder_name") or "").strip()
|
folder_name = str(kwargs.get("folder_name") or "").strip()
|
||||||
if not folder_name and not direct_export_download:
|
if not folder_name and not direct_export_download:
|
||||||
folder_name = self._folder_name_from_pipe(kwargs.get("pipe_obj"))
|
folder_name = self._folder_name_from_pipe(kwargs.get("pipe_obj"))
|
||||||
|
if not folder_name and not direct_export_download:
|
||||||
|
title_hint = str(kwargs.get("title") or "").strip()
|
||||||
|
if not title_hint:
|
||||||
|
title_hint = source_path.stem.replace("_", " ").strip()
|
||||||
|
if title_hint:
|
||||||
|
folder_name = title_hint
|
||||||
|
|
||||||
export_root = destination_root
|
export_root = destination_root
|
||||||
if folder_name and not direct_export_download:
|
if folder_name and not direct_export_download:
|
||||||
|
|||||||
@@ -612,6 +612,13 @@ class OpenLibrary(Provider):
|
|||||||
TABLE_AUTO_STAGES = {
|
TABLE_AUTO_STAGES = {
|
||||||
"openlibrary.edition": ["download-file"],
|
"openlibrary.edition": ["download-file"],
|
||||||
}
|
}
|
||||||
|
QUERY_ARG_CHOICES = {
|
||||||
|
"quality": ["high", "medium", "low"],
|
||||||
|
"language": [
|
||||||
|
"english", "spanish", "french", "german", "italian",
|
||||||
|
"portuguese", "polish", "russian", "chinese", "japanese",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def config_schema(cls) -> List[Dict[str, Any]]:
|
def config_schema(cls) -> List[Dict[str, Any]]:
|
||||||
@@ -1175,7 +1182,7 @@ class OpenLibrary(Provider):
|
|||||||
|
|
||||||
Returns a dict with the downloaded path and SearchResult when successful.
|
Returns a dict with the downloaded path and SearchResult when successful.
|
||||||
"""
|
"""
|
||||||
self,
|
sr = self.search_result_from_url(url)
|
||||||
if sr is None:
|
if sr is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user