Skip to content

Commit

Permalink
feat: plugin system
Browse files Browse the repository at this point in the history
  • Loading branch information
raymond-u committed Mar 15, 2024
1 parent df796b8 commit 6553794
Show file tree
Hide file tree
Showing 123 changed files with 2,049 additions and 1,453 deletions.
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Plugin system
- Allow login through a one-time code sent via email

### Changed

- Update JupyterLab to 4.1.2
- Raise the minimum required version of Python to 3.12
- Built-in apps are now installed as plugins
- The `modules` configuration has been renamed to `plugins`
- Guest role and user role accounts no longer have access to apps by default
- Hide the full screen button for narrow screen widths
- Update JupyterLab to 4.1.5
- Update RStudio to 4.3.3
- Update Xray to 1.8.9

### Fixed

Expand Down
9 changes: 2 additions & 7 deletions examples/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,6 @@ directories:
source: /mnt/data/shared_readonly
read_only: true

modules:
filebrowser:
enabled: true
rstudio:
enabled: false
password: passwd

network:
hostname: lungo.com
subnet: 192.168.2.0/24
Expand All @@ -36,6 +29,8 @@ network:
cert: /etc/ssl/certs/self-signed.crt
key: /etc/ssl/private/self-signed.key

plugins: { }

rules:
privileges:
unregistered:
Expand Down
2 changes: 1 addition & 1 deletion examples/users.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ accounts:
role: user
extra:
allowed_apps:
- rstudio
- all
- enabled: false
username: anonymous
name:
Expand Down
258 changes: 135 additions & 123 deletions poetry.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ keywords = ["homelab", "self-host"]
packages = [{ include = "lungo_cli", from = "src" }]

[tool.poetry.dependencies]
python = "^3.11"
python = "^3.12"
cryptography = "^42.0.4"
importlib-resources = "^6.0.1"
jinja2 = "^3.1.3"
platformdirs = "^4.2.0"
pydantic = { extras = ["email"], version = "^2.6.1" }
pydantic-yaml = "^1.2.1"
requests = "^2.31.0"
typer = { extras = ["all"], version = "^0.9.0" }
aenum = "^3.1.15"

[tool.poetry.group.dev.dependencies]
black = "^24.2.0"
Expand Down
5 changes: 4 additions & 1 deletion src/lungo_cli/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typer import Exit, Option, Typer

from .state import console
from ..commands import check, down, up
from ..commands import check, down, install, list, uninstall, up
from ..core.constants import APP_NAME, APP_NAME_CAPITALIZED
from ..helpers.common import get_app_version

Expand All @@ -17,6 +17,9 @@
app.command("check")(check.main)
app.command("up")(up.main)
app.command("down")(down.main)
app.command("list")(list.main)
app.command("install")(install.main)
app.command("uninstall")(uninstall.main)


def app_wrapper():
Expand Down
23 changes: 16 additions & 7 deletions src/lungo_cli/app/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,22 @@
from ..core.database import AccountManager
from ..core.file import FileUtils
from ..core.network import HttpApiClient
from ..core.plugin import PluginManager
from ..core.renderer import Renderer
from ..core.storage import Storage

_console = Console()
_client = HttpApiClient(_console)
_file_utils = FileUtils(_console)
_storage = Storage(_console, _file_utils)
_context_manager = ContextManager(_console, _file_utils, _storage)
_container = Container(_console, _file_utils, _storage, _context_manager)
_account_manager = AccountManager(_console, _file_utils, _storage, HttpApiClient(_console), _container)
_renderer = Renderer(_console, _file_utils, _storage)
_app_manager = AppManager(_console, _file_utils, _storage, _context_manager, _account_manager, _renderer)
_container = Container(_console, _context_manager, _file_utils, _storage)
_plugin_manager = PluginManager(_console, _context_manager, _file_utils, _storage)
_account_manager = AccountManager(_client, _console, _container, _file_utils, _storage)
_app_manager = AppManager(
_account_manager, _console, _context_manager, _file_utils, _plugin_manager, _renderer, _storage
)


def console() -> Console:
Expand All @@ -34,16 +39,20 @@ def context_manager() -> ContextManager:
return _context_manager


def renderer() -> Renderer:
return _renderer


def container() -> Container:
return _container


def account_manager() -> AccountManager:
return _account_manager
def plugin_manager() -> PluginManager:
return _plugin_manager


def renderer() -> Renderer:
return _renderer
def account_manager() -> AccountManager:
return _account_manager


def app_manager() -> AppManager:
Expand Down
1 change: 0 additions & 1 deletion src/lungo_cli/commands/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,5 @@
]
dev_type = Annotated[bool, Option("--dev", help="Use the development configuration.", show_default=False)]
force_init_type = Annotated[bool, Option("--force-init", help="Do a fresh initialization.", show_default=False)]
remove_lock_type = Annotated[bool, Option("--remove-lock", help="Remove the lock file.", show_default=False)]
quiet_type = Annotated[bool, Option("--quiet", "-q", help="Suppress all output except for errors.", show_default=False)]
verbosity_type = Annotated[int, Option("--verbose", "-v", count=True, help="Increase verbosity.", show_default=False)]
7 changes: 4 additions & 3 deletions src/lungo_cli/commands/check.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
from .base import config_dir_type, quiet_type, verbosity_type
from .base import config_dir_type, dev_type, quiet_type, verbosity_type
from ..app.state import app_manager, console


def main(
config_dir: config_dir_type = None,
dev: dev_type = False,
quiet: quiet_type = False,
verbosity: verbosity_type = 0,
) -> None:
"""
Check if the configuration is valid.
"""
app_manager().process_args(config_dir, quiet, verbosity)
app_manager().load_config()
app_manager().process_args(config_dir, dev, quiet, verbosity)
app_manager().load_config_and_plugins()

