pydantic-json-patch 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.
@@ -0,0 +1,27 @@
1
+ import typing as tp
2
+ from importlib.metadata import version
3
+
4
+ from pydantic import Discriminator
5
+
6
+ from .models import AddOp, CopyOp, MoveOp, Op, RemoveOp, ReplaceOp, TestOp, Tokens
7
+
8
+ __version__ = version(__name__)
9
+
10
+ Operation: tp.TypeAlias = tp.Annotated[
11
+ tp.Union[AddOp, CopyOp, MoveOp, RemoveOp, ReplaceOp, TestOp], Discriminator("op")
12
+ ]
13
+ JsonPatch: tp.TypeAlias = list[Operation]
14
+
15
+ __all__ = [
16
+ "__version__",
17
+ "AddOp",
18
+ "CopyOp",
19
+ "JsonPatch",
20
+ "MoveOp",
21
+ "Op",
22
+ "Operation",
23
+ "RemoveOp",
24
+ "ReplaceOp",
25
+ "TestOp",
26
+ "Tokens",
27
+ ]
@@ -0,0 +1,123 @@
1
+ import re
2
+ import typing as tp
3
+ from functools import cached_property
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field, ValidationInfo, model_validator
6
+ import typing_extensions as tx
7
+
8
+ _JSON_POINTER = re.compile(r"^(?:/(?:[^/~]|~[01])+)*$")
9
+
10
+ T = tp.TypeVar("T")
11
+ Op: tp.TypeAlias = tp.Literal["add", "copy", "move", "remove", "replace", "test"]
12
+ Tokens: tp.TypeAlias = tuple[str, ...]
13
+
14
+ # region base models
15
+
16
+
17
+ class _BaseOp(BaseModel):
18
+ model_config = ConfigDict(
19
+ frozen=True,
20
+ model_title_generator=lambda t: f"JsonPatch{t.__name__}eration",
21
+ )
22
+
23
+ @classmethod
24
+ def create(cls, *, path: str | Tokens, **kwargs) -> tx.Self:
25
+ (op,) = tp.get_args(cls.model_fields["op"].annotation)
26
+ pointer = path if isinstance(path, str) else cls._dump_pointer(path)
27
+ return cls(op=op, path=pointer, **kwargs)
28
+
29
+ op: Op
30
+ """The operation being represented."""
31
+
32
+ path: str = Field(examples=["/a/b/c"], pattern=_JSON_POINTER)
33
+ """A JSON pointer representing the path to apply the operation to."""
34
+
35
+ @cached_property
36
+ def path_tokens(self) -> Tokens:
37
+ """The decoded tokens in the 'path' JSON pointer."""
38
+ return self._load_pointer(self.path)
39
+
40
+ @staticmethod
41
+ def _dump_pointer(pointer: Tokens) -> str:
42
+ return "/".join(
43
+ ["", *(token.replace("~", "~0").replace("/", "~1") for token in pointer)]
44
+ )
45
+
46
+ @staticmethod
47
+ def _load_pointer(pointer: str) -> Tokens:
48
+ return tuple(
49
+ token.replace("~1", "/").replace("~0", "~")
50
+ for token in pointer.split("/")[1:]
51
+ )
52
+
53
+
54
+ class _FromOp(_BaseOp):
55
+ @classmethod
56
+ def create(cls, *, path: str | Tokens, from_: str | Tokens) -> tx.Self: # type: ignore[invalid-method-override]
57
+ pointer = from_ if isinstance(from_, str) else cls._dump_pointer(from_)
58
+ return super().create(path=path, **{"from": pointer})
59
+
60
+ from_: str = Field(alias="from", examples=["/a/b/d"], pattern=_JSON_POINTER)
61
+ """A JSON pointer representing the path to apply the operation from."""
62
+
63
+ @model_validator(mode="before")
64
+ @classmethod
65
+ def _pre_validate(cls, data: tp.Any, info: ValidationInfo) -> tp.Any:
66
+ if (
67
+ info.mode != "json"
68
+ and isinstance(data, dict)
69
+ and "from_" in data
70
+ and "from" not in data
71
+ ):
72
+ data["from"] = data.pop("from_")
73
+ return data
74
+
75
+ @cached_property
76
+ def from_tokens(self) -> Tokens:
77
+ """The decoded tokens in the 'from' JSON pointer."""
78
+ return self._load_pointer(self.from_)
79
+
80
+
81
+ class _ValueOp(_BaseOp, tp.Generic[T]):
82
+ @classmethod
83
+ def create(cls, *, path: str | Tokens, value: T) -> tx.Self: # type: ignore[invalid-method-override]
84
+ return super().create(path=path, value=value)
85
+
86
+ value: T = Field(examples=[42])
87
+ """The value to use in the operation."""
88
+
89
+
90
+ # endregion
91
+
92
+ # region public models
93
+
94
+
95
+ class AddOp(_ValueOp, tp.Generic[T]):
96
+ op: tp.Literal["add"]
97
+
98
+
99
+ class CopyOp(_FromOp):
100
+ op: tp.Literal["copy"]
101
+
102
+
103
+ class MoveOp(_FromOp):
104
+ op: tp.Literal["move"]
105
+
106
+
107
+ class RemoveOp(_BaseOp):
108
+ @classmethod
109
+ def create(cls, *, path: str | Tokens) -> tx.Self: # type: ignore[invalid-method-override]
110
+ return super().create(path=path)
111
+
112
+ op: tp.Literal["remove"]
113
+
114
+
115
+ class ReplaceOp(_ValueOp, tp.Generic[T]):
116
+ op: tp.Literal["replace"]
117
+
118
+
119
+ class TestOp(_ValueOp, tp.Generic[T]):
120
+ op: tp.Literal["test"]
121
+
122
+
123
+ # endregion
File without changes
@@ -0,0 +1,129 @@
1
+ Metadata-Version: 2.4
2
+ Name: pydantic_json_patch
3
+ Version: 0.1.0
4
+ Summary: Pydantic models for implementing JSON Patch.
5
+ Author: Jonathan Sharpe
6
+ Author-email: Jonathan Sharpe <mail@jonrshar.pe>
7
+ License-Expression: ISC
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Framework :: FastAPI
10
+ Classifier: Framework :: Pydantic
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: File Formats :: JSON
19
+ Classifier: Topic :: File Formats :: JSON :: JSON Schema
20
+ Requires-Dist: pydantic>=2.0
21
+ Requires-Dist: typing-extensions>=4.15.0
22
+ Requires-Python: >=3.10
23
+ Project-URL: repository, https://github.com/textbook/pydantic_json_patch
24
+ Project-URL: Issues, https://github.com/textbook/pydantic_json_patch/issues
25
+ Project-URL: Sponsor, https://ko-fi.com/textbook
26
+ Description-Content-Type: text/markdown
27
+
28
+ # Pydantic JSON Patch
29
+
30
+ [Pydantic] models for implementing [JSON Patch].
31
+
32
+ ## Installation
33
+
34
+ _Pydantic JSON Patch_ is published to [PyPI], and can be installed with e.g.:
35
+
36
+ ```shell
37
+ pip install pydantic-json-patch
38
+ ```
39
+
40
+ ## Models
41
+
42
+ A model is provided for each of the six JSON Patch operations:
43
+
44
+ - `AddOp`
45
+ - `CopyOp`
46
+ - `MoveOp`
47
+ - `RemoveOp`
48
+ - `ReplaceOp`
49
+ - `TestOp`
50
+
51
+ As repeating the op is a bit awkward (`CopyOp(op="copy", ...)`), a `create` factory method is available:
52
+
53
+ ```pycon
54
+ >>> from pydantic_json_patch import AddOp
55
+ >>> op = AddOp.create(path="/foo/bar", value=123)
56
+ >>> op
57
+ AddOp(op='add', path='/foo/bar', value=123)
58
+ >>> op.model_dump_json()
59
+ '{"op":"add","path":"/foo/bar","value":123}'
60
+ ```
61
+
62
+ Additionally, there are two compound models:
63
+
64
+ - `Operation` is the union of all the operators; and
65
+ - `JsonPatch` represents a list of that union type.
66
+
67
+ ### Pointer tokens
68
+
69
+ The `path` property (and `from` property, where present) of an operation is a [JSON Pointer].
70
+ This means that any `~` or `/` characters in property names need to be properly encoded.
71
+ To aid working with these, the models expose a read-only `path_tokens` property (and, where appropriate, `from_tokens`):
72
+
73
+ ```pycon
74
+ >>> from pydantic_json_patch import CopyOp
75
+ >>> op = CopyOp.model_validate_json('{"op":"copy","path":"/foo/bar~1new","from":"/foo/bar~0old"}')
76
+ >>> op
77
+ CopyOp(op='copy', path='/foo/bar~1new', from_='/foo/bar~0old')
78
+ >>> op.path_tokens
79
+ ('foo', 'bar/new')
80
+ >>> op.from_tokens
81
+ ('foo', 'bar~old')
82
+ ```
83
+
84
+ Similarly, the `create` factory methods can accept tuples of tokens, and will encode them appropriately:
85
+
86
+ ```pycon
87
+ >>> from pydantic_json_patch import TestOp
88
+ >>> op = TestOp.create(path=("annotations", "scope/value"), value=None)
89
+ >>> op
90
+ TestOp(op='test', path='/annotations/scope~1value', value=None)
91
+ >>> op.model_dump_json()
92
+ '{"op":"test","path":"/annotations/scope~1value","value":null}'
93
+ ```
94
+
95
+ ## FastAPI
96
+
97
+ You can use this package to validate a JSON Patch endpoint in a FastAPI application, for example:
98
+
99
+ ```python
100
+ import typing as tp
101
+ from uuid import UUID
102
+
103
+ from fastapi import Body, FastAPI
104
+
105
+ from pydantic_json_patch import JsonPatch
106
+
107
+ app = FastAPI()
108
+
109
+
110
+ @app.patch("/resource/{resource_id}")
111
+ def _(resource_id: UUID, operations: tp.Annotated[JsonPatch, Body()]) -> ...:
112
+ ...
113
+ ```
114
+
115
+ This will provide a sensible example of the request body:
116
+
117
+ [![Screenshot of Swagger UI request body example][swagger-example]][swagger-example]
118
+
119
+ and list the models along with the other schemas:
120
+
121
+ [![Screenshot of Swagger UI schema list][swagger-schemas]][swagger-schemas]
122
+
123
+ [fastapi]: https://fastapi.tiangolo.com/
124
+ [json patch]: https://datatracker.ietf.org/doc/html/rfc6902/
125
+ [json pointer]: https://datatracker.ietf.org/doc/html/rfc6901/
126
+ [pydantic]: https://docs.pydantic.dev/latest/
127
+ [pypi]: https://pypi.org/
128
+ [swagger-example]: docs/swagger-example.png
129
+ [swagger-schemas]: docs/swagger-schemas.png
@@ -0,0 +1,6 @@
1
+ pydantic_json_patch/__init__.py,sha256=yLGECB55CsaTCxMxRbPCGPLDZQlGSB3KJmx6KBlRjt8,563
2
+ pydantic_json_patch/models.py,sha256=Sy_ZFBwf9ft-h1U0KbDAeAnvS9rF0Ejpi2D-tVxK6Q8,3443
3
+ pydantic_json_patch/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ pydantic_json_patch-0.1.0.dist-info/WHEEL,sha256=jROcLULcdzropX2J55opKw4UHhPFREZax2XzS-Mvpxs,80
5
+ pydantic_json_patch-0.1.0.dist-info/METADATA,sha256=QGt2XbptttDMqnDWraK0tbm4Io4oG66NDg1l8cZphsc,3972
6
+ pydantic_json_patch-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.10.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any