Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Plugin Management System and Config Management #294

Merged
merged 10 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,55 @@
[![Github CI](https://github.com/yoda-pa/yoda/actions/workflows/ci.yml/badge.svg)](https://github.com/yoda-pa/yoda/actions/workflows/ci.yml)
[![PyPI version](https://badge.fury.io/py/yodapa.svg)](https://badge.fury.io/py/yodapa)

Personal Assistant on the command line.

## Installation

```bash
pip install yodapa

yoda --help
```

## Configure Yoda

```bash
yoda configure
```

## Plugins

### Write your own plugin for Yoda

Simply create a class with the `@yoda_plugin(name="plugin-name")` decorator and add methods to it. The non-private
methods will be automatically added as sub-commands to Yoda, with the command being the name you provide to the
decorator.

```python
import typer
from yodapa.plugin_manager.decorator import yoda_plugin


@yoda_plugin(name="hi")
class HiPlugin:
"""
Hi plugin. Say hello.

Example:
$ yoda hi hello --name MP
$ yoda hi hello
"""

def hello(self, name: str = None):
"""Say hello."""
name = name or "Padawan"
typer.echo(f"Hello {name}!")

def _private_method_should_not_be_added(self):
"""This method should not be added as a command."""
raise NotImplementedError()
```

## Development setup

```bash
Expand Down
81 changes: 72 additions & 9 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "yodapa"
version = "0.1.3"
version = "0.2.0"
description = "Personal Assistant on the command line"
authors = ["Man Parvesh Singh Randhawa <[email protected]>"]
license = "MIT"
Expand All @@ -15,6 +15,7 @@ homepage = "https://yoda-pa.github.io/"
python = "^3.9"
typer = "^0.12.5"
pytest = "^8.3.3"
pyyaml = "^6.0.2"

[tool.poetry.group.dev.dependencies]
pytest = "^8.3.3"
Expand Down
41 changes: 31 additions & 10 deletions src/cli/yoda.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,43 @@
from typing import Annotated, Optional

import typer

from yodapa import hi
from yodapa.hi import add_numbers
from yodapa.config import ConfigManager
from yodapa.plugin_manager.plugin import PluginManager


class Yoda:
"""Yoda main class."""

def __init__(self):
self.app = typer.Typer()
self.config = ConfigManager()
self.plugin_manager = PluginManager(self.app, self.config)

def init(self):
self.plugin_manager.discover_plugins()
self.plugin_manager.load_plugins()


yoda = Yoda()
yoda.init()

app = typer.Typer()
# define commands
app = yoda.app


@app.command()
def hello(name: str):
"""Greet someone by name."""
typer.echo(hi.hu(name))
def hello(name: Annotated[Optional[str], typer.Argument()] = None):
"""Say hello."""
name = name or yoda.config.get("user", "Skywalker")
typer.echo(f"Hello {name}!")


@app.command()
def add(a: int, b: int):
"""Add two numbers."""
result = add_numbers(a, b)
typer.echo(f"The sum of {a} and {b} is {result}")
def configure():
"""Configure Yoda."""
yoda.config.set("user", typer.prompt("What is your name?"))
typer.echo("Yoda configured!")


if __name__ == "__main__":
Expand Down
51 changes: 51 additions & 0 deletions src/yodapa/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from pathlib import Path
from typing import Any, Dict

import yaml


class ConfigManager:
"""Configuration manager class. Manages the configuration of Yoda."""

def __init__(self):
self.config_file = self.get_default_config_file()
self.config: Dict[str, Any] = dict()
self.load()

def load(self):
"""Load the configuration from the configuration file."""
if self.config_file.exists():
with open(self.config_file) as f:
self.config = yaml.safe_load(f)
else:
self.config = self.get_default_config()

def save(self):
"""Save the configuration to the configuration file."""
self.config_file.parent.mkdir(parents=True, exist_ok=True)
with open(self.config_file, "w") as f:
yaml.safe_dump(self.config, f)

def get(self, key: str, default: Any = None) -> Any:
"""Get a configuration value by key."""
return self.config.get(key, default)

def set(self, key: str, value: Any):
"""Set a configuration value by key."""
self.config[key] = value
self.save()

def get_yoda_config_dir(self) -> Path:
return Path.home() / ".yoda"

def get_yoda_plugins_dir(self) -> Path:
return self.get_yoda_config_dir() / "plugins"

def get_default_config_file(self) -> Path:
return self.get_yoda_config_dir() / "config.yaml"

def get_default_config(self) -> Dict[str, Any]:
return {
"user": "Skywalker",
"plugins": {},
}
9 changes: 0 additions & 9 deletions src/yodapa/hi.py

This file was deleted.

Empty file.
31 changes: 31 additions & 0 deletions src/yodapa/plugin_manager/decorator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import inspect
from typing import Annotated, Optional

import typer


def yoda_plugin(name: Annotated[Optional[str], typer.Argument()] = None):
"""
Decorator to turn a class into a Yoda PA plugin.
All public methods of the class are added as Typer commands.
"""

def decorator(cls):
nonlocal name
name = name or cls.__name__.lower()

def __init__(self):
self.typer_app = typer.Typer(name=name, help=f"{name} plugin commands")

for method_name, method in inspect.getmembers(self, predicate=inspect.ismethod):
# Skip private methods
if method_name.startswith("_"):
continue

self.typer_app.command()(method)

cls.__init__ = __init__
cls.name = name
return cls

return decorator
Loading
Loading