Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ jobs:
with:
python-version: "3.10"
poetry-version: "2.2.1"
code-checks-python-versions: '["3.10", "3.11", "3.12", "3.13"]'
code-checks-python-versions: '["3.10", "3.11", "3.12", "3.13", "3.14"]'
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ jobs:
promote: true # HACK: skip RC phase, project does not require stabilization period before releasing stable versions
python-version: "3.10"
poetry-version: "2.2.1"
code-checks-python-versions: '["3.10", "3.11", "3.12", "3.13"]'
code-checks-python-versions: '["3.10", "3.11", "3.12", "3.13", "3.14"]'
secrets: inherit
2 changes: 1 addition & 1 deletion .ort.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@ resolutions:
- message: ".*PyPI::starlette:0\\.49\\.1.*"
reason: "CANT_FIX_EXCEPTION"
comment: "BSD 3-Clause New or Revised License: https://github.com/Kludex/starlette/blob/0.49.1/LICENSE.md"
- message: ".*PyPI::(typing-extensions|urllib3).*"
- message: ".*PyPI::(typing-extensions|typing-inspection|urllib3|pydantic).*"
reason: "CANT_FIX_EXCEPTION"
comment: "ORT cannot pick up license information"
14 changes: 10 additions & 4 deletions aidial_sdk/_pydantic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

# ruff: noqa: F401

import sys
from collections.abc import Mapping
from typing import TYPE_CHECKING

Expand All @@ -19,7 +20,15 @@

INSTALLED_PYDANTIC_V2 = VERSION.startswith("2.")
USE_PYDANTIC_V2 = env_bool("PYDANTIC_V2", False)
PYDANTIC_V2 = INSTALLED_PYDANTIC_V2 and USE_PYDANTIC_V2

# Pydantic V1 (including the pydantic.v1 shim in pydantic>=2) is not
# compatible with Python 3.14+. Force V2 mode automatically when running
# on Python 3.14+ so that users don't have to set PYDANTIC_V2=1 manually.
_PYTHON_314_OR_LATER = sys.version_info >= (3, 14)

PYDANTIC_V2 = INSTALLED_PYDANTIC_V2 and (
USE_PYDANTIC_V2 or _PYTHON_314_OR_LATER
)

