pydantic-json-patch 0.1.3__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydantic_json_patch
3
- Version: 0.1.3
3
+ Version: 0.3.0
4
4
  Summary: Pydantic models for implementing JSON Patch.
5
5
  Author: Jonathan Sharpe
6
6
  Author-email: Jonathan Sharpe <mail@jonrshar.pe>
@@ -90,7 +90,7 @@ CopyOp(op='copy', path='/foo/bar~1new', from_='/foo/bar~0old')
90
90
  ('foo', 'bar~old')
91
91
  ```
92
92
 
93
- Similarly, the `create` factory methods can accept tuples of tokens, and will encode them appropriately:
93
+ Similarly, the `create` factory methods can accept sequences of tokens, and will encode them appropriately:
94
94
 
95
95
  ```python
96
96
  >>> from pydantic_json_patch import TestOp
@@ -139,17 +139,48 @@ uv sync
139
139
  uv run pre-commit install
140
140
  ```
141
141
 
142
+ The pre-commit hooks will ensure that the code style checks (using [isort] and [ruff]) are applied.
143
+
144
+ ### Testing
145
+
146
+ The test suite uses [pytest] and can be run with:
147
+
148
+ ```shell
149
+ uv run pytest
150
+ ```
151
+
152
+ Additionally, there is [ty] type-checking that can be run with:
153
+
154
+ ```shell
155
+ uv run ty check
156
+ ```
157
+
158
+ ### FastAPI
159
+
160
+ You can preview the FastAPI/Swagger documentation by running:
161
+
162
+ ```shell
163
+ uv run fastapi dev tests/app.py
164
+ ```
165
+
166
+ and visiting the Documentation link that's logged in the console.
167
+ This will auto-restart as you make changes.
168
+
142
169
  [ci-badge]: https://github.com/textbook/pydantic_json_patch/actions/workflows/push.yml/badge.svg
143
170
  [ci-page]: https://github.com/textbook/pydantic_json_patch/actions/workflows/push.yml
144
171
  [coverage-badge]: https://coveralls.io/repos/github/textbook/pydantic_json_patch/badge.svg?branch=main
145
172
  [coverage-page]: https://coveralls.io/github/textbook/pydantic_json_patch?branch=main
146
173
  [fastapi]: https://fastapi.tiangolo.com/
174
+ [isort]: https://pycqa.github.io/isort/
147
175
  [json patch]: https://datatracker.ietf.org/doc/html/rfc6902/
148
176
  [json pointer]: https://datatracker.ietf.org/doc/html/rfc6901/
149
177
  [pydantic]: https://docs.pydantic.dev/latest/
150
178
  [pypi]: https://pypi.org/
151
179
  [pypi-badge]: https://img.shields.io/pypi/v/pydantic-json-patch?logo=python&logoColor=white&label=PyPI
152
180
  [pypi-page]: https://pypi.org/project/pydantic-json-patch/
181
+ [pytest]: https://docs.pytest.org/en/stable/
182
+ [ruff]: https://docs.astral.sh/ruff/
153
183
  [swagger-example]: https://github.com/textbook/pydantic_json_patch/blob/main/docs/swagger-example.png?raw=true
154
184
  [swagger-schemas]: https://github.com/textbook/pydantic_json_patch/blob/main/docs/swagger-schemas.png?raw=true
185
+ [ty]: https://docs.astral.sh/ty/
155
186
  [uv]: https://docs.astral.sh/uv/
@@ -58,7 +58,7 @@ CopyOp(op='copy', path='/foo/bar~1new', from_='/foo/bar~0old')
58
58
  ('foo', 'bar~old')
59
59
  ```
60
60
 
61
- Similarly, the `create` factory methods can accept tuples of tokens, and will encode them appropriately:
61
+ Similarly, the `create` factory methods can accept sequences of tokens, and will encode them appropriately:
62
62
 
63
63
  ```python
64
64
  >>> from pydantic_json_patch import TestOp
@@ -107,17 +107,48 @@ uv sync
107
107
  uv run pre-commit install
108
108
  ```
109
109
 
