pydantic-json-patch 0.6.0__tar.gz → 0.6.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pydantic_json_patch-0.6.0 → pydantic_json_patch-0.6.2}/PKG-INFO +29 -2
- {pydantic_json_patch-0.6.0 → pydantic_json_patch-0.6.2}/README.md +28 -1
- {pydantic_json_patch-0.6.0 → pydantic_json_patch-0.6.2}/pyproject.toml +23 -1
- {pydantic_json_patch-0.6.0 → pydantic_json_patch-0.6.2}/src/pydantic_json_patch/__init__.py +7 -1
- {pydantic_json_patch-0.6.0 → pydantic_json_patch-0.6.2}/src/pydantic_json_patch/models.py +66 -8
- {pydantic_json_patch-0.6.0 → pydantic_json_patch-0.6.2}/src/pydantic_json_patch/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pydantic_json_patch
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
4
4
|
Summary: Pydantic models for implementing JSON Patch.
|
|
5
5
|
Author: Jonathan Sharpe
|
|
6
6
|
Author-email: Jonathan Sharpe <mail@jonrshar.pe>
|
|
@@ -81,7 +81,7 @@ ReplaceOp[str](op='replace', path='/foo/bar', value='hello')
|
|
|
81
81
|
|
|
82
82
|
Additionally, there are two compound types:
|
|
83
83
|
|
|
84
|
-
- `Operation` is the union of all the
|
|
84
|
+
- `Operation` is the union of all the operations; and
|
|
85
85
|
- `JsonPatch` is a Pydantic `RootModel` representing a sequence of operations.
|
|
86
86
|
|
|
87
87
|
`JsonPatch` can be used directly for validation:
|
|
@@ -152,6 +152,32 @@ and list the models along with the other schemas:
|
|
|
152
152
|
|
|
153
153
|
[![Screenshot of Swagger UI schema list][swagger-schemas]][swagger-schemas]
|
|
154
154
|
|
|
155
|
+
### Value type validation
|
|
156
|
+
|
|
157
|
+
You can also use a more specific type to apply type validation to the value properties:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
import typing as tp
|
|
161
|
+
from uuid import UUID
|
|
162
|
+
|
|
163
|
+
from fastapi import Body, FastAPI
|
|
164
|
+
from pydantic import Discriminator
|
|
165
|
+
|
|
166
|
+
from pydantic_json_patch import AddOp, TestOp
|
|
167
|
+
|
|
168
|
+
app = FastAPI()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@app.patch("/resource/{resource_id}")
|
|
172
|
+
def _(
|
|
173
|
+
resource_id: UUID,
|
|
174
|
+
operations: tp.Annotated[list[tp.Annotated[AddOp[int] | TestOp[int], Discriminator("op")]], Body()],
|
|
175
|
+
) -> ...:
|
|
176
|
+
...
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Note**: explicitly specifying the [discriminator][pydantic-discriminator] gives better results on _failed_ validation for unions of operations.
|
|
180
|
+
|
|
155
181
|
## Development
|
|
156
182
|
|
|
157
183
|
This project uses [uv] for managing dependencies.
|
|
@@ -198,6 +224,7 @@ This will auto-restart as you make changes.
|
|
|
198
224
|
[json patch]: https://datatracker.ietf.org/doc/html/rfc6902/
|
|
199
225
|
[json pointer]: https://datatracker.ietf.org/doc/html/rfc6901/
|
|
200
226
|
[pydantic]: https://docs.pydantic.dev/latest/
|
|
227
|
+
[pydantic-discriminator]: https://docs.pydantic.dev/latest/concepts/unions/#discriminated-unions-with-str-discriminators
|
|
201
228
|
[pypi]: https://pypi.org/
|
|
202
229
|
[pypi-badge]: https://img.shields.io/pypi/v/pydantic-json-patch?logo=python&logoColor=white&label=PyPI
|
|
203
230
|
[pypi-page]: https://pypi.org/project/pydantic-json-patch/
|
|
@@ -49,7 +49,7 @@ ReplaceOp[str](op='replace', path='/foo/bar', value='hello')
|
|
|
49
49
|
|
|
50
50
|
Additionally, there are two compound types:
|
|
51
51
|
|
|
52
|
-
- `Operation` is the union of all the
|
|
52
|
+
- `Operation` is the union of all the operations; and
|
|
53
53
|
- `JsonPatch` is a Pydantic `RootModel` representing a sequence of operations.
|
|
54
54
|
|
|
55
55
|
`JsonPatch` can be used directly for validation:
|
|
@@ -120,6 +120,32 @@ and list the models along with the other schemas:
|
|
|
120
120
|
|
|
121
121
|
[![Screenshot of Swagger UI schema list][swagger-schemas]][swagger-schemas]
|
|
122
122
|
|
|
123
|
+
### Value type validation
|
|
124
|
+
|
|
125
|
+
You can also use a more specific type to apply type validation to the value properties:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
import typing as tp
|
|
129
|
+
from uuid import UUID
|
|
130
|
+
|
|
131
|
+
from fastapi import Body, FastAPI
|
|
132
|
+
from pydantic import Discriminator
|
|
133
|
+
|
|
134
|
+
from pydantic_json_patch import AddOp, TestOp
|
|
135
|
+
|
|
136
|
+
app = FastAPI()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@app.patch("/resource/{resource_id}")
|
|
140
|
+
def _(
|
|
141
|
+
resource_id: UUID,
|
|
142
|
+
operations: tp.Annotated[list[tp.Annotated[AddOp[int] | TestOp[int], Discriminator("op")]], Body()],
|
|
143
|
+
) -> ...:
|
|
144
|
+
...
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Note**: explicitly specifying the [discriminator][pydantic-discriminator] gives better results on _failed_ validation for unions of operations.
|
|
148
|
+
|
|
123
149
|
## Development
|
|
124
150
|
|
|
125
151
|
This project uses [uv] for managing dependencies.
|
|
@@ -166,6 +192,7 @@ This will auto-restart as you make changes.
|
|
|
166
192
|
[json patch]: https://datatracker.ietf.org/doc/html/rfc6902/
|
|
167
193
|
[json pointer]: https://datatracker.ietf.org/doc/html/rfc6901/
|
|
168
194
|
[pydantic]: https://docs.pydantic.dev/latest/
|
|
195
|
+
[pydantic-discriminator]: https://docs.pydantic.dev/latest/concepts/unions/#discriminated-unions-with-str-discriminators
|
|
169
196
|
[pypi]: https://pypi.org/
|
|
170
197
|
[pypi-badge]: https://img.shields.io/pypi/v/pydantic-json-patch?logo=python&logoColor=white&label=PyPI
|
|
171
198
|
[pypi-page]: https://pypi.org/project/pydantic-json-patch/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pydantic_json_patch"
|
|
3
|
-
version = "0.6.
|
|
3
|
+
version = "0.6.2"
|
|
4
4
|
description = "Pydantic models for implementing JSON Patch."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -72,3 +72,25 @@ profile = "black"
|
|
|
72
72
|
[tool.pytest.ini_options]
|
|
73
73
|
addopts = "--doctest-glob=*.md" # validate README examples
|
|
74
74
|
python_classes = [] # avoid trying to run TestOp as a test
|
|
75
|
+
|
|
76
|
+
[tool.ruff.lint]
|
|
77
|
+
select = ["ALL"]
|
|
78
|
+
ignore = [
|
|
79
|
+
"COM812",
|
|
80
|
+
"D203",
|
|
81
|
+
"D213",
|
|
82
|
+
"D105",
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
[tool.ruff.lint.per-file-ignores]
|
|
86
|
+
"tests/**/*.py" = [
|
|
87
|
+
"ANN201",
|
|
88
|
+
"ANN401",
|
|
89
|
+
"C408",
|
|
90
|
+
"D",
|
|
91
|
+
"PLR2004",
|
|
92
|
+
"S101",
|
|
93
|
+
]
|
|
94
|
+
"bin/**/*.py" = [
|
|
95
|
+
"D",
|
|
96
|
+
]
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
"""Pydantic models for implementing `JSON Patch`_.
|
|
2
|
+
|
|
3
|
+
.. _JSON Patch: https://datatracker.ietf.org/doc/html/rfc6902/
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
|
|
1
7
|
from importlib.metadata import version
|
|
2
8
|
|
|
3
9
|
from .models import (
|
|
@@ -14,7 +20,6 @@ from .models import (
|
|
|
14
20
|
__version__ = version(__name__)
|
|
15
21
|
|
|
16
22
|
__all__ = [
|
|
17
|
-
"__version__",
|
|
18
23
|
"AddOp",
|
|
19
24
|
"CopyOp",
|
|
20
25
|
"JsonPatch",
|
|
@@ -23,4 +28,5 @@ __all__ = [
|
|
|
23
28
|
"RemoveOp",
|
|
24
29
|
"ReplaceOp",
|
|
25
30
|
"TestOp",
|
|
31
|
+
"__version__",
|
|
26
32
|
]
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
"""Core models and private implementation."""
|
|
2
|
+
|
|
1
3
|
import re
|
|
2
4
|
import typing as tp
|
|
3
|
-
from collections.abc import Sequence
|
|
5
|
+
from collections.abc import Iterator, Sequence
|
|
4
6
|
from functools import cached_property, lru_cache
|
|
5
7
|
|
|
6
8
|
import typing_extensions as tx
|
|
@@ -21,14 +23,21 @@ T = tx.TypeVar("T", default=tp.Any)
|
|
|
21
23
|
# region base models
|
|
22
24
|
|
|
23
25
|
|
|
26
|
+
def _generate_title(model: type[tp.Any]) -> str:
|
|
27
|
+
"""Strip any type metadata and expand the shortened name."""
|
|
28
|
+
name, *_ = model.__name__.partition("[")
|
|
29
|
+
return f"JsonPatch{name}eration"
|
|
30
|
+
|
|
31
|
+
|
|
24
32
|
class _BaseOp(BaseModel):
|
|
25
33
|
model_config = ConfigDict(
|
|
26
34
|
frozen=True,
|
|
27
|
-
model_title_generator=
|
|
35
|
+
model_title_generator=_generate_title,
|
|
28
36
|
)
|
|
29
37
|
|
|
30
38
|
@classmethod
|
|
31
|
-
def create(cls, *, path: str | Sequence[str], **kwargs) -> tx.Self:
|
|
39
|
+
def create(cls, *, path: str | Sequence[str], **kwargs: tp.Any) -> tx.Self: # noqa: ANN401
|
|
40
|
+
"""Return an instance of the appropriate operation."""
|
|
32
41
|
(op,) = tp.get_args(cls.model_fields["op"].annotation)
|
|
33
42
|
return cls(op=op, path=cls._dump_pointer(path), **kwargs)
|
|
34
43
|
|
|
@@ -68,8 +77,12 @@ class _BaseOp(BaseModel):
|
|
|
68
77
|
class _FromOp(_BaseOp):
|
|
69
78
|
@classmethod
|
|
70
79
|
def create(
|
|
71
|
-
cls,
|
|
80
|
+
cls,
|
|
81
|
+
*,
|
|
82
|
+
path: str | Sequence[str],
|
|
83
|
+
from_: str | Sequence[str],
|
|
72
84
|
) -> tx.Self: # ty: ignore[invalid-method-override] -- deliberately narrows **kwargs to named params
|
|
85
|
+
"""Return an instance of the appropriate operation."""
|
|
73
86
|
pointer = from_ if isinstance(from_, str) else cls._dump_pointer(from_)
|
|
74
87
|
return super().create(path=path, **{"from": pointer})
|
|
75
88
|
|
|
@@ -78,7 +91,7 @@ class _FromOp(_BaseOp):
|
|
|
78
91
|
|
|
79
92
|
@model_validator(mode="before")
|
|
80
93
|
@classmethod
|
|
81
|
-
def _pre_validate(cls, data: tp.Any, info: ValidationInfo) -> tp.Any:
|
|
94
|
+
def _pre_validate(cls, data: tp.Any, info: ValidationInfo) -> tp.Any: # noqa: ANN401
|
|
82
95
|
if (
|
|
83
96
|
info.mode != "json" # pragma: no mutate
|
|
84
97
|
and isinstance(data, dict)
|
|
@@ -97,6 +110,7 @@ class _FromOp(_BaseOp):
|
|
|
97
110
|
class _ValueOp(_BaseOp, tp.Generic[T]):
|
|
98
111
|
@classmethod
|
|
99
112
|
def create(cls, *, path: str | Sequence[str], value: T) -> tx.Self: # ty: ignore[invalid-method-override] -- deliberately narrows **kwargs to named params
|
|
113
|
+
"""Return an instance of the appropriate operation."""
|
|
100
114
|
return super().create(path=path, value=value)
|
|
101
115
|
|
|
102
116
|
value: T = Field(examples=[42])
|
|
@@ -109,30 +123,67 @@ class _ValueOp(_BaseOp, tp.Generic[T]):
|
|
|
109
123
|
|
|
110
124
|
|
|
111
125
|
class AddOp(_ValueOp[T], tp.Generic[T]):
|
|
126
|
+
"""Represents the `add`_ operation.
|
|
127
|
+
|
|
128
|
+
.. _add: https://datatracker.ietf.org/doc/html/rfc6902/#section-4.1
|
|
129
|
+
|
|
130
|
+
"""
|
|
131
|
+
|
|
112
132
|
op: tp.Literal["add"]
|
|
113
133
|
|
|
114
134
|
|
|
115
135
|
class CopyOp(_FromOp):
|
|
136
|
+
"""Represents the `copy`_ operation.
|
|
137
|
+
|
|
138
|
+
.. _copy: https://datatracker.ietf.org/doc/html/rfc6902/#section-4.5
|
|
139
|
+
|
|
140
|
+
"""
|
|
141
|
+
|
|
116
142
|
op: tp.Literal["copy"]
|
|
117
143
|
|
|
118
144
|
|
|
119
145
|
class MoveOp(_FromOp):
|
|
146
|
+
"""Represents the `move`_ operation.
|
|
147
|
+
|
|
148
|
+
.. _move: https://datatracker.ietf.org/doc/html/rfc6902/#section-4.4
|
|
149
|
+
|
|
150
|
+
"""
|
|
151
|
+
|
|
120
152
|
op: tp.Literal["move"]
|
|
121
153
|
|
|
122
154
|
|
|
123
155
|
class RemoveOp(_BaseOp):
|
|
156
|
+
"""Represents the `remove`_ operation.
|
|
157
|
+
|
|
158
|
+
.. _remove: https://datatracker.ietf.org/doc/html/rfc6902/#section-4.2
|
|
159
|
+
|
|
160
|
+
"""
|
|
161
|
+
|
|
124
162
|
@classmethod
|
|
125
163
|
def create(cls, *, path: str | Sequence[str]) -> tx.Self: # ty: ignore[invalid-method-override] -- deliberately narrows **kwargs to named params
|
|
164
|
+
"""Return an instance of the appropriate operation."""
|
|
126
165
|
return super().create(path=path)
|
|
127
166
|
|
|
128
167
|
op: tp.Literal["remove"]
|
|
129
168
|
|
|
130
169
|
|
|
131
170
|
class ReplaceOp(_ValueOp[T], tp.Generic[T]):
|
|
171
|
+
"""Represents the `replace`_ operation.
|
|
172
|
+
|
|
173
|
+
.. _replace: https://datatracker.ietf.org/doc/html/rfc6902/#section-4.3
|
|
174
|
+
|
|
175
|
+
"""
|
|
176
|
+
|
|
132
177
|
op: tp.Literal["replace"]
|
|
133
178
|
|
|
134
179
|
|
|
135
180
|
class TestOp(_ValueOp[T], tp.Generic[T]):
|
|
181
|
+
"""Represents the `test`_ operation.
|
|
182
|
+
|
|
183
|
+
.. _test: https://datatracker.ietf.org/doc/html/rfc6902/#section-4.6
|
|
184
|
+
|
|
185
|
+
"""
|
|
186
|
+
|
|
136
187
|
op: tp.Literal["test"]
|
|
137
188
|
|
|
138
189
|
|
|
@@ -141,16 +192,23 @@ class TestOp(_ValueOp[T], tp.Generic[T]):
|
|
|
141
192
|
# region compound models
|
|
142
193
|
|
|
143
194
|
Operation: tp.TypeAlias = tp.Annotated[
|
|
144
|
-
|
|
195
|
+
AddOp | CopyOp | MoveOp | RemoveOp | ReplaceOp | TestOp,
|
|
196
|
+
Discriminator("op"),
|
|
145
197
|
]
|
|
146
198
|
|
|
147
199
|
|
|
148
200
|
class JsonPatch(RootModel[Sequence[Operation]], Sequence[Operation]):
|
|
201
|
+
"""Represents a full JSON Patch `document`_.
|
|
202
|
+
|
|
203
|
+
.. _document: https://datatracker.ietf.org/doc/html/rfc6902/#section-3
|
|
204
|
+
|
|
205
|
+
"""
|
|
206
|
+
|
|
149
207
|
model_config = ConfigDict(frozen=True)
|
|
150
208
|
|
|
151
209
|
@model_validator(mode="before")
|
|
152
210
|
@classmethod
|
|
153
|
-
def _coerce_to_tuple(cls, value: tp.Any) -> tuple[tp.Any, ...]:
|
|
211
|
+
def _coerce_to_tuple(cls, value: tp.Any) -> tuple[tp.Any, ...]: # noqa: ANN401
|
|
154
212
|
if isinstance(value, Sequence) and not isinstance(value, tuple):
|
|
155
213
|
return tuple(value)
|
|
156
214
|
return value
|
|
@@ -164,7 +222,7 @@ class JsonPatch(RootModel[Sequence[Operation]], Sequence[Operation]):
|
|
|
164
222
|
def __getitem__(self, index):
|
|
165
223
|
return self.root[index]
|
|
166
224
|
|
|
167
|
-
def __iter__(self):
|
|
225
|
+
def __iter__(self) -> Iterator[Operation]: # ty: ignore[invalid-method-override] -- dict(model) doesn't make sense for a sequence of non-pairs
|
|
168
226
|
return iter(self.root)
|
|
169
227
|
|
|
170
228
|
def __len__(self) -> int:
|
|
File without changes
|