Skip to content

feat: add django-aqueduct typed settings for edx-platform K8s deployments#39

Open
blarghmatey wants to merge 4 commits into
mainfrom
feat/django-aqueduct-settings
Open

feat: add django-aqueduct typed settings for edx-platform K8s deployments#39
blarghmatey wants to merge 4 commits into
mainfrom
feat/django-aqueduct-settings

Conversation

@blarghmatey
Copy link
Copy Markdown
Member

What are the relevant tickets?

N/A

Description (What does it do?)

Introduces a django-aqueduct based configuration layer for edx-platform that replaces the legacy LMS_CFG/STUDIO_CFG YAML file loading with a structured, typed, K8s-native approach.

Files injected into the edx-platform image by the Dagger build:

Injected path Purpose
lms/envs/models/aqueduct.py Generated AqueductSettings pydantic model — typed snapshot of all edx-platform settings
lms/envs/models/base.py BaseProductionSettings — shared pydantic-settings source configuration and derived-setting validators
lms/envs/aqueduct.py Settings module entry point (DJANGO_SETTINGS_MODULE=lms.envs.aqueduct)
cms/envs/models/aqueduct.py CMS equivalent of the above
cms/envs/models/base.py Shared base (same source, injected to both service envs)
cms/envs/aqueduct.py CMS settings module entry point (DJANGO_SETTINGS_MODULE=cms.envs.aqueduct)

Settings loading strategy (highest → lowest priority):

  1. Environment variables — flat scalars injected via K8s envFrom ConfigMaps and Secrets
  2. YAML files under OL_SETTINGS_DIR (deep-merged, sorted 00→82) — complex types: DATABASES, CACHES, JWT_AUTH, FEATURES, MODULESTORE, etc.
  3. AqueductSettings field defaults — typed snapshot of common.py defaults

Companion PR: mitodl/ol-infrastructure — updates the Pulumi edxapp K8s resources to use the new settings module and produce flat env ConfigMaps for scalar settings.

How can this be tested?

  • Review the injected file structure in settings/
  • Build a lehrer image with dagger call build-platform using DJANGO_SETTINGS_MODULE=lms.envs.aqueduct and verify Django starts correctly
  • Regenerate the model files inside the container: DJANGO_SETTINGS_MODULE=lms.envs.aqueduct python manage.py generate_aqueduct_settings --output lms/envs/models/aqueduct.py

Additional Context

The settings/models/ files are excluded from mypy and intentionally carry dev-path defaults (e.g. /home/tmacey/...) — these are generated artifacts that should be regenerated from inside the Docker image to pick up production paths (/openedx/edx-platform/...). The defaults are always overridden at runtime by env vars and the YAML config files.

…ents

Introduces a pydantic-settings based configuration layer for edx-platform
that replaces the legacy LMS_CFG/STUDIO_CFG YAML file loading with a
structured, typed approach.

Layout injected into the edx-platform image by the Dagger build:
  lms/envs/models/base.py      - BaseProductionSettings (shared validators)
  lms/envs/models/aqueduct.py  - generated AqueductSettings pydantic model
  lms/envs/aqueduct.py         - settings module (DJANGO_SETTINGS_MODULE target)
  cms/envs/models/base.py      - BaseProductionSettings (shared)
  cms/envs/models/aqueduct.py  - generated AqueductSettings pydantic model
  cms/envs/aqueduct.py         - settings module

Settings are loaded in priority order:
  1. Env vars (flat scalars from K8s envFrom ConfigMaps/Secrets)
  2. YAML files under OL_SETTINGS_DIR (complex types: DATABASES, CACHES, etc.)
  3. AqueductSettings field defaults (typed snapshot of common.py)

Adds django-aqueduct==0.3.0 and upgrades pydantic-settings to the [yaml]
extra across all deployment/release pip package lists.

