From 678ec30686af0b99a0632dde01dcf592aed0e306 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Mon, 4 Nov 2024 17:11:58 +0000 Subject: [PATCH] Add `experimental_allow_partial` support (#10748) Co-authored-by: hyperlint-ai[bot] <154288675+hyperlint-ai[bot]@users.noreply.github.com> Co-authored-by: sydney-runkle Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com> --- docs/concepts/experimental.md | 241 ++++++++++++++++++++++++++++++ docs/plugins/main.py | 9 +- pdm.lock | 204 ++++++++++++------------- pydantic/functional_validators.py | 1 + pydantic/type_adapter.py | 41 ++++- pyproject.toml | 4 +- tests/test_allow_partial.py | 87 +++++++++++ tests/test_datetime.py | 4 +- 8 files changed, 479 insertions(+), 112 deletions(-) create mode 100644 tests/test_allow_partial.py diff --git a/docs/concepts/experimental.md b/docs/concepts/experimental.md index 379ecec1bee..99628353d36 100644 --- a/docs/concepts/experimental.md +++ b/docs/concepts/experimental.md @@ -149,3 +149,244 @@ This example uses plain idiomatic Python code that may be easier to understand, The approach you choose should really depend on your use case. You will have to compare verbosity, performance, ease of returning meaningful errors to your users, etc. to choose the right pattern. Just be mindful of abusing advanced patterns like the pipeline API just because you can. + +## Partial Validation + +Pydantic v2.10.0 introduces experimental support for "partial validation". + +This allows you to validate an incomplete JSON string, or a Python object representing incomplete input data. + +Partial validation is particularly helpful when processing the output of an LLM, where the model streams structured responses, and you may wish to begin validating the stream while you're still receiving data (e.g. to show partial data to users). + +!!! warning + Partial validation is an experimental feature and may change in future versions of Pydantic. The current implementation should be considered a proof of concept at this time and has a number of [limitations](#limitations-of-partial-validation). + +Partial validation can be enabled when using the three validation methods on `TypeAdapter`: [`TypeAdapter.validate_json()`][pydantic.TypeAdapter.validate_json], [`TypeAdapter.validate_python()`][pydantic.TypeAdapter.validate_python], and [`TypeAdapter.validate_strings()`][pydantic.TypeAdapter.validate_strings]. This allows you to parse and validation incomplete JSON, but also to validate Python objects created by parsing incomplete data of any format. + +`experiment_allow_partial` in action: + +```python +from typing import List + +from annotated_types import MinLen +from typing_extensions import Annotated, NotRequired, TypedDict + +from pydantic import TypeAdapter + + +class Foobar(TypedDict): # (1)! + a: int + b: NotRequired[float] + c: NotRequired[Annotated[str, MinLen(5)]] + + +ta = TypeAdapter(List[Foobar]) + +v = ta.validate_json('[{"a": 1, "b"', experimental_allow_partial=True) # (2)! +print(v) +#> [{'a': 1}] + +v = ta.validate_json( + '[{"a": 1, "b": 1.0, "c": "abcd', experimental_allow_partial=True # (3)! +) +print(v) +#> [{'a': 1, 'b': 1.0}] + +v = ta.validate_json( + '[{"b": 1.0, "c": "abcde"', experimental_allow_partial=True # (4)! +) +print(v) +#> [] + +v = ta.validate_json( + '[{"a": 1, "b": 1.0, "c": "abcde"},{"a": ', experimental_allow_partial=True +) +print(v) +#> [{'a': 1, 'b': 1.0, 'c': 'abcde'}] + +v = ta.validate_python([{'a': 1}], experimental_allow_partial=True) # (5)! +print(v) +#> [{'a': 1}] + +v = ta.validate_python( + [{'a': 1, 'b': 1.0, 'c': 'abcd'}], experimental_allow_partial=True # (6)! +) +print(v) +#> [{'a': 1, 'b': 1.0}] +``` + +1. The TypedDict `Foobar` has three field, but only `a` is required, that means that a valid instance of `Foobar` can be created even if the `b` and `c` fields are missing. +2. Parsing JSON, the input is valid JSON up to the point where the string is truncated. +3. In this case truncation of the input means the value of `c` (`abcd`) is invalid as input to `c` field, hence it's omitted. +4. The `a` field is required, so validation on the only item in the list fails and is dropped. +5. Partial validation also works with Python objects, it should have the same semantics as with JSON except of course you can't have a genuinely "incomplete" Python object. +6. The same as above but with a Python object, `c` is dropped as it's not required and failed validation. + +### How Partial Validation Works + +Partial validation follows the zen of Pydantic — it makes no guarantees about what the input data might have been, but it does guarantee to return a valid instance of the type you required, or raise a validation error. + +To do this, the `experimental_allow_partial` flag enables two pieces of behavior: + +#### 1. Partial JSON parsing + +The [jiter](https://github.com/pydantic/jiter) JSON parser used by Pydantic already supports parsing partial JSON, +`experimental_allow_partial` is simply passed to jiter via the `allow_partial` argument. + +!!! note + If you just want pure JSON parsing with support for partial JSON, you can use the [`jiter`](https://pypi.org/project/jiter/) Python library directly, or pass the `allow_partial` argument when calling [`pydantic_core.from_json`][pydantic_core.from_json]. + +#### 2. Ignore errors in the last element of the input {#2-ignore-errors-in-last} + +Only having access to part of the input data means errors can commonly occur in the last element of the input data. + +For example: + +* if a string has a constraint `MinLen(5)`, when you only see part of the input, validation might fail because part of the string is missing (e.g. `{"name": "Sam` instead of `{"name": "Samuel"}`) +* if an `int` field has a constraint `Ge(10)`, when you only see part of the input, validation might fail because the number is too small (e.g. `1` instead of `10`) +* if a `TypedDict` field has 3 required fields, but the partial input only has two of the fields, validation would fail because some field are missing +* etc. etc. — there are lost more cases like this + +The point is that if you only see part of some valid input data, validation errors can often occur in the last element of a sequence or last value of mapping. + +To avoid these errors breaking partial validation, Pydantic will ignore ALL errors in the last element of the input data. + +```py title="Errors in last element ignored" +from typing import List + +from annotated_types import MinLen +from typing_extensions import Annotated + +from pydantic import BaseModel, TypeAdapter + + +class MyModel(BaseModel): + a: int + b: Annotated[str, MinLen(5)] + + +ta = TypeAdapter(List[MyModel]) +v = ta.validate_json( + '[{"a": 1, "b": "12345"}, {"a": 1,', + experimental_allow_partial=True, +) +print(v) +#> [MyModel(a=1, b='12345')] +``` + +### Limitations of Partial Validation + +#### TypeAdapter only + +You can only pass `experiment_allow_partial` to [`TypeAdapter`][pydantic.TypeAdapter] methods, it's not yet supported via other Pydantic entry points like [`BaseModel`][pydantic.BaseModel]. + +#### Types supported + +Right now only a subset of collection validators know how to handle partial validation: + +- `list` +- `set` +- `frozenset` +- `dict` (as in `dict[X, Y]`) +- `TypedDict` — only non-required fields may be missing, e.g. via [`NotRequired`][typing.NotRequired] or [`total=False`][typing.TypedDict.__total__]) + +While you can use `experimental_allow_partial` while validating against types that include other collection validators, those types will be validated "all or nothing", and partial validation will not work on more nested types. + +E.g. in the [above](#2-ignore-errors-in-last) example partial validation works although the second item in the list is dropped completely since `BaseModel` doesn't (yet) support partial validation. + +But partial validation won't work at all in the follow example because `BaseModel` doesn't support partial validation so it doesn't forward the `allow_partial` instruction down to the list validator in `b`: + +```py +from typing import List + +from annotated_types import MinLen +from typing_extensions import Annotated + +from pydantic import BaseModel, TypeAdapter, ValidationError + + +class MyModel(BaseModel): + a: int = 1 + b: List[Annotated[str, MinLen(5)]] = [] # (1)! + + +ta = TypeAdapter(MyModel) +try: + v = ta.validate_json( + '{"a": 1, "b": ["12345", "12', experimental_allow_partial=True + ) +except ValidationError as e: + print(e) + """ + 1 validation error for MyModel + b.1 + String should have at least 5 characters [type=string_too_short, input_value='12', input_type=str] + """ +``` + +1. The list validator for `b` doesn't get the `allow_partial` instruction passed down to it by the model validator so it doesn't know to ignore errors in the last element of the input. + +#### Some invalid but complete JSON will be accepted + +The way [jiter](https://github.com/pydantic/jiter) (the JSON parser used by Pydantic) works means it's currently not possible to differentiate between complete JSON like `{"a": 1, "b": "12"}` and incomplete JSON like `{"a": 1, "b": "12`. + +This means that some invalid JSON will be accepted by Pydantic when using `experimental_allow_partial`, e.g.: + +```py +from annotated_types import MinLen +from typing_extensions import Annotated, TypedDict + +from pydantic import TypeAdapter + + +class Foobar(TypedDict, total=False): + a: int + b: Annotated[str, MinLen(5)] + + +ta = TypeAdapter(Foobar) + +v = ta.validate_json( + '{"a": 1, "b": "12', experimental_allow_partial=True # (1)! +) +print(v) +#> {'a': 1} + +v = ta.validate_json( + '{"a": 1, "b": "12"}', experimental_allow_partial=True # (2)! +) +print(v) +#> {'a': 1} +``` + +1. This will pass validation as expected although the last field will be omitted as it failed validation. +2. This will also pass validation since the binary representation of the JSON data passed to pydantic-core is indistinguishable from the previous case. + +#### Any error in the last field of the input will be ignored + +As described [above](#2-ignore-errors-in-last), many errors can result from truncating the input. Rather than trying to specifically ignore errors that could result from truncation, Pydantic ignores all errors in the last element of the input in partial validation mode. + +This means clearly invalid data will pass validation if the error is in the last field of the input: + +```py +from typing import List + +from annotated_types import Ge +from typing_extensions import Annotated + +from pydantic import TypeAdapter + +ta = TypeAdapter(List[Annotated[int, Ge(10)]]) +v = ta.validate_python([20, 30, 4], experimental_allow_partial=True) # (1)! +print(v) +#> [20, 30] + +ta = TypeAdapter(List[int]) + +v = ta.validate_python([1, 2, 'wrong'], experimental_allow_partial=True) # (2)! +print(v) +#> [1, 2] +``` + +1. As you would expect, this will pass validation since Pydantic correctly ignores the error in the (truncated) last item. +2. This will also pass validation since the error in the last item is ignored. diff --git a/docs/plugins/main.py b/docs/plugins/main.py index 64ad719c463..4dcff597f6e 100644 --- a/docs/plugins/main.py +++ b/docs/plugins/main.py @@ -94,7 +94,14 @@ def add_changelog() -> None: def add_mkdocs_run_deps() -> None: # set the pydantic, pydantic-core, pydantic-extra-types versions to configure for running examples in the browser pyproject_toml = (PROJECT_ROOT / 'pyproject.toml').read_text() - pydantic_core_version = re.search(r'pydantic-core==(.+?)["\']', pyproject_toml).group(1) + m = re.search(r'pydantic-core==(.+?)["\']', pyproject_toml) + if not m: + logger.info( + "Could not find pydantic-core version in pyproject.toml, this is expected if you're using a git ref" + ) + return + + pydantic_core_version = m.group(1) version_py = (PROJECT_ROOT / 'pydantic' / 'version.py').read_text() pydantic_version = re.search(r'^VERSION ?= (["\'])(.+)\1', version_py, flags=re.M).group(2) diff --git a/pdm.lock b/pdm.lock index 89170979c61..78f796e99b9 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "docs", "email", "linting", "memray", "mypy", "testing", "testing-extra", "timezone"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:08ebe3352983bbe7ee8a93f7e450cf19bbfbb32338734e2478cd819e331e4019" +content_hash = "sha256:7a84a75d5cba8f0175a73d2199097417a2bbea81febd5fc663da277fcbd70751" [[metadata.targets]] requires_python = ">=3.8" @@ -1567,7 +1567,7 @@ files = [ [[package]] name = "pydantic-core" -version = "2.25.1" +version = "2.26.0" requires_python = ">=3.8" summary = "Core functionality for Pydantic validation and serialization" groups = ["default"] @@ -1575,106 +1575,106 @@ dependencies = [ "typing-extensions!=4.7.0,>=4.6.0", ] files = [ - {file = "pydantic_core-2.25.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3bc7b722e7b2fe9a1ce9bf8f86c067647b3755ed86fa07da6a6518f2b38d1e82"}, - {file = "pydantic_core-2.25.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:273882cc3b2b1a93833f50f5f664dcb0d7cb9260249f24f7dad3700954677a0c"}, - {file = "pydantic_core-2.25.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1616c3d3f20c4f52274c9eb1969b2f222aa4cc982d81ff623cb82bcca8ee8abb"}, - {file = "pydantic_core-2.25.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:945bceb436202ae586b8d18c3731ebe5cabb0a3ad51c646fb4fe0d098f5812bf"}, - {file = "pydantic_core-2.25.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5451afa777910768026abba5c88b4bfa537fa83d31ab1ff451ba1392056c2e8c"}, - {file = "pydantic_core-2.25.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e5ed1eb7bd61178a1d633e886168dc42a89d23561cca2ce950a41823b542556"}, - {file = "pydantic_core-2.25.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f03fd5187faca1eff6825c4fd7be3845d2220681154e0dea36bdda5e95e4f054"}, - {file = "pydantic_core-2.25.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f3bac150a3e0ed33a5987fcf8bbacaa245afee6711096991528b55cc5664d692"}, - {file = "pydantic_core-2.25.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:89f0c2b4bc2fba6457f1469b48bc6cf9d88e02420f522a70cf5ba3f046a9902d"}, - {file = "pydantic_core-2.25.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d1e82672b28a92b177b2665d80421795a1b3d7c1163776a2f2e3629f95b009c3"}, - {file = "pydantic_core-2.25.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c89d28525ab712ae1f52ba546aea117a5bcfa9296d19f967b4e2b5f7a400dfb0"}, - {file = "pydantic_core-2.25.1-cp310-none-win32.whl", hash = "sha256:7c2dde337fd0a1e77124388d2d99348b36b743df7ce42a8aeeecbb82177c5ccf"}, - {file = "pydantic_core-2.25.1-cp310-none-win_amd64.whl", hash = "sha256:d3face60f1436c05e1ca0b03a8120b61f70dc61139301e6deff2ab442d3fe5a5"}, - {file = "pydantic_core-2.25.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:517818d99ae113597e9ff5264ca48ae5ac03e98d37dced7bd069ca4327ba4509"}, - {file = "pydantic_core-2.25.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:09fcaacbcdffbd4a4c9903c468a710b55b056653ecfc6ddaa4b6554819989d28"}, - {file = "pydantic_core-2.25.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c02c26ab7971a6a1f47db8fc881b33c44fad9d83ad7e3a7a5765a58014af52f3"}, - {file = "pydantic_core-2.25.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:386bea5d03843be8d1c941282a28d9cc8b8cc7419aa9152e965f8e7bca115506"}, - {file = "pydantic_core-2.25.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48aee08585b93abb9151e1087cc4aae7000c5127b847affa1d6f2e17906f62ca"}, - {file = "pydantic_core-2.25.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6bdee603ab577966da59e61a69280bc69e4fb2ed531a63b3983f515bc0dae152"}, - {file = "pydantic_core-2.25.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:355150ce11434f7eb6852367e01fc1e1af3939cba1ed5b84fb2b20afef8c11c8"}, - {file = "pydantic_core-2.25.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8a4c8f3d3d61a5a8f1dabc68f9a7f4dfcb2725737f50ffb8555af85fedaf8163"}, - {file = "pydantic_core-2.25.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ecf34fa1b98491b7edac3bfc0f194578e29a8190d84d0c015a38e14513392633"}, - {file = "pydantic_core-2.25.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:df90073cb58ae4e4d4b6ab99190bd296760f712fb5d3944d57e5bf669eeb26ec"}, - {file = "pydantic_core-2.25.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:adaca50e89fc8c0123e0e9fe478ee90df5cd8e5cdedca36a6e29d9584e9fb0ce"}, - {file = "pydantic_core-2.25.1-cp311-none-win32.whl", hash = "sha256:e2b8395595a41975c9028c9dc2a7647167ec72ac3f13d63485f5fd74b0f2501f"}, - {file = "pydantic_core-2.25.1-cp311-none-win_amd64.whl", hash = "sha256:80ce6ed970f5616f3ade51ac0fea73856fe5bd9af9f825c54d3f691878e05c29"}, - {file = "pydantic_core-2.25.1-cp311-none-win_arm64.whl", hash = "sha256:a4db1f6c7423d06bf55c3e0b8ef5b52f1b39911e3cc6670871c070f1697af23f"}, - {file = "pydantic_core-2.25.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:44ceb1d1dfe7eb172d934bd959eec324abfd9b298cdca6de42fe9b2ec1ac9ba6"}, - {file = "pydantic_core-2.25.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebb36fc38c98b0e6bb4862f99404ccd84fbdb61f19faf45122b52826c3f9706b"}, - {file = "pydantic_core-2.25.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b26ff1aa56dcb78bd56842487e1a6131b6ee02f0ae9738ebab2a09db31884f0f"}, - {file = "pydantic_core-2.25.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9d82a6460ed3b881fbe8e2ff30d6446ca2007cae8fe85c32938ddab00e462048"}, - {file = "pydantic_core-2.25.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74724f2fb6ebf65a9cb1a1e81df55fa3079ec8e3bfc32319bc9838cea0974f83"}, - {file = "pydantic_core-2.25.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6c8fae12e56e658558957e49824fcee79d440e88ea086b9770564978e7c9e50a"}, - {file = "pydantic_core-2.25.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1dd5c9bb2a104a137c2238ae91b9e0918668bbb6c85abbc2ebdae3e1e08cb629"}, - {file = "pydantic_core-2.25.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e3fc3c0bdf6dd6b3b34fd841693d10d0b54467d175f436b6f59be657af76e3d6"}, - {file = "pydantic_core-2.25.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:be211cd13103c3c25bf2ed0c5eb2cc02ddf4ce4d772d4536395db391d9e93f3b"}, - {file = "pydantic_core-2.25.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:5b5298e73a50f9238becf1c7149555b1d75694eae3ad0e9ac36cb31a245cee3a"}, - {file = "pydantic_core-2.25.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:304773353e0a57cd1eea40547f4771c2b65ae74f4e3ee085f62dbbdddcf05303"}, - {file = "pydantic_core-2.25.1-cp312-none-win32.whl", hash = "sha256:db78ed221a7edf25301e71fcf3952664f7bb2fe1b398c8b048094fc4cd31205d"}, - {file = "pydantic_core-2.25.1-cp312-none-win_amd64.whl", hash = "sha256:0e1ade3e12dbdc9cb5f441593a4330e3e36c71f27881ecfaf6792a86086ce9a5"}, - {file = "pydantic_core-2.25.1-cp312-none-win_arm64.whl", hash = "sha256:d5748acbf328c3a2e1f159d82231f89df74233c791fd1750100d684084bf9044"}, - {file = "pydantic_core-2.25.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:dd617816c231c9255c0ee3ad99e7668d46aa1884a660036ef83daf9344400cd5"}, - {file = "pydantic_core-2.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:05a80bbc14de746a88d335b4b88be47ede92d2c3846b74d03e01e803dc4a089f"}, - {file = "pydantic_core-2.25.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3e05ddb1da6aa0b3484f96ee4d64bf58f7a090c9bbe76935250c365faa1acff"}, - {file = "pydantic_core-2.25.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf800e1096b41c7e00b120a528535511539b641c32302df98b2fd86836ab3997"}, - {file = "pydantic_core-2.25.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee0289c725dd256e78602b182597cd768931954f5a3e8e8be2508a062a8f763e"}, - {file = "pydantic_core-2.25.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e26f92a7f956fcafcd10266be67362bae4e4a54ccb33290c93627c6d165bafbb"}, - {file = "pydantic_core-2.25.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f2b7c871502ec0186f7452812992378c24d6dcf43cbf4f01f4a7e9b34fdc6a6"}, - {file = "pydantic_core-2.25.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7720c69fe032fc9f288e87cbc1c9311aba0980b24041d39b962959fc6f64e391"}, - {file = "pydantic_core-2.25.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7f7fccdbb8fcc8c0e6f479b31b3cb00af8b3dc213f05689153435ba3e285486c"}, - {file = "pydantic_core-2.25.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:814accb8d57ec0be02d467d904ec4f466c4b787c968070a460b8b35ce1bc63ad"}, - {file = "pydantic_core-2.25.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:36ba0589001af5ec5dd509c566fbd9eeda0352341e9b31febb6d774d06047a40"}, - {file = "pydantic_core-2.25.1-cp313-none-win32.whl", hash = "sha256:0f8a2c183d34f859828d8aa4355a1889ad7b07ac36a7ae2ce3a03618559cfd41"}, - {file = "pydantic_core-2.25.1-cp313-none-win_amd64.whl", hash = "sha256:0705ecaf87d5f78f3fba169c2309049a510ad581c32649007c050c1c6c474e22"}, - {file = "pydantic_core-2.25.1-cp313-none-win_arm64.whl", hash = "sha256:ba423cd21f795fec3019f1a97ecd3f7efa44b31fc0731db320a7a7e566240685"}, - {file = "pydantic_core-2.25.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:12157a96809206e63e62cf10ba1075fb6f85711d2804fb7181c78cd8a5f19ec9"}, - {file = "pydantic_core-2.25.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9529822fb8b592941500ad03481faa2f7264f504d34cf58d990aa3592ae666c1"}, - {file = "pydantic_core-2.25.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f789888d855d110428764e4ed28e57f9d60a40cd4de38626b61424be1f0a74cd"}, - {file = "pydantic_core-2.25.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87802eea7b06d4201ca9f40228f305fb495e0999d849347c6f5d274d20bbab43"}, - {file = "pydantic_core-2.25.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2367d0c7ace1c72b837ce04a7b7cff29155fe971f4fbfb8beb80c57a44528cb"}, - {file = "pydantic_core-2.25.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd36614a23ab836d939d0e011c788b7c24157774d90eb1b407e48e8abcc2db6a"}, - {file = "pydantic_core-2.25.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea8566f380664d5faecbbb3750685c7e69e2e970c0ecefc0b7f96ebfac3fd586"}, - {file = "pydantic_core-2.25.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5989ce2662749cb38ed57c6762d912ddd9117e95678283c8855d93a5cd047c89"}, - {file = "pydantic_core-2.25.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:68a1e37af8fbf49c44f17e609f3cf10909a25816231a4b871d8fa544ce988b64"}, - {file = "pydantic_core-2.25.1-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:32b19b8da7b8f91e7d58a78ed08fc5033058d93f8d91cb6257cbad3bee30cec0"}, - {file = "pydantic_core-2.25.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ba09e6845a8ba46141791672a0caace87079680693c7ee1de28bef0a5c6d54e0"}, - {file = "pydantic_core-2.25.1-cp38-none-win32.whl", hash = "sha256:2e0a5ac3065808367b2f9c6acdad10ce8caf5d907502707412da2ae8a0b72cc3"}, - {file = "pydantic_core-2.25.1-cp38-none-win_amd64.whl", hash = "sha256:d49539771d312a131ca56e76e5ce3e9cd2c174231e3dc6274be8b6cd156e5c23"}, - {file = "pydantic_core-2.25.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e201e4ea41561dc5d19618ada18cd7ba118a4bbe16a8f062c9c32662626bf8ad"}, - {file = "pydantic_core-2.25.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6521562379e4bcbe91752d9a6755a15ca037116ab3b8727c87d5b320bf681ef4"}, - {file = "pydantic_core-2.25.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18646f9de75c7febc64bcef1394a04ffbf5b8e2663c7c3f342f26077769cdf2a"}, - {file = "pydantic_core-2.25.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e5ef8ca8b430a544037e9e55f44cc4c3601b65ed81098ca142a5469a4c290f14"}, - {file = "pydantic_core-2.25.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dfca5ce29d53fad4b7849293a5f286a8fc457fc1543b9b313b8d90cb1edcf809"}, - {file = "pydantic_core-2.25.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:324f5ded033de63d54cf57d0e5496c4e9dbbb43bbde66f07381d56005b684c30"}, - {file = "pydantic_core-2.25.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54e406dc99bda0c1f93a19d2a3dea6b62811346ff83f89950448cae741a8a301"}, - {file = "pydantic_core-2.25.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:919dae86f3e5ebac10fb0b4f1e941a2d7a25dc68c972836de2e3e13a192504e2"}, - {file = "pydantic_core-2.25.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:17863b469e5346a8a88f6a7df63c44dad2cafc1c9c823ec79f90d2a9b93a2436"}, - {file = "pydantic_core-2.25.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:0e0f5cc5bf71f8bcc43f306994bace6d68d6fa81d9fd2d1091431ae795cf8d8c"}, - {file = "pydantic_core-2.25.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:eaf265fbcab0919671adccbdf4e396a0f24d79603b400abfad10a18b279ef7dd"}, - {file = "pydantic_core-2.25.1-cp39-none-win32.whl", hash = "sha256:0d95d16a327604146f0682d20f56c99e9db637f99950966eafffb4a558c8be69"}, - {file = "pydantic_core-2.25.1-cp39-none-win_amd64.whl", hash = "sha256:0b7dad9e6cf0a82d81b4084262443b20f203bce2ebdb05839aa26e924b23a758"}, - {file = "pydantic_core-2.25.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8fb6d52e452d1a8b39e5acbfc0426a7159b0b4eaa9593f51e1c9e63241b6e1ea"}, - {file = "pydantic_core-2.25.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:007b3a167ade9c37b73e4e9f0b60d95f736e884dd8c8eb14f6bef1e1777b437a"}, - {file = "pydantic_core-2.25.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47e30d1bc2eabc44308a10d63eb6c103a73f0147951b47e79bba782853a3c50f"}, - {file = "pydantic_core-2.25.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3625594a6f67b81d460bc05d12c5e4f29c6ac1764a83fa2ab5a816754e42d1a0"}, - {file = "pydantic_core-2.25.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc8d35699ce4ea8c538e328fed96405799f8f4a89edac2687498e8be29a90b32"}, - {file = "pydantic_core-2.25.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e5704f4659ed3471b9cf2d2d96a0c7df56b3d894b327f00781329915f1ad80b1"}, - {file = "pydantic_core-2.25.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:51fb42b4ff09d80814483fb0821574dcc24d08c42aa91de6364fc607c5caaf1a"}, - {file = "pydantic_core-2.25.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:c66cda574248db30f2d428f5fcc0377020c0910b1b8b2ef6ed037a430b530b16"}, - {file = "pydantic_core-2.25.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7804efac52bc2f1120ab51f1afa5aaaa6b91c1dd4c6d3f58cb307fa4dd1fdf4a"}, - {file = "pydantic_core-2.25.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c02209a870f2819bb53e3e50890ee7eb42b8be0a288abb138a1087eaf209cedc"}, - {file = "pydantic_core-2.25.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:9bc8aedf187675e39658782924316e6354cc6e3f81d6c56f3245d056605aa42c"}, - {file = "pydantic_core-2.25.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbf38f2470b1a1f0affd9ecc34ed3ea172703769a9b6359ae0ed93b8fb938b19"}, - {file = "pydantic_core-2.25.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d95b5a15d7a09de35c81e4eaba337adfe4d0bd78be2b13e356e9c2afd4fe0b0"}, - {file = "pydantic_core-2.25.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:391737b781291229acef3fd905a5e1aaec6be2da7e0b4168335b22a000194a4c"}, - {file = "pydantic_core-2.25.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0b8108a663063ff7df48bea39d6e84a87fe12f61a84f16d5a9521af89b9395d2"}, - {file = "pydantic_core-2.25.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2650f6c99eab83464b1fe50d1ea443a573c76c3dadaf665221d34d1d6dc96bbd"}, - {file = "pydantic_core-2.25.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:08b0e21a64288973437ef7ada876f2ee8e108987bece37dc27e87a62a42647aa"}, - {file = "pydantic_core-2.25.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7937d91a9bc03efa28361d6f4824b468b05756ce3c421e3b1fdc344141f3e50b"}, - {file = "pydantic_core-2.25.1.tar.gz", hash = "sha256:a2c6f872902b679b8ca5f121c802a2b74bc24f1b7d62ddf42474f1652d3215b1"}, + {file = "pydantic_core-2.26.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:fa77bb565223821cedb59e1fc4e9654fc55c3cfe8bf35cb6a23ceb3e4314ff1f"}, + {file = "pydantic_core-2.26.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e06ea3a4d2dd8213de98abafbd82455997daf5ed2c9ac858e13f1fe929e8ebff"}, + {file = "pydantic_core-2.26.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d717b3ee2208ee80a91382fd2d0d000c50f775a2a3a9b59b05e70063a82e767"}, + {file = "pydantic_core-2.26.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebd2ad03dff9a72952972cf89eaf62afc110dc4efc920b3ab1d1fe4cec350ebc"}, + {file = "pydantic_core-2.26.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c71d75e49d3d566f73389289a6470983a01e580ddc1d06105a31fd87410211da"}, + {file = "pydantic_core-2.26.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69f94e23cf93f487962eb5fd2294ca252f16b4f251b5a06b59f471777d842d3c"}, + {file = "pydantic_core-2.26.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e7282095ce7e535ba187f04171a9b149d244dfd4ae27b10953966fb1bbb7938"}, + {file = "pydantic_core-2.26.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f6bcc8f7f54a80aca5347ac540e2bea327114bbdb13ca1523c6b7672b0863c2a"}, + {file = "pydantic_core-2.26.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ffb4278eb5e1aefee951e18332ff5d8b2e76f40efc7f4931755525871de2dbb0"}, + {file = "pydantic_core-2.26.0-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:579e67fbd86d9fa9941198b0642e988d7e169df2e1f1d07b93bcd555c8075670"}, + {file = "pydantic_core-2.26.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fbca9dd66c7d47cc103288c93fd1f472626236c5d015dae1cbe236ffd84d7ebb"}, + {file = "pydantic_core-2.26.0-cp310-none-win32.whl", hash = "sha256:5a2e33c88c5f8d96d56e0c68e95f87663cc3ce4c20f207d0b382533bec836610"}, + {file = "pydantic_core-2.26.0-cp310-none-win_amd64.whl", hash = "sha256:f7b7f4ff5f1fc67b4bab2cbab5d5bd321a0bf40ed63bde9a0d439d78ad97d9c2"}, + {file = "pydantic_core-2.26.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a784ef6bbc8f3086601cba9fee29b6e608889a823762af5bb0b92227298d376a"}, + {file = "pydantic_core-2.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61c6b18e7cc5ab80a8f78b1181cd1fec18ea7b8e3b871995b337d6e5622c5a9f"}, + {file = "pydantic_core-2.26.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d561fa3a7b8267645f678a114105c4b91906da70fd4772cd0bf15f5b35987149"}, + {file = "pydantic_core-2.26.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77411c34d38c3fd0998d34e7a4e2a46432511f6b96096438691d08dfe103d40d"}, + {file = "pydantic_core-2.26.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b105bfd776f303e61b8d12f73dd8613be93d3df634d0a6a7d435661038530d8b"}, + {file = "pydantic_core-2.26.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d81b369d40e5077624e22ddbd5d3a2090b5eeab1fe836552019718f93c114fc0"}, + {file = "pydantic_core-2.26.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:616be5b07fb64d23a8ed6b711732d3038698b89f67d76f97248819626415bed8"}, + {file = "pydantic_core-2.26.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2e4de25be2bb212057f674ce211f395baa6181cadcc83ddf014bb29148515bef"}, + {file = "pydantic_core-2.26.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d736f34c75600020739d57ffd1a106e36e4afecf6f0d70db804fe9612195f0c6"}, + {file = "pydantic_core-2.26.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3867f3e474aa9191fb40dd413e7fbcaa1e2099603aae3da0d66141d9eb83b937"}, + {file = "pydantic_core-2.26.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:063a33d46af3593978bce031eac378c9603ca0a11719b0f7bd1969aa9e870a8c"}, + {file = "pydantic_core-2.26.0-cp311-none-win32.whl", hash = "sha256:82a803d0ae210f3f7dddbd774ad5a2cb53e40676cda7c91f670cf8297832225c"}, + {file = "pydantic_core-2.26.0-cp311-none-win_amd64.whl", hash = "sha256:7b35ab4a675b43acfb483dcdf9e11ef845b68ac9d8b6ca81dbaa522f38da0ed6"}, + {file = "pydantic_core-2.26.0-cp311-none-win_arm64.whl", hash = "sha256:b1fc2653ccc27bf7917cb70f13c64f4122edc4bc992e8be8c04ee9306dafce57"}, + {file = "pydantic_core-2.26.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9401038c16d560ae01551319141c0ffd2e1b80f9659ea535e076ca23ae55e866"}, + {file = "pydantic_core-2.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a91967938a5dd16d2074307d98a2232cec494d6215d97f5c0f09135be478517d"}, + {file = "pydantic_core-2.26.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3525264ca53dd25de397584fe83aa949042854229bbfe940ff7cb19ef5238691"}, + {file = "pydantic_core-2.26.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2076ead709b2e2f6c32677822a53d72c3aac0a91c9a189256390a67990fdd729"}, + {file = "pydantic_core-2.26.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a33391da35242c051004e23dc6ca138237943b1c182f6f7d7cd73b784047a76"}, + {file = "pydantic_core-2.26.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da5090641c03599317079b258c99a13c15ed2e4de334d6e4b5c39e5555f3f296"}, + {file = "pydantic_core-2.26.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:550f622b247b1209b8880043834be7f1ac24c33f63f54cd53ee4a6f62c80b9ec"}, + {file = "pydantic_core-2.26.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:48ef296842a01398305d2901c5c60347f8508d2f7d83bcfd9d3438bdfad96f5e"}, + {file = "pydantic_core-2.26.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6bc9eb88b2b48527b9b6b7a79a80a8e48b07e1334f659d09f5dd26ebb19cfd9c"}, + {file = "pydantic_core-2.26.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:d9651e747f64017aaa9ed8eab70a9ceada438b4395c8379614032dd178f96d57"}, + {file = "pydantic_core-2.26.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c005173da25bb76c54373374434d922ba3888f430879fdaf749d7abb4d8ea3"}, + {file = "pydantic_core-2.26.0-cp312-none-win32.whl", hash = "sha256:3f411e6d6d3fb93af5bb111313bb0cd68a6e38bae5afb79f2de530b3b44fad33"}, + {file = "pydantic_core-2.26.0-cp312-none-win_amd64.whl", hash = "sha256:f615cba236fdd4b3f366d42427d447856b1afa19f5e40e24aa1da56b6f042e30"}, + {file = "pydantic_core-2.26.0-cp312-none-win_arm64.whl", hash = "sha256:9e0330e20adae0571934ac745b240a0809eb2d85ee76e786bafe0d30a6ccd8cb"}, + {file = "pydantic_core-2.26.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:18305e996cac7053c82f10bd945ef67a8f6e80acc947f8929ddc0af87d5eb3cb"}, + {file = "pydantic_core-2.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:036fdd55f2f435dcb06dccdb9e1074fb8b9121560e5bed19f40a56c807b878a7"}, + {file = "pydantic_core-2.26.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:330d10f9112599863008d130a3ddba54e5bcafc5b9d24f62ace63e054b72f169"}, + {file = "pydantic_core-2.26.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f829f593a9ccb1fca8d9e98ef76bed89081c2c8acbdcf6ce230a27b8b5d7d9c0"}, + {file = "pydantic_core-2.26.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9965d2b196e64f1a008b1ef6d1cdb7d4c9da1ea757f49428d0ec497c766f50ad"}, + {file = "pydantic_core-2.26.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2279b4086e698aa6c21a1e4987530492704d7bbee696b251c00e26bd37d9037"}, + {file = "pydantic_core-2.26.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997b1bd0605a5e7b2509ed5642f9a3fe3078abfca5e4c940dbd572df815a7559"}, + {file = "pydantic_core-2.26.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0101d0b5a333a14d95a109f82ca95fe2ce34b4e48fd7dbc0fbe1a4e623aa94c7"}, + {file = "pydantic_core-2.26.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:32c0c99180cecd8fb1c91b9695873b0ff9bf381518977776757b253291cf0235"}, + {file = "pydantic_core-2.26.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:4e437d26da787b3ad8b96cac47d00d3786457d68b4e2f5d7ee03a9a2d4bc12ab"}, + {file = "pydantic_core-2.26.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:04d64cb57f9ebde2c88976ef7846a915e7a7e74944d7c1d674314bc1e38b404c"}, + {file = "pydantic_core-2.26.0-cp313-none-win32.whl", hash = "sha256:023bdce63154fa2285d236b350723c82bc76d051e60de6f761441e9680c8a940"}, + {file = "pydantic_core-2.26.0-cp313-none-win_amd64.whl", hash = "sha256:2bfeffdee3175c2593f0e4371c5b44da2f8044d5f5dd98be665fc8a6464008c8"}, + {file = "pydantic_core-2.26.0-cp313-none-win_arm64.whl", hash = "sha256:3a4c5666eed3d8e9c9b89dd56a96e3cbc261c239c92a87a15fc55affe5d379f9"}, + {file = "pydantic_core-2.26.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:3d8b599013a80e591eff37ec3e0151b2d86d7feeadd77f1b11061bd7987d08b9"}, + {file = "pydantic_core-2.26.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d6bc543db35ddb53995e0f2a0f18fb0f3ad55e73c38c414a23f4ae8592bd42c0"}, + {file = "pydantic_core-2.26.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4a8a96b6a228c597fa232ba3e4ddc482abb1cd69e340ab0418195e4c520cb1b"}, + {file = "pydantic_core-2.26.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b26991a3fb41ebb73acba7a7811787ba5ed8176c557708c68aef46d9fcbfed20"}, + {file = "pydantic_core-2.26.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3b608c1b08671c111ff41aa167994438de0633bb321fae1700bcbe4608b5a6c"}, + {file = "pydantic_core-2.26.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6b38f53dec01ea7e203b211b33f4019100e83465d4d935ab9120d820a0ea4d3"}, + {file = "pydantic_core-2.26.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd41f98f58819d6bf16b2e50c5f67d8ece09c99e41a950cf5ed76b5b4586814d"}, + {file = "pydantic_core-2.26.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fae508dd35f42adc03dba8bd0f6b18bfea23edc41ca5c6c75cc6262800929556"}, + {file = "pydantic_core-2.26.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2a3051216d2e1daf1ee22426452e05c8fdb5f476e9806cec67b1534391d2d549"}, + {file = "pydantic_core-2.26.0-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:e2255b4b90746666e4725c7b8e26747d5396ef1ef724fd88be8dde8e9a8f5029"}, + {file = "pydantic_core-2.26.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f994cfd732d56e2ed5b44e018d131c8d2273b00121b720d84a3d8b88f2a7d1c9"}, + {file = "pydantic_core-2.26.0-cp38-none-win32.whl", hash = "sha256:1be8973dcda3f6336acf1939e9e948f2611b27ddd9454f0ed58586710c248d75"}, + {file = "pydantic_core-2.26.0-cp38-none-win_amd64.whl", hash = "sha256:27b71639a044c816b87498d006d2a6887a8fc1f56ffabad34c54da97eca69aae"}, + {file = "pydantic_core-2.26.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:df2054459dd0373cf6d9b6e374cd952c8018fb135b9162e1c8e1ee60456c3c22"}, + {file = "pydantic_core-2.26.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8e9bd70d40d2b6d91186e886e8634e8f4d29122fb918f36c88789f0a8dc16f08"}, + {file = "pydantic_core-2.26.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1fc6d44b5fa4f5805ff8beb03250bd4c3d014658afca73926d1a0ea06cfec9b"}, + {file = "pydantic_core-2.26.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:27e0ccdad2db7159b766061e947b568ef7cd3c209f7722b36867a5a4184c0a1d"}, + {file = "pydantic_core-2.26.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:204cf1a3aac1339ac2926cce415f5b5e4a2cb1eb7bd6c92aef4cd3c0040b0faa"}, + {file = "pydantic_core-2.26.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a860c3fd2bd3c521c189b4af32a12c3f16e342c97b395f220a0822bc5c18374e"}, + {file = "pydantic_core-2.26.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6c39003ca3411e150dadd991ff0c12e15c01609ad3c387fc4b92b5773eebfa9"}, + {file = "pydantic_core-2.26.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cc607fad021f699f6891056a39d419cfb572dfde2d59c5062f60dc1508264e95"}, + {file = "pydantic_core-2.26.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f6b1b04c4c0f56fd7dd8d95475bcc4da816a79cfcebd5eb030794fe293c23203"}, + {file = "pydantic_core-2.26.0-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:af445f7740ed3549f9478e3665273c47532c17ffa8fd5f6e20742fd7ae7fe487"}, + {file = "pydantic_core-2.26.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2d816f3f80e5ccec2c5ffccd2fa57f36e9fb6b15d7c210ad793cdfded7feedfc"}, + {file = "pydantic_core-2.26.0-cp39-none-win32.whl", hash = "sha256:8f3622aee8d2411c894721110a196abc1779eb0b271da4700bbf75a3e7b0c535"}, + {file = "pydantic_core-2.26.0-cp39-none-win_amd64.whl", hash = "sha256:2a8af9ada5bb86016cbe18861aacdea64aa2572e6d6ec8a9ad687f97c4cf50a5"}, + {file = "pydantic_core-2.26.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:54c52a1ce830de3d93ec1d2af391a0d3f3255092c5ebf160be9e117796e3d472"}, + {file = "pydantic_core-2.26.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:727282cc2ac6ab40861e8e461b8c75ca4c60b95faae631d119e780993a14c5f7"}, + {file = "pydantic_core-2.26.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8bcf674bad21e3a4e83b4f4e214a3a689f0a3ff49f0d858738b68dddb5c301f6"}, + {file = "pydantic_core-2.26.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d85c60fe46d1501af0007137553ae5fc1b8c6aa72ebf3b4bce806a983a163f"}, + {file = "pydantic_core-2.26.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b26d88c9c8677699a9d134e4eac063662abe2b1053c9821adbd000a894f2d5ea"}, + {file = "pydantic_core-2.26.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:83fe1dcdf9faccfed14b60fd6ea0770814fb0510e7c5e9fb74d5713c2fa9f139"}, + {file = "pydantic_core-2.26.0-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:114059f663569af42d90d8771f33bb0526eb8f89dbd0ff0f448f7d549cb28c03"}, + {file = "pydantic_core-2.26.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:7daed5575b5163016bceeed0c6a31d508c4e4aca4eef6ecdb5266f07130ae988"}, + {file = "pydantic_core-2.26.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:56d163e491360cb7806f3b0c9325706b1794a217e5dea7bd6462730151e655c6"}, + {file = "pydantic_core-2.26.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ca8231f51600fa4f5116eeb7d491f0d535f2a3fe8d5fff7c40febf90d3ebccfb"}, + {file = "pydantic_core-2.26.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:fbc787251b9c6180202b8496ce5dbff51c1ec78e70f95e073a2f495455363def"}, + {file = "pydantic_core-2.26.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3380e05787a7fc9a41fe0e343fbe8d160bfb28bcd1bc96d7671576e0ee5dea30"}, + {file = "pydantic_core-2.26.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:785d0b12c29f2b6cb5f1f0ce1a239bd7ac13fd978239b14e0e23a40487ecc212"}, + {file = "pydantic_core-2.26.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56c6607026c311169a079bc6dcd8f6a7a2e29ae4ad13b145893eac6f1c6941c4"}, + {file = "pydantic_core-2.26.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:1ea69ae5c52fa6b1e1ccf9a278d5f8f1a9891f912693ed3c00b88b0ceea11d4f"}, + {file = "pydantic_core-2.26.0-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3c8e7e2f733a04cc96e7e0913b95906ffd6300cf84c9dac4433a243bf2d1aed5"}, + {file = "pydantic_core-2.26.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:8a0300cdbea0d4568b39fc7be3436cc702f9ee1655b8dd899ec874ff2b663a4b"}, + {file = "pydantic_core-2.26.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b42f4de434e6e7a57c127b519f907c4928be33fd1db8000f2b74992cb031decb"}, + {file = "pydantic_core-2.26.0.tar.gz", hash = "sha256:4578d4914bdbbd18963b4c611fa39d912d0dbeeffef8211fb546e45176a72d1d"}, ] [[package]] diff --git a/pydantic/functional_validators.py b/pydantic/functional_validators.py index eeb7c707418..be96df045a8 100644 --- a/pydantic/functional_validators.py +++ b/pydantic/functional_validators.py @@ -176,6 +176,7 @@ class PlainValidator: Example: ```py from typing import Union + from typing_extensions import Annotated from pydantic import BaseModel, PlainValidator diff --git a/pydantic/type_adapter.py b/pydantic/type_adapter.py index e6915f7a074..4f26af18590 100644 --- a/pydantic/type_adapter.py +++ b/pydantic/type_adapter.py @@ -335,6 +335,7 @@ def validate_python( strict: bool | None = None, from_attributes: bool | None = None, context: dict[str, Any] | None = None, + experimental_allow_partial: bool = False, ) -> T: """Validate a Python object against the model. @@ -343,6 +344,8 @@ def validate_python( strict: Whether to strictly check types. from_attributes: Whether to extract data from object attributes. context: Additional context to pass to the validator. + experimental_allow_partial: **Experimental** whether to enable + [partial validation](../concepts/experimental.md#partial-validation), e.g. to process streams. !!! note When using `TypeAdapter` with a Pydantic `dataclass`, the use of the `from_attributes` @@ -351,11 +354,23 @@ def validate_python( Returns: The validated object. """ - return self.validator.validate_python(object, strict=strict, from_attributes=from_attributes, context=context) + return self.validator.validate_python( + object, + strict=strict, + from_attributes=from_attributes, + context=context, + allow_partial=experimental_allow_partial, + ) @_frame_depth(1) def validate_json( - self, data: str | bytes, /, *, strict: bool | None = None, context: dict[str, Any] | None = None + self, + data: str | bytes, + /, + *, + strict: bool | None = None, + context: dict[str, Any] | None = None, + experimental_allow_partial: bool = False, ) -> T: """Usage docs: https://docs.pydantic.dev/2.10/concepts/json/#json-parsing @@ -365,25 +380,41 @@ def validate_json( data: The JSON data to validate against the model. strict: Whether to strictly check types. context: Additional context to use during validation. + experimental_allow_partial: **Experimental** whether to enable + [partial validation](../concepts/experimental.md#partial-validation), e.g. to process streams. Returns: The validated object. """ - return self.validator.validate_json(data, strict=strict, context=context) + return self.validator.validate_json( + data, strict=strict, context=context, allow_partial=experimental_allow_partial + ) @_frame_depth(1) - def validate_strings(self, obj: Any, /, *, strict: bool | None = None, context: dict[str, Any] | None = None) -> T: + def validate_strings( + self, + obj: Any, + /, + *, + strict: bool | None = None, + context: dict[str, Any] | None = None, + experimental_allow_partial: bool = False, + ) -> T: """Validate object contains string data against the model. Args: obj: The object contains string data to validate. strict: Whether to strictly check types. context: Additional context to use during validation. + experimental_allow_partial: **Experimental** whether to enable + [partial validation](../concepts/experimental.md#partial-validation), e.g. to process streams. Returns: The validated object. """ - return self.validator.validate_strings(obj, strict=strict, context=context) + return self.validator.validate_strings( + obj, strict=strict, context=context, allow_partial=experimental_allow_partial + ) @_frame_depth(1) def get_default_value(self, *, strict: bool | None = None, context: dict[str, Any] | None = None) -> Some[T] | None: diff --git a/pyproject.toml b/pyproject.toml index 18df82549bf..ae2cbe30426 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,8 +49,8 @@ classifiers = [ requires-python = '>=3.8' dependencies = [ "typing-extensions>=4.12.2", - 'annotated-types>=0.6.0', - "pydantic-core==2.25.1", + "annotated-types>=0.6.0", + "pydantic-core==2.26.0", ] dynamic = ['version', 'readme'] diff --git a/tests/test_allow_partial.py b/tests/test_allow_partial.py new file mode 100644 index 00000000000..2ee504eabbb --- /dev/null +++ b/tests/test_allow_partial.py @@ -0,0 +1,87 @@ +from typing import Dict, List, Tuple + +import pytest +from annotated_types import Ge +from typing_extensions import Annotated, TypedDict + +from pydantic import TypeAdapter, ValidationError + +from .conftest import Err + + +@pytest.mark.parametrize( + 'mode,value,expected', + [ + ('python', {'a': 1, 'b': 'b', 'c': (3, '4')}, {'a': 1, 'b': 'b', 'c': (3, '4')}), + ('python', {'a': 1, 'b': 'b', 'c': (3,)}, {'a': 1, 'b': 'b'}), + ('python', {'a': 1, 'b': 'b'}, {'a': 1, 'b': 'b'}), + ('json', '{"a": 1, "b": "b", "c": [3, "4"]}', {'a': 1, 'b': 'b', 'c': (3, '4')}), + ('json', '{"a": 1, "b": "b", "c": [3, "4"]}', {'a': 1, 'b': 'b', 'c': (3, '4')}), + ('json', '{"a": 1, "b": "b", "c": [3]}', {'a': 1, 'b': 'b'}), + ('json', '{"a": 1, "b": "b", "c": [3', {'a': 1, 'b': 'b'}), + ('json', '{"a": 1, "b": "b', {'a': 1, 'b': 'b'}), + ('json', '{"a": 1, "b": ', {'a': 1}), + ('python', {'a': 1, 'c': (3,), 'b': 'b'}, Err(r'c\.1\s+Field required')), + ('json', '{"a": 1, "c": [3], "b": "b"}', Err(r'c\.1\s+Field required')), + ], +) +def test_typed_dict(mode, value, expected): + class Foobar(TypedDict, total=False): + a: int + b: str + c: Tuple[int, str] + + ta = TypeAdapter(Foobar) + if mode == 'python': + if isinstance(expected, Err): + with pytest.raises(ValidationError, match=expected.message): + ta.validate_python(value, experimental_allow_partial=True) + else: + assert ta.validate_python(value, experimental_allow_partial=True) == expected + else: + if isinstance(expected, Err): + with pytest.raises(ValidationError, match=expected.message): + ta.validate_json(value, experimental_allow_partial=True) + else: + assert ta.validate_json(value, experimental_allow_partial=True) == expected + + +@pytest.mark.parametrize( + 'mode,value,expected', + [ + ('python', [10, 20, 30], [10, 20, 30]), + ('python', ['10', '20', '30'], [10, 20, 30]), + ('python', [10, 20, 30], [10, 20, 30]), + ('python', [10, 20, 3], [10, 20]), + ('json', '[10, 20, 30]', [10, 20, 30]), + ('json', '[10, 20, 30', [10, 20, 30]), + ('json', '[10, 20, 3', [10, 20]), + ], +) +def test_list(mode, value, expected): + ta = TypeAdapter(List[Annotated[int, Ge(10)]]) + if mode == 'python': + if isinstance(expected, Err): + with pytest.raises(ValidationError, match=expected.message): + ta.validate_python(value, experimental_allow_partial=True) + else: + assert ta.validate_python(value, experimental_allow_partial=True) == expected + else: + if isinstance(expected, Err): + with pytest.raises(ValidationError, match=expected.message): + ta.validate_json(value, experimental_allow_partial=True) + else: + assert ta.validate_json(value, experimental_allow_partial=True) == expected + + +def test_dict(): + ta = TypeAdapter(Dict[str, Annotated[int, Ge(10)]]) + eap = dict(experimental_allow_partial=True) + + assert ta.validate_python({'a': 10, 'b': 20, 'c': 30}, **eap) == {'a': 10, 'b': 20, 'c': 30} + assert ta.validate_python({'a': 10, 'b': 20, 'c': 3}, **eap) == {'a': 10, 'b': 20} + assert ta.validate_strings({'a': '10', 'b': '20', 'c': '30'}, strict=True, **eap) == {'a': 10, 'b': 20, 'c': 30} + assert ta.validate_strings({'a': '10', 'b': '20', 'c': '3'}, strict=True, **eap) == {'a': 10, 'b': 20} + assert ta.validate_json('{"a": 10, "b": 20, "c": 30}', **eap) == {'a': 10, 'b': 20, 'c': 30} + assert ta.validate_json('{"a": 10, "b": 20, "c": 3', **eap) == {'a': 10, 'b': 20} + assert ta.validate_json('{"a": 10, "b": 20, "c": 3}', **eap) == {'a': 10, 'b': 20} diff --git a/tests/test_datetime.py b/tests/test_datetime.py index 0bcabf99c28..2b95f7acaa2 100644 --- a/tests/test_datetime.py +++ b/tests/test_datetime.py @@ -87,7 +87,7 @@ class DateModel(BaseModel): (float('inf'), Err('Input should be a valid date or datetime, dates after 9999')), (int('1' + '0' * 100), Err('Input should be a valid date or datetime, dates after 9999')), (1e1000, Err('Input should be a valid date or datetime, dates after 9999')), - (float('-infinity'), Err('Input should be a valid date or datetime, dates before 1600')), + (float('-infinity'), Err('Input should be a valid date or datetime, dates before 0000')), (float('nan'), Err('Input should be a valid date or datetime, NaN values not permitted')), ], ) @@ -184,7 +184,7 @@ class DatetimeModel(BaseModel): (1_549_316_052_104_324, Err('Input should be a valid datetime, dates after 9999')), # nowish in μs (1_549_316_052_104_324_096, Err('Input should be a valid datetime, dates after 9999')), # nowish in ns (float('inf'), Err('Input should be a valid datetime, dates after 9999')), - (float('-inf'), Err('Input should be a valid datetime, dates before 1600')), + (float('-inf'), Err('Input should be a valid datetime, dates before 0000')), (1e50, Err('Input should be a valid datetime, dates after 9999')), (float('nan'), Err('Input should be a valid datetime, NaN values not permitted')), ],