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
 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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
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.")

    OVERRIDE_FILE: str | None = Field(default=None, description="Override for the 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("OVERRIDE_FILE", mode="before")
    @classmethod
    def resolve_override_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 str(Path(v).expanduser().resolve())
        return v

    @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])
        if self.OVERRIDE_FILE:
            command.extend(["-f", self.OVERRIDE_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
101
102
103
104
105
106
107
108
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])
    if self.OVERRIDE_FILE:
        command.extend(["-f", self.OVERRIDE_FILE])
    return command

get_compose_command() staticmethod

Detect the available docker-compose command.

Source code in opensampl/config/server.py
92
93
94
95
96
97
98
99
@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
130
131
132
133
134
135
136
137
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
62
63
64
65
66
@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
68
69
70
71
72
73
74
@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
84
85
86
87
88
89
90
@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())

resolve_override_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
76
77
78
79
80
81
82
@field_validator("OVERRIDE_FILE", mode="before")
@classmethod
def resolve_override_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 str(Path(v).expanduser().resolve())
    return v

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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
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
28
29
30
31
32
def get_resolved_resource_path(pkg: 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())