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.
Files changed (40) hide show
  1. {flyshell-1.2.3/src/flyshell.egg-info → flyshell-1.2.5}/PKG-INFO +1 -1
  2. {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/abstract/base_plugin.py +8 -2
  3. {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/constants.py +2 -2
  4. {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/core/directory.py +12 -0
  5. flyshell-1.2.5/src/flyshell/core/env.py +40 -0
  6. {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/core/loader.py +19 -6
  7. {flyshell-1.2.3 → flyshell-1.2.5/src/flyshell.egg-info}/PKG-INFO +1 -1
  8. {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell.egg-info/SOURCES.txt +1 -0
  9. flyshell-1.2.5/tests/test_env_keys.py +115 -0
  10. flyshell-1.2.3/src/flyshell/core/env.py +0 -9
  11. {flyshell-1.2.3 → flyshell-1.2.5}/LICENSE +0 -0
  12. {flyshell-1.2.3 → flyshell-1.2.5}/README.md +0 -0
  13. {flyshell-1.2.3 → flyshell-1.2.5}/pyproject.toml +0 -0
  14. {flyshell-1.2.3 → flyshell-1.2.5}/setup.cfg +0 -0
  15. {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/__init__.py +0 -0
  16. {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/abstract/__init__.py +0 -0
  17. {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/abstract/base_cmd.py +0 -0
  18. {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/abstract/parent_cmd.py +0 -0
  19. {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/builtin/__init__.py +0 -0
  20. {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/builtin/diskops.py +0 -0
  21. {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/builtin/fs.py +0 -0
  22. {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/builtin/system.py +0 -0
  23. {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/builtin/utils.py +0 -0
  24. {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/core/__init__.py +0 -0
  25. {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/core/auth.py +0 -0
  26. {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/core/console.py +0 -0
  27. {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/core/data.py +0 -0
  28. {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell/main.py +0 -0
  29. {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell.egg-info/dependency_links.txt +0 -0
  30. {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell.egg-info/entry_points.txt +0 -0
  31. {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell.egg-info/requires.txt +0 -0
  32. {flyshell-1.2.3 → flyshell-1.2.5}/src/flyshell.egg-info/top_level.txt +0 -0
  33. {flyshell-1.2.3 → flyshell-1.2.5}/tests/test_auth.py +0 -0
  34. {flyshell-1.2.3 → flyshell-1.2.5}/tests/test_data.py +0 -0
  35. {flyshell-1.2.3 → flyshell-1.2.5}/tests/test_directory.py +0 -0
  36. {flyshell-1.2.3 → flyshell-1.2.5}/tests/test_diskops.py +0 -0
  37. {flyshell-1.2.3 → flyshell-1.2.5}/tests/test_fs.py +0 -0
  38. {flyshell-1.2.3 → flyshell-1.2.5}/tests/test_loader.py +0 -0
  39. {flyshell-1.2.3 → flyshell-1.2.5}/tests/test_security.py +0 -0
  40. {flyshell-1.2.3 → flyshell-1.2.5}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flyshell
3
- Version: 1.2.3
3
+ Version: 1.2.5
4
4
  Summary: A modular, zero-dependency command-line environment in Python.
5
5
  License: MIT
6
6
  Requires-Python: >=3.10
@@ -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
- self.save_storage()
33
+ pass
28
34
 
29
35
  def execute(self, args: list):
30
36
  raise NotImplementedError("Plugins must implement the execute method")
@@ -5,8 +5,8 @@ import os
5
5
  import platform
6
6
  import time
7
7
 
8
- BUILD = 128
9
- VERSION = "1.2.3"
8
+ BUILD = 130
9
+ VERSION = "1.2.5"
10
10
 
11
11
 
12
12
  def _resolve_app_dir() -> Path:
@@ -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
- ctx = build_context(resolved_name, file_path)
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
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flyshell
3
- Version: 1.2.3
3
+ Version: 1.2.5
4
4
  Summary: A modular, zero-dependency command-line environment in Python.
5
5
  License: MIT
6
6
  Requires-Python: >=3.10
@@ -30,6 +30,7 @@ tests/test_auth.py
30
30
  tests/test_data.py
31
31
  tests/test_directory.py
32
32
  tests/test_diskops.py
33
+ tests/test_env_keys.py
33
34
  tests/test_fs.py
34
35
  tests/test_loader.py
35
36
  tests/test_security.py
@@ -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
@@ -1,9 +0,0 @@
1
- # src\flyshell\core\env.py
2
-
3
- from flyshell import constants as c
4
-
5
-
6
- def create_env():
7
- env_path = c.APP_DIR / "flyshell_keys.env"
8
- if not env_path.exists():
9
- env_path.touch()
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