110
+ The pre-commit hooks will ensure that the code style checks (using [isort] and [ruff]) are applied.
111
+
112
+ ### Testing
113
+
114
+ The test suite uses [pytest] and can be run with:
115
+
116
+ ```shell
117
+ uv run pytest
118
+ ```
119
+
120
+ Additionally, there is [ty] type-checking that can be run with:
121
+
122
+ ```shell
123
+ uv run ty check
124
+ ```
125
+
126
+ ### FastAPI
127
+
128
+ You can preview the FastAPI/Swagger documentation by running:
129
+
130
+ ```shell
131
+ uv run fastapi dev tests/app.py
132
+ ```
133
+
134
+ and visiting the Documentation link that's logged in the console.
135
+ This will auto-restart as you make changes.
136
+
110
137
  [ci-badge]: https://github.com/textbook/pydantic_json_patch/actions/workflows/push.yml/badge.svg
111
138
  [ci-page]: https://github.com/textbook/pydantic_json_patch/actions/workflows/push.yml
112
139
  [coverage-badge]: https://coveralls.io/repos/github/textbook/pydantic_json_patch/badge.svg?branch=main
113
140
  [coverage-page]: https://coveralls.io/github/textbook/pydantic_json_patch?branch=main
114
141
  [fastapi]: https://fastapi.tiangolo.com/
142
+ [isort]: https://pycqa.github.io/isort/
115
143
  [json patch]: https://datatracker.ietf.org/doc/html/rfc6902/
116
144
  [json pointer]: https://datatracker.ietf.org/doc/html/rfc6901/
117
145
  [pydantic]: https://docs.pydantic.dev/latest/
118
146
  [pypi]: https://pypi.org/
119
147
  [pypi-badge]: https://img.shields.io/pypi/v/pydantic-json-patch?logo=python&logoColor=white&label=PyPI
120
148
  [pypi-page]: https://pypi.org/project/pydantic-json-patch/
149
+ [pytest]: https://docs.pytest.org/en/stable/
150
+ [ruff]: https://docs.astral.sh/ruff/
121
151
  [swagger-example]: https://github.com/textbook/pydantic_json_patch/blob/main/docs/swagger-example.png?raw=true
122
152
  [swagger-schemas]: https://github.com/textbook/pydantic_json_patch/blob/main/docs/swagger-schemas.png?raw=true
153
+ [ty]: https://docs.astral.sh/ty/
123
154
  [uv]: https://docs.astral.sh/uv/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pydantic_json_patch"
3
- version = "0.1.3"
3
+ version = "0.3.0"
4
4
  description = "Pydantic models for implementing JSON Patch."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -3,7 +3,7 @@ from importlib.metadata import version
3
3
 
4
4
  from pydantic import Discriminator
5
5
 
6
- from .models import AddOp, CopyOp, MoveOp, Op, RemoveOp, ReplaceOp, TestOp, Tokens
6
+ from .models import AddOp, CopyOp, MoveOp, RemoveOp, ReplaceOp, TestOp
7
7
 
8
8
  __version__ = version(__name__)
9
9
 
@@ -18,10 +18,8 @@ __all__ = [
18
18
  "CopyOp",
19
19
  "JsonPatch",
20
20
  "MoveOp",
21
- "Op",
22
21
  "Operation",
23
22
  "RemoveOp",
24
23
  "ReplaceOp",
25
24
  "TestOp",
26
- "Tokens",
27
25
  ]
@@ -1,15 +1,13 @@
1
1
  import re
2
2
  import typing as tp
3
- from functools import cached_property
3
+ from collections.abc import Sequence
4
+ from functools import cached_property, lru_cache
4
5
 
5
6
  import typing_extensions as tx
6
7
  from pydantic import BaseModel, ConfigDict, Field, ValidationInfo, model_validator
7
8
 
8
9
  _JSON_POINTER = re.compile(r"^(?:/(?:[^/~]|~[01])+)*$")
9
10
 
10
- T = tp.TypeVar("T")
11
- Op: tp.TypeAlias = tp.Literal["add", "copy", "move", "remove", "replace", "test"]
12
- Tokens: tp.TypeAlias = tuple[str, ...]
13
11
 
14
12
  # region base models
15
13
 
@@ -21,39 +19,48 @@ class _BaseOp(BaseModel):
21
19
  )
22
20
 
23
21
  @classmethod
24
- def create(cls, *, path: str | Tokens, **kwargs) -> tx.Self:
22
+ def create(cls, *, path: str | Sequence[str], **kwargs) -> tx.Self:
25
23
  (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)
24
+ return cls(op=op, path=cls._dump_pointer(path), **kwargs)
28
25
 
29
- op: Op
26
+ op: tp.Literal["add", "copy", "move", "remove", "replace", "test"]
30
27
  """The operation being represented."""
