pydantic-takibiapi 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 - present, pydantic-takibiapi Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.4
2
+ Name: pydantic-takibiapi
3
+ Version: 0.1.0
4
+ Summary: takibiapi with pydantic models as the schemas, checked strictly
5
+ Keywords: openapi,asgi,pydantic,rest,api,validation
6
+ Author: nakita628
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
17
+ Classifier: Typing :: Typed
18
+ Requires-Dist: takibiapi>=0.1.0,<0.2
19
+ Requires-Dist: pydantic>=2.11
20
+ Requires-Python: >=3.12
21
+ Project-URL: Repository, https://github.com/nakita628/takibi
22
+ Project-URL: Issues, https://github.com/nakita628/takibi/issues
23
+ Description-Content-Type: text/markdown
24
+
25
+ # pydantic-takibiapi
26
+
27
+ [TakibiAPI](../takibiapi) with [pydantic](https://pydantic.dev/) models as the schemas, checked strictly: constraints, unknown keys and responses included.
28
+
29
+ ```sh
30
+ pip install pydantic-takibiapi
31
+ ```
32
+
33
+ ## Quick start
34
+
35
+ ```python
36
+ from pydantic_takibiapi import Context, Model, TakibiAPI, createRoute
37
+
38
+
39
+ class Message(Model):
40
+ message: str
41
+
42
+
43
+ class getTakibiRoute(
44
+ createRoute(
45
+ {
46
+ "method": "get",
47
+ "path": "/takibi",
48
+ "responses": {200: {"description": "takibišŸ”„", "content": {"application/json": {"schema": Message}}}},
49
+ }
50
+ )
51
+ ):
52
+ """Say takibišŸ”„."""
53
+
54
+
55
+ def getTakibiRouteHandler(c: Context[getTakibiRoute]):
56
+ return c.json(Message(message="takibišŸ”„"))
57
+
58
+
59
+ app = TakibiAPI()
60
+ app.openapi(getTakibiRoute, getTakibiRouteHandler)
61
+ app.doc("/doc", {"openapi": "3.1.0", "info": {"title": "takibi API", "version": "1.0.0"}})
62
+ ```
63
+
64
+ ```bash
65
+ granian --interface rsgi myapp:app # or: uvicorn myapp:app
66
+ curl localhost:8000/takibi # {"message":"takibišŸ”„"}
67
+ ```
68
+
69
+ Import `TakibiAPI` from `pydantic_takibiapi`; everything else works as in Takibi.
70
+
71
+ ## Models
72
+
73
+ Every model, nested ones included, must be strict: subclass `Model` (a `BaseModel` with `strict=True, extra="forbid"`). `app.openapi` raises `TypeError` otherwise. `Field` constraints, aliases and validators work as in pydantic.
74
+
75
+ ## Checks
76
+
77
+ - **Requests**: `c.req.valid(...)` gives a checked model instance. Path, query, header and cookie text is converted (`"7"` → `7`); JSON bodies are strict (`"1"` is not an `int`) and reject unknown keys. A failure gets **422** with pydantic's error types as `code`.
78
+ - **Responses**: `c.json(value, status)` checks `value` against the model declared for that status. A mismatch raises `ResponseValidationError` and answers **500** (details in the log, not in the response).
79
+
80
+ ```python
81
+ app.onError(lambda c: c.json({"error": str(c.error)}, 500)) # replaces the 500, ResponseValidationError included
82
+ ```
83
+
84
+ ## OpenAPI
85
+
86
+ Schemas come from pydantic's JSON Schema; every model is a component (`Name-Input` / `Name-Output` when reading and writing differ). `"3.0.x"` documents are converted from it.
87
+
88
+ ## Development
89
+
90
+ From the repository root: `bazel test //pypi/pydantic-takibiapi:test`. See the root README.
91
+
92
+ ## License
93
+
94
+ Distributed under the MIT License. See [LICENSE](https://github.com/nakita628/takibi/blob/main/pypi/pydantic-takibiapi/LICENSE) for more information.
@@ -0,0 +1,70 @@
1
+ # pydantic-takibiapi
2
+
3
+ [TakibiAPI](../takibiapi) with [pydantic](https://pydantic.dev/) models as the schemas, checked strictly: constraints, unknown keys and responses included.
4
+
5
+ ```sh
6
+ pip install pydantic-takibiapi
7
+ ```
8
+
9
+ ## Quick start
10
+
11
+ ```python
12
+ from pydantic_takibiapi import Context, Model, TakibiAPI, createRoute
13
+
14
+
15
+ class Message(Model):
16
+ message: str
17
+
18
+
19
+ class getTakibiRoute(
20
+ createRoute(
21
+ {
22
+ "method": "get",
23
+ "path": "/takibi",
24
+ "responses": {200: {"description": "takibišŸ”„", "content": {"application/json": {"schema": Message}}}},
25
+ }
26
+ )
27
+ ):
28
+ """Say takibišŸ”„."""
29
+
30
+
31
+ def getTakibiRouteHandler(c: Context[getTakibiRoute]):
32
+ return c.json(Message(message="takibišŸ”„"))
33
+
34
+
35
+ app = TakibiAPI()
36
+ app.openapi(getTakibiRoute, getTakibiRouteHandler)
37
+ app.doc("/doc", {"openapi": "3.1.0", "info": {"title": "takibi API", "version": "1.0.0"}})
38
+ ```
39
+
40
+ ```bash
41
+ granian --interface rsgi myapp:app # or: uvicorn myapp:app
42
+ curl localhost:8000/takibi # {"message":"takibišŸ”„"}
43
+ ```
44
+
45
+ Import `TakibiAPI` from `pydantic_takibiapi`; everything else works as in Takibi.
46
+
47
+ ## Models
48
+
49
+ Every model, nested ones included, must be strict: subclass `Model` (a `BaseModel` with `strict=True, extra="forbid"`). `app.openapi` raises `TypeError` otherwise. `Field` constraints, aliases and validators work as in pydantic.
50
+
51
+ ## Checks
52
+
53
+ - **Requests**: `c.req.valid(...)` gives a checked model instance. Path, query, header and cookie text is converted (`"7"` → `7`); JSON bodies are strict (`"1"` is not an `int`) and reject unknown keys. A failure gets **422** with pydantic's error types as `code`.
54
+ - **Responses**: `c.json(value, status)` checks `value` against the model declared for that status. A mismatch raises `ResponseValidationError` and answers **500** (details in the log, not in the response).
55
+
56
+ ```python
57
+ app.onError(lambda c: c.json({"error": str(c.error)}, 500)) # replaces the 500, ResponseValidationError included
58
+ ```
59
+
60
+ ## OpenAPI
61
+
62
+ Schemas come from pydantic's JSON Schema; every model is a component (`Name-Input` / `Name-Output` when reading and writing differ). `"3.0.x"` documents are converted from it.
63
+
64
+ ## Development
65
+
66
+ From the repository root: `bazel test //pypi/pydantic-takibiapi:test`. See the root README.
67
+
68
+ ## License
69
+
70
+ Distributed under the MIT License. See [LICENSE](https://github.com/nakita628/takibi/blob/main/pypi/pydantic-takibiapi/LICENSE) for more information.
@@ -0,0 +1,49 @@
1
+ [project]
2
+ name = "pydantic-takibiapi"
3
+ version = "0.1.0"
4
+ description = "takibiapi with pydantic models as the schemas, checked strictly"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ keywords = [
10
+ "openapi",
11
+ "asgi",
12
+ "pydantic",
13
+ "rest",
14
+ "api",
15
+ "validation",
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 3 - Alpha",
19
+ "Intended Audience :: Developers",
20
+ "Operating System :: OS Independent",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Programming Language :: Python :: 3.14",
25
+ "Topic :: Internet :: WWW/HTTP :: HTTP Servers",
26
+ "Typing :: Typed",
27
+ ]
28
+ dependencies = [
29
+ "takibiapi>=0.1.0,<0.2",
30
+ "pydantic>=2.11",
31
+ ]
32
+
33
+ [[project.authors]]
34
+ name = "nakita628"
35
+
36
+ [project.urls]
37
+ Repository = "https://github.com/nakita628/takibi"
38
+ Issues = "https://github.com/nakita628/takibi/issues"
39
+
40
+ [build-system]
41
+ requires = ["uv_build>=0.8,<0.14"]
42
+ build-backend = "uv_build"
43
+
44
+ [tool.uv.build-backend]
45
+ module-root = "python"
46
+ module-name = "pydantic_takibiapi"
47
+
48
+ [tool.uv.sources.takibiapi]
49
+ workspace = true
@@ -0,0 +1,37 @@
1
+ [project]
2
+ name = "pydantic-takibiapi"
3
+ version = "0.1.0"
4
+ description = "takibiapi with pydantic models as the schemas, checked strictly"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ authors = [{ name = "nakita628" }]
10
+ keywords = ["openapi", "asgi", "pydantic", "rest", "api", "validation"]
11
+ classifiers = [
12
+ "Development Status :: 3 - Alpha",
13
+ "Intended Audience :: Developers",
14
+ "Operating System :: OS Independent",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Programming Language :: Python :: 3.13",
18
+ "Programming Language :: Python :: 3.14",
19
+ "Topic :: Internet :: WWW/HTTP :: HTTP Servers",
20
+ "Typing :: Typed",
21
+ ]
22
+ dependencies = ["takibiapi>=0.1.0,<0.2", "pydantic>=2.11"]
23
+
24
+ [project.urls]
25
+ Repository = "https://github.com/nakita628/takibi"
26
+ Issues = "https://github.com/nakita628/takibi/issues"
27
+
28
+ [build-system]
29
+ requires = ["uv_build>=0.8,<0.14"]
30
+ build-backend = "uv_build"
31
+
32
+ [tool.uv.build-backend]
33
+ module-root = "python"
34
+ module-name = "pydantic_takibiapi"
35
+
36
+ [tool.uv.sources]
37
+ takibiapi = { workspace = true }
@@ -0,0 +1,52 @@
1
+ """pydantic-takibiapi: takibi with pydantic models as the schemas, checked strictly.
2
+
3
+ from pydantic_takibiapi import Context, Model, Response, TakibiAPI, createRoute
4
+
5
+ Schemas are :class:`Model` subclasses (pydantic models with ``strict=True`` and
6
+ ``extra="forbid"``). Requests are checked with pydantic before the handler runs, ``Field``
7
+ constraints included; responses are checked against the declared model; the OpenAPI document
8
+ comes from pydantic's JSON Schema.
9
+ """
10
+
11
+ from pydantic_takibiapi.app import TakibiAPI
12
+ from pydantic_takibiapi.model import Model, ValidationErrorItem, ValidationProblem
13
+ from pydantic_takibiapi.schemas import PydanticSchemas
14
+ from takibiapi import (
15
+ Context,
16
+ Middleware,
17
+ Next,
18
+ OpenAPIConfig,
19
+ Request,
20
+ RequestConfig,
21
+ Response,
22
+ ResponseConfig,
23
+ ResponseValidationError,
24
+ Route,
25
+ RouteConfig,
26
+ ValidationError,
27
+ createRoute,
28
+ problem,
29
+ )
30
+
31
+ __all__ = [
32
+ "Context",
33
+ "Middleware",
34
+ "Model",
35
+ "Next",
36
+ "OpenAPIConfig",
37
+ "PydanticSchemas",
38
+ "Request",
39
+ "RequestConfig",
40
+ "Response",
41
+ "ResponseConfig",
42
+ "ResponseValidationError",
43
+ "Route",
44
+ "RouteConfig",
45
+ "TakibiAPI",
46
+ "ValidationError",
47
+ "ValidationErrorItem",
48
+ "ValidationProblem",
49
+ "createRoute",
50
+ "problem",
51
+ ]
52
+ __version__ = "0.1.0"
@@ -0,0 +1,22 @@
1
+ """``TakibiAPI``: takibi with pydantic models as its schemas."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import takibiapi
8
+
9
+ from .schemas import PydanticSchemas
10
+
11
+
12
+ class TakibiAPI(takibiapi.TakibiAPI):
13
+ """``takibiapi.TakibiAPI`` whose routes are described with pydantic models (``Model``): requests
14
+ are checked with pydantic, constraints included, before the handler runs (422 when they do
15
+ not match), and ``c.json(...)`` checks the response against the declared model (500 when the
16
+ handler breaks its own contract). Everything else (routing, middleware, ASGI / RSGI, the
17
+ OpenAPI document) is takibi's."""
18
+
19
+ schemas: Any = PydanticSchemas()
20
+
21
+
22
+ __all__ = ["TakibiAPI"]
@@ -0,0 +1,41 @@
1
+ """``Model``: the base of every schema, a pydantic model with the strict settings takibi requires."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field
8
+
9
+
10
+ class Model(BaseModel):
11
+ """A strict pydantic model: no type conversion in JSON (``"1"`` is not an ``int``; parameters
12
+ from the path, query, headers and cookies are still read from their text), and no keys the
13
+ model does not declare.
14
+
15
+ class Todo(Model):
16
+ id: int = Field(ge=1)
17
+ title: str = Field(min_length=1, max_length=100)
18
+ """
19
+
20
+ model_config = ConfigDict(strict=True, extra="forbid")
21
+
22
+
23
+ class ValidationErrorItem(Model):
24
+ in_: Annotated[str, Field(alias="in", description="Where the value was sent: path, query, header, cookie or body")]
25
+ path: Annotated[str, Field(description="Dot-separated path inside that part", examples=["title"])]
26
+ code: Annotated[str, Field(description="The pydantic error type", examples=["string_too_short"])]
27
+ message: Annotated[str, Field(examples=["String should have at least 1 character"])]
28
+
29
+
30
+ class ValidationProblem(Model):
31
+ """RFC 9457 Problem Details for a request part that does not match its schema."""
32
+
33
+ type: Annotated[str, Field(examples=["/problems/validation-error"])]
34
+ title: Annotated[str, Field(examples=["Unprocessable Content"])]
35
+ status: Annotated[int, Field(examples=[422])]
36
+ detail: str
37
+ instance: Annotated[str, Field(description="The request path")]
38
+ errors: list[ValidationErrorItem]
39
+
40
+
41
+ __all__ = ["Model", "ValidationErrorItem", "ValidationProblem"]
@@ -0,0 +1,241 @@
1
+ """``PydanticSchemas``: pydantic models as takibi's schemas (see ``takibiapi.schemas``).
2
+
3
+ Requests are checked with pydantic, constraints included (``Field(ge=1, min_length=1)``):
4
+ parameters from the path, query, headers and cookies are read from their text, JSON bodies
5
+ strictly (``"1"`` is not an ``int``) and without undeclared keys. Responses are checked against
6
+ the model the route declares for the status before they are written. Every model must be strict
7
+ (``Model``, or ``strict=True`` and ``extra="forbid"``).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import enum
13
+ import functools
14
+ import types
15
+ import typing
16
+ from collections.abc import Iterator
17
+ from typing import Any, Literal, Union, get_args, get_origin
18
+
19
+ import pydantic
20
+ from pydantic import BaseModel, TypeAdapter
21
+ from pydantic.fields import FieldInfo
22
+ from pydantic.json_schema import JsonSchemaMode
23
+
24
+ from takibiapi import ValidationError
25
+
26
+ from .model import ValidationProblem
27
+
28
+ REF_TEMPLATE = "#/components/schemas/{model}"
29
+
30
+
31
+ def _issues(exc: pydantic.ValidationError) -> list[dict[str, Any]]:
32
+ return [{"path": list(e["loc"]), "code": e["type"], "message": e["msg"]} for e in exc.errors(include_url=False)]
33
+
34
+
35
+ def _strict_error(tp: Any) -> str | None:
36
+ """Why ``tp`` is not strict enough, or None: every model must be strict and forbid extra keys,
37
+ and plain dataclasses / TypedDicts (whose settings cannot say so) are not allowed."""
38
+ seen: set[int] = set()
39
+
40
+ def walk(tp: Any) -> Iterator[str]:
41
+ origin = get_origin(tp)
42
+ if origin is typing.Annotated:
43
+ yield from walk(get_args(tp)[0])
44
+ return
45
+ if isinstance(tp, type) and issubclass(tp, BaseModel):
46
+ if id(tp) in seen:
47
+ return
48
+ seen.add(id(tp))
49
+ config = tp.model_config
50
+ if config.get("strict") is not True or config.get("extra") != "forbid":
51
+ yield f"{tp.__name__} is not strict (subclass pydantic_takibiapi.Model)"
52
+ for field in tp.model_fields.values():
53
+ yield from walk(field.annotation)
54
+ return
55
+ if typing.is_typeddict(tp) or (isinstance(tp, type) and hasattr(tp, "__dataclass_fields__")):
56
+ yield f"{tp.__name__} is a {'TypedDict' if typing.is_typeddict(tp) else 'dataclass'} (use a Model)"
57
+ return
58
+ for arg in get_args(tp):
59
+ if not isinstance(arg, (str, int, float, bool, enum.Enum)) and arg is not Ellipsis and arg is not None:
60
+ yield from walk(arg)
61
+
62
+ return next(walk(tp), None)
63
+
64
+
65
+ def _strict_adapter(tp: Any) -> TypeAdapter[Any]:
66
+ error = _strict_error(tp)
67
+ if error is not None:
68
+ raise TypeError(error)
69
+ return TypeAdapter(tp)
70
+
71
+
72
+ def _repeats(annotation: Any) -> bool:
73
+ """Whether a query field collects every value of its key (``list[str]``, ``list[int] | None``)."""
74
+ args = get_args(annotation) if get_origin(annotation) in (Union, types.UnionType) else (annotation,)
75
+ return any(a in (list, tuple, set, frozenset) or get_origin(a) in (list, tuple, set, frozenset) for a in args)
76
+
77
+
78
+ class _Checker:
79
+ __slots__ = ("_adapter", "names")
80
+
81
+ def __init__(self, tp: Any, target: str) -> None:
82
+ self._adapter = _strict_adapter(tp)
83
+ if target == "json":
84
+ self.names: list[tuple[str, bool]] = []
85
+ return
86
+ if not (isinstance(tp, type) and issubclass(tp, BaseModel)):
87
+ raise TypeError(f"request {target} parts must be a Model, got {tp!r}")
88
+ self.names = [(f.alias or name, _repeats(f.annotation)) for name, f in tp.model_fields.items()]
89
+
90
+ def decode(self, value: Any, *, coerce: bool) -> Any:
91
+ try: # parameters arrive as text: convert ("7" -> 7) even though the model is strict
92
+ return self._adapter.validate_python(value, strict=False if coerce else None)
93
+ except pydantic.ValidationError as exc:
94
+ raise ValidationError(_issues(exc)) from None
95
+
96
+ def decode_json(self, data: bytes) -> Any:
97
+ try:
98
+ return self._adapter.validate_json(data)
99
+ except pydantic.ValidationError as exc:
100
+ errors = exc.errors(include_url=False)
101
+ if errors and errors[0]["type"] == "json_invalid":
102
+ raise ValueError(errors[0]["msg"]) from None
103
+ raise ValidationError(_issues(exc)) from None
104
+
105
+
106
+ class _Encoder:
107
+ __slots__ = ("_adapter",)
108
+
109
+ def __init__(self, tp: Any) -> None:
110
+ self._adapter = _strict_adapter(tp)
111
+
112
+ def encode_json(self, value: Any) -> bytes:
113
+ try:
114
+ checked = self._adapter.validate_python(value)
115
+ except pydantic.ValidationError as exc:
116
+ raise ValidationError(_issues(exc)) from None
117
+ return self._adapter.dump_json(checked)
118
+
119
+
120
+ class _DocBuilder:
121
+ """Collects every schema the document needs and generates them in one pydantic pass, so named
122
+ models are written once to ``components.schemas`` (with ``-Input`` / ``-Output`` variants when
123
+ a model reads and writes differently). ``of`` returns a placeholder dict, filled in place when
124
+ ``components`` is read (``build_document`` reads it last)."""
125
+
126
+ def __init__(self, openapi31: bool) -> None:
127
+ self.openapi31 = openapi31
128
+ self._pending: list[tuple[dict[str, Any], JsonSchemaMode, TypeAdapter[Any]]] = []
129
+ self._components: dict[str, Any] | None = None
130
+
131
+ def of(self, tp: Any, io: Literal["input", "output"] = "input") -> dict[str, Any]:
132
+ placeholder: dict[str, Any] = {}
133
+ mode: JsonSchemaMode = "validation" if io == "input" else "serialization"
134
+ self._pending.append((placeholder, mode, _adapter_for_doc(tp)))
135
+ return placeholder
136
+
137
+ def parameters(self, tp: Any, location: str) -> list[dict[str, Any]]:
138
+ if not (isinstance(tp, type) and issubclass(tp, BaseModel)):
139
+ raise TypeError(f"{location} parameters must be a Model, got {tp!r}")
140
+ out: list[dict[str, Any]] = []
141
+ for name, field in tp.model_fields.items():
142
+ param: dict[str, Any] = {
143
+ "name": field.alias or name,
144
+ "in": location,
145
+ "required": location == "path" or field.is_required(),
146
+ "schema": self.of(typing.Annotated[field.annotation, _without_alias(field)]),
147
+ }
148
+ if field.description:
149
+ param["description"] = field.description
150
+ out.append(param)
151
+ return out
152
+
153
+ @property
154
+ def components(self) -> dict[str, Any]:
155
+ if self._components is None:
156
+ inputs: list[tuple[int, JsonSchemaMode, TypeAdapter[Any]]] = [
157
+ (i, mode, adapter) for i, (_, mode, adapter) in enumerate(self._pending)
158
+ ]
159
+ schemas, definitions = TypeAdapter.json_schemas(inputs, ref_template=REF_TEMPLATE)
160
+ for i, (placeholder, mode, _) in enumerate(self._pending):
161
+ placeholder.update(self._convert(_untitled(schemas[(i, mode)])))
162
+ defs = definitions.get("$defs", {})
163
+ self._components = {name: self._convert(schema) for name, schema in defs.items()}
164
+ return self._components
165
+
166
+ def _convert(self, schema: Any) -> Any:
167
+ return schema if self.openapi31 else to_openapi30(schema)
168
+
169
+
170
+ @functools.cache
171
+ def _adapter_for_doc(tp: Any) -> TypeAdapter[Any]:
172
+ return TypeAdapter(tp)
173
+
174
+
175
+ def _without_alias(field: FieldInfo) -> FieldInfo:
176
+ """The field's constraints and description, for a parameter's own schema."""
177
+ return FieldInfo.merge_field_infos(field, alias=None, validation_alias=None, serialization_alias=None, title=None)
178
+
179
+
180
+ def _untitled(schema: dict[str, Any]) -> dict[str, Any]:
181
+ return {k: v for k, v in schema.items() if k != "title"}
182
+
183
+
184
+ def to_openapi30(schema: Any) -> Any:
185
+ """pydantic's JSON Schema (2020-12, as OpenAPI 3.1 uses) written for OpenAPI 3.0: ``null`` in
186
+ unions becomes ``nullable``, ``const`` an ``enum``, ``examples`` an ``example``, numeric
187
+ ``exclusiveMinimum`` / ``exclusiveMaximum`` the boolean form, and ``$ref`` siblings ``allOf``."""
188
+ if isinstance(schema, list):
189
+ return [to_openapi30(s) for s in schema]
190
+ if not isinstance(schema, dict):
191
+ return schema
192
+ out = {k: to_openapi30(v) if isinstance(v, (dict, list)) else v for k, v in schema.items()}
193
+ for key in ("anyOf", "oneOf"):
194
+ members = out.get(key)
195
+ if isinstance(members, list) and {"type": "null"} in members:
196
+ rest = [m for m in members if m != {"type": "null"}]
197
+ del out[key]
198
+ if len(rest) == 1 and "$ref" not in rest[0]:
199
+ out = {**rest[0], **out}
200
+ elif len(rest) == 1:
201
+ out["allOf"] = rest
202
+ else:
203
+ out[key] = rest
204
+ out["nullable"] = True
205
+ if isinstance(out.get("type"), list):
206
+ kinds = [t for t in out["type"] if t != "null"]
207
+ if len(kinds) < len(out["type"]):
208
+ out["nullable"] = True
209
+ out["type"] = kinds[0] if len(kinds) == 1 else kinds
210
+ if "const" in out:
211
+ out["enum"] = [out.pop("const")]
212
+ if isinstance(out.get("examples"), list):
213
+ examples = out.pop("examples")
214
+ if examples:
215
+ out["example"] = examples[0]
216
+ for exclusive, bound in (("exclusiveMinimum", "minimum"), ("exclusiveMaximum", "maximum")):
217
+ if isinstance(out.get(exclusive), (int, float)) and not isinstance(out.get(exclusive), bool):
218
+ out[bound] = out.pop(exclusive)
219
+ out[exclusive] = True
220
+ if "$ref" in out and len(out) > 1:
221
+ ref = out.pop("$ref")
222
+ out["allOf"] = [{"$ref": ref}, *out.get("allOf", [])]
223
+ return out
224
+
225
+
226
+ class PydanticSchemas:
227
+ """pydantic models as takibi's schemas: request and response checks, constraints included."""
228
+
229
+ validation_problem: Any = ValidationProblem
230
+
231
+ def checker(self, tp: Any, target: str) -> _Checker:
232
+ return _Checker(tp, target)
233
+
234
+ def response_encoder(self, tp: Any) -> _Encoder:
235
+ return _Encoder(tp)
236
+
237
+ def builder(self, openapi31: bool) -> _DocBuilder:
238
+ return _DocBuilder(openapi31)
239
+
240
+
241
+ __all__ = ["PydanticSchemas", "to_openapi30"]