fsrest 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fsrest-0.1.0/.gitignore +9 -0
- fsrest-0.1.0/LICENSE +22 -0
- fsrest-0.1.0/PKG-INFO +133 -0
- fsrest-0.1.0/README.md +88 -0
- fsrest-0.1.0/pyproject.toml +55 -0
- fsrest-0.1.0/src/fsrest/__init__.py +28 -0
- fsrest-0.1.0/src/fsrest/base.py +163 -0
- fsrest-0.1.0/src/fsrest/exceptions.py +46 -0
- fsrest-0.1.0/src/fsrest/py.typed +1 -0
- fsrest-0.1.0/src/fsrest/schemas.py +49 -0
- fsrest-0.1.0/tests/test_crud.py +126 -0
- fsrest-0.1.0/uv.lock +538 -0
fsrest-0.1.0/.gitignore
ADDED
fsrest-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 huoyinghui
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
fsrest-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fsrest
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reusable, framework-agnostic REST CRUD logic for Pydantic applications
|
|
5
|
+
Project-URL: Homepage, https://github.com/pydtools/fsrest
|
|
6
|
+
Project-URL: Repository, https://github.com/pydtools/fsrest
|
|
7
|
+
Project-URL: Issues, https://github.com/pydtools/fsrest/issues
|
|
8
|
+
Author-email: huoyinghui <hyhlinux@gmail.com>
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2026 huoyinghui
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Keywords: crud,dao,fastapi,pydantic,rest
|
|
33
|
+
Classifier: Development Status :: 3 - Alpha
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Programming Language :: Python :: 3
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
41
|
+
Classifier: Typing :: Typed
|
|
42
|
+
Requires-Python: >=3.9
|
|
43
|
+
Requires-Dist: pydantic<3,>=1.10
|
|
44
|
+
Description-Content-Type: text/markdown
|
|
45
|
+
|
|
46
|
+
# fsrest
|
|
47
|
+
|
|
48
|
+
`fsrest` extracts reusable single-resource REST CRUD orchestration from application code. It is framework-independent: request/response objects are Pydantic models, while persistence is supplied through a small DAO protocol.
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install fsrest
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Python 3.9+ and Pydantic 1.10/2.x are supported.
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
Bind a DAO to `RestCrudLogicBase`. Request schemas provide the small conversion methods needed to turn HTTP-facing data into DAO fields.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from pydantic import BaseModel
|
|
64
|
+
from fsrest import RestCrudLogicBase, RestPageReqSchema, RestPageRespSchema
|
|
65
|
+
|
|
66
|
+
class Item(BaseModel):
|
|
67
|
+
id: str
|
|
68
|
+
name: str
|
|
69
|
+
|
|
70
|
+
class Filters(BaseModel):
|
|
71
|
+
name: str | None = None
|
|
72
|
+
|
|
73
|
+
class Ordering(BaseModel):
|
|
74
|
+
field: str = "id"
|
|
75
|
+
|
|
76
|
+
class PageData(BaseModel):
|
|
77
|
+
items: list[Item]
|
|
78
|
+
total: int
|
|
79
|
+
|
|
80
|
+
class ListQuery(RestPageReqSchema):
|
|
81
|
+
name: str | None = None
|
|
82
|
+
|
|
83
|
+
def build_filters(self) -> Filters:
|
|
84
|
+
return Filters(name=self.name)
|
|
85
|
+
|
|
86
|
+
def build_ordering(self) -> Ordering:
|
|
87
|
+
return Ordering()
|
|
88
|
+
|
|
89
|
+
def build_response(self, *, page_data: PageData) -> RestPageRespSchema[Item]:
|
|
90
|
+
return RestPageRespSchema[Item](
|
|
91
|
+
items=page_data.items,
|
|
92
|
+
total=page_data.total,
|
|
93
|
+
page=self.page,
|
|
94
|
+
page_size=self.page_size,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
class ItemDao:
|
|
98
|
+
@classmethod
|
|
99
|
+
def list_schema_page(cls, *, filters, ordering, page, page_size) -> PageData:
|
|
100
|
+
...
|
|
101
|
+
|
|
102
|
+
# Also implement get_schema_by_id, create_schema,
|
|
103
|
+
# update_schema_by_id, and delete_by_id.
|
|
104
|
+
|
|
105
|
+
class ItemLogic(RestCrudLogicBase):
|
|
106
|
+
dao_rest_crud = ItemDao
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The library raises `RestApiError` for missing records and failed deletes. To integrate with an application's existing exception middleware, subclass it and bind `error_class`:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
class ApplicationApiError(RestApiError):
|
|
113
|
+
error_code = 400455
|
|
114
|
+
|
|
115
|
+
class ItemLogic(RestCrudLogicBase):
|
|
116
|
+
dao_rest_crud = ItemDao
|
|
117
|
+
error_class = ApplicationApiError
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Development and publishing
|
|
121
|
+
|
|
122
|
+
From the `pytools` repository root, use the unified release script:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
python make.py fsrest test
|
|
126
|
+
python make.py fsrest build
|
|
127
|
+
python make.py fsrest publish
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`publish` uploads the artifacts under `fsrest/dist/` using the PyPI credentials
|
|
131
|
+
configured in `~/.pypirc`. Before publishing a new release, update the version
|
|
132
|
+
in `pyproject.toml`, run tests, and build fresh artifacts.
|
|
133
|
+
|
fsrest-0.1.0/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# fsrest
|
|
2
|
+
|
|
3
|
+
`fsrest` extracts reusable single-resource REST CRUD orchestration from application code. It is framework-independent: request/response objects are Pydantic models, while persistence is supplied through a small DAO protocol.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install fsrest
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Python 3.9+ and Pydantic 1.10/2.x are supported.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
Bind a DAO to `RestCrudLogicBase`. Request schemas provide the small conversion methods needed to turn HTTP-facing data into DAO fields.
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from pydantic import BaseModel
|
|
19
|
+
from fsrest import RestCrudLogicBase, RestPageReqSchema, RestPageRespSchema
|
|
20
|
+
|
|
21
|
+
class Item(BaseModel):
|
|
22
|
+
id: str
|
|
23
|
+
name: str
|
|
24
|
+
|
|
25
|
+
class Filters(BaseModel):
|
|
26
|
+
name: str | None = None
|
|
27
|
+
|
|
28
|
+
class Ordering(BaseModel):
|
|
29
|
+
field: str = "id"
|
|
30
|
+
|
|
31
|
+
class PageData(BaseModel):
|
|
32
|
+
items: list[Item]
|
|
33
|
+
total: int
|
|
34
|
+
|
|
35
|
+
class ListQuery(RestPageReqSchema):
|
|
36
|
+
name: str | None = None
|
|
37
|
+
|
|
38
|
+
def build_filters(self) -> Filters:
|
|
39
|
+
return Filters(name=self.name)
|
|
40
|
+
|
|
41
|
+
def build_ordering(self) -> Ordering:
|
|
42
|
+
return Ordering()
|
|
43
|
+
|
|
44
|
+
def build_response(self, *, page_data: PageData) -> RestPageRespSchema[Item]:
|
|
45
|
+
return RestPageRespSchema[Item](
|
|
46
|
+
items=page_data.items,
|
|
47
|
+
total=page_data.total,
|
|
48
|
+
page=self.page,
|
|
49
|
+
page_size=self.page_size,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
class ItemDao:
|
|
53
|
+
@classmethod
|
|
54
|
+
def list_schema_page(cls, *, filters, ordering, page, page_size) -> PageData:
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
# Also implement get_schema_by_id, create_schema,
|
|
58
|
+
# update_schema_by_id, and delete_by_id.
|
|
59
|
+
|
|
60
|
+
class ItemLogic(RestCrudLogicBase):
|
|
61
|
+
dao_rest_crud = ItemDao
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The library raises `RestApiError` for missing records and failed deletes. To integrate with an application's existing exception middleware, subclass it and bind `error_class`:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
class ApplicationApiError(RestApiError):
|
|
68
|
+
error_code = 400455
|
|
69
|
+
|
|
70
|
+
class ItemLogic(RestCrudLogicBase):
|
|
71
|
+
dao_rest_crud = ItemDao
|
|
72
|
+
error_class = ApplicationApiError
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Development and publishing
|
|
76
|
+
|
|
77
|
+
From the `pytools` repository root, use the unified release script:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
python make.py fsrest test
|
|
81
|
+
python make.py fsrest build
|
|
82
|
+
python make.py fsrest publish
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`publish` uploads the artifacts under `fsrest/dist/` using the PyPI credentials
|
|
86
|
+
configured in `~/.pypirc`. Before publishing a new release, update the version
|
|
87
|
+
in `pyproject.toml`, run tests, and build fresh artifacts.
|
|
88
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fsrest"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Reusable, framework-agnostic REST CRUD logic for Pydantic applications"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "huoyinghui", email = "hyhlinux@gmail.com" },
|
|
13
|
+
]
|
|
14
|
+
requires-python = ">=3.9"
|
|
15
|
+
dependencies = [
|
|
16
|
+
"pydantic>=1.10,<3",
|
|
17
|
+
]
|
|
18
|
+
keywords = ["rest", "crud", "pydantic", "fastapi", "dao"]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 3 - Alpha",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.9",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
"Typing :: Typed",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/pydtools/fsrest"
|
|
33
|
+
Repository = "https://github.com/pydtools/fsrest"
|
|
34
|
+
Issues = "https://github.com/pydtools/fsrest/issues"
|
|
35
|
+
|
|
36
|
+
[dependency-groups]
|
|
37
|
+
dev = [
|
|
38
|
+
"build>=1.2",
|
|
39
|
+
"pytest>=7",
|
|
40
|
+
"ruff>=0.9",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["src/fsrest"]
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
pythonpath = ["src"]
|
|
48
|
+
testpaths = ["tests"]
|
|
49
|
+
|
|
50
|
+
[tool.ruff]
|
|
51
|
+
line-length = 120
|
|
52
|
+
target-version = "py39"
|
|
53
|
+
|
|
54
|
+
[tool.ruff.lint]
|
|
55
|
+
select = ["E", "F", "I", "UP"]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Public API for fsrest."""
|
|
2
|
+
|
|
3
|
+
from .base import (
|
|
4
|
+
RestCreateReqProtocol,
|
|
5
|
+
RestCrudDaoProtocol,
|
|
6
|
+
RestCrudLogicBase,
|
|
7
|
+
RestIdReqProtocol,
|
|
8
|
+
RestListReqProtocol,
|
|
9
|
+
RestUpdateReqProtocol,
|
|
10
|
+
)
|
|
11
|
+
from .exceptions import APIError, RestApiError
|
|
12
|
+
from .schemas import RestCrudSchemaMapSchema, RestDeleteRespSchema, RestPageReqSchema, RestPageRespSchema
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"APIError",
|
|
16
|
+
"RestApiError",
|
|
17
|
+
"RestCreateReqProtocol",
|
|
18
|
+
"RestCrudDaoProtocol",
|
|
19
|
+
"RestCrudLogicBase",
|
|
20
|
+
"RestCrudSchemaMapSchema",
|
|
21
|
+
"RestDeleteRespSchema",
|
|
22
|
+
"RestIdReqProtocol",
|
|
23
|
+
"RestListReqProtocol",
|
|
24
|
+
"RestPageReqSchema",
|
|
25
|
+
"RestPageRespSchema",
|
|
26
|
+
"RestUpdateReqProtocol",
|
|
27
|
+
]
|
|
28
|
+
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Reusable single-resource REST CRUD logic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Generic, Protocol, TypeVar, cast
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from .exceptions import RestApiError
|
|
10
|
+
from .schemas import RestCrudSchemaMapSchema, RestDeleteRespSchema
|
|
11
|
+
|
|
12
|
+
RestItemSchemaT = TypeVar("RestItemSchemaT", bound=BaseModel)
|
|
13
|
+
RestListReqSchemaT = TypeVar("RestListReqSchemaT", bound=BaseModel)
|
|
14
|
+
RestListRespSchemaT = TypeVar("RestListRespSchemaT", bound=BaseModel)
|
|
15
|
+
RestListFilterSchemaT = TypeVar("RestListFilterSchemaT", bound=BaseModel)
|
|
16
|
+
RestOrderingSchemaT = TypeVar("RestOrderingSchemaT", bound=BaseModel)
|
|
17
|
+
RestPageDataSchemaT = TypeVar("RestPageDataSchemaT", bound=BaseModel)
|
|
18
|
+
RestGetReqSchemaT = TypeVar("RestGetReqSchemaT", bound=BaseModel)
|
|
19
|
+
RestCreateReqSchemaT = TypeVar("RestCreateReqSchemaT", bound=BaseModel)
|
|
20
|
+
RestCreateFieldsSchemaT = TypeVar("RestCreateFieldsSchemaT", bound=BaseModel)
|
|
21
|
+
RestUpdateReqSchemaT = TypeVar("RestUpdateReqSchemaT", bound=BaseModel)
|
|
22
|
+
RestUpdateFieldsSchemaT = TypeVar("RestUpdateFieldsSchemaT", bound=BaseModel)
|
|
23
|
+
RestDeleteReqSchemaT = TypeVar("RestDeleteReqSchemaT", bound=BaseModel)
|
|
24
|
+
|
|
25
|
+
ItemT_co = TypeVar("ItemT_co", bound=BaseModel, covariant=True)
|
|
26
|
+
FilterT_co = TypeVar("FilterT_co", bound=BaseModel, covariant=True)
|
|
27
|
+
FilterT_contra = TypeVar("FilterT_contra", bound=BaseModel, contravariant=True)
|
|
28
|
+
OrderingT_co = TypeVar("OrderingT_co", bound=BaseModel, covariant=True)
|
|
29
|
+
OrderingT_contra = TypeVar("OrderingT_contra", bound=BaseModel, contravariant=True)
|
|
30
|
+
PageDataT_co = TypeVar("PageDataT_co", bound=BaseModel, covariant=True)
|
|
31
|
+
PageDataT_contra = TypeVar("PageDataT_contra", bound=BaseModel, contravariant=True)
|
|
32
|
+
ListRespT_co = TypeVar("ListRespT_co", bound=BaseModel, covariant=True)
|
|
33
|
+
CreateFieldsT_co = TypeVar("CreateFieldsT_co", bound=BaseModel, covariant=True)
|
|
34
|
+
CreateFieldsT_contra = TypeVar("CreateFieldsT_contra", bound=BaseModel, contravariant=True)
|
|
35
|
+
UpdateFieldsT_co = TypeVar("UpdateFieldsT_co", bound=BaseModel, covariant=True)
|
|
36
|
+
UpdateFieldsT_contra = TypeVar("UpdateFieldsT_contra", bound=BaseModel, contravariant=True)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class RestListReqProtocol(Protocol[FilterT_co, OrderingT_co, PageDataT_contra, ListRespT_co]):
|
|
40
|
+
page: int
|
|
41
|
+
page_size: int
|
|
42
|
+
|
|
43
|
+
def build_filters(self) -> FilterT_co: ...
|
|
44
|
+
|
|
45
|
+
def build_ordering(self) -> OrderingT_co: ...
|
|
46
|
+
|
|
47
|
+
def build_response(self, *, page_data: PageDataT_contra) -> ListRespT_co: ...
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class RestIdReqProtocol(Protocol):
|
|
51
|
+
id: str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class RestCreateReqProtocol(Protocol[CreateFieldsT_co]):
|
|
55
|
+
def build_create_fields(self) -> CreateFieldsT_co: ...
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class RestUpdateReqProtocol(Protocol[UpdateFieldsT_co]):
|
|
59
|
+
id: str
|
|
60
|
+
|
|
61
|
+
def build_update_fields(self) -> UpdateFieldsT_co: ...
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class RestCrudDaoProtocol(
|
|
65
|
+
Protocol[ItemT_co, FilterT_contra, OrderingT_contra, PageDataT_co, CreateFieldsT_contra, UpdateFieldsT_contra]
|
|
66
|
+
):
|
|
67
|
+
@classmethod
|
|
68
|
+
def list_schema_page(
|
|
69
|
+
cls, *, filters: FilterT_contra, ordering: OrderingT_contra, page: int, page_size: int
|
|
70
|
+
) -> PageDataT_co: ...
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def get_schema_by_id(cls, item_id: str) -> ItemT_co | None: ...
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def create_schema(cls, *, fields: CreateFieldsT_contra) -> ItemT_co: ...
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def update_schema_by_id(cls, *, item_id: str, fields: UpdateFieldsT_contra) -> ItemT_co | None: ...
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def delete_by_id(cls, item_id: str) -> bool: ...
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class RestCrudLogicBase(
|
|
86
|
+
Generic[
|
|
87
|
+
RestItemSchemaT,
|
|
88
|
+
RestListReqSchemaT,
|
|
89
|
+
RestListRespSchemaT,
|
|
90
|
+
RestListFilterSchemaT,
|
|
91
|
+
RestOrderingSchemaT,
|
|
92
|
+
RestPageDataSchemaT,
|
|
93
|
+
RestGetReqSchemaT,
|
|
94
|
+
RestCreateReqSchemaT,
|
|
95
|
+
RestCreateFieldsSchemaT,
|
|
96
|
+
RestUpdateReqSchemaT,
|
|
97
|
+
RestUpdateFieldsSchemaT,
|
|
98
|
+
RestDeleteReqSchemaT,
|
|
99
|
+
]
|
|
100
|
+
):
|
|
101
|
+
"""Base logic for a single-resource REST CRUD API.
|
|
102
|
+
|
|
103
|
+
Subclasses bind ``dao_rest_crud``. ``schema_map`` remains available for
|
|
104
|
+
endpoint generators and introspection, but the CRUD methods do not require it.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
dao_rest_crud: type[
|
|
108
|
+
RestCrudDaoProtocol[
|
|
109
|
+
RestItemSchemaT,
|
|
110
|
+
RestListFilterSchemaT,
|
|
111
|
+
RestOrderingSchemaT,
|
|
112
|
+
RestPageDataSchemaT,
|
|
113
|
+
RestCreateFieldsSchemaT,
|
|
114
|
+
RestUpdateFieldsSchemaT,
|
|
115
|
+
]
|
|
116
|
+
]
|
|
117
|
+
schema_map: RestCrudSchemaMapSchema
|
|
118
|
+
error_class: type[RestApiError] = RestApiError
|
|
119
|
+
|
|
120
|
+
@classmethod
|
|
121
|
+
def list_items(cls, *, query: RestListReqSchemaT) -> RestListRespSchemaT:
|
|
122
|
+
contract = cast(
|
|
123
|
+
RestListReqProtocol[RestListFilterSchemaT, RestOrderingSchemaT, RestPageDataSchemaT, RestListRespSchemaT],
|
|
124
|
+
query,
|
|
125
|
+
)
|
|
126
|
+
page_data = cls.dao_rest_crud.list_schema_page(
|
|
127
|
+
filters=contract.build_filters(),
|
|
128
|
+
ordering=contract.build_ordering(),
|
|
129
|
+
page=contract.page,
|
|
130
|
+
page_size=contract.page_size,
|
|
131
|
+
)
|
|
132
|
+
return contract.build_response(page_data=page_data)
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def get_item(cls, *, query: RestGetReqSchemaT) -> RestItemSchemaT:
|
|
136
|
+
contract = cast(RestIdReqProtocol, query)
|
|
137
|
+
item = cls.dao_rest_crud.get_schema_by_id(contract.id)
|
|
138
|
+
if item is None:
|
|
139
|
+
raise cls.error_class(f"Record not found: {contract.id}")
|
|
140
|
+
return item
|
|
141
|
+
|
|
142
|
+
@classmethod
|
|
143
|
+
def create_item(cls, *, payload: RestCreateReqSchemaT) -> RestItemSchemaT:
|
|
144
|
+
contract = cast(RestCreateReqProtocol[RestCreateFieldsSchemaT], payload)
|
|
145
|
+
return cls.dao_rest_crud.create_schema(fields=contract.build_create_fields())
|
|
146
|
+
|
|
147
|
+
@classmethod
|
|
148
|
+
def update_item(cls, *, payload: RestUpdateReqSchemaT) -> RestItemSchemaT:
|
|
149
|
+
contract = cast(RestUpdateReqProtocol[RestUpdateFieldsSchemaT], payload)
|
|
150
|
+
item = cls.dao_rest_crud.update_schema_by_id(item_id=contract.id, fields=contract.build_update_fields())
|
|
151
|
+
if item is None:
|
|
152
|
+
raise cls.error_class(f"Record not found: {contract.id}")
|
|
153
|
+
return item
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def delete_item(cls, *, payload: RestDeleteReqSchemaT) -> RestDeleteRespSchema:
|
|
157
|
+
contract = cast(RestIdReqProtocol, payload)
|
|
158
|
+
if cls.dao_rest_crud.get_schema_by_id(contract.id) is None:
|
|
159
|
+
raise cls.error_class(f"Record not found: {contract.id}")
|
|
160
|
+
if not cls.dao_rest_crud.delete_by_id(contract.id):
|
|
161
|
+
raise cls.error_class(f"Failed to delete record: {contract.id}")
|
|
162
|
+
return RestDeleteRespSchema(id=contract.id, deleted=True)
|
|
163
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Exceptions raised by :mod:`fsrest`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RestApiError(Exception):
|
|
9
|
+
"""A framework-independent REST business error.
|
|
10
|
+
|
|
11
|
+
``code`` and ``error_code`` are intentionally plain integers so an
|
|
12
|
+
application can translate the exception into its own HTTP response type.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
code = 400
|
|
16
|
+
error_code = 400455
|
|
17
|
+
message = "REST operation failed"
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
message: str | None = None,
|
|
22
|
+
*,
|
|
23
|
+
code: int | None = None,
|
|
24
|
+
error_code: int | None = None,
|
|
25
|
+
data: Any = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
self.message = message if message is not None else type(self).message
|
|
28
|
+
self.code = code if code is not None else type(self).code
|
|
29
|
+
self.error_code = error_code if error_code is not None else type(self).error_code
|
|
30
|
+
self.data = data
|
|
31
|
+
super().__init__(self.message)
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def msg(self) -> str:
|
|
35
|
+
"""Compatibility alias used by applications that call the field ``msg``."""
|
|
36
|
+
|
|
37
|
+
return self.message
|
|
38
|
+
|
|
39
|
+
@msg.setter
|
|
40
|
+
def msg(self, value: str) -> None:
|
|
41
|
+
self.message = value
|
|
42
|
+
self.args = (value,)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Compatibility with the original internal package.
|
|
46
|
+
APIError = RestApiError
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Common Pydantic schemas used by REST CRUD logic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Generic, TypeVar
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
RestItemSchemaT = TypeVar("RestItemSchemaT", bound=BaseModel)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RestPageReqSchema(BaseModel):
|
|
13
|
+
"""Page-number pagination input."""
|
|
14
|
+
|
|
15
|
+
page: int = Field(default=1, ge=1, le=100000, description="Page number, starting at 1")
|
|
16
|
+
page_size: int = Field(default=20, ge=1, le=200, description="Number of records per page")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RestPageRespSchema(BaseModel, Generic[RestItemSchemaT]):
|
|
20
|
+
"""Page-number pagination response."""
|
|
21
|
+
|
|
22
|
+
items: list[RestItemSchemaT] = Field(default_factory=list, description="Records on the current page")
|
|
23
|
+
total: int = Field(default=0, ge=0, description="Total number of records")
|
|
24
|
+
page: int = Field(default=1, ge=1, description="Current page number")
|
|
25
|
+
page_size: int = Field(default=20, ge=1, description="Number of records per page")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RestDeleteRespSchema(BaseModel):
|
|
29
|
+
"""Deletion response."""
|
|
30
|
+
|
|
31
|
+
id: str = Field(..., description="ID of the deleted record")
|
|
32
|
+
deleted: bool = Field(default=True, description="Whether the record was deleted")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class RestCrudSchemaMapSchema(BaseModel):
|
|
36
|
+
"""Concrete schema types bound to a :class:`RestCrudLogicBase` subclass."""
|
|
37
|
+
|
|
38
|
+
item_schema: type[BaseModel]
|
|
39
|
+
list_req_schema: type[BaseModel]
|
|
40
|
+
list_resp_schema: type[BaseModel]
|
|
41
|
+
list_filter_schema: type[BaseModel]
|
|
42
|
+
ordering_schema: type[BaseModel]
|
|
43
|
+
page_data_schema: type[BaseModel]
|
|
44
|
+
get_req_schema: type[BaseModel]
|
|
45
|
+
create_req_schema: type[BaseModel]
|
|
46
|
+
create_fields_schema: type[BaseModel]
|
|
47
|
+
update_req_schema: type[BaseModel]
|
|
48
|
+
update_fields_schema: type[BaseModel]
|
|
49
|
+
delete_req_schema: type[BaseModel]
|