Pyjoyment 0.1.0__py3-none-any.whl

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.
Files changed (200) hide show
  1. Pyjo/App/ASGI.py +735 -0
  2. Pyjo/App/CGI.py +773 -0
  3. Pyjo/App/FastCGI.py +631 -0
  4. Pyjo/App/Lambda.py +987 -0
  5. Pyjo/App/WSGI.py +427 -0
  6. Pyjo/App/__init__.py +409 -0
  7. Pyjo/Asset/File.py +377 -0
  8. Pyjo/Asset/Memory.py +235 -0
  9. Pyjo/Asset/__init__.py +200 -0
  10. Pyjo/Base.py +215 -0
  11. Pyjo/Cache.py +119 -0
  12. Pyjo/Collection.py +570 -0
  13. Pyjo/Content/MultiPart.py +325 -0
  14. Pyjo/Content/Single.py +193 -0
  15. Pyjo/Content/__init__.py +806 -0
  16. Pyjo/Cookie/Request.py +90 -0
  17. Pyjo/Cookie/Response.py +294 -0
  18. Pyjo/Cookie/__init__.py +143 -0
  19. Pyjo/DOM/CSS.py +1019 -0
  20. Pyjo/DOM/HTML.py +550 -0
  21. Pyjo/DOM/__init__.py +1329 -0
  22. Pyjo/Date.py +294 -0
  23. Pyjo/EventEmitter.py +522 -0
  24. Pyjo/Exception.py +494 -0
  25. Pyjo/File.py +959 -0
  26. Pyjo/GetOpt.py +605 -0
  27. Pyjo/Headers.py +760 -0
  28. Pyjo/HelloWorld.py +77 -0
  29. Pyjo/Home.py +109 -0
  30. Pyjo/IOLoop/ChildProcess.py +591 -0
  31. Pyjo/IOLoop/Client.py +736 -0
  32. Pyjo/IOLoop/Datagram.py +982 -0
  33. Pyjo/IOLoop/Messages.py +664 -0
  34. Pyjo/IOLoop/Server.py +458 -0
  35. Pyjo/IOLoop/Stdio.py +446 -0
  36. Pyjo/IOLoop/Stream.py +644 -0
  37. Pyjo/IOLoop/Subprocess.py +419 -0
  38. Pyjo/IOLoop/TLS.py +360 -0
  39. Pyjo/IOLoop/__init__.py +1036 -0
  40. Pyjo/IOLoop/certs/README.md +41 -0
  41. Pyjo/IOLoop/certs/ca.crt +20 -0
  42. Pyjo/IOLoop/certs/server.crt +21 -0
  43. Pyjo/IOLoop/certs/server.key +28 -0
  44. Pyjo/JSON/Pointer.py +135 -0
  45. Pyjo/JSON/RPC.py +772 -0
  46. Pyjo/JSON/Schema.py +2506 -0
  47. Pyjo/JSON/__init__.py +573 -0
  48. Pyjo/Loader.py +342 -0
  49. Pyjo/Log.py +723 -0
  50. Pyjo/Message/Request.py +725 -0
  51. Pyjo/Message/Response.py +476 -0
  52. Pyjo/Message/__init__.py +830 -0
  53. Pyjo/Parameters.py +472 -0
  54. Pyjo/Path.py +419 -0
  55. Pyjo/Promise.py +894 -0
  56. Pyjo/Prove.py +1305 -0
  57. Pyjo/Reactor/Asyncio.py +854 -0
  58. Pyjo/Reactor/Poll.py +439 -0
  59. Pyjo/Reactor/__init__.py +525 -0
  60. Pyjo/SSE.py +119 -0
  61. Pyjo/Server/ASGI.py +486 -0
  62. Pyjo/Server/CGI.py +259 -0
  63. Pyjo/Server/Daemon.py +1007 -0
  64. Pyjo/Server/FastCGI.py +517 -0
  65. Pyjo/Server/Hot/__init__.py +687 -0
  66. Pyjo/Server/Hot/__main__.py +5 -0
  67. Pyjo/Server/Lambda.py +641 -0
  68. Pyjo/Server/Prefork.py +917 -0
  69. Pyjo/Server/Prethread.py +394 -0
  70. Pyjo/Server/Reload/Backend/Poll.py +114 -0
  71. Pyjo/Server/Reload/Backend/__init__.py +126 -0
  72. Pyjo/Server/Reload/__init__.py +482 -0
  73. Pyjo/Server/Reload/__main__.py +5 -0
  74. Pyjo/Server/Stdio.py +228 -0
  75. Pyjo/Server/WSGI.py +349 -0
  76. Pyjo/Server/__init__.py +396 -0
  77. Pyjo/String/Bytes.py +532 -0
  78. Pyjo/String/HTML.py +210 -0
  79. Pyjo/String/Mixin.py +333 -0
  80. Pyjo/String/Template.py +470 -0
  81. Pyjo/String/Unicode.py +553 -0
  82. Pyjo/String/__init__.py +0 -0
  83. Pyjo/TOML.py +232 -0
  84. Pyjo/Template.py +1290 -0
  85. Pyjo/Test/Pyjo.py +1454 -0
  86. Pyjo/Test/__init__.py +1233 -0
  87. Pyjo/Test/pytest_plugin.py +305 -0
  88. Pyjo/Transaction/HTTP.py +374 -0
  89. Pyjo/Transaction/WebSocket.py +709 -0
  90. Pyjo/Transaction/__init__.py +429 -0
  91. Pyjo/URL/Template.py +459 -0
  92. Pyjo/URL/__init__.py +651 -0
  93. Pyjo/Upload.py +139 -0
  94. Pyjo/UserAgent/CookieJar.py +391 -0
  95. Pyjo/UserAgent/Proxy.py +138 -0
  96. Pyjo/UserAgent/Server.py +239 -0
  97. Pyjo/UserAgent/Transactor.py +766 -0
  98. Pyjo/UserAgent/__init__.py +1243 -0
  99. Pyjo/Util.py +3567 -0
  100. Pyjo/WebSocket.py +332 -0
  101. Pyjo/YAML.py +1167 -0
  102. Pyjo/__init__.py +441 -0
  103. Pyjo/__main__.py +35 -0
  104. Pyjo/py.typed +0 -0
  105. Pyjoyment/Command/Author/__init__.py +20 -0
  106. Pyjoyment/Command/Author/generate/__init__.py +106 -0
  107. Pyjoyment/Command/Author/generate/app.py +299 -0
  108. Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
  109. Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
  110. Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
  111. Pyjoyment/Command/Author/generate/lite_app.py +113 -0
  112. Pyjoyment/Command/Author/generate/plugin.py +267 -0
  113. Pyjoyment/Command/Author/generate/pyproject.py +123 -0
  114. Pyjoyment/Command/Author/inflate.py +101 -0
  115. Pyjoyment/Command/__init__.py +332 -0
  116. Pyjoyment/Command/asgi.py +92 -0
  117. Pyjoyment/Command/cgi.py +90 -0
  118. Pyjoyment/Command/daemon.py +160 -0
  119. Pyjoyment/Command/eval.py +128 -0
  120. Pyjoyment/Command/fastcgi.py +99 -0
  121. Pyjoyment/Command/get.py +305 -0
  122. Pyjoyment/Command/lambda.py +117 -0
  123. Pyjoyment/Command/prefork.py +181 -0
  124. Pyjoyment/Command/prethread.py +126 -0
  125. Pyjoyment/Command/routes.py +133 -0
  126. Pyjoyment/Command/stdio.py +92 -0
  127. Pyjoyment/Command/version.py +141 -0
  128. Pyjoyment/Command/wsgi.py +93 -0
  129. Pyjoyment/Commands.py +546 -0
  130. Pyjoyment/Controller.py +1307 -0
  131. Pyjoyment/HelloWorld.py +56 -0
  132. Pyjoyment/Lite.py +494 -0
  133. Pyjoyment/Plugin/AllowedHosts.py +205 -0
  134. Pyjoyment/Plugin/Config.py +248 -0
  135. Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
  136. Pyjoyment/Plugin/EPLRenderer.py +134 -0
  137. Pyjoyment/Plugin/EPRenderer.py +212 -0
  138. Pyjoyment/Plugin/HeaderCondition.py +98 -0
  139. Pyjoyment/Plugin/Health.py +325 -0
  140. Pyjoyment/Plugin/JSONConfig.py +172 -0
  141. Pyjoyment/Plugin/Mount.py +223 -0
  142. Pyjoyment/Plugin/PyRenderer.py +209 -0
  143. Pyjoyment/Plugin/TOMLConfig.py +141 -0
  144. Pyjoyment/Plugin/TagHelpers.py +959 -0
  145. Pyjoyment/Plugin/YAMLConfig.py +178 -0
  146. Pyjoyment/Plugin/__init__.py +80 -0
  147. Pyjoyment/Plugins.py +215 -0
  148. Pyjoyment/Renderer.py +673 -0
  149. Pyjoyment/Routes/Match.py +251 -0
  150. Pyjoyment/Routes/Pattern.py +443 -0
  151. Pyjoyment/Routes/Route.py +827 -0
  152. Pyjoyment/Routes/__init__.py +419 -0
  153. Pyjoyment/Sessions.py +255 -0
  154. Pyjoyment/Static.py +421 -0
  155. Pyjoyment/Types.py +238 -0
  156. Pyjoyment/Validator/Validation.py +395 -0
  157. Pyjoyment/Validator/__init__.py +304 -0
  158. Pyjoyment/__init__.py +1083 -0
  159. Pyjoyment/__main__.py +5 -0
  160. Pyjoyment/py.typed +0 -0
  161. Pyjoyment/resources/public/favicon.ico +0 -0
  162. Pyjoyment/resources/public/favicon.svg +345 -0
  163. Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
  164. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
  165. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
  166. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
  167. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
  168. Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
  169. Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
  170. Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
  171. Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
  172. Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
  173. Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
  174. Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
  175. Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
  176. Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
  177. Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
  178. Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
  179. Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
  180. Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
  181. Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
  182. Pyjoyment/resources/public/pyjo/logo.png +0 -0
  183. Pyjoyment/resources/public/pyjo/logo.svg +346 -0
  184. Pyjoyment/resources/public/pyjo/notfound.png +0 -0
  185. Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
  186. Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
  187. Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
  188. Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
  189. Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
  190. Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
  191. Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
  192. Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
  193. Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
  194. Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
  195. Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
  196. pyjoyment-0.1.0.dist-info/METADATA +376 -0
  197. pyjoyment-0.1.0.dist-info/RECORD +200 -0
  198. pyjoyment-0.1.0.dist-info/WHEEL +4 -0
  199. pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
  200. pyjoyment-0.1.0.dist-info/licenses/LICENSE +201 -0