if TYPE_CHECKING:
from pydantic import (
Expand All @@ -37,7 +46,6 @@
from pydantic import field_validator as validator
from pydantic._internal._model_construction import ModelMetaclass
from pydantic.fields import FieldInfo
from pydantic.v1.validators import make_literal_validator

HeadersType = Mapping[str, str]
else:
Expand All @@ -60,7 +68,6 @@
from pydantic import field_validator as validator
from pydantic._internal._model_construction import ModelMetaclass
from pydantic.fields import FieldInfo
from pydantic.v1.validators import make_literal_validator

# In Pydantic V2, skip validation to preserve the case-insensitive MutableHeaders object
HeadersType = Annotated[Mapping[str, str], SkipValidation]
Expand All @@ -82,7 +89,6 @@
from pydantic.main import ModelMetaclass
from pydantic.v1 import ValidationError, root_validator
from pydantic.v1.fields import FieldInfo
from pydantic.v1.validators import make_literal_validator

def _fail(*args, **kwargs):
raise ImportError("ConfigDict is only supported in Pydantic v2")
Expand Down
12 changes: 7 additions & 5 deletions aidial_sdk/_pydantic/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import aidial_sdk._pydantic as pydantic
from aidial_sdk._pydantic import PYDANTIC_V2

_IncEx = set[int] | set[str] | dict[int, Any] | dict[str, Any] | None
_IncEx = set[int] | set[str] | Mapping[int, Any] | Mapping[str, Any]


class BaseModel(pydantic.BaseModel):
Expand All @@ -30,16 +30,17 @@ def model_dump(
self,
*,
mode: Literal["json", "python"] | str = "python",
include: _IncEx = None,
exclude: _IncEx = None,
by_alias: bool = False,
include: _IncEx | None = None,
exclude: _IncEx | None = None,
by_alias: bool | None = None,
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
round_trip: bool = False,
warnings: bool | Literal["none", "warn", "error"] = True,
context: dict[str, Any] | None = None,
serialize_as_any: bool = False,
**kwargs,
) -> dict[str, Any]:
if mode not in {"json", "python"}:
raise ValueError("mode must be either 'json' or 'python'")
Expand All @@ -56,10 +57,11 @@ def model_dump(
dumped = super().dict( # pyright: ignore[reportDeprecated]
include=include,
exclude=exclude,
by_alias=by_alias,
by_alias=bool(by_alias),
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
**kwargs,
)

return (
Expand Down
20 changes: 18 additions & 2 deletions aidial_sdk/chat_completion/form.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
BaseModel,
FieldInfo,
ModelMetaclass,
make_literal_validator,
validator,
)
from aidial_sdk._pydantic._model_config import ModelConfigWrapper
Expand Down Expand Up @@ -191,9 +190,26 @@ def _create_class(cls: type[_Model]) -> type[_Model]:
return _create_class


def _make_literal_validator(
literal_type: Any,
) -> Callable[[Any], Any]:
"""Pure-Python equivalent of pydantic.v1's make_literal_validator."""
permitted_choices = get_args(literal_type)
allowed_choices = {v: v for v in permitted_choices}

def literal_validator(v: Any) -> Any:
try:
return allowed_choices[v]
except (KeyError, TypeError):
cs = ", ".join(repr(c) for c in permitted_choices)
raise ValueError(f"unexpected value; permitted: {cs}")

return literal_validator


def _create_field_validator(field_name: str, enum_values: Sequence[Any]):
literal_type = Literal[enum_values]
literal_validator = make_literal_validator(literal_type)
literal_validator = _make_literal_validator(literal_type)

if PYDANTIC_V2:
return validator(field_name)(literal_validator)
Expand Down
2 changes: 1 addition & 1 deletion aidial_sdk/deployment/rate.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@


class RateRequest(FromRequestDeploymentMixin):
response_id: StrictStr = Field(None, alias="responseId")
response_id: StrictStr | None = Field(None, alias="responseId")
rate: bool = False
29 changes: 18 additions & 11 deletions aidial_sdk/pydantic/v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ def Field(*args, **kwargs) -> Any: # type: ignore
raise ImportError("The Field helper is only supported in Pydantic v2")

else:
import re

import annotated_types
from pydantic import types
from pydantic.aliases import AliasChoices, AliasPath
from pydantic.config import JsonDict
from pydantic.fields import Field as PydanticField
from pydantic_core import PydanticUndefined

Expand All @@ -35,7 +40,9 @@ def Field(*args, **kwargs) -> Any: # type: ignore
def Field(
default: Any = PydanticUndefined,
*,
default_factory: Callable[[], Any] | None = _Unset,
default_factory: Callable[[], Any]
| Callable[[dict[str, Any]], Any]
| None = _Unset,
alias: str | None = _Unset,
alias_priority: int | None = _Unset,
validation_alias: str | AliasPath | AliasChoices | None = _Unset,
Expand All @@ -44,21 +51,21 @@ def Field(
description: str | None = _Unset,
examples: list[Any] | None = _Unset,
exclude: bool | None = _Unset,
discriminator: str | None = _Unset,
json_schema_extra: dict[str, Any]
| Callable[[dict[str, Any]], None]
discriminator: str | types.Discriminator | None = _Unset,
json_schema_extra: JsonDict
| Callable[[JsonDict], None]
| None = _Unset,
frozen: bool | None = _Unset,
validate_default: bool | None = _Unset,
repr: bool = _Unset,
init_var: bool | None = _Unset,
kw_only: bool | None = _Unset,
pattern: str | None = _Unset,
pattern: str | re.Pattern[str] | None = _Unset,
strict: bool | None = _Unset,
gt: float | None = _Unset,
ge: float | None = _Unset,
lt: float | None = _Unset,
le: float | None = _Unset,
gt: annotated_types.SupportsGt | None = _Unset,
ge: annotated_types.SupportsGe | None = _Unset,
lt: annotated_types.SupportsLt | None = _Unset,
le: annotated_types.SupportsLe | None = _Unset,
multiple_of: float | None = _Unset,
allow_inf_nan: bool | None = _Unset,
max_digits: int | None = _Unset,
Expand All @@ -77,15 +84,15 @@ def Field(
else:

def _extra(x: dict[str, Any]) -> None:
json_schema_extra({**x, "buttons": buttons})
json_schema_extra({**x, "buttons": buttons}) # type: ignore

new_extra = _extra
else:
new_extra = json_schema_extra

return PydanticField(
default=default,
default_factory=default_factory,
default_factory=default_factory, # type: ignore
alias=alias,
alias_priority=alias_priority,
validation_alias=validation_alias,
Expand Down
35 changes: 24 additions & 11 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,25 +35,38 @@ class UsePydanticV2(Enum):
NO = "0"


@nox.session(python=["3.10", "3.11", "3.12", "3.13"])
@nox.session(python=["3.10", "3.11", "3.12", "3.13", "3.14"])
# Testing against earliest and latest supported versions of the dependencies
@nox.parametrize(
"pydantic",
"pydantic_info",
[
("1.10.17", UsePydanticV2.NO),
("2.8.2", UsePydanticV2.NO),
("2.8.2", UsePydanticV2.YES),
("2.13.1", UsePydanticV2.NO),
("2.13.1", UsePydanticV2.YES),
("1.10.17", UsePydanticV2.NO, False),
("2.8.2", UsePydanticV2.NO, False),
("2.8.2", UsePydanticV2.YES, False),
# Python 3.14 is supported since Pydantic v2.12
("2.13.1", UsePydanticV2.NO, True),
("2.13.1", UsePydanticV2.YES, True),
],
)
@nox.parametrize("httpx", ["0.25.0", "0.27.0"])
@nox.parametrize("httpx_version", ["0.25.0", "0.27.0"])
def test(
session: nox.Session, pydantic: tuple[str, UsePydanticV2], httpx: str
session: nox.Session,
pydantic_info: tuple[str, UsePydanticV2, bool],
httpx_version: str,
) -> None:
"""Runs tests"""
pydantic_version, use_pydantic_v2, python_314_supported = pydantic_info

if session.python == "3.14":
if not python_314_supported:
session.skip("Python 3.14 is supported since Pydantic v2.12")
if httpx_version == "0.25.0":
session.skip("Earlier versions of httpx do not support Python 3.14")

session.run("poetry", "install", external=True)
session.install(f"pydantic=={pydantic[0]}", f"httpx=={httpx}")
session.install(f"pydantic=={pydantic_version}", f"httpx=={httpx_version}")
session.run(
"pytest", *session.posargs, env={"PYDANTIC_V2": str(pydantic[1].value)}
"pytest",
*session.posargs,
env={"PYDANTIC_V2": str(use_pydantic_v2.value)},
)
Loading
Loading