flyshell 1.2.4__tar.gz → 1.2.6__tar.gz
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.
- {flyshell-1.2.4/src/flyshell.egg-info → flyshell-1.2.6}/PKG-INFO +1 -1
- {flyshell-1.2.4 → flyshell-1.2.6}/src/flyshell/abstract/base_plugin.py +7 -1
- flyshell-1.2.6/src/flyshell/builtin/pkg.py +33 -0
- {flyshell-1.2.4 → flyshell-1.2.6}/src/flyshell/constants.py +3 -2
- {flyshell-1.2.4 → flyshell-1.2.6}/src/flyshell/core/directory.py +12 -1
- flyshell-1.2.6/src/flyshell/core/env.py +129 -0
- {flyshell-1.2.4 → flyshell-1.2.6}/src/flyshell/core/loader.py +19 -6
- {flyshell-1.2.4 → flyshell-1.2.6/src/flyshell.egg-info}/PKG-INFO +1 -1
- {flyshell-1.2.4 → flyshell-1.2.6}/src/flyshell.egg-info/SOURCES.txt +3 -0
- flyshell-1.2.6/tests/test_env_keys.py +153 -0
- flyshell-1.2.6/tests/test_pkg.py +50 -0
- flyshell-1.2.4/src/flyshell/core/env.py +0 -9
- {flyshell-1.2.4 → flyshell-1.2.6}/LICENSE +0 -0
- {flyshell-1.2.4 → flyshell-1.2.6}/README.md +0 -0
- {flyshell-1.2.4 → flyshell-1.2.6}/pyproject.toml +0 -0
- {flyshell-1.2.4 → flyshell-1.2.6}/setup.cfg +0 -0
- {flyshell-1.2.4 → flyshell-1.2.6}/src/flyshell/__init__.py +0 -0
- {flyshell-1.2.4 → flyshell-1.2.6}/src/flyshell/abstract/__init__.py +0 -0
- {flyshell-1.2.4 → flyshell-1.2.6}/src/flyshell/abstract/base_cmd.py +0 -0
- {flyshell-1.2.4 → flyshell-1.2.6}/src/flyshell/abstract/parent_cmd.py +0 -0
- {flyshell-1.2.4 → flyshell-1.2.6}/src/flyshell/builtin/__init__.py +0 -0
- {flyshell-1.2.4 → flyshell-1.2.6}/src/flyshell/builtin/diskops.py +0 -0
- {flyshell-1.2.4 → flyshell-1.2.6}/src/flyshell/builtin/fs.py +0 -0
- {flyshell-1.2.4 → flyshell-1.2.6}/src/flyshell/builtin/system.py +0 -0
- {flyshell-1.2.4 → flyshell-1.2.6}/src/flyshell/builtin/utils.py +0 -0
- {flyshell-1.2.4 → flyshell-1.2.6}/src/flyshell/core/__init__.py +0 -0
- {flyshell-1.2.4 → flyshell-1.2.6}/src/flyshell/core/auth.py +0 -0
- {flyshell-1.2.4 → flyshell-1.2.6}/src/flyshell/core/console.py +0 -0
- {flyshell-1.2.4 → flyshell-1.2.6}/src/flyshell/core/data.py +0 -0
- {flyshell-1.2.4 → flyshell-1.2.6}/src/flyshell/main.py +0 -0
- {flyshell-1.2.4 → flyshell-1.2.6}/src/flyshell.egg-info/dependency_links.txt +0 -0
- {flyshell-1.2.4 → flyshell-1.2.6}/src/flyshell.egg-info/entry_points.txt +0 -0
- {flyshell-1.2.4 → flyshell-1.2.6}/src/flyshell.egg-info/requires.txt +0 -0
- {flyshell-1.2.4 → flyshell-1.2.6}/src/flyshell.egg-info/top_level.txt +0 -0
- {flyshell-1.2.4 → flyshell-1.2.6}/tests/test_auth.py +0 -0
- {flyshell-1.2.4 → flyshell-1.2.6}/tests/test_data.py +0 -0
- {flyshell-1.2.4 → flyshell-1.2.6}/tests/test_directory.py +0 -0
- {flyshell-1.2.4 → flyshell-1.2.6}/tests/test_diskops.py +0 -0
- {flyshell-1.2.4 → flyshell-1.2.6}/tests/test_fs.py +0 -0
- {flyshell-1.2.4 → flyshell-1.2.6}/tests/test_loader.py +0 -0
- {flyshell-1.2.4 → flyshell-1.2.6}/tests/test_security.py +0 -0
- {flyshell-1.2.4 → flyshell-1.2.6}/tests/test_utils.py +0 -0
|
@@ -12,14 +12,20 @@ class BasePlugin:
|
|
|
12
12
|
identifier: str = ""
|
|
13
13
|
usage: str = ""
|
|
14
14
|
extended_help: str = ""
|
|
15
|
-
requires_api = False
|
|
15
|
+
requires_api: bool = False
|
|
16
|
+
api_keys: list = []
|
|
16
17
|
|
|
17
18
|
def __init__(self, context: dict):
|
|
18
19
|
self.context = context
|
|
19
20
|
self.plugin_name = context.get("plugin_name", self.name)
|
|
20
21
|
self.storage = context.get("storage", {})
|
|
22
|
+
self.api_keys_data = context.get("api_keys", {})
|
|
23
|
+
self.env_file_path = context.get("env_path", None)
|
|
21
24
|
self.on_load()
|
|
22
25
|
|
|
26
|
+
def get_api_key(self, key_name: str, default=None):
|
|
27
|
+
return self.api_keys_data.get(key_name, default)
|
|
28
|
+
|
|
23
29
|
def on_load(self):
|
|
24
30
|
pass
|
|
25
31
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# src\flyshell\builtin\pkg.py
|
|
2
|
+
|
|
3
|
+
from typing import List, Tuple
|
|
4
|
+
from flyshell.abstract.base_cmd import BaseCmd
|
|
5
|
+
from flyshell.abstract.parent_cmd import ParentCmd
|
|
6
|
+
from flyshell.core import env
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PkgApi(BaseCmd):
|
|
10
|
+
name: str = "api"
|
|
11
|
+
description: str = "View configured package/plugin API keys"
|
|
12
|
+
min_args: int = 0
|
|
13
|
+
options: str = "Package name"
|
|
14
|
+
aliases: List[str] = ["keys"]
|
|
15
|
+
|
|
16
|
+
def execute(self, args: List[str]) -> Tuple[int, str]:
|
|
17
|
+
if not args:
|
|
18
|
+
code, output = env.get_all_keys()
|
|
19
|
+
else:
|
|
20
|
+
package_name = args[0]
|
|
21
|
+
code, output = env.get_plugin_keys_formatted(package_name)
|
|
22
|
+
return (code, f"\n{output}\n")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Pkg(ParentCmd):
|
|
26
|
+
name: str = "pkg"
|
|
27
|
+
description: str = "Manage Flyshell packages, plugins, and dependencies"
|
|
28
|
+
aliases: List[str] = ["package"]
|
|
29
|
+
|
|
30
|
+
def get_subcommands(self) -> List[BaseCmd]:
|
|
31
|
+
return [
|
|
32
|
+
PkgApi(),
|
|
33
|
+
]
|
|
@@ -5,8 +5,8 @@ import os
|
|
|
5
5
|
import platform
|
|
6
6
|
import time
|
|
7
7
|
|
|
8
|
-
BUILD =
|
|
9
|
-
VERSION = "1.2.
|
|
8
|
+
BUILD = 131
|
|
9
|
+
VERSION = "1.2.6"
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def _resolve_app_dir() -> Path:
|
|
@@ -53,6 +53,7 @@ REQUIRED_COMPONENTS = [
|
|
|
53
53
|
"builtin",
|
|
54
54
|
"builtin/diskops.py",
|
|
55
55
|
"builtin/fs.py",
|
|
56
|
+
"builtin/pkg.py",
|
|
56
57
|
"builtin/system.py",
|
|
57
58
|
"builtin/utils.py",
|
|
58
59
|
"constants.py",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from flyshell import constants as c
|
|
4
4
|
from flyshell.abstract.base_cmd import BaseCmd
|
|
5
5
|
from flyshell.abstract.parent_cmd import ParentCmd
|
|
6
|
-
from flyshell.builtin import diskops, fs, system, utils
|
|
6
|
+
from flyshell.builtin import diskops, fs, pkg, system, utils
|
|
7
7
|
from flyshell.core import auth, data
|
|
8
8
|
|
|
9
9
|
from datetime import datetime, timezone
|
|
@@ -64,6 +64,7 @@ INSTALLED_COMMANDS: list[BaseCmd] = [
|
|
|
64
64
|
diskops.MkDir(),
|
|
65
65
|
diskops.Move(),
|
|
66
66
|
system.OpenFile(),
|
|
67
|
+
pkg.Pkg(),
|
|
67
68
|
utils.Reboot(),
|
|
68
69
|
diskops.Rename(),
|
|
69
70
|
diskops.Rm(),
|
|
@@ -160,6 +161,16 @@ def _execute_single(raw_cmd: str) -> int:
|
|
|
160
161
|
elif cmd_name in c.PLUGINS:
|
|
161
162
|
log(raw_cmd)
|
|
162
163
|
plugin = c.PLUGINS[cmd_name]
|
|
164
|
+
required_keys = getattr(plugin, "api_keys", [])
|
|
165
|
+
available_keys = getattr(plugin, "api_keys_data", {})
|
|
166
|
+
missing = [k for k in required_keys if k not in available_keys]
|
|
167
|
+
if missing:
|
|
168
|
+
env_path = getattr(plugin, "env_file_path", "flyshell_keys.env")
|
|
169
|
+
print(
|
|
170
|
+
f"\nPlugin Error: Cannot execute '{cmd_name}'. Missing required API key(s): {', '.join(missing)}"
|
|
171
|
+
)
|
|
172
|
+
print(f"Define them in {env_path} and retry.\n")
|
|
173
|
+
return 1
|
|
163
174
|
try:
|
|
164
175
|
result = plugin.execute(args)
|
|
165
176
|
return result if isinstance(result, int) else 0
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# src/flyshell/core/env.py
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from flyshell import constants as c
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_env_path() -> Path:
|
|
8
|
+
primary = c.APP_DIR / "flyshell_keys.env"
|
|
9
|
+
if primary.exists():
|
|
10
|
+
return primary
|
|
11
|
+
fallback = c.APP_DIR / ".env"
|
|
12
|
+
if fallback.exists():
|
|
13
|
+
return fallback
|
|
14
|
+
return primary
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create_env():
|
|
18
|
+
env_path = get_env_path()
|
|
19
|
+
if not env_path.exists():
|
|
20
|
+
env_path.touch()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def read_env_file() -> dict[str, str]:
|
|
24
|
+
env_path = get_env_path()
|
|
25
|
+
if not env_path.exists():
|
|
26
|
+
return {}
|
|
27
|
+
entries = {}
|
|
28
|
+
with open(env_path, "r", encoding="utf-8") as f:
|
|
29
|
+
for line in f:
|
|
30
|
+
line = line.strip()
|
|
31
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
32
|
+
continue
|
|
33
|
+
raw_k, raw_v = line.split("=", 1)
|
|
34
|
+
clean_k = raw_k.strip().replace("[", "").replace("]", "")
|
|
35
|
+
clean_v = raw_v.strip().strip("\"'").replace("[", "").replace("]", "")
|
|
36
|
+
if clean_k:
|
|
37
|
+
entries[clean_k] = clean_v
|
|
38
|
+
return entries
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _resolve_plugin_dir_name(target_name: str) -> tuple[str, object | None]:
|
|
42
|
+
"""Resolves an identifier, display name, or dir name to (dir_name, plugin_instance)."""
|
|
43
|
+
target_lower = target_name.strip().lower()
|
|
44
|
+
if hasattr(c, "PLUGINS"):
|
|
45
|
+
if target_lower in c.PLUGINS:
|
|
46
|
+
plugin = c.PLUGINS[target_lower]
|
|
47
|
+
dir_name = plugin.context.get("plugin_dir_name") or target_lower
|
|
48
|
+
dir_name = (
|
|
49
|
+
Path(dir_name).stem
|
|
50
|
+
if "\\" in str(dir_name) or "/" in str(dir_name)
|
|
51
|
+
else str(dir_name)
|
|
52
|
+
)
|
|
53
|
+
return (dir_name, plugin)
|
|
54
|
+
for cmd_name, plugin in c.PLUGINS.items():
|
|
55
|
+
p_display = getattr(plugin, "name", "").lower()
|
|
56
|
+
p_dir = str(plugin.context.get("plugin_dir_name", "")).lower()
|
|
57
|
+
p_dir_clean = Path(p_dir).stem
|
|
58
|
+
if target_lower in (cmd_name.lower(), p_display, p_dir, p_dir_clean):
|
|
59
|
+
resolved_dir = plugin.context.get("plugin_dir_name") or cmd_name
|
|
60
|
+
resolved_dir = (
|
|
61
|
+
Path(resolved_dir).stem
|
|
62
|
+
if "\\" in str(resolved_dir) or "/" in str(resolved_dir)
|
|
63
|
+
else str(resolved_dir)
|
|
64
|
+
)
|
|
65
|
+
return (resolved_dir, plugin)
|
|
66
|
+
return (target_name.strip(), None)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_plugin_api_keys(plugin_name_or_dir: str) -> dict[str, str]:
|
|
70
|
+
all_env = read_env_file()
|
|
71
|
+
dir_name, _ = _resolve_plugin_dir_name(plugin_name_or_dir)
|
|
72
|
+
prefixes = {
|
|
73
|
+
f"{dir_name.lower()}_",
|
|
74
|
+
f"{plugin_name_or_dir.strip().lower()}_",
|
|
75
|
+
}
|
|
76
|
+
plugin_keys = {}
|
|
77
|
+
for raw_k, val in all_env.items():
|
|
78
|
+
k_lower = raw_k.lower()
|
|
79
|
+
for prefix in prefixes:
|
|
80
|
+
if k_lower.startswith(prefix):
|
|
81
|
+
key_name = raw_k[len(prefix) :]
|
|
82
|
+
plugin_keys[key_name] = val
|
|
83
|
+
break
|
|
84
|
+
return plugin_keys
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_all_keys() -> tuple[int, str]:
|
|
88
|
+
"""Retrieve and display all configured API keys in the environment."""
|
|
89
|
+
env_path = get_env_path()
|
|
90
|
+
if not env_path.exists():
|
|
91
|
+
return (1, f"File Config Error: Environment file not found at '{env_path}'.")
|
|
92
|
+
try:
|
|
93
|
+
all_env = read_env_file()
|
|
94
|
+
if not all_env:
|
|
95
|
+
return (0, "No API keys configured.")
|
|
96
|
+
lines = ["Configured API keys:"]
|
|
97
|
+
for k, v in sorted(all_env.items()):
|
|
98
|
+
lines.append(f"{k} = {v}")
|
|
99
|
+
return (0, "\n".join(lines))
|
|
100
|
+
except Exception as e:
|
|
101
|
+
return (1, f"File Error: An unknown error occurred: {e}")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_plugin_keys_formatted(target_name: str) -> tuple[int, str]:
|
|
105
|
+
if not target_name or not isinstance(target_name, str):
|
|
106
|
+
return (1, "Command Error: A valid plugin name must be provided.")
|
|
107
|
+
env_path = get_env_path()
|
|
108
|
+
if not env_path.exists():
|
|
109
|
+
return (1, f"File Config Error: Environment file not found at '{env_path}'.")
|
|
110
|
+
dir_name, plugin = _resolve_plugin_dir_name(target_name)
|
|
111
|
+
configured_keys = get_plugin_api_keys(target_name)
|
|
112
|
+
required_keys = getattr(plugin, "api_keys", []) if plugin else []
|
|
113
|
+
if not configured_keys and not required_keys:
|
|
114
|
+
return (0, f"No API keys found or required for plugin '{target_name}'.")
|
|
115
|
+
lines = [f"API Keys for plugin '{target_name}' (folder: '{dir_name}'):"]
|
|
116
|
+
lower_configured = {k.lower(): (k, v) for k, v in configured_keys.items()}
|
|
117
|
+
seen_keys = set()
|
|
118
|
+
for req in required_keys:
|
|
119
|
+
req_lower = req.lower()
|
|
120
|
+
seen_keys.add(req_lower)
|
|
121
|
+
if req_lower in lower_configured:
|
|
122
|
+
orig_k, val = lower_configured[req_lower]
|
|
123
|
+
lines.append(f"{orig_k} = {val} [Required]")
|
|
124
|
+
else:
|
|
125
|
+
lines.append(f"{req} = [MISSING / NOT SET] [Required]")
|
|
126
|
+
for k_lower, (orig_k, val) in sorted(lower_configured.items()):
|
|
127
|
+
if k_lower not in seen_keys:
|
|
128
|
+
lines.append(f"{orig_k} = {val} [Optional/Extra]")
|
|
129
|
+
return (0, "\n".join(lines))
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from flyshell import constants as c
|
|
4
4
|
from flyshell.abstract.base_plugin import BasePlugin
|
|
5
|
-
from flyshell.core import data
|
|
5
|
+
from flyshell.core import data, env
|
|
6
6
|
from flyshell.core.directory import RESERVED_NAMES
|
|
7
7
|
|
|
8
8
|
from pathlib import Path
|
|
@@ -50,15 +50,24 @@ def scan_plugins(plugin_folder=None):
|
|
|
50
50
|
for _, obj in inspect.getmembers(module, inspect.isclass):
|
|
51
51
|
if issubclass(obj, BasePlugin) and obj is not BasePlugin:
|
|
52
52
|
resolved_name = getattr(obj, "identifier", None) or default_name
|
|
53
|
-
|
|
54
|
-
# Also verify the resolved identifier doesn't conflict with a reserved name
|
|
55
53
|
if resolved_name in RESERVED_NAMES:
|
|
56
54
|
print(
|
|
57
55
|
f"\n⚠️ Collision Error - Plugin '{resolved_name}' conflicts with a built-in Flyshell command and will not be loaded."
|
|
58
56
|
)
|
|
59
57
|
break
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
ctx = build_context(resolved_name, default_name, file_path)
|
|
59
|
+
required_keys = getattr(obj, "api_keys", [])
|
|
60
|
+
found_keys = ctx["api_keys"]
|
|
61
|
+
missing_keys = [k for k in required_keys if k not in found_keys]
|
|
62
|
+
if missing_keys:
|
|
63
|
+
env_file = ctx["env_path"]
|
|
64
|
+
print(
|
|
65
|
+
f"\n⚠️ API Key Error - Plugin '{resolved_name}' is missing required key(s): {', '.join(missing_keys)}"
|
|
66
|
+
)
|
|
67
|
+
print(f"Please define them in: {env_file} as:")
|
|
68
|
+
for mk in missing_keys:
|
|
69
|
+
print(f"{default_name}_{mk}=<your_key>")
|
|
70
|
+
break
|
|
62
71
|
instance = obj(ctx)
|
|
63
72
|
candidates.setdefault(resolved_name, []).append(
|
|
64
73
|
(instance, file_path)
|
|
@@ -92,11 +101,15 @@ def scan_plugins(plugin_folder=None):
|
|
|
92
101
|
print("\nNo plugins loaded.")
|
|
93
102
|
|
|
94
103
|
|
|
95
|
-
def build_context(plugin_name, file_path=None):
|
|
104
|
+
def build_context(plugin_name, default_name, file_path=None):
|
|
96
105
|
plugin_storage = data.get_all_plugin_data(plugin_name)
|
|
106
|
+
api_keys = env.get_plugin_api_keys(str(default_name))
|
|
97
107
|
context = {
|
|
98
108
|
"plugin_name": plugin_name,
|
|
109
|
+
"plugin_dir_name": str(default_name),
|
|
99
110
|
"file_path": str(file_path) if file_path else None,
|
|
111
|
+
"env_path": str(env.get_env_path()),
|
|
112
|
+
"api_keys": api_keys,
|
|
100
113
|
"metadata": {"host_os": c.HOST_OS, "version": c.VERSION, "build": c.BUILD},
|
|
101
114
|
"storage": plugin_storage,
|
|
102
115
|
}
|
|
@@ -17,6 +17,7 @@ src/flyshell/abstract/parent_cmd.py
|
|
|
17
17
|
src/flyshell/builtin/__init__.py
|
|
18
18
|
src/flyshell/builtin/diskops.py
|
|
19
19
|
src/flyshell/builtin/fs.py
|
|
20
|
+
src/flyshell/builtin/pkg.py
|
|
20
21
|
src/flyshell/builtin/system.py
|
|
21
22
|
src/flyshell/builtin/utils.py
|
|
22
23
|
src/flyshell/core/__init__.py
|
|
@@ -30,7 +31,9 @@ tests/test_auth.py
|
|
|
30
31
|
tests/test_data.py
|
|
31
32
|
tests/test_directory.py
|
|
32
33
|
tests/test_diskops.py
|
|
34
|
+
tests/test_env_keys.py
|
|
33
35
|
tests/test_fs.py
|
|
34
36
|
tests/test_loader.py
|
|
37
|
+
tests/test_pkg.py
|
|
35
38
|
tests/test_security.py
|
|
36
39
|
tests/test_utils.py
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# tests\test_env_keys.py
|
|
2
|
+
|
|
3
|
+
# tests/test_env_api_keys.py
|
|
4
|
+
|
|
5
|
+
from flyshell import constants as c
|
|
6
|
+
from flyshell.abstract.base_plugin import BasePlugin
|
|
7
|
+
from flyshell.core import directory, env, loader
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def mock_env_file(tmp_path, monkeypatch):
|
|
14
|
+
"""Sets up a temporary flyshell_keys.env and points env/c.APP_DIR to it."""
|
|
15
|
+
env_file = tmp_path / "flyshell_keys.env"
|
|
16
|
+
monkeypatch.setattr(c, "APP_DIR", tmp_path)
|
|
17
|
+
monkeypatch.setattr(env, "get_env_path", lambda: env_file)
|
|
18
|
+
return env_file
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_env_read_and_key_parsing(mock_env_file):
|
|
22
|
+
mock_env_file.write_text(
|
|
23
|
+
"# Comments should be ignored\n"
|
|
24
|
+
"sample_api_token=secret_123\n"
|
|
25
|
+
'sample_secondary_key="another_val"\n'
|
|
26
|
+
"otherplugin_key=val\n"
|
|
27
|
+
"invalid_line_without_equals\n"
|
|
28
|
+
)
|
|
29
|
+
keys = env.get_plugin_api_keys("sample")
|
|
30
|
+
assert keys == {
|
|
31
|
+
"api_token": "secret_123",
|
|
32
|
+
"secondary_key": "another_val",
|
|
33
|
+
}
|
|
34
|
+
assert "key" not in keys
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_extra_keys_are_included(mock_env_file):
|
|
38
|
+
mock_env_file.write_text(
|
|
39
|
+
"myplug_required_one=abc\n"
|
|
40
|
+
"myplug_bonus_feature=xyz\n"
|
|
41
|
+
"myplug_third_param=12345\n"
|
|
42
|
+
)
|
|
43
|
+
keys = env.get_plugin_api_keys("myplug")
|
|
44
|
+
assert keys == {
|
|
45
|
+
"required_one": "abc",
|
|
46
|
+
"bonus_feature": "xyz",
|
|
47
|
+
"third_param": "12345",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_loader_populates_context_correctly(tmp_path, mock_env_file, monkeypatch):
|
|
52
|
+
mock_env_file.write_text("dummy_token=token123\n")
|
|
53
|
+
plugin_file = tmp_path / "dummy.py"
|
|
54
|
+
plugin_file.write_text(
|
|
55
|
+
"from flyshell.abstract.base_plugin import BasePlugin\n"
|
|
56
|
+
"class DummyPlugin(BasePlugin):\n"
|
|
57
|
+
" api_keys = ['token']\n"
|
|
58
|
+
" def execute(self, args): return 0\n"
|
|
59
|
+
)
|
|
60
|
+
monkeypatch.setattr(c, "USER_PLUGIN_DIR", tmp_path)
|
|
61
|
+
loader.scan_plugins(plugin_folder=tmp_path)
|
|
62
|
+
assert "dummy" in c.PLUGINS
|
|
63
|
+
plugin = c.PLUGINS["dummy"]
|
|
64
|
+
assert plugin.context["plugin_name"] == "dummy"
|
|
65
|
+
assert plugin.context["plugin_dir_name"] == "dummy"
|
|
66
|
+
assert plugin.context["file_path"] == str(plugin_file)
|
|
67
|
+
assert plugin.context["env_path"] == str(mock_env_file)
|
|
68
|
+
assert plugin.api_keys_data == {"token": "token123"}
|
|
69
|
+
assert plugin.get_api_key("token") == "token123"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_loader_rejects_plugin_with_missing_keys(
|
|
73
|
+
tmp_path, mock_env_file, monkeypatch, capsys
|
|
74
|
+
):
|
|
75
|
+
mock_env_file.write_text("needkey_first=val1\n")
|
|
76
|
+
plugin_file = tmp_path / "needkey.py"
|
|
77
|
+
plugin_file.write_text(
|
|
78
|
+
"from flyshell.abstract.base_plugin import BasePlugin\n"
|
|
79
|
+
"class NeedKeyPlugin(BasePlugin):\n"
|
|
80
|
+
" api_keys = ['first', 'second']\n"
|
|
81
|
+
" def execute(self, args): return 0\n"
|
|
82
|
+
)
|
|
83
|
+
monkeypatch.setattr(c, "USER_PLUGIN_DIR", tmp_path)
|
|
84
|
+
loader.scan_plugins(plugin_folder=tmp_path)
|
|
85
|
+
assert "needkey" not in c.PLUGINS
|
|
86
|
+
out = capsys.readouterr().out
|
|
87
|
+
assert "API Key Error" in out
|
|
88
|
+
assert "second" in out
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_directory_execute_fallback_when_keys_missing(
|
|
92
|
+
mock_env_file, monkeypatch, capsys
|
|
93
|
+
):
|
|
94
|
+
class IncompletePlugin(BasePlugin):
|
|
95
|
+
api_keys = ["secret_key"]
|
|
96
|
+
executed = False
|
|
97
|
+
|
|
98
|
+
def execute(self, args):
|
|
99
|
+
self.executed = True
|
|
100
|
+
return 0
|
|
101
|
+
|
|
102
|
+
ctx = {
|
|
103
|
+
"plugin_name": "broken_keys",
|
|
104
|
+
"plugin_dir_name": "broken_keys",
|
|
105
|
+
"file_path": None,
|
|
106
|
+
"env_path": str(mock_env_file),
|
|
107
|
+
"api_keys": {},
|
|
108
|
+
}
|
|
109
|
+
instance = IncompletePlugin(ctx)
|
|
110
|
+
monkeypatch.setitem(c.PLUGINS, "broken_keys", instance)
|
|
111
|
+
exit_code = directory._execute_single("broken_keys")
|
|
112
|
+
assert exit_code == 1
|
|
113
|
+
assert instance.executed is False
|
|
114
|
+
out = capsys.readouterr().out
|
|
115
|
+
assert "Missing required API key(s): secret_key" in out
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_get_all_keys_success(mock_env_file):
|
|
119
|
+
mock_env_file.write_text("pluginA_token=123\npluginB_secret=xyz\n")
|
|
120
|
+
code, output = env.get_all_keys()
|
|
121
|
+
assert code == 0
|
|
122
|
+
assert "pluginA_token = 123" in output
|
|
123
|
+
assert "pluginB_secret = xyz" in output
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_get_all_keys_empty_file(mock_env_file):
|
|
127
|
+
mock_env_file.write_text("")
|
|
128
|
+
code, output = env.get_all_keys()
|
|
129
|
+
assert code == 0
|
|
130
|
+
assert "No API keys configured." in output
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_get_all_keys_missing_file(tmp_path, monkeypatch):
|
|
134
|
+
missing_path = tmp_path / "does_not_exist.env"
|
|
135
|
+
monkeypatch.setattr(env, "get_env_path", lambda: missing_path)
|
|
136
|
+
code, output = env.get_all_keys()
|
|
137
|
+
assert code == 1
|
|
138
|
+
assert "Environment file not found" in output
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_get_plugin_keys_formatted_success(mock_env_file):
|
|
142
|
+
mock_env_file.write_text("weather_api_key=token123\nweather_units=metric\n")
|
|
143
|
+
code, output = env.get_plugin_keys_formatted("weather")
|
|
144
|
+
assert code == 0
|
|
145
|
+
assert "weather" in output
|
|
146
|
+
assert "api_key = token123" in output
|
|
147
|
+
assert "units = metric" in output
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_get_plugin_keys_formatted_invalid_input():
|
|
151
|
+
code, output = env.get_plugin_keys_formatted("")
|
|
152
|
+
assert code == 1
|
|
153
|
+
assert "valid plugin name" in output
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# tests/test_pkg.py
|
|
2
|
+
|
|
3
|
+
from flyshell import constants as c
|
|
4
|
+
from flyshell.builtin import pkg
|
|
5
|
+
from flyshell.core import env
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def mock_env_file(tmp_path, monkeypatch):
|
|
12
|
+
c.PLUGINS.clear()
|
|
13
|
+
env_file = tmp_path / "flyshell_keys.env"
|
|
14
|
+
monkeypatch.setattr(c, "APP_DIR", tmp_path)
|
|
15
|
+
monkeypatch.setattr(env, "get_env_path", lambda: env_file)
|
|
16
|
+
yield env_file
|
|
17
|
+
c.PLUGINS.clear()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_pkg_dispatches_help_on_empty_args(capsys):
|
|
21
|
+
parent = pkg.Pkg()
|
|
22
|
+
code, msg = parent.execute([])
|
|
23
|
+
assert code == 0
|
|
24
|
+
out = capsys.readouterr().out
|
|
25
|
+
assert "Command Family: pkg" in out
|
|
26
|
+
assert "api (alias: keys)" in out
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_pkg_unknown_subcommand():
|
|
30
|
+
parent = pkg.Pkg()
|
|
31
|
+
code, msg = parent.execute(["unknown_cmd"])
|
|
32
|
+
assert code == 1
|
|
33
|
+
assert "Unknown subcommand 'unknown_cmd'" in msg
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_pkg_api_all_keys(mock_env_file):
|
|
37
|
+
mock_env_file.write_text("dummy_token=123\n")
|
|
38
|
+
parent = pkg.Pkg()
|
|
39
|
+
code, msg = parent.execute(["api"])
|
|
40
|
+
assert code == 0
|
|
41
|
+
assert "dummy_token = 123" in msg
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_pkg_api_plugin_keys(mock_env_file):
|
|
45
|
+
mock_env_file.write_text("weather_key=secret\nother_key=skip\n")
|
|
46
|
+
parent = pkg.Pkg()
|
|
47
|
+
code, msg = parent.execute(["api", "weather"])
|
|
48
|
+
assert code == 0
|
|
49
|
+
assert "key = secret" in msg
|
|
50
|
+
assert "other_key" not in msg
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|