Pyjo/JSON/Schema.py ADDED
@@ -0,0 +1,2506 @@
1
+ """
2
+ Pyjo.JSON.Schema - JSON Schema from type hints, and its validation
3
+ ==================================================================
4
+ ::
5
+
6
+ from Pyjo.JSON.Schema import Schema, z
7
+
8
+ def weather(city: str, days: int = 3) -> dict[str, float]:
9
+ \"\"\"Forecast of the weather.
10
+
11
+ :param city: Name of the city.
12
+ :param days: Number of days.
13
+ \"\"\"
14
+
15
+ print(Schema.parameters(weather).data)
16
+ print(Schema.returns(weather).data)
17
+
18
+ print(z.object(city=z.string().describe('Name of the city'), days=z.int().min(1).optional()).data)
19
+
20
+ result = Schema.parameters(weather).validate({'city': 'Warsaw', 'days': 'three'})
21
+ if not result:
22
+ print(result)
23
+
24
+ # Or import the module, as in Google Python Style Guide
25
+
26
+ import Pyjo.JSON.Schema
27
+
28
+ print(Pyjo.JSON.Schema.of(list[int]).data)
29
+ print(Pyjo.JSON.Schema.new({'type': 'string'}).data)
30
+
31
+ :mod:`Pyjo.JSON.Schema` makes `JSON Schema 2020-12
32
+ <https://json-schema.org/draft/2020-12>`_ from the type hints of Python, from
33
+ the annotations of a type or of the parameters and the return value of a
34
+ function, with the descriptions of its docstring, or builds it with a chain of
35
+ methods, after `zod <https://zod.dev>`_, so that the schema of a tool of
36
+ JavaScript ports as it is. Both give an object of :mod:`Pyjo.JSON.Schema`,
37
+ which holds the schema in :attr:`~Pyjo_JSON_Schema.data`, as :mod:`Pyjo.JSON`
38
+ encodes it, and so does a schema written by hand.
39
+
40
+ The types of the type hints, and the schemas that they give:
41
+
42
+ * ``bool``, ``int``, ``float``, ``str`` and ``None`` are ``boolean``,
43
+ ``integer``, ``number``, ``string`` and ``null``, and ``Any`` and ``object``
44
+ are ``{}``, any value;
45
+ * ``list[T]``, ``set[T]`` and ``frozenset[T]`` are an ``array`` of ``items``,
46
+ the sets with ``uniqueItems``, ``tuple[A, B]`` an ``array`` of
47
+ ``prefixItems``, and ``tuple[T, ...]`` one of ``items``;
48
+ * ``dict[str, T]`` is an ``object`` of ``additionalProperties``;
49
+ * ``T | None`` and ``Optional[T]`` add ``null`` to the ``type`` of a schema
50
+ that has only a type, and are an ``anyOf`` with ``null`` otherwise, and other
51
+ unions are ``anyOf``;
52
+ * ``Literal`` is a ``const`` or an ``enum``, and so is an :class:`enum.Enum` of
53
+ its values, with their ``type`` where they have one;
54
+ * a ``TypedDict`` is an ``object`` whose ``required`` are its required keys,
55
+ closed with ``closed=True`` of :pep:`728`, and a dataclass one whose
56
+ ``required`` are the fields without a default;
57
+ * :class:`datetime.datetime`, :class:`datetime.date`, :class:`datetime.time`,
58
+ :class:`uuid.UUID` and :mod:`Pyjo.URL` are a ``string`` with the ``format``
59
+ ``date-time``, ``date``, ``time``, ``uuid`` and ``uri``;
60
+ * ``Annotated[T, ...]`` adds a string as the ``description``, and the keywords
61
+ of a :class:`dict`, as ``minimum`` and ``pattern``, or of a schema, as
62
+ ``z.email()``, without a class of a library of validation;
63
+ * a ``type`` alias is its value, and a ``NewType`` its type.
64
+
65
+ A ``TypedDict``, a dataclass, an :class:`enum.Enum` or a ``type`` alias that is
66
+ used more than once, or a type that refers to itself, goes to ``$defs``, where
67
+ a ``$ref`` refers to it. A schema that refers to itself refers to its root with
68
+ ``#``, so that the root stays an ``object``, as the ``inputSchema`` of a tool
69
+ of MCP has to be. The description of a ``TypedDict``, a dataclass or an
70
+ :class:`enum.Enum` is the first paragraph of its docstring.
71
+
72
+ A type that has no schema, as ``bytes``, a ``TypeVar`` or a class of its own,
73
+ fails with a :mod:`Pyjo.Exception` that names it and where it is. The
74
+ annotations are those that :func:`typing.get_type_hints` gives, lazy as of
75
+ :pep:`649`, or strings with ``from __future__ import annotations``, so a name
76
+ that the module of a type does not have fails too.
77
+
78
+ A schema has no ``$schema``, as the schemas of MCP and OpenAPI, whose dialect is
79
+ known. :data:`DIALECT` names it on request, with
80
+ :meth:`~Pyjo_JSON_Schema.meta`. ::
81
+
82
+ # {'$schema': 'https://json-schema.org/draft/2020-12/schema', 'type': 'string'}
83
+ z.string().meta({'$schema': Pyjo.JSON.Schema.DIALECT}).data
84
+
85
+ The builder is a subset of zod, what JSON Schema has, without its
86
+ transformations, refinements, coercions and the types that are inferred from a
87
+ schema. Its methods are those of zod in the names of Python, ``multiple_of``
88
+ for ``multipleOf``, and ``z`` is the class itself, so that
89
+ ``z.object({...})`` reads as in JavaScript. An ``object`` is as zod gives it
90
+ for the input, which a tool gets: a property that has a ``default`` is not
91
+ required, and the properties that it does not name are not forbidden, unless
92
+ the object is strict. Where zod gives another schema of the same, the method
93
+ says so.
94
+
95
+ Validation
96
+ ----------
97
+
98
+ :meth:`~Pyjo_JSON_Schema.validate` validates a value, as :mod:`Pyjo.JSON`
99
+ decodes it, with a schema of any of the three forms, and gives a
100
+ :class:`Result`, which is false if the value is not valid, with the errors of
101
+ the value, each with the JSON Pointer of the value and of the keyword. ::
102
+
103
+ # /days: Expected integer - got string.
104
+ print(z.object(days=z.int()).validate({'days': 'three'}))
105
+
106
+ It is the validation of JSON Schema 2020-12 with the keywords that the schema of
107
+ a tool, of a body of an API or of a configuration has:
108
+
109
+ * ``type``, ``enum`` and ``const``;
110
+ * ``minimum``, ``maximum``, ``exclusiveMinimum``, ``exclusiveMaximum`` and
111
+ ``multipleOf`` of numbers;
112
+ * ``minLength``, ``maxLength``, ``pattern`` and ``format`` of strings;
113
+ * ``items``, ``prefixItems``, ``minItems``, ``maxItems``, ``uniqueItems``,
114
+ ``contains``, ``minContains`` and ``maxContains`` of arrays;
115
+ * ``properties``, ``required``, ``additionalProperties``,
116
+ ``patternProperties``, ``propertyNames``, ``minProperties``,
117
+ ``maxProperties``, ``dependentRequired`` and ``dependentSchemas`` of
118
+ objects;
119
+ * ``allOf``, ``anyOf``, ``oneOf``, ``not``, ``if``, ``then`` and ``else``;
120
+ * ``$ref`` to the schemas of the document, by a JSON Pointer, as to ``$defs``,
121
+ by their ``$anchor`` and by the ``$id`` of a schema in the schema, also to
122
+ itself, and to the schemas of the registry of
123
+ :meth:`~Pyjo_JSON_Schema.add_schema`, which it never downloads.
124
+
125
+ The keywords of 2020-12 that it does not support, ``$dynamicRef``,
126
+ ``unevaluatedItems`` and ``unevaluatedProperties``, and a ``$schema`` of
127
+ another dialect, fail with a :mod:`Pyjo.Exception`, as a ``$ref`` to a schema
128
+ that it does not have does, and a keyword that 2020-12 does not have, as
129
+ ``additionalItems`` of the older drafts, is ignored, as the specification says
130
+ of a keyword that it does not know. The annotations, as ``title``,
131
+ ``default`` and ``contentMediaType``, validate nothing.
132
+
133
+ The values are those of JSON: ``bool`` is not an ``integer`` nor a ``number``,
134
+ a ``float`` without a fraction is an ``integer``, ``1`` and ``1.0`` are equal
135
+ for ``enum``, ``const`` and ``uniqueItems``, ``True`` and ``1`` are not, and a
136
+ ``tuple`` is an ``array``. A value that JSON does not have, as ``bytes``, a
137
+ ``set``, ``nan`` or a name of a property that is not a string, is an error of
138
+ its own, whatever the schema.
139
+
140
+ ``format`` is an annotation, as JSON Schema has it by default, and an
141
+ assertion with ``formats``, for the formats ``date-time``, ``date``, ``time``
142
+ and ``duration`` of RFC 3339, ``email``, ``hostname``, ``ipv4``, ``ipv6``,
143
+ ``uri``, ``uri-reference``, ``uuid``, ``regex``, ``json-pointer`` and
144
+ ``relative-json-pointer``. Another format, as ``idn-email``, ``iri`` or
145
+ ``uri-template``, stays an annotation.
146
+
147
+ A ``pattern`` is a regular expression of ECMAScript, which is one of
148
+ :mod:`re` with these translations: ``\\d``, ``\\w`` and ``\\b`` are of ASCII,
149
+ ``\\s`` is the white space of ECMAScript, ``.`` matches no line terminator,
150
+ ``$`` matches only at the end, a named group is ``(?<name>...)`` and its
151
+ backreference ``\\k<name>``, ``\\p{...}`` is a general category of Unicode,
152
+ or ``ASCII``, ``Any`` and ``Assigned``, and ``\\cX``, ``\\u{...}``, ``[]``
153
+ and ``[^]`` are as ECMAScript has them. What :mod:`re` does not have, as a
154
+ lookbehind of a variable width or ``\\p{Script=...}``, and the syntax of
155
+ Python that ECMAScript does not have, as ``(?P<name>...)``, fail with a
156
+ :mod:`Pyjo.Exception`.
157
+
158
+ See also
159
+ --------
160
+
161
+ :mod:`Pyjo.JSON`, :mod:`Pyjo.JSON.Pointer`, https://mojolicious.org.
162
+
163
+ It has no counterpart in Mojolicious, it is written from the specification of
164
+ JSON Schema 2020-12, its builder is inspired by `zod <https://zod.dev>`_, and
165
+ its validation is tested with the official test suite of JSON Schema,
166
+ `JSON-Schema-Test-Suite <https://github.com/json-schema-org/JSON-Schema-Test-Suite>`_.
167
+
168
+ Classes
169
+ -------
170
+ """
171
+
172
+ import Pyjo.Base
173
+ import Pyjo.Collection
174
+ import Pyjo.Exception
175
+ import Pyjo.JSON
176
+ import Pyjo.JSON.Pointer
177
+ import Pyjo.URL
178
+
179
+ from Pyjo.JSON import to_json
180
+ from Pyjo.Util import punycode_decode
181
+
182
+ import builtins
183
+ import collections
184
+ import contextlib
185
+ import dataclasses
186
+ import datetime
187
+ import enum
188
+ import fractions
189
+ import functools
190
+ import inspect
191
+ import ipaddress
192
+ import math
193
+ import re
194
+ import sys
195
+ import types
196
+ import typing
197
+ import unicodedata
198
+ import uuid
199
+
200
+ from collections.abc import Callable, Iterable
201
+ from typing import Any, Self, cast
202
+
203
+
204
+ type Anything = builtins.object
205
+
206
+ DIALECT = 'https://json-schema.org/draft/2020-12/schema'
207
+ """The dialect of the schemas, JSON Schema 2020-12, as ``$schema`` names it."""
208
+
209
+ # The keywords of a constraint for each type, as min() and max() set them
210
+ _MIN = {'string': 'minLength', 'number': 'minimum', 'integer': 'minimum', 'array': 'minItems'}
211
+ _MAX = {'string': 'maxLength', 'number': 'maximum', 'integer': 'maximum', 'array': 'maxItems'}
212
+
213
+ # The qualifiers of the keys of a TypedDict, ReadOnly of Python 3.13 too
214
+ _QUALIFIERS = tuple(
215
+ q for q in (typing.Required, typing.NotRequired, getattr(typing, 'ReadOnly', None)) if q is not None
216
+ )
217
+
218
+ # The formats of the types of the standard library
219
+ _FORMATS: dict[type, str] = {
220
+ datetime.datetime: 'date-time',
221
+ datetime.date: 'date',
222
+ datetime.time: 'time',
223
+ uuid.UUID: 'uuid',
224
+ }
225
+
226
+ # The sections of a docstring of the Google Python Style Guide
227
+ _RE_ARGS = re.compile(r'(?:Args|Arguments|Parameters|Params|Keyword Args|Keyword Arguments):')
228
+ _RE_RETURNS = re.compile(r'(?:Returns|Return|Yields):')
229
+ _RE_SECTION = re.compile(r'[A-Z][A-Za-z ]*:')
230
+ _RE_FIELD = re.compile(r':(param|parameter|arg|argument|key|keyword|returns?|type|rtype|raises?)\b([^:]*):\s*(.*)')
231
+ _RE_ITEM = re.compile(r'\*{0,2}(\w+)(?:\s*\([^)]*\))?:\s*(.*)')
232
+ _RE_RETURN_TYPE = re.compile(r'[\w.\[\], |]+:\s+(.*)')
233
+
234
+ _MISSING = builtins.object()
235
+
236
+ # The names of the classes that the class hides, as its methods int, enum and object do in the lazy annotations of
237
+ # PEP 649
238
+ type _Enum = enum.Enum
239
+
240
+
241
+ class _stringmethod[S]:
242
+ """A modifier of a string that the class has too, for a new string, as ``z.email()`` of zod."""
243
+
244
+ def __init__(self, func: Callable[[S], S]) -> None:
245
+ self.func = func
246
+ self.__doc__ = func.__doc__
247
+
248
+ def __get__(self, obj: S | None, objtype: Any = None) -> Callable[[], S]:
249
+ def bound() -> S:
250
+ return self.func(obj if obj is not None else objtype.string())
251
+
252
+ bound.__doc__ = self.__doc__
253
+ return bound
254
+
255
+
256
+ class Pyjo_JSON_Schema(Pyjo.Base.object):
257
+ """
258
+ :mod:`Pyjo.JSON.Schema` inherits all methods from :mod:`Pyjo.Base` and
259
+ implements the following new ones.
260
+ """
261
+
262
+ _keywords: dict[str, Any] | bool
263
+ _optional: bool
264
+ _lazy: '_Lazy | None'
265
+ _data: dict[str, Any] | bool | None
266
+ _schemas: 'dict[str, Pyjo_JSON_Schema | dict[str, Any] | bool]'
267
+ _index: '_Index | None'
268
+
269
+ def __init__(self, data: dict[str, Any] | bool | None = None) -> None:
270
+ """::
271
+
272
+ schema = Pyjo.JSON.Schema.new()
273
+ schema = Pyjo.JSON.Schema.new({'type': 'string', 'minLength': 1})
274
+
275
+ Build new :mod:`Pyjo.JSON.Schema` object, of a schema written by hand,
276
+ which can hold other objects of :mod:`Pyjo.JSON.Schema`, or of any value
277
+ without arguments.
278
+ """
279
+ self._keywords = {} if data is None else data
280
+ self._optional = False
281
+ self._lazy = None
282
+ self._data = None
283
+ self._schemas = {}
284
+ self._index = None
285
+
286
+ self.name: str | None = None
287
+ """::
288
+
289
+ name = schema.name
290
+ schema.name = 'Category'
291
+
292
+ The name of the schema in ``$defs``, where it goes when a schema holds
293
+ it more than once, as the name of its type, or ``Schema`` and a number
294
+ without a name.
295
+ """
296
+
297
+ def __json__(self) -> dict[str, Any] | bool:
298
+ """::
299
+
300
+ data = schema.__json__()
301
+
302
+ The schema as :mod:`Pyjo.JSON` encodes it, :attr:`data`.
303
+ """
304
+ return self.data
305
+
306
+ def __repr__(self) -> str:
307
+ return f'{type(self).__module__}.{type(self).__qualname__}({self.data!r})'
308
+
309
+ @property
310
+ def data(self) -> dict[str, Any] | bool:
311
+ """::
312
+
313
+ data = schema.data
314
+
315
+ The schema as a :class:`dict`, with ``$defs`` for the types that are used
316
+ more than once or refer to themselves, or ``True`` or ``False`` for a
317
+ schema of any value or of none. ::
318
+
319
+ # {'type': 'array', 'items': {'type': 'integer'}}
320
+ Pyjo.JSON.Schema.of(list[int]).data
321
+ """
322
+ if self._data is None:
323
+ self._data = _Emitter(self).emit()
324
+ return self._data
325
+
326
+ @property
327
+ def is_optional(self) -> bool:
328
+ """::
329
+
330
+ boolean = schema.is_optional
331
+
332
+ True if the schema is of a property that an object does not require, as
333
+ :meth:`optional` and :meth:`default` make it.
334
+ """
335
+ return self._optional
336
+
337
+ @property
338
+ def is_lazy(self) -> bool:
339
+ """::
340
+
341
+ boolean = schema.is_lazy
342
+
343
+ True if the schema is the one that a function gives, as :meth:`lazy`
344
+ makes it.
345
+ """
346
+ return self._lazy is not None
347
+
348
+ @property
349
+ def keywords(self) -> dict[str, Any] | bool:
350
+ """::
351
+
352
+ keywords = schema.keywords
353
+
354
+ The keywords of the schema, which can hold other objects of
355
+ :mod:`Pyjo.JSON.Schema`, as :attr:`data` has them without them.
356
+ """
357
+ return self._keywords
358
+
359
+ def resolve(self) -> 'Pyjo_JSON_Schema':
360
+ """::
361
+
362
+ schema = schema.resolve()
363
+
364
+ The schema that the function of a lazy schema gives, or the schema
365
+ itself.
366
+ """
367
+ return self._lazy.resolve(self) if self._lazy is not None else self
368
+
369
+ # Types
370
+
371
+ @classmethod
372
+ def of(cls, tp: Any) -> Self:
373
+ """::
374
+
375
+ schema = Pyjo.JSON.Schema.of(list[int])
376
+ schema = Pyjo.JSON.Schema.of(Point)
377
+
378
+ The schema of a type of the type hints, or the schema itself, or a schema
379
+ written by hand as a :class:`dict`. ::
380
+
381
+ # {'type': ['string', 'null']}
382
+ Pyjo.JSON.Schema.of(str | None).data
383
+
384
+ # {'type': 'object', 'additionalProperties': {'type': 'number'}}
385
+ Pyjo.JSON.Schema.of(dict[str, float]).data
386
+ """
387
+ return cast(Self, _Compiler(cls).compile(tp, ''))
388
+
389
+ @classmethod
390
+ def parameters(cls, function: Callable[..., Any], skip: int = 0) -> Self:
391
+ """::
392
+
393
+ schema = Pyjo.JSON.Schema.parameters(tool)
394
+ schema = Pyjo.JSON.Schema.parameters(tool, skip=1)
395
+
396
+ The schema of the parameters of a function, as the ``inputSchema`` of a
397
+ tool, an ``object`` whose ``properties`` are the parameters, with the
398
+ ones without a default ``required``, and the first paragraph of its
399
+ docstring as its ``description``. The description of a parameter is the
400
+ one of the field ``:param x:`` of its docstring, in reStructuredText, or
401
+ of the section ``Args:`` of the Google Python Style Guide.
402
+
403
+ ``skip`` leaves out the first parameters, as the context or the
404
+ controller that a tool or an action gets before its arguments; ``self``
405
+ of a bound method is already gone. ``**kwargs: T`` gives the
406
+ ``additionalProperties`` of the schema of ``T``, and without it the
407
+ object has no other properties. A parameter without an annotation is of
408
+ any value. ``*args`` and a parameter that is only positional fail, as an
409
+ object of JSON has no members without a name. ::
410
+
411
+ def weather(c, city: str, days: int = 3):
412
+ \"\"\"Forecast of the weather.
413
+
414
+ Args:
415
+ city: Name of the city.
416
+ \"\"\"
417
+
418
+ # {'type': 'object', 'description': 'Forecast of the weather.',
419
+ # 'properties': {'city': {'type': 'string', 'description': 'Name of the city.'},
420
+ # 'days': {'type': 'integer', 'default': 3}},
421
+ # 'required': ['city'], 'additionalProperties': False}
422
+ Pyjo.JSON.Schema.parameters(weather, skip=1).data
423
+ """
424
+ return cast(Self, _Compiler(cls).parameters(function, skip))
425
+
426
+ @classmethod
427
+ def returns(cls, function: Callable[..., Any]) -> Self | None:
428
+ """::
429
+
430
+ schema = Pyjo.JSON.Schema.returns(tool)
431
+
432
+ The schema of the return value of a function, as the ``outputSchema`` of
433
+ a tool, with the description of the field ``:returns:`` of its
434
+ docstring, or of its section ``Returns:``, or ``None`` for a function
435
+ without the annotation of its return value. ::
436
+
437
+ def forecast(city: str) -> list[float]:
438
+ \"\"\"Temperatures.
439
+
440
+ :returns: One for each day.
441
+ \"\"\"
442
+
443
+ # {'type': 'array', 'items': {'type': 'number'}, 'description': 'One for each day.'}
444
+ Pyjo.JSON.Schema.returns(forecast).data
445
+ """
446
+ return cast(Self | None, _Compiler(cls).returns(function))
447
+
448
+ # Builder
449
+
450
+ @classmethod
451
+ def string(cls) -> Self:
452
+ """::
453
+
454
+ schema = z.string()
455
+
456
+ The schema of a string. ::
457
+
458
+ # {'type': 'string', 'minLength': 1, 'maxLength': 5}
459
+ z.string().min(1).max(5).data
460
+ """
461
+ return cls({'type': 'string'})
462
+
463
+ @classmethod
464
+ def number(cls) -> Self:
465
+ """::
466
+
467
+ schema = z.number()
468
+
469
+ The schema of a number. ::
470
+
471
+ # {'type': 'number', 'exclusiveMinimum': 0}
472
+ z.number().gt(0).data
473
+ """
474
+ return cls({'type': 'number'})
475
+
476
+ @classmethod
477
+ def boolean(cls) -> Self:
478
+ """::
479
+
480
+ schema = z.boolean()
481
+
482
+ The schema of ``true`` or ``false``.
483
+ """
484
+ return cls({'type': 'boolean'})
485
+
486
+ @classmethod
487
+ def null(cls) -> Self:
488
+ """::
489
+
490
+ schema = z.null()
491
+
492
+ The schema of ``null``.
493
+ """
494
+ return cls({'type': 'null'})
495
+
496
+ @classmethod
497
+ def literal(cls, value: Any) -> Self:
498
+ """::
499
+
500
+ schema = z.literal('celsius')
501
+ schema = z.literal(['celsius', 'fahrenheit'])
502
+
503
+ The schema of a value, as ``const``, or of one of a list of values, as
504
+ ``enum``, with their ``type`` where they have one. ::
505
+
506
+ # {'type': 'string', 'const': 'celsius'}
507
+ z.literal('celsius').data
508
+ """
509
+ values = cast(list[Any], value) if isinstance(value, (list, tuple)) else [value]
510
+ return cls(_literal(values))
511
+
512
+ @classmethod
513
+ def union(cls, options: Iterable[Any]) -> Self:
514
+ """::
515
+
516
+ schema = z.union([z.string(), z.int()])
517
+
518
+ The schema of a value of one of the options, as ``anyOf``.
519
+ """
520
+ return cls({'anyOf': [cls.of(option) for option in options]})
521
+
522
+ @classmethod
523
+ def array(cls, element: Any = None) -> Self:
524
+ """::
525
+
526
+ schema = z.array(z.string())
527
+
528
+ The schema of an array of elements, of any value without them. ::
529
+
530
+ # {'type': 'array', 'items': {'type': 'string'}, 'minItems': 1}
531
+ z.array(z.string()).nonempty().data
532
+ """
533
+ keywords: dict[str, Any] = {'type': 'array'}
534
+ if element is not None:
535
+ keywords['items'] = cls.of(element)
536
+ return cls(keywords)
537
+
538
+ @classmethod
539
+ def tuple(cls, elements: Iterable[Any], rest: Any = None) -> Self:
540
+ """::
541
+
542
+ schema = z.tuple([z.string(), z.int()])
543
+ schema = z.tuple([z.string()], z.int())
544
+
545
+ The schema of an array of the elements, each of its own, and of the
546
+ elements of ``rest`` after them, if any. zod also gives ``items: false``
547
+ to a tuple without ``rest``, which ``maxItems`` already says. ::
548
+
549
+ # {'type': 'array', 'prefixItems': [{'type': 'string'}, {'type': 'integer'}],
550
+ # 'minItems': 2, 'maxItems': 2}
551
+ z.tuple([z.string(), z.int()]).data
552
+ """
553
+ prefix = [cls.of(element) for element in elements]
554
+ keywords: dict[str, Any] = {'type': 'array', 'prefixItems': prefix, 'minItems': len(prefix)}
555
+ if rest is None:
556
+ keywords['maxItems'] = len(prefix)
557
+ else:
558
+ keywords['items'] = cls.of(rest)
559
+ return cls(keywords)
560
+
561
+ @classmethod
562
+ def record(cls, key: Any, value: Any = None) -> Self:
563
+ """::
564
+
565
+ schema = z.record(z.string(), z.int())
566
+ schema = z.record(z.int())
567
+
568
+ The schema of an object whose properties are all of a value, with the
569
+ schema of their names where it is not any string. zod also gives
570
+ ``propertyNames: {'type': 'string'}``, which every name of JSON is. ::
571
+
572
+ # {'type': 'object', 'additionalProperties': {'type': 'integer'}}
573
+ z.record(z.string(), z.int()).data
574
+ """
575
+ if value is None:
576
+ key, value = None, key
577
+ keywords: dict[str, Any] = {'type': 'object', 'additionalProperties': cls.of(value)}
578
+ if key is not None:
579
+ names = cls.of(key)
580
+ if names._keywords != {'type': 'string'}:
581
+ keywords['propertyNames'] = names
582
+ return cls(keywords)
583
+
584
+ @classmethod
585
+ def strict_object(cls, shape: dict[str, Any] | None = None, /, **kwargs: Any) -> Self:
586
+ """::
587
+
588
+ schema = z.strict_object({'city': z.string()})
589
+
590
+ The schema of an object without other properties, :meth:`object` and
591
+ :meth:`strict`.
592
+ """
593
+ return cls.object(shape, **kwargs).strict()
594
+
595
+ @classmethod
596
+ def loose_object(cls, shape: dict[str, Any] | None = None, /, **kwargs: Any) -> Self:
597
+ """::
598
+
599
+ schema = z.loose_object({'city': z.string()})
600
+
601
+ The schema of an object that allows other properties, :meth:`object` and
602
+ :meth:`loose`.
603
+ """
604
+ return cls.object(shape, **kwargs).loose()
605
+
606
+ @classmethod
607
+ def lazy(cls, getter: Callable[[], Any]) -> Self:
608
+ """::
609
+
610
+ schema = z.lazy(lambda: category)
611
+
612
+ The schema that a function gives when :attr:`data` is made, so that a
613
+ schema can refer to itself, or to another one that is made after it. ::
614
+
615
+ category = z.object(name=z.string(), subcategories=z.array(z.lazy(lambda: category)))
616
+
617
+ # {'type': 'object',
618
+ # 'properties': {'name': {'type': 'string'},
619
+ # 'subcategories': {'type': 'array', 'items': {'$ref': '#'}}},
620
+ # 'required': ['name', 'subcategories']}
621
+ category.data
622
+ """
623
+ schema = cls({})
624
+ schema._lazy = _Lazy(getter)
625
+ return schema
626
+
627
+ # Modifiers
628
+
629
+ def min(self, value: float) -> Self:
630
+ """::
631
+
632
+ schema = schema.min(1)
633
+
634
+ The least length of a string, the least value of a number, or the least
635
+ number of elements of an array, as ``minLength``, ``minimum`` and
636
+ ``minItems``.
637
+ """
638
+ return self._constraint(_MIN, value, 'min')
639
+
640
+ def max(self, value: float) -> Self:
641
+ """::
642
+
643
+ schema = schema.max(5)
644
+
645
+ The greatest length of a string, the greatest value of a number, or the
646
+ greatest number of elements of an array, as ``maxLength``, ``maximum``
647
+ and ``maxItems``.
648
+ """
649
+ return self._constraint(_MAX, value, 'max')
650
+
651
+ def gte(self, value: float) -> Self:
652
+ """::
653
+
654
+ schema = schema.gte(1)
655
+
656
+ The same as :meth:`min`.
657
+ """
658
+ return self.min(value)
659
+
660
+ def lte(self, value: float) -> Self:
661
+ """::
662
+
663
+ schema = schema.lte(5)
664
+
665
+ The same as :meth:`max`.
666
+ """
667
+ return self.max(value)
668
+
669
+ def length(self, value: builtins.int) -> Self:
670
+ """::
671
+
672
+ schema = schema.length(3)
673
+
674
+ The length of a string, or the number of elements of an array.
675
+ """
676
+ return self.min(value).max(value)
677
+
678
+ def nonempty(self) -> Self:
679
+ """::
680
+
681
+ schema = schema.nonempty()
682
+
683
+ A string or an array that is not empty, :meth:`min` of 1.
684
+ """
685
+ return self.min(1)
686
+
687
+ def gt(self, value: float) -> Self:
688
+ """::
689
+
690
+ schema = schema.gt(0)
691
+
692
+ A number greater than the value, ``exclusiveMinimum``.
693
+ """
694
+ return self._number('exclusiveMinimum', value, 'gt')
695
+
696
+ def lt(self, value: float) -> Self:
697
+ """::
698
+
699
+ schema = schema.lt(10)
700
+
701
+ A number less than the value, ``exclusiveMaximum``.
702
+ """
703
+ return self._number('exclusiveMaximum', value, 'lt')
704
+
705
+ def multiple_of(self, value: float) -> Self:
706
+ """::
707
+
708
+ schema = schema.multiple_of(2)
709
+
710
+ A number that is a multiple of the value, ``multipleOf``.
711
+ """
712
+ return self._number('multipleOf', value, 'multiple_of')
713
+
714
+ def regex(self, pattern: str | re.Pattern[str]) -> Self:
715
+ """::
716
+
717
+ schema = schema.regex(r'^[a-z]+$')
718
+ schema = schema.regex(re.compile(r'^[a-z]+$'))
719
+
720
+ A string that matches a regular expression, ``pattern``, which JSON
721
+ Schema has in the syntax of ECMAScript.
722
+ """
723
+ return self._string('pattern', pattern.pattern if isinstance(pattern, re.Pattern) else pattern, 'regex')
724
+
725
+ @_stringmethod
726
+ def email(self) -> 'Pyjo_JSON_Schema':
727
+ """::
728
+
729
+ schema = z.email()
730
+ schema = z.string().email()
731
+
732
+ A string of the format ``email``. zod also gives the ``pattern`` of its
733
+ own regular expression.
734
+ """
735
+ return self._string('format', 'email', 'email')
736
+
737
+ @_stringmethod
738
+ def url(self) -> 'Pyjo_JSON_Schema':
739
+ """::
740
+
741
+ schema = z.url()
742
+ schema = z.string().url()
743
+
744
+ A string of the format ``uri``.
745
+ """
746
+ return self._string('format', 'uri', 'url')
747
+
748
+ @_stringmethod
749
+ def uuid(self) -> 'Pyjo_JSON_Schema':
750
+ """::
751
+
752
+ schema = z.uuid()
753
+ schema = z.string().uuid()
754
+
755
+ A string of the format ``uuid``. zod also gives the ``pattern`` of its
756
+ own regular expression.
757
+ """
758
+ return self._string('format', 'uuid', 'uuid')
759
+
760
+ def datetime(self) -> Self:
761
+ """::
762
+
763
+ schema = schema.datetime()
764
+
765
+ A string of the format ``date-time``, as ``z.iso.datetime()``. zod also
766
+ gives the ``pattern`` of its own regular expression.
767
+ """
768
+ return self._string('format', 'date-time', 'datetime')
769
+
770
+ def date(self) -> Self:
771
+ """::
772
+
773
+ schema = schema.date()
774
+
775
+ A string of the format ``date``, as ``z.iso.date()``.
776
+ """
777
+ return self._string('format', 'date', 'date')
778
+
779
+ def time(self) -> Self:
780
+ """::
781
+
782
+ schema = schema.time()
783
+
784
+ A string of the format ``time``, as ``z.iso.time()``. zod gives only the
785
+ ``pattern`` of its own regular expression.
786
+ """
787
+ return self._string('format', 'time', 'time')
788
+
789
+ def strict(self) -> Self:
790
+ """::
791
+
792
+ schema = schema.strict()
793
+
794
+ An object without other properties, ``additionalProperties: false``.
795
+ """
796
+ return self._object(False, 'strict')
797
+
798
+ def loose(self) -> Self:
799
+ """::
800
+
801
+ schema = schema.loose()
802
+
803
+ An object that allows other properties, ``additionalProperties: {}``.
804
+ """
805
+ return self._object({}, 'loose')
806
+
807
+ def passthrough(self) -> Self:
808
+ """::
809
+
810
+ schema = schema.passthrough()
811
+
812
+ The same as :meth:`loose`.
813
+ """
814
+ return self.loose()
815
+
816
+ def optional(self) -> Self:
817
+ """::
818
+
819
+ schema = schema.optional()
820
+
821
+ The schema of a property that an object does not require. ::
822
+
823
+ # {'type': 'object', 'properties': {'days': {'type': 'integer'}}}
824
+ z.object(days=z.int().optional()).data
825
+ """
826
+ schema = self._copy()
827
+ schema._optional = True
828
+ return schema
829
+
830
+ def nullable(self) -> Self:
831
+ """::
832
+
833
+ schema = schema.nullable()
834
+
835
+ The schema of the value or of ``null``, in its ``type`` where the
836
+ schema has only a type, and in ``anyOf`` otherwise. ::
837
+
838
+ # {'type': ['string', 'null']}
839
+ z.string().nullable().data
840
+ """
841
+ schema = type(self)(_nullable(self))
842
+ schema._optional = self._optional
843
+ return schema
844
+
845
+ def nullish(self) -> Self:
846
+ """::
847
+
848
+ schema = schema.nullish()
849
+
850
+ :meth:`nullable` and :meth:`optional`.
851
+ """
852
+ return self.nullable().optional()
853
+
854
+ def default(self, value: Any) -> Self:
855
+ """::
856
+
857
+ schema = schema.default(3)
858
+
859
+ The value of a property that is not given, ``default``, which an object
860
+ does not require, as zod has it for the input.
861
+ """
862
+ return self.meta(default=value).optional()
863
+
864
+ def describe(self, description: str) -> Self:
865
+ """::
866
+
867
+ schema = schema.describe('Name of the city')
868
+
869
+ The description of the value, ``description``.
870
+ """
871
+ return self.meta(description=description)
872
+
873
+ def meta(self, keywords: dict[str, Any] | None = None, /, **kwargs: Any) -> Self:
874
+ """::
875
+
876
+ schema = schema.meta({'$schema': Pyjo.JSON.Schema.DIALECT})
877
+ schema = schema.meta(title='City', examples=['Warsaw'])
878
+
879
+ Other keywords of the schema. zod keeps them as the metadata of the
880
+ schema too.
881
+ """
882
+ schema = self._copy()
883
+ schema._keywords = {**cast(dict[str, Any], schema._keywords), **(keywords or {}), **kwargs}
884
+ return schema
885
+
886
+ # Validation
887
+
888
+ def add_schema(self, schema: 'Pyjo_JSON_Schema | dict[str, Any] | bool', uri: str | None = None) -> Self:
889
+ """::
890
+
891
+ schema = schema.add_schema({'$id': 'https://example.com/address', 'type': 'object'})
892
+ schema = schema.add_schema(z.string(), 'https://example.com/name')
893
+
894
+ Add a schema to the registry of the schema, under its ``$id`` or the
895
+ URI, where a ``$ref`` of the schema finds it, and its ``$id`` and
896
+ ``$anchor`` too.
897
+ """
898
+ if uri is None:
899
+ data = schema.data if isinstance(schema, Pyjo_JSON_Schema) else schema
900
+ uri = data.get('$id') if isinstance(data, dict) else None
901
+ if not isinstance(uri, str):
902
+ raise Pyjo.Exception.new('JSON Schema has no $id of the schema to add')
903
+ self._schemas[_uri('', uri)[0]] = schema
904
+ self._index = None
905
+ return self
906
+
907
+ def parse(self, value: Any, formats: bool = False) -> Any:
908
+ """::
909
+
910
+ value = schema.parse({'city': 'Warsaw'})
911
+
912
+ The value, if the schema validates it, as :meth:`validate` does, or a
913
+ :mod:`Pyjo.Exception` with the errors, one in each line.
914
+ """
915
+ result = self.validate(value, formats)
916
+ if not result:
917
+ raise Pyjo.Exception.new(str(result))
918
+ return value
919
+
920
+ def validate(self, value: Any, formats: bool = False) -> 'Result':
921
+ """::
922
+
923
+ result = schema.validate({'city': 'Warsaw'})
924
+ result = schema.validate({'date': '2026-10-23'}, formats=True)
925
+
926
+ Validate a value, as :mod:`Pyjo.JSON` decodes it, and return a
927
+ :class:`Result`, which is true if the value is valid, with the
928
+ :class:`Error` objects of the value otherwise. ``format`` is an
929
+ annotation, as JSON Schema has it by default, and an assertion with
930
+ ``formats``. ::
931
+
932
+ # "/days: Expected integer - got string."
933
+ str(z.object(days=z.int()).validate({'days': 'three'}))
934
+ """
935
+ if self._index is None:
936
+ self._index = _Index(self.data, self._schemas)
937
+ return Result(_Validator(self._index, formats).validate(value))
938
+
939
+ # Internals
940
+
941
+ def _copy(self) -> Self:
942
+ if isinstance(self._keywords, bool):
943
+ raise Pyjo.Exception.new(f'JSON Schema {self._keywords} has no keywords')
944
+ schema = type(self)(dict(self._keywords))
945
+ schema._optional = self._optional
946
+ schema._lazy = self._lazy
947
+ schema._schemas = dict(self._schemas)
948
+ return schema
949
+
950
+ def _types(self) -> list[str]:
951
+ types = self._keywords.get('type') if isinstance(self._keywords, dict) else None
952
+ return [types] if isinstance(types, str) else [t for t in cast(list[str], types or []) if t != 'null']
953
+
954
+ def _constraint(self, keywords: dict[str, str], value: float, method: str) -> Self:
955
+ for t in self._types():
956
+ if t in keywords:
957
+ return self.meta({keywords[t]: value})
958
+ raise Pyjo.Exception.new(f'JSON Schema has no {method} for {self._kind()}')
959
+
960
+ def _number(self, keyword: str, value: float, method: str) -> Self:
961
+ if not {'number', 'integer'} & set(self._types()):
962
+ raise Pyjo.Exception.new(f'JSON Schema has no {method} for {self._kind()}')
963
+ return self.meta({keyword: value})
964
+
965
+ def _string(self, keyword: str, value: str, method: str) -> Self:
966
+ if 'string' not in self._types():
967
+ raise Pyjo.Exception.new(f'JSON Schema has no {method} for {self._kind()}')
968
+ return self.meta({keyword: value})
969
+
970
+ def _object(self, additional: Any, method: str) -> Self:
971
+ if 'object' not in self._types():
972
+ raise Pyjo.Exception.new(f'JSON Schema has no {method} for {self._kind()}')
973
+ return self.meta(additionalProperties=additional)
974
+
975
+ def _kind(self) -> str:
976
+ types = self._types()
977
+ return f'the type {types[0]}' if types else 'a schema without a type'
978
+
979
+ # The names of builtins, last, as they hide them in the class
980
+
981
+ @classmethod
982
+ def any(cls) -> Self:
983
+ """::
984
+
985
+ schema = z.any()
986
+
987
+ The schema of any value, ``{}``.
988
+ """
989
+ return cls({})
990
+
991
+ @classmethod
992
+ def int(cls) -> Self:
993
+ """::
994
+
995
+ schema = z.int()
996
+
997
+ The schema of an integer. zod also gives the least and the greatest
998
+ safe integer of JavaScript as its ``minimum`` and ``maximum``, which an
999
+ integer of Python does not have. ::
1000
+
1001
+ # {'type': 'integer', 'minimum': 1, 'maximum': 7}
1002
+ z.int().min(1).max(7).data
1003
+ """
1004
+ return cls({'type': 'integer'})
1005
+
1006
+ @classmethod
1007
+ def enum(cls, values: Iterable[Any] | type[_Enum]) -> Self:
1008
+ """::
1009
+
1010
+ schema = z.enum(['celsius', 'fahrenheit'])
1011
+ schema = z.enum(Unit)
1012
+
1013
+ The schema of one of the values, or of the values of an
1014
+ :class:`enum.Enum`, as ``enum``, with their ``type`` where they have
1015
+ one. ::
1016
+
1017
+ # {'type': 'string', 'enum': ['celsius', 'fahrenheit']}
1018
+ z.enum(['celsius', 'fahrenheit']).data
1019
+ """
1020
+ if isinstance(values, type):
1021
+ return cls(_literal([member.value for member in values], True))
1022
+ return cls(_literal(builtins.list(values), True))
1023
+
1024
+ @classmethod
1025
+ def object(cls, shape: dict[str, Any] | None = None, /, **kwargs: Any) -> Self:
1026
+ """::
1027
+
1028
+ schema = z.object({'city': z.string(), 'days': z.int().optional()})
1029
+ schema = z.object(city=z.string(), days=z.int().optional())
1030
+
1031
+ The schema of an object of the properties, which it requires, but the
1032
+ optional ones, as :meth:`optional` and :meth:`default` make them. A
1033
+ name that is a keyword of Python, or not a name of Python, goes in the
1034
+ :class:`dict`. zod also gives ``additionalProperties: false`` for the
1035
+ output, and this is the schema of the input, which a tool gets. ::
1036
+
1037
+ # {'type': 'object', 'properties': {'class': {'type': 'string'}}, 'required': ['class']}
1038
+ z.object({'class': z.string()}).data
1039
+ """
1040
+ properties = {name: cls.of(value) for name, value in {**(shape or {}), **kwargs}.items()}
1041
+ keywords: dict[str, Any] = {'type': 'object', 'properties': properties}
1042
+ required = [name for name, value in properties.items() if not value._optional]
1043
+ if required:
1044
+ keywords['required'] = required
1045
+ return cls(keywords)
1046
+
1047
+ iso: '_ISO'
1048
+ """::
1049
+
1050
+ schema = z.iso.datetime()
1051
+ schema = z.iso.date()
1052
+ schema = z.iso.time()
1053
+
1054
+ The schemas of the strings of the formats ``date-time``, ``date`` and
1055
+ ``time``, as :meth:`datetime`, :meth:`date` and :meth:`time` of a string.
1056
+ """
1057
+
1058
+
1059
+ class Error(Pyjo.Base.object):
1060
+ """
1061
+ An error of a value, which :meth:`~Pyjo_JSON_Schema.validate` gives, with
1062
+ where it is in the value and in the schema. ::
1063
+
1064
+ # "/days: Expected integer - got string."
1065
+ str(error)
1066
+ """
1067
+
1068
+ def __init__(self, path: str = '', keyword_path: str = '', message: str = '') -> None:
1069
+ self.path = path
1070
+ """::
1071
+
1072
+ path = error.path
1073
+
1074
+ The JSON Pointer of the value in the validated value, as
1075
+ :mod:`Pyjo.JSON.Pointer` takes it, or of the missing property of
1076
+ ``required``.
1077
+ """
1078
+
1079
+ self.keyword_path = keyword_path
1080
+ """::
1081
+
1082
+ keyword_path = error.keyword_path
1083
+
1084
+ The JSON Pointer of the keyword in the schema, through the ``$ref``
1085
+ that the validation follows, as ``/properties/days/type``.
1086
+ """
1087
+
1088
+ self.message = message
1089
+ """::
1090
+
1091
+ message = error.message
1092
+
1093
+ What is wrong with the value.
1094
+ """
1095
+
1096
+ self._expected: list[str] | None = None
1097
+
1098
+ def __json__(self) -> dict[str, str]:
1099
+ """::
1100
+
1101
+ data = error.__json__()
1102
+
1103
+ The error as :mod:`Pyjo.JSON` encodes it, with ``path``,
1104
+ ``keyword_path`` and ``message``.
1105
+ """
1106
+ return {'path': self.path, 'keyword_path': self.keyword_path, 'message': self.message}
1107
+
1108
+ def __repr__(self) -> str:
1109
+ return f'{type(self).__module__}.{type(self).__qualname__}({self.__json__()!r})'
1110
+
1111
+ def __str__(self) -> str:
1112
+ """::
1113
+
1114
+ string = str(error)
1115
+
1116
+ The path, with ``/`` for the whole value, and the message.
1117
+ """
1118
+ return f'{self.path or "/"}: {self.message}'
1119
+
1120
+
1121
+ class Result:
1122
+ """
1123
+ The result of :meth:`~Pyjo_JSON_Schema.validate`, which is true if the
1124
+ value is valid, as the assertions of :mod:`Pyjo.Test` are. ::
1125
+
1126
+ result = schema.validate(value)
1127
+ if not result:
1128
+ for error in result.errors:
1129
+ print(error.path, error.message)
1130
+ """
1131
+
1132
+ def __init__(self, errors: Iterable[Error] = ()) -> None:
1133
+ self.errors: Pyjo.Collection.object[Error] = Pyjo.Collection.new(*errors)
1134
+ """::
1135
+
1136
+ errors = result.errors
1137
+
1138
+ The :class:`Error` objects of the value, in a :mod:`Pyjo.Collection`,
1139
+ empty if the value is valid.
1140
+ """
1141
+
1142
+ def __bool__(self) -> bool:
1143
+ """::
1144
+
1145
+ boolean = bool(result)
1146
+
1147
+ The value of :attr:`is_valid`.
1148
+ """
1149
+ return self.is_valid
1150
+
1151
+ def __json__(self) -> dict[str, Any]:
1152
+ """::
1153
+
1154
+ data = result.__json__()
1155
+
1156
+ The result as :mod:`Pyjo.JSON` encodes it, ``valid`` and the ``errors``.
1157
+ """
1158
+ return {'valid': self.is_valid, 'errors': [error.__json__() for error in self.errors]}
1159
+
1160
+ def __repr__(self) -> str:
1161
+ return f'<{type(self).__module__}.{type(self).__qualname__} is_valid={self.is_valid}>'
1162
+
1163
+ def __str__(self) -> str:
1164
+ """::
1165
+
1166
+ string = str(result)
1167
+
1168
+ The errors, one in each line, or an empty string if the value is
1169
+ valid.
1170
+ """
1171
+ return '\n'.join(str(error) for error in self.errors)
1172
+
1173
+ @property
1174
+ def is_valid(self) -> bool:
1175
+ """::
1176
+
1177
+ boolean = result.is_valid
1178
+
1179
+ True if the value is valid, without errors.
1180
+ """
1181
+ return not len(self.errors)
1182
+
1183
+
1184
+ class _Lazy:
1185
+ """The function of a lazy schema, and the schema that it gives, which the copies of the lazy schema share, so that
1186
+ they refer to the same schema."""
1187
+
1188
+ def __init__(self, getter: Callable[[], Any]) -> None:
1189
+ self.getter = getter
1190
+ self.target: Pyjo_JSON_Schema | None = None
1191
+
1192
+ def resolve(self, schema: Pyjo_JSON_Schema) -> Pyjo_JSON_Schema:
1193
+ if self.target is None:
1194
+ self.target = schema.of(self.getter())
1195
+ return self.target
1196
+
1197
+
1198
+ class _ISO:
1199
+ def __get__(self, obj: Anything, objtype: type[Pyjo_JSON_Schema]) -> Self:
1200
+ self._cls = objtype
1201
+ return self
1202
+
1203
+ def datetime(self) -> Pyjo_JSON_Schema:
1204
+ return self._cls.string().datetime()
1205
+
1206
+ def date(self) -> Pyjo_JSON_Schema:
1207
+ return self._cls.string().date()
1208
+
1209
+ def time(self) -> Pyjo_JSON_Schema:
1210
+ return self._cls.string().time()
1211
+
1212
+
1213
+ Pyjo_JSON_Schema.iso = _ISO()
1214
+
1215
+
1216
+ class _Compiler:
1217
+ """The schemas of the types of the type hints, the same object for the same type in one schema."""
1218
+
1219
+ def __init__(self, cls: type[Pyjo_JSON_Schema]) -> None:
1220
+ self.cls = cls
1221
+ self.done: dict[Any, Pyjo_JSON_Schema] = {}
1222
+ self.doing: set[Any] = set()
1223
+
1224
+ def parameters(self, function: Callable[..., Any], skip: int) -> Pyjo_JSON_Schema:
1225
+ signature, hints = _signature(function)
1226
+ description, params, _ = _docstring(function)
1227
+ properties: dict[str, Pyjo_JSON_Schema] = {}
1228
+ required: list[str] = []
1229
+ additional: Any = False
1230
+ for param in builtins.list(signature.parameters.values())[skip:]:
1231
+ where = f' in the parameter {param.name}'
1232
+ if param.kind is param.VAR_POSITIONAL:
1233
+ raise Pyjo.Exception.new(f'JSON Schema has no positional members for the parameter {param.name}')
1234
+ if param.kind is param.POSITIONAL_ONLY:
1235
+ raise Pyjo.Exception.new(f'JSON Schema has no positional members for the parameter {param.name}')
1236
+ schema = self.compile(hints.get(param.name, Any), where)
1237
+ if param.kind is param.VAR_KEYWORD:
1238
+ additional = schema
1239
+ continue
1240
+ if param.name in params:
1241
+ schema = schema.describe(params[param.name])
1242
+ if param.default is param.empty:
1243
+ required.append(param.name)
1244
+ elif (value := _json_value(param.default)) is not _MISSING:
1245
+ schema = schema.meta(default=value)
1246
+ properties[param.name] = schema
1247
+ keywords: dict[str, Any] = {'type': 'object'}
1248
+ if description:
1249
+ keywords['description'] = description
1250
+ keywords['properties'] = properties
1251
+ if required:
1252
+ keywords['required'] = required
1253
+ keywords['additionalProperties'] = additional
1254
+ return self.cls(keywords)
1255
+
1256
+ def returns(self, function: Callable[..., Any]) -> Pyjo_JSON_Schema | None:
1257
+ _, hints = _signature(function)
1258
+ if 'return' not in hints:
1259
+ return None
1260
+ schema = self.compile(hints['return'], ' in the return value')
1261
+ _, _, description = _docstring(function)
1262
+ return schema.describe(description) if description else schema
1263
+
1264
+ def compile(self, tp: Any, where: str) -> Pyjo_JSON_Schema:
1265
+ cls = self.cls
1266
+ if isinstance(tp, Pyjo_JSON_Schema):
1267
+ return tp
1268
+ if isinstance(tp, (dict, bool)):
1269
+ return cls.new(cast(dict[str, Any] | bool, tp))
1270
+ if tp is None or tp is type(None):
1271
+ return cls.null()
1272
+ if tp is Any or tp is builtins.object:
1273
+ return cls.any()
1274
+ if tp is bool:
1275
+ return cls.boolean()
1276
+ if tp is builtins.int:
1277
+ return cls.int()
1278
+ if tp is float:
1279
+ return cls.number()
1280
+ if tp is str:
1281
+ return cls.string()
1282
+ if isinstance(tp, type) and tp in _FORMATS:
1283
+ return cls.string().meta(format=_FORMATS[tp])
1284
+ if isinstance(tp, type) and issubclass(tp, Pyjo.URL.object):
1285
+ return cls.string().url()
1286
+
1287
+ origin, args = typing.get_origin(tp), typing.get_args(tp)
1288
+ if origin is typing.Annotated:
1289
+ return self.annotated(args[0], args[1:], where)
1290
+ if origin in (typing.Union, types.UnionType):
1291
+ return self.union(args, where)
1292
+ if origin is typing.Literal:
1293
+ return cls(_literal(builtins.list(args)))
1294
+ if origin is not None and origin in _QUALIFIERS:
1295
+ return self.compile(args[0], where)
1296
+ if tp in (list, set, frozenset, builtins.tuple) or origin in (list, set, frozenset):
1297
+ schema = cls.array(self.compile(args[0], where) if args else None)
1298
+ return schema.meta(uniqueItems=True) if set in (tp, origin) or frozenset in (tp, origin) else schema
1299
+ if origin is builtins.tuple:
1300
+ if len(args) == 2 and args[1] is Ellipsis:
1301
+ return cls.array(self.compile(args[0], where))
1302
+ return cls.tuple([self.compile(arg, where) for arg in args if arg != ()])
1303
+ if tp is dict:
1304
+ return cls({'type': 'object'})
1305
+ if origin is dict:
1306
+ if args[0] is not str:
1307
+ raise Pyjo.Exception.new(f'JSON Schema has no names of {_name(args[0])}{where}')
1308
+ return cls.record(self.compile(args[1], where))
1309
+ if isinstance(tp, typing.TypeAliasType):
1310
+ return self.named(tp, where, lambda: self.compile(tp.__value__, where))
1311
+ if isinstance(tp, typing.NewType):
1312
+ return self.compile(tp.__supertype__, where)
1313
+ if isinstance(tp, type) and issubclass(tp, enum.Enum):
1314
+ return self.named(tp, where, lambda: self.enumeration(tp))
1315
+ if typing.is_typeddict(cast(Any, tp)):
1316
+ return self.named(tp, where, lambda: self.typeddict(tp, where))
1317
+ if isinstance(tp, type) and dataclasses.is_dataclass(tp):
1318
+ return self.named(tp, where, lambda: self.dataclass(tp, where))
1319
+ raise Pyjo.Exception.new(f'JSON Schema does not support {_name(tp)}{where}')
1320
+
1321
+ def named(self, tp: Any, where: str, make: Callable[[], Pyjo_JSON_Schema]) -> Pyjo_JSON_Schema:
1322
+ # A type that is in the making refers to itself, the same type again is the same object, which goes to $defs
1323
+ if tp in self.done:
1324
+ return self.done[tp]
1325
+ if tp in self.doing:
1326
+ return self.cls.lazy(lambda: self.done[tp])
1327
+ self.doing.add(tp)
1328
+ try:
1329
+ schema = make()
1330
+ finally:
1331
+ self.doing.discard(tp)
1332
+ schema.name = tp.__name__
1333
+ self.done[tp] = schema
1334
+ return schema
1335
+
1336
+ def annotated(self, tp: Any, metadata: tuple[Any, ...], where: str) -> Pyjo_JSON_Schema:
1337
+ keywords: dict[str, Any] = {}
1338
+ for value in metadata:
1339
+ if isinstance(value, str):
1340
+ keywords['description'] = value
1341
+ elif isinstance(value, dict):
1342
+ keywords.update(cast(dict[str, Any], value))
1343
+ elif isinstance(value, Pyjo_JSON_Schema) and isinstance(value.keywords, dict):
1344
+ keywords.update(value.keywords)
1345
+ schema = self.compile(tp, where)
1346
+ return schema.meta(keywords) if keywords else schema
1347
+
1348
+ def union(self, args: tuple[Any, ...], where: str) -> Pyjo_JSON_Schema:
1349
+ options = [arg for arg in args if arg is not None and arg is not type(None)]
1350
+ schemas = [self.compile(arg, where) for arg in options]
1351
+ if len(schemas) == 1:
1352
+ return schemas[0].nullable() if len(options) < len(args) else schemas[0]
1353
+ if len(options) < len(args):
1354
+ schemas.append(self.cls.null())
1355
+ return self.cls({'anyOf': schemas})
1356
+
1357
+ def enumeration(self, tp: type[enum.Enum]) -> Pyjo_JSON_Schema:
1358
+ schema = self.cls.enum(tp)
1359
+ description = _description(tp)
1360
+ return schema.describe(description) if description else schema
1361
+
1362
+ def typeddict(self, tp: Any, where: str) -> Pyjo_JSON_Schema:
1363
+ hints = _hints(tp, f' of {_name(tp)}')
1364
+ required = cast(frozenset[str], tp.__required_keys__)
1365
+ properties = {name: self.compile(hint, f' in the key {name} of {_name(tp)}') for name, hint in hints.items()}
1366
+ keywords = self.object(tp, properties, [name for name in hints if name in required])
1367
+ if getattr(tp, '__closed__', None):
1368
+ keywords['additionalProperties'] = False
1369
+ extra = getattr(tp, '__extra_items__', None)
1370
+ if extra is not None and getattr(extra, '__name__', None) != 'NoExtraItems':
1371
+ keywords['additionalProperties'] = self.compile(extra, f' in the extra items of {_name(tp)}')
1372
+ return self.cls(keywords)
1373
+
1374
+ def dataclass(self, tp: type[Any], where: str) -> Pyjo_JSON_Schema:
1375
+ hints = _hints(tp, f' of {_name(tp)}')
1376
+ properties: dict[str, Pyjo_JSON_Schema] = {}
1377
+ required: list[str] = []
1378
+ for field in dataclasses.fields(tp):
1379
+ if not field.init:
1380
+ continue
1381
+ schema = self.compile(hints.get(field.name, Any), f' in the field {field.name} of {_name(tp)}')
1382
+ if field.default is dataclasses.MISSING and field.default_factory is dataclasses.MISSING:
1383
+ required.append(field.name)
1384
+ elif (value := _json_value(field.default)) is not _MISSING:
1385
+ schema = schema.meta(default=value)
1386
+ properties[field.name] = schema
1387
+ return self.cls(self.object(tp, properties, required))
1388
+
1389
+ def object(self, tp: Any, properties: dict[str, Pyjo_JSON_Schema], required: list[str]) -> dict[str, Any]:
1390
+ keywords: dict[str, Any] = {'type': 'object'}
1391
+ description = _description(tp)
1392
+ if description:
1393
+ keywords['description'] = description
1394
+ keywords['properties'] = properties
1395
+ if required:
1396
+ keywords['required'] = required
1397
+ return keywords
1398
+
1399
+
1400
+ class _Emitter:
1401
+ """The data of a schema, with $defs for the named schemas used again and the ones that refer to themselves."""
1402
+
1403
+ def __init__(self, root: Pyjo_JSON_Schema) -> None:
1404
+ self.root = root
1405
+ self.targets: dict[builtins.int, Pyjo_JSON_Schema] = {}
1406
+ self.counts: collections.Counter[builtins.int] = collections.Counter()
1407
+ self.recursive: set[builtins.int] = set()
1408
+ self.needed: set[builtins.int] = set()
1409
+ self.names: dict[builtins.int, str] = {}
1410
+ self.defs: dict[str, Any] = {}
1411
+
1412
+ def emit(self) -> dict[str, Any] | bool:
1413
+ root = self.resolve(self.root)
1414
+ self.count(root, [])
1415
+ self.needed = self.recursive | {
1416
+ key for key, count in self.counts.items() if count > 1 and self.targets[key].name is not None
1417
+ }
1418
+ self.needed.discard(id(root))
1419
+ data = self.keywords(root)
1420
+ if self.root is not root:
1421
+ data = self.merge(data, self.keywords(self.root))
1422
+ if self.defs and isinstance(data, dict):
1423
+ data['$defs'] = self.defs
1424
+ return data
1425
+
1426
+ def resolve(self, schema: Pyjo_JSON_Schema) -> Pyjo_JSON_Schema:
1427
+ seen: set[builtins.int] = set()
1428
+ target = schema
1429
+ while target.is_lazy:
1430
+ if id(target) in seen:
1431
+ raise Pyjo.Exception.new('JSON Schema of a lazy schema that gives itself')
1432
+ seen.add(id(target))
1433
+ target = target.resolve()
1434
+ self.targets[id(target)] = target
1435
+ return target
1436
+
1437
+ def count(self, value: Any, stack: list[builtins.int]) -> None:
1438
+ if isinstance(value, Pyjo_JSON_Schema):
1439
+ target = self.resolve(value)
1440
+ key = id(target)
1441
+ if key in stack:
1442
+ self.recursive.add(key)
1443
+ return
1444
+ self.counts[key] += 1
1445
+ if self.counts[key] == 1:
1446
+ stack.append(key)
1447
+ self.count(target.keywords, stack)
1448
+ stack.pop()
1449
+ elif isinstance(value, dict):
1450
+ for item in cast(dict[str, Any], value).values():
1451
+ self.count(item, stack)
1452
+ elif isinstance(value, (list, builtins.tuple)):
1453
+ for item in cast(list[Any], value):
1454
+ self.count(item, stack)
1455
+
1456
+ def value(self, value: Any) -> Any:
1457
+ if isinstance(value, Pyjo_JSON_Schema):
1458
+ target = self.resolve(value)
1459
+ key = id(target)
1460
+ own = self.keywords(value) if value is not target else {}
1461
+ if key == id(self.resolve(self.root)):
1462
+ return self.merge({'$ref': '#'}, own)
1463
+ if key in self.needed:
1464
+ return self.merge({'$ref': f'#/$defs/{self.define(target)}'}, own)
1465
+ return self.merge(self.keywords(target), own)
1466
+ if isinstance(value, dict):
1467
+ return {name: self.value(item) for name, item in cast(dict[str, Any], value).items()}
1468
+ if isinstance(value, (list, builtins.tuple)):
1469
+ return [self.value(item) for item in cast(list[Any], value)]
1470
+ return value
1471
+
1472
+ def keywords(self, schema: Pyjo_JSON_Schema) -> dict[str, Any] | bool:
1473
+ if isinstance(schema.keywords, bool):
1474
+ return schema.keywords
1475
+ return {name: self.value(item) for name, item in schema.keywords.items()}
1476
+
1477
+ def define(self, target: Pyjo_JSON_Schema) -> str:
1478
+ key = id(target)
1479
+ if key not in self.names:
1480
+ base = target.name or 'Schema'
1481
+ name, n = base, 1
1482
+ while name in self.defs or name in self.names.values():
1483
+ n += 1
1484
+ name = f'{base}{n}'
1485
+ self.names[key] = name
1486
+ self.defs[name] = {}
1487
+ self.defs[name] = self.keywords(target)
1488
+ return self.names[key]
1489
+
1490
+ def merge(self, data: dict[str, Any] | bool, own: dict[str, Any] | bool) -> dict[str, Any] | bool:
1491
+ if isinstance(own, bool) or not own:
1492
+ return data
1493
+ if isinstance(data, bool):
1494
+ return own if data else {**own, 'not': {}}
1495
+ return {**data, **own}
1496
+
1497
+
1498
+ class _Index:
1499
+ """The schemas by their URI, of $id, $anchor and the registry, with the base URI of each schema."""
1500
+
1501
+ def __init__(
1502
+ self, root: dict[str, Any] | bool, schemas: dict[str, Pyjo_JSON_Schema | dict[str, Any] | bool]
1503
+ ) -> None:
1504
+ self.resources: dict[str, dict[str, Any] | bool] = {}
1505
+ self.anchors: dict[str, dict[str, Any]] = {}
1506
+ self.bases: dict[builtins.int, str] = {}
1507
+ self.schemas = dict(schemas)
1508
+ self.keep: list[Any] = []
1509
+ self.patterns: dict[str, re.Pattern[str]] = {}
1510
+ self.root = root
1511
+ self.add(root, '')
1512
+
1513
+ def add(self, schema: Pyjo_JSON_Schema | dict[str, Any] | bool, uri: str) -> None:
1514
+ data = schema.data if isinstance(schema, Pyjo_JSON_Schema) else schema
1515
+ # The ids of the schemas are the keys of the bases, so the schemas stay
1516
+ self.keep.append(data)
1517
+ self.resources[uri] = data
1518
+ self.walk(data, uri, '')
1519
+
1520
+ def walk(self, schema: Any, base: str, where: str) -> None:
1521
+ if isinstance(schema, bool):
1522
+ return
1523
+ if not isinstance(schema, dict):
1524
+ raise Pyjo.Exception.new(f'JSON Schema has no schema in {where or "/"}')
1525
+ schema = cast(dict[str, Any], schema)
1526
+ dialect = schema.get('$schema')
1527
+ if dialect is not None and (not isinstance(dialect, str) or dialect.rstrip('#') != DIALECT):
1528
+ raise Pyjo.Exception.new(f'JSON Schema does not support the dialect {dialect}')
1529
+ for keyword in _UNSUPPORTED:
1530
+ if keyword in schema:
1531
+ raise Pyjo.Exception.new(f'JSON Schema does not support {keyword} in {where or "/"}')
1532
+ if isinstance(schema.get('$id'), str):
1533
+ base = _uri(base, schema['$id'])[0]
1534
+ self.resources.setdefault(base, schema)
1535
+ for keyword in ('$anchor', '$dynamicAnchor'):
1536
+ if isinstance(schema.get(keyword), str):
1537
+ self.anchors[f'{base}#{schema[keyword]}'] = schema
1538
+ self.bases[id(schema)] = base
1539
+ for keyword, value in schema.items():
1540
+ if keyword in _SCHEMA:
1541
+ self.walk(value, base, f'{where}/{keyword}')
1542
+ elif keyword in _SCHEMAS and isinstance(value, dict):
1543
+ for name, item in cast(dict[str, Any], value).items():
1544
+ self.walk(item, base, f'{where}/{keyword}/{_escape(name)}')
1545
+ elif keyword in _SCHEMA_LISTS and isinstance(value, list):
1546
+ for i, item in enumerate(cast(list[Any], value)):
1547
+ self.walk(item, base, f'{where}/{keyword}/{i}')
1548
+ elif keyword in _SCHEMAS or keyword in _SCHEMA_LISTS:
1549
+ raise Pyjo.Exception.new(f'JSON Schema has no schemas in {where}/{keyword}')
1550
+
1551
+ def pattern(self, source: str) -> re.Pattern[str]:
1552
+ # The regular expressions of the schemas, which are compiled once for the schema
1553
+ pattern = self.patterns.get(source)
1554
+ if pattern is None:
1555
+ pattern = self.patterns[source] = _regex(source)
1556
+ return pattern
1557
+
1558
+ def resolve(self, base: str, ref: str, where: str) -> tuple[dict[str, Any] | bool, str]:
1559
+ uri, fragment = _uri(base, ref)
1560
+ if uri not in self.resources and uri in self.schemas:
1561
+ self.add(self.schemas.pop(uri), uri)
1562
+ if uri not in self.resources:
1563
+ # An embedded resource of a schema of the registry, which has to be indexed to be found, of the schemas
1564
+ # that the validation supports
1565
+ while self.schemas:
1566
+ key, schema = self.schemas.popitem()
1567
+ with contextlib.suppress(Pyjo.Exception.object):
1568
+ self.add(schema, key)
1569
+ target: Any = None
1570
+ if fragment.startswith('/') or not fragment:
1571
+ pointer = Pyjo.JSON.Pointer.new(self.resources.get(uri))
1572
+ if uri in self.resources and pointer.contains(fragment):
1573
+ target = pointer.get(fragment)
1574
+ else:
1575
+ target = self.anchors.get(f'{uri}#{fragment}')
1576
+ if not isinstance(target, (dict, bool)):
1577
+ raise Pyjo.Exception.new(f'JSON Schema has no schema of the $ref {ref} in {where}')
1578
+ found = cast(dict[str, Any] | bool, target)
1579
+ return found, self.bases.get(id(found), uri)
1580
+
1581
+
1582
+ @dataclasses.dataclass(slots=True)
1583
+ class _At:
1584
+ """A value and a schema whose keywords validate it, with the JSON Pointers of the value and of the keyword."""
1585
+
1586
+ value: Any
1587
+ kind: str
1588
+ schema: dict[str, Any]
1589
+ base: str
1590
+ path: str
1591
+ where: str
1592
+ errors: list[Error]
1593
+
1594
+ def error(self, message: str, path: str | None = None, where: str | None = None) -> Error:
1595
+ error = Error(self.path if path is None else path, self.where if where is None else where, message)
1596
+ self.errors.append(error)
1597
+ return error
1598
+
1599
+ def child(self, name: Any) -> str:
1600
+ return f'{self.path}/{_escape(name)}'
1601
+
1602
+
1603
+ class _Validator:
1604
+ """The evaluation of a value against a schema, which collects the errors."""
1605
+
1606
+ def __init__(self, index: _Index, formats: bool) -> None:
1607
+ self.index = index
1608
+ self.formats = formats
1609
+ self.stack: set[tuple[builtins.int, str]] = set()
1610
+
1611
+ def validate(self, value: Any) -> list[Error]:
1612
+ errors: list[Error] = []
1613
+ _json_errors(value, '', errors)
1614
+ if not errors:
1615
+ self.evaluate(value, self.index.root, '', '', '', errors)
1616
+ return errors
1617
+
1618
+ def evaluate(
1619
+ self, value: Any, schema: dict[str, Any] | bool, base: str, path: str, where: str, errors: list[Error]
1620
+ ) -> None:
1621
+ if schema is True:
1622
+ return
1623
+ if schema is False:
1624
+ errors.append(Error(path, where, 'Not allowed.'))
1625
+ return
1626
+ key = (id(schema), path)
1627
+ if key in self.stack:
1628
+ raise Pyjo.Exception.new(f'JSON Schema refers to itself in {where} without a value of its own')
1629
+ self.stack.add(key)
1630
+ try:
1631
+ at = _At(value, _instance_type(value), schema, self.index.bases.get(id(schema), base), path, where, errors)
1632
+ for keyword, argument in schema.items():
1633
+ check = _KEYWORDS.get(keyword)
1634
+ if check is not None:
1635
+ at.where = f'{where}/{keyword}'
1636
+ check(self, at, argument)
1637
+ finally:
1638
+ self.stack.discard(key)
1639
+
1640
+ def errors(
1641
+ self, value: Any, schema: Any, at: _At, path: str | None = None, where: str | None = None
1642
+ ) -> list[Error]:
1643
+ errors: list[Error] = []
1644
+ self.evaluate(
1645
+ value, schema, at.base, at.path if path is None else path, at.where if where is None else where, errors
1646
+ )
1647
+ return errors
1648
+
1649
+ def apply(self, value: Any, schema: Any, at: _At, path: str | None = None, where: str | None = None) -> None:
1650
+ self.evaluate(
1651
+ value, schema, at.base, at.path if path is None else path, at.where if where is None else where, at.errors
1652
+ )
1653
+
1654
+ # Types
1655
+
1656
+ def type(self, at: _At, argument: Any) -> None:
1657
+ types = [argument] if isinstance(argument, str) else cast(list[str], argument)
1658
+ if at.kind not in types and not (at.kind == 'integer' and 'number' in types):
1659
+ error = at.error(f'Expected {" or ".join(types)} - got {_type_name(at.value)}.')
1660
+ error._expected = types # pyright: ignore[reportPrivateUsage]
1661
+
1662
+ def enum(self, at: _At, argument: Any) -> None:
1663
+ if _canonical(at.value) not in [_canonical(item) for item in cast(list[Any], argument)]:
1664
+ at.error(f'Not in enum: {", ".join(to_json(item) for item in argument)}.')
1665
+
1666
+ def const(self, at: _At, argument: Any) -> None:
1667
+ if _canonical(at.value) != _canonical(argument):
1668
+ at.error(f'Not the const: {to_json(argument)}.')
1669
+
1670
+ # Numbers
1671
+
1672
+ def minimum(self, at: _At, argument: Any) -> None:
1673
+ if at.kind in _NUMBERS and at.value < argument:
1674
+ at.error(f'Less than the minimum {to_json(argument)}.')
1675
+
1676
+ def maximum(self, at: _At, argument: Any) -> None:
1677
+ if at.kind in _NUMBERS and at.value > argument:
1678
+ at.error(f'Greater than the maximum {to_json(argument)}.')
1679
+
1680
+ def exclusive_minimum(self, at: _At, argument: Any) -> None:
1681
+ if at.kind in _NUMBERS and at.value <= argument:
1682
+ at.error(f'Not greater than the exclusive minimum {to_json(argument)}.')
1683
+
1684
+ def exclusive_maximum(self, at: _At, argument: Any) -> None:
1685
+ if at.kind in _NUMBERS and at.value >= argument:
1686
+ at.error(f'Not less than the exclusive maximum {to_json(argument)}.')
1687
+
1688
+ def multiple_of(self, at: _At, argument: Any) -> None:
1689
+ if at.kind in _NUMBERS and (_fraction(at.value) / _fraction(argument)).denominator != 1:
1690
+ at.error(f'Not a multiple of {to_json(argument)}.')
1691
+
1692
+ # Strings
1693
+
1694
+ def min_length(self, at: _At, argument: Any) -> None:
1695
+ if at.kind == 'string' and len(at.value) < argument:
1696
+ at.error(f'Shorter than {to_json(argument)} characters.')
1697
+
1698
+ def max_length(self, at: _At, argument: Any) -> None:
1699
+ if at.kind == 'string' and len(at.value) > argument:
1700
+ at.error(f'Longer than {to_json(argument)} characters.')
1701
+
1702
+ def pattern(self, at: _At, argument: Any) -> None:
1703
+ if at.kind == 'string' and not self.index.pattern(argument).search(at.value):
1704
+ at.error(f'Does not match the pattern {argument}.')
1705
+
1706
+ def format(self, at: _At, argument: Any) -> None:
1707
+ check = _FORMAT_CHECKS.get(argument)
1708
+ if self.formats and at.kind == 'string' and check is not None and not check(at.value):
1709
+ at.error(f'Does not match the format {argument}.')
1710
+
1711
+ # Arrays
1712
+
1713
+ def items(self, at: _At, argument: Any) -> None:
1714
+ if at.kind == 'array':
1715
+ start = len(cast(list[Any], at.schema.get('prefixItems', [])))
1716
+ for i, item in enumerate(at.value[start:], start):
1717
+ self.apply(item, argument, at, at.child(i))
1718
+
1719
+ def prefix_items(self, at: _At, argument: Any) -> None:
1720
+ if at.kind == 'array':
1721
+ for i, (item, schema) in enumerate(zip(at.value, argument, strict=False)):
1722
+ self.apply(item, schema, at, at.child(i), f'{at.where}/{i}')
1723
+
1724
+ def min_items(self, at: _At, argument: Any) -> None:
1725
+ if at.kind == 'array' and len(at.value) < argument:
1726
+ at.error(f'Fewer than {to_json(argument)} items.')
1727
+
1728
+ def max_items(self, at: _At, argument: Any) -> None:
1729
+ if at.kind == 'array' and len(at.value) > argument:
1730
+ at.error(f'More than {to_json(argument)} items.')
1731
+
1732
+ def unique_items(self, at: _At, argument: Any) -> None:
1733
+ if at.kind == 'array' and argument is True:
1734
+ seen: dict[Any, builtins.int] = {}
1735
+ for i, item in enumerate(at.value):
1736
+ first = seen.setdefault(_canonical(item), i)
1737
+ if first != i:
1738
+ at.error(f'Items {first} and {i} are equal.')
1739
+ return
1740
+
1741
+ def contains(self, at: _At, argument: Any) -> None:
1742
+ if at.kind != 'array':
1743
+ return
1744
+ count = sum(not self.errors(item, argument, at, at.child(i)) for i, item in enumerate(at.value))
1745
+ least, most = at.schema.get('minContains', 1), at.schema.get('maxContains')
1746
+ parent = at.where.rpartition('/')[0]
1747
+ if count < least and 'minContains' in at.schema:
1748
+ at.error(f'Fewer than {to_json(least)} items match the schema of contains.', where=f'{parent}/minContains')
1749
+ elif count < least:
1750
+ at.error('No item matches the schema of contains.')
1751
+ if most is not None and count > most:
1752
+ at.error(f'More than {to_json(most)} items match the schema of contains.', where=f'{parent}/maxContains')
1753
+
1754
+ # Objects
1755
+
1756
+ def properties(self, at: _At, argument: Any) -> None:
1757
+ if at.kind == 'object':
1758
+ for name, schema in cast(dict[str, Any], argument).items():
1759
+ if name in at.value:
1760
+ self.apply(at.value[name], schema, at, at.child(name), f'{at.where}/{_escape(name)}')
1761
+
1762
+ def pattern_properties(self, at: _At, argument: Any) -> None:
1763
+ if at.kind == 'object':
1764
+ for pattern, schema in cast(dict[str, Any], argument).items():
1765
+ for name, item in cast(dict[str, Any], at.value).items():
1766
+ if self.index.pattern(pattern).search(name):
1767
+ self.apply(item, schema, at, at.child(name), f'{at.where}/{_escape(pattern)}')
1768
+
1769
+ def additional_properties(self, at: _At, argument: Any) -> None:
1770
+ if at.kind != 'object':
1771
+ return
1772
+ properties = cast(dict[str, Any], at.schema.get('properties', {}))
1773
+ patterns = [
1774
+ self.index.pattern(pattern) for pattern in cast(dict[str, Any], at.schema.get('patternProperties', {}))
1775
+ ]
1776
+ for name, item in cast(dict[str, Any], at.value).items():
1777
+ if name in properties or any(pattern.search(name) for pattern in patterns):
1778
+ continue
1779
+ if argument is False:
1780
+ at.error('Property not allowed.', at.child(name))
1781
+ else:
1782
+ self.apply(item, argument, at, at.child(name))
1783
+
1784
+ def property_names(self, at: _At, argument: Any) -> None:
1785
+ if at.kind == 'object':
1786
+ for name in at.value:
1787
+ self.apply(name, argument, at, at.child(name))
1788
+
1789
+ def required(self, at: _At, argument: Any) -> None:
1790
+ if at.kind == 'object':
1791
+ for name in cast(list[str], argument):
1792
+ if name not in at.value:
1793
+ at.error('Missing property.', at.child(name))
1794
+
1795
+ def min_properties(self, at: _At, argument: Any) -> None:
1796
+ if at.kind == 'object' and len(at.value) < argument:
1797
+ at.error(f'Fewer than {to_json(argument)} properties.')
1798
+
1799
+ def max_properties(self, at: _At, argument: Any) -> None:
1800
+ if at.kind == 'object' and len(at.value) > argument:
1801
+ at.error(f'More than {to_json(argument)} properties.')
1802
+
1803
+ def dependent_required(self, at: _At, argument: Any) -> None:
1804
+ if at.kind == 'object':
1805
+ for name, names in cast(dict[str, list[str]], argument).items():
1806
+ for required in names if name in at.value else []:
1807
+ if required not in at.value:
1808
+ message = f'Missing property, which {to_json(name)} requires.'
1809
+ at.error(message, at.child(required), f'{at.where}/{_escape(name)}')
1810
+
1811
+ def dependent_schemas(self, at: _At, argument: Any) -> None:
1812
+ if at.kind == 'object':
1813
+ for name, schema in cast(dict[str, Any], argument).items():
1814
+ if name in at.value:
1815
+ self.apply(at.value, schema, at, where=f'{at.where}/{_escape(name)}')
1816
+
1817
+ # Combinations
1818
+
1819
+ def all_of(self, at: _At, argument: Any) -> None:
1820
+ for i, schema in enumerate(cast(list[Any], argument)):
1821
+ self.apply(at.value, schema, at, where=f'{at.where}/{i}')
1822
+
1823
+ def any_of(self, at: _At, argument: Any) -> None:
1824
+ failures: list[list[Error]] = []
1825
+ for i, schema in enumerate(cast(list[Any], argument)):
1826
+ failure = self.errors(at.value, schema, at, where=f'{at.where}/{i}')
1827
+ if not failure:
1828
+ return
1829
+ failures.append(failure)
1830
+ at.errors.extend(_mismatch(at, failures))
1831
+
1832
+ def one_of(self, at: _At, argument: Any) -> None:
1833
+ failures: list[list[Error]] = []
1834
+ valid: list[str] = []
1835
+ for i, schema in enumerate(cast(list[Any], argument)):
1836
+ failure = self.errors(at.value, schema, at, where=f'{at.where}/{i}')
1837
+ if failure:
1838
+ failures.append(failure)
1839
+ else:
1840
+ valid.append(str(i))
1841
+ if len(valid) > 1:
1842
+ at.error(f'Matches more than one schema of oneOf: {", ".join(valid)}.')
1843
+ elif not valid:
1844
+ at.errors.extend(_mismatch(at, failures))
1845
+
1846
+ def not_(self, at: _At, argument: Any) -> None:
1847
+ if not self.errors(at.value, argument, at):
1848
+ at.error('Matches the schema of not.')
1849
+
1850
+ def if_(self, at: _At, argument: Any) -> None:
1851
+ keyword = 'else' if self.errors(at.value, argument, at) else 'then'
1852
+ if keyword in at.schema:
1853
+ self.apply(at.value, at.schema[keyword], at, where=f'{at.where.rpartition("/")[0]}/{keyword}')
1854
+
1855
+ # References
1856
+
1857
+ def ref(self, at: _At, argument: Any) -> None:
1858
+ target, base = self.index.resolve(at.base, argument, at.where)
1859
+ self.evaluate(at.value, target, base, at.path, at.where, at.errors)
1860
+
1861
+ def unsupported(self, at: _At, argument: Any) -> None:
1862
+ raise Pyjo.Exception.new(f'JSON Schema does not support {at.where.rpartition("/")[2]} in {at.where}')
1863
+
1864
+
1865
+ type _Check = Callable[[_Validator, _At, Any], None]
1866
+
1867
+ # The keywords of the validation, and the ones of 2020-12 that it does not support, which a schema refers to through
1868
+ # a $ref of a JSON Pointer too
1869
+ _KEYWORDS: dict[str, _Check] = {
1870
+ 'type': _Validator.type,
1871
+ 'enum': _Validator.enum,
1872
+ 'const': _Validator.const,
1873
+ 'minimum': _Validator.minimum,
1874
+ 'maximum': _Validator.maximum,
1875
+ 'exclusiveMinimum': _Validator.exclusive_minimum,
1876
+ 'exclusiveMaximum': _Validator.exclusive_maximum,
1877
+ 'multipleOf': _Validator.multiple_of,
1878
+ 'minLength': _Validator.min_length,
1879
+ 'maxLength': _Validator.max_length,
1880
+ 'pattern': _Validator.pattern,
1881
+ 'format': _Validator.format,
1882
+ 'items': _Validator.items,
1883
+ 'prefixItems': _Validator.prefix_items,
1884
+ 'minItems': _Validator.min_items,
1885
+ 'maxItems': _Validator.max_items,
1886
+ 'uniqueItems': _Validator.unique_items,
1887
+ 'contains': _Validator.contains,
1888
+ 'properties': _Validator.properties,
1889
+ 'patternProperties': _Validator.pattern_properties,
1890
+ 'additionalProperties': _Validator.additional_properties,
1891
+ 'propertyNames': _Validator.property_names,
1892
+ 'required': _Validator.required,
1893
+ 'minProperties': _Validator.min_properties,
1894
+ 'maxProperties': _Validator.max_properties,
1895
+ 'dependentRequired': _Validator.dependent_required,
1896
+ 'dependentSchemas': _Validator.dependent_schemas,
1897
+ 'allOf': _Validator.all_of,
1898
+ 'anyOf': _Validator.any_of,
1899
+ 'oneOf': _Validator.one_of,
1900
+ 'not': _Validator.not_,
1901
+ 'if': _Validator.if_,
1902
+ '$ref': _Validator.ref,
1903
+ '$dynamicRef': _Validator.unsupported,
1904
+ 'unevaluatedItems': _Validator.unsupported,
1905
+ 'unevaluatedProperties': _Validator.unsupported,
1906
+ }
1907
+
1908
+ # The types of the numbers, and the keywords of 2020-12 that the validation does not support
1909
+ _NUMBERS = ('integer', 'number')
1910
+ _UNSUPPORTED = ('$dynamicRef', 'unevaluatedItems', 'unevaluatedProperties')
1911
+
1912
+ # The keywords of a schema, of an object of schemas and of an array of schemas
1913
+ _SCHEMA = (
1914
+ 'additionalProperties',
1915
+ 'contains',
1916
+ 'contentSchema',
1917
+ 'else',
1918
+ 'if',
1919
+ 'items',
1920
+ 'not',
1921
+ 'propertyNames',
1922
+ 'then',
1923
+ )
1924
+ _SCHEMAS = ('$defs', 'dependentSchemas', 'patternProperties', 'properties')
1925
+ _SCHEMA_LISTS = ('allOf', 'anyOf', 'oneOf', 'prefixItems')
1926
+
1927
+
1928
+ def _mismatch(at: _At, failures: list[list[Error]]) -> list[Error]:
1929
+ # The errors of the schemas of anyOf or oneOf of the type of the value, or one of the types that they expect
1930
+ def expected(failure: list[Error]) -> list[str] | None:
1931
+ for error in failure:
1932
+ if error._expected is not None and error.path == at.path: # pyright: ignore[reportPrivateUsage]
1933
+ return error._expected # pyright: ignore[reportPrivateUsage]
1934
+ return None
1935
+
1936
+ errors = [error for failure in failures if expected(failure) is None for error in failure]
1937
+ if errors:
1938
+ return errors
1939
+ types = list(dict.fromkeys(t for failure in failures for t in expected(failure) or []))
1940
+ error = Error(at.path, at.where, f'Expected {" or ".join(types)} - got {_type_name(at.value)}.')
1941
+ error._expected = types # pyright: ignore[reportPrivateUsage]
1942
+ return [error]
1943
+
1944
+
1945
+ def _json_errors(value: Any, path: str, errors: list[Error]) -> None:
1946
+ # The values that JSON does not have, which no schema validates
1947
+ if isinstance(value, dict):
1948
+ for name, item in cast(dict[Any, Any], value).items():
1949
+ if not isinstance(name, str):
1950
+ errors.append(Error(path, '', f'Not a JSON value: the name {name!r}.'))
1951
+ else:
1952
+ _json_errors(item, f'{path}/{_escape(name)}', errors)
1953
+ elif isinstance(value, (list, builtins.tuple)):
1954
+ for i, item in enumerate(cast(list[Any], value)):
1955
+ _json_errors(item, f'{path}/{i}', errors)
1956
+ elif isinstance(value, float) and not math.isfinite(value):
1957
+ errors.append(Error(path, '', f'Not a JSON value: {value!r}.'))
1958
+ elif not (value is None or isinstance(value, (bool, builtins.int, float, str))):
1959
+ errors.append(Error(path, '', f'Not a JSON value: {type(value).__name__}.'))
1960
+
1961
+
1962
+ def _instance_type(value: Any) -> str:
1963
+ if isinstance(value, float):
1964
+ return 'integer' if value.is_integer() else 'number'
1965
+ return _type_name(value)
1966
+
1967
+
1968
+ def _type_name(value: Any) -> str:
1969
+ return _json_type(value) or ('object' if isinstance(value, dict) else 'array')
1970
+
1971
+
1972
+ def _canonical(value: Any) -> Any:
1973
+ # A value that is equal to another one as JSON has it: 1 and 1.0 are, True and 1 are not
1974
+ if isinstance(value, bool) or value is None:
1975
+ return ('literal', value)
1976
+ if isinstance(value, (builtins.int, float)):
1977
+ return ('number', value)
1978
+ if isinstance(value, dict):
1979
+ return ('object', frozenset((name, _canonical(item)) for name, item in cast(dict[str, Any], value).items()))
1980
+ if isinstance(value, (list, builtins.tuple)):
1981
+ return ('array', builtins.tuple(_canonical(item) for item in cast(list[Any], value)))
1982
+ return ('string', value)
1983
+
1984
+
1985
+ def _fraction(number: float) -> fractions.Fraction:
1986
+ # The number as JSON writes it, so that 0.0075 is a multiple of 0.0001
1987
+ return fractions.Fraction(number) if isinstance(number, builtins.int) else fractions.Fraction(repr(number))
1988
+
1989
+
1990
+ def _escape(name: Any) -> str:
1991
+ return str(name).replace('~', '~0').replace('/', '~1')
1992
+
1993
+
1994
+ def _uri(base: str, ref: str) -> tuple[str, str]:
1995
+ # The URI of a reference without its fragment, and the fragment, decoded
1996
+ url = Pyjo.URL.new(ref).to_abs(Pyjo.URL.new(base))
1997
+ fragment = url.fragment or ''
1998
+ url.fragment = None
1999
+ return url.to_str(), fragment
2000
+
2001
+
2002
+ # The classes of ECMAScript, ASCII but the white space, as ranges of code points
2003
+ _ECMA_CLASSES: dict[str, list[builtins.tuple[builtins.int, builtins.int]]] = {
2004
+ 'd': [(0x30, 0x39)],
2005
+ 'w': [(0x30, 0x39), (0x41, 0x5A), (0x5F, 0x5F), (0x61, 0x7A)],
2006
+ 's': [
2007
+ (0x09, 0x0D),
2008
+ (0x20, 0x20),
2009
+ (0xA0, 0xA0),
2010
+ (0x1680, 0x1680),
2011
+ (0x2000, 0x200A),
2012
+ (0x2028, 0x2029),
2013
+ (0x202F, 0x202F),
2014
+ (0x205F, 0x205F),
2015
+ (0x3000, 0x3000),
2016
+ (0xFEFF, 0xFEFF),
2017
+ ],
2018
+ }
2019
+
2020
+
2021
+ def _ranges(ranges: list[builtins.tuple[builtins.int, builtins.int]], negate: bool) -> str:
2022
+ if negate:
2023
+ ranges = _complement(ranges)
2024
+ return ''.join(f'\\U{low:08x}' if low == high else f'\\U{low:08x}-\\U{high:08x}' for low, high in ranges)
2025
+
2026
+
2027
+ # The groups of ECMAScript, with the modifiers of ES2025
2028
+ _RE_ECMA_GROUP = re.compile(r'\(\?(?:[:=!]|<[=!]|[ims]*(?:-[ims]*)?:)')
2029
+
2030
+ # The general categories of Unicode, by their aliases, which \p of ECMAScript takes
2031
+ _CATEGORIES = {
2032
+ 'C': 'C Other',
2033
+ 'Cc': 'Cc Control cntrl',
2034
+ 'Cf': 'Cf Format',
2035
+ 'Cn': 'Cn Unassigned',
2036
+ 'Co': 'Co Private_Use',
2037
+ 'Cs': 'Cs Surrogate',
2038
+ 'L': 'L Letter',
2039
+ 'LC': 'LC Cased_Letter',
2040
+ 'Ll': 'Ll Lowercase_Letter',
2041
+ 'Lm': 'Lm Modifier_Letter',
2042
+ 'Lo': 'Lo Other_Letter',
2043
+ 'Lt': 'Lt Titlecase_Letter',
2044
+ 'Lu': 'Lu Uppercase_Letter',
2045
+ 'M': 'M Mark Combining_Mark',
2046
+ 'Mc': 'Mc Spacing_Mark',
2047
+ 'Me': 'Me Enclosing_Mark',
2048
+ 'Mn': 'Mn Nonspacing_Mark',
2049
+ 'N': 'N Number',
2050
+ 'Nd': 'Nd Decimal_Number digit',
2051
+ 'Nl': 'Nl Letter_Number',
2052
+ 'No': 'No Other_Number',
2053
+ 'P': 'P Punctuation punct',
2054
+ 'Pc': 'Pc Connector_Punctuation',
2055
+ 'Pd': 'Pd Dash_Punctuation',
2056
+ 'Pe': 'Pe Close_Punctuation',
2057
+ 'Pf': 'Pf Final_Punctuation',
2058
+ 'Pi': 'Pi Initial_Punctuation',
2059
+ 'Po': 'Po Other_Punctuation',
2060
+ 'Ps': 'Ps Open_Punctuation',
2061
+ 'S': 'S Symbol',
2062
+ 'Sc': 'Sc Currency_Symbol',
2063
+ 'Sk': 'Sk Modifier_Symbol',
2064
+ 'Sm': 'Sm Math_Symbol',
2065
+ 'So': 'So Other_Symbol',
2066
+ 'Z': 'Z Separator',
2067
+ 'Zl': 'Zl Line_Separator',
2068
+ 'Zp': 'Zp Paragraph_Separator',
2069
+ 'Zs': 'Zs Space_Separator',
2070
+ }
2071
+ _CATEGORY_ALIASES = {alias: category for category, aliases in _CATEGORIES.items() for alias in aliases.split()}
2072
+
2073
+
2074
+ @functools.cache
2075
+ def _category_ranges() -> dict[str, list[builtins.tuple[builtins.int, builtins.int]]]:
2076
+ # The ranges of the code points of each general category of unicodedata, once a pattern has \p
2077
+ ranges: dict[str, list[builtins.tuple[builtins.int, builtins.int]]] = {}
2078
+ start, current = 0, unicodedata.category('\0')
2079
+ for code in range(1, sys.maxunicode + 2):
2080
+ category = unicodedata.category(chr(code)) if code <= sys.maxunicode else ''
2081
+ if category != current:
2082
+ ranges.setdefault(current, []).append((start, code - 1))
2083
+ start, current = code, category
2084
+ return ranges
2085
+
2086
+
2087
+ def _property(source: str, name: str) -> list[builtins.tuple[builtins.int, builtins.int]]:
2088
+ # The code points of a property of \p, a general category or ASCII, Any and Assigned
2089
+ name = name.removeprefix('General_Category=').removeprefix('gc=')
2090
+ if name in ('ASCII', 'Any'):
2091
+ return [(0, 0x7F if name == 'ASCII' else sys.maxunicode)]
2092
+ category = 'Cn' if name == 'Assigned' else _CATEGORY_ALIASES.get(name)
2093
+ if category is None:
2094
+ raise Pyjo.Exception.new(f'JSON Schema does not support the pattern {source}: the property {name}')
2095
+ ranges = sorted(
2096
+ r
2097
+ for c, rs in _category_ranges().items()
2098
+ if c == category or (len(category) == 1 and c[0] == category) or (category == 'LC' and c in ('Ll', 'Lt', 'Lu'))
2099
+ for r in rs
2100
+ )
2101
+ if name == 'Assigned':
2102
+ return _complement(ranges)
2103
+ return ranges
2104
+
2105
+
2106
+ def _complement(
2107
+ ranges: list[builtins.tuple[builtins.int, builtins.int]],
2108
+ ) -> list[builtins.tuple[builtins.int, builtins.int]]:
2109
+ complement: list[builtins.tuple[builtins.int, builtins.int]] = []
2110
+ start = 0
2111
+ for low, high in ranges:
2112
+ if low > start:
2113
+ complement.append((start, low - 1))
2114
+ start = high + 1
2115
+ return [*complement, (start, sys.maxunicode)] if start <= sys.maxunicode else complement
2116
+
2117
+
2118
+ def _regex(source: str) -> re.Pattern[str]:
2119
+ # A regular expression of ECMAScript as one of Python: \d, \w and \b are ASCII, \s has the white space of
2120
+ # ECMAScript, . matches no line terminator, $ only the end, and the named groups have the syntax of Python
2121
+ out: list[str] = []
2122
+ i, in_class = 0, False
2123
+ while i < len(source):
2124
+ c = source[i]
2125
+ if c == '\\' and i + 1 < len(source):
2126
+ e = source[i + 1]
2127
+ i += 2
2128
+ if e in 'dDsSwW' or (e in 'pP' and source.startswith('{', i) and '}' in source[i:]):
2129
+ if e in 'pP':
2130
+ end = source.index('}', i)
2131
+ ranges = _ranges(_property(source, source[i + 1 : end]), e == 'P')
2132
+ i = end + 1
2133
+ else:
2134
+ ranges = _ranges(_ECMA_CLASSES[e.lower()], e.isupper())
2135
+ out.append(ranges if in_class else f'[{ranges}]')
2136
+ elif e in 'bB' and not in_class:
2137
+ out.append(f'(?a:\\{e})')
2138
+ elif e == 'k' and source.startswith('<', i) and '>' in source[i:]:
2139
+ end = source.index('>', i)
2140
+ out.append(f'(?P={source[i + 1 : end]})')
2141
+ i = end + 1
2142
+ elif e == 'c' and i < len(source) and source[i].isascii() and source[i].isalpha():
2143
+ out.append(f'\\x{ord(source[i]) % 32:02x}')
2144
+ i += 1
2145
+ elif e == 'u' and source.startswith('{', i) and '}' in source[i:]:
2146
+ end = source.index('}', i)
2147
+ out.append(f'\\U{builtins.int(source[i + 1 : end], 16):08x}')
2148
+ i = end + 1
2149
+ elif e.isascii() and e.isalpha() and e not in 'bBcdDfknpPrsStuvwWx':
2150
+ raise Pyjo.Exception.new(f'JSON Schema does not support the pattern {source}: \\{e} of Python')
2151
+ else:
2152
+ out.append(c + e)
2153
+ continue
2154
+ if in_class:
2155
+ if c == ']':
2156
+ in_class = False
2157
+ out.append(f'\\{c}' if c in '[&~|' else c)
2158
+ elif source.startswith('[]', i):
2159
+ out.append('(?!)')
2160
+ i += 1
2161
+ elif source.startswith('[^]', i):
2162
+ out.append('[\\s\\S]')
2163
+ i += 2
2164
+ elif c == '[':
2165
+ in_class = True
2166
+ out.append('[^' if source.startswith('[^', i) else '[')
2167
+ i += 1 if source.startswith('[^', i) else 0
2168
+ elif c == '.':
2169
+ out.append('[^\\n\\r\\u2028\\u2029]')
2170
+ elif c == '$':
2171
+ out.append('\\Z')
2172
+ elif source.startswith('(?<', i) and not source.startswith(('(?<=', '(?<!'), i):
2173
+ out.append('(?P<')
2174
+ i += 2
2175
+ elif source.startswith('(?', i) and not _RE_ECMA_GROUP.match(source, i):
2176
+ raise Pyjo.Exception.new(f'JSON Schema does not support the pattern {source}: a group of Python')
2177
+ else:
2178
+ out.append(c)
2179
+ i += 1
2180
+ try:
2181
+ return re.compile(''.join(out))
2182
+ except re.error as e:
2183
+ raise Pyjo.Exception.new(f'JSON Schema does not support the pattern {source}: {e}') from None
2184
+
2185
+
2186
+ # The formats of RFC 3339, of RFC 3986 and the others, which validate a string
2187
+ _RE_DATE = re.compile(r'([0-9]{4})-([0-9]{2})-([0-9]{2})')
2188
+ _RE_TIME = re.compile(r'([0-9]{2}):([0-9]{2}):([0-9]{2})(?:\.[0-9]+)?(?:[Zz]|([+-])([0-9]{2}):([0-9]{2}))')
2189
+ _RE_DURATION = re.compile(
2190
+ r'P(?:[0-9]+W|(?:[0-9]+Y(?:[0-9]+M(?:[0-9]+D)?)?|[0-9]+M(?:[0-9]+D)?|[0-9]+D)(?:T(?:{0}))?|T(?:{0}))'.format(
2191
+ r'[0-9]+H(?:[0-9]+M(?:[0-9]+S)?)?|[0-9]+M(?:[0-9]+S)?|[0-9]+S'
2192
+ )
2193
+ )
2194
+ _RE_EMAIL_LOCAL = re.compile(
2195
+ r'''[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x20\x21\x23-\x5b\x5d-\x7e]|\\[\x20-\x7e])*"'''
2196
+ )
2197
+ _RE_HOSTNAME_LABEL = re.compile(r'[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?')
2198
+ _RE_IPV4 = re.compile(
2199
+ r'(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])'
2200
+ )
2201
+ _RE_JSON_POINTER = re.compile(r'(?:/(?:[^~/]|~[01])*)*')
2202
+ _RE_RELATIVE_JSON_POINTER = re.compile(r'(?:0|[1-9][0-9]*)(?:#|(?:/(?:[^~/]|~[01])*)*)')
2203
+ _RE_UUID = re.compile(r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}')
2204
+
2205
+ _URI_PCHAR = r"(?:[A-Za-z0-9\-._~!$&'()*+,;=:@]|%[0-9A-Fa-f]{2})"
2206
+ _URI_SEGMENT_NC = r"(?:[A-Za-z0-9\-._~!$&'()*+,;=@]|%[0-9A-Fa-f]{2})"
2207
+ _URI_AUTHORITY = (
2208
+ r"//(?:(?:[A-Za-z0-9\-._~!$&'()*+,;=:]|%[0-9A-Fa-f]{2})*@)?"
2209
+ r"(?:\[(?P<ip>[^\]]*)\]|(?:[A-Za-z0-9\-._~!$&'()*+,;=]|%[0-9A-Fa-f]{2})*)(?::[0-9]*)?"
2210
+ )
2211
+ _URI_ABSOLUTE = rf'/(?:{_URI_PCHAR}+(?:/{_URI_PCHAR}*)*)?'
2212
+ _URI_END = rf'(?:\?(?:{_URI_PCHAR}|[/?])*)?(?:#(?:{_URI_PCHAR}|[/?])*)?'
2213
+ _URI = (
2214
+ rf'[A-Za-z][A-Za-z0-9+\-.]*:(?:{_URI_AUTHORITY}(?:/{_URI_PCHAR}*)*|{_URI_ABSOLUTE}'
2215
+ rf'|{_URI_PCHAR}+(?:/{_URI_PCHAR}*)*)?{_URI_END}'
2216
+ )
2217
+ _RE_URI = re.compile(_URI)
2218
+ _RE_URI_REFERENCE = re.compile(
2219
+ rf'{_URI}|(?:{_URI_AUTHORITY.replace("(?P<ip>", "(?P<relative_ip>")}(?:/{_URI_PCHAR}*)*|{_URI_ABSOLUTE}'
2220
+ rf'|{_URI_SEGMENT_NC}+(?:/{_URI_PCHAR}*)*)?{_URI_END}'
2221
+ )
2222
+
2223
+
2224
+ def _date(year: str, month: str, day: str) -> bool:
2225
+ try:
2226
+ datetime.date(builtins.int(year), builtins.int(month), builtins.int(day))
2227
+ except ValueError:
2228
+ return False
2229
+ return True
2230
+
2231
+
2232
+ def _time(value: str) -> bool:
2233
+ match = _RE_TIME.fullmatch(value)
2234
+ if match is None:
2235
+ return False
2236
+ hour, minute, second, sign, offset_hour, offset_minute = match.groups('0')
2237
+ if builtins.int(hour) > 23 or builtins.int(minute) > 59 or builtins.int(second) > 60:
2238
+ return False
2239
+ if builtins.int(offset_hour) > 23 or builtins.int(offset_minute) > 59:
2240
+ return False
2241
+ if second == '60':
2242
+ # A leap second is the last second of a day of UTC
2243
+ offset = (builtins.int(offset_hour) * 60 + builtins.int(offset_minute)) * (-1 if sign == '-' else 1)
2244
+ return (builtins.int(hour) * 60 + builtins.int(minute) - offset) % 1440 == 23 * 60 + 59
2245
+ return True
2246
+
2247
+
2248
+ def _format_date(value: str) -> bool:
2249
+ match = _RE_DATE.fullmatch(value)
2250
+ return match is not None and _date(*match.groups())
2251
+
2252
+
2253
+ def _format_date_time(value: str) -> bool:
2254
+ match = _RE_DATE.match(value)
2255
+ return match is not None and value[10:11] in ('T', 't') and _date(*match.groups()) and _time(value[11:])
2256
+
2257
+
2258
+ def _format_email(value: str) -> bool:
2259
+ local, at, domain = value.rpartition('@')
2260
+ if not at or _RE_EMAIL_LOCAL.fullmatch(local) is None:
2261
+ return False
2262
+ if domain[:6].lower() == '[ipv6:' and domain.endswith(']'):
2263
+ return _format_ipv6(domain[6:-1])
2264
+ if domain.startswith('[') and domain.endswith(']'):
2265
+ return _RE_IPV4.fullmatch(domain[1:-1]) is not None
2266
+ return _format_hostname(domain)
2267
+
2268
+
2269
+ def _format_hostname(value: str) -> bool:
2270
+ labels = value.split('.')
2271
+ return len(value) <= 253 and all(_RE_HOSTNAME_LABEL.fullmatch(label) and _punycode(label) for label in labels)
2272
+
2273
+
2274
+ def _punycode(label: str) -> bool:
2275
+ # An A-label of IDNA is Punycode
2276
+ if label[:4].lower() != 'xn--':
2277
+ return True
2278
+ try:
2279
+ punycode_decode(label[4:])
2280
+ except UnicodeError:
2281
+ return False
2282
+ return True
2283
+
2284
+
2285
+ def _format_ipv6(value: str) -> bool:
2286
+ if not value.isascii() or '%' in value:
2287
+ return False
2288
+ try:
2289
+ ipaddress.IPv6Address(value)
2290
+ except ValueError:
2291
+ return False
2292
+ return True
2293
+
2294
+
2295
+ def _format_regex(value: str) -> bool:
2296
+ try:
2297
+ _regex(value)
2298
+ except Pyjo.Exception.object:
2299
+ return False
2300
+ return True
2301
+
2302
+
2303
+ def _format_uri(regex: re.Pattern[str]) -> Callable[[str], bool]:
2304
+ def check(value: str) -> bool:
2305
+ match = regex.fullmatch(value)
2306
+ if match is None:
2307
+ return False
2308
+ ip = match.groupdict().get('ip') or match.groupdict().get('relative_ip')
2309
+ return (
2310
+ ip is None
2311
+ or _format_ipv6(ip)
2312
+ or re.fullmatch(r"[vV][0-9A-Fa-f]+\.[A-Za-z0-9\-._~!$&'()*+,;=:]+", ip) is not None
2313
+ )
2314
+
2315
+ return check
2316
+
2317
+
2318
+ _FORMAT_CHECKS: dict[str, Callable[[str], bool]] = {
2319
+ 'date': _format_date,
2320
+ 'date-time': _format_date_time,
2321
+ 'duration': lambda value: _RE_DURATION.fullmatch(value) is not None,
2322
+ 'email': _format_email,
2323
+ 'hostname': _format_hostname,
2324
+ 'ipv4': lambda value: _RE_IPV4.fullmatch(value) is not None,
2325
+ 'ipv6': _format_ipv6,
2326
+ 'json-pointer': lambda value: _RE_JSON_POINTER.fullmatch(value) is not None,
2327
+ 'regex': _format_regex,
2328
+ 'relative-json-pointer': lambda value: _RE_RELATIVE_JSON_POINTER.fullmatch(value) is not None,
2329
+ 'time': _time,
2330
+ 'uri': _format_uri(_RE_URI),
2331
+ 'uri-reference': _format_uri(_RE_URI_REFERENCE),
2332
+ 'uuid': lambda value: _RE_UUID.fullmatch(value) is not None,
2333
+ }
2334
+
2335
+
2336
+ def _literal(values: list[Any], enumeration: bool = False) -> dict[str, Any]:
2337
+ keywords: dict[str, Any] = {}
2338
+ types = {_json_type(value) for value in values}
2339
+ if len(types) == 1 and None not in types:
2340
+ keywords['type'] = types.pop()
2341
+ if len(values) == 1 and not enumeration:
2342
+ keywords['const'] = _json_value(values[0])
2343
+ else:
2344
+ keywords['enum'] = [_json_value(value) for value in values]
2345
+ return keywords
2346
+
2347
+
2348
+ def _nullable(schema: Pyjo_JSON_Schema) -> dict[str, Any]:
2349
+ keywords = schema.keywords
2350
+ if not schema.is_lazy and isinstance(keywords, dict) and builtins.list(keywords) == ['type']:
2351
+ types = keywords['type']
2352
+ if isinstance(types, str):
2353
+ return {'type': [types, 'null']}
2354
+ return {'type': [*cast(list[str], types), 'null']} if 'null' not in types else {'type': types}
2355
+ return {'anyOf': [schema, {'type': 'null'}]}
2356
+
2357
+
2358
+ def _json_type(value: Any) -> str | None:
2359
+ if isinstance(value, enum.Enum):
2360
+ value = value.value
2361
+ if value is None:
2362
+ return 'null'
2363
+ if isinstance(value, bool):
2364
+ return 'boolean'
2365
+ if isinstance(value, builtins.int):
2366
+ return 'integer'
2367
+ if isinstance(value, float):
2368
+ return 'number'
2369
+ if isinstance(value, str):
2370
+ return 'string'
2371
+ return None
2372
+
2373
+
2374
+ def _json_value(value: Any) -> Any:
2375
+ # A default or a literal as JSON has it, or _MISSING for one that it has not
2376
+ if isinstance(value, enum.Enum):
2377
+ value = value.value
2378
+ if value is None or isinstance(value, (bool, builtins.int, float, str)):
2379
+ return value
2380
+ if isinstance(value, (datetime.datetime, datetime.date, datetime.time)):
2381
+ return value.isoformat()
2382
+ if isinstance(value, uuid.UUID):
2383
+ return str(value)
2384
+ if isinstance(value, (list, builtins.tuple)):
2385
+ items = [_json_value(item) for item in cast(list[Any], value)]
2386
+ return _MISSING if _MISSING in items else items
2387
+ if isinstance(value, dict):
2388
+ items = {name: _json_value(item) for name, item in cast(dict[Any, Any], value).items()}
2389
+ if _MISSING in items.values() or not all(isinstance(name, str) for name in items):
2390
+ return _MISSING
2391
+ return items
2392
+ return _MISSING
2393
+
2394
+
2395
+ def _name(tp: Any) -> str:
2396
+ if isinstance(tp, type):
2397
+ return tp.__name__ if tp.__module__ == 'builtins' else f'{tp.__module__}.{tp.__qualname__}'
2398
+ return repr(tp)
2399
+
2400
+
2401
+ def _hints(obj: Any, where: str) -> dict[str, Any]:
2402
+ try:
2403
+ return typing.get_type_hints(obj, include_extras=True)
2404
+ except NameError as e:
2405
+ raise Pyjo.Exception.new(f'JSON Schema{where}: {e}') from e
2406
+
2407
+
2408
+ def _signature(function: Callable[..., Any]) -> tuple[inspect.Signature, dict[str, Any]]:
2409
+ target: Any = function
2410
+ # The annotations of a class are those of its method __init__, and of another callable object of __call__
2411
+ if isinstance(target, type):
2412
+ target = target.__init__
2413
+ elif not (inspect.isfunction(target) or inspect.ismethod(target)):
2414
+ target = target.__call__
2415
+ return inspect.signature(function), _hints(
2416
+ target, f' of the function {getattr(function, "__qualname__", function)}'
2417
+ )
2418
+
2419
+
2420
+ def _description(tp: Any) -> str | None:
2421
+ if not isinstance(tp, type):
2422
+ return None
2423
+ name, doc = tp.__name__, tp.__dict__.get('__doc__')
2424
+ # A docstring of its own, not the one that dataclasses makes of the signature, a descriptor on Python 3.15
2425
+ if not isinstance(doc, str) or not doc:
2426
+ return None
2427
+ if dataclasses.is_dataclass(tp) and re.fullmatch(re.escape(name) + r'\(.*\)', doc, re.S):
2428
+ return None
2429
+ return _docstringto_json(inspect.cleandoc(doc))[0]
2430
+
2431
+
2432
+ def _docstring(function: Callable[..., Any]) -> tuple[str | None, dict[str, str], str | None]:
2433
+ return _docstringto_json(inspect.getdoc(function) or '')
2434
+
2435
+
2436
+ def _docstringto_json(doc: str) -> tuple[str | None, dict[str, str], str | None]:
2437
+ # The first paragraph, and the descriptions of the parameters and of the return value, in the fields of
2438
+ # reStructuredText or in the sections of the Google Python Style Guide, each with the lines that continue it
2439
+ lines = doc.splitlines()
2440
+ paragraph: list[str] = []
2441
+ for line in lines:
2442
+ if not line.strip() or _RE_FIELD.match(line) or _RE_ARGS.fullmatch(line) or _RE_SECTION.fullmatch(line):
2443
+ break
2444
+ paragraph.append(line.strip())
2445
+
2446
+ params: dict[str, list[str]] = {}
2447
+ returns: list[str] = []
2448
+ current: list[str] | None = None
2449
+ section: str | None = None
2450
+ indent = item = 0
2451
+ for line in lines:
2452
+ text = line.strip()
2453
+ depth = len(line) - len(line.lstrip())
2454
+ if not text:
2455
+ continue
2456
+ if section is not None and depth <= indent:
2457
+ section = current = None
2458
+ if section == 'args':
2459
+ match = _RE_ITEM.fullmatch(text)
2460
+ if match and (current is None or depth <= item):
2461
+ item, current = depth, [match.group(2)]
2462
+ params[match.group(1)] = current
2463
+ elif current is not None:
2464
+ current.append(text)
2465
+ elif section == 'returns':
2466
+ match = _RE_RETURN_TYPE.fullmatch(text)
2467
+ returns.append(match.group(1) if match and not returns else text)
2468
+ elif _RE_ARGS.fullmatch(text):
2469
+ section, indent, current = 'args', depth, None
2470
+ elif _RE_RETURNS.fullmatch(text):
2471
+ section, indent, current = 'returns', depth, None
2472
+ elif _RE_SECTION.fullmatch(text):
2473
+ section, indent, current = 'other', depth, None
2474
+ elif match := _RE_FIELD.match(text):
2475
+ kind, name, rest = match.groups()
2476
+ if kind in ('return', 'returns'):
2477
+ current = returns
2478
+ returns.append(rest)
2479
+ elif kind in ('type', 'rtype', 'raise', 'raises') or not name.split():
2480
+ current = None
2481
+ else:
2482
+ current = [rest]
2483
+ params[name.split()[-1]] = current
2484
+ elif current is not None and depth > 0:
2485
+ current.append(text)
2486
+ else:
2487
+ current = None
2488
+
2489
+ descriptions = {name: ' '.join(text).strip() for name, text in params.items()}
2490
+ return (
2491
+ ' '.join(paragraph) or None,
2492
+ {name: text for name, text in descriptions.items() if text},
2493
+ ' '.join(returns).strip() or None,
2494
+ )
2495
+
2496
+
2497
+ new = Pyjo_JSON_Schema.new
2498
+ object = Pyjo_JSON_Schema
2499
+ Schema = Pyjo_JSON_Schema
2500
+
2501
+ of = Pyjo_JSON_Schema.of
2502
+ parameters = Pyjo_JSON_Schema.parameters
2503
+ returns = Pyjo_JSON_Schema.returns
2504
+
2505
+ # The builder of schemas, the class itself, as z of zod
2506
+ z = Pyjo_JSON_Schema