Source code for duck.app.base

"""
Base application primitives shared by Duck application classes.
"""

from typing import Optional, Dict, Callable

from duck.exceptions.all import ApplicationError
from duck.meta import Meta
from duck.utils.net import is_ipv4, is_ipv6
from duck.utils.path import url_normalize
from duck.utils.urlcrack import URL


APPS_REGISTRY: Dict[str, "BaseApp"] = {}


[docs] class BaseApp: """ Provides shared configuration and URL helpers for Duck app classes. Subclasses are responsible for creating their own server instance and implementing lifecycle methods such as `start_server`, `run`, and `stop`. """ DEFAULT_ADDR = "localhost" DEFAULT_PORT = 8000 def __init__( self, name: Optional[str] = None, addr: str = DEFAULT_ADDR, port: int = DEFAULT_PORT, domain: Optional[str] = None, server_url: Optional[str] = None, uses_ipv6: bool = False, enable_https: bool = False, no_checks: bool = False, workers: Optional[int] = None, force_worker_processes: bool = False, events: Optional[Dict[str, Optional[Callable]]] = None, ) -> None: """ Initializes shared app configuration. Args: name: Unique name to your application. addr: Address the server binds to. port: Port the server binds to. domain: Public-facing domain. Defaults to the bind address. server_url: Public-facing absolute server URL. uses_ipv6: Whether the app should bind using IPv6. enable_https: Whether HTTPS is enabled for this app. workers: Optional number of server workers. force_worker_processes: Determines whether to use worker **processes** instead of the default worker **threads**. By default, when `workers` is greater than 1, the server will use worker **threads**. Threads avoid cross-process synchronization issues—such as component registry mismatches (e.g., issues with Lively components) that occur when state lives in separate processes. Set this flag to `True` only when process isolation is explicitly desired **and** you do not require shared in-memory synchronization between workers. events: Events to handle e.g. {"on_start": some_callable}. Defaults to None. Raises: ApplicationError: If the provided bind address is invalid. """ # Note: Domain and Server URL may be different. # Validate bind address before storing it self.validate_addr(addr=addr, uses_ipv6=uses_ipv6) # Store runtime configuration self.name = self.resolve_name(name) self.enable_https = enable_https self.workers = workers self.force_worker_processes = force_worker_processes # Store network configuration self.addr = addr self.port = port self.uses_ipv6 = uses_ipv6 self.original_domain = domain self.domain = self.resolve_domain(addr=addr, domain=domain, uses_ipv6=uses_ipv6) self.server_url = self.resolve_server_url(server_url) self.no_checks = no_checks # Event map self.event_map = {"on_start": None, "on_pre_stop": None, **(events or {})} # Server is assigned by subclasses self.server = None # Run extra checks if not no_checks: self.run_checks() # Register ports when requested self.register_ports() # Add app to created apps. BaseApp.register_app(self.name, self) @property def running(self) -> bool: """ Returns True if the main server running else False. """ return self.server.running @property def server_up(self) -> bool: """ Checks whether the assigned server is running. Returns: True if the server exists and is running, otherwise False. """ return bool(self.server and self.server.running) @property def absolute_uri(self) -> str: """ Returns application server absolute `URL`. """ return self.server_url @property def absolute_ws_uri(self) -> str: """ Returns application server absolute WebSockets `URL`. """ url = self.server_url url_obj = URL(url) url_obj.scheme = "ws" if url_obj.scheme == "http" else 'wss' return url_obj.to_str()
[docs] @classmethod def get_all_apps(self) -> Dict[str, "BaseApp"]: """ Returns all created apps. """ return APPS_REGISTRY
[docs] @classmethod def get_app_by_name(name: str): """ Returns an app instance by name or else an ApplicationError is raised. """ app = APPS_REGISTRY.get(name, None) if not app: raise ApplicationError(f"Application with name '{name}' not found in registry.") # Finally, return the app instance. return app
[docs] @classmethod def register_app(cls, name: str, app: "BaseApp"): """ Registers an application. """ app = APPS_REGISTRY.get(name, None) if app: raise ApplicationError(f"An app with the name '{name}' already exists.") # Register app in registry APPS_REGISTRY[name] = app
[docs] @staticmethod def validate_addr(addr: str, uses_ipv6: bool = False) -> None: """ Validates the bind address. Args: addr: Address to validate. uses_ipv6: Whether the address should be validated as IPv6. Raises: ApplicationError: If the address is invalid. """ # Allow named hosts like localhost if str(addr).isalnum(): return # Validate IPv6 addresses if uses_ipv6 and not is_ipv6(addr): raise ApplicationError( "Argument uses_ipv6=True but addr is not a valid IPv6 address." ) # Validate IPv4 addresses if not uses_ipv6 and not is_ipv4(addr): raise ApplicationError("Argument `addr` is not a valid IPv4 address.")
[docs] @staticmethod def resolve_domain( addr: str, domain: Optional[str] = None, uses_ipv6: bool = False, ) -> str: """ Resolves the public domain for the app. Args: addr: Bind address used as fallback. domain: Explicit public-facing domain. uses_ipv6: Whether the address is IPv6. Returns: A browser-safe domain string. """ # Use explicit domain when provided resolved_domain = domain or (f"[{addr}]" if uses_ipv6 else addr) # Avoid exposing 0.0.0.0 as a browser URL if is_ipv4(resolved_domain) and resolved_domain.startswith("0"): return "localhost" return resolved_domain
[docs] def resolve_name( self, name: Optional[str] = None, ) -> str: """ Resolves a unique identifier for the app. Args: name: Name to use. Returns: A unique name string. """ if name: if name in APPS_REGISTRY: raise ApplicationError(f"Another app with the name '{name}' already exists. Please use a different name.") else: name = f"{self.__class__.__name__}-{len(APPS_REGISTRY)}" return name
[docs] def resolve_server_url(self, server_url: Optional[str] = None) -> str: """ Resolves the public absolute server URL. Args: server_url: Explicit public-facing URL. Returns: Absolute server URL for URL generation. """ # Respect explicit public URL for proxy/CDN deployments if server_url: return url_normalize(server_url) # Build URL from app protocol and domain protocol = "https" if self.enable_https else "http" return url_normalize(f"{protocol}://{self.domain}:{self.port}")
[docs] def run_checks(self): """ Run applications checks, will be implemented by subclass. """ pass
[docs] def register_ports(self) -> None: """ Registers a ports as occupied by this app - will be implemented by subclass. Note: It registers the app port by default. """ from duck.utils.port_registry import PortRegistry PortRegistry.register_port(self.port, f"{self}")
[docs] def build_absolute_uri(self, path: str = "") -> str: """ Builds an absolute HTTP URL from a path. Args: path: URL path to append to the app URL. Returns: Normalized absolute URL. """ return url_normalize(f"{self.absolute_uri}/{path.lstrip('/')}")
[docs] def build_absolute_ws_uri(self, path: str = "") -> str: """ Builds an absolute WebSocket URL from a path. Args: path: URL path to append to the WebSocket URL. Returns: Normalized absolute WebSocket URL. """ return url_normalize(f"{self.absolute_ws_uri}/{path.lstrip('/')}")
[docs] def run(self): """ The method for running the web application. """ raise NotImplementedError("The method 'run' must be implemented.")
[docs] def stop(self): """ The method for stopping the web application. """ raise NotImplementedError("The method 'stop' must be implemented.")
[docs] def register_event(self, event: str, handler: Optional[Callable] = None): """ Register an event. Args: event: The event to be handled. handler: An optional callable to handle the event. Defaults to None. """ self.event_map[event] = handler
[docs] def dispatch_event(self, event: str): """ Dispatch an event and make event handlers handle the event. """ event_handler = self.event_map.get(event, None) if event_handler is None and event not in self.event_map: raise ApplicationError(f"Event '{event}' does not appear to be registered.") # Execute event handler. if event_handler: event_handler(event, self)
[docs] def _on_app_start(self): """ Internal method called on application start. """ self.dispatch_event("on_start")