forked from platypush/platypush
Refactored the interface of Platydock and manifest utils.
This commit is contained in:
parent
a99ffea37c
commit
71c5291190
2 changed files with 285 additions and 151 deletions
|
@ -4,6 +4,7 @@ Dockerfile for Platypush starting from a configuration file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
from enum import Enum
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
|
@ -11,73 +12,108 @@ import sys
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
|
|
||||||
from platypush.config import Config
|
from platypush.config import Config
|
||||||
from platypush.utils.manifest import Dependencies
|
from platypush.utils.manifest import Dependencies, InstallContext, PackageManagers
|
||||||
|
|
||||||
ERR_PREFIX = '\n\033[6;31;47mERROR\033[0;91m '
|
|
||||||
ERR_SUFFIX = '\033[0m'
|
|
||||||
|
|
||||||
|
|
||||||
def generate_dockerfile(cfgfile: str) -> str:
|
class BaseImage(Enum):
|
||||||
"""
|
"""
|
||||||
Generate a Dockerfile based on a configuration file.
|
Supported base images for Dockerfiles.
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALPINE = 'alpine'
|
||||||
|
UBUNTU = 'ubuntu'
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""
|
||||||
|
Explicit __str__ override for argparse purposes.
|
||||||
|
"""
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
|
class DockerfileGenerator:
|
||||||
|
"""
|
||||||
|
Generate a Dockerfile from on a configuration file.
|
||||||
|
|
||||||
:param cfgfile: Path to the configuration file.
|
:param cfgfile: Path to the configuration file.
|
||||||
:return: The content of the generated Dockerfile.
|
:param image: The base image to use.
|
||||||
"""
|
"""
|
||||||
Config.init(cfgfile)
|
|
||||||
new_file_lines = []
|
|
||||||
ports = _get_exposed_ports()
|
|
||||||
deps = Dependencies.from_config(cfgfile, pkg_manager='apk')
|
|
||||||
is_after_expose_cmd = False
|
|
||||||
base_file = os.path.join(
|
|
||||||
str(pathlib.Path(inspect.getfile(Config)).parent), 'docker', 'base.Dockerfile'
|
|
||||||
)
|
|
||||||
|
|
||||||
with open(base_file, 'r') as f:
|
_pkg_manager_by_base_image = {
|
||||||
file_lines = [line.rstrip() for line in f.readlines()]
|
BaseImage.ALPINE: PackageManagers.APK,
|
||||||
|
BaseImage.UBUNTU: PackageManagers.APT,
|
||||||
for line in file_lines:
|
|
||||||
if line.startswith('RUN cd /install '):
|
|
||||||
for new_line in deps.before:
|
|
||||||
new_file_lines.append('RUN ' + new_line)
|
|
||||||
|
|
||||||
for new_line in deps.to_pkg_install_commands(
|
|
||||||
pkg_manager='apk', skip_sudo=True
|
|
||||||
):
|
|
||||||
new_file_lines.append('RUN ' + new_line)
|
|
||||||
elif line == 'RUN rm -rf /install':
|
|
||||||
for new_line in deps.to_pip_install_commands():
|
|
||||||
new_file_lines.append('RUN ' + new_line)
|
|
||||||
|
|
||||||
for new_line in deps.after:
|
|
||||||
new_file_lines.append('RUN' + new_line)
|
|
||||||
elif line.startswith('EXPOSE ') and ports:
|
|
||||||
if not is_after_expose_cmd:
|
|
||||||
new_file_lines.extend([f'EXPOSE {port}' for port in ports])
|
|
||||||
is_after_expose_cmd = True
|
|
||||||
|
|
||||||
continue
|
|
||||||
|
|
||||||
new_file_lines.append(line)
|
|
||||||
|
|
||||||
return '\n'.join(new_file_lines)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_exposed_ports() -> Iterable[int]:
|
|
||||||
"""
|
|
||||||
:return: The listen ports used by the backends enabled in the configuration
|
|
||||||
file.
|
|
||||||
"""
|
|
||||||
backends_config = Config.get_backends()
|
|
||||||
return {
|
|
||||||
int(port)
|
|
||||||
for port in (
|
|
||||||
backends_config.get('http', {}).get('port'),
|
|
||||||
backends_config.get('tcp', {}).get('port'),
|
|
||||||
)
|
|
||||||
if port
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def __init__(self, cfgfile: str, image: BaseImage) -> None:
|
||||||
|
self.cfgfile = os.path.abspath(os.path.expanduser(cfgfile))
|
||||||
|
self.image = image
|
||||||
|
|
||||||
|
def generate(self) -> str:
|
||||||
|
"""
|
||||||
|
Generate a Dockerfile based on a configuration file.
|
||||||
|
|
||||||
|
:param cfgfile: Path to the configuration file.
|
||||||
|
:return: The content of the generated Dockerfile.
|
||||||
|
"""
|
||||||
|
Config.init(self.cfgfile)
|
||||||
|
new_file_lines = []
|
||||||
|
ports = self._get_exposed_ports()
|
||||||
|
pkg_manager = self._pkg_manager_by_base_image[self.image]
|
||||||
|
deps = Dependencies.from_config(
|
||||||
|
self.cfgfile,
|
||||||
|
pkg_manager=pkg_manager,
|
||||||
|
install_context=InstallContext.DOCKER,
|
||||||
|
)
|
||||||
|
|
||||||
|
is_after_expose_cmd = False
|
||||||
|
base_file = os.path.join(
|
||||||
|
str(pathlib.Path(inspect.getfile(Config)).parent),
|
||||||
|
'docker',
|
||||||
|
f'{self.image}.Dockerfile',
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(base_file, 'r') as f:
|
||||||
|
file_lines = [line.rstrip() for line in f.readlines()]
|
||||||
|
|
||||||
|
for line in file_lines:
|
||||||
|
if line.startswith('RUN cd /install '):
|
||||||
|
for new_line in deps.before:
|
||||||
|
new_file_lines.append('RUN ' + new_line)
|
||||||
|
|
||||||
|
for new_line in deps.to_pkg_install_commands():
|
||||||
|
new_file_lines.append('RUN ' + new_line)
|
||||||
|
elif line == 'RUN rm -rf /install':
|
||||||
|
for new_line in deps.to_pip_install_commands():
|
||||||
|
new_file_lines.append('RUN ' + new_line)
|
||||||
|
|
||||||
|
for new_line in deps.after:
|
||||||
|
new_file_lines.append('RUN' + new_line)
|
||||||
|
elif line.startswith('EXPOSE ') and ports:
|
||||||
|
if not is_after_expose_cmd:
|
||||||
|
new_file_lines.extend([f'EXPOSE {port}' for port in ports])
|
||||||
|
is_after_expose_cmd = True
|
||||||
|
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_file_lines.append(line)
|
||||||
|
|
||||||
|
return '\n'.join(new_file_lines)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_exposed_ports() -> Iterable[int]:
|
||||||
|
"""
|
||||||
|
:return: The listen ports used by the backends enabled in the configuration
|
||||||
|
file.
|
||||||
|
"""
|
||||||
|
backends_config = Config.get_backends()
|
||||||
|
return {
|
||||||
|
int(port)
|
||||||
|
for port in (
|
||||||
|
backends_config.get('http', {}).get('port'),
|
||||||
|
backends_config.get('tcp', {}).get('port'),
|
||||||
|
)
|
||||||
|
if port
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""
|
"""
|
||||||
|
@ -87,22 +123,44 @@ def main():
|
||||||
prog='platydock',
|
prog='platydock',
|
||||||
add_help=False,
|
add_help=False,
|
||||||
description='Create a Platypush Dockerfile from a config.yaml.',
|
description='Create a Platypush Dockerfile from a config.yaml.',
|
||||||
epilog='Use platydock <action> --help to get additional help.',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument('-h', '--help', action='store_true', help='Show usage')
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'cfgfile', type=str, nargs=1, help='The path to the configuration file.'
|
'-h', '--help', dest='show_usage', action='store_true', help='Show usage'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'cfgfile', type=str, nargs='?', help='The path to the configuration file.'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--image',
|
||||||
|
'-i',
|
||||||
|
dest='image',
|
||||||
|
required=False,
|
||||||
|
type=BaseImage,
|
||||||
|
choices=list(BaseImage),
|
||||||
|
default=BaseImage.ALPINE,
|
||||||
|
help='Base image to use for the Dockerfile.',
|
||||||
)
|
)
|
||||||
|
|
||||||
opts, _ = parser.parse_known_args(sys.argv[1:])
|
opts, _ = parser.parse_known_args(sys.argv[1:])
|
||||||
cfgfile = os.path.abspath(os.path.expanduser(opts.cfgfile[0]))
|
if opts.show_usage:
|
||||||
dockerfile = generate_dockerfile(cfgfile)
|
parser.print_help()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if not opts.cfgfile:
|
||||||
|
print(
|
||||||
|
f'Please specify a configuration file.\nRun {sys.argv[0]} --help to get the available options.',
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
dockerfile = DockerfileGenerator(opts.cfgfile, image=opts.image).generate()
|
||||||
print(dockerfile)
|
print(dockerfile)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
sys.exit(main())
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
import enum
|
from enum import Enum
|
||||||
import importlib
|
import importlib
|
||||||
import inspect
|
import inspect
|
||||||
import json
|
import json
|
||||||
|
@ -20,22 +21,102 @@ from typing import (
|
||||||
Type,
|
Type,
|
||||||
Union,
|
Union,
|
||||||
)
|
)
|
||||||
|
from typing_extensions import override
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from platypush.message.event import Event
|
from platypush.message.event import Event
|
||||||
|
|
||||||
supported_package_managers = {
|
|
||||||
'apk': ['apk', 'add', '--update', '--no-interactive', '--no-cache'],
|
|
||||||
'apt': ['apt', 'install', '-y'],
|
|
||||||
'pacman': ['pacman', '-S', '--noconfirm'],
|
|
||||||
}
|
|
||||||
|
|
||||||
_available_package_manager = None
|
_available_package_manager = None
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ManifestType(enum.Enum):
|
@dataclass
|
||||||
|
class PackageManager:
|
||||||
|
"""
|
||||||
|
Representation of a package manager.
|
||||||
|
"""
|
||||||
|
|
||||||
|
executable: str
|
||||||
|
""" The executable name. """
|
||||||
|
command: Iterable[str] = field(default_factory=tuple)
|
||||||
|
""" The command to execute, as a sequence of strings. """
|
||||||
|
|
||||||
|
|
||||||
|
class PackageManagers(Enum):
|
||||||
|
"""
|
||||||
|
Supported package managers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
APK = PackageManager(
|
||||||
|
executable='apk',
|
||||||
|
command=('apk', 'add', '--update', '--no-interactive', '--no-cache'),
|
||||||
|
)
|
||||||
|
|
||||||
|
APT = PackageManager(
|
||||||
|
executable='apt',
|
||||||
|
command=('DEBIAN_FRONTEND=noninteractive', 'apt', 'install', '-y'),
|
||||||
|
)
|
||||||
|
|
||||||
|
PACMAN = PackageManager(
|
||||||
|
executable='pacman',
|
||||||
|
command=('pacman', '-S', '--noconfirm'),
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_command(cls, name: str) -> Iterable[str]:
|
||||||
|
"""
|
||||||
|
:param name: The name of the package manager executable to get the
|
||||||
|
command for.
|
||||||
|
:return: The base command to execute, as a sequence of strings.
|
||||||
|
"""
|
||||||
|
pkg_manager = next(iter(pm for pm in cls if pm.value.executable == name), None)
|
||||||
|
if not pkg_manager:
|
||||||
|
raise ValueError(f'Unknown package manager: {name}')
|
||||||
|
|
||||||
|
return pkg_manager.value.command
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def scan(cls) -> Optional["PackageManagers"]:
|
||||||
|
"""
|
||||||
|
Get the name of the available package manager on the system, if supported.
|
||||||
|
"""
|
||||||
|
# pylint: disable=global-statement
|
||||||
|
global _available_package_manager
|
||||||
|
if _available_package_manager:
|
||||||
|
return _available_package_manager
|
||||||
|
|
||||||
|
available_package_managers = [
|
||||||
|
pkg_manager
|
||||||
|
for pkg_manager in cls
|
||||||
|
if shutil.which(pkg_manager.value.executable)
|
||||||
|
]
|
||||||
|
|
||||||
|
if not available_package_managers:
|
||||||
|
logger.warning(
|
||||||
|
'\nYour OS does not provide any of the supported package managers.\n'
|
||||||
|
'You may have to install some optional dependencies manually.\n'
|
||||||
|
'Supported package managers: %s.\n',
|
||||||
|
', '.join([pm.value.executable for pm in cls]),
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
_available_package_manager = available_package_managers[0]
|
||||||
|
return _available_package_manager
|
||||||
|
|
||||||
|
|
||||||
|
class InstallContext(Enum):
|
||||||
|
"""
|
||||||
|
Supported installation contexts.
|
||||||
|
"""
|
||||||
|
|
||||||
|
NONE = None
|
||||||
|
DOCKER = 'docker'
|
||||||
|
VENV = 'venv'
|
||||||
|
|
||||||
|
|
||||||
|
class ManifestType(Enum):
|
||||||
"""
|
"""
|
||||||
Manifest types.
|
Manifest types.
|
||||||
"""
|
"""
|
||||||
|
@ -58,15 +139,32 @@ class Dependencies:
|
||||||
""" pip dependencies. """
|
""" pip dependencies. """
|
||||||
after: List[str] = field(default_factory=list)
|
after: List[str] = field(default_factory=list)
|
||||||
""" Commands to execute after the component is installed. """
|
""" Commands to execute after the component is installed. """
|
||||||
|
pkg_manager: Optional[PackageManagers] = None
|
||||||
|
""" Override the default package manager detected on the system. """
|
||||||
|
install_context: InstallContext = InstallContext.NONE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _is_venv(self) -> bool:
|
||||||
|
"""
|
||||||
|
:return: True if the dependencies scanning logic is running either in a
|
||||||
|
virtual environment or in a virtual environment preparation
|
||||||
|
context.
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
self.install_context == InstallContext.VENV or sys.prefix != sys.base_prefix
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_config(
|
def from_config(
|
||||||
cls, conf_file: Optional[str] = None, pkg_manager: Optional[str] = None
|
cls,
|
||||||
|
conf_file: Optional[str] = None,
|
||||||
|
pkg_manager: Optional[PackageManagers] = None,
|
||||||
|
install_context: InstallContext = InstallContext.NONE,
|
||||||
) -> "Dependencies":
|
) -> "Dependencies":
|
||||||
"""
|
"""
|
||||||
Parse the required dependencies from a configuration file.
|
Parse the required dependencies from a configuration file.
|
||||||
"""
|
"""
|
||||||
deps = cls()
|
deps = cls(pkg_manager=pkg_manager, install_context=install_context)
|
||||||
|
|
||||||
for manifest in Manifests.by_config(conf_file, pkg_manager=pkg_manager):
|
for manifest in Manifests.by_config(conf_file, pkg_manager=pkg_manager):
|
||||||
deps.before += manifest.install.before
|
deps.before += manifest.install.before
|
||||||
|
@ -76,26 +174,19 @@ class Dependencies:
|
||||||
|
|
||||||
return deps
|
return deps
|
||||||
|
|
||||||
def to_pkg_install_commands(
|
def to_pkg_install_commands(self) -> Generator[str, None, None]:
|
||||||
self, pkg_manager: Optional[str] = None, skip_sudo: bool = False
|
|
||||||
) -> Generator[str, None, None]:
|
|
||||||
"""
|
"""
|
||||||
Generates the package manager commands required to install the given
|
Generates the package manager commands required to install the given
|
||||||
dependencies on the system.
|
dependencies on the system.
|
||||||
|
|
||||||
:param pkg_manager: Force package manager to use (default: looks for
|
|
||||||
the one available on the system).
|
|
||||||
:param skip_sudo: Skip sudo when installing packages (default: it will
|
|
||||||
look if the current user is root and use sudo otherwise).
|
|
||||||
"""
|
"""
|
||||||
wants_sudo = not skip_sudo and os.getuid() != 0
|
wants_sudo = self.install_context != InstallContext.DOCKER and os.getuid() != 0
|
||||||
pkg_manager = pkg_manager or get_available_package_manager()
|
pkg_manager = self.pkg_manager or PackageManagers.scan()
|
||||||
if self.packages and pkg_manager:
|
if self.packages and pkg_manager:
|
||||||
yield ' '.join(
|
yield ' '.join(
|
||||||
[
|
[
|
||||||
*(['sudo'] if wants_sudo else []),
|
*(['sudo'] if wants_sudo else []),
|
||||||
*supported_package_managers[pkg_manager],
|
*pkg_manager.value.command,
|
||||||
*sorted(self.packages),
|
*sorted(self.packages), # type: ignore
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -104,11 +195,14 @@ class Dependencies:
|
||||||
Generates the pip commands required to install the given dependencies on
|
Generates the pip commands required to install the given dependencies on
|
||||||
the system.
|
the system.
|
||||||
"""
|
"""
|
||||||
# Recent versions want an explicit --break-system-packages option when
|
wants_break_system_packages = not (
|
||||||
# installing packages via pip outside of a virtual environment
|
# Docker installations shouldn't require --break-system-packages in pip
|
||||||
wants_break_system_packages = (
|
self.install_context == InstallContext.DOCKER
|
||||||
sys.version_info > (3, 10)
|
# --break-system-packages has been introduced in Python 3.10
|
||||||
and sys.prefix == sys.base_prefix # We're not in a venv
|
or sys.version_info < (3, 11)
|
||||||
|
# If we're in a virtual environment then we don't need
|
||||||
|
# --break-system-packages
|
||||||
|
or self._is_venv
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.pip:
|
if self.pip:
|
||||||
|
@ -118,24 +212,15 @@ class Dependencies:
|
||||||
+ ' '.join(sorted(self.pip))
|
+ ' '.join(sorted(self.pip))
|
||||||
)
|
)
|
||||||
|
|
||||||
def to_install_commands(
|
def to_install_commands(self) -> Generator[str, None, None]:
|
||||||
self, pkg_manager: Optional[str] = None, skip_sudo: bool = False
|
|
||||||
) -> Generator[str, None, None]:
|
|
||||||
"""
|
"""
|
||||||
Generates the commands required to install the given dependencies on
|
Generates the commands required to install the given dependencies on
|
||||||
this system.
|
this system.
|
||||||
|
|
||||||
:param pkg_manager: Force package manager to use (default: looks for
|
|
||||||
the one available on the system).
|
|
||||||
:param skip_sudo: Skip sudo when installing packages (default: it will
|
|
||||||
look if the current user is root and use sudo otherwise).
|
|
||||||
"""
|
"""
|
||||||
for cmd in self.before:
|
for cmd in self.before:
|
||||||
yield cmd
|
yield cmd
|
||||||
|
|
||||||
for cmd in self.to_pkg_install_commands(
|
for cmd in self.to_pkg_install_commands():
|
||||||
pkg_manager=pkg_manager, skip_sudo=skip_sudo
|
|
||||||
):
|
|
||||||
yield cmd
|
yield cmd
|
||||||
|
|
||||||
for cmd in self.to_pip_install_commands():
|
for cmd in self.to_pip_install_commands():
|
||||||
|
@ -145,7 +230,7 @@ class Dependencies:
|
||||||
yield cmd
|
yield cmd
|
||||||
|
|
||||||
|
|
||||||
class Manifest:
|
class Manifest(ABC):
|
||||||
"""
|
"""
|
||||||
Base class for plugin/backend manifests.
|
Base class for plugin/backend manifests.
|
||||||
"""
|
"""
|
||||||
|
@ -156,10 +241,10 @@ class Manifest:
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
install: Optional[Dict[str, Iterable[str]]] = None,
|
install: Optional[Dict[str, Iterable[str]]] = None,
|
||||||
events: Optional[Mapping] = None,
|
events: Optional[Mapping] = None,
|
||||||
pkg_manager: Optional[str] = None,
|
pkg_manager: Optional[PackageManagers] = None,
|
||||||
**_,
|
**_,
|
||||||
):
|
):
|
||||||
self._pkg_manager = pkg_manager or get_available_package_manager()
|
self._pkg_manager = pkg_manager or PackageManagers.scan()
|
||||||
self.description = description
|
self.description = description
|
||||||
self.install = self._init_deps(install or {})
|
self.install = self._init_deps(install or {})
|
||||||
self.events = self._init_events(events or {})
|
self.events = self._init_events(events or {})
|
||||||
|
@ -167,6 +252,13 @@ class Manifest:
|
||||||
self.component_name = '.'.join(package.split('.')[2:])
|
self.component_name = '.'.join(package.split('.')[2:])
|
||||||
self.component = None
|
self.component = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def manifest_type(self) -> ManifestType:
|
||||||
|
"""
|
||||||
|
:return: The type of the manifest.
|
||||||
|
"""
|
||||||
|
|
||||||
def _init_deps(self, install: Mapping[str, Iterable[str]]) -> Dependencies:
|
def _init_deps(self, install: Mapping[str, Iterable[str]]) -> Dependencies:
|
||||||
deps = Dependencies()
|
deps = Dependencies()
|
||||||
for key, items in install.items():
|
for key, items in install.items():
|
||||||
|
@ -176,7 +268,7 @@ class Manifest:
|
||||||
deps.before += items
|
deps.before += items
|
||||||
elif key == 'after':
|
elif key == 'after':
|
||||||
deps.after += items
|
deps.after += items
|
||||||
elif key == self._pkg_manager:
|
elif self._pkg_manager and key == self._pkg_manager.value.executable:
|
||||||
deps.packages.update(items)
|
deps.packages.update(items)
|
||||||
|
|
||||||
return deps
|
return deps
|
||||||
|
@ -201,7 +293,9 @@ class Manifest:
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_file(cls, filename: str, pkg_manager: Optional[str] = None) -> "Manifest":
|
def from_file(
|
||||||
|
cls, filename: str, pkg_manager: Optional[PackageManagers] = None
|
||||||
|
) -> "Manifest":
|
||||||
"""
|
"""
|
||||||
Parse a manifest filename into a ``Manifest`` class.
|
Parse a manifest filename into a ``Manifest`` class.
|
||||||
"""
|
"""
|
||||||
|
@ -210,9 +304,21 @@ class Manifest:
|
||||||
|
|
||||||
assert 'type' in manifest, f'Manifest file {filename} has no type field'
|
assert 'type' in manifest, f'Manifest file {filename} has no type field'
|
||||||
comp_type = ManifestType(manifest.pop('type'))
|
comp_type = ManifestType(manifest.pop('type'))
|
||||||
manifest_class = _manifest_class_by_type[comp_type]
|
manifest_class = cls.by_type(comp_type)
|
||||||
return manifest_class(**manifest, pkg_manager=pkg_manager)
|
return manifest_class(**manifest, pkg_manager=pkg_manager)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def by_type(cls, manifest_type: ManifestType) -> Type["Manifest"]:
|
||||||
|
"""
|
||||||
|
:return: The manifest class corresponding to the given manifest type.
|
||||||
|
"""
|
||||||
|
if manifest_type == ManifestType.PLUGIN:
|
||||||
|
return PluginManifest
|
||||||
|
if manifest_type == ManifestType.BACKEND:
|
||||||
|
return BackendManifest
|
||||||
|
|
||||||
|
raise ValueError(f'Unknown manifest type: {manifest_type}')
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
"""
|
"""
|
||||||
:return: A JSON serialized representation of the manifest.
|
:return: A JSON serialized representation of the manifest.
|
||||||
|
@ -225,19 +331,23 @@ class Manifest:
|
||||||
'.'.join([evt_type.__module__, evt_type.__name__]): doc
|
'.'.join([evt_type.__module__, evt_type.__name__]): doc
|
||||||
for evt_type, doc in self.events.items()
|
for evt_type, doc in self.events.items()
|
||||||
},
|
},
|
||||||
'type': _manifest_type_by_class[self.__class__].value,
|
'type': self.manifest_type.value,
|
||||||
'package': self.package,
|
'package': self.package,
|
||||||
'component_name': self.component_name,
|
'component_name': self.component_name,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-few-public-methods
|
|
||||||
class PluginManifest(Manifest):
|
class PluginManifest(Manifest):
|
||||||
"""
|
"""
|
||||||
Plugin manifest.
|
Plugin manifest.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@override
|
||||||
|
def manifest_type(self) -> ManifestType:
|
||||||
|
return ManifestType.PLUGIN
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
class BackendManifest(Manifest):
|
class BackendManifest(Manifest):
|
||||||
|
@ -245,6 +355,11 @@ class BackendManifest(Manifest):
|
||||||
Backend manifest.
|
Backend manifest.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@override
|
||||||
|
def manifest_type(self) -> ManifestType:
|
||||||
|
return ManifestType.BACKEND
|
||||||
|
|
||||||
|
|
||||||
class Manifests:
|
class Manifests:
|
||||||
"""
|
"""
|
||||||
|
@ -253,7 +368,7 @@ class Manifests:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def by_base_class(
|
def by_base_class(
|
||||||
base_class: Type, pkg_manager: Optional[str] = None
|
base_class: Type, pkg_manager: Optional[PackageManagers] = None
|
||||||
) -> Generator[Manifest, None, None]:
|
) -> Generator[Manifest, None, None]:
|
||||||
"""
|
"""
|
||||||
Get all the manifest files declared under the base path of a given class
|
Get all the manifest files declared under the base path of a given class
|
||||||
|
@ -267,7 +382,7 @@ class Manifests:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def by_config(
|
def by_config(
|
||||||
conf_file: Optional[str] = None,
|
conf_file: Optional[str] = None,
|
||||||
pkg_manager: Optional[str] = None,
|
pkg_manager: Optional[PackageManagers] = None,
|
||||||
) -> Generator[Manifest, None, None]:
|
) -> Generator[Manifest, None, None]:
|
||||||
"""
|
"""
|
||||||
Get all the manifest objects associated to the extensions declared in a
|
Get all the manifest objects associated to the extensions declared in a
|
||||||
|
@ -294,42 +409,3 @@ class Manifests:
|
||||||
os.path.join(app_dir, 'plugins', *name.split('.'), 'manifest.yaml'),
|
os.path.join(app_dir, 'plugins', *name.split('.'), 'manifest.yaml'),
|
||||||
pkg_manager=pkg_manager,
|
pkg_manager=pkg_manager,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_available_package_manager() -> Optional[str]:
|
|
||||||
"""
|
|
||||||
Get the name of the available package manager on the system, if supported.
|
|
||||||
"""
|
|
||||||
# pylint: disable=global-statement
|
|
||||||
global _available_package_manager
|
|
||||||
if _available_package_manager:
|
|
||||||
return _available_package_manager
|
|
||||||
|
|
||||||
available_package_managers = [
|
|
||||||
pkg_manager
|
|
||||||
for pkg_manager in supported_package_managers
|
|
||||||
if shutil.which(pkg_manager)
|
|
||||||
]
|
|
||||||
|
|
||||||
if not available_package_managers:
|
|
||||||
logger.warning(
|
|
||||||
'\nYour OS does not provide any of the supported package managers.\n'
|
|
||||||
'You may have to install some optional dependencies manually.\n'
|
|
||||||
'Supported package managers: %s.\n',
|
|
||||||
', '.join(supported_package_managers.keys()),
|
|
||||||
)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
_available_package_manager = available_package_managers[0]
|
|
||||||
return _available_package_manager
|
|
||||||
|
|
||||||
|
|
||||||
_manifest_class_by_type: Mapping[ManifestType, Type[Manifest]] = {
|
|
||||||
ManifestType.PLUGIN: PluginManifest,
|
|
||||||
ManifestType.BACKEND: BackendManifest,
|
|
||||||
}
|
|
||||||
|
|
||||||
_manifest_type_by_class: Mapping[Type[Manifest], ManifestType] = {
|
|
||||||
cls: t for t, cls in _manifest_class_by_type.items()
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue