Skip to content

hololinked.config.Configuration

Allows to auto apply common settings used throughout the package, instead of passing these settings as arguments.

Import global_config variable instead of instantiating this class. Please check global_config docstring for supported values or website documentation.

This implementation needs to be improved in general. Consider opening an issue if you have suggestions at GitHub.

Source code in repo/hololinked/hololinked/config.py
class Configuration:
    """
    Allows to auto apply common settings used throughout the package, instead of passing these settings as arguments.

    Import `global_config` variable instead of instantiating this class. Please check `global_config` docstring for supported values
    or [website documentation](https://docs.hololinked.dev/api-reference/global-config/).

    This implementation needs to be improved in general. Consider opening an issue
    if you have suggestions at [GitHub](https://github.com/hololinked-dev/hololinked/issues).
    """

    __slots__ = [
        "app_name",
        # folders
        "TEMP_DIR",
        # TCP sockets
        "TCP_SOCKET_SEARCH_START_PORT",
        "TCP_SOCKET_SEARCH_END_PORT",
        # HTTP server
        "ALLOW_CORS",
        # database
        "DB_CONFIG_FILE",
        # Eventloop
        "USE_UVLOOP",
        "TRACE_MALLOC",
        # schema validation
        "VALIDATE_SCHEMAS",
        # ZMQ
        "ZMQ_CONTEXT",
        # execution context defaults
        "DEFAULT_INVOKATION_TIMEOUT",
        "DEFAULT_EXECUTION_TIMEOUT",
        "DEFAULT_ONEWAY",
        "DEFAULT_FETCH_EXECUTION_LOGS",
        # make debugging easier
        "DEBUG",
        # logging
        "LOG_LEVEL",
        "USE_LOG_FILE",
        "LOG_FILENAME",
        "ROTATE_LOG_FILES",
        "LOGFILE_BACKUP_COUNT",
        # "USE_STRUCTLOG",
        "COLORED_LOGS",
        # serializers
        "ALLOW_PICKLE",
        "ALLOW_UNKNOWN_SERIALIZATION",
        # internal
        "_sockets_folder",
        "_secrets_folder",
        "_logs_folder",
        "_db_folder",
    ]

    def __init__(self, app_name: str | None = None):
        self.app_name = app_name
        self._sockets_folder = "sockets"
        self._secrets_folder = "secrets"
        self._logs_folder = "logs"
        self._db_folder = "db"
        self.load_variables()

    def load_variables(self):
        """Set default values. This method is called during `__init__`."""
        # note that all variables have not been implemented yet,
        # things just come and go as of now
        self.TEMP_DIR = os.path.join(os.path.expanduser("~"), ".hololinked")
        self.TCP_SOCKET_SEARCH_START_PORT = 60000
        self.TCP_SOCKET_SEARCH_END_PORT = 65535
        self.ALLOW_CORS = False
        self.DB_CONFIG_FILE = None
        self.USE_UVLOOP = False
        self.TRACE_MALLOC = False
        # self.VALIDATE_SCHEMA_ON_CLIENT = False
        self.VALIDATE_SCHEMAS = False
        # created on first use by zmq_context(), so that importing this package does not require
        # pyzmq, nor spin up an IO thread for callers who never touch ZMQ
        self.ZMQ_CONTEXT = None
        self.DEFAULT_INVOKATION_TIMEOUT = 5
        self.DEFAULT_EXECUTION_TIMEOUT = 5
        self.DEFAULT_ONEWAY = False
        self.DEFAULT_FETCH_EXECUTION_LOGS = False
        self.DEBUG = False
        self.LOG_LEVEL = logging.DEBUG if self.DEBUG else logging.INFO
        # self.USE_STRUCTLOG = True
        self.COLORED_LOGS = False
        self.USE_LOG_FILE = False
        # generate_main_script_log_filename() always falls back to a default name
        self.LOG_FILENAME = os.path.join(self.TEMP_DIR_LOGS, generate_main_script_log_filename(self.app_name))  # ty: ignore[no-matching-overload]
        self.ROTATE_LOG_FILES = True
        self.LOGFILE_BACKUP_COUNT = 14
        # Add the filename of the main script importing this module
        self.ALLOW_PICKLE = False
        self.ALLOW_UNKNOWN_SERIALIZATION = False

        self.setup()

    def setup(self):
        """
        Actions to be done to recreate global configuration state after changing config values.

        Called after `load_variables` and `set` methods.

        Please call this method after changing config values directly specific to logging or event loop policy
        """
        self.setup_temp_dirs()

        set_global_event_loop_policy(self.USE_UVLOOP)
        if self.TRACE_MALLOC:
            tracemalloc.start()

        from .logger import setup_logging

        self.LOG_LEVEL = logging.DEBUG if self.DEBUG else self.LOG_LEVEL

        # if self.USE_STRUCTLOG: # no other option for now
        setup_logging(
            log_level=self.LOG_LEVEL,
            colored_logs=self.COLORED_LOGS,
            log_file=self.LOG_FILENAME if self.USE_LOG_FILE else None,
            rotate_log_files=self.ROTATE_LOG_FILES,
            logfile_backup_count=self.LOGFILE_BACKUP_COUNT,
        )

    def copy(self):
        """
        Returns a copy of this config as another object.

        Returns
        -------
        Configuration
            a copy of this configuration
        """
        other = object.__new__(Configuration)
        for item in self.__slots__:
            setattr(other, item, getattr(self, item))
        return other

    def set(self, **kwargs):
        """
        Sets multiple config values at once, and recreates necessary global states.

        `load_variables` sets default values first, then overwrites with environment file values.
        This method only overwrites the specified values.
        """
        for item, value in kwargs.items():
            setattr(self, item, value)
        self.setup()

    def asdict(self):
        """
        Returns this config as a regular dictionary.

        Returns
        -------
        dict
            the configuration values keyed by their names
        """
        return {item: getattr(self, item) for item in self.__slots__}

    def zmq_context(self) -> "Any":
        """
        Returns the global ZMQ async context, creating it on first call.

        Returns
        -------
        zmq.asyncio.Context
            the process-wide ZMQ async context

        Raises
        ------
        ImportError
            if `pyzmq` is not installed - pip install pyzmq.
        """
        if self.ZMQ_CONTEXT is None:
            import zmq.asyncio

            self.ZMQ_CONTEXT = zmq.asyncio.Context()  # ty: ignore[invalid-argument-type]
        return self.ZMQ_CONTEXT

    def set_default_eventloop_settings(
        self,
        invokation_timeout: int | None = None,
        execution_timeout: int | None = None,
        oneway: bool = False,
    ) -> None:
        """
        Sets the default eventloop and scheduler settings.

        Parameters
        ----------
        invokation_timeout: int, optional
            seconds to wait for an operation to start, 5 when not given
        execution_timeout: int, optional
            seconds to wait for an operation to finish, 5 when not given
        oneway: bool
            whether callers should expect no reply by default
        """
        self.DEFAULT_INVOKATION_TIMEOUT = invokation_timeout or 5
        self.DEFAULT_EXECUTION_TIMEOUT = execution_timeout or 5
        self.DEFAULT_ONEWAY = oneway

        try:
            from .server.zmq.message import default_scheduler_execution_context
        except ImportError:
            return  # no pyzmq, so there are no ZMQ headers to keep in step
        default_scheduler_execution_context.invokation_timeout = self.DEFAULT_INVOKATION_TIMEOUT
        default_scheduler_execution_context.execution_timeout = self.DEFAULT_EXECUTION_TIMEOUT
        default_scheduler_execution_context.oneway = self.DEFAULT_ONEWAY

    # def set_default_thing_execution_context(
    #     self,
    #     fetch_execution_logs: bool = False,
    # ) -> None:
    #     """
    #     Sets the default thing execution context for the application.

    #     Parameters
    #     ----------
    #     fetch_execution_logs: bool
    #         whether to collect a `Thing`'s log records during execution by default
    #     """
    #     self.DEFAULT_FETCH_EXECUTION_LOGS = fetch_execution_logs

    @property
    def TEMP_DIR_SOCKETS(self) -> str:
        """Returns the temporary directory path for IPC sockets."""
        return os.path.join(self.TEMP_DIR, self._sockets_folder)

    @property
    def TEMP_DIR_LOGS(self) -> str:
        """Returns the temporary directory path for log files."""
        return os.path.join(self.TEMP_DIR, self._logs_folder)

    @property
    def TEMP_DIR_DB(self) -> str:
        """Returns the temporary directory path for database files."""
        return os.path.join(self.TEMP_DIR, self._db_folder)

    @property
    def TEMP_DIR_SECRETS(self) -> str:
        """Returns the temporary directory path for secret files."""
        return os.path.join(self.TEMP_DIR, self._secrets_folder)

    def setup_temp_dirs(self) -> None:
        """Create every temporary directory the package writes to, if missing."""
        for directory in [
            self.TEMP_DIR,
            self.TEMP_DIR_SOCKETS,
            self.TEMP_DIR_LOGS,
            self.TEMP_DIR_DB,
            self.TEMP_DIR_SECRETS,
        ]:
            try:
                os.mkdir(directory)
            except FileExistsError:
                pass
            except PermissionError:
                warnings.warn(f"permission denied to create directory {directory}", UserWarning)

    def set_temp_dir(self, path: str) -> None:
        """Sets the base directory path for temporary files and application data (sockets, logs, databases, secrets)."""
        self.TEMP_DIR = path
        self.setup_temp_dirs()

    def set_sockets_folders(self, path: str) -> None:
        """Sets the temporary directory path for IPC sockets."""
        self._sockets_folder = path
        self.setup_temp_dirs()

    def set_logs_folder(self, path: str) -> None:
        """Sets the temporary directory path for log files."""
        self._logs_folder = path
        self.setup_temp_dirs()

    def set_db_folder(self, path: str) -> None:
        """Sets the temporary directory path for database files."""
        self._db_folder = path
        self.setup_temp_dirs()

    def set_secrets_folder(self, path: str) -> None:
        """Sets the temporary directory path for secret files."""
        self._secrets_folder = path
        self.setup_temp_dirs()

    def cleanup_temp_dirs(self, cleanup_databases: bool = False) -> None:
        """
        Cleans up temporary directories used by hololinked, all log files and IPC sockets are removed.

        If `cleanup_databases` is `True`, database files are also removed.
        """
        directories = [self.TEMP_DIR_SOCKETS, self.TEMP_DIR_LOGS]
        if cleanup_databases:
            directories.append(self.TEMP_DIR_DB)
        for directory in directories:
            try:
                shutil.rmtree(directory)
            except FileNotFoundError:
                pass
            except PermissionError:
                warnings.warn(f"permission denied to cleanup directory {directory}", UserWarning)

    def __del__(self):
        if self.ZMQ_CONTEXT is not None:
            self.ZMQ_CONTEXT.term()

Attributes

TEMP_DIR_DB property

TEMP_DIR_DB: str

Returns the temporary directory path for database files.

TEMP_DIR_LOGS property

TEMP_DIR_LOGS: str

Returns the temporary directory path for log files.

TEMP_DIR_SECRETS property

TEMP_DIR_SECRETS: str

Returns the temporary directory path for secret files.

TEMP_DIR_SOCKETS property

TEMP_DIR_SOCKETS: str

Returns the temporary directory path for IPC sockets.

Functions

asdict

asdict()

Returns this config as a regular dictionary.

Returns:

Type Description
dict

the configuration values keyed by their names

Source code in repo/hololinked/hololinked/config.py
def asdict(self):
    """
    Returns this config as a regular dictionary.

    Returns
    -------
    dict
        the configuration values keyed by their names
    """
    return {item: getattr(self, item) for item in self.__slots__}

cleanup_temp_dirs

cleanup_temp_dirs(cleanup_databases: bool = False) -> None

Cleans up temporary directories used by hololinked, all log files and IPC sockets are removed.

If cleanup_databases is True, database files are also removed.

Source code in repo/hololinked/hololinked/config.py
def cleanup_temp_dirs(self, cleanup_databases: bool = False) -> None:
    """
    Cleans up temporary directories used by hololinked, all log files and IPC sockets are removed.

    If `cleanup_databases` is `True`, database files are also removed.
    """
    directories = [self.TEMP_DIR_SOCKETS, self.TEMP_DIR_LOGS]
    if cleanup_databases:
        directories.append(self.TEMP_DIR_DB)
    for directory in directories:
        try:
            shutil.rmtree(directory)
        except FileNotFoundError:
            pass
        except PermissionError:
            warnings.warn(f"permission denied to cleanup directory {directory}", UserWarning)

copy

copy()

Returns a copy of this config as another object.

Returns:

Type Description
Configuration

a copy of this configuration

Source code in repo/hololinked/hololinked/config.py
def copy(self):
    """
    Returns a copy of this config as another object.

    Returns
    -------
    Configuration
        a copy of this configuration
    """
    other = object.__new__(Configuration)
    for item in self.__slots__:
        setattr(other, item, getattr(self, item))
    return other

load_variables

load_variables()

Set default values. This method is called during __init__.

Source code in repo/hololinked/hololinked/config.py
def load_variables(self):
    """Set default values. This method is called during `__init__`."""
    # note that all variables have not been implemented yet,
    # things just come and go as of now
    self.TEMP_DIR = os.path.join(os.path.expanduser("~"), ".hololinked")
    self.TCP_SOCKET_SEARCH_START_PORT = 60000
    self.TCP_SOCKET_SEARCH_END_PORT = 65535
    self.ALLOW_CORS = False
    self.DB_CONFIG_FILE = None
    self.USE_UVLOOP = False
    self.TRACE_MALLOC = False
    # self.VALIDATE_SCHEMA_ON_CLIENT = False
    self.VALIDATE_SCHEMAS = False
    # created on first use by zmq_context(), so that importing this package does not require
    # pyzmq, nor spin up an IO thread for callers who never touch ZMQ
    self.ZMQ_CONTEXT = None
    self.DEFAULT_INVOKATION_TIMEOUT = 5
    self.DEFAULT_EXECUTION_TIMEOUT = 5
    self.DEFAULT_ONEWAY = False
    self.DEFAULT_FETCH_EXECUTION_LOGS = False
    self.DEBUG = False
    self.LOG_LEVEL = logging.DEBUG if self.DEBUG else logging.INFO
    # self.USE_STRUCTLOG = True
    self.COLORED_LOGS = False
    self.USE_LOG_FILE = False
    # generate_main_script_log_filename() always falls back to a default name
    self.LOG_FILENAME = os.path.join(self.TEMP_DIR_LOGS, generate_main_script_log_filename(self.app_name))  # ty: ignore[no-matching-overload]
    self.ROTATE_LOG_FILES = True
    self.LOGFILE_BACKUP_COUNT = 14
    # Add the filename of the main script importing this module
    self.ALLOW_PICKLE = False
    self.ALLOW_UNKNOWN_SERIALIZATION = False

    self.setup()

