Skip to content

Commit

Permalink
Merge pull request #22 from mikejgray/FEAT_PHALPlugins
Browse files Browse the repository at this point in the history
feat: phal plugins
  • Loading branch information
mikejgray authored Mar 4, 2024
2 parents 20b8515 + 148a312 commit ef08757
Show file tree
Hide file tree
Showing 9 changed files with 2,456 additions and 99 deletions.
1,725 changes: 1,628 additions & 97 deletions API.md

Large diffs are not rendered by default.

22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Add that to your `~/.bashrc` or `~/.zshrc` file to make it permanent. Be sure to

In a new directory, run:

`projen new ovosskill --from "@mikejgray/ovos-skill-projen@latest"`
`npx projen new ovosskill --from "@mikejgray/ovos-skill-projen@latest"`

**NOTE**: This repo is not yet available on NPM. Please add the following to your `~/.npmrc` file (create one if it doesn't exist), with [a GitHub token that has packages:read permissions](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry):

Expand Down Expand Up @@ -52,6 +52,26 @@ Example:

After editing `.projenrc.json`, run `pj` to regenerate the project files. This can automatically keep your project up to date with the latest changes, including managing your `setup.py` file.

## Create a new PHAL plugin template

In a new directory, run:

`npx projen new ovosphal --from "@mikejgray/ovos-skill-projen@latest"`

The instructions are the same as for creating a new skill template, except that you have an additional option to set the PHAL plugin as admin or not. The default is `false`, indicating that the PHAL plugin will not run as root.

**NOTE:** If you do set the PHAL plugin to `admin` and thus run as root, you will also need to update your OVOS config to explicitly allow your root plugins. This is a security risk and should only be done if you understand the implications. For more information about Admin PHAL, [see the OVOS technical manual](https://openvoiceos.github.io/ovos-technical-manual/PHAL/#admin-phal).

Example OVOS config:

```json
{
"PHAL": {
"admin": "ovos-phal-plugin-helloworld": {"enabled": true}
}
}
```

### setup.py ownership

Note that projen takes ownership of the `setup.py` file, and the expectation is that manual edits are not allowed. If you need to make changes to the `setup.py` file, you should do so by editing `.projenrc.json` and running `pj` to regenerate the file.
Expand Down
24 changes: 24 additions & 0 deletions src/files/README.phal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
interface readmePhalInterface {
skillClass: string;
authorName: string;
authorHandle: string;
};

export const readmePhalMd = ({
skillClass,
authorName,
authorHandle,
}: readmePhalInterface): string => {
return `# ${skillClass}
Template for an OVOS PHAL plugin.
\`\`\`python
self.bus.on("mycroft.ready", self.on_mycroft_ready)
\`\`\`
## Credits
${authorName} (@${authorHandle})
`;
};
28 changes: 28 additions & 0 deletions src/files/__init__.phal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from ovos_bus_client import Message
from ovos_plugin_manager.phal import PHALPlugin


class HelloWorldPlugin(PHALPlugin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.bus.on("mycroft.ready", self.on_mycroft_ready)

@property
def my_setting(self):
"""Dynamically get the my_setting from the config file(s).
Example:
{
"PHAL": {
"ovos-phal-plugin-helloworld": {
"my_setting": "my_value"
}
}
}
If it doesn't exist, return the default value.
"""
return self.config.get("my_setting", "default_value")

def on_mycroft_ready(self, message: Message):
"""Take action when OVOS is ready."""
self.log.info("OVOS reported ready, now I can do something!")
# Implement something here
135 changes: 135 additions & 0 deletions src/files/setup.py.phal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
interface setupPyInterface {
/**
* The URL of the plugin's GitHub repository.
*/
repositoryUrl: string;
/**
* The name of the directory containing the plugin's code.
* @default "" (root)
*/
packageDir: string;
/**
* The name of the plugin's author.
*/
author: string;
/**
* The email address of the plugin's author.
*/
authorAddress: string;
/**
* The name of the plugin class.
*/
pluginClass: string;
/**
* The name of the plugin's PyPi package.
*/
pypiName: string;
/**
* The description of the plugin.
*/
description: string;
/**
* The license of the plugin.
*/
license: string;
/**
* Whether the plugin is an admin PHAL plugin, meaning it runs as root.
* @default false
*/
admin?: boolean;
}

export const setupPhalPy = ({
repositoryUrl,
packageDir,
author,
authorAddress,
pypiName,
pluginClass,
description,
license,
admin,
}: setupPyInterface): string => {
return `#!/usr/bin/env python3
from setuptools import setup
from os import walk, path
BASEDIR = path.abspath(path.dirname(__file__))
URL = "${repositoryUrl}"
plugin_CLAZZ = "${pluginClass}" # needs to match __init__.py class name
PYPI_NAME = "${pypiName}" # pip install PYPI_NAME
# below derived from github url to ensure standard plugin_id
plugin_AUTHOR, plugin_NAME = URL.split(".com/")[-1].split("/")
plugin_PKG = plugin_NAME.lower().replace("-", "_")
PLUGIN_ENTRY_POINT = f"{plugin_NAME.lower()}.{plugin_AUTHOR.lower()}={plugin_PKG}:{plugin_CLAZZ}"
# plugin_id=package_name:pluginClass
BASE_PATH = BASE_PATH = path.abspath(path.join(path.dirname(__file__), "${packageDir}"))
def get_version():
"""Find the version of the package"""
version = None
version_file = path.join(BASE_PATH, "version.py")
major, minor, build, alpha = (None, None, None, None)
with open(version_file) as f:
for line in f:
if "VERSION_MAJOR" in line:
major = line.split("=")[1].strip()
elif "VERSION_MINOR" in line:
minor = line.split("=")[1].strip()
elif "VERSION_BUILD" in line:
build = line.split("=")[1].strip()
elif "VERSION_ALPHA" in line:
alpha = line.split("=")[1].strip()
if (major and minor and build and alpha) or "# END_VERSION_BLOCK" in line:
break
version = f"{major}.{minor}.{build}"
if alpha and int(alpha) > 0:
version += f"a{alpha}"
return version
def get_requirements(requirements_filename: str):
requirements_file = path.join(path.dirname(__file__), requirements_filename)
with open(requirements_file, "r", encoding="utf-8") as r:
requirements = r.readlines()
requirements = [r.strip() for r in requirements if r.strip() and not r.strip().startswith("#")]
return requirements
def find_resource_files():
resource_base_dirs = ("locale", "intents", "dialog", "vocab", "regex", "ui")
package_data = ["*.json"]
for res in resource_base_dirs:
if path.isdir(path.join(BASE_PATH, res)):
for directory, _, files in walk(path.join(BASE_PATH, res)):
if files:
package_data.append(path.join(directory.replace(BASE_PATH, "").lstrip("/"), "*"))
return package_data
with open("README.md", "r") as f:
long_description = f.read()
setup(
name=PYPI_NAME,
version=get_version(),
description="${description}",
long_description=long_description,
long_description_content_type="text/markdown",
url=URL,
author="${author}",
author_email="${authorAddress}",
license="${license}",
package_dir={plugin_PKG: "${packageDir ?? '.'}"},
package_data={plugin_PKG: find_resource_files()},
packages=[plugin_PKG],
include_package_data=True,
install_requires=get_requirements("requirements.txt"),
keywords="ovos plugin voice assistant",
entry_points={${admin ? 'ovos.plugin.phal.admin' : 'ovos.plugin.phal'}: PLUGIN_ENTRY_POINT},
)
`;
};
2 changes: 1 addition & 1 deletion src/files/setup.py.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
interface setupPyInterface {
export interface setupPyInterface {
/**
* The URL of the skill's GitHub repository.
*/
Expand Down
Loading

0 comments on commit ef08757

Please sign in to comment.