From ebe3b2c7f0ca9f206b2fed0c8e53269f18e62774 Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Mon, 25 Nov 2024 12:50:23 -0500 Subject: [PATCH] Build: allow partial override of build steps (#11710) I was replacing some of the config models with dataclasses, but I found myself re-implementing some helpers that pydantic provides, so I'm introducing that new dep, it has everything we need, we may be able to use it to simplify our validation once all models are migrated to pydantic. ### About incompatible options I decided to just not allow formats when `build.jobs.build` is used, seems just easier that way. But not sure if users may want to just override the html build step while still using the default build pdf step, if that's the case, we may need to support using formats... or have another way of saying "use the default". build.jobs.create_environment is kind of incompatible with python/conda. Since users will need to manually create the environment, but they may still want to use the defaults from build.jobs.install, or maybe they can't even use the defaults, as they are really tied to the default create_environment step. Which bring us to the next point, if build.jobs.create_environment is overridden, users will likely need to override build.jobs.install and build.jobs.build as well... Maybe just save the user some time and require them to override all of them? Or maybe just let them figure it out by themselves with the errors? We could gather more information once we see some real usage of this... ### Override them all I chose to make build.html required if users override that step, seems logical to do so. ~~Do we want to allow users to build all formats? I'm starting with html and pdf, that seem the most used. But shouldn't be a problem to support all of them. I guess my only question would be about naming, `htmlzip` has always been a weird name, maybe just zip?~~ I just went ahead and allowed all, with the same name as formats. ### Docs I didn't add docs yet because there is the question... should we just expose this to users? Or maybe just test it internally for now? Closes https://github.com/readthedocs/readthedocs.org/issues/11551 --- .circleci/config.yml | 2 +- readthedocs/config/config.py | 38 ++++- readthedocs/config/exceptions.py | 3 + readthedocs/config/models.py | 55 ++++--- readthedocs/config/notifications.py | 12 ++ readthedocs/config/tests/test_config.py | 112 ++++++++++++++ readthedocs/core/utils/objects.py | 22 +++ readthedocs/doc_builder/director.py | 36 ++++- .../projects/tests/test_build_tasks.py | 144 +++++++++++++++++- .../rtd_tests/fixtures/spec/v2/schema.json | 121 ++++++++------- requirements/deploy.txt | 12 ++ requirements/docker.txt | 12 ++ requirements/pip.in | 1 + requirements/pip.txt | 8 + requirements/testing.txt | 12 ++ 15 files changed, 494 insertions(+), 96 deletions(-) create mode 100644 readthedocs/core/utils/objects.py 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