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