flyshell 1.2.3__tar.gz → 1.2.5__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.3/src/flyshell.egg-info → flyshell-1.2.5}/PKG-INFO +1 -1
- {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/abstract/base_plugin.py +8 -2
- {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/constants.py +2 -2
- {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/core/directory.py +12 -0
- flyshell-1.2.5/src/flyshell/core/env.py +40 -0
- {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/core/loader.py +19 -6
- {flyshell-1.2.3 → flyshell-1.2.5/src/flyshell.egg-info}/PKG-INFO +1 -1
- {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell.egg-info/SOURCES.txt +1 -0
- flyshell-1.2.5/tests/test_env_keys.py +115 -0
- flyshell-1.2.3/src/flyshell/core/env.py +0 -9
- {flyshell-1.2.3 → flyshell-1.2.5}/LICENSE +0 -0
- {flyshell-1.2.3 → flyshell-1.2.5}/README.md +0 -0
- {flyshell-1.2.3 → flyshell-1.2.5}/pyproject.toml +0 -0
- {flyshell-1.2.3 → flyshell-1.2.5}/setup.cfg +0 -0
- {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/__init__.py +0 -0
- {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/abstract/__init__.py +0 -0
- {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/abstract/base_cmd.py +0 -0
- {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/abstract/parent_cmd.py +0 -0
- {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/builtin/__init__.py +0 -0
- {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/builtin/diskops.py +0 -0
- {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/builtin/fs.py +0 -0
- {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/builtin/system.py +0 -0
- {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/builtin/utils.py +0 -0
- {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/core/__init__.py +0 -0
- {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/core/auth.py +0 -0
- {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/core/console.py +0 -0
- {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/core/data.py +0 -0
- {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/main.py +0 -0
- {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell.egg-info/dependency_links.txt +0 -0
- {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell.egg-info/entry_points.txt +0 -0
- {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell.egg-info/requires.txt +0 -0
- {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell.egg-info/top_level.txt +0 -0
- {flyshell-1.2.3 → flyshell-1.2.5}/tests/test_auth.py +0 -0
- {flyshell-1.2.3 → flyshell-1.2.5}/tests/test_data.py +0 -0
- {flyshell-1.2.3 → flyshell-1.2.5}/tests/test_directory.py +0 -0
- {flyshell-1.2.3 → flyshell-1.2.5}/tests/test_diskops.py +0 -0
- {flyshell-1.2.3 → flyshell-1.2.5}/tests/test_fs.py +0 -0
- {flyshell-1.2.3 → flyshell-1.2.5}/tests/test_loader.py +0 -0
- {flyshell-1.2.3 → flyshell-1.2.5}/tests/test_security.py +0 -0
- {flyshell-1.2.3 → flyshell-1.2.5}/tests/test_utils.py +0 -0
|
@@ -12,19 +12,25 @@ 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
|
|
|
26
32
|
def on_unload(self):
|
|
27
|
-
|
|
33
|
+
pass
|
|
28
34
|
|
|
29
35
|
def execute(self, args: list):
|
|
30
36
|
raise NotImplementedError("Plugins must implement the execute method")
|
|
@@ -160,6 +160,16 @@ def _execute_single(raw_cmd: str) -> int:
|
|
|
160
160
|
elif cmd_name in c.PLUGINS:
|
|
161
161
|
log(raw_cmd)
|
|
162
162
|
plugin = c.PLUGINS[cmd_name]
|
|
163
|
+
required_keys = getattr(plugin, "api_keys", [])
|
|
164
|
+
available_keys = getattr(plugin, "api_keys_data", {})
|
|
165
|
+
missing = [k for k in required_keys if k not in available_keys]
|
|
166
|
+
if missing:
|
|
167
|
+
env_path = getattr(plugin, "env_file_path", "flyshell_keys.env")
|
|
168
|
+
print(
|
|
169
|
+
f"\nPlugin Error: Cannot execute '{cmd_name}'. Missing required API key(s): {', '.join(missing)}"
|
|
170
|
+
)
|
|
171
|
+
print(f"Define them in {env_path} and retry.\n")
|
|
172
|
+
return 1
|
|
163
173
|
try:
|
|
164
174
|
result = plugin.execute(args)
|
|
165
175
|
return result if isinstance(result, int) else 0
|
|
@@ -176,6 +186,8 @@ def _execute_single(raw_cmd: str) -> int:
|
|
|
176
186
|
finally:
|
|
177
187
|
if hasattr(plugin, "on_unload"):
|
|
178
188
|
plugin.on_unload()
|
|
189
|
+
if hasattr(plugin, "save_storage"):
|
|
190
|
+
plugin.save_storage()
|
|
179
191
|
else:
|
|
180
192
|
print(
|
|
181
193
|
f"\nCommand Error: '{cmd_name}' is not a recognised command. Use 'cmds' for help.\n"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# src\flyshell\core\env.py
|
|
2
|
+
|
|
3
|
+
from flyshell import constants as c
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_env_path() -> Path:
|
|
8
|
+
return c.APP_DIR / "flyshell_keys.env"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def create_env():
|
|
12
|
+
env_path = get_env_path()
|
|
13
|
+
if not env_path.exists():
|
|
14
|
+
env_path.touch()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def read_env_file() -> dict[str, str]:
|
|
18
|
+
env_path = get_env_path()
|
|
19
|
+
if not env_path.exists():
|
|
20
|
+
return {}
|
|
21
|
+
entries = {}
|
|
22
|
+
with open(env_path, "r", encoding="utf-8") as f:
|
|
23
|
+
for line in f:
|
|
24
|
+
line = line.strip()
|
|
25
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
26
|
+
continue
|
|
27
|
+
key, val = line.split("=", 1)
|
|
28
|
+
entries[key.strip()] = val.strip().strip("\"'")
|
|
29
|
+
return entries
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_plugin_api_keys(plugin_dir_name: str) -> dict[str, str]:
|
|
33
|
+
all_env = read_env_file()
|
|
34
|
+
prefix = f"{plugin_dir_name}_".lower()
|
|
35
|
+
plugin_keys = {}
|
|
36
|
+
for raw_k, val in all_env.items():
|
|
37
|
+
if raw_k.lower().startswith(prefix):
|
|
38
|
+
key_name = raw_k[len(prefix) :]
|
|
39
|
+
plugin_keys[key_name] = val
|
|
40
|
+
return plugin_keys
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
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
|
|
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
|