Source code for aioli.package

# -*- coding: utf-8 -*-

import logging
import re

from marshmallow.exceptions import ValidationError
from aioli.config import PackageConfigSchema


NAME_REGEX = re.compile(r"^[a-zA-Z0-9_]*$")
PATH_REGEX = re.compile(r"^/[a-zA-Z0-9-_]*$")

# Semantic version regex
# 1 - Major
# 2 - Minor
# 3 - Patch
# 4 (optional) - Pre-release version info
# 5 (optional) - Metadata (build time, number, etc.)

VERSION_REGEX = re.compile(
    r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-[a-zA-Z\d][-a-zA-Z.\d]*)?(\+[a-zA-Z\d][-a-zA-Z.\d]*)?$"
)


[docs]class Package: """Associates components and meta with a package, for registration with a Aioli Application. :param name: Package name ([a-z, A-Z, 0-9, -]) :param description: Package description :param version: Package semver version :param controllers: List of Controller classes to register with the Package :param services: List of Services classes to register with the Package :param config: Package Configuration Schema :ivar app: Application instance :ivar log: Package logger :ivar state: Package state :ivar path: Package Path :ivar name: Package Name :ivar version: Package Version :ivar config: Package config :ivar controllers: List of Controllers registered with the Package :ivar services: List of Services registered with the Package """ class State: __state = {} def __setitem__(self, key, value): self.__state[key] = value def __getitem__(self, item): return self.__state.get(item) __name = None __version = None __path = None app = None conf = {} log: logging.Logger services = [] controllers = [] def __init__( self, name, description, version, controllers=None, services=None, config=None, ): assert not controllers or isinstance(controllers, list), f"{name} controllers must be a list or None" assert not services or isinstance(services, list), f"{name} services must be a list " assert name not in ["aioli", "aioli_core"], f"Name {name} is reserved and cannot be used" if config is None: self.conf_schema = PackageConfigSchema elif not issubclass(config, PackageConfigSchema): raise Exception( f"Invalid config type in {name}: {config}. Must be subclass of {PackageConfigSchema}, or None" ) else: self.conf_schema = config self.state = Package.State() self.name = name self.description = description self.version = version self._services = set(services or []) self._controllers = set(controllers or []) async def detach_services(self): for obj in self.services: await obj.on_shutdown() async def attach_services(self): for obj in self.services: await obj.on_startup() async def attach_controllers(self): for obj in self.services: await obj.on_startup() async def register(self, app, config): self.app = app self.log = logging.getLogger(f"aioli.pkg.{self.name}") self.path = config.get("path", f"/{self.name}") try: self.conf = self.conf_schema(self.name).load(config) except ValidationError as e: raise Exception(f"Package {self.name} failed configuration validation: {e.messages}") if self._controllers: self.controllers = [ctrl_cls(self) for ctrl_cls in self._controllers] self.services = [svc_cls(self) for svc_cls in self._services] @property def path(self): return self.__path @path.setter def path(self, value): if not self._controllers: return if not PATH_REGEX.match(value): raise Exception(f"Package {self.name} version is not a valid SemVer string") self.__path = value @property def version(self): return self.__version @version.setter def version(self, value): if not VERSION_REGEX.match(value): raise Exception(f"Package {self.name} version is not a valid SemVer string") self.__version = value @property def name(self): return self.__name @name.setter def name(self, value): if not NAME_REGEX.match(value): raise Exception(f"Invalid identifier '{value}' - may only contain alphanumeric and underscore characters.") self.__name = value