Excludes settings/ and src/lehrer/ from mypy: settings/ files import
pydantic, django_aqueduct, and openedx.* which only resolve inside the
edx-platform container at runtime; src/lehrer/ uses dagger which is not
in lehrer's own dev venv.
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces django-aqueduct settings integration for both LMS and CMS, adding shared base production settings (BaseProductionSettings) and service-specific entry points using pydantic-settings to load configuration from environment variables and YAML files. It also configures pre-commit hooks, updates pip package lists with django-aqueduct and pydantic-settings[yaml], and updates the Lehrer build process to inject these settings. Feedback on the changes suggests conditionally loading YamlConfigSettingsSource only when YAML files exist to prevent runtime errors, using getattr and setattr to safely access dynamically resolved settings fields across the settings models, and updating outdated paths in the docstring of settings/models/base.py.

Comment thread settings/models/base.py Outdated
Comment on lines +131 to +158
@classmethod
def settings_customise_sources(
cls,
settings_cls,
init_settings,
env_settings,
dotenv_settings,
file_secret_settings,
):
"""Two-tier loading: env vars for flat scalars, YAML for complex types.

Priority (first wins):

1. Environment variables — K8s injects flat scalars from ConfigMap and
Secret ``envFrom`` refs directly into the process environment.
2. YAML files (sorted) — complex settings (lists, nested dicts) that
cannot be represented cleanly as individual env vars. Files are
deep-merged so ``FEATURES`` from a service-specific ConfigMap is
merged into, not replaced by, the base ``FEATURES`` dict.
"""
return (
env_settings,
YamlConfigSettingsSource(
settings_cls,
yaml_file=_sorted_yaml_files(_SETTINGS_DIR),
deep_merge=True,
),
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If no YAML files are found in _SETTINGS_DIR, _sorted_yaml_files returns an empty list. Passing an empty list to YamlConfigSettingsSource can cause runtime validation errors or unexpected behavior in pydantic-settings. It is safer to conditionally include YamlConfigSettingsSource only when YAML files are actually present.

    @classmethod
    def settings_customise_sources(
        cls,
        settings_cls,
        init_settings,
        env_settings,
        dotenv_settings,
        file_secret_settings,
    ):
        """Two-tier loading: env vars for flat scalars, YAML for complex types.

        Priority (first wins):

        1. Environment variables — K8s injects flat scalars from ConfigMap and
           Secret ``envFrom`` refs directly into the process environment.
        2. YAML files (sorted) — complex settings (lists, nested dicts) that
           cannot be represented cleanly as individual env vars.  Files are
           deep-merged so ``FEATURES`` from a service-specific ConfigMap is
           merged into, not replaced by, the base ``FEATURES`` dict.
        """
        sources = [env_settings]
        yaml_files = _sorted_yaml_files(_SETTINGS_DIR)
        if yaml_files:
            sources.append(
                YamlConfigSettingsSource(
                    settings_cls,
                    yaml_file=yaml_files,
                    deep_merge=True,
                )
            )
        return tuple(sources)

Comment thread settings/models/base.py
Comment on lines +164 to +260
@model_validator(mode="after")
def _derive_celery_queue_names(self) -> "BaseProductionSettings":
"""Build CELERY_DEFAULT_* queue names from SERVICE_VARIANT."""
if self.CELERY_DEFAULT_QUEUE is None:
queue = f"edx.{self.SERVICE_VARIANT}.core.default"
self.CELERY_DEFAULT_QUEUE = queue
self.CELERY_DEFAULT_ROUTING_KEY = queue # type: ignore[attr-defined]
self.CELERY_DEFAULT_EXCHANGE = queue # type: ignore[attr-defined]
return self

@model_validator(mode="after")
def _derive_broker_url(self) -> "BaseProductionSettings":
"""Build BROKER_URL from CELERY_BROKER_* components.

CELERY_BROKER_HOSTNAME and CELERY_BROKER_PASSWORD arrive as flat env
vars (hostname from a ConfigMap, password from a Secret).
"""
if self.CELERY_BROKER_TRANSPORT and not getattr(self, "BROKER_URL", None):
self.BROKER_URL = ( # type: ignore[attr-defined]
f"{self.CELERY_BROKER_TRANSPORT}://"
f"{self.CELERY_BROKER_USER}:{self.CELERY_BROKER_PASSWORD}"
f"@{self.CELERY_BROKER_HOSTNAME}/{self.CELERY_BROKER_VHOST}"
)
if isinstance(self.CELERY_BROKER_USE_SSL, dict):
self.BROKER_USE_SSL = self.CELERY_BROKER_USE_SSL
return self

@model_validator(mode="after")
def _derive_static_paths(self) -> "BaseProductionSettings":
"""Override STATIC_ROOT / STATIC_URL from *_BASE env-var variants."""
if self.STATIC_ROOT_BASE:
self.STATIC_ROOT = self.STATIC_ROOT_BASE # type: ignore[assignment]
if self.STATIC_URL_BASE:
url = self.STATIC_URL_BASE
if not url.endswith("/"):
url += "/"
self.STATIC_URL = url
return self

@model_validator(mode="after")
def _derive_mako_module_dir(self) -> "BaseProductionSettings":
"""MAKO_MODULE_DIR lives in the system temp dir, keyed by service variant."""
if self.MAKO_MODULE_DIR is None:
self.MAKO_MODULE_DIR = os.path.join( # type: ignore[assignment]
tempfile.gettempdir(), f"edx_mako_{self.SERVICE_VARIANT}"
)
return self

@model_validator(mode="after")
def _derive_statici18n_root(self) -> "BaseProductionSettings":
"""STATICI18N_ROOT mirrors STATIC_ROOT."""
if self.STATICI18N_ROOT is None and self.STATIC_ROOT:
self.STATICI18N_ROOT = self.STATIC_ROOT
return self

@model_validator(mode="after")
def _derive_language_settings(self) -> "BaseProductionSettings":
"""Populate LANGUAGE_COOKIE_NAME (from YAML alias) and LANGUAGE_DICT."""
if self.LANGUAGE_COOKIE:
self.LANGUAGE_COOKIE_NAME = self.LANGUAGE_COOKIE
if self.LANGUAGES:
self.LANGUAGE_DICT = dict(self.LANGUAGES) # type: ignore[arg-type, attr-defined]
return self

@model_validator(mode="after")
def _derive_elastic_search_config(self) -> "BaseProductionSettings":
"""Use ELASTIC_SEARCH_CONFIG_ES7 as the authoritative search config."""
if self.ELASTIC_SEARCH_CONFIG_ES7:
object.__setattr__(
self, "ELASTIC_SEARCH_CONFIG", self.ELASTIC_SEARCH_CONFIG_ES7
)
return self

@model_validator(mode="after")
def _derive_timezone(self) -> "BaseProductionSettings":
"""Align Django TIME_ZONE with Celery's timezone."""
if self.CELERY_TIMEZONE:
self.TIME_ZONE = self.CELERY_TIMEZONE
return self

@model_validator(mode="after")
def _derive_logging(self) -> "BaseProductionSettings":
"""Build LOGGING from log-dir / environment / loglevel.

All three inputs are flat scalars arriving as env vars; they are
declared as explicit fields so pydantic populates them before this
validator runs.
"""
from openedx.core.lib.logsettings import get_logger_config # noqa: PLC0415

self.LOGGING = get_logger_config( # type: ignore[attr-defined]
self.LOG_DIR,
logging_env=self.LOGGING_ENV,
local_loglevel=self.LOCAL_LOGLEVEL,
service_variant=self.SERVICE_VARIANT,
)
return self
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Since BaseProductionSettings is a shared base class and does not define the dynamically resolved settings fields (which are generated in AqueductSettings), accessing them directly (e.g., self.SERVICE_VARIANT) can raise AttributeError at runtime if they are missing or if the model generation is incomplete. Using getattr and setattr provides a robust fallback mechanism, prevents runtime crashes, and eliminates the need for type: ignore comments.

    @model_validator(mode="after")
    def _derive_celery_queue_names(self) -> "BaseProductionSettings":
        """Build CELERY_DEFAULT_* queue names from SERVICE_VARIANT."""
        celery_default_queue = getattr(self, "CELERY_DEFAULT_QUEUE", None)
        service_variant = getattr(self, "SERVICE_VARIANT", None)
        if celery_default_queue is None and service_variant:
            queue = f"edx.{service_variant}.core.default"
            setattr(self, "CELERY_DEFAULT_QUEUE", queue)
            setattr(self, "CELERY_DEFAULT_ROUTING_KEY", queue)
            setattr(self, "CELERY_DEFAULT_EXCHANGE", queue)
        return self

    @model_validator(mode="after")
    def _derive_broker_url(self) -> "BaseProductionSettings":
        """Build BROKER_URL from CELERY_BROKER_* components.

        CELERY_BROKER_HOSTNAME and CELERY_BROKER_PASSWORD arrive as flat env
        vars (hostname from a ConfigMap, password from a Secret).
        """
        celery_broker_transport = getattr(self, "CELERY_BROKER_TRANSPORT", None)
        broker_url = getattr(self, "BROKER_URL", None)
        if celery_broker_transport and not broker_url:
            user = getattr(self, "CELERY_BROKER_USER", "")
            password = getattr(self, "CELERY_BROKER_PASSWORD", "")
            hostname = getattr(self, "CELERY_BROKER_HOSTNAME", "")
            vhost = getattr(self, "CELERY_BROKER_VHOST", "")
            setattr(
                self,
                "BROKER_URL",
                f"{celery_broker_transport}://{user}:{password}@{hostname}/{vhost}",
            )
        if isinstance(self.CELERY_BROKER_USE_SSL, dict):
            self.BROKER_USE_SSL = self.CELERY_BROKER_USE_SSL
        return self

    @model_validator(mode="after")
    def _derive_static_paths(self) -> "BaseProductionSettings":
        """Override STATIC_ROOT / STATIC_URL from *_BASE env-var variants."""
        if self.STATIC_ROOT_BASE:
            setattr(self, "STATIC_ROOT", self.STATIC_ROOT_BASE)
        if self.STATIC_URL_BASE:
            url = self.STATIC_URL_BASE
            if not url.endswith("/"):
                url += "/"
            setattr(self, "STATIC_URL", url)
        return self

    @model_validator(mode="after")
    def _derive_mako_module_dir(self) -> "BaseProductionSettings":
        """MAKO_MODULE_DIR lives in the system temp dir, keyed by service variant."""
        mako_module_dir = getattr(self, "MAKO_MODULE_DIR", None)
        service_variant = getattr(self, "SERVICE_VARIANT", None)
        if mako_module_dir is None and service_variant:
            setattr(
                self,
                "MAKO_MODULE_DIR",
                os.path.join(tempfile.gettempdir(), f"edx_mako_{service_variant}"),
            )
        return self

    @model_validator(mode="after")
    def _derive_statici18n_root(self) -> "BaseProductionSettings":
        """STATICI18N_ROOT mirrors STATIC_ROOT."""
        statici18n_root = getattr(self, "STATICI18N_ROOT", None)
        static_root = getattr(self, "STATIC_ROOT", None)
        if statici18n_root is None and static_root:
            setattr(self, "STATICI18N_ROOT", static_root)
        return self

    @model_validator(mode="after")
    def _derive_language_settings(self) -> "BaseProductionSettings":
        """Populate LANGUAGE_COOKIE_NAME (from YAML alias) and LANGUAGE_DICT."""
        if self.LANGUAGE_COOKIE:
            setattr(self, "LANGUAGE_COOKIE_NAME", self.LANGUAGE_COOKIE)
        languages = getattr(self, "LANGUAGES", None)
        if languages:
            setattr(self, "LANGUAGE_DICT", dict(languages))
        return self

    @model_validator(mode="after")
    def _derive_elastic_search_config(self) -> "BaseProductionSettings":
        """Use ELASTIC_SEARCH_CONFIG_ES7 as the authoritative search config."""
        if self.ELASTIC_SEARCH_CONFIG_ES7:
            setattr(self, "ELASTIC_SEARCH_CONFIG", self.ELASTIC_SEARCH_CONFIG_ES7)
        return self

    @model_validator(mode="after")
    def _derive_timezone(self) -> "BaseProductionSettings":
        """Align Django TIME_ZONE with Celery's timezone."""
        celery_timezone = getattr(self, "CELERY_TIMEZONE", None)
        if celery_timezone:
            setattr(self, "TIME_ZONE", celery_timezone)
        return self

    @model_validator(mode="after")
    def _derive_logging(self) -> "BaseProductionSettings":
        """Build LOGGING from log-dir / environment / loglevel.

        All three inputs are flat scalars arriving as env vars; they are
        declared as explicit fields so pydantic populates them before this
        validator runs.
        """
        from openedx.core.lib.logsettings import get_logger_config  # noqa: PLC0415

        service_variant = getattr(self, "SERVICE_VARIANT", None)
        logging_config = get_logger_config(
            self.LOG_DIR,
            logging_env=self.LOGGING_ENV,
            local_loglevel=self.LOCAL_LOGLEVEL,
            service_variant=service_variant,
        )
        setattr(self, "LOGGING", logging_config)
        return self

Comment thread settings/lms/aqueduct.py
Comment on lines +47 to +94
@model_validator(mode="after")
def _derive_urlconf(self) -> LMSProductionSettings:
if self.ROOT_URLCONF is None:
self.ROOT_URLCONF = "lms.urls"
return self

@model_validator(mode="after")
def _derive_segment_key(self) -> LMSProductionSettings:
"""Map env-var SEGMENT_KEY → Django LMS_SEGMENT_KEY."""
if self.SEGMENT_KEY is not None:
self.LMS_SEGMENT_KEY = self.SEGMENT_KEY
return self

@model_validator(mode="after")
def _derive_cors_credentials(self) -> LMSProductionSettings:
"""Enable CORS credentials when the relevant FEATURES flags are on."""
features = self.FEATURES or {}
if isinstance(features, dict) and (
features.get("ENABLE_CORS_HEADERS")
or features.get("ENABLE_CROSS_DOMAIN_CSRF_COOKIE")
):
object.__setattr__(self, "CORS_ALLOW_CREDENTIALS", True)
return self

@model_validator(mode="after")
def _derive_authentication_backends(self) -> LMSProductionSettings:
"""Prepend THIRD_PARTY_AUTH_BACKENDS before AUTHENTICATION_BACKENDS.

Arrives via YAML (complex list type). Deduplication preserves order.
"""
if not self.THIRD_PARTY_AUTH_BACKENDS:
return self
features = self.FEATURES or {}
if isinstance(features, dict) and not features.get(
"ENABLE_THIRD_PARTY_AUTH", True
):
return self
existing = list(self.AUTHENTICATION_BACKENDS or [])
self.AUTHENTICATION_BACKENDS = self.THIRD_PARTY_AUTH_BACKENDS + [
b for b in existing if b not in self.THIRD_PARTY_AUTH_BACKENDS
]
return self

@model_validator(mode="after")
def _derive_social_auth_clean_usernames(self) -> LMSProductionSettings:
if self.SOCIAL_AUTH_CLEAN_USERNAMES is None:
self.SOCIAL_AUTH_CLEAN_USERNAMES = False # type: ignore[assignment]
return self
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Use getattr and setattr for robust settings derivation in LMSProductionSettings. This prevents potential AttributeError at runtime and eliminates the need for type: ignore comments.

    @model_validator(mode="after")
    def _derive_urlconf(self) -> LMSProductionSettings:
        if getattr(self, "ROOT_URLCONF", None) is None:
            setattr(self, "ROOT_URLCONF", "lms.urls")
        return self

    @model_validator(mode="after")
    def _derive_segment_key(self) -> LMSProductionSettings:
        """Map env-var SEGMENT_KEY → Django LMS_SEGMENT_KEY."""
        if self.SEGMENT_KEY is not None:
            setattr(self, "LMS_SEGMENT_KEY", self.SEGMENT_KEY)
        return self

    @model_validator(mode="after")
    def _derive_cors_credentials(self) -> LMSProductionSettings:
        """Enable CORS credentials when the relevant FEATURES flags are on."""
        features = getattr(self, "FEATURES", None) or {}
        if isinstance(features, dict) and (
            features.get("ENABLE_CORS_HEADERS")
            or features.get("ENABLE_CROSS_DOMAIN_CSRF_COOKIE")
        ):
            setattr(self, "CORS_ALLOW_CREDENTIALS", True)
        return self

    @model_validator(mode="after")
    def _derive_authentication_backends(self) -> LMSProductionSettings:
        """Prepend THIRD_PARTY_AUTH_BACKENDS before AUTHENTICATION_BACKENDS.

        Arrives via YAML (complex list type).  Deduplication preserves order.
        """
        if not self.THIRD_PARTY_AUTH_BACKENDS:
            return self
        features = getattr(self, "FEATURES", None) or {}
        if isinstance(features, dict) and not features.get(
            "ENABLE_THIRD_PARTY_AUTH", True
        ):
            return self
        existing = list(getattr(self, "AUTHENTICATION_BACKENDS", None) or [])
        new_backends = self.THIRD_PARTY_AUTH_BACKENDS + [
            b for b in existing if b not in self.THIRD_PARTY_AUTH_BACKENDS
        ]
        setattr(self, "AUTHENTICATION_BACKENDS", new_backends)
        return self

    @model_validator(mode="after")
    def _derive_social_auth_clean_usernames(self) -> LMSProductionSettings:
        if getattr(self, "SOCIAL_AUTH_CLEAN_USERNAMES", None) is None:
            setattr(self, "SOCIAL_AUTH_CLEAN_USERNAMES", False)
        return self

Comment thread settings/models/base.py
Comment on lines +3 to +10
Injected by the lehrer Dagger build into both:
/openedx/edx-platform/lms/envs/mitol/ol_production_base.py
/openedx/edx-platform/cms/envs/mitol/ol_production_base.py

Each service's ``ol_production.py`` imports from this module using a relative
import (``from .ol_production_base import OLBaseProductionSettings``), so the
single source file works correctly in both namespaces without cross-service
imports.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The docstring references outdated paths (mitol/ol_production_base.py) and class names (OLBaseProductionSettings). Update it to reflect the new structure (models/base.py and BaseProductionSettings).

Suggested change
Injected by the lehrer Dagger build into both:
/openedx/edx-platform/lms/envs/mitol/ol_production_base.py
/openedx/edx-platform/cms/envs/mitol/ol_production_base.py
Each service's ``ol_production.py`` imports from this module using a relative
import (``from .ol_production_base import OLBaseProductionSettings``), so the
single source file works correctly in both namespaces without cross-service
imports.
Injected by the lehrer Dagger build into both:
/openedx/edx-platform/lms/envs/models/base.py
/openedx/edx-platform/cms/envs/models/base.py
Each service's ``aqueduct.py`` imports from this module using a relative
import (``from .models.base import BaseProductionSettings``), so the
single source file works correctly in both namespaces without cross-service
imports.

Comment thread settings/cms/aqueduct.py
Comment on lines +41 to +45
@model_validator(mode="after")
def _derive_urlconf(self) -> CMSProductionSettings:
if self.ROOT_URLCONF is None:
self.ROOT_URLCONF = "cms.urls"
return self
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Use getattr and setattr for consistency and safety when setting ROOT_URLCONF in CMSProductionSettings.

Suggested change
@model_validator(mode="after")
def _derive_urlconf(self) -> CMSProductionSettings:
if self.ROOT_URLCONF is None:
self.ROOT_URLCONF = "cms.urls"
return self
@model_validator(mode="after")
def _derive_urlconf(self) -> CMSProductionSettings:
if getattr(self, "ROOT_URLCONF", None) is None:
setattr(self, "ROOT_URLCONF", "cms.urls")
return self

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces a django-aqueduct based, typed settings layer for the edx-platform image built by lehrer. Six new files are injected into the container so that production LMS/CMS can be driven via DJANGO_SETTINGS_MODULE=lms.envs.aqueduct / cms.envs.aqueduct, with env vars (flat scalars) layered over deep-merged YAML (complex types) over generated pydantic defaults. Required runtime packages (django-aqueduct==0.3.0 and the [yaml] extra of pydantic-settings) are added to all changed deployment/release pip lists, and a dev dependency group plus a mypy exclusion for settings/ are added to pyproject.toml.

Changes:

  • Add settings/models/base.py (shared BaseProductionSettings) and per-service settings/{lms,cms}/aqueduct.py entry points plus generated settings/{lms,cms}/models/aqueduct.py pydantic models.
  • Extend collected in src/lehrer/main.py to create lms/envs/models and cms/envs/models dirs and copy the aqueduct base/models/entry-point files into both service envs.
  • Bump pydantic-settings to the [yaml] extra and add django-aqueduct==0.3.0 across master/teak/ulmo/verawood pip package lists; exclude settings/ from mypy in pyproject.toml.

Reviewed changes

Copilot reviewed 18 out of 20 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/lehrer/main.py Creates models/ dirs and copies new base/aqueduct/model files into both lms/envs and cms/envs.
settings/models/base.py Shared BaseProductionSettings with two-tier env-var-then-YAML source order and derived-setting validators (celery, broker, static, logging, etc.).
settings/lms/aqueduct.py LMS settings module entry point and LMS-specific derived fields/validators.
settings/cms/aqueduct.py CMS settings module entry point with cms.urls default.
settings/cms/models/aqueduct.py Generated typed snapshot of CMS settings defaults (excluded from mypy).
pyproject.toml Adds dev dependency group and [tool.mypy] exclude = ["settings/"].
.pre-commit-config.yaml Adds the full pre-commit suite, with settings/, src/lehrer/, and dockerfiles/ excluded from mypy.
pip_package_lists/{master,teak,ulmo,verawood}/{mitx,mitx-staging,mitxonline,xpro}.txt Switches pydantic-settings to the [yaml] extra and adds django-aqueduct==0.3.0.

Comment thread settings/models/base.py Outdated
Comment on lines +1 to +10
"""Shared OL Kubernetes production settings base for both LMS and CMS.

Injected by the lehrer Dagger build into both:
/openedx/edx-platform/lms/envs/mitol/ol_production_base.py
/openedx/edx-platform/cms/envs/mitol/ol_production_base.py

Each service's ``ol_production.py`` imports from this module using a relative
import (``from .ol_production_base import OLBaseProductionSettings``), so the
single source file works correctly in both namespaces without cross-service
imports.
Comment thread settings/cms/models/aqueduct.py Outdated
Comment on lines +893 to +895
JWT_AUTH: dict[str, Any] = Field(
default=None
) # OPAQUE: original dict value is not serialisable
- Guard YamlConfigSettingsSource: only include it when YAML files are
  actually present, preventing validation errors when OL_SETTINGS_DIR is
  absent or empty (e.g. local development without a K8s config dir)
- Replace direct attribute access on fields not declared in
  BaseProductionSettings with getattr(..., None) for safe reads; keep
  direct assignment self.FOO = value for writes (avoids ruff B010 while
  still being safe with pydantic's extra='allow' model)
- Fix stale docstring in models/base.py: update injection paths from
  lms/envs/mitol/ol_production_base.py to lms/envs/models/base.py,
  class name from OLBaseProductionSettings to BaseProductionSettings,
  and import example from .ol_production_base to .models.base
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants