diff --git a/CITATION.cff b/CITATION.cff index de1294d0666..6fbe3bdbff7 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -44,5 +44,5 @@ keywords: - hints - typing license: MIT -version: v2.10.3 -date-released: 2024-12-03 +version: v2.10.4 +date-released: 2024-12-18 diff --git a/HISTORY.md b/HISTORY.md index 3ca97b58c09..75cd1f1b2a8 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,26 @@ +## v2.10.4 (2024-12-18) + +[GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.10.4) + +### What's Changed + +#### Packaging + +* Bump `pydantic-core` to v2.27.2 by @davidhewitt in [#11138](https://github.com/pydantic/pydantic/pull/11138) + +#### Fixes + +* Fix for comparison of `AnyUrl` objects by @alexprabhat99 in [#11082](https://github.com/pydantic/pydantic/pull/11082) +* Properly fetch PEP 695 type params for functions, do not fetch annotations from signature by @Viicos in [#11093](https://github.com/pydantic/pydantic/pull/11093) +* Include JSON Schema input core schema in function schemas by @Viicos in [#11085](https://github.com/pydantic/pydantic/pull/11085) +* Add `len` to `_BaseUrl` to avoid TypeError by @Kharianne in [#11111](https://github.com/pydantic/pydantic/pull/11111) +* Make sure the type reference is removed from the seen references by @Viicos in [#11143](https://github.com/pydantic/pydantic/pull/11143) + +### New Contributors + +* @alexprabhat99 made their first contribution in [#11082](https://github.com/pydantic/pydantic/pull/11082) +* @Kharianne made their first contribution in [#11111](https://github.com/pydantic/pydantic/pull/11111) + ## v2.10.3 (2024-12-03) [GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.10.3) @@ -196,6 +219,8 @@ Pre-release, see [the GitHub release](https://github.com/pydantic/pydantic/relea Pre-release, see [the GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.9.0b1) for details. + + ## v2.9.2 (2024-09-17) [GitHub release](https://github.com/pydantic/pydantic/releases/tag/v2.9.2) @@ -1545,8 +1570,6 @@ First pre-release of Pydantic V2! See [this post](https://docs.pydantic.dev/blog/pydantic-v2-alpha/) for more details. - - ## v1.10.19 (2024-11-06) * Add warning when v2 model is nested in v1 model by @sydney-runkle in https://github.com/pydantic/pydantic/pull/10432 diff --git a/docs/api/pydantic_core.md b/docs/api/pydantic_core.md index ee92a1ad42b..a1c7766c29f 100644 --- a/docs/api/pydantic_core.md +++ b/docs/api/pydantic_core.md @@ -12,6 +12,7 @@ - PydanticCustomError - PydanticKnownError - PydanticOmit + - PydanticUseDefault - PydanticSerializationError - PydanticSerializationUnexpectedValue - Url diff --git a/docs/concepts/fields.md b/docs/concepts/fields.md index 8aedcadb118..9c81a75a382 100644 --- a/docs/concepts/fields.md +++ b/docs/concepts/fields.md @@ -1,7 +1,82 @@ ??? api "API Documentation" [`pydantic.fields.Field`][pydantic.fields.Field]
-The [`Field`][pydantic.fields.Field] function is used to customize and add metadata to fields of models. +In this section, we will go through the available mechanisms to customize Pydantic model fields: +default values, JSON Schema metadata, constraints, etc. + +To do so, the [`Field()`][pydantic.fields.Field] function is used a lot, and behaves the same way as +the standard library [`field()`][dataclasses.field] function for dataclasses: + +```python +from pydantic import BaseModel, Field + + +class Model(BaseModel): + name: str = Field(frozen=True) +``` + +!!! note + Even though `name` is assigned a value, it is still required and has no default value. If you want + to emphasize on the fact that a value must be provided, you can use the [ellipsis][Ellipsis]: + + ```python {lint="skip" test="skip"} + class Model(BaseModel): + name: str = Field(..., frozen=True) + ``` + + However, its usage is discouraged as it doesn't play well with static type checkers. + +## The annotated pattern + +To apply constraints or attach [`Field()`][pydantic.fields.Field] functions to a model field, Pydantic +supports the [`Annotated`][typing.Annotated] typing construct to attach metadata to an annotation: + +```python +from typing_extensions import Annotated + +from pydantic import BaseModel, Field, WithJsonSchema + + +class Model(BaseModel): + name: Annotated[str, Field(strict=True), WithJsonSchema({'extra': 'data'})] +``` + +As far as static type checkers are concerned, `name` is still typed as `str`, but Pydantic leverages +the available metadata to add validation logic, type constraints, etc. + +Using this pattern has some advantages: + +- Using the `f: = Field(...)` form can be confusing and might trick users into thinking `f` + has a default value, while in reality it is still required. +- You can provide an arbitrary amount of metadata elements for a field. As shown in the example above, + the [`Field()`][pydantic.fields.Field] function only supports a limited set of constraints/metadata, + and you may have to use different Pydantic utilities such as [`WithJsonSchema`][pydantic.WithJsonSchema] + in some cases. +- Types can be made reusable (see the documentation on [custom types](./types.md#using-the-annotated-pattern) + using this pattern). + +However, note that certain arguments to the [`Field()`][pydantic.fields.Field] function (namely, `default`, +`default_factory`, and `alias`) are taken into account by static type checkers to synthesize a correct +`__init__` method. The annotated pattern is *not* understood by them, so you should use the normal +assignment form instead. + +!!! tip + The annotated pattern can also be used to add metadata to specific parts of the type. For instance, + [validation constraints](#field-constraints) can be added this way: + + ```python + from typing import List + + from typing_extensions import Annotated + + from pydantic import BaseModel, Field + + + class Model(BaseModel): + int_list: List[Annotated[int, Field(gt=0)]] + # Valid: [1, 3] + # Invalid: [-1, 2] + ``` ## Default values @@ -54,30 +129,67 @@ print(user.username) The `data` argument will *only* contain the already validated data, based on the [order of model fields](./models.md#field-ordering) (the above example would fail if `username` were to be defined before `email`). +## Validate default values -## Using `Annotated` - -The [`Field`][pydantic.fields.Field] function can also be used together with [`Annotated`][annotated]. +By default, Pydantic will *not* validate default values. The `validate_default` field parameter +(or the [`validate_default`][pydantic.ConfigDict.validate_default] configuration value) can be used +to enable this behavior: ```python -from uuid import uuid4 +from pydantic import BaseModel, Field, ValidationError -from typing_extensions import Annotated -from pydantic import BaseModel, Field +class User(BaseModel): + age: int = Field(default='twelve', validate_default=True) -class User(BaseModel): - id: Annotated[str, Field(default_factory=lambda: uuid4().hex)] +try: + user = User() +except ValidationError as e: + print(e) + """ + 1 validation error for User + age + Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='twelve', input_type=str] + """ ``` -!!! note - Defaults can be set outside [`Annotated`][annotated] as the assigned value or with `Field.default_factory` inside - [`Annotated`][annotated]. The `Field.default` argument is not supported inside [`Annotated`][annotated]. +### Mutable default values + +A common source of bugs in Python is to use a mutable object as a default value for a function or method argument, +as the same instance ends up being reused in each call. +The [`dataclasses`][dataclasses] module actually raises an error in this case, indicating that you should use +a [default factory](https://docs.python.org/3/library/dataclasses.html#default-factory-functions) instead. + +While the same thing can be done in Pydantic, it is not required. In the event that the default value is not hashable, +Pydantic will create a deep copy of the default value when creating each instance of the model: + +```python +from typing import Dict, List + +from pydantic import BaseModel + + +class Model(BaseModel): + item_counts: List[Dict[str, int]] = [{}] + + +m1 = Model() +m1.item_counts[0]['a'] = 1 +print(m1.item_counts) +#> [{'a': 1}] + +m2 = Model() +print(m2.item_counts) +#> [{}] +``` ## Field aliases +!!! tip + Read more about aliases in the [dedicated section](./alias.md). + For validation and serialization, you can define an alias for a field. There are three ways to define an alias: @@ -256,11 +368,7 @@ print(user.model_dump(by_alias=True)) # (2)! #> {'my_serialization_alias': 1} ``` - All of the above will likely also apply to other tools that respect the - [`@typing.dataclass_transform`](https://docs.python.org/3/library/typing.html#typing.dataclass_transform) - decorator, such as Pyright. - -For more information on alias usage, see the [Alias] concepts page. +[](){#field-constraints} ## Numeric Constraints @@ -503,31 +611,6 @@ print(model.model_dump()) # (1)! 1. The `baz` field is not included in the `model_dump()` output, since it is an init-only field. -## Validate Default Values - -The parameter `validate_default` can be used to control whether the default value of the field should be validated. - -By default, the default value of the field is not validated. - -```python -from pydantic import BaseModel, Field, ValidationError - - -class User(BaseModel): - age: int = Field(default='twelve', validate_default=True) - - -try: - user = User() -except ValidationError as e: - print(e) - """ - 1 validation error for User - age - Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='twelve', input_type=str] - """ -``` - ## Field Representation The parameter `repr` can be used to control whether the field should be included in the string diff --git a/docs/concepts/json_schema.md b/docs/concepts/json_schema.md index 9703e29f8a2..c221cfbbaa9 100644 --- a/docs/concepts/json_schema.md +++ b/docs/concepts/json_schema.md @@ -660,32 +660,24 @@ print(json.dumps(ta.json_schema(), indent=2)) [`pydantic.json_schema.WithJsonSchema`][pydantic.json_schema.WithJsonSchema]
!!! tip - Using [`WithJsonSchema`][pydantic.json_schema.WithJsonSchema]] is preferred over + Using [`WithJsonSchema`][pydantic.json_schema.WithJsonSchema] is preferred over [implementing `__get_pydantic_json_schema__`](#implementing_get_pydantic_json_schema) for custom types, as it's more simple and less error-prone. The [`WithJsonSchema`][pydantic.json_schema.WithJsonSchema] annotation can be used to override the generated (base) JSON schema for a given type without the need to implement `__get_pydantic_core_schema__` -or `__get_pydantic_json_schema__` on the type itself. - -This provides a way to set a JSON schema for types that would otherwise raise errors when producing a JSON schema, -such as `Callable`, or types that have an [`is-instance`][pydantic_core.core_schema.is_instance_schema] core schema. - -For example, the use of a [`PlainValidator`][pydantic.functional_validators.PlainValidator] in the following example -would otherwise raise an error when producing a JSON schema because the [`PlainValidator`][pydantic.functional_validators.PlainValidator] -is a `Callable`. However, by using the [`WithJsonSchema`][pydantic.json_schema.WithJsonSchema] -annotation, we can override the generated JSON schema for the custom `MyInt` type: +or `__get_pydantic_json_schema__` on the type itself. Note that this overrides the whole JSON Schema generation process +for the field (in the following example, the `'type'` also needs to be provided). ```python {output="json"} import json from typing_extensions import Annotated -from pydantic import BaseModel, PlainValidator, WithJsonSchema +from pydantic import BaseModel, WithJsonSchema MyInt = Annotated[ int, - PlainValidator(lambda v: int(v) + 1), WithJsonSchema({'type': 'integer', 'examples': [1, 0, -1]}), ] @@ -694,9 +686,6 @@ class Model(BaseModel): a: MyInt -print(Model(a='1').a) -#> 2 - print(json.dumps(Model.model_json_schema(), indent=2)) """ { @@ -721,9 +710,9 @@ print(json.dumps(Model.model_json_schema(), indent=2)) ``` !!! note - As discussed in [this issue](https://github.com/pydantic/pydantic/issues/8208), in the future, it's likely that Pydantic will add - builtin support for JSON schema generation for types like [`PlainValidator`][pydantic.functional_validators.PlainValidator], - but the [`WithJsonSchema`][pydantic.json_schema.WithJsonSchema] annotation will still be useful for other custom types. + You might be tempted to use the [`WithJsonSchema`][pydantic.json_schema.WithJsonSchema] annotation + to fine-tune the JSON Schema of fields having [validators](./validators.md) attached. Instead, it + is recommended to use [the `json_schema_input_type` argument](./validators.md#json-schema-and-field-validators). ### `SkipJsonSchema` annotation @@ -743,7 +732,7 @@ This method receives two positional arguments: 1. The type annotation that corresponds to this type (so in the case of `TheType[T][int]` it would be `TheType[int]`). 2. A handler/callback to call the next implementer of `__get_pydantic_core_schema__`. -The handler system works just like [`mode='wrap'` validators](validators.md#annotated-validators). +The handler system works just like [*wrap* field validators](validators.md#field-wrap-validator). In this case the input is the type and the output is a `core_schema`. Here is an example of a custom type that *overrides* the generated `core_schema`: diff --git a/docs/concepts/validators.md b/docs/concepts/validators.md index 31bce4c3555..350563bea16 100644 --- a/docs/concepts/validators.md +++ b/docs/concepts/validators.md @@ -1,635 +1,645 @@ -## Annotated Validators +In addition to Pydantic's [built-in validation capabilities](./fields.md#field-constraints), +you can leverage custom validators at the field and model levels to enforce more complex constraints +and ensure the integrity of your data. + +!!! tip + Want to quickly jump to the relevant validator section? + +
+ + - Field validators + + --- + + - [field *after* validators](#field-after-validator) + - [field *before* validators](#field-before-validator) + - [field *plain* validators](#field-plain-validator) + - [field *wrap* validators](#field-wrap-validator) + + - Model validators + + --- + + - [model *before* validators](#model-before-validator) + - [model *after* validators](#model-after-validator) + - [model *wrap* validators](#model-wrap-validator) + +
+ +## Field validators ??? api "API Documentation" [`pydantic.functional_validators.WrapValidator`][pydantic.functional_validators.WrapValidator]
[`pydantic.functional_validators.PlainValidator`][pydantic.functional_validators.PlainValidator]
[`pydantic.functional_validators.BeforeValidator`][pydantic.functional_validators.BeforeValidator]
[`pydantic.functional_validators.AfterValidator`][pydantic.functional_validators.AfterValidator]
+ [`pydantic.functional_validators.field_validator`][pydantic.functional_validators.field_validator]
-Pydantic provides a way to apply validators via use of `Annotated`. -You should use this whenever you want to bind validation to a type instead of model or field. - -```python -from typing import Any, List +In its simplest form, a field validator is a callable taking the value to be validated as an argument and +**returning the validated value**. The callable can perform checks for specific conditions (see +[raising validation errors](#raising-validation-errors)) and make changes to the validated value (coercion or mutation). -from typing_extensions import Annotated +**Four** different types of validators can be used. They can all be defined using the +[annotated pattern](./fields.md#the-annotated-pattern) or using the +[`field_validator()`][pydantic.field_validator] decorator, applied on a [class method][classmethod]: -from pydantic import BaseModel, ValidationError -from pydantic.functional_validators import AfterValidator +[](){#field-after-validator} +- __*After* validators__: run after Pydantic's internal validation. They are generally more type safe and thus easier to implement. -def check_squares(v: int) -> int: - assert v**0.5 % 1 == 0, f'{v} is not a square number' - return v + === "Annotated pattern" + Here is an example of a validator performing a validation check, and returning the value unchanged. -def double(v: Any) -> Any: - return v * 2 + ```python + from typing_extensions import Annotated + from pydantic import AfterValidator, BaseModel, ValidationError -MyNumber = Annotated[int, AfterValidator(double), AfterValidator(check_squares)] + def is_even(value: int) -> int: + if value % 2 == 1: + raise ValueError(f'{value} is not an even number') + return value # (1)! -class DemoModel(BaseModel): - number: List[MyNumber] + class Model(BaseModel): + number: Annotated[int, AfterValidator(is_even)] -print(DemoModel(number=[2, 8])) -#> number=[4, 16] -try: - DemoModel(number=[2, 4]) -except ValidationError as e: - print(e) - """ - 1 validation error for DemoModel - number.1 - Assertion failed, 8 is not a square number - assert ((8 ** 0.5) % 1) == 0 [type=assertion_error, input_value=4, input_type=int] - """ -``` -In this example we used some type aliases (`MyNumber = Annotated[...]`). -While this can help with legibility of the code, it is not required, you can use `Annotated` directly in a model field type hint. -These type aliases are also not actual types but you can use a similar approach with `TypeAliasType` to create actual types. -See [Custom Types](../concepts/types.md#custom-types) for a more detailed explanation of custom types. + try: + Model(number=1) + except ValidationError as err: + print(err) + """ + 1 validation error for Model + number + Value error, 1 is not an even number [type=value_error, input_value=1, input_type=int] + """ + ``` -It is also worth noting that you can nest `Annotated` inside other types. -In this example we used that to apply validation to the inner items of a list. -The same approach can be used for dict keys, etc. + 1. Note that it is important to return the validated value. -### Before, After, Wrap and Plain validators + === "Decorator" -Pydantic provides multiple types of validator functions: + Here is an example of a validator performing a validation check, and returning the value unchanged, + this time using the [`field_validator()`][pydantic.field_validator] decorator. -* `After` validators run after Pydantic's internal parsing. They are generally more type safe and thus easier to implement. -* `Before` validators run before Pydantic's internal parsing and validation (e.g. coercion of a `str` to an `int`). These are more flexible than `After` validators since they can modify the raw input, but they also have to deal with the raw input, which in theory could be any arbitrary object. -* `Plain` validators are like a `mode='before'` validator but they terminate validation immediately, no further validators are called and Pydantic does not do any of its internal validation. -* `Wrap` validators are the most flexible of all. You can run code before or after Pydantic and other validators do their thing or you can terminate validation immediately, both with a successful value or an error. + ```python + from pydantic import BaseModel, ValidationError, field_validator -You can use multiple before, after, or `mode='wrap'` validators, but only one `PlainValidator` since a plain validator -will not call any inner validators. -Here's an example of a `mode='wrap'` validator: + class Model(BaseModel): + number: int -```python -import json -from typing import Any, List + @field_validator('number', mode='after') # (1)! + @classmethod + def is_even(cls, value: int) -> int: + if value % 2 == 1: + raise ValueError(f'{value} is not an even number') + return value # (2)! -from typing_extensions import Annotated -from pydantic import ( - BaseModel, - ValidationError, - ValidationInfo, - ValidatorFunctionWrapHandler, -) -from pydantic.functional_validators import WrapValidator - - -def maybe_strip_whitespace( - v: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo -) -> int: - if info.mode == 'json': - assert isinstance(v, str), 'In JSON mode the input must be a string!' - # you can call the handler multiple times try: - return handler(v) - except ValidationError: - return handler(v.strip()) - assert info.mode == 'python' - assert isinstance(v, int), 'In Python mode the input must be an int!' - # do no further validation - return v - - -MyNumber = Annotated[int, WrapValidator(maybe_strip_whitespace)] - - -class DemoModel(BaseModel): - number: List[MyNumber] - - -print(DemoModel(number=[2, 8])) -#> number=[2, 8] -print(DemoModel.model_validate_json(json.dumps({'number': [' 2 ', '8']}))) -#> number=[2, 8] -try: - DemoModel(number=['2']) -except ValidationError as e: - print(e) - """ - 1 validation error for DemoModel - number.0 - Assertion failed, In Python mode the input must be an int! - assert False - + where False = isinstance('2', int) [type=assertion_error, input_value='2', input_type=str] - """ -``` + Model(number=1) + except ValidationError as err: + print(err) + """ + 1 validation error for Model + number + Value error, 1 is not an even number [type=value_error, input_value=1, input_type=int] + """ + ``` -The same "modes" apply to `@field_validator`, which is discussed in the next section. + 1. `'after'` is the default mode for the decorator, and can be omitted. + 2. Note that it is important to return the validated value. -### Ordering of validators within `Annotated` + ??? example "Example mutating the value" + Here is an example of a validator making changes to the validated value (no exception is raised). -Order of validation metadata within `Annotated` matters. -Validation goes from right to left and back. -That is, it goes from right to left running all "before" validators (or calling into "wrap" validators), then left to right back out calling all "after" validators. + === "Annotated pattern" -```python -from typing import Any, Callable, List, cast + ```python + from typing_extensions import Annotated -from typing_extensions import Annotated, TypedDict + from pydantic import AfterValidator, BaseModel -from pydantic import ( - AfterValidator, - BaseModel, - BeforeValidator, - PlainValidator, - ValidationInfo, - ValidatorFunctionWrapHandler, - WrapValidator, -) -from pydantic.functional_validators import field_validator + def double_number(value: int) -> int: + return value * 2 -class Context(TypedDict): - logs: List[str] + class Model(BaseModel): + number: Annotated[int, AfterValidator(double_number)] -def make_validator(label: str) -> Callable[[Any, ValidationInfo], Any]: - def validator(v: Any, info: ValidationInfo) -> Any: - context = cast(Context, info.context) - context['logs'].append(label) - return v - return validator + print(Model(number=2)) + #> number=4 + ``` + === "Decorator" -def make_wrap_validator( - label: str, -) -> Callable[[Any, ValidatorFunctionWrapHandler, ValidationInfo], Any]: - def validator( - v: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo - ) -> Any: - context = cast(Context, info.context) - context['logs'].append(f'{label}: pre') - result = handler(v) - context['logs'].append(f'{label}: post') - return result + ```python + from pydantic import BaseModel, field_validator - return validator + class Model(BaseModel): + number: int -class A(BaseModel): - x: Annotated[ - str, - BeforeValidator(make_validator('before-1')), - AfterValidator(make_validator('after-1')), - WrapValidator(make_wrap_validator('wrap-1')), - BeforeValidator(make_validator('before-2')), - AfterValidator(make_validator('after-2')), - WrapValidator(make_wrap_validator('wrap-2')), - BeforeValidator(make_validator('before-3')), - AfterValidator(make_validator('after-3')), - WrapValidator(make_wrap_validator('wrap-3')), - BeforeValidator(make_validator('before-4')), - AfterValidator(make_validator('after-4')), - WrapValidator(make_wrap_validator('wrap-4')), - ] - y: Annotated[ - str, - BeforeValidator(make_validator('before-1')), - AfterValidator(make_validator('after-1')), - WrapValidator(make_wrap_validator('wrap-1')), - BeforeValidator(make_validator('before-2')), - AfterValidator(make_validator('after-2')), - WrapValidator(make_wrap_validator('wrap-2')), - PlainValidator(make_validator('plain')), - BeforeValidator(make_validator('before-3')), - AfterValidator(make_validator('after-3')), - WrapValidator(make_wrap_validator('wrap-3')), - BeforeValidator(make_validator('before-4')), - AfterValidator(make_validator('after-4')), - WrapValidator(make_wrap_validator('wrap-4')), - ] + @field_validator('number', mode='after') # (1)! + @classmethod + def double_number(cls, value: int) -> int: + return value * 2 - val_x_before = field_validator('x', mode='before')( - make_validator('val_x before') - ) - val_x_after = field_validator('x', mode='after')( - make_validator('val_x after') - ) - val_y_wrap = field_validator('y', mode='wrap')( - make_wrap_validator('val_y wrap') - ) + print(Model(number=2)) + #> number=4 + ``` -context = Context(logs=[]) - -A.model_validate({'x': 'abc', 'y': 'def'}, context=context) -print(context['logs']) -""" -[ - 'val_x before', - 'wrap-4: pre', - 'before-4', - 'wrap-3: pre', - 'before-3', - 'wrap-2: pre', - 'before-2', - 'wrap-1: pre', - 'before-1', - 'after-1', - 'wrap-1: post', - 'after-2', - 'wrap-2: post', - 'after-3', - 'wrap-3: post', - 'after-4', - 'wrap-4: post', - 'val_x after', - 'val_y wrap: pre', - 'wrap-4: pre', - 'before-4', - 'wrap-3: pre', - 'before-3', - 'plain', - 'after-3', - 'wrap-3: post', - 'after-4', - 'wrap-4: post', - 'val_y wrap: post', -] -""" -``` + 1. `'after'` is the default mode for the decorator, and can be omitted. -## Validation of default values +[](){#field-before-validator} -Validators won't run when the default value is used. -This applies both to `@field_validator` validators and `Annotated` validators. -You can force them to run with `Field(validate_default=True)`. Setting `validate_default` to `True` has the closest behavior to using `always=True` in `validator` in Pydantic v1. However, you are generally better off using a `@model_validator(mode='before')` where the function is called before the inner validator is called. +- __*Before* validators__: run before Pydantic's internal parsing and validation (e.g. coercion of a `str` to an `int`). + These are more flexible than [*after* validators](#field-after-validator), but they also have to deal with the raw input, which + in theory could be any arbitrary object. The value returned from this callable is then validated against the provided type annotation + by Pydantic. + === "Annotated pattern" -```python -from typing_extensions import Annotated + ```python + from typing import Any, List -from pydantic import BaseModel, Field, field_validator + from typing_extensions import Annotated + from pydantic import BaseModel, BeforeValidator, ValidationError -class Model(BaseModel): - x: str = 'abc' - y: Annotated[str, Field(validate_default=True)] = 'xyz' - @field_validator('x', 'y') - @classmethod - def double(cls, v: str) -> str: - return v * 2 - - -print(Model()) -#> x='abc' y='xyzxyz' -print(Model(x='foo')) -#> x='foofoo' y='xyzxyz' -print(Model(x='abc')) -#> x='abcabc' y='xyzxyz' -print(Model(x='foo', y='bar')) -#> x='foofoo' y='barbar' -``` + def ensure_list(value: Any) -> Any: # (1)! + if not isinstance(value, list): # (2)! + return [value] + else: + return value -## Field validators -??? api "API Documentation" - [`pydantic.functional_validators.field_validator`][pydantic.functional_validators.field_validator]
+ class Model(BaseModel): + numbers: Annotated[List[int], BeforeValidator(ensure_list)] -If you want to attach a validator to a specific field of a model you can use the `@field_validator` decorator. -```python -from pydantic import ( - BaseModel, - ValidationError, - ValidationInfo, - field_validator, -) + print(Model(numbers=2)) + #> numbers=[2] + try: + Model(numbers='str') + except ValidationError as err: + print(err) # (3)! + """ + 1 validation error for Model + numbers.0 + Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='str', input_type=str] + """ + ``` + 1. Notice the use of [`Any`][typing.Any] as a type hint for `value`. *Before* validators take the raw input, which + can be anything. -class UserModel(BaseModel): - name: str - id: int + 2. Note that you might want to check for other sequence types (such as tuples) that would normally successfully + validate against the `list` type. *Before* validators give you more flexibility, but you have to account for + every possible case. - @field_validator('name') - @classmethod - def name_must_contain_space(cls, v: str) -> str: - if ' ' not in v: - raise ValueError('must contain a space') - return v.title() + 3. Pydantic still performs validation against the `int` type, no matter if our `ensure_list` validator + did operations on the original input type. - # you can select multiple fields, or use '*' to select all fields - @field_validator('id', 'name') - @classmethod - def check_alphanumeric(cls, v: str, info: ValidationInfo) -> str: - if isinstance(v, str): - # info.field_name is the name of the field being validated - is_alphanumeric = v.replace(' ', '').isalnum() - assert is_alphanumeric, f'{info.field_name} must be alphanumeric' - return v + === "Decorator" + ```python + from typing import Any, List -print(UserModel(name='John Doe', id=1)) -#> name='John Doe' id=1 - -try: - UserModel(name='samuel', id=1) -except ValidationError as e: - print(e) - """ - 1 validation error for UserModel - name - Value error, must contain a space [type=value_error, input_value='samuel', input_type=str] - """ - -try: - UserModel(name='John Doe', id='abc') -except ValidationError as e: - print(e) - """ - 1 validation error for UserModel - id - Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='abc', input_type=str] - """ - -try: - UserModel(name='John Doe!', id=1) -except ValidationError as e: - print(e) - """ - 1 validation error for UserModel - name - Assertion failed, name must be alphanumeric - assert False [type=assertion_error, input_value='John Doe!', input_type=str] - """ -``` + from pydantic import BaseModel, ValidationError, field_validator -A few things to note on validators: -* `@field_validator`s are "class methods", so the first argument value they receive is the `UserModel` class, not an instance of `UserModel`. We recommend you use the `@classmethod` decorator on them below the `@field_validator` decorator to get proper type checking. -* the second argument is the field value to validate; it can be named as you please -* the third argument, if present, is an instance of `pydantic.ValidationInfo` -* validators should either return the parsed value or raise a `ValueError` or `AssertionError` (``assert`` statements may be used). -* A single validator can be applied to multiple fields by passing it multiple field names. -* A single validator can also be called on *all* fields by passing the special value `'*'`. + class Model(BaseModel): + numbers: List[int] -!!! warning - If you make use of `assert` statements, keep in mind that running - Python with the [`-O` optimization flag](https://docs.python.org/3/using/cmdline.html#cmdoption-o) - disables `assert` statements, and **validators will stop working**. + @field_validator('numbers', mode='before') + @classmethod + def ensure_list(cls, value: Any) -> Any: # (1)! + if not isinstance(value, list): # (2)! + return [value] + else: + return value -!!! note - `FieldValidationInfo` is **deprecated** in 2.4, use `ValidationInfo` instead. + print(Model(numbers=2)) + #> numbers=[2] + try: + Model(numbers='str') + except ValidationError as err: + print(err) # (3)! + """ + 1 validation error for Model + numbers.0 + Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='str', input_type=str] + """ + ``` -If you want to access values from another field inside a `@field_validator`, this may be possible using `ValidationInfo.data`, which is a dict of field name to field value. -Validation is done in the order fields are defined, so you have to be careful when using `ValidationInfo.data` to not access a field that has not yet been validated/populated — in the code above, for example, you would not be able to access `info.data['id']` from within `name_must_contain_space`. -However, in most cases where you want to perform validation using multiple field values, it is better to use `@model_validator` which is discussed in the section below. + 1. Notice the use of [`Any`][typing.Any] as a type hint for `value`. *Before* validators take the raw input, which + can be anything. -## Model validators + 2. Note that you might want to check for other sequence types (such as tuples) that would normally successfully + validate against the `list` type. *Before* validators give you more flexibility, but you have to account for + every possible case. -??? api "API Documentation" - [`pydantic.functional_validators.model_validator`][pydantic.functional_validators.model_validator]
+ 3. Pydantic still performs validation against the `int` type, no matter if our `ensure_list` validator + did operations on the original input type. -Validation can also be performed on the entire model's data using `@model_validator`. +[](){#field-plain-validator} -```python -from typing import Any +- __*Plain* validators__: act similarly to *before* validators but they **terminate validation immediately** after returning, + so no further validators are called and Pydantic does not do any of its internal validation against the field type. -from typing_extensions import Self + === "Annotated pattern" -from pydantic import BaseModel, ValidationError, model_validator + ```python + from typing import Any + from typing_extensions import Annotated -class UserModel(BaseModel): - username: str - password1: str - password2: str + from pydantic import BaseModel, PlainValidator - @model_validator(mode='before') - @classmethod - def check_card_number_omitted(cls, data: Any) -> Any: - if isinstance(data, dict): - assert ( - 'card_number' not in data - ), 'card_number should not be included' - return data - - @model_validator(mode='after') - def check_passwords_match(self) -> Self: - pw1 = self.password1 - pw2 = self.password2 - if pw1 is not None and pw2 is not None and pw1 != pw2: - raise ValueError('passwords do not match') - return self - - -print(UserModel(username='scolvin', password1='zxcvbn', password2='zxcvbn')) -#> username='scolvin' password1='zxcvbn' password2='zxcvbn' -try: - UserModel(username='scolvin', password1='zxcvbn', password2='zxcvbn2') -except ValidationError as e: - print(e) - """ - 1 validation error for UserModel - Value error, passwords do not match [type=value_error, input_value={'username': 'scolvin', '... 'password2': 'zxcvbn2'}, input_type=dict] - """ - -try: - UserModel( - username='scolvin', - password1='zxcvbn', - password2='zxcvbn', - card_number='1234', - ) -except ValidationError as e: - print(e) - """ - 1 validation error for UserModel - Assertion failed, card_number should not be included - assert 'card_number' not in {'card_number': '1234', 'password1': 'zxcvbn', 'password2': 'zxcvbn', 'username': 'scolvin'} [type=assertion_error, input_value={'username': 'scolvin', '..., 'card_number': '1234'}, input_type=dict] - """ -``` -!!! note "On return type checking" - Methods decorated with `@model_validator` should return the self instance at the end of the method. - For type checking purposes, you can use `Self` from either `typing` or the `typing_extensions` backport as the - return type of the decorated method. - In the context of the above example, you could also use `def check_passwords_match(self: 'UserModel') -> 'UserModel'` to indicate that the method returns an instance of the model. + def val_number(value: Any) -> Any: + if isinstance(value, int): + return value * 2 + else: + return value -!!! warning "On not returning `self`" - If you fail to return `self` at the end of a `@model_validator` method (either, returning `None` or returning something other than `self`), - you may encounter unexpected behavior. - Specifically, for nested models, if you return `None` (or equivalently, don't include a `return` statement), - despite a potentially successful validation, the nested model will be `None` in the parent model. + class Model(BaseModel): + number: Annotated[int, PlainValidator(val_number)] - Returning a value other than `self` causes unexpected behavior at the top level of validation when validating via `__init__`. - In order to avoid this, we recommend one of: - 1. Simply mutate and return `self` at the end of the method. - 2. If you must return a value other than `self`, use a method like `model_validate` where you can directly fetch the return value. - Here's an example of the unexpected behavior, and the warning you'll receive: + print(Model(number=4)) + #> number=8 + print(Model(number='invalid')) # (1)! + #> number='invalid' + ``` - ```python {test="skip"} - from pydantic import BaseModel - from pydantic.functional_validators import model_validator + 1. Although `'invalid'` shouldn't validate against the `int` type, Pydantic accepts the input. + === "Decorator" - class Child(BaseModel): - name: str + ```python + from typing import Any - @model_validator(mode='after') # type: ignore - def validate_model(self) -> 'Child': - return Child.model_construct(name='different!') + from pydantic import BaseModel, field_validator - print(repr(Child(name='foo'))) - """ - UserWarning: A custom validator is returning a value other than `self`. - Returning anything other than `self` from a top level model validator isn't supported when validating via `__init__`. - See the `model_validator` docs (https://docs.pydantic.dev/latest/concepts/validators/#model-validators) for more details. + class Model(BaseModel): + number: int - Child(name='foo') - """ - ``` + @field_validator('number', mode='plain') + @classmethod + def val_number(cls, value: Any) -> Any: + if isinstance(value, int): + return value * 2 + else: + return value -!!! note "On Inheritance" - A `@model_validator` defined in a base class will be called during the validation of a subclass instance. - Overriding a `@model_validator` in a subclass will override the base class' `@model_validator`, and thus only the subclass' version of said `@model_validator` will be called. + print(Model(number=4)) + #> number=8 + print(Model(number='invalid')) # (1)! + #> number='invalid' + ``` -Model validators can be `mode='before'`, `mode='after'` or `mode='wrap'`. + 1. Although `'invalid'` shouldn't validate against the `int` type, Pydantic accepts the input. -Before model validators are passed the raw input which is often a `dict[str, Any]` but could also be an instance of the model itself (e.g. if `UserModel.model_validate(UserModel.construct(...))` is called) or anything else since you can pass arbitrary objects into `model_validate`. -Because of this `mode='before'` validators are extremely flexible and powerful but can be cumbersome and error prone to implement. -Before model validators should be class methods. -The first argument should be `cls` (and we also recommend you use `@classmethod` below `@model_validator` for proper type checking), the second argument will be the input (you should generally type it as `Any` and use `isinstance` to narrow the type) and the third argument (if present) will be a `pydantic.ValidationInfo`. +[](){#field-wrap-validator} -`mode='after'` validators are instance methods and always receive an instance of the model as the first argument. Be sure to return the instance at the end of your validator. -You should not use `(cls, ModelType)` as the signature, instead just use `(self)` and let type checkers infer the type of `self` for you. -Since these are fully type safe they are often easier to implement than `mode='before'` validators. -If any field fails to validate, `mode='after'` validators for that field will not be called. +- __*Wrap* validators__: are the most flexible of all. You can run code before or after Pydantic and other validators + process the input, or you can terminate validation immediately, either by returning the value early or by raising an + error. -## Handling errors in validators + Such validators must be defined with a **mandatory** extra `handler` parameter: a callable taking the value to be validated + as an argument. Internally, this handler will delegate validation of the value to Pydantic. You are free to wrap the call + to the handler in a [`try..except`][handling exceptions] block, or not call it at all. -As mentioned in the previous sections you can raise either a `ValueError` or `AssertionError` (including ones generated by `assert ...` statements) within a validator to indicate validation failed. -You can also raise a `PydanticCustomError` which is a bit more verbose but gives you extra flexibility. -Any other errors (including `TypeError`) are bubbled up and not wrapped in a `ValidationError`. + [handling exceptions]: https://docs.python.org/3/tutorial/errors.html#handling-exceptions -```python -from pydantic_core import PydanticCustomError + === "Annotated pattern" -from pydantic import BaseModel, ValidationError, field_validator + ```python {lint="skip"} + from typing import Any + from typing_extensions import Annotated -class Model(BaseModel): - x: int + from pydantic import BaseModel, Field, ValidationError, ValidatorFunctionWrapHandler, WrapValidator - @field_validator('x') - @classmethod - def validate_x(cls, v: int) -> int: - if v % 42 == 0: - raise PydanticCustomError( - 'the_answer_error', - '{number} is the answer!', - {'number': v}, - ) - return v + def truncate(value: Any, handler: ValidatorFunctionWrapHandler) -> str: + try: + return handler(value) + except ValidationError as err: + if err.errors()[0]['type'] == 'string_too_long': + return handler(value[:5]) + else: + raise -try: - Model(x=42 * 2) -except ValidationError as e: - print(e) - """ - 1 validation error for Model - x - 84 is the answer! [type=the_answer_error, input_value=84, input_type=int] - """ -``` -## Special Types + class Model(BaseModel): + my_string: Annotated[str, Field(max_length=5), WrapValidator(truncate)] + + + print(Model(my_string='abcde')) + #> my_string='abcde' + print(Model(my_string='abcdef')) + #> my_string='abcde' + ``` + + === "Decorator" + + ```python {lint="skip"} + from typing import Any + + from typing_extensions import Annotated + + from pydantic import BaseModel, Field, ValidationError, ValidatorFunctionWrapHandler, field_validator + + + class Model(BaseModel): + my_string: Annotated[str, Field(max_length=5)] + + @field_validator('my_string', mode='wrap') + @classmethod + def truncate(cls, value: Any, handler: ValidatorFunctionWrapHandler) -> str: + try: + return handler(value) + except ValidationError as err: + if err.errors()[0]['type'] == 'string_too_long': + return handler(value[:5]) + else: + raise + + + print(Model(my_string='abcde')) + #> my_string='abcde' + print(Model(my_string='abcdef')) + #> my_string='abcde' + ``` + +!!! note "Validation of default values" + As mentioned in the [fields documentation](./fields.md#validate-default-values), default values of fields + are *not* validated unless configured to do so, and thus custom validators will not be applied as well. -Pydantic provides a few special types that can be used to customize validation. +### Which validator pattern to use -- [`InstanceOf`][pydantic.functional_validators.InstanceOf] is a type that can be used to validate that a value is an instance of a given class. +While both approaches can achieve the same thing, each pattern provides different benefits. + +#### Using the annotated pattern + +One of the key benefits of using the [annotated pattern](./fields.md#the-annotated-pattern) is to make +validators reusable: ```python from typing import List -from pydantic import BaseModel, InstanceOf, ValidationError +from typing_extensions import Annotated + +from pydantic import AfterValidator, BaseModel -class Fruit: - def __repr__(self): - return self.__class__.__name__ +def is_even(value: int) -> int: + if value % 2 == 1: + raise ValueError(f'{value} is not an even number') + return value -class Banana(Fruit): ... +EvenNumber = Annotated[str, AfterValidator(is_even)] -class Apple(Fruit): ... +class Model1(BaseModel): + my_number: EvenNumber -class Basket(BaseModel): - fruits: List[InstanceOf[Fruit]] +class Model2(BaseModel): + other_number: Annotated[EvenNumber, AfterValidator(lambda v: v + 2)] -print(Basket(fruits=[Banana(), Apple()])) -#> fruits=[Banana, Apple] -try: - Basket(fruits=[Banana(), 'Apple']) -except ValidationError as e: - print(e) - """ - 1 validation error for Basket - fruits.1 - Input should be an instance of Fruit [type=is_instance_of, input_value='Apple', input_type=str] - """ +class Model3(BaseModel): + list_of_even_numbers: List[EvenNumber] # (1)! ``` -- [`SkipValidation`][pydantic.functional_validators.SkipValidation] is a type that can be used to skip validation on a field. +1. As mentioned in the [annotated pattern](./fields.md#the-annotated-pattern) documentation, + we can also make use of validators for specific parts of the annotation (in this case, + validation is applied for list items, but not the whole list). -```python -from typing import List +It is also easier to understand which validators are applied to a type, by just looking at the field annotation. -from pydantic import BaseModel, SkipValidation +#### Using the decorator pattern +One of the key benefits of using the [`field_validator()`][pydantic.field_validator] decorator is to apply +the function to multiple fields: -class Model(BaseModel): - names: List[SkipValidation[str]] +```python +from pydantic import BaseModel, field_validator -m = Model(names=['foo', 'bar']) -print(m) -#> names=['foo', 'bar'] +class Model(BaseModel): + f1: str + f2: str -m = Model(names=['foo', 123]) # (1)! -print(m) -#> names=['foo', 123] + @field_validator('f1', 'f2', mode='before') + @classmethod + def capitalize(cls, value: str) -> str: + return value.capitalize() ``` -1. Note that the validation of the second item is skipped. If it has the wrong type it will emit a warning during serialization. +Here are a couple additional notes about the decorator usage: + +- If you want the validator to apply to all fields (including the ones defined in subclasses), you can pass + `'*'` as the field name argument. +- By default, the decorator will ensure the provided field name(s) are defined on the model. If you want to + disable this check during class creation, you can do so by passing `False` to the `check_fields` argument. + This is useful when the field validator is defined on a base class, and the field is expected to be set + on subclasses. + +## Model validators + +??? api "API Documentation" + [`pydantic.functional_validators.model_validator`][pydantic.functional_validators.model_validator]
+ +Validation can also be performed on the entire model's data using the [`model_validator()`][pydantic.model_validator] +decorator. + +**Three** different types of model validators can be used: + +[](){#model-after-validator} + +- __*After* validators__: run after the whole model has been validated. As such, they are defined as + *instance* methods and can be seen as post-initialization hooks. Important note: the validated instance + should be returned. + ```python + from typing_extensions import Self + + from pydantic import BaseModel, model_validator + + + class UserModel(BaseModel): + username: str + password: str + password_repeat: str + + @model_validator(mode='after') + def check_passwords_match(self) -> Self: + if self.password != self.password_repeat: + raise ValueError('Passwords do not match') + return self + ``` -## Field checks +[](){#model-before-validator} -During class creation, validators are checked to confirm that the fields they specify actually exist on the model. +- __*Before* validators__: are run before the model is instantiated. These are more flexible than *after* validators, + but they also have to deal with the raw input, which in theory could be any arbitrary object. + ```python + from typing import Any -This may be undesirable if, for example, you want to define a validator to validate fields that will only be present -on subclasses of the model where the validator is defined. + from pydantic import BaseModel, model_validator -If you want to disable these checks during class creation, you can pass `check_fields=False` as a keyword argument to -the validator. -## Validation Context + class UserModel(BaseModel): + username: str -You can pass a context object to the validation methods which can be accessed from the `info` -argument to decorated validator functions: + @model_validator(mode='before') + @classmethod + def check_card_number_not_present(cls, data: Any) -> Any: # (1)! + if isinstance(data, dict): # (2)! + if 'card_number' in data: + raise ValueError("'card_number' should not be included") + return data + ``` + + 1. Notice the use of [`Any`][typing.Any] as a type hint for `data`. *Before* validators take the raw input, which + can be anything. + 2. Most of the time, the input data will be a dictionary (e.g. when calling `UserModel(username='...')`). However, + this is not always the case. For instance, if the [`from_attributes`][pydantic.ConfigDict.from_attributes] + configuration value is set, you might receive an arbitrary class instance for the `data` argument. + +[](){#model-wrap-validator} + +- __*Wrap* validators__: are the most flexible of all. You can run code before or after Pydantic and + other validators process the input data, or you can terminate validation immediately, either by returning + the data early or by raising an error. + ```python {lint="skip"} + import logging + from typing import Any + + from typing_extensions import Self + + from pydantic import BaseModel, ModelWrapValidatorHandler, ValidationError, model_validator + + + class UserModel(BaseModel): + username: str + + @model_validator(mode='wrap') + @classmethod + def log_failed_validation(cls, data: Any, handler: ModelWrapValidatorHandler[Self]) -> Self: + try: + return handler(data) + except ValidationError: + logging.error('Model %s failed to validate with data %s', cls, data) + raise + ``` + +!!! note "On inheritance" + A model validator defined in a base class will be called during the validation of a subclass instance. + + Overriding a model validator in a subclass will override the base class' validator, and thus only the subclass' version of said validator will be called. + +## Raising validation errors + +To raise a validation error, three types of exceptions can be used: + +- [`ValueError`][]: this is the most common exception raised inside validators. +- [`AssertionError`][]: using the [assert][] statement also works, but be aware that these statements + are skipped when Python is run with the [-O][] optimization flag. +- [`PydanticCustomError`][pydantic_core.PydanticCustomError]: a bit more verbose, but provides extra flexibility: + ```python + from pydantic_core import PydanticCustomError + + from pydantic import BaseModel, ValidationError, field_validator + + + class Model(BaseModel): + x: int + + @field_validator('x', mode='after') + @classmethod + def validate_x(cls, v: int) -> int: + if v % 42 == 0: + raise PydanticCustomError( + 'the_answer_error', + '{number} is the answer!', + {'number': v}, + ) + return v + + + try: + Model(x=42 * 2) + except ValidationError as e: + print(e) + """ + 1 validation error for Model + x + 84 is the answer! [type=the_answer_error, input_value=84, input_type=int] + """ + ``` + +## Validation info + +Both the field and model validators callables (in all modes) can optionally take an extra +[`ValidationInfo`][pydantic.ValidationInfo] argument, providing useful extra information, such as: + +- [already validated data](#validation-data) +- [user defined context](#validation-context) +- the current validation mode: either `'python'` or `'json'` (see the [`mode`][pydantic.ValidationInfo.mode] property) +- the current field name (see the [`field_name`][pydantic.ValidationInfo.field_name] property). + +### Validation data + +For field validators, the already validated data can be accessed using the [`data`][pydantic.ValidationInfo.data] +property. Here is an example than can be used as an alternative to the [*after* model validator](#model-after-validator) +example: + +```python +from pydantic import BaseModel, ValidationInfo, field_validator + + +class UserModel(BaseModel): + password: str + password_repeat: str + username: str + + @field_validator('password_repeat', mode='after') + @classmethod + def check_passwords_match(cls, value: str, info: ValidationInfo) -> str: + if value != info.data['password']: + raise ValueError('Passwords do not match') + return value +``` + +!!! warning + As validation is performed in the [order fields are defined](./models.md#field-ordering), you have to + make sure you are not accessing a field that hasn't been validated yet. In the code above, for example, + the `username` validated value is not available yet, as it is defined *after* `password_repeat`. + +The [`data`][pydantic.ValidationInfo.data] property is `None` for [model validators](#model-validators). + +### Validation context + +You can pass a context object to the [validation methods](./models.md#validating-data), which can be accessed +inside the validator functions using the [`context`][pydantic.ValidationInfo.context] property: ```python from pydantic import BaseModel, ValidationInfo, field_validator @@ -638,12 +648,11 @@ from pydantic import BaseModel, ValidationInfo, field_validator class Model(BaseModel): text: str - @field_validator('text') + @field_validator('text', mode='after') @classmethod - def remove_stopwords(cls, v: str, info: ValidationInfo): - context = info.context - if context: - stopwords = context.get('stopwords', set()) + def remove_stopwords(cls, v: str, info: ValidationInfo) -> str: + if isinstance(info.context, dict): + stopwords = info.context.get('stopwords', set()) v = ' '.join(w for w in v.split() if w.lower() not in stopwords) return v @@ -653,160 +662,235 @@ print(Model.model_validate(data)) # no context #> text='This is an example document' print(Model.model_validate(data, context={'stopwords': ['this', 'is', 'an']})) #> text='example document' -print(Model.model_validate(data, context={'stopwords': ['document']})) -#> text='This is an example' ``` -This is useful when you need to dynamically update the validation behavior during runtime. For example, if you wanted -a field to have a dynamically controllable set of allowed values, this could be done by passing the allowed values -by context, and having a separate mechanism for updating what is allowed: +Similarly, you can [use a context for serialization](../concepts/serialization.md#serialization-context). -```python -from typing import Any, Dict, List +??? note "Providing context when directly instantiating a model" + It is currently not possible to provide a context when directly instantiating a model + (i.e. when calling `Model(...)`). You can work around this through the use of a + [`ContextVar`][contextvars.ContextVar] and a custom `__init__` method: -from pydantic import ( - BaseModel, - ValidationError, - ValidationInfo, - field_validator, -) + ```python + from __future__ import annotations -_allowed_choices = ['a', 'b', 'c'] + from contextlib import contextmanager + from contextvars import ContextVar + from typing import Any, Generator + from pydantic import BaseModel, ValidationInfo, field_validator -def set_allowed_choices(allowed_choices: List[str]) -> None: - global _allowed_choices - _allowed_choices = allowed_choices + _init_context_var = ContextVar('_init_context_var', default=None) -def get_context() -> Dict[str, Any]: - return {'allowed_choices': _allowed_choices} + @contextmanager + def init_context(value: dict[str, Any]) -> Generator[None]: + token = _init_context_var.set(value) + try: + yield + finally: + _init_context_var.reset(token) -class Model(BaseModel): - choice: str + class Model(BaseModel): + my_number: int - @field_validator('choice') - @classmethod - def validate_choice(cls, v: str, info: ValidationInfo): - allowed_choices = info.context.get('allowed_choices') - if allowed_choices and v not in allowed_choices: - raise ValueError(f'choice must be one of {allowed_choices}') - return v + def __init__(self, /, **data: Any) -> None: + self.__pydantic_validator__.validate_python( + data, + self_instance=self, + context=_init_context_var.get(), + ) + + @field_validator('my_number') + @classmethod + def multiply_with_context(cls, value: int, info: ValidationInfo) -> int: + if isinstance(info.context, dict): + multiplier = info.context.get('multiplier', 1) + value = value * multiplier + return value + + + print(Model(my_number=2)) + #> my_number=2 + with init_context({'multiplier': 3}): + print(Model(my_number=2)) + #> my_number=6 -print(Model.model_validate({'choice': 'a'}, context=get_context())) -#> choice='a' - -try: - print(Model.model_validate({'choice': 'd'}, context=get_context())) -except ValidationError as exc: - print(exc) - """ - 1 validation error for Model - choice - Value error, choice must be one of ['a', 'b', 'c'] [type=value_error, input_value='d', input_type=str] - """ - -set_allowed_choices(['b', 'c']) - -try: - print(Model.model_validate({'choice': 'a'}, context=get_context())) -except ValidationError as exc: - print(exc) - """ - 1 validation error for Model - choice - Value error, choice must be one of ['b', 'c'] [type=value_error, input_value='a', input_type=str] - """ + print(Model(my_number=2)) + #> my_number=2 + ``` + +## Ordering of validators + +When using the [annotated pattern](#using-the-annotated-pattern), the order in which validators are applied +is defined as follows: [*before*](#field-before-validator) and [*wrap*](#field-wrap-validator) validators +are run from right to left, and [*after*](#field-after-validator) validators are then run from left to right: + +```python {lint="skip" test="skip"} +from pydantic import AfterValidator, BaseModel, BeforeValidator, WrapValidator + + +class Model(BaseModel): + name: Annotated[ + str, + AfterValidator(runs_3rd), + AfterValidator(runs_4th), + BeforeValidator(runs_2nd), + WrapValidator(runs_1st), + ] ``` -Similarly, you can [use a context for serialization](../concepts/serialization.md#serialization-context). +Internally, validators defined using [the decorator](#using-the-decorator-pattern) are converted to their annotated +form counterpart and added last after the existing metadata for the field. This means that the same ordering +logic applies. -### Using validation context with `BaseModel` initialization -Although there is no way to specify a context in the standard `BaseModel` initializer, you can work around this through -the use of `contextvars.ContextVar` and a custom `__init__` method: +## Special types -```python -from contextlib import contextmanager -from contextvars import ContextVar -from typing import Any, Dict, Iterator +Pydantic provides a few special utilities that can be used to customize validation. -from pydantic import BaseModel, ValidationInfo, field_validator +- [`InstanceOf`][pydantic.functional_validators.InstanceOf] can be used to validate that a value is an instance of a given class. + ```python + from typing import List -_init_context_var = ContextVar('_init_context_var', default=None) + from pydantic import BaseModel, InstanceOf, ValidationError -@contextmanager -def init_context(value: Dict[str, Any]) -> Iterator[None]: - token = _init_context_var.set(value) - try: - yield - finally: - _init_context_var.reset(token) + class Fruit: + def __repr__(self): + return self.__class__.__name__ -class Model(BaseModel): - my_number: int + class Banana(Fruit): ... - def __init__(self, /, **data: Any) -> None: - self.__pydantic_validator__.validate_python( - data, - self_instance=self, - context=_init_context_var.get(), - ) - @field_validator('my_number') - @classmethod - def multiply_with_context(cls, value: int, info: ValidationInfo) -> int: - if info.context: - multiplier = info.context.get('multiplier', 1) - value = value * multiplier - return value + class Apple(Fruit): ... -print(Model(my_number=2)) -#> my_number=2 + class Basket(BaseModel): + fruits: List[InstanceOf[Fruit]] -with init_context({'multiplier': 3}): - print(Model(my_number=2)) - #> my_number=6 -print(Model(my_number=2)) -#> my_number=2 -``` + print(Basket(fruits=[Banana(), Apple()])) + #> fruits=[Banana, Apple] + try: + Basket(fruits=[Banana(), 'Apple']) + except ValidationError as e: + print(e) + """ + 1 validation error for Basket + fruits.1 + Input should be an instance of Fruit [type=is_instance_of, input_value='Apple', input_type=str] + """ + ``` + +- [`SkipValidation`][pydantic.functional_validators.SkipValidation] can be used to skip validation on a field. + ```python + from typing import List + + from pydantic import BaseModel, SkipValidation + + + class Model(BaseModel): + names: List[SkipValidation[str]] + + + m = Model(names=['foo', 'bar']) + print(m) + #> names=['foo', 'bar'] + + m = Model(names=['foo', 123]) # (1)! + print(m) + #> names=['foo', 123] + ``` -## Reusing Validators + 1. Note that the validation of the second item is skipped. If it has the wrong type it will emit a + warning during serialization. -Occasionally, you will want to use the same validator on multiple fields/models (e.g. to normalize some input data). -The "naive" approach would be to write a separate function, then call it from multiple decorators. -Obviously, this entails a lot of repetition and boiler plate code. -The following approach demonstrates how you can reuse a validator so that redundancy is minimized and the models become again almost declarative. +- [`PydanticUseDefault`][pydantic_core.PydanticUseDefault] can be used to notify Pydantic that the default value + should be used. + ```python + from typing import Any + + from pydantic_core import PydanticUseDefault + from typing_extensions import Annotated + + from pydantic import BaseModel, BeforeValidator + + + def default_if_none(value: Any) -> Any: + if value is None: + raise PydanticUseDefault() + return value + + + class Model(BaseModel): + name: Annotated[str, BeforeValidator(default_if_none)] = 'default_name' + + + print(Model(name=None)) + #> name='default_name' + ``` + +## JSON Schema and field validators + +When using [*before*](#field-before-validator), [*plain*](#field-plain-validator) or [*wrap*](#field-wrap-validator) +field validators, the accepted input type may be different from the field annotation. + +Consider the following example: ```python +from typing import Any + from pydantic import BaseModel, field_validator -def normalize(name: str) -> str: - return ' '.join((word.capitalize()) for word in name.split(' ')) +class Model(BaseModel): + value: str + + @field_validator('value', mode='before') + @classmethod + def cast_ints(cls, value: Any) -> Any: + if isinstance(value, int): + return str(value) + else: + return value + + +print(Model(value='a')) +#> value='a' +print(Model(value=1)) +#> value='1' +``` +While the type hint for `value` is `str`, the `cast_ints` validator also allows integers. To specify the correct +input type, the `json_schema_input_type` argument can be provided: -class Producer(BaseModel): - name: str +```python +from typing import Any, Union - _normalize_name = field_validator('name')(normalize) +from pydantic import BaseModel, field_validator -class Consumer(BaseModel): - name: str +class Model(BaseModel): + value: str - _normalize_name = field_validator('name')(normalize) + @field_validator( + 'value', mode='before', json_schema_input_type=Union[int, str] + ) + @classmethod + def cast_ints(cls, value: Any) -> Any: + if isinstance(value, int): + return str(value) + else: + return value -jane_doe = Producer(name='JaNe DOE') -print(repr(jane_doe)) -#> Producer(name='Jane Doe') -john_doe = Consumer(name='joHN dOe') -print(repr(john_doe)) -#> Consumer(name='John Doe') +print(Model.model_json_schema()['properties']['value']) +#> {'anyOf': [{'type': 'integer'}, {'type': 'string'}], 'title': 'Value'} ``` + +As a convenience, Pydantic will use the field type if the argument is not provided (unless you are using +a [*plain*](#field-plain-validator) validator, in which case `json_schema_input_type` defaults to +[`Any`][typing.Any] as the field type is completely discarded). diff --git a/docs/errors/usage_errors.md b/docs/errors/usage_errors.md index e2b26bb8f39..5eb41152619 100644 --- a/docs/errors/usage_errors.md +++ b/docs/errors/usage_errors.md @@ -1207,7 +1207,9 @@ except PydanticUserError as exc_info: ## Unsupported type for `validate_call` {#validate-call-type} -`validate_call` has some limitations on the callables it can validate. This error is raised when you try to use it with an unsupported callable. Currently the supported callables are functions (including lambdas) and methods and instances of [`partial`][functools.partial]. In the case of [`partial`][functools.partial], the function being partially applied must be one of the supported callables. +`validate_call` has some limitations on the callables it can validate. This error is raised when you try to use it with an unsupported callable. +Currently the supported callables are functions (including lambdas, but not built-ins) and methods and instances of [`partial`][functools.partial]. +In the case of [`partial`][functools.partial], the function being partially applied must be one of the supported callables. ### `@classmethod`, `@staticmethod`, and `@property` @@ -1260,9 +1262,10 @@ class A2: def __new__(cls): ... ``` -### Custom callable +### Callable instances -Although you can create custom callable types in Python by implementing a `__call__` method, currently the instances of these types cannot be validated with `validate_call`. This may change in the future, but for now, you should use `validate_call` explicitly on `__call__` instead. +Although instances can be callable by implementing a `__call__` method, currently the instances of these types cannot be validated with `validate_call`. +This may change in the future, but for now, you should use `validate_call` explicitly on `__call__` instead. ```python from pydantic import PydanticUserError, validate_call diff --git a/docs/plugins/main.py b/docs/plugins/main.py index 9ef4c9074d3..fe7b418b4f9 100644 --- a/docs/plugins/main.py +++ b/docs/plugins/main.py @@ -189,7 +189,10 @@ def add_tabs(match: re.Match[str]) -> str: else: return '\n\n'.join(output) - return re.sub(r'^(``` *py.*?)\n(.+?)^```(\s+(?:^\d+\. .+?\n)*)', add_tabs, markdown, flags=re.M | re.S) + # Note: we should move away from this regex approach. It does not handle edge cases (indented code blocks inside + # other blocks, etc) and can lead to bugs in the rendering of annotations. Edit with care and make sure the rendered + # documentation does not break: + return re.sub(r'(``` *py.*?)\n(.+?)^```(\s+(?:^\d+\. (?:[^\n][\n]?)+\n?)*)', add_tabs, markdown, flags=re.M | re.S) def _upgrade_code(code: str, min_version: int) -> str: diff --git a/pydantic/__init__.py b/pydantic/__init__.py index 3c3c130f0fb..7663c976b04 100644 --- a/pydantic/__init__.py +++ b/pydantic/__init__.py @@ -34,6 +34,7 @@ AfterValidator, BeforeValidator, InstanceOf, + ModelWrapValidatorHandler, PlainValidator, SkipValidation, WrapValidator, @@ -74,6 +75,7 @@ 'WrapValidator', 'SkipValidation', 'InstanceOf', + 'ModelWrapValidatorHandler', # JSON Schema 'WithJsonSchema', # deprecated V1 functional validators, these are imported via `__getattr__` below @@ -240,6 +242,7 @@ 'WrapValidator': (__spec__.parent, '.functional_validators'), 'SkipValidation': (__spec__.parent, '.functional_validators'), 'InstanceOf': (__spec__.parent, '.functional_validators'), + 'ModelWrapValidatorHandler': (__spec__.parent, '.functional_validators'), # JSON Schema 'WithJsonSchema': (__spec__.parent, '.json_schema'), # functional serializers diff --git a/pydantic/_internal/_core_metadata.py b/pydantic/_internal/_core_metadata.py index c8d452443cb..89e3e78833c 100644 --- a/pydantic/_internal/_core_metadata.py +++ b/pydantic/_internal/_core_metadata.py @@ -4,8 +4,6 @@ from warnings import warn if TYPE_CHECKING: - from pydantic_core import CoreSchema - from ..config import JsonDict, JsonSchemaExtraCallable from ._schema_generation_shared import ( GetJsonSchemaFunction, @@ -20,7 +18,6 @@ class CoreMetadata(TypedDict, total=False): pydantic_js_annotation_functions: List of JSON schema functions that don't resolve refs during application. pydantic_js_prefer_positional_arguments: Whether JSON schema generator will prefer positional over keyword arguments for an 'arguments' schema. - pydantic_js_input_core_schema: Schema associated with the input value for the associated custom validation function. Only applies to before, plain, and wrap validators. pydantic_js_udpates: key / value pair updates to apply to the JSON schema for a type. pydantic_js_extra: WIP, either key/value pair updates to apply to the JSON schema, or a custom callable. @@ -37,7 +34,6 @@ class CoreMetadata(TypedDict, total=False): pydantic_js_functions: list[GetJsonSchemaFunction] pydantic_js_annotation_functions: list[GetJsonSchemaFunction] pydantic_js_prefer_positional_arguments: bool - pydantic_js_input_core_schema: CoreSchema pydantic_js_updates: JsonDict pydantic_js_extra: JsonDict | JsonSchemaExtraCallable diff --git a/pydantic/_internal/_core_utils.py b/pydantic/_internal/_core_utils.py index e4c1aed9c1f..f6ab20e43df 100644 --- a/pydantic/_internal/_core_utils.py +++ b/pydantic/_internal/_core_utils.py @@ -280,12 +280,38 @@ def handle_dict_schema(self, schema: core_schema.DictSchema, f: Walk) -> core_sc schema['values_schema'] = self.walk(values_schema, f) return schema - def handle_function_schema(self, schema: AnyFunctionSchema, f: Walk) -> core_schema.CoreSchema: - if not is_function_with_inner_schema(schema): - return schema + def handle_function_after_schema( + self, schema: core_schema.AfterValidatorFunctionSchema, f: Walk + ) -> core_schema.CoreSchema: schema['schema'] = self.walk(schema['schema'], f) return schema + def handle_function_before_schema( + self, schema: core_schema.BeforeValidatorFunctionSchema, f: Walk + ) -> core_schema.CoreSchema: + schema['schema'] = self.walk(schema['schema'], f) + if 'json_schema_input_schema' in schema: + schema['json_schema_input_schema'] = self.walk(schema['json_schema_input_schema'], f) + return schema + + # TODO duplicate schema types for serializers and validators, needs to be deduplicated: + def handle_function_plain_schema( + self, schema: core_schema.PlainValidatorFunctionSchema | core_schema.PlainSerializerFunctionSerSchema, f: Walk + ) -> core_schema.CoreSchema: + if 'json_schema_input_schema' in schema: + schema['json_schema_input_schema'] = self.walk(schema['json_schema_input_schema'], f) + return schema # pyright: ignore[reportReturnType] + + # TODO duplicate schema types for serializers and validators, needs to be deduplicated: + def handle_function_wrap_schema( + self, schema: core_schema.WrapValidatorFunctionSchema | core_schema.WrapSerializerFunctionSerSchema, f: Walk + ) -> core_schema.CoreSchema: + if 'schema' in schema: + schema['schema'] = self.walk(schema['schema'], f) + if 'json_schema_input_schema' in schema: + schema['json_schema_input_schema'] = self.walk(schema['json_schema_input_schema'], f) + return schema # pyright: ignore[reportReturnType] + def handle_union_schema(self, schema: core_schema.UnionSchema, f: Walk) -> core_schema.CoreSchema: new_choices: list[CoreSchema | tuple[CoreSchema, str]] = [] for v in schema['choices']: diff --git a/pydantic/_internal/_generate_schema.py b/pydantic/_internal/_generate_schema.py index ee26d080f62..4d4a6a63660 100644 --- a/pydantic/_internal/_generate_schema.py +++ b/pydantic/_internal/_generate_schema.py @@ -22,7 +22,7 @@ from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network from itertools import chain from operator import attrgetter -from types import BuiltinFunctionType, BuiltinMethodType, FunctionType, LambdaType, MethodType +from types import FunctionType, LambdaType, MethodType from typing import ( TYPE_CHECKING, Any, @@ -155,8 +155,6 @@ LambdaType, FunctionType, MethodType, - BuiltinFunctionType, - BuiltinMethodType, partial, ] @@ -1816,6 +1814,8 @@ def _call_schema(self, function: ValidateCallSupportedTypes) -> core_schema.Call TODO support functional validators once we support them in Config """ sig = signature(function) + globalns, localns = self._types_namespace + type_hints = _typing_extra.get_function_type_hints(function, globalns=globalns, localns=localns) mode_lookup: dict[_ParameterKind, Literal['positional_only', 'positional_or_keyword', 'keyword_only']] = { Parameter.POSITIONAL_ONLY: 'positional_only', @@ -1832,13 +1832,7 @@ def _call_schema(self, function: ValidateCallSupportedTypes) -> core_schema.Call if p.annotation is sig.empty: annotation = typing.cast(Any, Any) else: - # Note: This was originally get by `_typing_extra.get_function_type_hints`, - # but we switch to simply `p.annotation` to support bultins (e.g. `sorted`). - # May need to revisit if anything breaks. - annotation = ( - _typing_extra._make_forward_ref(p.annotation) if isinstance(p.annotation, str) else p.annotation - ) - annotation = self._resolve_forward_ref(annotation) + annotation = type_hints[name] parameter_mode = mode_lookup.get(p.kind) if parameter_mode is not None: diff --git a/pydantic/_internal/_generics.py b/pydantic/_internal/_generics.py index d6fea96cb76..8a9de22162a 100644 --- a/pydantic/_internal/_generics.py +++ b/pydantic/_internal/_generics.py @@ -426,7 +426,8 @@ def generic_recursion_self_type( yield self_type else: previously_seen_type_refs.add(type_ref) - yield None + yield + previously_seen_type_refs.remove(type_ref) finally: if token: _generic_recursion_cache.reset(token) diff --git a/pydantic/_internal/_namespace_utils.py b/pydantic/_internal/_namespace_utils.py index 9b956d1db29..799c4c4ebf8 100644 --- a/pydantic/_internal/_namespace_utils.py +++ b/pydantic/_internal/_namespace_utils.py @@ -6,7 +6,7 @@ from functools import cached_property from typing import Any, Callable, Iterator, Mapping, NamedTuple, TypeVar -from typing_extensions import TypeAlias, TypeAliasType +from typing_extensions import ParamSpec, TypeAlias, TypeAliasType, TypeVarTuple GlobalsNamespace: TypeAlias = 'dict[str, Any]' """A global namespace. @@ -24,6 +24,8 @@ This namespace type is expected as the `locals` argument during annotations evaluation. """ +_TypeVarLike: TypeAlias = 'TypeVar | ParamSpec | TypeVarTuple' + class NamespacesTuple(NamedTuple): """A tuple of globals and locals to be used during annotations evaluation. @@ -123,7 +125,11 @@ def ns_for_function(obj: Callable[..., Any], parent_namespace: MappingNamespace # passed as a separate argument. However, internally, `_eval_type` calls # `ForwardRef._evaluate` which will merge type params with the localns, # essentially mimicking what we do here. - type_params: tuple[TypeVar, ...] = () + type_params: tuple[_TypeVarLike, ...] + if hasattr(obj, '__type_params__'): + type_params = obj.__type_params__ + else: + type_params = () if parent_namespace is not None: # We also fetch type params from the parent namespace. If present, it probably # means the function was defined in a class. This is to support the following: diff --git a/pydantic/functional_validators.py b/pydantic/functional_validators.py index 16f645d39ad..aa3da8c2713 100644 --- a/pydantic/functional_validators.py +++ b/pydantic/functional_validators.py @@ -26,7 +26,7 @@ @dataclasses.dataclass(frozen=True, **_internal_dataclass.slots_true) class AfterValidator: - """Usage docs: https://docs.pydantic.dev/2.10/concepts/validators/#annotated-validators + """Usage docs: https://docs.pydantic.dev/2.10/concepts/validators/#field-validators A metadata class that indicates that a validation should be applied **after** the inner validation logic. @@ -86,7 +86,7 @@ def _from_decorator(cls, decorator: _decorators.Decorator[_decorators.FieldValid @dataclasses.dataclass(frozen=True, **_internal_dataclass.slots_true) class BeforeValidator: - """Usage docs: https://docs.pydantic.dev/2.10/concepts/validators/#annotated-validators + """Usage docs: https://docs.pydantic.dev/2.10/concepts/validators/#field-validators A metadata class that indicates that a validation should be applied **before** the inner validation logic. @@ -127,14 +127,6 @@ def __get_pydantic_core_schema__(self, source_type: Any, handler: GetCoreSchemaH if self.json_schema_input_type is PydanticUndefined else handler.generate_schema(self.json_schema_input_type) ) - # Try to resolve the original schema if required, because schema cleaning - # won't inline references in metadata: - if input_schema is not None: - try: - input_schema = handler.resolve_ref_schema(input_schema) - except LookupError: - pass - metadata = {'pydantic_js_input_core_schema': input_schema} if input_schema is not None else {} info_arg = _inspect_validator(self.func, 'before') if info_arg: @@ -143,11 +135,13 @@ def __get_pydantic_core_schema__(self, source_type: Any, handler: GetCoreSchemaH func, schema=schema, field_name=handler.field_name, - metadata=metadata, + json_schema_input_schema=input_schema, ) else: func = cast(core_schema.NoInfoValidatorFunction, self.func) - return core_schema.no_info_before_validator_function(func, schema=schema, metadata=metadata) + return core_schema.no_info_before_validator_function( + func, schema=schema, json_schema_input_schema=input_schema + ) @classmethod def _from_decorator(cls, decorator: _decorators.Decorator[_decorators.FieldValidatorDecoratorInfo]) -> Self: @@ -159,7 +153,7 @@ def _from_decorator(cls, decorator: _decorators.Decorator[_decorators.FieldValid @dataclasses.dataclass(frozen=True, **_internal_dataclass.slots_true) class PlainValidator: - """Usage docs: https://docs.pydantic.dev/2.10/concepts/validators/#annotated-validators + """Usage docs: https://docs.pydantic.dev/2.10/concepts/validators/#field-validators A metadata class that indicates that a validation should be applied **instead** of the inner validation logic. @@ -229,13 +223,6 @@ def __get_pydantic_core_schema__(self, source_type: Any, handler: GetCoreSchemaH serialization = None input_schema = handler.generate_schema(self.json_schema_input_type) - # Try to resolve the original schema if required, because schema cleaning - # won't inline references in metadata: - try: - input_schema = handler.resolve_ref_schema(input_schema) - except LookupError: - pass - metadata = {'pydantic_js_input_core_schema': input_schema} if input_schema is not None else {} info_arg = _inspect_validator(self.func, 'plain') if info_arg: @@ -244,14 +231,14 @@ def __get_pydantic_core_schema__(self, source_type: Any, handler: GetCoreSchemaH func, field_name=handler.field_name, serialization=serialization, # pyright: ignore[reportArgumentType] - metadata=metadata, + json_schema_input_schema=input_schema, ) else: func = cast(core_schema.NoInfoValidatorFunction, self.func) return core_schema.no_info_plain_validator_function( func, serialization=serialization, # pyright: ignore[reportArgumentType] - metadata=metadata, + json_schema_input_schema=input_schema, ) @classmethod @@ -264,7 +251,7 @@ def _from_decorator(cls, decorator: _decorators.Decorator[_decorators.FieldValid @dataclasses.dataclass(frozen=True, **_internal_dataclass.slots_true) class WrapValidator: - """Usage docs: https://docs.pydantic.dev/2.10/concepts/validators/#annotated-validators + """Usage docs: https://docs.pydantic.dev/2.10/concepts/validators/#field-validators A metadata class that indicates that a validation should be applied **around** the inner validation logic. @@ -312,14 +299,6 @@ def __get_pydantic_core_schema__(self, source_type: Any, handler: GetCoreSchemaH if self.json_schema_input_type is PydanticUndefined else handler.generate_schema(self.json_schema_input_type) ) - # Try to resolve the original schema if required, because schema cleaning - # won't inline references in metadata: - if input_schema is not None: - try: - input_schema = handler.resolve_ref_schema(input_schema) - except LookupError: - pass - metadata = {'pydantic_js_input_core_schema': input_schema} if input_schema is not None else {} info_arg = _inspect_validator(self.func, 'wrap') if info_arg: @@ -328,14 +307,14 @@ def __get_pydantic_core_schema__(self, source_type: Any, handler: GetCoreSchemaH func, schema=schema, field_name=handler.field_name, - metadata=metadata, + json_schema_input_schema=input_schema, ) else: func = cast(core_schema.NoInfoWrapValidatorFunction, self.func) return core_schema.no_info_wrap_validator_function( func, schema=schema, - metadata=metadata, + json_schema_input_schema=input_schema, ) @classmethod diff --git a/pydantic/json_schema.py b/pydantic/json_schema.py index 301332e9ba7..3471e0df589 100644 --- a/pydantic/json_schema.py +++ b/pydantic/json_schema.py @@ -1053,9 +1053,7 @@ def function_before_schema(self, schema: core_schema.BeforeValidatorFunctionSche Returns: The generated JSON schema. """ - if self._mode == 'validation' and ( - input_schema := schema.get('metadata', {}).get('pydantic_js_input_core_schema') - ): + if self._mode == 'validation' and (input_schema := schema.get('json_schema_input_schema')): return self.generate_inner(input_schema) return self.generate_inner(schema['schema']) @@ -1080,9 +1078,7 @@ def function_plain_schema(self, schema: core_schema.PlainValidatorFunctionSchema Returns: The generated JSON schema. """ - if self._mode == 'validation' and ( - input_schema := schema.get('metadata', {}).get('pydantic_js_input_core_schema') - ): + if self._mode == 'validation' and (input_schema := schema.get('json_schema_input_schema')): return self.generate_inner(input_schema) return self.handle_invalid_for_json_schema( @@ -1098,9 +1094,7 @@ def function_wrap_schema(self, schema: core_schema.WrapValidatorFunctionSchema) Returns: The generated JSON schema. """ - if self._mode == 'validation' and ( - input_schema := schema.get('metadata', {}).get('pydantic_js_input_core_schema') - ): + if self._mode == 'validation' and (input_schema := schema.get('json_schema_input_schema')): return self.generate_inner(input_schema) return self.generate_inner(schema['schema']) diff --git a/pydantic/networks.py b/pydantic/networks.py index 755835199ce..5668f77b397 100644 --- a/pydantic/networks.py +++ b/pydantic/networks.py @@ -224,9 +224,24 @@ def __deepcopy__(self, memo: dict) -> Self: def __eq__(self, other: Any) -> bool: return self.__class__ is other.__class__ and self._url == other._url + def __lt__(self, other: Any) -> bool: + return self.__class__ is other.__class__ and self._url < other._url + + def __gt__(self, other: Any) -> bool: + return self.__class__ is other.__class__ and self._url > other._url + + def __le__(self, other: Any) -> bool: + return self.__class__ is other.__class__ and self._url <= other._url + + def __ge__(self, other: Any) -> bool: + return self.__class__ is other.__class__ and self._url >= other._url + def __hash__(self) -> int: return hash(self._url) + def __len__(self) -> int: + return len(str(self._url)) + @classmethod def build( cls, @@ -385,6 +400,9 @@ def __eq__(self, other: Any) -> bool: def __hash__(self) -> int: return hash(self._url) + def __len__(self) -> int: + return len(str(self._url)) + @classmethod def build( cls, diff --git a/pydantic/validate_call_decorator.py b/pydantic/validate_call_decorator.py index 20ee9e084b9..b5ac03c15fc 100644 --- a/pydantic/validate_call_decorator.py +++ b/pydantic/validate_call_decorator.py @@ -4,6 +4,7 @@ import inspect from functools import partial +from types import BuiltinFunctionType from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast, overload from ._internal import _generate_schema, _typing_extra, _validate_call @@ -42,6 +43,8 @@ def _check_function_type(function: object) -> None: return + if isinstance(function, BuiltinFunctionType): + raise PydanticUserError(f'Input built-in function `{function}` is not supported', code=_INVALID_TYPE_ERROR_CODE) if isinstance(function, (classmethod, staticmethod, property)): name = type(function).__name__ raise PydanticUserError( diff --git a/pydantic/version.py b/pydantic/version.py index 9176145c64f..d85a8c75cf7 100644 --- a/pydantic/version.py +++ b/pydantic/version.py @@ -4,7 +4,7 @@ __all__ = 'VERSION', 'version_info' -VERSION = '2.10.3' +VERSION = '2.10.4' """The version of Pydantic.""" diff --git a/pyproject.toml b/pyproject.toml index 8e2569ec117..3fe661c3735 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ requires-python = '>=3.8' dependencies = [ 'typing-extensions>=4.12.2', 'annotated-types>=0.6.0', - 'pydantic-core==2.27.1', + 'pydantic-core==2.27.2', ] dynamic = ['version', 'readme'] diff --git a/tests/test_generics.py b/tests/test_generics.py index 2e11d871f85..470471731cf 100644 --- a/tests/test_generics.py +++ b/tests/test_generics.py @@ -3099,3 +3099,41 @@ class Holder(BaseModel, Generic[T]): holder2 = Holder(inner=Inner(inner=1)) # implies that validation succeeds for both assert holder1 == holder2 + + +def test_generic_mro_multi_level(): + """Pass another generic model as an arg. + + See https://github.com/pydantic/pydantic/issues/11024. + """ + + T = TypeVar('T') + + class GenericBaseModel(BaseModel, Generic[T]): ... + + class EnumerableModel(GenericBaseModel[T]): + values: List[T] + + T1 = TypeVar('T1') + T2 = TypeVar('T2') + + class CombineModel(BaseModel, Generic[T1, T2]): + field_1: T1 + field_2: T2 + + class EnumerableCombineModel(EnumerableModel[CombineModel[T1, T2]]): ... + + m = EnumerableCombineModel[int, int] + + mro = ( + EnumerableCombineModel[int, int], + EnumerableCombineModel, + EnumerableModel[CombineModel[int, int]], + EnumerableModel, + GenericBaseModel[CombineModel[int, int]], + GenericBaseModel, + BaseModel, + Generic, + object, + ) + assert m.__mro__ == tuple(HasRepr(repr(m)) for m in mro) diff --git a/tests/test_json_schema.py b/tests/test_json_schema.py index c64e7f813b0..14520b55b77 100644 --- a/tests/test_json_schema.py +++ b/tests/test_json_schema.py @@ -6531,31 +6531,39 @@ def validate_f(cls, value: Any) -> int: ... @pytest.mark.parametrize( 'validator', [ - PlainValidator(lambda v: v, json_schema_input_type='Sub'), - BeforeValidator(lambda v: v, json_schema_input_type='Sub'), - WrapValidator(lambda v, h: h(v), json_schema_input_type='Sub'), + PlainValidator(lambda v: v, json_schema_input_type='Union[Sub1, Sub2]'), + BeforeValidator(lambda v: v, json_schema_input_type='Union[Sub1, Sub2]'), + WrapValidator(lambda v, h: h(v), json_schema_input_type='Union[Sub1, Sub2]'), ], ) def test_json_schema_input_type_with_refs(validator) -> None: - """Test that `'definition-ref` schemas for `json_schema_input_type` are inlined. + """Test that `'definition-ref` schemas for `json_schema_input_type` are supported. See: https://github.com/pydantic/pydantic/issues/10434. + See: https://github.com/pydantic/pydantic/issues/11033 """ - class Sub(BaseModel): + class Sub1(BaseModel): + pass + + class Sub2(BaseModel): pass class Model(BaseModel): sub: Annotated[ - Sub, - PlainSerializer(lambda v: v, return_type=Sub), + Union[Sub1, Sub2], + PlainSerializer(lambda v: v, return_type=Union[Sub1, Sub2]), validator, ] json_schema = Model.model_json_schema() - assert 'Sub' in json_schema['$defs'] - assert json_schema['properties']['sub']['$ref'] == '#/$defs/Sub' + assert 'Sub1' in json_schema['$defs'] + assert 'Sub2' in json_schema['$defs'] + assert json_schema['properties']['sub'] == { + 'anyOf': [{'$ref': '#/$defs/Sub1'}, {'$ref': '#/$defs/Sub2'}], + 'title': 'Sub', + } @pytest.mark.parametrize( diff --git a/tests/test_networks.py b/tests/test_networks.py index b70296b9714..2e7c507395a 100644 --- a/tests/test_networks.py +++ b/tests/test_networks.py @@ -13,6 +13,7 @@ BaseModel, ClickHouseDsn, CockroachDsn, + Field, FileUrl, FtpUrl, HttpUrl, @@ -1144,3 +1145,38 @@ def test_json_schema() -> None: ser_json_schema = ta.json_schema(mode='serialization') assert ser_json_schema == {'type': 'string', 'format': 'uri', 'minLength': 1, 'maxLength': 2083} + + +def test_any_url_comparison() -> None: + first_url = AnyUrl('https://a.com') + second_url = AnyUrl('https://b.com') + + assert first_url < second_url + assert second_url > first_url + assert first_url <= second_url + assert second_url >= first_url + + +def test_max_length_base_url() -> None: + class Model(BaseModel): + url: AnyUrl = Field(max_length=20) + + # _BaseUrl/AnyUrl adds trailing slash: https://github.com/pydantic/pydantic/issues/7186 + # once solved the second expected line can be removed + expected = 'https://example.com' + expected = f'{expected}/' + assert len(Model(url='https://example.com').url) == len(expected) + + with pytest.raises(ValidationError, match=r'Value should have at most 20 items after validation'): + Model(url='https://example.com/longer') + + +def test_max_length_base_multi_host() -> None: + class Model(BaseModel): + postgres: PostgresDsn = Field(max_length=45) + + expected = 'postgres://user:pass@localhost:5432/foobar' + assert len(Model(postgres=expected).postgres) == len(expected) + + with pytest.raises(ValidationError, match=r'Value should have at most 45 items after validation'): + Model(postgres='postgres://user:pass@localhost:5432/foobarbazfoo') diff --git a/tests/test_utils.py b/tests/test_utils.py index 820fe9d322c..ba838264688 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -637,29 +637,6 @@ def walk(s, recurse): } -def test_handle_function_schema(): - schema = core_schema.with_info_before_validator_function( - lambda v, _info: v, core_schema.float_schema(), field_name='field_name' - ) - - def walk(s, recurse): - # change type to str - if s['type'] == 'float': - s['type'] = 'str' - return s - - schema = _WalkCoreSchema().handle_function_schema(schema, walk) - assert schema['type'] == 'function-before' - assert schema['schema'] == {'type': 'str'} - - def walk1(s, recurse): - # this is here to make sure this function is not called - assert False - - schema = _WalkCoreSchema().handle_function_schema(core_schema.int_schema(), walk1) - assert schema['type'] == 'int' - - def test_handle_call_schema(): param_a = core_schema.arguments_parameter(name='a', schema=core_schema.str_schema(), mode='positional_only') args_schema = core_schema.arguments_schema([param_a]) diff --git a/tests/test_validate_call.py b/tests/test_validate_call.py index a265359c212..181b3638c99 100644 --- a/tests/test_validate_call.py +++ b/tests/test_validate_call.py @@ -64,6 +64,14 @@ def m(self, x: int): ... validate_call([]) +def validate_bare_none() -> None: + @validate_call + def func(f: None): + return f + + assert func(f=None) is None + + def test_validate_class() -> None: class A: @validate_call @@ -113,17 +121,9 @@ def __call__(self, x: int) -> int: def test_invalid_signature() -> None: - # In some versions, these functions may not have a valid signature - for func in (max, min, breakpoint, sorted, compile, print, [].append, {}.popitem, int().bit_length): - try: - inspect.signature(func) - assert validate_call(func).__name__ == func.__name__ - assert validate_call(func).__qualname__ == func.__qualname__ - assert validate_call(partial(func)).__name__ == f'partial({func.__name__})' - assert validate_call(partial(func)).__qualname__ == f'partial({func.__qualname__})' - except ValueError: - with pytest.raises(PydanticUserError, match=(f"Input function `{func}` doesn't have a valid signature")): - validate_call(func) + # Builtins functions not supported: + with pytest.raises(PydanticUserError, match=(f'Input built-in function `{breakpoint}` is not supported')): + validate_call(breakpoint) class A: def f(): ... @@ -1129,11 +1129,11 @@ def test_validate_call_with_pep_695_syntax(create_module) -> None: from pydantic import validate_call @validate_call -def find_max_no_validate_return[T](args: Iterable[T]) -> T: +def find_max_no_validate_return[T](args: 'Iterable[T]') -> T: return sorted(args, reverse=True)[0] @validate_call(validate_return=True) -def find_max_validate_return[T](args: Iterable[T]) -> T: +def find_max_validate_return[T](args: 'Iterable[T]') -> T: return sorted(args, reverse=True)[0] """ ) diff --git a/uv.lock b/uv.lock index a7eba385e11..adbe5493db0 100644 --- a/uv.lock +++ b/uv.lock @@ -1,7 +1,7 @@ version = 1 requires-python = ">=3.8" resolution-markers = [ - "python_full_version < '3.9' or (python_full_version < '3.12' and platform_system != 'Windows' and sys_platform != 'win32')", + "(python_full_version < '3.9' and platform_system == 'Windows') or (python_full_version < '3.12' and platform_system != 'Windows' and sys_platform != 'win32') or (python_full_version < '3.9' and sys_platform == 'win32')", "python_full_version >= '3.9' and python_full_version < '3.12' and platform_system == 'Windows' and sys_platform != 'win32'", "python_full_version >= '3.12' and sys_platform != 'win32'", "python_full_version >= '3.9' and python_full_version < '3.12' and sys_platform == 'win32'", @@ -46,8 +46,8 @@ name = "astunparse" version = "1.6.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "six", marker = "python_full_version < '3.9' or (python_full_version < '3.12' and platform_system != 'Windows' and sys_platform != 'win32')" }, - { name = "wheel", marker = "python_full_version < '3.9' or (python_full_version < '3.12' and platform_system != 'Windows' and sys_platform != 'win32')" }, + { name = "six", marker = "(python_full_version < '3.9' and platform_system == 'Windows') or (python_full_version < '3.12' and platform_system != 'Windows' and sys_platform != 'win32') or (python_full_version < '3.9' and sys_platform == 'win32')" }, + { name = "wheel", marker = "(python_full_version < '3.9' and platform_system == 'Windows') or (python_full_version < '3.12' and platform_system != 'Windows' and sys_platform != 'win32') or (python_full_version < '3.9' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f3/af/4182184d3c338792894f34a62672919db7ca008c89abee9b564dd34d8029/astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872", size = 18290 } wheels = [ @@ -1324,7 +1324,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.10.3" +version = "2.10.4" source = { editable = "." } dependencies = [ { name = "annotated-types" }, @@ -1429,7 +1429,7 @@ typechecking = [ requires-dist = [ { name = "annotated-types", specifier = ">=0.6.0" }, { name = "email-validator", marker = "extra == 'email'", specifier = ">=2.0.0" }, - { name = "pydantic-core", specifier = "==2.27.1" }, + { name = "pydantic-core", specifier = "==2.27.2" }, { name = "typing-extensions", specifier = ">=4.12.2" }, { name = "tzdata", marker = "python_full_version >= '3.9' and platform_system == 'Windows' and extra == 'timezone'" }, ] @@ -1521,112 +1521,112 @@ typechecking = [ [[package]] name = "pydantic-core" -version = "2.27.1" +version = "2.27.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a6/9f/7de1f19b6aea45aeb441838782d68352e71bfa98ee6fa048d5041991b33e/pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235", size = 412785 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/ce/60fd96895c09738648c83f3f00f595c807cb6735c70d3306b548cc96dd49/pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a", size = 1897984 }, - { url = "https://files.pythonhosted.org/packages/fd/b9/84623d6b6be98cc209b06687d9bca5a7b966ffed008d15225dd0d20cce2e/pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b", size = 1807491 }, - { url = "https://files.pythonhosted.org/packages/01/72/59a70165eabbc93b1111d42df9ca016a4aa109409db04304829377947028/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278", size = 1831953 }, - { url = "https://files.pythonhosted.org/packages/7c/0c/24841136476adafd26f94b45bb718a78cb0500bd7b4f8d667b67c29d7b0d/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05", size = 1856071 }, - { url = "https://files.pythonhosted.org/packages/53/5e/c32957a09cceb2af10d7642df45d1e3dbd8596061f700eac93b801de53c0/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4", size = 2038439 }, - { url = "https://files.pythonhosted.org/packages/e4/8f/979ab3eccd118b638cd6d8f980fea8794f45018255a36044dea40fe579d4/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f", size = 2787416 }, - { url = "https://files.pythonhosted.org/packages/02/1d/00f2e4626565b3b6d3690dab4d4fe1a26edd6a20e53749eb21ca892ef2df/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08", size = 2134548 }, - { url = "https://files.pythonhosted.org/packages/9d/46/3112621204128b90898adc2e721a3cd6cf5626504178d6f32c33b5a43b79/pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6", size = 1989882 }, - { url = "https://files.pythonhosted.org/packages/49/ec/557dd4ff5287ffffdf16a31d08d723de6762bb1b691879dc4423392309bc/pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807", size = 1995829 }, - { url = "https://files.pythonhosted.org/packages/6e/b2/610dbeb74d8d43921a7234555e4c091cb050a2bdb8cfea86d07791ce01c5/pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c", size = 2091257 }, - { url = "https://files.pythonhosted.org/packages/8c/7f/4bf8e9d26a9118521c80b229291fa9558a07cdd9a968ec2d5c1026f14fbc/pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206", size = 2143894 }, - { url = "https://files.pythonhosted.org/packages/1f/1c/875ac7139c958f4390f23656fe696d1acc8edf45fb81e4831960f12cd6e4/pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c", size = 1816081 }, - { url = "https://files.pythonhosted.org/packages/d7/41/55a117acaeda25ceae51030b518032934f251b1dac3704a53781383e3491/pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17", size = 1981109 }, - { url = "https://files.pythonhosted.org/packages/27/39/46fe47f2ad4746b478ba89c561cafe4428e02b3573df882334bd2964f9cb/pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8", size = 1895553 }, - { url = "https://files.pythonhosted.org/packages/1c/00/0804e84a78b7fdb394fff4c4f429815a10e5e0993e6ae0e0b27dd20379ee/pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330", size = 1807220 }, - { url = "https://files.pythonhosted.org/packages/01/de/df51b3bac9820d38371f5a261020f505025df732ce566c2a2e7970b84c8c/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52", size = 1829727 }, - { url = "https://files.pythonhosted.org/packages/5f/d9/c01d19da8f9e9fbdb2bf99f8358d145a312590374d0dc9dd8dbe484a9cde/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4", size = 1854282 }, - { url = "https://files.pythonhosted.org/packages/5f/84/7db66eb12a0dc88c006abd6f3cbbf4232d26adfd827a28638c540d8f871d/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c", size = 2037437 }, - { url = "https://files.pythonhosted.org/packages/34/ac/a2537958db8299fbabed81167d58cc1506049dba4163433524e06a7d9f4c/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de", size = 2780899 }, - { url = "https://files.pythonhosted.org/packages/4a/c1/3e38cd777ef832c4fdce11d204592e135ddeedb6c6f525478a53d1c7d3e5/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025", size = 2135022 }, - { url = "https://files.pythonhosted.org/packages/7a/69/b9952829f80fd555fe04340539d90e000a146f2a003d3fcd1e7077c06c71/pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e", size = 1987969 }, - { url = "https://files.pythonhosted.org/packages/05/72/257b5824d7988af43460c4e22b63932ed651fe98804cc2793068de7ec554/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919", size = 1994625 }, - { url = "https://files.pythonhosted.org/packages/73/c3/78ed6b7f3278a36589bcdd01243189ade7fc9b26852844938b4d7693895b/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c", size = 2090089 }, - { url = "https://files.pythonhosted.org/packages/8d/c8/b4139b2f78579960353c4cd987e035108c93a78371bb19ba0dc1ac3b3220/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc", size = 2142496 }, - { url = "https://files.pythonhosted.org/packages/3e/f8/171a03e97eb36c0b51981efe0f78460554a1d8311773d3d30e20c005164e/pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9", size = 1811758 }, - { url = "https://files.pythonhosted.org/packages/6a/fe/4e0e63c418c1c76e33974a05266e5633e879d4061f9533b1706a86f77d5b/pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5", size = 1980864 }, - { url = "https://files.pythonhosted.org/packages/50/fc/93f7238a514c155a8ec02fc7ac6376177d449848115e4519b853820436c5/pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89", size = 1864327 }, - { url = "https://files.pythonhosted.org/packages/be/51/2e9b3788feb2aebff2aa9dfbf060ec739b38c05c46847601134cc1fed2ea/pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f", size = 1895239 }, - { url = "https://files.pythonhosted.org/packages/7b/9e/f8063952e4a7d0127f5d1181addef9377505dcce3be224263b25c4f0bfd9/pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02", size = 1805070 }, - { url = "https://files.pythonhosted.org/packages/2c/9d/e1d6c4561d262b52e41b17a7ef8301e2ba80b61e32e94520271029feb5d8/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c", size = 1828096 }, - { url = "https://files.pythonhosted.org/packages/be/65/80ff46de4266560baa4332ae3181fffc4488ea7d37282da1a62d10ab89a4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac", size = 1857708 }, - { url = "https://files.pythonhosted.org/packages/d5/ca/3370074ad758b04d9562b12ecdb088597f4d9d13893a48a583fb47682cdf/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb", size = 2037751 }, - { url = "https://files.pythonhosted.org/packages/b1/e2/4ab72d93367194317b99d051947c071aef6e3eb95f7553eaa4208ecf9ba4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529", size = 2733863 }, - { url = "https://files.pythonhosted.org/packages/8a/c6/8ae0831bf77f356bb73127ce5a95fe115b10f820ea480abbd72d3cc7ccf3/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35", size = 2161161 }, - { url = "https://files.pythonhosted.org/packages/f1/f4/b2fe73241da2429400fc27ddeaa43e35562f96cf5b67499b2de52b528cad/pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089", size = 1993294 }, - { url = "https://files.pythonhosted.org/packages/77/29/4bb008823a7f4cc05828198153f9753b3bd4c104d93b8e0b1bfe4e187540/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381", size = 2001468 }, - { url = "https://files.pythonhosted.org/packages/f2/a9/0eaceeba41b9fad851a4107e0cf999a34ae8f0d0d1f829e2574f3d8897b0/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb", size = 2091413 }, - { url = "https://files.pythonhosted.org/packages/d8/36/eb8697729725bc610fd73940f0d860d791dc2ad557faaefcbb3edbd2b349/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae", size = 2154735 }, - { url = "https://files.pythonhosted.org/packages/52/e5/4f0fbd5c5995cc70d3afed1b5c754055bb67908f55b5cb8000f7112749bf/pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c", size = 1833633 }, - { url = "https://files.pythonhosted.org/packages/ee/f2/c61486eee27cae5ac781305658779b4a6b45f9cc9d02c90cb21b940e82cc/pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16", size = 1986973 }, - { url = "https://files.pythonhosted.org/packages/df/a6/e3f12ff25f250b02f7c51be89a294689d175ac76e1096c32bf278f29ca1e/pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e", size = 1883215 }, - { url = "https://files.pythonhosted.org/packages/0f/d6/91cb99a3c59d7b072bded9959fbeab0a9613d5a4935773c0801f1764c156/pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073", size = 1895033 }, - { url = "https://files.pythonhosted.org/packages/07/42/d35033f81a28b27dedcade9e967e8a40981a765795c9ebae2045bcef05d3/pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08", size = 1807542 }, - { url = "https://files.pythonhosted.org/packages/41/c2/491b59e222ec7e72236e512108ecad532c7f4391a14e971c963f624f7569/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf", size = 1827854 }, - { url = "https://files.pythonhosted.org/packages/e3/f3/363652651779113189cefdbbb619b7b07b7a67ebb6840325117cc8cc3460/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737", size = 1857389 }, - { url = "https://files.pythonhosted.org/packages/5f/97/be804aed6b479af5a945daec7538d8bf358d668bdadde4c7888a2506bdfb/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2", size = 2037934 }, - { url = "https://files.pythonhosted.org/packages/42/01/295f0bd4abf58902917e342ddfe5f76cf66ffabfc57c2e23c7681a1a1197/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107", size = 2735176 }, - { url = "https://files.pythonhosted.org/packages/9d/a0/cd8e9c940ead89cc37812a1a9f310fef59ba2f0b22b4e417d84ab09fa970/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51", size = 2160720 }, - { url = "https://files.pythonhosted.org/packages/73/ae/9d0980e286627e0aeca4c352a60bd760331622c12d576e5ea4441ac7e15e/pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a", size = 1992972 }, - { url = "https://files.pythonhosted.org/packages/bf/ba/ae4480bc0292d54b85cfb954e9d6bd226982949f8316338677d56541b85f/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc", size = 2001477 }, - { url = "https://files.pythonhosted.org/packages/55/b7/e26adf48c2f943092ce54ae14c3c08d0d221ad34ce80b18a50de8ed2cba8/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960", size = 2091186 }, - { url = "https://files.pythonhosted.org/packages/ba/cc/8491fff5b608b3862eb36e7d29d36a1af1c945463ca4c5040bf46cc73f40/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23", size = 2154429 }, - { url = "https://files.pythonhosted.org/packages/78/d8/c080592d80edd3441ab7f88f865f51dae94a157fc64283c680e9f32cf6da/pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05", size = 1833713 }, - { url = "https://files.pythonhosted.org/packages/83/84/5ab82a9ee2538ac95a66e51f6838d6aba6e0a03a42aa185ad2fe404a4e8f/pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337", size = 1987897 }, - { url = "https://files.pythonhosted.org/packages/df/c3/b15fb833926d91d982fde29c0624c9f225da743c7af801dace0d4e187e71/pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5", size = 1882983 }, - { url = "https://files.pythonhosted.org/packages/97/bb/c62074a65a32ed279bef44862e89fabb5ab1a81df8a9d383bddb4f49a1e0/pydantic_core-2.27.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5897bec80a09b4084aee23f9b73a9477a46c3304ad1d2d07acca19723fb1de62", size = 1901535 }, - { url = "https://files.pythonhosted.org/packages/9b/59/e224c93f95ffd4f5d37f1d148c569eda8ae23446ab8daf3a211ac0533e08/pydantic_core-2.27.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d0165ab2914379bd56908c02294ed8405c252250668ebcb438a55494c69f44ab", size = 1781287 }, - { url = "https://files.pythonhosted.org/packages/11/e2/33629134e577543b9335c5ca9bbfd2348f5023fda956737777a7a3b86788/pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b9af86e1d8e4cfc82c2022bfaa6f459381a50b94a29e95dcdda8442d6d83864", size = 1834575 }, - { url = "https://files.pythonhosted.org/packages/fe/16/82e0849b3c6deb0330c07f1a8d55708d003ec8b1fd38ac84c7a830e25252/pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f6c8a66741c5f5447e047ab0ba7a1c61d1e95580d64bce852e3df1f895c4067", size = 1857948 }, - { url = "https://files.pythonhosted.org/packages/6b/4e/cdee588a7440bc58b6351e8b8dc2432e38b1144b5ae6625bfbdfb7fa76d9/pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a42d6a8156ff78981f8aa56eb6394114e0dedb217cf8b729f438f643608cbcd", size = 2041138 }, - { url = "https://files.pythonhosted.org/packages/1d/0e/73e0d1dff37a29c31e5b3e8587d228ced736cc7af9f81f6d7d06aa47576c/pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64c65f40b4cd8b0e049a8edde07e38b476da7e3aaebe63287c899d2cff253fa5", size = 2783820 }, - { url = "https://files.pythonhosted.org/packages/9a/b1/f164d05be347b99b91327ea9dd1118562951d2c86e1ea943ef73636b0810/pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdcf339322a3fae5cbd504edcefddd5a50d9ee00d968696846f089b4432cf78", size = 2138035 }, - { url = "https://files.pythonhosted.org/packages/72/44/cf1f20d3036d7e1545eafde0af4f3172075573a407a3a20313115c8990ff/pydantic_core-2.27.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf99c8404f008750c846cb4ac4667b798a9f7de673ff719d705d9b2d6de49c5f", size = 1991778 }, - { url = "https://files.pythonhosted.org/packages/5d/4c/486d8ddd595892e7d791f26dfd3e51bd8abea478eb7747fe2bbe890a2177/pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f1edcea27918d748c7e5e4d917297b2a0ab80cad10f86631e488b7cddf76a36", size = 1996644 }, - { url = "https://files.pythonhosted.org/packages/33/2a/9a1cd4c8aca242816be431583a3250797f2932fad32d35ad5aefcea179bc/pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:159cac0a3d096f79ab6a44d77a961917219707e2a130739c64d4dd46281f5c2a", size = 2091778 }, - { url = "https://files.pythonhosted.org/packages/8f/61/03576dac806c49e76a714c23f501420b0aeee80f97b995fc4b28fe63a010/pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:029d9757eb621cc6e1848fa0b0310310de7301057f623985698ed7ebb014391b", size = 2146020 }, - { url = "https://files.pythonhosted.org/packages/72/82/e236d762052d24949aabad3952bc2c8635a470d6f3cbdd69498692afa679/pydantic_core-2.27.1-cp38-none-win32.whl", hash = "sha256:a28af0695a45f7060e6f9b7092558a928a28553366519f64083c63a44f70e618", size = 1819443 }, - { url = "https://files.pythonhosted.org/packages/6e/89/26816cad528ca5d4af9be33aa91507504c4576100e53b371b5bc6d3c797b/pydantic_core-2.27.1-cp38-none-win_amd64.whl", hash = "sha256:2d4567c850905d5eaaed2f7a404e61012a51caf288292e016360aa2b96ff38d4", size = 1979478 }, - { url = "https://files.pythonhosted.org/packages/bc/6a/d741ce0c7da75ce9b394636a406aace00ad992ae417935ef2ad2e67fb970/pydantic_core-2.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967", size = 1898376 }, - { url = "https://files.pythonhosted.org/packages/bd/68/6ba18e30f10c7051bc55f1dffeadbee51454b381c91846104892a6d3b9cd/pydantic_core-2.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60", size = 1777246 }, - { url = "https://files.pythonhosted.org/packages/36/b8/6f1b7c5f068c00dfe179b8762bc1d32c75c0e9f62c9372174b1b64a74aa8/pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854", size = 1832148 }, - { url = "https://files.pythonhosted.org/packages/d9/83/83ff64d599847f080a93df119e856e3bd93063cced04b9a27eb66d863831/pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aee66be87825cdf72ac64cb03ad4c15ffef4143dbf5c113f64a5ff4f81477bf9", size = 1856371 }, - { url = "https://files.pythonhosted.org/packages/72/e9/974e6c73f59627c446833ecc306cadd199edab40abcfa093372a5a5c0156/pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b748c44bb9f53031c8cbc99a8a061bc181c1000c60a30f55393b6e9c45cc5bd", size = 2038686 }, - { url = "https://files.pythonhosted.org/packages/5e/bb/5e912d02dcf29aebb2da35e5a1a26088c39ffc0b1ea81242ee9db6f1f730/pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ca038c7f6a0afd0b2448941b6ef9d5e1949e999f9e5517692eb6da58e9d44be", size = 2785725 }, - { url = "https://files.pythonhosted.org/packages/85/d7/936846087424c882d89c853711687230cd60179a67c79c34c99b64f92625/pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0bd57539da59a3e4671b90a502da9a28c72322a4f17866ba3ac63a82c4498e", size = 2135177 }, - { url = "https://files.pythonhosted.org/packages/82/72/5a386e5ce8d3e933c3f283e61357474181c39383f38afffc15a6152fa1c5/pydantic_core-2.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac6c2c45c847bbf8f91930d88716a0fb924b51e0c6dad329b793d670ec5db792", size = 1989877 }, - { url = "https://files.pythonhosted.org/packages/ce/5c/b1c417a5fd67ce132d78d16a6ba7629dc7f188dbd4f7c30ef58111ee5147/pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b94d4ba43739bbe8b0ce4262bcc3b7b9f31459ad120fb595627eaeb7f9b9ca01", size = 1996006 }, - { url = "https://files.pythonhosted.org/packages/dd/04/4e18f2c42b29929882f30e4c09a3a039555158995a4ac730a73585198a66/pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:00e6424f4b26fe82d44577b4c842d7df97c20be6439e8e685d0d715feceb9fb9", size = 2091441 }, - { url = "https://files.pythonhosted.org/packages/06/84/5a332345b7efb5ab361f916eaf7316ef010e72417e8c7dd3d34462ee9840/pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:38de0a70160dd97540335b7ad3a74571b24f1dc3ed33f815f0880682e6880131", size = 2144471 }, - { url = "https://files.pythonhosted.org/packages/54/58/23caa58c35d36627156789c0fb562264c12cfdb451c75eb275535188a96f/pydantic_core-2.27.1-cp39-none-win32.whl", hash = "sha256:7ccebf51efc61634f6c2344da73e366c75e735960b5654b63d7e6f69a5885fa3", size = 1816563 }, - { url = "https://files.pythonhosted.org/packages/f7/9c/e83f08adc8e222b43c7f11d98b27eba08f21bcb259bcbf74743ce903c49c/pydantic_core-2.27.1-cp39-none-win_amd64.whl", hash = "sha256:a57847b090d7892f123726202b7daa20df6694cbd583b67a592e856bff603d6c", size = 1983137 }, - { url = "https://files.pythonhosted.org/packages/7c/60/e5eb2d462595ba1f622edbe7b1d19531e510c05c405f0b87c80c1e89d5b1/pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6", size = 1894016 }, - { url = "https://files.pythonhosted.org/packages/61/20/da7059855225038c1c4326a840908cc7ca72c7198cb6addb8b92ec81c1d6/pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676", size = 1771648 }, - { url = "https://files.pythonhosted.org/packages/8f/fc/5485cf0b0bb38da31d1d292160a4d123b5977841ddc1122c671a30b76cfd/pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d", size = 1826929 }, - { url = "https://files.pythonhosted.org/packages/a1/ff/fb1284a210e13a5f34c639efc54d51da136074ffbe25ec0c279cf9fbb1c4/pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c", size = 1980591 }, - { url = "https://files.pythonhosted.org/packages/f1/14/77c1887a182d05af74f6aeac7b740da3a74155d3093ccc7ee10b900cc6b5/pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27", size = 1981326 }, - { url = "https://files.pythonhosted.org/packages/06/aa/6f1b2747f811a9c66b5ef39d7f02fbb200479784c75e98290d70004b1253/pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f", size = 1989205 }, - { url = "https://files.pythonhosted.org/packages/7a/d2/8ce2b074d6835f3c88d85f6d8a399790043e9fdb3d0e43455e72d19df8cc/pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed", size = 2079616 }, - { url = "https://files.pythonhosted.org/packages/65/71/af01033d4e58484c3db1e5d13e751ba5e3d6b87cc3368533df4c50932c8b/pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f", size = 2133265 }, - { url = "https://files.pythonhosted.org/packages/33/72/f881b5e18fbb67cf2fb4ab253660de3c6899dbb2dba409d0b757e3559e3d/pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c", size = 2001864 }, - { url = "https://files.pythonhosted.org/packages/85/3e/f6f75ba36678fee11dd07a7729e9ed172ecf31e3f50a5d636e9605eee2af/pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5fde892e6c697ce3e30c61b239330fc5d569a71fefd4eb6512fc6caec9dd9e2f", size = 1894250 }, - { url = "https://files.pythonhosted.org/packages/d3/2d/a40578918e2eb5b4ee0d206a4fb6c4040c2bf14e28d29fba9bd7e7659d16/pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:816f5aa087094099fff7edabb5e01cc370eb21aa1a1d44fe2d2aefdfb5599b31", size = 1772035 }, - { url = "https://files.pythonhosted.org/packages/7f/ee/0377e9f4ca5a47e8885f670a65c0a647ddf9ce98d50bf7547cf8e1ee5771/pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c10c309e18e443ddb108f0ef64e8729363adbfd92d6d57beec680f6261556f3", size = 1827025 }, - { url = "https://files.pythonhosted.org/packages/fe/0b/a24d9ef762d05bebdfafd6d5d176b990728fa9ec8ea7b6040d6fb5f3caaa/pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98476c98b02c8e9b2eec76ac4156fd006628b1b2d0ef27e548ffa978393fd154", size = 1980927 }, - { url = "https://files.pythonhosted.org/packages/00/bd/deadc1722eb7dfdf787a3bbcd32eabbdcc36931fd48671a850e1b9f2cd77/pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3027001c28434e7ca5a6e1e527487051136aa81803ac812be51802150d880dd", size = 1980918 }, - { url = "https://files.pythonhosted.org/packages/f0/05/5d09d0b0e92053d538927308ea1d35cb25ab543d9c3e2eb2d7653bc73690/pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7699b1df36a48169cdebda7ab5a2bac265204003f153b4bd17276153d997670a", size = 1989990 }, - { url = "https://files.pythonhosted.org/packages/5b/7e/f7191346d1c3ac66049f618ee331359f8552a8b68a2daf916003c30b6dc8/pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1c39b07d90be6b48968ddc8c19e7585052088fd7ec8d568bb31ff64c70ae3c97", size = 2079871 }, - { url = "https://files.pythonhosted.org/packages/f3/65/2caf4f7ad65413a137d43cb9578c54d1abd3224be786ad840263c1bf9e0f/pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:46ccfe3032b3915586e469d4972973f893c0a2bb65669194a5bdea9bacc088c2", size = 2133569 }, - { url = "https://files.pythonhosted.org/packages/fd/ab/718d9a1c41bb8d3e0e04d15b68b8afc135f8fcf552705b62f226225065c7/pydantic_core-2.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:62ba45e21cf6571d7f716d903b5b7b6d2617e2d5d67c0923dc47b9d41369f840", size = 2002035 }, +sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/bc/fed5f74b5d802cf9a03e83f60f18864e90e3aed7223adaca5ffb7a8d8d64/pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa", size = 1895938 }, + { url = "https://files.pythonhosted.org/packages/71/2a/185aff24ce844e39abb8dd680f4e959f0006944f4a8a0ea372d9f9ae2e53/pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c", size = 1815684 }, + { url = "https://files.pythonhosted.org/packages/c3/43/fafabd3d94d159d4f1ed62e383e264f146a17dd4d48453319fd782e7979e/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a", size = 1829169 }, + { url = "https://files.pythonhosted.org/packages/a2/d1/f2dfe1a2a637ce6800b799aa086d079998959f6f1215eb4497966efd2274/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5", size = 1867227 }, + { url = "https://files.pythonhosted.org/packages/7d/39/e06fcbcc1c785daa3160ccf6c1c38fea31f5754b756e34b65f74e99780b5/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c", size = 2037695 }, + { url = "https://files.pythonhosted.org/packages/7a/67/61291ee98e07f0650eb756d44998214231f50751ba7e13f4f325d95249ab/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7", size = 2741662 }, + { url = "https://files.pythonhosted.org/packages/32/90/3b15e31b88ca39e9e626630b4c4a1f5a0dfd09076366f4219429e6786076/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a", size = 1993370 }, + { url = "https://files.pythonhosted.org/packages/ff/83/c06d333ee3a67e2e13e07794995c1535565132940715931c1c43bfc85b11/pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236", size = 1996813 }, + { url = "https://files.pythonhosted.org/packages/7c/f7/89be1c8deb6e22618a74f0ca0d933fdcb8baa254753b26b25ad3acff8f74/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962", size = 2005287 }, + { url = "https://files.pythonhosted.org/packages/b7/7d/8eb3e23206c00ef7feee17b83a4ffa0a623eb1a9d382e56e4aa46fd15ff2/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9", size = 2128414 }, + { url = "https://files.pythonhosted.org/packages/4e/99/fe80f3ff8dd71a3ea15763878d464476e6cb0a2db95ff1c5c554133b6b83/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af", size = 2155301 }, + { url = "https://files.pythonhosted.org/packages/2b/a3/e50460b9a5789ca1451b70d4f52546fa9e2b420ba3bfa6100105c0559238/pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", size = 1816685 }, + { url = "https://files.pythonhosted.org/packages/57/4c/a8838731cb0f2c2a39d3535376466de6049034d7b239c0202a64aaa05533/pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", size = 1982876 }, + { url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 }, + { url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 }, + { url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 }, + { url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 }, + { url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 }, + { url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 }, + { url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 }, + { url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 }, + { url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 }, + { url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 }, + { url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 }, + { url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 }, + { url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 }, + { url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 }, + { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, + { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, + { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, + { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, + { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, + { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, + { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, + { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, + { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, + { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, + { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, + { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, + { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, + { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, + { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, + { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, + { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, + { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, + { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, + { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, + { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, + { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, + { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, + { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, + { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, + { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, + { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, + { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, + { url = "https://files.pythonhosted.org/packages/43/53/13e9917fc69c0a4aea06fd63ed6a8d6cda9cf140ca9584d49c1650b0ef5e/pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506", size = 1899595 }, + { url = "https://files.pythonhosted.org/packages/f4/20/26c549249769ed84877f862f7bb93f89a6ee08b4bee1ed8781616b7fbb5e/pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320", size = 1775010 }, + { url = "https://files.pythonhosted.org/packages/35/eb/8234e05452d92d2b102ffa1b56d801c3567e628fdc63f02080fdfc68fd5e/pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145", size = 1830727 }, + { url = "https://files.pythonhosted.org/packages/8f/df/59f915c8b929d5f61e5a46accf748a87110ba145156f9326d1a7d28912b2/pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1", size = 1868393 }, + { url = "https://files.pythonhosted.org/packages/d5/52/81cf4071dca654d485c277c581db368b0c95b2b883f4d7b736ab54f72ddf/pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228", size = 2040300 }, + { url = "https://files.pythonhosted.org/packages/9c/00/05197ce1614f5c08d7a06e1d39d5d8e704dc81971b2719af134b844e2eaf/pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046", size = 2738785 }, + { url = "https://files.pythonhosted.org/packages/f7/a3/5f19bc495793546825ab160e530330c2afcee2281c02b5ffafd0b32ac05e/pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5", size = 1996493 }, + { url = "https://files.pythonhosted.org/packages/ed/e8/e0102c2ec153dc3eed88aea03990e1b06cfbca532916b8a48173245afe60/pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a", size = 1998544 }, + { url = "https://files.pythonhosted.org/packages/fb/a3/4be70845b555bd80aaee9f9812a7cf3df81550bce6dadb3cfee9c5d8421d/pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d", size = 2007449 }, + { url = "https://files.pythonhosted.org/packages/e3/9f/b779ed2480ba355c054e6d7ea77792467631d674b13d8257085a4bc7dcda/pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9", size = 2129460 }, + { url = "https://files.pythonhosted.org/packages/a0/f0/a6ab0681f6e95260c7fbf552874af7302f2ea37b459f9b7f00698f875492/pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da", size = 2159609 }, + { url = "https://files.pythonhosted.org/packages/8a/2b/e1059506795104349712fbca647b18b3f4a7fd541c099e6259717441e1e0/pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b", size = 1819886 }, + { url = "https://files.pythonhosted.org/packages/aa/6d/df49c17f024dfc58db0bacc7b03610058018dd2ea2eaf748ccbada4c3d06/pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad", size = 1980773 }, + { url = "https://files.pythonhosted.org/packages/27/97/3aef1ddb65c5ccd6eda9050036c956ff6ecbfe66cb7eb40f280f121a5bb0/pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993", size = 1896475 }, + { url = "https://files.pythonhosted.org/packages/ad/d3/5668da70e373c9904ed2f372cb52c0b996426f302e0dee2e65634c92007d/pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308", size = 1772279 }, + { url = "https://files.pythonhosted.org/packages/8a/9e/e44b8cb0edf04a2f0a1f6425a65ee089c1d6f9c4c2dcab0209127b6fdfc2/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4", size = 1829112 }, + { url = "https://files.pythonhosted.org/packages/1c/90/1160d7ac700102effe11616e8119e268770f2a2aa5afb935f3ee6832987d/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf", size = 1866780 }, + { url = "https://files.pythonhosted.org/packages/ee/33/13983426df09a36d22c15980008f8d9c77674fc319351813b5a2739b70f3/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76", size = 2037943 }, + { url = "https://files.pythonhosted.org/packages/01/d7/ced164e376f6747e9158c89988c293cd524ab8d215ae4e185e9929655d5c/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118", size = 2740492 }, + { url = "https://files.pythonhosted.org/packages/8b/1f/3dc6e769d5b7461040778816aab2b00422427bcaa4b56cc89e9c653b2605/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630", size = 1995714 }, + { url = "https://files.pythonhosted.org/packages/07/d7/a0bd09bc39283530b3f7c27033a814ef254ba3bd0b5cfd040b7abf1fe5da/pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54", size = 1997163 }, + { url = "https://files.pythonhosted.org/packages/2d/bb/2db4ad1762e1c5699d9b857eeb41959191980de6feb054e70f93085e1bcd/pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f", size = 2005217 }, + { url = "https://files.pythonhosted.org/packages/53/5f/23a5a3e7b8403f8dd8fc8a6f8b49f6b55c7d715b77dcf1f8ae919eeb5628/pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362", size = 2127899 }, + { url = "https://files.pythonhosted.org/packages/c2/ae/aa38bb8dd3d89c2f1d8362dd890ee8f3b967330821d03bbe08fa01ce3766/pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96", size = 2155726 }, + { url = "https://files.pythonhosted.org/packages/98/61/4f784608cc9e98f70839187117ce840480f768fed5d386f924074bf6213c/pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e", size = 1817219 }, + { url = "https://files.pythonhosted.org/packages/57/82/bb16a68e4a1a858bb3768c2c8f1ff8d8978014e16598f001ea29a25bf1d1/pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67", size = 1985382 }, + { url = "https://files.pythonhosted.org/packages/46/72/af70981a341500419e67d5cb45abe552a7c74b66326ac8877588488da1ac/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e", size = 1891159 }, + { url = "https://files.pythonhosted.org/packages/ad/3d/c5913cccdef93e0a6a95c2d057d2c2cba347815c845cda79ddd3c0f5e17d/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8", size = 1768331 }, + { url = "https://files.pythonhosted.org/packages/f6/f0/a3ae8fbee269e4934f14e2e0e00928f9346c5943174f2811193113e58252/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3", size = 1822467 }, + { url = "https://files.pythonhosted.org/packages/d7/7a/7bbf241a04e9f9ea24cd5874354a83526d639b02674648af3f350554276c/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f", size = 1979797 }, + { url = "https://files.pythonhosted.org/packages/4f/5f/4784c6107731f89e0005a92ecb8a2efeafdb55eb992b8e9d0a2be5199335/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133", size = 1987839 }, + { url = "https://files.pythonhosted.org/packages/6d/a7/61246562b651dff00de86a5f01b6e4befb518df314c54dec187a78d81c84/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc", size = 1998861 }, + { url = "https://files.pythonhosted.org/packages/86/aa/837821ecf0c022bbb74ca132e117c358321e72e7f9702d1b6a03758545e2/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50", size = 2116582 }, + { url = "https://files.pythonhosted.org/packages/81/b0/5e74656e95623cbaa0a6278d16cf15e10a51f6002e3ec126541e95c29ea3/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9", size = 2151985 }, + { url = "https://files.pythonhosted.org/packages/63/37/3e32eeb2a451fddaa3898e2163746b0cffbbdbb4740d38372db0490d67f3/pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151", size = 2004715 }, + { url = "https://files.pythonhosted.org/packages/29/0e/dcaea00c9dbd0348b723cae82b0e0c122e0fa2b43fa933e1622fd237a3ee/pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656", size = 1891733 }, + { url = "https://files.pythonhosted.org/packages/86/d3/e797bba8860ce650272bda6383a9d8cad1d1c9a75a640c9d0e848076f85e/pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278", size = 1768375 }, + { url = "https://files.pythonhosted.org/packages/41/f7/f847b15fb14978ca2b30262548f5fc4872b2724e90f116393eb69008299d/pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb", size = 1822307 }, + { url = "https://files.pythonhosted.org/packages/9c/63/ed80ec8255b587b2f108e514dc03eed1546cd00f0af281e699797f373f38/pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd", size = 1979971 }, + { url = "https://files.pythonhosted.org/packages/a9/6d/6d18308a45454a0de0e975d70171cadaf454bc7a0bf86b9c7688e313f0bb/pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc", size = 1987616 }, + { url = "https://files.pythonhosted.org/packages/82/8a/05f8780f2c1081b800a7ca54c1971e291c2d07d1a50fb23c7e4aef4ed403/pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b", size = 1998943 }, + { url = "https://files.pythonhosted.org/packages/5e/3e/fe5b6613d9e4c0038434396b46c5303f5ade871166900b357ada4766c5b7/pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b", size = 2116654 }, + { url = "https://files.pythonhosted.org/packages/db/ad/28869f58938fad8cc84739c4e592989730bfb69b7c90a8fff138dff18e1e/pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2", size = 2152292 }, + { url = "https://files.pythonhosted.org/packages/a1/0c/c5c5cd3689c32ed1fe8c5d234b079c12c281c051759770c05b8bed6412b5/pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35", size = 2004961 }, ] [[package]]