config.py - Configuring Runestone BookServer

Many thing about Runestone BookServer are configurable. This is the place to change the configuration for most things. Private things should be configured in the environment so they are not accidentally committed to Github. Defaults provided here may be overridden by environment variables Per.

Imports

These are listed in the order prescribed by PEP 8.

Standard library

from enum import Enum
from functools import lru_cache
import os
from pathlib import Path
 

Third-party imports

import pkg_resources
from pydantic import BaseSettings
 

Local application imports

from .applogger import rslogger
 

Settings

Define the possible bookserver configurations. The values assigned must be strings, since Pydantic will match these with environment variables.

 
 
class BookServerConfig(Enum):
    development = "development"
    test = "test"
    production = "production"
 
 

A enum for the type of database in use.

class DatabaseType(Enum):
    SQLite = 0
    PostgreSQL = 1


class Settings(BaseSettings):

Pydantic provides a wonderful utility to handle settings. The beauty of it is that you can specify variables with or without default values, and Pydantic will check your environment variables in a case-insensitive way. So that if you have PROD_DBURL` set in the environment it will be set as the value for prod_dburl` in settings. This is a really nice way to keep from committing any data you want to keep private.

 
    google_ga: str = ""
 

Provide a path to the book server files. The leading underscore prevents environment variables from affecting this value. See the docs, which don’t say this explicitly, but testing confirms it.

    _book_server_path: str = str(
        Path(pkg_resources.resource_filename("bookserver", "")).absolute()
    )
 

The path to the Runestone application inside web2py.

    runestone_path: Path = Path(
        os.environ.get(
            "RUNESTONE_PATH",
            Path(_book_server_path).parents[1] / "web2py/applications/runestone",
        )
    ).resolve()
 

book_path: specify the directory to serve books from. For now, default to serving from the same place as the Runestone server, since the server uses some of these files.

    book_path: Path = runestone_path / "books"
 

The path to store error logs.

    error_path: Path = Path.home() / "Runestone/errors"
 

Define the mode of operation for the webserver, taken from BookServerConfig`. This looks a bit odd, since the string value will be parsed by Pydantic into a Config.

warning

When using an Enum for a configuration setting you cannot compare against a string. The value will actually be BookServerConfig.development` or whatever. Our style will be to compare against the Enum not the .name attribute.

    book_server_config: BookServerConfig = "development"  # type: ignore
 

Database setup: this must be an standard (synchronous) connection; for example:

  • sqlite:///./runestone.db

  • postgresql://postgres:bully@localhost/runestone

    dburl: str = f"sqlite:///{_book_server_path}/runestone.db"
    dev_dburl: str = f"sqlite:///{_book_server_path}/runestone_dev.db"
    test_dburl: str = f"sqlite:///{_book_server_path}/runestone_test.db"
 

Given a sync database URI, return its async equivalent.

    @staticmethod
    def _sync_to_async_uri(sync_uri: str) -> str:
        return sync_uri.replace("sqlite://", "sqlite+aiosqlite://", 1).replace(
            "postgresql://", "postgresql+asyncpg://", 1
        )
 

Determine the synchronous database URL based on the book_server_config and the dburls above.

    @property
    def _sync_database_url(self) -> str:
        return {
            "development": self.dev_dburl,
            "test": self.test_dburl,
            "production": self.dburl,
        }[self.book_server_config.value]
 

Return the async equivalent of the URI from sync_database_url.

    @property
    def database_url(self) -> str:
        return self._sync_to_async_uri(self._sync_database_url)
 

Determine the database type from the URL.

    @property
    def database_type(self) -> DatabaseType:
        dburl = self.database_url
        if dburl.startswith("sqlite"):
            return DatabaseType.SQLite
        elif dburl.startswith("postgresql"):
            return DatabaseType.PostgreSQL
        else:
            raise RuntimeError(f"Unknown database type; URL is {dburl}.")
 

Setting db_echo to True makes for a LOT of sqlalchemy output - it gives you the SQL for every query!

    db_echo = False
 

The docker-compose.yml file will set the REDIS_URI environment variable

    redis_uri = "redis://localhost:6379/0"
 

Select normal mode or a high-stakes assessment mode (for administering a examination). In this mode, answers to supported question types are not shown.

    is_exam: bool = False
 

This module provides “/auth/login” but it is super basic For now, logins are meant to be handled by the parallel runestone server. For production usage set the LOGIN_URL to /runestone/default/user

    login_url: str = "/auth/login"
 

Configure ads. TODO: Link to the place in the Runestone Components where this is used.

    adsenseid: str = ""
    num_banners: int = 0
    serve_ad: bool = False
    academy_mode: bool = True
    lti_only_mode: bool = False
 

This is the secret key used for generating the JWT token.

    jwt_secret: str = "supersecret"
 

This is the private key web2py uses for hashing passwords.

    @property
    def web2py_private_key(self) -> str:

Put the cache here; above the def, it produces TypeError: unhashable type: 'Settings'.

        @lru_cache
        def read_key():
            key_file = self.runestone_path / "private/auth.key"
            if key_file.exists():
                with open(key_file, encoding="utf-8") as f:
                    return f.read().strip()
            else:
                rslogger.error(
                    "No Key file is found will default to settings.jwt_secret"
                )
                return self.jwt_secret

        return read_key()
 

if you want to reinitialize your database set this to Yes. All data in the database will be lost! This will only work for development and test book_server_config settings.

    drop_tables: str = "No"


settings = Settings()