1import importlib.metadata
  2import sys
  3from importlib.metadata import entry_points
  4from pathlib import Path
  5from typing import Self
  6
  7from plain.logs.configure import configure_logging
  8from plain.packages import packages_registry
  9
 10from .secret import Secret
 11from .user_settings import Settings
 12
 13try:
 14    __version__ = importlib.metadata.version("plain")
 15except importlib.metadata.PackageNotFoundError:
 16    __version__ = "dev"
 17
 18
 19# Made available without setup or settings
 20APP_PATH = Path.cwd() / "app"
 21PLAIN_TEMP_PATH = Path.cwd() / ".plain"
 22
 23# from plain.runtime import settings
 24settings = Settings()
 25
 26_is_setup_complete = False
 27
 28
 29class AppPathNotFound(RuntimeError):
 30    pass
 31
 32
 33class SetupError(RuntimeError):
 34    pass
 35
 36
 37def setup() -> None:
 38    """
 39    Configure the settings (this happens as a side effect of accessing the
 40    first setting), configure logging and populate the app registry.
 41    """
 42    global _is_setup_complete
 43
 44    if _is_setup_complete:
 45        raise SetupError(
 46            "Plain runtime is already set up. You can only call `setup()` once."
 47        )
 48    else:
 49        # Make sure we don't call setup() again
 50        _is_setup_complete = True
 51
 52    # Packages can hook into the setup process through an entrypoint.
 53    for entry_point in entry_points().select(group="plain.setup"):
 54        entry_point.load()()
 55
 56    if not APP_PATH.exists():
 57        raise AppPathNotFound(
 58            "No app directory found. Are you sure you're in a Plain project?"
 59        )
 60
 61    # Automatically put the project dir on the Python path
 62    # which doesn't otherwise happen when you run `plain` commands.
 63    # This makes "app.<module>" imports and relative imports work.
 64    if APP_PATH.parent.as_posix() not in sys.path:
 65        sys.path.insert(0, APP_PATH.parent.as_posix())
 66
 67    configure_logging(
 68        plain_log_level=settings.FRAMEWORK_LOG_LEVEL,
 69        app_log_level=settings.LOG_LEVEL,
 70        app_log_format=settings.LOG_FORMAT,
 71        log_stream=settings.LOG_STREAM,
 72    )
 73
 74    packages_registry.populate(settings.INSTALLED_PACKAGES)
 75
 76
 77class SettingsReference(str):
 78    """
 79    String subclass which references a current settings value. It's treated as
 80    the value in memory but serializes to a settings.NAME attribute reference.
 81    """
 82
 83    def __new__(self, setting_name: str) -> Self:
 84        value = getattr(settings, setting_name)
 85        return str.__new__(self, value)
 86
 87    def __init__(self, setting_name: str):
 88        self.setting_name = setting_name
 89
 90
 91__all__ = [
 92    "APP_PATH",
 93    "AppPathNotFound",
 94    "PLAIN_TEMP_PATH",
 95    "Secret",
 96    "SetupError",
 97    "SettingsReference",
 98    "__version__",
 99    "settings",
100    "setup",
101]