console().print("Configuration is valid.")
3 changes: 1 addition & 2 deletions src/lungo_cli/commands/down.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@ def main(
"""
Stop the service.
"""
app_manager().process_args(config_dir, quiet, verbosity)
app_manager().process_args(config_dir, dev, quiet, verbosity)
app_manager().load_config()
app_manager().process_args_deferred(dev)

container().set_preferred_tool(container_tool)

Expand Down
44 changes: 44 additions & 0 deletions src/lungo_cli/commands/install.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from typing import Annotated

from typer import Argument

from .base import config_dir_type, dev_type, quiet_type, verbosity_type
from ..app.state import app_manager, console, plugin_manager
from ..helpers.format import format_input


def main(
args: Annotated[
list[str],
Argument(
help="Names of the plugins to add.",
show_default=False,
),
],
config_dir: config_dir_type = None,
dev: dev_type = False,
quiet: quiet_type = False,
verbosity: verbosity_type = 0,
) -> None:
"""
Install or upgrade plugins.
"""
app_manager().process_args(config_dir, dev, quiet, verbosity)
app_manager().load_config()

count = 0

for arg in args:
plugin_cls = next((x for x in plugin_manager().installable_plugin_classes if x.config.name == arg), None)

if plugin_cls is None:
console().print_warning(f"Plugin {format_input(arg)} not found or not installable. Skipping.")
continue

plugin_manager().add_plugin(plugin_cls)
count += 1

if count == 1:
console().print("1 plugin added successfully.")
elif count > 1:
console().print(f"{count} plugins added successfully.")
84 changes: 84 additions & 0 deletions src/lungo_cli/commands/list.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from typing import Annotated, Optional

from rich.table import Table
from typer import Argument

from .base import config_dir_type, dev_type, quiet_type, verbosity_type
from ..app.state import app_manager, console, plugin_manager
from ..helpers.format import format_input


def main(
args: Annotated[
Optional[list[str]],
Argument(
help="Names of the plugins to list.",
show_default=False,
),
] = None,
config_dir: config_dir_type = None,
dev: dev_type = False,
quiet: quiet_type = False,
verbosity: verbosity_type = 0,
) -> None:
"""
List all installed and custom plugins.
"""
app_manager().process_args(config_dir, dev, quiet, verbosity)
app_manager().load_config()

plugin_classes = plugin_manager().installed_plugin_classes

for installable_plugin_cls in plugin_manager().installable_plugin_classes:
if plugin_cls := next((x for x in plugin_classes if x.config.name == installable_plugin_cls.config.name), None):
plugin_cls.installable = True
plugin_cls.alt_version = installable_plugin_cls.config.version
else:
installable_plugin_cls.alt_version = installable_plugin_cls.config.version
plugin_classes.append(installable_plugin_cls)

if args:
for arg in args:
if plugin_cls := next((x for x in plugin_classes if x.config.name == arg), None):
console().print(
f"{plugin_cls.config.name}"
f"{f' ({plugin_cls.config.descriptive_name})' if plugin_cls.config.descriptive_name else ''}"
)
console().print(
f"{(plugin_cls.installed and plugin_cls.config.version) or '-'}"
f" -> {(plugin_cls.installable and plugin_cls.alt_version) or '-'}"
if plugin_cls.installable
else ""
)
console().print(
f"{'Custom' if plugin_cls.custom else 'Built-in'} plugin "
f"({'installed' if plugin_cls.installed else 'not installed'})"
)
console().print(plugin_cls.config.description or "No description.")
else:
console().print(f"Plugin {format_input(arg)} not found. Skipping.")

console().request_newline()
else:
if len(plugin_classes) == 0:
console().print("No plugins found.")
return

plugin_classes.sort(key=lambda x: x.config.name)
plugin_classes.sort(key=lambda x: not x.installed)

table = Table()
table.add_column("Name")
table.add_column("Current")
table.add_column("Available")
table.add_column("Installed")

for plugin_cls in plugin_classes:
table.add_row(
plugin_cls.config.name,
(plugin_cls.installed and plugin_cls.config.version) or "-",
(plugin_cls.installable and plugin_cls.alt_version) or "-",
"Yes" if plugin_cls.installed else "No",
)

console().print(table)
44 changes: 44 additions & 0 deletions src/lungo_cli/commands/uninstall.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from typing import Annotated

from typer import Argument

from .base import config_dir_type, dev_type, quiet_type, verbosity_type
from ..app.state import app_manager, console, plugin_manager
from ..helpers.format import format_input


def main(
args: Annotated[
list[str],
Argument(
help="Names of the plugins to remove.",
show_default=False,
),
],
config_dir: config_dir_type = None,
dev: dev_type = False,
quiet: quiet_type = False,
verbosity: verbosity_type = 0,
) -> None:
"""
Uninstall plugins.
"""
app_manager().process_args(config_dir, dev, quiet, verbosity)
app_manager().load_config()

count = 0

for arg in args:
plugin_cls = next((x for x in plugin_manager().installed_plugin_classes if x.config.name == arg), None)

if plugin_cls is None:
console().print_warning(f"Plugin {format_input(arg)} not found or not installed. Skipping.")
continue

plugin_manager().remove_plugin(plugin_cls)
count += 1

if count == 1:
console().print("1 plugin removed successfully.")
elif count > 1:
console().print(f"{count} plugins removed successfully.")
Loading

0 comments on commit 6553794

Please sign in to comment.