Source code for oarepo_config.generic_parameters
#!/usr/bin/env python3
#
# Copyright (c) 2026 CESNET z.s.p.o.
#
# This file is a part of oarepo-config (see https://github.com/oarepo/oarepo-config).
#
# oarepo-config is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.
#
"""Generic configuration parameters for oarepo-config."""
from __future__ import annotations
from pathlib import Path
from typing import Any
from urllib.parse import urlparse
from flask import current_app
from invenio_oauthclient.views.client import auto_redirect_login
from werkzeug.local import LocalProxy
from .base import (
load_configuration_variables,
merge_with_caller,
set_constants_in_caller,
)
[docs]
def configure_global_logging() -> None:
"""Configure global logging settings."""
import logging
import os
log_level = os.environ.get("PYTHON_DEFAULT_LOG_LEVEL", None)
if log_level is not None:
try:
normalized_log_level = log_level.upper().strip()
logging.basicConfig(level=normalized_log_level)
logging.getLogger().setLevel(normalized_log_level)
except ValueError:
logging.basicConfig(level=logging.INFO)
logging.getLogger().setLevel(logging.INFO)
logging.getLogger().exception("Invalid log level: %s, setting to INFO", log_level)
[docs]
def configure_generic_parameters( # noqa PLR0915
languages: tuple[tuple[str, str], ...] = (("en", "English"),),
use_path_pid_ids: bool = False,
) -> None:
"""Set up the core, infrastructure-level configuration of the repository.
This is the main "plumbing" function that should normally be called
first, before any of the other ``configure_*`` functions. It reads
most of its values from the deployment's environment configuration
(the ``variables``/``.env`` files or process environment variables,
all prefixed with ``INVENIO_``) and uses them to configure:
* the site's public URLs, and allowed security headers
* whether local login/registration/password-reset is available
* the database, search engine (OpenSearch), file storage (S3),
cache and message queue (Redis/RabbitMQ) connections
* translation/locale and file-upload defaults
* default identifier schemes recognised for names, funders and
affiliations (e.g. ORCID, ROR, VEDIDK, Scopus author ID)
Because most values come from the environment, this function will
fail to start the application if a required ``INVENIO_...`` variable
is missing from the deployment's configuration - see the project's
README for the full list of variables it expects.
Args:
languages: Extra UI languages to offer, as a tuple of
``(language_code, label)`` pairs, e.g.
``(("cs", _("Czech")), ("de", _("German")))``. English is
always available and does not need to be listed.
use_path_pid_ids: Set this to ``True`` only if your repository
uses record identifiers that themselves contain a slash
(``/``); it adjusts the URLs used for managing record access
so that such identifiers keep working.
Invenio configuration variables set, grouped by area:
Public URLs & security headers
* ``APP_ALLOWED_HOSTS``
* ``SITE_UI_URL``, ``SITE_API_URL``
* ``APP_DEFAULT_SECURE_HEADERS``
Local login & accounts
* ``ACCOUNTS_LOCAL_LOGIN_ENABLED``, ``SECURITY_REGISTERABLE``,
``SECURITY_RECOVERABLE``, ``SECURITY_CHANGEABLE``,
``SECURITY_CONFIRMABLE``,
``SECURITY_LOGIN_WITHOUT_CONFIRMATION``
* ``SESSION_COOKIE_SECURE``
* ``RATELIMIT_GUEST_USER``, ``RATELIMIT_AUTHENTICATED_USER``
* ``OAUTHCLIENT_REMOTE_APPS`` - starts out empty here; extended
by :func:`configure_einfra_oidc` if used.
* ``ACCOUNTS_LOGIN_VIEW_FUNCTION``,
``OAUTHCLIENT_AUTO_REDIRECT_TO_EXTERNAL_LOGIN``
Database
* ``SQLALCHEMY_DATABASE_URI``
Translation/locale
* ``BABEL_DEFAULT_LOCALE``, ``BABEL_DEFAULT_TIMEZONE``,
``I18N_LANGUAGES``
Files & storage
* ``SEND_FILE_MAX_AGE_DEFAULT``, ``FILES_REST_STORAGE_FACTORY``
* ``S3_ENDPOINT_URL``, ``S3_ACCESS_KEY_ID``,
``S3_SECRET_ACCESS_KEY``
* ``FILES_REST_STORAGE_CLASS_LIST``,
``FILES_REST_DEFAULT_STORAGE_CLASS``,
``FILES_REST_DEFAULT_QUOTA_SIZE``
* ``APP_RDM_DEPOSIT_FORM_QUOTA``
User profiles
* ``USERPROFILES_READ_ONLY``
OAI-PMH
* ``OAISERVER_ID_PREFIX``
Search (OpenSearch)
* ``SEARCH_INDEX_PREFIX``, ``SEARCH_HOSTS``,
``SEARCH_CLIENT_CONFIG``
* ``VOCABULARIES_SERVICE_CONFIG``,
``VOCABULARIES_RESOURCE_CONFIG`` - only if the optional
``oarepo_vocabularies`` package is installed.
Caches (Redis)
* ``INVENIO_CACHE_TYPE``, ``CACHE_REDIS_URL``,
``ACCOUNTS_SESSION_REDIS_URL``,
``COMMUNITIES_IDENTITIES_CACHE_REDIS_URL``
Background jobs (Celery/RabbitMQ)
* ``CELERY_BROKER_URL``, ``BROKER_URL``,
``CELERY_RESULT_BACKEND``
JSON schemas
* ``RECORDS_REFRESOLVER_CLS``, ``RECORDS_REFRESOLVER_STORE``,
``JSONSCHEMAS_HOST``
Secrets
* ``SECRET_KEY``
Records/REST compatibility
* ``DASHBOARD_RECORD_CREATE_URL``, ``RECORD_ROUTES``,
``RECORDS_REST_ENDPOINTS``
DataCite/DOIs
* ``DATACITE_TEST_MODE``
Mail
* ``MAIL_DEFAULT_SENDER``
* ``MAIL_SUPPRESS_SEND`` - only if ``INVENIO_MAIL_SUPPRESS_SEND``
is set in the environment.
Identifier/name/funder/affiliation vocabularies
* ``VOCABULARIES_NAMES_SCHEMES``, ``VOCABULARIES_FUNDER_SCHEMES``,
``VOCABULARIES_AFFILIATION_SCHEMES`` - merged with any
existing value.
* ``APP_RDM_IDENTIFIER_SCHEMES_UI``
* ``VOCABULARIES_DATASTREAM_READERS``,
``VOCABULARIES_DATASTREAM_WRITERS``,
``VOCABULARIES_DATASTREAM_TRANSFORMERS`` - seeded from
``invenio-app-rdm``'s defaults, merged with any existing
value; see :func:`configure_datastreams` to add your own.
Miscellaneous
* ``ROR_CLIENT_ID``, ``SESSION_COOKIE_DOMAIN``,
``COLLECT_STORAGE``
In addition, if ``use_path_pid_ids`` is ``True``, this patches the
``url_prefix`` of ``RDMParentRecordLinksResourceConfig``,
``RDMParentGrantsResourceConfig``, ``RDMGrantUserAccessResourceConfig``
and ``RDMGrantGroupAccessResourceConfig`` directly - this is a class
attribute change, not a new ``invenio.cfg`` variable.
Example:
.. code-block:: python
config.configure_generic_parameters(
languages=(
("cs", _("Czech")),
("de", _("German")),
),
)
"""
# see https://inveniordm.docs.cern.ch/install/configuration/ for the meaning
# of the variables here
env = load_configuration_variables()
configure_global_logging()
# generic
APP_ALLOWED_HOSTS = ["0.0.0.0", "localhost", "127.0.0.1"] # noqa S104
SITE_UI_URL = env.get("INVENIO_SITE_UI_URL", None) or f"https://{env.INVENIO_UI_HOST}:{env.INVENIO_UI_PORT}"
SITE_API_URL = env.get("INVENIO_SITE_API_URL", None) or f"https://{env.INVENIO_API_HOST}:{env.INVENIO_API_PORT}/api"
# security
APP_DEFAULT_SECURE_HEADERS: dict[str, Any] = {
"content_security_policy": {
"default-src": [
"'self'",
"fonts.googleapis.com", # for fonts
"*.gstatic.com", # for fonts
"data:", # for fonts
"'unsafe-inline'", # for inline scripts and styles
"blob:", # for pdf preview
# Add your own policies here (e.g. analytics)
],
"script-src": [
"'self'",
"blob:",
"'wasm-unsafe-eval'", # for WASM-based workers and file uploads
# Multipart file uploads use a Web Worker running `hash-wasm` to compute content checksums
# (e.g., MD5) of uploaded parts. This requires both 'blob:' and 'wasm-unsafe-eval'
# enabled in `script-src`.
],
},
"content_security_policy_report_only": False,
"content_security_policy_report_uri": None,
"force_file_save": False,
"force_https": True,
"force_https_permanent": False,
"frame_options": "sameorigin",
"frame_options_allow_from": None,
"session_cookie_http_only": True,
"session_cookie_secure": True,
"strict_transport_security": True,
"strict_transport_security_include_subdomains": True,
"strict_transport_security_max_age": 31556926, # One year in seconds
"strict_transport_security_preload": False,
}
# enable local login
ACCOUNTS_LOCAL_LOGIN_ENABLED = env.INVENIO_ACCOUNTS_LOCAL_LOGIN_ENABLED
# local login: allow users to register
SECURITY_REGISTERABLE = env.INVENIO_SECURITY_REGISTERABLE
# local login: allow users to reset the password
SECURITY_RECOVERABLE = env.INVENIO_SECURITY_RECOVERABLE
# local login: allow users to change psw
SECURITY_CHANGEABLE = env.INVENIO_SECURITY_CHANGEABLE
# local login: users can confirm e-mail address
SECURITY_CONFIRMABLE = env.INVENIO_SECURITY_CONFIRMABLE
# require users to confirm email before being able to login
SECURITY_LOGIN_WITHOUT_CONFIRMATION = env.INVENIO_SECURITY_LOGIN_WITHOUT_CONFIRMATION
SESSION_COOKIE_SECURE = True
# user security settings
RATELIMIT_GUEST_USER = "5000 per hour;500 per minute"
RATELIMIT_AUTHENTICATED_USER = "20000 per hour;2000 per minute"
OAUTHCLIENT_REMOTE_APPS: dict[str, Any] = {} # configure external login providers
ACCOUNTS_LOGIN_VIEW_FUNCTION = auto_redirect_login # autoredirect to external login if enabled
OAUTHCLIENT_AUTO_REDIRECT_TO_EXTERNAL_LOGIN = True # autoredirect to external login
# database
SQLALCHEMY_DATABASE_URI = env.get("INVENIO_SQLALCHEMY_DATABASE_URI", None) or (
"postgresql+psycopg2://"
f"{env.INVENIO_DATABASE_USER}:{env.INVENIO_DATABASE_PASSWORD}"
f"@{env.INVENIO_DATABASE_HOST}:{env.INVENIO_DATABASE_PORT}"
f"/{env.INVENIO_DATABASE_DBNAME}"
)
# i18n
BABEL_DEFAULT_LOCALE = "en"
BABEL_DEFAULT_TIMEZONE = "Europe/Prague"
I18N_LANGUAGES = list(languages)
# files
SEND_FILE_MAX_AGE_DEFAULT = 300
FILES_REST_STORAGE_FACTORY = "invenio_s3.s3fs_storage_factory"
S3_ENDPOINT_URL = env.get("INVENIO_S3_ENDPOINT_URL", None) or (
f"{env.INVENIO_S3_PROTOCOL}://{env.INVENIO_S3_HOST}:{env.INVENIO_S3_PORT}/"
)
S3_ACCESS_KEY_ID = env.INVENIO_S3_ACCESS_KEY
S3_SECRET_ACCESS_KEY = env.INVENIO_S3_SECRET_KEY
APP_DEFAULT_SECURE_HEADERS["content_security_policy"]["default-src"].append(S3_ENDPOINT_URL)
FILES_REST_STORAGE_CLASS_LIST = {
"L": "Local",
}
FILES_REST_DEFAULT_STORAGE_CLASS = "L"
# user profiles
USERPROFILES_READ_ONLY = False # allow users to change profile info (name, email, etc...)
OAISERVER_ID_PREFIX = LocalProxy(lambda: urlparse(current_app.config["SITE_UI_URL"]).hostname or "")
# search
SEARCH_INDEX_PREFIX = env.INVENIO_SEARCH_INDEX_PREFIX
SEARCH_HOSTS = [
{"host": env.INVENIO_OPENSEARCH_HOST, "port": env.INVENIO_OPENSEARCH_PORT},
]
SEARCH_CLIENT_CONFIG = {
"use_ssl": env.INVENIO_OPENSEARCH_USE_SSL,
"verify_certs": env.INVENIO_OPENSEARCH_VERIFY_CERTS,
"ssl_assert_hostname": env.INVENIO_OPENSEARCH_ASSERT_HOSTNAME,
"ssl_show_warn": env.INVENIO_OPENSEARCH_SHOW_WARN,
"ca_certs": env.get("INVENIO_OPENSEARCH_CA_CERTS_PATH", None),
}
# caches
INVENIO_CACHE_TYPE = "redis"
CACHE_REDIS_URL = env.get("INVENIO_CACHE_REDIS_URL", None) or (
f"redis://{env.INVENIO_REDIS_HOST}:{env.INVENIO_REDIS_PORT}/{env.INVENIO_REDIS_CACHE_DB}"
)
ACCOUNTS_SESSION_REDIS_URL = env.get("INVENIO_ACCOUNTS_SESSION_REDIS_URL", None) or (
f"redis://{env.INVENIO_REDIS_HOST}:{env.INVENIO_REDIS_PORT}/{env.INVENIO_REDIS_SESSION_DB}"
)
COMMUNITIES_IDENTITIES_CACHE_REDIS_URL = env.get("INVENIO_COMMUNITIES_IDENTITIES_CACHE_REDIS_URL", None) or (
f"redis://{env.INVENIO_REDIS_HOST}:{env.INVENIO_REDIS_PORT}/{env.INVENIO_REDIS_COMMUNITIES_CACHE_DB}"
)
# json schemas for validation
RECORDS_REFRESOLVER_CLS = "invenio_records.resolver.InvenioRefResolver"
RECORDS_REFRESOLVER_STORE = "invenio_jsonschemas.proxies.current_refresolver_store"
JSONSCHEMAS_HOST = SITE_UI_URL
# vocabularies
try:
from oarepo_vocabularies.resources.config import VocabulariesResourceConfig
from oarepo_vocabularies.services.config import VocabulariesConfig
VOCABULARIES_SERVICE_CONFIG = VocabulariesConfig
VOCABULARIES_RESOURCE_CONFIG = VocabulariesResourceConfig
except ImportError:
# keep the default Invenio vocabularies config
pass
# Redis port redirection
# ---------------------
CELERY_BROKER_URL = env.get("INVENIO_CELERY_BROKER_URL", None) or (
f"amqp://{env.INVENIO_RABBIT_USER}:{env.INVENIO_RABBIT_PASSWORD}"
f"@{env.INVENIO_RABBIT_HOST}:{env.INVENIO_RABBIT_PORT}/"
)
BROKER_URL = CELERY_BROKER_URL
CELERY_RESULT_BACKEND = env.get("INVENIO_CELERY_RESULT_BACKEND", None) or (
f"redis://{env.INVENIO_REDIS_HOST}:{env.INVENIO_REDIS_PORT}/{env.INVENIO_REDIS_CELERY_RESULT_DB}"
)
# Instance secret key, used to encrypt stuff (for example, access tokens) inside database
SECRET_KEY = env.INVENIO_SECRET_KEY
DASHBOARD_RECORD_CREATE_URL = None
# Do not add default records as we provide our own compatibility layer
RECORD_ROUTES: dict = {} # type: ignore[var-annotated]
RECORDS_REST_ENDPOINTS: dict = {} # type: ignore[var-annotated]
# datacite & dois default
DATACITE_TEST_MODE = True
MAIL_DEFAULT_SENDER = env.get(
"INVENIO_MAIL_DEFAULT_SENDER",
"please-set-invenio_mail_default_sender@test.com",
)
if env.get("INVENIO_MAIL_SUPPRESS_SEND", None) is not None:
MAIL_SUPPRESS_SEND = env.get("INVENIO_MAIL_SUPPRESS_SEND", True)
# default schemes for identifiers
from invenio_vocabularies import config as vocab_config
from oarepo.initial_rdm_config import is_researcher_id, is_scopus_id, is_vedidk
VOCABULARIES_NAMES_SCHEMES = merge_with_caller(
"VOCABULARIES_NAMES_SCHEMES",
{
**vocab_config.VOCABULARIES_NAMES_SCHEMES,
"vedidk": {"label": "VEDIDK", "validator": is_vedidk},
"scopusId": {"label": "Scopus ID", "validator": is_scopus_id},
"researcherId": {"label": "Researcher ID", "validator": is_researcher_id},
},
)
# adding crossrefFunderId to funder schemes
VOCABULARIES_FUNDER_SCHEMES = merge_with_caller(
"VOCABULARIES_FUNDER_SCHEMES",
{
**vocab_config.VOCABULARIES_FUNDER_SCHEMES,
"crossrefFunderId": {
"label": "CrossrefFunderID",
"validator": lambda _: True,
},
},
)
# List of affiliations is curated, validators are not needed.
VOCABULARIES_AFFILIATION_SCHEMES = merge_with_caller(
"VOCABULARIES_AFFILIATION_SCHEMES",
{
**vocab_config.VOCABULARIES_AFFILIATION_SCHEMES,
"ico": {"label": "ICO", "validator": lambda _: True},
"url": {"label": "URL", "validator": lambda _: True},
},
)
from invenio_app_rdm import config as app_rdm_config
FILES_REST_DEFAULT_QUOTA_SIZE = 10**10
APP_RDM_DEPOSIT_FORM_QUOTA = {
"maxFiles": 10,
"maxStorage": FILES_REST_DEFAULT_QUOTA_SIZE,
}
APP_RDM_IDENTIFIER_SCHEMES_UI = {
"orcid": {
"url_prefix": "http://orcid.org/",
"icon": "images/orcid.svg",
"label": "ORCID",
},
"ror": {
"url_prefix": "https://ror.org/",
"icon": "images/ror-icon.svg",
"label": "ROR",
},
"gnd": {
"url_prefix": "http://d-nb.info/gnd/",
"icon": "images/gnd-icon.svg",
"label": "GND",
},
}
VOCABULARIES_DATASTREAM_READERS = merge_with_caller(
"VOCABULARIES_DATASTREAM_READERS",
app_rdm_config.VOCABULARIES_DATASTREAM_READERS,
)
VOCABULARIES_DATASTREAM_WRITERS = merge_with_caller(
"VOCABULARIES_DATASTREAM_WRITERS",
app_rdm_config.VOCABULARIES_DATASTREAM_WRITERS,
)
VOCABULARIES_DATASTREAM_TRANSFORMERS = merge_with_caller(
"VOCABULARIES_DATASTREAM_TRANSFORMERS",
app_rdm_config.VOCABULARIES_DATASTREAM_TRANSFORMERS,
)
if use_path_pid_ids:
from invenio_rdm_records.resources.config import (
RDMGrantGroupAccessResourceConfig,
RDMGrantUserAccessResourceConfig,
RDMParentGrantsResourceConfig,
RDMParentRecordLinksResourceConfig,
)
RDMParentRecordLinksResourceConfig.url_prefix = "/records/<path:pid_value>/access"
RDMParentGrantsResourceConfig.url_prefix = "/records/<path:pid_value>/access"
RDMGrantUserAccessResourceConfig.url_prefix = "/records/<path:pid_value>/access"
RDMGrantGroupAccessResourceConfig.url_prefix = "/records/<path:pid_value>/access"
OAUTHCLIENT_AUTO_REDIRECT_TO_EXTERNAL_LOGIN = False
# ROR client id - not used for authentication, just to increase rate limits
ROR_CLIENT_ID = "3764CLXJ4R9GT9L496QSKU2G3ZWVF71A"
SESSION_COOKIE_DOMAIN = None
# Flask-Collect configured to not use symlinks, as they can break the build due to duplicate static assets.
COLLECT_STORAGE = "flask_collect.storage.file"
import os
import platform
if Path("/opt/homebrew/lib").exists() and platform.system() == "Darwin":
os.environ["DYLD_FALLBACK_LIBRARY_PATH"] = "/opt/homebrew/lib"
set_constants_in_caller(locals())