Skip to content

opensampl.config.server

Pydantic BaseSettings Object used to access and set the openSAMPL-server configuration options.

This module provides the main configuration class for openSAMPL-server, handling environment variables, configuration validation, and settings management.

ServerConfig

Bases: BaseConfig

Configuration specific to server-side CLI operations.

Source code in opensampl/config/server.py
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
class ServerConfig(BaseConfig):
    """Configuration specific to server-side CLI operations."""

    model_config = SettingsConfigDict(env_file=".env", extra="ignore", env_prefix="OPENSAMPL_SERVER__")

    COMPOSE_FILE: str = Field(default="", description="Fully resolved path to the Docker Compose file.")

    DOCKER_ENV_FILE: str = Field(default="", description="Fully resolved path to the Docker .env file.")

    docker_env_values: dict[str, Any] = Field(default_factory=dict, init=False)

    @property
    def _ignore_in_set(self) -> list[str]:
        """The fields to ignore when setting the configuration."""
        ignored = super()._ignore_in_set.copy()
        ignored.extend(["docker_env_values"])

        # Don't save compose file or docker env file if using defaults
        if get_resolved_resource_path(opensampl.server, "docker-compose.yaml") == self.COMPOSE_FILE:
            ignored.append("COMPOSE_FILE")
        if get_resolved_resource_path(opensampl.server, "default.env") == self.DOCKER_ENV_FILE:
            ignored.append("DOCKER_ENV_FILE")

        return ignored

    @model_validator(mode="after")
    def get_docker_values(self) -> "ServerConfig":
        """Get the values that the docker containers will use on startup"""
        self.docker_env_values = dotenv_values(self.DOCKER_ENV_FILE)
        return self

    @field_validator("COMPOSE_FILE", mode="before")
    @classmethod
    def resolve_compose_file(cls, v: Any) -> str:
        """Resolve the provided compose file for docker to use, or default to the docker-compose.yaml provided"""
        if v == "":
            return get_resolved_resource_path(opensampl.server, "docker-compose.yaml")
        return str(Path(v).expanduser().resolve())

    @field_validator("DOCKER_ENV_FILE", mode="before")
    @classmethod
    def resolve_docker_env_file(cls, v: Any) -> str:
        """Resolve the provided env file for docker containers to use, or default to the default.env provided"""
        if v == "":
            return get_resolved_resource_path(opensampl.server, "default.env")
        return str(Path(v).expanduser().resolve())

    @staticmethod
    def get_compose_command() -> str:
        """Detect the available docker-compose command."""
        if check_command(["docker-compose", "--version"]):
            return "docker-compose"
        if check_command(["docker", "compose", "--version"]):
            return "docker compose"
        raise ImportError("Neither 'docker compose' nor 'docker-compose' is installed. Please install Docker Compose.")

    def build_docker_compose_base(self):
        """Build the docker compose command, including env file and compose file"""
        compose_command = self.get_compose_command()
        command = shlex.split(compose_command)
        command.extend(["--env-file", self.DOCKER_ENV_FILE, "-f", self.COMPOSE_FILE])
        return command

    def set_by_name(self, name: str, value: Any):
        """
        Set setting's value in the env file for current instance.

        Uses env_prefix for ServerConfig-specific fields, base name for inherited fields.
        """
        setting = self.get_by_name(name)
        if setting is None:
            raise ValueError(f"Setting {name} not found")

        # Check if this field is defined in ServerConfig vs inherited from BaseConfig
        server_fields = set(ServerConfig.model_fields.keys()) - set(BaseConfig.model_fields.keys())
        env_key = f"{self.model_config.get('env_prefix', '')}{name}" if name in server_fields else name

        if not self.env_file.is_file():
            logger.info("Env file does not exist. Creating one to save setting.")
            self.env_file.touch()

        set_key(self.env_file, env_key, str(value))

    def get_db_url(self):
        """Return the database URL for the Timescale db that will be created with the docker-compose environment."""
        user = self.docker_env_values.get("POSTGRES_USER")
        password = self.docker_env_values.get("POSTGRES_PASSWORD")
        db = self.docker_env_values.get("POSTGRES_DB")
        if all(x is not None for x in [user, password, db]):
            return f"postgresql://{user}:{password}@localhost:5415/{db}"
        raise ValueError("Database environment variables POSTGRES_USER, POSTGRES_PASSWORD, or POSTGRES_DB are not set.")

build_docker_compose_base()

Build the docker compose command, including env file and compose file

Source code in opensampl/config/server.py
87
88
89
90
91
92
def build_docker_compose_base(self):
    """Build the docker compose command, including env file and compose file"""
    compose_command = self.get_compose_command()
    command = shlex.split(compose_command)
    command.extend(["--env-file", self.DOCKER_ENV_FILE, "-f", self.COMPOSE_FILE])
    return command

get_compose_command() staticmethod

Detect the available docker-compose command.

Source code in opensampl/config/server.py
78
79
80
81
82
83
84
85
@staticmethod
def get_compose_command() -> str:
    """Detect the available docker-compose command."""
    if check_command(["docker-compose", "--version"]):
        return "docker-compose"
    if check_command(["docker", "compose", "--version"]):
        return "docker compose"
    raise ImportError("Neither 'docker compose' nor 'docker-compose' is installed. Please install Docker Compose.")

get_db_url()

Return the database URL for the Timescale db that will be created with the docker-compose environment.

Source code in opensampl/config/server.py
114
115
116
117
118
119
120
121
def get_db_url(self):
    """Return the database URL for the Timescale db that will be created with the docker-compose environment."""
    user = self.docker_env_values.get("POSTGRES_USER")
    password = self.docker_env_values.get("POSTGRES_PASSWORD")
    db = self.docker_env_values.get("POSTGRES_DB")
    if all(x is not None for x in [user, password, db]):
        return f"postgresql://{user}:{password}@localhost:5415/{db}"
    raise ValueError("Database environment variables POSTGRES_USER, POSTGRES_PASSWORD, or POSTGRES_DB are not set.")

get_docker_values()

Get the values that the docker containers will use on startup

Source code in opensampl/config/server.py
56
57
58
59
60
@model_validator(mode="after")
def get_docker_values(self) -> "ServerConfig":
    """Get the values that the docker containers will use on startup"""
    self.docker_env_values = dotenv_values(self.DOCKER_ENV_FILE)
    return self

resolve_compose_file(v) classmethod

Resolve the provided compose file for docker to use, or default to the docker-compose.yaml provided

Source code in opensampl/config/server.py
62
63
64
65
66
67
68
@field_validator("COMPOSE_FILE", mode="before")
@classmethod
def resolve_compose_file(cls, v: Any) -> str:
    """Resolve the provided compose file for docker to use, or default to the docker-compose.yaml provided"""
    if v == "":
        return get_resolved_resource_path(opensampl.server, "docker-compose.yaml")
    return str(Path(v).expanduser().resolve())

resolve_docker_env_file(v) classmethod

Resolve the provided env file for docker containers to use, or default to the default.env provided

Source code in opensampl/config/server.py
70
71
72
73
74
75
76
@field_validator("DOCKER_ENV_FILE", mode="before")
@classmethod
def resolve_docker_env_file(cls, v: Any) -> str:
    """Resolve the provided env file for docker containers to use, or default to the default.env provided"""
    if v == "":
        return get_resolved_resource_path(opensampl.server, "default.env")
    return str(Path(v).expanduser().resolve())

set_by_name(name, value)

Set setting's value in the env file for current instance.

Uses env_prefix for ServerConfig-specific fields, base name for inherited fields.

Source code in opensampl/config/server.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
def set_by_name(self, name: str, value: Any):
    """
    Set setting's value in the env file for current instance.

    Uses env_prefix for ServerConfig-specific fields, base name for inherited fields.
    """
    setting = self.get_by_name(name)
    if setting is None:
        raise ValueError(f"Setting {name} not found")

    # Check if this field is defined in ServerConfig vs inherited from BaseConfig
    server_fields = set(ServerConfig.model_fields.keys()) - set(BaseConfig.model_fields.keys())
    env_key = f"{self.model_config.get('env_prefix', '')}{name}" if name in server_fields else name

    if not self.env_file.is_file():
        logger.info("Env file does not exist. Creating one to save setting.")
        self.env_file.touch()

    set_key(self.env_file, env_key, str(value))

get_resolved_resource_path(pkg, relative_path)

Retrieve the resolved path to a resource in a package.

Source code in opensampl/config/server.py
24
25
26
27
28
def get_resolved_resource_path(pkg: Union[str, ModuleType], relative_path: str) -> str:
    """Retrieve the resolved path to a resource in a package."""
    resource = files(pkg).joinpath(relative_path)
    with as_file(resource) as real_path:
        return str(real_path.resolve())