diff --git a/.circleci/config.yml b/.circleci/config.yml
index c5d62d7fa5e..0ce781b9945 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -81,7 +81,7 @@ jobs:
- restore_cache:
keys:
- pre-commit-cache-{{ checksum "pre-commit-cache-key.txt" }}
- - run: pip install --user 'tox<5'
+ - run: pip install --user tox
- run: tox -e pre-commit
- run: tox -e migrations
- node/install:
diff --git a/readthedocs/config/config.py b/readthedocs/config/config.py
index 2452621e647..53690534716 100644
--- a/readthedocs/config/config.py
+++ b/readthedocs/config/config.py
@@ -16,6 +16,7 @@
from .find import find_one
from .models import (
BuildJobs,
+ BuildJobsBuildTypes,
BuildTool,
BuildWithOs,
Conda,
@@ -318,11 +319,9 @@ def validate_build_config_with_os(self):
# ones, we could validate the value of each of them is a list of
# commands. However, I don't think we should validate the "command"
# looks like a real command.
+ valid_jobs = list(BuildJobs.model_fields.keys())
for job in jobs.keys():
- validate_choice(
- job,
- BuildJobs.__slots__,
- )
+ validate_choice(job, valid_jobs)
commands = []
with self.catch_validation_error("build.commands"):
@@ -346,6 +345,14 @@ def validate_build_config_with_os(self):
)
build["jobs"] = {}
+
+ with self.catch_validation_error("build.jobs.build"):
+ build["jobs"]["build"] = self.validate_build_jobs_build(jobs)
+ # Remove the build.jobs.build key from the build.jobs dict,
+ # since it's the only key that should be a dictionary,
+ # it was already validated above.
+ jobs.pop("build", None)
+
for job, job_commands in jobs.items():
with self.catch_validation_error(f"build.jobs.{job}"):
build["jobs"][job] = [
@@ -370,6 +377,29 @@ def validate_build_config_with_os(self):
build["apt_packages"] = self.validate_apt_packages()
return build
+ def validate_build_jobs_build(self, build_jobs):
+ result = {}
+ build_jobs_build = build_jobs.get("build", {})
+ validate_dict(build_jobs_build)
+
+ allowed_build_types = list(BuildJobsBuildTypes.model_fields.keys())
+ for build_type, build_commands in build_jobs_build.items():
+ validate_choice(build_type, allowed_build_types)
+ if build_type != "html" and build_type not in self.formats:
+ raise ConfigError(
+ message_id=ConfigError.BUILD_JOBS_BUILD_TYPE_MISSING_IN_FORMATS,
+ format_values={
+ "build_type": build_type,
+ },
+ )
+ with self.catch_validation_error(f"build.jobs.build.{build_type}"):
+ result[build_type] = [
+ validate_string(build_command)
+ for build_command in validate_list(build_commands)
+ ]
+
+ return result
+
def validate_apt_packages(self):
apt_packages = []
with self.catch_validation_error("build.apt_packages"):
diff --git a/readthedocs/config/exceptions.py b/readthedocs/config/exceptions.py
index 637ffd71cf7..d0fcb30b5f1 100644
--- a/readthedocs/config/exceptions.py
+++ b/readthedocs/config/exceptions.py
@@ -13,6 +13,9 @@ class ConfigError(BuildUserError):
INVALID_VERSION = "config:base:invalid-version"
NOT_BUILD_TOOLS_OR_COMMANDS = "config:build:missing-build-tools-commands"
BUILD_JOBS_AND_COMMANDS = "config:build:jobs-and-commands"
+ BUILD_JOBS_BUILD_TYPE_MISSING_IN_FORMATS = (
+ "config:build:jobs:build:missing-in-formats"
+ )
APT_INVALID_PACKAGE_NAME_PREFIX = "config:apt:invalid-package-name-prefix"
APT_INVALID_PACKAGE_NAME = "config:apt:invalid-package-name"
USE_PIP_FOR_EXTRA_REQUIREMENTS = "config:python:pip-required"
diff --git a/readthedocs/config/models.py b/readthedocs/config/models.py
index 16b0ad58750..c96e2462586 100644
--- a/readthedocs/config/models.py
+++ b/readthedocs/config/models.py
@@ -1,4 +1,5 @@
"""Models for the response of the configuration object."""
+from pydantic import BaseModel
from readthedocs.config.utils import to_dict
@@ -37,33 +38,41 @@ class BuildTool(Base):
__slots__ = ("version", "full_version")
-class BuildJobs(Base):
+class BuildJobsBuildTypes(BaseModel):
+
+ """Object used for `build.jobs.build` key."""
+
+ html: list[str] | None = None
+ pdf: list[str] | None = None
+ epub: list[str] | None = None
+ htmlzip: list[str] | None = None
+
+ def as_dict(self):
+ # Just to keep compatibility with the old implementation.
+ return self.model_dump()
+
+
+class BuildJobs(BaseModel):
"""Object used for `build.jobs` key."""
- __slots__ = (
- "pre_checkout",
- "post_checkout",
- "pre_system_dependencies",
- "post_system_dependencies",
- "pre_create_environment",
- "post_create_environment",
- "pre_install",
- "post_install",
- "pre_build",
- "post_build",
- )
+ pre_checkout: list[str] = []
+ post_checkout: list[str] = []
+ pre_system_dependencies: list[str] = []
+ post_system_dependencies: list[str] = []
+ pre_create_environment: list[str] = []
+ create_environment: list[str] | None = None
+ post_create_environment: list[str] = []
+ pre_install: list[str] = []
+ install: list[str] | None = None
+ post_install: list[str] = []
+ pre_build: list[str] = []
+ build: BuildJobsBuildTypes = BuildJobsBuildTypes()
+ post_build: list[str] = []
- def __init__(self, **kwargs):
- """
- Create an empty list as a default for all possible builds.jobs configs.
-
- This is necessary because it makes the code cleaner when we add items to these lists,
- without having to check for a dict to be created first.
- """
- for step in self.__slots__:
- kwargs.setdefault(step, [])
- super().__init__(**kwargs)
+ def as_dict(self):
+ # Just to keep compatibility with the old implementation.
+ return self.model_dump()
class Python(Base):
diff --git a/readthedocs/config/notifications.py b/readthedocs/config/notifications.py
index ccccebd5c33..d3334390d00 100644
--- a/readthedocs/config/notifications.py
+++ b/readthedocs/config/notifications.py
@@ -124,6 +124,18 @@
),
type=ERROR,
),
+ Message(
+ id=ConfigError.BUILD_JOBS_BUILD_TYPE_MISSING_IN_FORMATS,
+ header=_("Invalid configuration option"),
+ body=_(
+ textwrap.dedent(
+ """
+ The {{ build_type }}
build type was defined in build.jobs.build
, but it wasn't included in formats
.
+ """
+ ).strip(),
+ ),
+ type=ERROR,
+ ),
Message(
id=ConfigError.APT_INVALID_PACKAGE_NAME_PREFIX,
header=_("Invalid APT package name"),
diff --git a/readthedocs/config/tests/test_config.py b/readthedocs/config/tests/test_config.py
index c72e2c256b6..e61d76a5c7a 100644
--- a/readthedocs/config/tests/test_config.py
+++ b/readthedocs/config/tests/test_config.py
@@ -14,6 +14,7 @@
from readthedocs.config.exceptions import ConfigError, ConfigValidationError
from readthedocs.config.models import (
BuildJobs,
+ BuildJobsBuildTypes,
BuildWithOs,
PythonInstall,
PythonInstallRequirements,
@@ -627,17 +628,120 @@ def test_jobs_build_config(self):
assert build.build.jobs.pre_create_environment == [
"echo pre_create_environment"
]
+ assert build.build.jobs.create_environment is None
assert build.build.jobs.post_create_environment == [
"echo post_create_environment"
]
assert build.build.jobs.pre_install == ["echo pre_install", "echo `date`"]
+ assert build.build.jobs.install is None
assert build.build.jobs.post_install == ["echo post_install"]
assert build.build.jobs.pre_build == [
"echo pre_build",
'sed -i -e "s|{VERSION}|${READTHEDOCS_VERSION_NAME}|g"',
]
+ assert build.build.jobs.build == BuildJobsBuildTypes()
assert build.build.jobs.post_build == ["echo post_build"]
+ def test_build_jobs_partial_override(self):
+ build = get_build_config(
+ {
+ "formats": ["pdf", "htmlzip", "epub"],
+ "build": {
+ "os": "ubuntu-20.04",
+ "tools": {"python": "3"},
+ "jobs": {
+ "create_environment": ["echo make_environment"],
+ "install": ["echo install"],
+ "build": {
+ "html": ["echo build html"],
+ "pdf": ["echo build pdf"],
+ "epub": ["echo build epub"],
+ "htmlzip": ["echo build htmlzip"],
+ },
+ },
+ },
+ },
+ )
+ build.validate()
+ assert isinstance(build.build, BuildWithOs)
+ assert isinstance(build.build.jobs, BuildJobs)
+ assert build.build.jobs.create_environment == ["echo make_environment"]
+ assert build.build.jobs.install == ["echo install"]
+ assert build.build.jobs.build.html == ["echo build html"]
+ assert build.build.jobs.build.pdf == ["echo build pdf"]
+ assert build.build.jobs.build.epub == ["echo build epub"]
+ assert build.build.jobs.build.htmlzip == ["echo build htmlzip"]
+
+ def test_build_jobs_build_should_match_formats(self):
+ build = get_build_config(
+ {
+ "formats": ["pdf"],
+ "build": {
+ "os": "ubuntu-24.04",
+ "tools": {"python": "3"},
+ "jobs": {
+ "build": {
+ "epub": ["echo build epub"],
+ },
+ },
+ },
+ },
+ )
+ with raises(ConfigError) as excinfo:
+ build.validate()
+ assert (
+ excinfo.value.message_id
+ == ConfigError.BUILD_JOBS_BUILD_TYPE_MISSING_IN_FORMATS
+ )
+
+ def test_build_jobs_build_defaults(self):
+ build = get_build_config(
+ {
+ "build": {
+ "os": "ubuntu-24.04",
+ "tools": {"python": "3"},
+ "jobs": {
+ "build": {
+ "html": ["echo build html"],
+ },
+ },
+ },
+ },
+ )
+ build.validate()
+ assert build.build.jobs.build.html == ["echo build html"]
+ assert build.build.jobs.build.pdf is None
+ assert build.build.jobs.build.htmlzip is None
+ assert build.build.jobs.build.epub is None
+
+ def test_build_jobs_partial_override_empty_commands(self):
+ build = get_build_config(
+ {
+ "formats": ["pdf"],
+ "build": {
+ "os": "ubuntu-24.04",
+ "tools": {"python": "3"},
+ "jobs": {
+ "create_environment": [],
+ "install": [],
+ "build": {
+ "html": [],
+ "pdf": [],
+ },
+ },
+ },
+ },
+ )
+ build.validate()
+ assert isinstance(build.build, BuildWithOs)
+ assert isinstance(build.build.jobs, BuildJobs)
+ assert build.build.jobs.create_environment == []
+ assert build.build.jobs.install == []
+ assert build.build.jobs.build.html == []
+ assert build.build.jobs.build.pdf == []
+ assert build.build.jobs.build.epub == None
+ assert build.build.jobs.build.htmlzip == None
+
@pytest.mark.parametrize(
"value",
[
@@ -1757,10 +1861,18 @@ def test_as_dict_new_build_config(self, tmpdir):
"pre_system_dependencies": [],
"post_system_dependencies": [],
"pre_create_environment": [],
+ "create_environment": None,
"post_create_environment": [],
"pre_install": [],
+ "install": None,
"post_install": [],
"pre_build": [],
+ "build": {
+ "html": None,
+ "pdf": None,
+ "epub": None,
+ "htmlzip": None,
+ },
"post_build": [],
},
"apt_packages": [],
diff --git a/readthedocs/core/utils/objects.py b/readthedocs/core/utils/objects.py
new file mode 100644
index 00000000000..a1b26da700e
--- /dev/null
+++ b/readthedocs/core/utils/objects.py
@@ -0,0 +1,22 @@
+# Sentinel value to check if a default value was provided,
+# so we can differentiate when None is provided as a default value
+# and when it was not provided at all.
+_DEFAULT = object()
+
+
+def get_dotted_attribute(obj, attribute, default=_DEFAULT):
+ """
+ Allow to get nested attributes from an object using a dot notation.
+
+ This behaves similar to getattr, but allows to get nested attributes.
+ Similar, if a default value is provided, it will be returned if the
+ attribute is not found, otherwise it will raise an AttributeError.
+ """
+ for attr in attribute.split("."):
+ if hasattr(obj, attr):
+ obj = getattr(obj, attr)
+ elif default is not _DEFAULT:
+ return default
+ else:
+ raise AttributeError(f"Object {obj} has no attribute {attr}")
+ return obj
diff --git a/readthedocs/doc_builder/director.py b/readthedocs/doc_builder/director.py
index 8f49c4074cf..02a9b806757 100644
--- a/readthedocs/doc_builder/director.py
+++ b/readthedocs/doc_builder/director.py
@@ -18,6 +18,7 @@
from readthedocs.config.config import CONFIG_FILENAME_REGEX
from readthedocs.config.find import find_one
from readthedocs.core.utils.filesystem import safe_open
+from readthedocs.core.utils.objects import get_dotted_attribute
from readthedocs.doc_builder.config import load_yaml_config
from readthedocs.doc_builder.exceptions import BuildUserError
from readthedocs.doc_builder.loader import get_builder_class
@@ -184,7 +185,6 @@ def build(self):
3. build PDF
4. build ePub
"""
-
self.run_build_job("pre_build")
# Build all formats
@@ -306,21 +306,35 @@ def system_dependencies(self):
# Language environment
def create_environment(self):
+ if self.data.config.build.jobs.create_environment is not None:
+ self.run_build_job("create_environment")
+ return
self.language_environment.setup_base()
# Install
def install(self):
+ if self.data.config.build.jobs.install is not None:
+ self.run_build_job("install")
+ return
+
self.language_environment.install_core_requirements()
self.language_environment.install_requirements()
# Build
def build_html(self):
+ if self.data.config.build.jobs.build.html is not None:
+ self.run_build_job("build.html")
+ return
return self.build_docs_class(self.data.config.doctype)
def build_pdf(self):
if "pdf" not in self.data.config.formats or self.data.version.type == EXTERNAL:
return False
+ if self.data.config.build.jobs.build.pdf is not None:
+ self.run_build_job("build.pdf")
+ return
+
# Mkdocs has no pdf generation currently.
if self.is_type_sphinx():
return self.build_docs_class("sphinx_pdf")
@@ -334,6 +348,10 @@ def build_htmlzip(self):
):
return False
+ if self.data.config.build.jobs.build.htmlzip is not None:
+ self.run_build_job("build.htmlzip")
+ return
+
# We don't generate a zip for mkdocs currently.
if self.is_type_sphinx():
return self.build_docs_class("sphinx_singlehtmllocalmedia")
@@ -343,6 +361,10 @@ def build_epub(self):
if "epub" not in self.data.config.formats or self.data.version.type == EXTERNAL:
return False
+ if self.data.config.build.jobs.build.epub is not None:
+ self.run_build_job("build.epub")
+ return
+
# Mkdocs has no epub generation currently.
if self.is_type_sphinx():
return self.build_docs_class("sphinx_epub")
@@ -372,14 +394,17 @@ def run_build_job(self, job):
- python path/to/myscript.py
pre_build:
- sed -i **/*.rst -e "s|{version}|v3.5.1|g"
+ build:
+ html:
+ - make html
+ pdf:
+ - make pdf
In this case, `self.data.config.build.jobs.pre_build` will contains
`sed` command.
"""
- if (
- getattr(self.data.config.build, "jobs", None) is None
- or getattr(self.data.config.build.jobs, job, None) is None
- ):
+ commands = get_dotted_attribute(self.data.config, f"build.jobs.{job}", None)
+ if not commands:
return
cwd = self.data.project.checkout_path(self.data.version.slug)
@@ -387,7 +412,6 @@ def run_build_job(self, job):
if job not in ("pre_checkout", "post_checkout"):
environment = self.build_environment
- commands = getattr(self.data.config.build.jobs, job, [])
for command in commands:
environment.run(command, escape_command=False, cwd=cwd)
diff --git a/readthedocs/projects/tests/test_build_tasks.py b/readthedocs/projects/tests/test_build_tasks.py
index e1a44976ac7..5fa64b6673b 100644
--- a/readthedocs/projects/tests/test_build_tasks.py
+++ b/readthedocs/projects/tests/test_build_tasks.py
@@ -556,16 +556,24 @@ def test_successful_build(
"os": "ubuntu-22.04",
"commands": [],
"jobs": {
- "post_build": [],
+ "pre_checkout": [],
"post_checkout": [],
- "post_create_environment": [],
- "post_install": [],
+ "pre_system_dependencies": [],
"post_system_dependencies": [],
- "pre_build": [],
- "pre_checkout": [],
"pre_create_environment": [],
+ "create_environment": None,
+ "post_create_environment": [],
"pre_install": [],
- "pre_system_dependencies": [],
+ "install": None,
+ "post_install": [],
+ "pre_build": [],
+ "build": {
+ "html": None,
+ "pdf": None,
+ "epub": None,
+ "htmlzip": None,
+ },
+ "post_build": [],
},
"tools": {
"python": {
@@ -1203,6 +1211,130 @@ def test_build_jobs(self, load_yaml_config):
any_order=True,
)
+ @mock.patch("readthedocs.doc_builder.director.load_yaml_config")
+ def test_build_jobs_partial_build_override(self, load_yaml_config):
+ config = BuildConfigV2(
+ {
+ "version": 2,
+ "formats": ["pdf", "epub", "htmlzip"],
+ "build": {
+ "os": "ubuntu-24.04",
+ "tools": {"python": "3.12"},
+ "jobs": {
+ "create_environment": ["echo create_environment"],
+ "install": ["echo install"],
+ "build": {
+ "html": ["echo build html"],
+ "pdf": ["echo build pdf"],
+ "epub": ["echo build epub"],
+ "htmlzip": ["echo build htmlzip"],
+ },
+ "post_build": ["echo end of build"],
+ },
+ },
+ },
+ source_file="readthedocs.yml",
+ )
+ config.validate()
+ load_yaml_config.return_value = config
+ self._trigger_update_docs_task()
+
+ python_version = settings.RTD_DOCKER_BUILD_SETTINGS["tools"]["python"]["3.12"]
+ self.mocker.mocks["environment.run"].assert_has_calls(
+ [
+ mock.call("asdf", "install", "python", python_version),
+ mock.call("asdf", "global", "python", python_version),
+ mock.call("asdf", "reshim", "python", record=False),
+ mock.call(
+ "python",
+ "-mpip",
+ "install",
+ "-U",
+ "virtualenv",
+ "setuptools",
+ ),
+ mock.call(
+ "echo create_environment",
+ escape_command=False,
+ cwd=mock.ANY,
+ ),
+ mock.call(
+ "echo install",
+ escape_command=False,
+ cwd=mock.ANY,
+ ),
+ mock.call(
+ "echo build html",
+ escape_command=False,
+ cwd=mock.ANY,
+ ),
+ mock.call(
+ "echo build htmlzip",
+ escape_command=False,
+ cwd=mock.ANY,
+ ),
+ mock.call(
+ "echo build pdf",
+ escape_command=False,
+ cwd=mock.ANY,
+ ),
+ mock.call(
+ "echo build epub",
+ escape_command=False,
+ cwd=mock.ANY,
+ ),
+ mock.call(
+ "echo end of build",
+ escape_command=False,
+ cwd=mock.ANY,
+ ),
+ ]
+ )
+
+ @mock.patch("readthedocs.doc_builder.director.load_yaml_config")
+ def test_build_jobs_partial_build_override_empty_commands(self, load_yaml_config):
+ config = BuildConfigV2(
+ {
+ "version": 2,
+ "formats": ["pdf"],
+ "build": {
+ "os": "ubuntu-24.04",
+ "tools": {"python": "3.12"},
+ "jobs": {
+ "create_environment": [],
+ "install": [],
+ "build": {
+ "html": [],
+ "pdf": [],
+ },
+ "post_build": ["echo end of build"],
+ },
+ },
+ },
+ source_file="readthedocs.yml",
+ )
+ config.validate()
+ load_yaml_config.return_value = config
+ self._trigger_update_docs_task()
+
+ python_version = settings.RTD_DOCKER_BUILD_SETTINGS["tools"]["python"]["3.12"]
+ self.mocker.mocks["environment.run"].assert_has_calls(
+ [
+ mock.call("asdf", "install", "python", python_version),
+ mock.call("asdf", "global", "python", python_version),
+ mock.call("asdf", "reshim", "python", record=False),
+ mock.call(
+ "python",
+ "-mpip",
+ "install",
+ "-U",
+ "virtualenv",
+ "setuptools",
+ ),
+ mock.call("echo end of build", escape_command=False, cwd=mock.ANY),
+ ]
+ )
+
@mock.patch("readthedocs.doc_builder.director.tarfile")
@mock.patch("readthedocs.doc_builder.director.build_tools_storage")
@mock.patch("readthedocs.doc_builder.director.load_yaml_config")
diff --git a/readthedocs/rtd_tests/fixtures/spec/v2/schema.json b/readthedocs/rtd_tests/fixtures/spec/v2/schema.json
index 2cb5559f5f2..716b04e55ef 100644
--- a/readthedocs/rtd_tests/fixtures/spec/v2/schema.json
+++ b/readthedocs/rtd_tests/fixtures/spec/v2/schema.json
@@ -9,9 +9,7 @@
"title": "Version",
"description": "The version of the spec to use.",
"type": "number",
- "enum": [
- 2
- ]
+ "enum": [2]
},
"formats": {
"title": "Formats",
@@ -20,17 +18,11 @@
{
"type": "array",
"items": {
- "enum": [
- "htmlzip",
- "pdf",
- "epub"
- ]
+ "enum": ["htmlzip", "pdf", "epub"]
}
},
{
- "enum": [
- "all"
- ]
+ "enum": ["all"]
}
],
"default": []
@@ -46,9 +38,7 @@
"type": "string"
}
},
- "required": [
- "environment"
- ]
+ "required": ["environment"]
},
"build": {
"title": "Build",
@@ -98,6 +88,14 @@
"type": "string"
}
},
+ "create_environment": {
+ "description": "Override the default environment creation process.",
+ "type": "array",
+ "items": {
+ "title": "Custom commands",
+ "type": "string"
+ }
+ },
"post_create_environment": {
"type": "array",
"items": {
@@ -112,6 +110,14 @@
"type": "string"
}
},
+ "install": {
+ "description": "Override the default installation process.",
+ "type": "array",
+ "items": {
+ "title": "Custom commands",
+ "type": "string"
+ }
+ },
"post_install": {
"type": "array",
"items": {
@@ -126,6 +132,41 @@
"type": "string"
}
},
+ "build": {
+ "description": "Override the default build process.",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "html": {
+ "type": "array",
+ "items": {
+ "title": "Custom commands",
+ "type": "string"
+ }
+ },
+ "htmlzip": {
+ "type": "array",
+ "items": {
+ "title": "Custom commands",
+ "type": "string"
+ }
+ },
+ "pdf": {
+ "type": "array",
+ "items": {
+ "title": "Custom commands",
+ "type": "string"
+ }
+ },
+ "epub": {
+ "type": "array",
+ "items": {
+ "title": "Custom commands",
+ "type": "string"
+ }
+ }
+ }
+ },
"post_build": {
"type": "array",
"items": {
@@ -165,22 +206,10 @@
]
},
"nodejs": {
- "enum": [
- "14",
- "16",
- "18",
- "19",
- "20",
- "22",
- "23",
- "latest"
- ]
+ "enum": ["14", "16", "18", "19", "20", "22", "23", "latest"]
},
"ruby": {
- "enum": [
- "3.3",
- "latest"
- ]
+ "enum": ["3.3", "latest"]
},
"rust": {
"enum": [
@@ -230,10 +259,7 @@
}
}
},
- "required": [
- "os",
- "tools"
- ],
+ "required": ["os", "tools"],
"additionalProperties": false
},
"python": {
@@ -255,9 +281,7 @@
"type": "string"
}
},
- "required": [
- "requirements"
- ]
+ "required": ["requirements"]
},
{
"properties": {
@@ -269,10 +293,7 @@
"method": {
"title": "Method",
"description": "Install using python setup.py install or pip.",
- "enum": [
- "pip",
- "setuptools"
- ],
+ "enum": ["pip", "setuptools"],
"default": "pip"
},
"extra_requirements": {
@@ -285,9 +306,7 @@
"default": []
}
},
- "required": [
- "path"
- ]
+ "required": ["path"]
}
]
}
@@ -303,11 +322,7 @@
"builder": {
"title": "Builder",
"description": "The builder type for the sphinx documentation.",
- "enum": [
- "html",
- "dirhtml",
- "singlehtml"
- ],
+ "enum": ["html", "dirhtml", "singlehtml"],
"default": "html"
},
"configuration": {
@@ -359,9 +374,7 @@
}
},
{
- "enum": [
- "all"
- ]
+ "enum": ["all"]
}
],
"default": []
@@ -377,9 +390,7 @@
}
},
{
- "enum": [
- "all"
- ]
+ "enum": ["all"]
}
],
"default": []
@@ -425,8 +436,6 @@
"additionalProperties": false
}
},
- "required": [
- "version"
- ],
+ "required": ["version"],
"additionalProperties": false
}
diff --git a/requirements/deploy.txt b/requirements/deploy.txt
index e80cc0fff66..856585f92a5 100644
--- a/requirements/deploy.txt
+++ b/requirements/deploy.txt
@@ -8,6 +8,10 @@ amqp==5.3.1
# via
# -r requirements/pip.txt
# kombu
+annotated-types==0.7.0
+ # via
+ # -r requirements/pip.txt
+ # pydantic
asgiref==3.8.1
# via
# -r requirements/pip.txt
@@ -309,6 +313,12 @@ pycparser==2.22
# via
# -r requirements/pip.txt
# cffi
+pydantic==2.9.2
+ # via -r requirements/pip.txt
+pydantic-core==2.23.4
+ # via
+ # -r requirements/pip.txt
+ # pydantic
pygments==2.18.0
# via
# -r requirements/pip.txt
@@ -422,6 +432,8 @@ typing-extensions==4.12.2
# ipython
# psycopg
# psycopg-pool
+ # pydantic
+ # pydantic-core
tzdata==2024.2
# via
# -r requirements/pip.txt
diff --git a/requirements/docker.txt b/requirements/docker.txt
index 1225cafb58d..bb1accd52c5 100644
--- a/requirements/docker.txt
+++ b/requirements/docker.txt
@@ -8,6 +8,10 @@ amqp==5.3.1
# via
# -r requirements/pip.txt
# kombu
+annotated-types==0.7.0
+ # via
+ # -r requirements/pip.txt
+ # pydantic
asgiref==3.8.1
# via
# -r requirements/pip.txt
@@ -333,6 +337,12 @@ pycparser==2.22
# via
# -r requirements/pip.txt
# cffi
+pydantic==2.9.2
+ # via -r requirements/pip.txt
+pydantic-core==2.23.4
+ # via
+ # -r requirements/pip.txt
+ # pydantic
pygments==2.18.0
# via
# -r requirements/pip.txt
@@ -454,6 +464,8 @@ typing-extensions==4.12.2
# ipython
# psycopg
# psycopg-pool
+ # pydantic
+ # pydantic-core
# rich
# tox
tzdata==2024.2
diff --git a/requirements/pip.in b/requirements/pip.in
index 6813aeb4975..e8f98c23001 100644
--- a/requirements/pip.in
+++ b/requirements/pip.in
@@ -45,6 +45,7 @@ requests-toolbelt
slumber
pyyaml
Pygments
+pydantic
dnspython
diff --git a/requirements/pip.txt b/requirements/pip.txt
index 1551ae53cf6..89ef51f3650 100644
--- a/requirements/pip.txt
+++ b/requirements/pip.txt
@@ -6,6 +6,8 @@
#
amqp==5.3.1
# via kombu
+annotated-types==0.7.0
+ # via pydantic
asgiref==3.8.1
# via
# django
@@ -221,6 +223,10 @@ psycopg-pool==3.2.4
# via psycopg
pycparser==2.22
# via cffi
+pydantic==2.9.2
+ # via -r requirements/pip.in
+pydantic-core==2.23.4
+ # via pydantic
pygments==2.18.0
# via -r requirements/pip.in
pyjwt[crypto]==2.10.0
@@ -302,6 +308,8 @@ typing-extensions==4.12.2
# elasticsearch-dsl
# psycopg
# psycopg-pool
+ # pydantic
+ # pydantic-core
tzdata==2024.2
# via
# -r requirements/pip.in
diff --git a/requirements/testing.txt b/requirements/testing.txt
index 4037ffd4a54..ad344fbd13e 100644
--- a/requirements/testing.txt
+++ b/requirements/testing.txt
@@ -10,6 +10,10 @@ amqp==5.3.1
# via
# -r requirements/pip.txt
# kombu
+annotated-types==0.7.0
+ # via
+ # -r requirements/pip.txt
+ # pydantic
asgiref==3.8.1
# via
# -r requirements/pip.txt
@@ -311,6 +315,12 @@ pycparser==2.22
# via
# -r requirements/pip.txt
# cffi
+pydantic==2.9.2
+ # via -r requirements/pip.txt
+pydantic-core==2.23.4
+ # via
+ # -r requirements/pip.txt
+ # pydantic
pygments==2.18.0
# via
# -r requirements/pip.txt
@@ -452,6 +462,8 @@ typing-extensions==4.12.2
# elasticsearch-dsl
# psycopg
# psycopg-pool
+ # pydantic
+ # pydantic-core
# sphinx
tzdata==2024.2
# via