Source code for harp.config.builders.system

from typing import TYPE_CHECKING, Callable, Type, cast

from asgiref.typing import ASGIApplication
from whistle import IAsyncEventDispatcher

from harp import __revision__, __version__, get_logger
from harp.asgi import ASGIKernel
from harp.asgi.events import EVENT_CORE_REQUEST, EVENT_CORE_VIEW
from harp.event_dispatcher import LoggingAsyncEventDispatcher
from harp.services import Container, Services
from harp.typing import GlobalSettings
from harp.utils.network import Bind
from harp.views.json import on_json_response

from .. import ApplicationsRegistry
from ..events import (
    EVENT_BIND,
    EVENT_BOUND,
    EVENT_READY,
    EVENT_SHUTDOWN,
    OnBindEvent,
    OnBoundEvent,
    OnReadyEvent,
    OnShutdownEvent,
)

if TYPE_CHECKING:
    from harp.controllers import ProxyControllerResolver

logger = get_logger(__name__)


[docs] class System: """ The core, global-like system objects, encapsulated together. Useful to manipulate the system from a high-level perspective, either from a server adapter point of view or from tests. Bootstrapping will create a System instance, which will be used to start the various subsystems. Then, the subsystems will work on their own, and should not need to have a system view. Attributes: config (GlobalSettings): The global configuration settings for the application. dispatcher (IAsyncEventDispatcher): The event dispatcher for handling application-wide events. provider (Services): The service provider for dependency injection and service management. kernel (ASGIKernel): The ASGI kernel for handling HTTP requests. binds (list[Bind]): A list of network binds specifying where the application should listen for incoming requests. Methods: dispose(): Asynchronously disposes of the system resources, primarily the ASGI kernel. """
[docs] def __init__( self, config: GlobalSettings, dispatcher: IAsyncEventDispatcher, provider: Services, /, *, asgi_app: ASGIApplication, binds: list[Bind], ): self._config = config self._dispatcher = dispatcher self._provider = provider self._asgi_app = asgi_app self._binds = binds
@property def config(self) -> GlobalSettings: return self._config @property def dispatcher(self) -> IAsyncEventDispatcher: return self._dispatcher @property def provider(self) -> Services: return self._provider @property def asgi_app(self) -> ASGIApplication: return self._asgi_app @property def binds(self) -> list[Bind]: return self._binds
[docs] async def dispose(self): """ Asynchronously disposes of the system resources, primarily the ASGI kernel. """ if self.asgi_app: try: event = OnShutdownEvent(self.asgi_app, self.provider) await self.dispatcher.adispatch(EVENT_SHUTDOWN, event) except Exception as exc: logger.fatal("💣 Fatal while dispatching «%s» event: %s", EVENT_SHUTDOWN, exc) raise finally: self._asgi_app = None
[docs] class SystemBuilder: """ A builder class for constructing the System instance, which represents the core of the HARP application. This class is responsible for assembling the necessary components of the System, including the global configuration, event dispatcher, service provider, and ASGI kernel, based on the provided configuration. Attributes: AsyncEventDispatcherType (Type[IAsyncEventDispatcher]): The type of the event dispatcher to use. ContainerType (Type[Container]): The type of the container to use for dependency injection. KernelType (Type[ASGIKernel]): The type of the ASGI kernel to use for handling HTTP requests. configuration_builder (ConfigurationBuilder): The configuration builder used to assemble the global configuration. hostname (str): The hostname where the application should listen for incoming requests. Methods: abuild() -> System: Asynchronously builds and returns an instance of the System class. """ AsyncEventDispatcherType: Type[IAsyncEventDispatcher] = LoggingAsyncEventDispatcher ContainerType: Type[Container] = Container KernelType: Type[ASGIKernel] = ASGIKernel
[docs] def __init__( self, applications=ApplicationsRegistry | Callable[[], ApplicationsRegistry], configuration=GlobalSettings | Callable[[], GlobalSettings], /, *, hostname: str = "[::]", ): self._applications = applications self._configuration = configuration self.hostname = hostname
@property def applications(self) -> ApplicationsRegistry: if callable(self._applications): self._applications = self._applications() return self._applications @property def configuration(self) -> GlobalSettings: if callable(self._configuration): self._configuration = self._configuration() return self._configuration
[docs] async def abuild(self) -> System: """ Asynchronously builds and returns an instance of the System class, ready for use. Returns: System: An instance of the System class, assembled based on the provided configuration and components. """ logger.info(f"🎙 HARP v.{__version__} ({__revision__})") # Get lazy configuration. config = self.configuration logger.info(f"📦 {', '.join(self.applications.keys())}") for name, app in self.applications.items(): logger.debug(f'... "{name}" application loaded from "{app.path}"') # Prepare and dispatch «bind» event. dispatcher = self.build_dispatcher() container = self.build_container(config, dispatcher) await self.dispatch_bind_event(dispatcher, container, config) # todo: this looks like not the right place, should not be tightly coupled to the factory from harp.controllers import ProxyControllerResolver controller_resolver = ProxyControllerResolver() container.add_instance(controller_resolver, ProxyControllerResolver) # Prepare and dispatch «bound» event. provider = container.build_provider() await self.dispatch_bound_event(dispatcher, provider, controller_resolver) # Build the kernel and dispatch «ready» event. event = await self.dispatch_ready_event(dispatcher, provider, controller_resolver) # Send back a coherent view of the system. return System(config, dispatcher, provider, asgi_app=event.asgi_app, binds=event.binds)
[docs] def build_dispatcher(self): dispatcher = cast(IAsyncEventDispatcher, self.AsyncEventDispatcherType()) self.applications.register_events(dispatcher) self.__setup_core_events(dispatcher) return dispatcher
def __setup_core_events(self, dispatcher): # todo move into core or extension, this is not system or proxy related from harp.controllers.default import on_health_request dispatcher.add_listener(EVENT_CORE_REQUEST, on_health_request, priority=-100) dispatcher.add_listener(EVENT_CORE_VIEW, on_json_response)
[docs] def build_container(self, config, dispatcher): container = cast(Container, self.ContainerType()) container.add_instance(dispatcher, IAsyncEventDispatcher) container.add_instance(config, GlobalSettings) self.applications.register_services(container, config) return container
[docs] async def dispatch_bind_event( self, dispatcher: IAsyncEventDispatcher, container: Container, config, ) -> OnBindEvent: # dispatch "bind" event: this is the last chance to add services to the container try: return cast( OnBindEvent, await dispatcher.adispatch(EVENT_BIND, OnBindEvent(container, config)), ) except Exception as exc: logger.fatal("💣 Fatal while dispatching «%s» event: %s", EVENT_BIND, exc) raise
[docs] async def dispatch_bound_event( self, dispatcher: IAsyncEventDispatcher, provider: Services, resolver: "ProxyControllerResolver", ) -> OnBoundEvent: # dispatch "bound" event: you get a resolved container, do your thing try: return cast( OnBoundEvent, await dispatcher.adispatch(EVENT_BOUND, OnBoundEvent(provider, resolver)), ) except Exception as exc: logger.fatal("💣 Fatal while dispatching «%s» event: %s", EVENT_BOUND, exc) raise
[docs] async def dispatch_ready_event( self, dispatcher: IAsyncEventDispatcher, provider: Services, controller_resolver: "ProxyControllerResolver", ): # todo: this should be instanciated by the service container, probably using a factory to dispatch the event, # etc. Binds should not sit here, but where? asgi_app = self.KernelType(dispatcher=dispatcher, resolver=controller_resolver) binds = [Bind(host=self.hostname, port=port) for port in controller_resolver.ports] try: return cast( OnReadyEvent, await dispatcher.adispatch(EVENT_READY, OnReadyEvent(provider, asgi_app, binds)), ) except Exception as exc: logger.fatal("💣 Fatal while dispatching «%s» event: %s", EVENT_READY, exc) raise