31
28
 
32
29
  path: str = Field(examples=["/a/b/c"], pattern=_JSON_POINTER)
33
30
  """A JSON pointer representing the path to apply the operation to."""
34
31
 
35
32
  @cached_property
36
- def path_tokens(self) -> Tokens:
33
+ def path_tokens(self) -> tuple[str, ...]:
37
34
  """The decoded tokens in the 'path' JSON pointer."""
38
35
  return self._load_pointer(self.path)
39
36
 
37
+ @classmethod
38
+ def _dump_pointer(cls, pointer: Sequence[str]) -> str:
39
+ if isinstance(pointer, str):
40
+ return pointer
41
+ return "/".join(["", *(cls._encode_token(token) for token in pointer)])
42
+
40
43
  @staticmethod
41
- def _dump_pointer(pointer: Tokens) -> str:
42
- return "/".join(
43
- ["", *(token.replace("~", "~0").replace("/", "~1") for token in pointer)]
44
- )
44
+ @lru_cache
45
+ def _decode_token(token: str) -> str:
46
+ return token.replace("~1", "/").replace("~0", "~")
45
47
 
46
48
  @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
- )
49
+ @lru_cache
50
+ def _encode_token(token: str) -> str:
51
+ return token.replace("~", "~0").replace("/", "~1")
52
+
53
+ @classmethod
54
+ @lru_cache
55
+ def _load_pointer(cls, pointer: str) -> tuple[str, ...]:
56
+ return tuple(cls._decode_token(token) for token in pointer.split("/")[1:])
52
57
 
53
58
 
54
59
  class _FromOp(_BaseOp):
55
60
  @classmethod
56
- def create(cls, *, path: str | Tokens, from_: str | Tokens) -> tx.Self: # type: ignore[invalid-method-override]
61
+ def create(
62
+ cls, *, path: str | Sequence[str], from_: str | Sequence[str]
63
+ ) -> tx.Self: # type: ignore[invalid-method-override]
57
64
  pointer = from_ if isinstance(from_, str) else cls._dump_pointer(from_)
58
65
  return super().create(path=path, **{"from": pointer})
59
66
 
@@ -73,17 +80,17 @@ class _FromOp(_BaseOp):
73
80
  return data
74
81
 
75
82
  @cached_property
76
- def from_tokens(self) -> Tokens:
83
+ def from_tokens(self) -> tuple[str, ...]:
77
84
  """The decoded tokens in the 'from' JSON pointer."""
78
85
  return self._load_pointer(self.from_)
79
86
 
80
87
 
81
- class _ValueOp(_BaseOp, tp.Generic[T]):
88
+ class _ValueOp(_BaseOp):
82
89
  @classmethod
83
- def create(cls, *, path: str | Tokens, value: T) -> tx.Self: # type: ignore[invalid-method-override]
90
+ def create(cls, *, path: str | Sequence[str], value: tp.Any) -> tx.Self: # type: ignore[invalid-method-override]
84
91
  return super().create(path=path, value=value)
85
92
 
86
- value: T = Field(examples=[42])
93
+ value: tp.Any = Field(examples=[42])
87
94
  """The value to use in the operation."""
88
95
 
89
96
 
@@ -92,7 +99,7 @@ class _ValueOp(_BaseOp, tp.Generic[T]):
92
99
  # region public models
93
100
 
94
101
 
95
- class AddOp(_ValueOp, tp.Generic[T]):
102
+ class AddOp(_ValueOp):
96
103
  op: tp.Literal["add"]
97
104
 
98
105
 
@@ -106,17 +113,17 @@ class MoveOp(_FromOp):
106
113
 
107
114
  class RemoveOp(_BaseOp):
108
115
  @classmethod
109
- def create(cls, *, path: str | Tokens) -> tx.Self: # type: ignore[invalid-method-override]
116
+ def create(cls, *, path: str | Sequence[str]) -> tx.Self: # type: ignore[invalid-method-override]
110
117
  return super().create(path=path)
111
118
 
112
119
  op: tp.Literal["remove"]
113
120
 
114
121
 
115
- class ReplaceOp(_ValueOp, tp.Generic[T]):
122
+ class ReplaceOp(_ValueOp):
116
123
  op: tp.Literal["replace"]
117
124
 
118
125
 
119
- class TestOp(_ValueOp, tp.Generic[T]):
126
+ class TestOp(_ValueOp):
120
127
  op: tp.Literal["test"]
121
128
 
122
129