set

set(**kwargs)

Sets multiple config values at once, and recreates necessary global states.

load_variables sets default values first, then overwrites with environment file values. This method only overwrites the specified values.

Source code in repo/hololinked/hololinked/config.py
def set(self, **kwargs):
    """
    Sets multiple config values at once, and recreates necessary global states.

    `load_variables` sets default values first, then overwrites with environment file values.
    This method only overwrites the specified values.
    """
    for item, value in kwargs.items():
        setattr(self, item, value)
    self.setup()

set_db_folder

set_db_folder(path: str) -> None

Sets the temporary directory path for database files.

Source code in repo/hololinked/hololinked/config.py
def set_db_folder(self, path: str) -> None:
    """Sets the temporary directory path for database files."""
    self._db_folder = path
    self.setup_temp_dirs()

set_default_eventloop_settings

set_default_eventloop_settings(invokation_timeout: int | None = None, execution_timeout: int | None = None, oneway: bool = False) -> None

Sets the default eventloop and scheduler settings.

Parameters:

Name Type Description Default

invokation_timeout

int | None

seconds to wait for an operation to start, 5 when not given

None

execution_timeout

int | None

seconds to wait for an operation to finish, 5 when not given

None

oneway

bool

whether callers should expect no reply by default

False
Source code in repo/hololinked/hololinked/config.py
def set_default_eventloop_settings(
    self,
    invokation_timeout: int | None = None,
    execution_timeout: int | None = None,
    oneway: bool = False,
) -> None:
    """
    Sets the default eventloop and scheduler settings.

    Parameters
    ----------
    invokation_timeout: int, optional
        seconds to wait for an operation to start, 5 when not given
    execution_timeout: int, optional
        seconds to wait for an operation to finish, 5 when not given
    oneway: bool
        whether callers should expect no reply by default
    """
    self.DEFAULT_INVOKATION_TIMEOUT = invokation_timeout or 5
    self.DEFAULT_EXECUTION_TIMEOUT = execution_timeout or 5
    self.DEFAULT_ONEWAY = oneway

    try:
        from .server.zmq.message import default_scheduler_execution_context
    except ImportError:
        return  # no pyzmq, so there are no ZMQ headers to keep in step
    default_scheduler_execution_context.invokation_timeout = self.DEFAULT_INVOKATION_TIMEOUT
    default_scheduler_execution_context.execution_timeout = self.DEFAULT_EXECUTION_TIMEOUT
    default_scheduler_execution_context.oneway = self.DEFAULT_ONEWAY

set_logs_folder

set_logs_folder(path: str) -> None

Sets the temporary directory path for log files.

Source code in repo/hololinked/hololinked/config.py
def set_logs_folder(self, path: str) -> None:
    """Sets the temporary directory path for log files."""
    self._logs_folder = path
    self.setup_temp_dirs()

set_secrets_folder

set_secrets_folder(path: str) -> None

Sets the temporary directory path for secret files.

Source code in repo/hololinked/hololinked/config.py
def set_secrets_folder(self, path: str) -> None:
    """Sets the temporary directory path for secret files."""
    self._secrets_folder = path
    self.setup_temp_dirs()

set_sockets_folders

set_sockets_folders(path: str) -> None

Sets the temporary directory path for IPC sockets.

Source code in repo/hololinked/hololinked/config.py
def set_sockets_folders(self, path: str) -> None:
    """Sets the temporary directory path for IPC sockets."""
    self._sockets_folder = path
    self.setup_temp_dirs()

set_temp_dir

set_temp_dir(path: str) -> None

Sets the base directory path for temporary files and application data (sockets, logs, databases, secrets).

Source code in repo/hololinked/hololinked/config.py
def set_temp_dir(self, path: str) -> None:
    """Sets the base directory path for temporary files and application data (sockets, logs, databases, secrets)."""
    self.TEMP_DIR = path
    self.setup_temp_dirs()

setup

setup()

Actions to be done to recreate global configuration state after changing config values.

Called after load_variables and set methods.

Please call this method after changing config values directly specific to logging or event loop policy

Source code in repo/hololinked/hololinked/config.py
def setup(self):
    """
    Actions to be done to recreate global configuration state after changing config values.

    Called after `load_variables` and `set` methods.

    Please call this method after changing config values directly specific to logging or event loop policy
    """
    self.setup_temp_dirs()

    set_global_event_loop_policy(self.USE_UVLOOP)
    if self.TRACE_MALLOC:
        tracemalloc.start()

    from .logger import setup_logging

    self.LOG_LEVEL = logging.DEBUG if self.DEBUG else self.LOG_LEVEL

    # if self.USE_STRUCTLOG: # no other option for now
    setup_logging(
        log_level=self.LOG_LEVEL,
        colored_logs=self.COLORED_LOGS,
        log_file=self.LOG_FILENAME if self.USE_LOG_FILE else None,
        rotate_log_files=self.ROTATE_LOG_FILES,
        logfile_backup_count=self.LOGFILE_BACKUP_COUNT,
    )

setup_temp_dirs

setup_temp_dirs() -> None

Create every temporary directory the package writes to, if missing.

Source code in repo/hololinked/hololinked/config.py
def setup_temp_dirs(self) -> None:
    """Create every temporary directory the package writes to, if missing."""
    for directory in [
        self.TEMP_DIR,
        self.TEMP_DIR_SOCKETS,
        self.TEMP_DIR_LOGS,
        self.TEMP_DIR_DB,
        self.TEMP_DIR_SECRETS,
    ]:
        try:
            os.mkdir(directory)
        except FileExistsError:
            pass
        except PermissionError:
            warnings.warn(f"permission denied to create directory {directory}", UserWarning)

zmq_context

zmq_context() -> Any

Returns the global ZMQ async context, creating it on first call.

Returns:

Type Description
Context

the process-wide ZMQ async context

Raises:

Type Description
ImportError

if pyzmq is not installed - pip install pyzmq.

Source code in repo/hololinked/hololinked/config.py
def zmq_context(self) -> "Any":
    """
    Returns the global ZMQ async context, creating it on first call.

    Returns
    -------
    zmq.asyncio.Context
        the process-wide ZMQ async context

    Raises
    ------
    ImportError
        if `pyzmq` is not installed - pip install pyzmq.
    """
    if self.ZMQ_CONTEXT is None:
        import zmq.asyncio

        self.ZMQ_CONTEXT = zmq.asyncio.Context()  # ty: ignore[invalid-argument-type]
    return self.ZMQ_CONTEXT