apexdevkit 1.5.20__tar.gz → 1.5.22__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.
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/PKG-INFO +1 -1
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/apexdevkit/fastapi/builder.py +23 -0
- apexdevkit-1.5.22/apexdevkit/fastapi/resource.py +177 -0
- apexdevkit-1.5.22/apexdevkit/fastapi/response.py +130 -0
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/apexdevkit/fastapi/router.py +19 -273
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/pyproject.toml +1 -1
- apexdevkit-1.5.20/apexdevkit/fastapi/response.py +0 -64
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/LICENSE +0 -0
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/README.md +0 -0
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/apexdevkit/__init__.py +0 -0
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/apexdevkit/annotation/__init__.py +0 -0
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/apexdevkit/annotation/deprecate.py +0 -0
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/apexdevkit/error.py +0 -0
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/apexdevkit/fastapi/__init__.py +0 -0
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/apexdevkit/fastapi/dependable.py +0 -0
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/apexdevkit/fastapi/docs.py +0 -0
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/apexdevkit/fastapi/schema.py +0 -0
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/apexdevkit/fastapi/service.py +0 -0
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/apexdevkit/formatter.py +0 -0
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/apexdevkit/http/__init__.py +0 -0
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/apexdevkit/http/fake.py +0 -0
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/apexdevkit/http/fluent.py +0 -0
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/apexdevkit/http/httpx.py +0 -0
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/apexdevkit/http/json.py +0 -0
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/apexdevkit/http/url.py +0 -0
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/apexdevkit/py.typed +0 -0
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/apexdevkit/repository/__init__.py +0 -0
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/apexdevkit/repository/base.py +0 -0
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/apexdevkit/repository/connector.py +0 -0
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/apexdevkit/repository/database.py +0 -0
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/apexdevkit/repository/in_memory.py +0 -0
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/apexdevkit/repository/interface.py +0 -0
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/apexdevkit/testing/__init__.py +0 -0
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/apexdevkit/testing/database.py +0 -0
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/apexdevkit/testing/fake.py +0 -0
- {apexdevkit-1.5.20 → apexdevkit-1.5.22}/apexdevkit/testing/rest.py +0 -0
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
1
2
|
from dataclasses import dataclass, field
|
|
2
3
|
from typing import Any, Self
|
|
3
4
|
|
|
4
5
|
from fastapi import APIRouter, FastAPI
|
|
5
6
|
|
|
7
|
+
from apexdevkit.fastapi.service import RestfulService
|
|
8
|
+
|
|
6
9
|
|
|
7
10
|
@dataclass
|
|
8
11
|
class FastApiBuilder:
|
|
@@ -37,3 +40,23 @@ class FastApiBuilder:
|
|
|
37
40
|
self.app.include_router(value, prefix=f"/{key}", tags=[key.title()])
|
|
38
41
|
|
|
39
42
|
return self
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class RestfulServiceBuilder(ABC):
|
|
47
|
+
parent_id: str = field(init=False)
|
|
48
|
+
user: Any = field(init=False)
|
|
49
|
+
|
|
50
|
+
def with_user(self, user: Any) -> "RestfulServiceBuilder":
|
|
51
|
+
self.user = user
|
|
52
|
+
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
def with_parent(self, identity: str) -> "RestfulServiceBuilder":
|
|
56
|
+
self.parent_id = identity
|
|
57
|
+
|
|
58
|
+
return self
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def build(self) -> RestfulService: # pragma: no cover
|
|
62
|
+
pass
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from functools import cached_property
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
|
|
5
|
+
from starlette.responses import JSONResponse
|
|
6
|
+
|
|
7
|
+
from apexdevkit.error import DoesNotExistError, ExistsError, ForbiddenError
|
|
8
|
+
from apexdevkit.fastapi.builder import RestfulServiceBuilder
|
|
9
|
+
from apexdevkit.fastapi.response import RestfulResponse
|
|
10
|
+
from apexdevkit.testing import RestfulName
|
|
11
|
+
|
|
12
|
+
_Response = JSONResponse | dict[str, Any]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class RestfulResource:
|
|
17
|
+
name: RestfulName
|
|
18
|
+
infra: RestfulServiceBuilder
|
|
19
|
+
parent: RestfulName
|
|
20
|
+
|
|
21
|
+
@cached_property
|
|
22
|
+
def response(self) -> RestfulResponse:
|
|
23
|
+
return RestfulResponse(name=self.name)
|
|
24
|
+
|
|
25
|
+
def create_one(self, User, ParentId, Item) -> Callable[..., _Response]: # type: ignore
|
|
26
|
+
def endpoint(user: User, parent_id: ParentId, item: Item) -> _Response:
|
|
27
|
+
try:
|
|
28
|
+
service = self.infra.with_user(user).with_parent(parent_id).build()
|
|
29
|
+
except DoesNotExistError as e:
|
|
30
|
+
return JSONResponse(RestfulResponse(self.parent).not_found(e), 404)
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
item = service.create_one(item)
|
|
34
|
+
except ExistsError as e:
|
|
35
|
+
return JSONResponse(self.response.exists(e), 409)
|
|
36
|
+
except ForbiddenError as e:
|
|
37
|
+
return JSONResponse(self.response.forbidden(e), 403)
|
|
38
|
+
|
|
39
|
+
return self.response.created_one(item)
|
|
40
|
+
|
|
41
|
+
return endpoint
|
|
42
|
+
|
|
43
|
+
def create_many(self, User, ParentId, Collection) -> Callable[..., _Response]: # type: ignore
|
|
44
|
+
def endpoint(user: User, parent_id: ParentId, items: Collection) -> _Response:
|
|
45
|
+
try:
|
|
46
|
+
service = self.infra.with_user(user).with_parent(parent_id).build()
|
|
47
|
+
except DoesNotExistError as e:
|
|
48
|
+
return JSONResponse(RestfulResponse(self.parent).not_found(e), 404)
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
return self.response.created_many(service.create_many(items))
|
|
52
|
+
except ExistsError as e:
|
|
53
|
+
return JSONResponse(self.response.exists(e), 409)
|
|
54
|
+
except ForbiddenError as e:
|
|
55
|
+
return JSONResponse(self.response.forbidden(e), 403)
|
|
56
|
+
|
|
57
|
+
return endpoint
|
|
58
|
+
|
|
59
|
+
def read_one(self, User, ParentId, ItemId) -> Callable[..., _Response]: # type: ignore
|
|
60
|
+
def endpoint(user: User, parent_id: ParentId, item_id: ItemId) -> _Response:
|
|
61
|
+
try:
|
|
62
|
+
service = self.infra.with_user(user).with_parent(parent_id).build()
|
|
63
|
+
except DoesNotExistError as e:
|
|
64
|
+
return JSONResponse(RestfulResponse(self.parent).not_found(e), 404)
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
return self.response.found_one(service.read_one(item_id))
|
|
68
|
+
except DoesNotExistError as e:
|
|
69
|
+
return JSONResponse(self.response.not_found(e), 404)
|
|
70
|
+
except ForbiddenError as e:
|
|
71
|
+
return JSONResponse(self.response.forbidden(e), 403)
|
|
72
|
+
|
|
73
|
+
return endpoint
|
|
74
|
+
|
|
75
|
+
def read_all(self, User, ParentId) -> Callable[..., _Response]: # type: ignore
|
|
76
|
+
def endpoint(user: User, parent_id: ParentId) -> _Response:
|
|
77
|
+
try:
|
|
78
|
+
service = self.infra.with_user(user).with_parent(parent_id).build()
|
|
79
|
+
except DoesNotExistError as e:
|
|
80
|
+
return JSONResponse(RestfulResponse(self.parent).not_found(e), 404)
|
|
81
|
+
try:
|
|
82
|
+
return self.response.found_many(list(service.read_all()))
|
|
83
|
+
except ForbiddenError as e:
|
|
84
|
+
return JSONResponse(self.response.forbidden(e), 403)
|
|
85
|
+
|
|
86
|
+
return endpoint
|
|
87
|
+
|
|
88
|
+
def update_one(self, User, ParentId, ItemId, Updates) -> Callable[..., _Response]: # type: ignore
|
|
89
|
+
def endpoint(
|
|
90
|
+
user: User,
|
|
91
|
+
parent_id: ParentId,
|
|
92
|
+
item_id: ItemId,
|
|
93
|
+
updates: Updates,
|
|
94
|
+
) -> _Response:
|
|
95
|
+
try:
|
|
96
|
+
service = self.infra.with_user(user).with_parent(parent_id).build()
|
|
97
|
+
except DoesNotExistError as e:
|
|
98
|
+
return JSONResponse(RestfulResponse(self.parent).not_found(e), 404)
|
|
99
|
+
try:
|
|
100
|
+
service.update_one(item_id, **updates)
|
|
101
|
+
except DoesNotExistError as e:
|
|
102
|
+
return JSONResponse(self.response.not_found(e), 404)
|
|
103
|
+
except ForbiddenError as e:
|
|
104
|
+
return JSONResponse(self.response.forbidden(e), 403)
|
|
105
|
+
|
|
106
|
+
return self.response.ok()
|
|
107
|
+
|
|
108
|
+
return endpoint
|
|
109
|
+
|
|
110
|
+
def update_many(self, User, ParentId, Collection) -> Callable[..., _Response]: # type: ignore
|
|
111
|
+
def endpoint(user: User, parent_id: ParentId, items: Collection) -> _Response:
|
|
112
|
+
try:
|
|
113
|
+
service = self.infra.with_user(user).with_parent(parent_id).build()
|
|
114
|
+
except DoesNotExistError as e:
|
|
115
|
+
return JSONResponse(RestfulResponse(self.parent).not_found(e), 404)
|
|
116
|
+
try:
|
|
117
|
+
service.update_many(items)
|
|
118
|
+
except DoesNotExistError as e:
|
|
119
|
+
return JSONResponse(self.response.not_found(e), 404)
|
|
120
|
+
except ForbiddenError as e:
|
|
121
|
+
return JSONResponse(self.response.forbidden(e), 403)
|
|
122
|
+
|
|
123
|
+
return self.response.ok()
|
|
124
|
+
|
|
125
|
+
return endpoint
|
|
126
|
+
|
|
127
|
+
def replace_one(self, User, ParentId, Item) -> Callable[..., _Response]: # type: ignore
|
|
128
|
+
def endpoint(user: User, parent_id: ParentId, item: Item) -> _Response:
|
|
129
|
+
try:
|
|
130
|
+
service = self.infra.with_user(user).with_parent(parent_id).build()
|
|
131
|
+
except DoesNotExistError as e:
|
|
132
|
+
return JSONResponse(RestfulResponse(self.parent).not_found(e), 404)
|
|
133
|
+
try:
|
|
134
|
+
service.replace_one(item)
|
|
135
|
+
except DoesNotExistError as e:
|
|
136
|
+
return JSONResponse(self.response.not_found(e), 404)
|
|
137
|
+
except ForbiddenError as e:
|
|
138
|
+
return JSONResponse(self.response.forbidden(e), 403)
|
|
139
|
+
|
|
140
|
+
return self.response.ok()
|
|
141
|
+
|
|
142
|
+
return endpoint
|
|
143
|
+
|
|
144
|
+
def replace_many(self, User, ParentId, Collection) -> Callable[..., _Response]: # type: ignore
|
|
145
|
+
def endpoint(user: User, parent_id: ParentId, items: Collection) -> _Response:
|
|
146
|
+
try:
|
|
147
|
+
service = self.infra.with_user(user).with_parent(parent_id).build()
|
|
148
|
+
except DoesNotExistError as e:
|
|
149
|
+
return JSONResponse(RestfulResponse(self.parent).not_found(e), 404)
|
|
150
|
+
try:
|
|
151
|
+
service.replace_many(items)
|
|
152
|
+
except DoesNotExistError as e:
|
|
153
|
+
return JSONResponse(self.response.not_found(e), 404)
|
|
154
|
+
except ForbiddenError as e:
|
|
155
|
+
return JSONResponse(self.response.forbidden(e), 403)
|
|
156
|
+
|
|
157
|
+
return self.response.ok()
|
|
158
|
+
|
|
159
|
+
return endpoint
|
|
160
|
+
|
|
161
|
+
def delete_one(self, User, ParentId, ItemId) -> Callable[..., _Response]: # type: ignore
|
|
162
|
+
def endpoint(user: User, parent_id: ParentId, item_id: ItemId) -> _Response:
|
|
163
|
+
try:
|
|
164
|
+
service = self.infra.with_user(user).with_parent(parent_id).build()
|
|
165
|
+
except DoesNotExistError as e:
|
|
166
|
+
return JSONResponse(RestfulResponse(self.parent).not_found(e), 404)
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
service.delete_one(item_id)
|
|
170
|
+
except DoesNotExistError as e:
|
|
171
|
+
return JSONResponse(self.response.not_found(e), 404)
|
|
172
|
+
except ForbiddenError as e:
|
|
173
|
+
return JSONResponse(self.response.forbidden(e), 403)
|
|
174
|
+
|
|
175
|
+
return self.response.ok()
|
|
176
|
+
|
|
177
|
+
return endpoint
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Iterable
|
|
5
|
+
|
|
6
|
+
from fastapi import status
|
|
7
|
+
from fastapi.responses import JSONResponse
|
|
8
|
+
|
|
9
|
+
from apexdevkit.error import DoesNotExistError, ExistsError, ForbiddenError
|
|
10
|
+
from apexdevkit.testing import RestfulName
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SuccessResponse(dict[str, Any]):
|
|
14
|
+
def __init__(self, status_code: int, **kwargs: Any):
|
|
15
|
+
super().__init__(
|
|
16
|
+
{
|
|
17
|
+
"status": "success",
|
|
18
|
+
"code": status_code,
|
|
19
|
+
"data": {**kwargs},
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ResourceFound(SuccessResponse):
|
|
25
|
+
def __init__(self, **kwargs: Any):
|
|
26
|
+
super().__init__(status_code=status.HTTP_200_OK, **kwargs)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ResourceCreated(SuccessResponse):
|
|
30
|
+
def __init__(self, **kwargs: Any):
|
|
31
|
+
super().__init__(status_code=status.HTTP_201_CREATED, **kwargs)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ErrorResponse(JSONResponse):
|
|
35
|
+
def __init__(self, status_code: int, message: str, **kwargs: Any):
|
|
36
|
+
content = {
|
|
37
|
+
"status": "fail",
|
|
38
|
+
"code": status_code,
|
|
39
|
+
"error": {"message": message},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if kwargs:
|
|
43
|
+
content["data"] = {**kwargs}
|
|
44
|
+
|
|
45
|
+
super().__init__(status_code=status_code, content=content)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class BadRequest(ErrorResponse):
|
|
49
|
+
def __init__(self, message: str, **kwargs: Any) -> None:
|
|
50
|
+
super().__init__(
|
|
51
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
52
|
+
message=message,
|
|
53
|
+
**kwargs,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ResourceNotFound(ErrorResponse):
|
|
58
|
+
def __init__(self, message: str):
|
|
59
|
+
super().__init__(status_code=status.HTTP_404_NOT_FOUND, message=message)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ResourceExists(ErrorResponse):
|
|
63
|
+
def __init__(self, message: str, **kwargs: Any):
|
|
64
|
+
super().__init__(
|
|
65
|
+
status_code=status.HTTP_409_CONFLICT,
|
|
66
|
+
message=message,
|
|
67
|
+
**kwargs,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class RestfulResponse:
|
|
73
|
+
name: RestfulName
|
|
74
|
+
|
|
75
|
+
def _response(self, code: int, data: Any, error: str = "") -> dict[str, Any]:
|
|
76
|
+
content: dict[str, Any] = {"code": code, "status": "success"}
|
|
77
|
+
|
|
78
|
+
if error:
|
|
79
|
+
content["status"] = "fail"
|
|
80
|
+
content["error"] = {"message": error}
|
|
81
|
+
|
|
82
|
+
match data:
|
|
83
|
+
case None:
|
|
84
|
+
content["data"] = {}
|
|
85
|
+
case list():
|
|
86
|
+
content["data"] = {self.name.plural: data, "count": len(data)}
|
|
87
|
+
case _:
|
|
88
|
+
content["data"] = {self.name.singular: data}
|
|
89
|
+
|
|
90
|
+
return content
|
|
91
|
+
|
|
92
|
+
def ok(self) -> dict[str, Any]:
|
|
93
|
+
return self._response(200, data=None)
|
|
94
|
+
|
|
95
|
+
def not_found(self, e: DoesNotExistError) -> dict[str, Any]:
|
|
96
|
+
name = self.name.singular.capitalize()
|
|
97
|
+
|
|
98
|
+
return self._response(
|
|
99
|
+
404,
|
|
100
|
+
data={"id": str(e.id)},
|
|
101
|
+
error=f"An item<{name}> with id<{e.id}> does not exist.",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def exists(self, e: ExistsError) -> dict[str, Any]:
|
|
105
|
+
name = self.name.singular.capitalize()
|
|
106
|
+
|
|
107
|
+
return self._response(
|
|
108
|
+
409,
|
|
109
|
+
data={"id": str(e.id)},
|
|
110
|
+
error=f"An item<{name}> with the {e} already exists.",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def forbidden(self, e: ForbiddenError) -> dict[str, Any]:
|
|
114
|
+
return self._response(
|
|
115
|
+
403,
|
|
116
|
+
data={"id": str(e.id)},
|
|
117
|
+
error=e.message,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def created_one(self, item: Any) -> dict[str, Any]:
|
|
121
|
+
return self._response(201, item)
|
|
122
|
+
|
|
123
|
+
def created_many(self, items: Iterable[Any]) -> dict[str, Any]:
|
|
124
|
+
return self._response(201, list(items))
|
|
125
|
+
|
|
126
|
+
def found_one(self, item: Any) -> dict[str, Any]:
|
|
127
|
+
return self._response(200, item)
|
|
128
|
+
|
|
129
|
+
def found_many(self, items: list[Any]) -> dict[str, Any]:
|
|
130
|
+
return self._response(200, items)
|
|
@@ -1,104 +1,21 @@
|
|
|
1
|
-
from abc import ABC, abstractmethod
|
|
2
1
|
from dataclasses import dataclass, field
|
|
3
2
|
from functools import cached_property
|
|
4
|
-
from typing import Annotated, Any, Callable,
|
|
3
|
+
from typing import Annotated, Any, Callable, Self, TypeVar
|
|
5
4
|
|
|
6
5
|
from fastapi import APIRouter, Depends, Path
|
|
7
6
|
from fastapi.responses import JSONResponse
|
|
8
7
|
|
|
9
|
-
from apexdevkit.
|
|
8
|
+
from apexdevkit.fastapi.builder import RestfulServiceBuilder
|
|
9
|
+
from apexdevkit.fastapi.resource import RestfulResource
|
|
10
10
|
from apexdevkit.fastapi.schema import RestfulSchema, SchemaFields
|
|
11
11
|
from apexdevkit.fastapi.service import RawCollection, RawItem, RestfulService
|
|
12
12
|
from apexdevkit.testing import RestfulName
|
|
13
13
|
|
|
14
14
|
_Response = JSONResponse | dict[str, Any]
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
@dataclass
|
|
18
|
-
class RestfulResponse:
|
|
19
|
-
name: RestfulName
|
|
20
|
-
|
|
21
|
-
def _response(self, code: int, data: Any, error: str = "") -> dict[str, Any]:
|
|
22
|
-
content: dict[str, Any] = {"code": code, "status": "success"}
|
|
23
|
-
|
|
24
|
-
if error:
|
|
25
|
-
content["status"] = "fail"
|
|
26
|
-
content["error"] = {"message": error}
|
|
27
|
-
|
|
28
|
-
match data:
|
|
29
|
-
case None:
|
|
30
|
-
content["data"] = {}
|
|
31
|
-
case list():
|
|
32
|
-
content["data"] = {self.name.plural: data, "count": len(data)}
|
|
33
|
-
case _:
|
|
34
|
-
content["data"] = {self.name.singular: data}
|
|
35
|
-
|
|
36
|
-
return content
|
|
37
|
-
|
|
38
|
-
def ok(self) -> dict[str, Any]:
|
|
39
|
-
return self._response(200, data=None)
|
|
40
|
-
|
|
41
|
-
def not_found(self, e: DoesNotExistError) -> dict[str, Any]:
|
|
42
|
-
name = self.name.singular.capitalize()
|
|
43
|
-
|
|
44
|
-
return self._response(
|
|
45
|
-
404,
|
|
46
|
-
data={"id": str(e.id)},
|
|
47
|
-
error=f"An item<{name}> with id<{e.id}> does not exist.",
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
def exists(self, e: ExistsError) -> dict[str, Any]:
|
|
51
|
-
name = self.name.singular.capitalize()
|
|
52
|
-
|
|
53
|
-
return self._response(
|
|
54
|
-
409,
|
|
55
|
-
data={"id": str(e.id)},
|
|
56
|
-
error=f"An item<{name}> with the {e} already exists.",
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
def forbidden(self, e: ForbiddenError) -> dict[str, Any]:
|
|
60
|
-
return self._response(
|
|
61
|
-
403,
|
|
62
|
-
data={"id": str(e.id)},
|
|
63
|
-
error=e.message,
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
def created_one(self, item: Any) -> dict[str, Any]:
|
|
67
|
-
return self._response(201, item)
|
|
68
|
-
|
|
69
|
-
def created_many(self, items: Iterable[Any]) -> dict[str, Any]:
|
|
70
|
-
return self._response(201, list(items))
|
|
71
|
-
|
|
72
|
-
def found_one(self, item: Any) -> dict[str, Any]:
|
|
73
|
-
return self._response(200, item)
|
|
74
|
-
|
|
75
|
-
def found_many(self, items: list[Any]) -> dict[str, Any]:
|
|
76
|
-
return self._response(200, items)
|
|
77
|
-
|
|
78
|
-
|
|
79
16
|
T = TypeVar("T")
|
|
80
17
|
|
|
81
18
|
|
|
82
|
-
@dataclass
|
|
83
|
-
class RestfulServiceBuilder(ABC):
|
|
84
|
-
parent_id: str = field(init=False)
|
|
85
|
-
user: Any = field(init=False)
|
|
86
|
-
|
|
87
|
-
def with_user(self, user: Any) -> "RestfulServiceBuilder":
|
|
88
|
-
self.user = user
|
|
89
|
-
|
|
90
|
-
return self
|
|
91
|
-
|
|
92
|
-
def with_parent(self, identity: str) -> "RestfulServiceBuilder":
|
|
93
|
-
self.parent_id = identity
|
|
94
|
-
|
|
95
|
-
return self
|
|
96
|
-
|
|
97
|
-
@abstractmethod
|
|
98
|
-
def build(self) -> RestfulService: # pragma: no cover
|
|
99
|
-
pass
|
|
100
|
-
|
|
101
|
-
|
|
102
19
|
@dataclass
|
|
103
20
|
class PreBuiltRestfulService(RestfulServiceBuilder): # pragma: no cover
|
|
104
21
|
service: RestfulService
|
|
@@ -119,22 +36,23 @@ class RestfulRouter:
|
|
|
119
36
|
|
|
120
37
|
name: RestfulName = field(init=False)
|
|
121
38
|
fields: SchemaFields = field(init=False)
|
|
122
|
-
infra: RestfulServiceBuilder = field(init=False)
|
|
123
39
|
|
|
124
40
|
parent: str = field(init=False, default="")
|
|
125
41
|
|
|
42
|
+
infra: RestfulServiceBuilder = field(init=False)
|
|
43
|
+
|
|
126
44
|
def __post_init__(self) -> None: # pragma: no cover
|
|
127
45
|
if self.service:
|
|
128
|
-
self.
|
|
129
|
-
|
|
130
|
-
@cached_property
|
|
131
|
-
def response(self) -> RestfulResponse:
|
|
132
|
-
return RestfulResponse(name=self.name)
|
|
46
|
+
self.with_infra(PreBuiltRestfulService(self.service))
|
|
133
47
|
|
|
134
48
|
@cached_property
|
|
135
49
|
def schema(self) -> RestfulSchema:
|
|
136
50
|
return RestfulSchema(name=self.name, fields=self.fields)
|
|
137
51
|
|
|
52
|
+
@property
|
|
53
|
+
def resource(self) -> RestfulResource:
|
|
54
|
+
return RestfulResource(self.name, self.infra, RestfulName(self.parent))
|
|
55
|
+
|
|
138
56
|
@property
|
|
139
57
|
def id_alias(self) -> str:
|
|
140
58
|
return self.name.singular + "_id"
|
|
@@ -174,7 +92,7 @@ class RestfulRouter:
|
|
|
174
92
|
) -> Self:
|
|
175
93
|
self.router.add_api_route(
|
|
176
94
|
"",
|
|
177
|
-
self.create_one(
|
|
95
|
+
self.resource.create_one(
|
|
178
96
|
User=Annotated[
|
|
179
97
|
Any,
|
|
180
98
|
Depends(extract_user),
|
|
@@ -198,26 +116,6 @@ class RestfulRouter:
|
|
|
198
116
|
|
|
199
117
|
return self
|
|
200
118
|
|
|
201
|
-
def create_one(self, User, ParentId, Item) -> Callable[..., _Response]: # type: ignore
|
|
202
|
-
def endpoint(user: User, parent_id: ParentId, item: Item) -> _Response:
|
|
203
|
-
try:
|
|
204
|
-
service = self.infra.with_user(user).with_parent(parent_id).build()
|
|
205
|
-
except DoesNotExistError as e:
|
|
206
|
-
return JSONResponse(
|
|
207
|
-
RestfulResponse(RestfulName(self.parent)).not_found(e), 404
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
try:
|
|
211
|
-
item = service.create_one(item)
|
|
212
|
-
except ExistsError as e:
|
|
213
|
-
return JSONResponse(self.response.exists(e), 409)
|
|
214
|
-
except ForbiddenError as e:
|
|
215
|
-
return JSONResponse(self.response.forbidden(e), 403)
|
|
216
|
-
|
|
217
|
-
return self.response.created_one(item)
|
|
218
|
-
|
|
219
|
-
return endpoint
|
|
220
|
-
|
|
221
119
|
def with_create_many_endpoint(
|
|
222
120
|
self,
|
|
223
121
|
is_documented: bool = True,
|
|
@@ -225,7 +123,7 @@ class RestfulRouter:
|
|
|
225
123
|
) -> Self:
|
|
226
124
|
self.router.add_api_route(
|
|
227
125
|
"/batch",
|
|
228
|
-
self.create_many(
|
|
126
|
+
self.resource.create_many(
|
|
229
127
|
User=Annotated[
|
|
230
128
|
Any,
|
|
231
129
|
Depends(extract_user),
|
|
@@ -249,24 +147,6 @@ class RestfulRouter:
|
|
|
249
147
|
|
|
250
148
|
return self
|
|
251
149
|
|
|
252
|
-
def create_many(self, User, ParentId, Collection) -> Callable[..., _Response]: # type: ignore
|
|
253
|
-
def endpoint(user: User, parent_id: ParentId, items: Collection) -> _Response:
|
|
254
|
-
try:
|
|
255
|
-
service = self.infra.with_user(user).with_parent(parent_id).build()
|
|
256
|
-
except DoesNotExistError as e:
|
|
257
|
-
return JSONResponse(
|
|
258
|
-
RestfulResponse(RestfulName(self.parent)).not_found(e), 404
|
|
259
|
-
)
|
|
260
|
-
|
|
261
|
-
try:
|
|
262
|
-
return self.response.created_many(service.create_many(items))
|
|
263
|
-
except ExistsError as e:
|
|
264
|
-
return JSONResponse(self.response.exists(e), 409)
|
|
265
|
-
except ForbiddenError as e:
|
|
266
|
-
return JSONResponse(self.response.forbidden(e), 403)
|
|
267
|
-
|
|
268
|
-
return endpoint
|
|
269
|
-
|
|
270
150
|
def with_read_one_endpoint(
|
|
271
151
|
self,
|
|
272
152
|
is_documented: bool = True,
|
|
@@ -274,7 +154,7 @@ class RestfulRouter:
|
|
|
274
154
|
) -> Self:
|
|
275
155
|
self.router.add_api_route(
|
|
276
156
|
self.item_path,
|
|
277
|
-
self.read_one(
|
|
157
|
+
self.resource.read_one(
|
|
278
158
|
User=Annotated[
|
|
279
159
|
Any,
|
|
280
160
|
Depends(extract_user),
|
|
@@ -298,24 +178,6 @@ class RestfulRouter:
|
|
|
298
178
|
|
|
299
179
|
return self
|
|
300
180
|
|
|
301
|
-
def read_one(self, User, ParentId, ItemId) -> Callable[..., _Response]: # type: ignore
|
|
302
|
-
def endpoint(user: User, parent_id: ParentId, item_id: ItemId) -> _Response:
|
|
303
|
-
try:
|
|
304
|
-
service = self.infra.with_user(user).with_parent(parent_id).build()
|
|
305
|
-
except DoesNotExistError as e:
|
|
306
|
-
return JSONResponse(
|
|
307
|
-
RestfulResponse(RestfulName(self.parent)).not_found(e), 404
|
|
308
|
-
)
|
|
309
|
-
|
|
310
|
-
try:
|
|
311
|
-
return self.response.found_one(service.read_one(item_id))
|
|
312
|
-
except DoesNotExistError as e:
|
|
313
|
-
return JSONResponse(self.response.not_found(e), 404)
|
|
314
|
-
except ForbiddenError as e:
|
|
315
|
-
return JSONResponse(self.response.forbidden(e), 403)
|
|
316
|
-
|
|
317
|
-
return endpoint
|
|
318
|
-
|
|
319
181
|
def with_read_all_endpoint(
|
|
320
182
|
self,
|
|
321
183
|
is_documented: bool = True,
|
|
@@ -323,7 +185,7 @@ class RestfulRouter:
|
|
|
323
185
|
) -> Self:
|
|
324
186
|
self.router.add_api_route(
|
|
325
187
|
"",
|
|
326
|
-
self.read_all(
|
|
188
|
+
self.resource.read_all(
|
|
327
189
|
User=Annotated[
|
|
328
190
|
Any,
|
|
329
191
|
Depends(extract_user),
|
|
@@ -343,21 +205,6 @@ class RestfulRouter:
|
|
|
343
205
|
|
|
344
206
|
return self
|
|
345
207
|
|
|
346
|
-
def read_all(self, User, ParentId) -> Callable[..., _Response]: # type: ignore
|
|
347
|
-
def endpoint(user: User, parent_id: ParentId) -> _Response:
|
|
348
|
-
try:
|
|
349
|
-
service = self.infra.with_user(user).with_parent(parent_id).build()
|
|
350
|
-
except DoesNotExistError as e:
|
|
351
|
-
return JSONResponse(
|
|
352
|
-
RestfulResponse(RestfulName(self.parent)).not_found(e), 404
|
|
353
|
-
)
|
|
354
|
-
try:
|
|
355
|
-
return self.response.found_many(list(service.read_all()))
|
|
356
|
-
except ForbiddenError as e:
|
|
357
|
-
return JSONResponse(self.response.forbidden(e), 403)
|
|
358
|
-
|
|
359
|
-
return endpoint
|
|
360
|
-
|
|
361
208
|
def with_update_one_endpoint(
|
|
362
209
|
self,
|
|
363
210
|
is_documented: bool = True,
|
|
@@ -365,7 +212,7 @@ class RestfulRouter:
|
|
|
365
212
|
) -> Self:
|
|
366
213
|
self.router.add_api_route(
|
|
367
214
|
self.item_path,
|
|
368
|
-
self.update_one(
|
|
215
|
+
self.resource.update_one(
|
|
369
216
|
User=Annotated[
|
|
370
217
|
Any,
|
|
371
218
|
Depends(extract_user),
|
|
@@ -393,30 +240,6 @@ class RestfulRouter:
|
|
|
393
240
|
|
|
394
241
|
return self
|
|
395
242
|
|
|
396
|
-
def update_one(self, User, ParentId, ItemId, Updates) -> Callable[..., _Response]: # type: ignore
|
|
397
|
-
def endpoint(
|
|
398
|
-
user: User,
|
|
399
|
-
parent_id: ParentId,
|
|
400
|
-
item_id: ItemId,
|
|
401
|
-
updates: Updates,
|
|
402
|
-
) -> _Response:
|
|
403
|
-
try:
|
|
404
|
-
service = self.infra.with_user(user).with_parent(parent_id).build()
|
|
405
|
-
except DoesNotExistError as e:
|
|
406
|
-
return JSONResponse(
|
|
407
|
-
RestfulResponse(RestfulName(self.parent)).not_found(e), 404
|
|
408
|
-
)
|
|
409
|
-
try:
|
|
410
|
-
service.update_one(item_id, **updates)
|
|
411
|
-
except DoesNotExistError as e:
|
|
412
|
-
return JSONResponse(self.response.not_found(e), 404)
|
|
413
|
-
except ForbiddenError as e:
|
|
414
|
-
return JSONResponse(self.response.forbidden(e), 403)
|
|
415
|
-
|
|
416
|
-
return self.response.ok()
|
|
417
|
-
|
|
418
|
-
return endpoint
|
|
419
|
-
|
|
420
243
|
def with_update_many_endpoint(
|
|
421
244
|
self,
|
|
422
245
|
is_documented: bool = True,
|
|
@@ -424,7 +247,7 @@ class RestfulRouter:
|
|
|
424
247
|
) -> Self:
|
|
425
248
|
self.router.add_api_route(
|
|
426
249
|
"",
|
|
427
|
-
self.update_many(
|
|
250
|
+
self.resource.update_many(
|
|
428
251
|
User=Annotated[
|
|
429
252
|
Any,
|
|
430
253
|
Depends(extract_user),
|
|
@@ -448,25 +271,6 @@ class RestfulRouter:
|
|
|
448
271
|
|
|
449
272
|
return self
|
|
450
273
|
|
|
451
|
-
def update_many(self, User, ParentId, Collection) -> Callable[..., _Response]: # type: ignore
|
|
452
|
-
def endpoint(user: User, parent_id: ParentId, items: Collection) -> _Response:
|
|
453
|
-
try:
|
|
454
|
-
service = self.infra.with_user(user).with_parent(parent_id).build()
|
|
455
|
-
except DoesNotExistError as e:
|
|
456
|
-
return JSONResponse(
|
|
457
|
-
RestfulResponse(RestfulName(self.parent)).not_found(e), 404
|
|
458
|
-
)
|
|
459
|
-
try:
|
|
460
|
-
service.update_many(items)
|
|
461
|
-
except DoesNotExistError as e:
|
|
462
|
-
return JSONResponse(self.response.not_found(e), 404)
|
|
463
|
-
except ForbiddenError as e:
|
|
464
|
-
return JSONResponse(self.response.forbidden(e), 403)
|
|
465
|
-
|
|
466
|
-
return self.response.ok()
|
|
467
|
-
|
|
468
|
-
return endpoint
|
|
469
|
-
|
|
470
274
|
def with_replace_one_endpoint(
|
|
471
275
|
self,
|
|
472
276
|
is_documented: bool = True,
|
|
@@ -474,7 +278,7 @@ class RestfulRouter:
|
|
|
474
278
|
) -> Self:
|
|
475
279
|
self.router.add_api_route(
|
|
476
280
|
"",
|
|
477
|
-
self.replace_one(
|
|
281
|
+
self.resource.replace_one(
|
|
478
282
|
User=Annotated[
|
|
479
283
|
Any,
|
|
480
284
|
Depends(extract_user),
|
|
@@ -498,25 +302,6 @@ class RestfulRouter:
|
|
|
498
302
|
|
|
499
303
|
return self
|
|
500
304
|
|
|
501
|
-
def replace_one(self, User, ParentId, Item) -> Callable[..., _Response]: # type: ignore
|
|
502
|
-
def endpoint(user: User, parent_id: ParentId, item: Item) -> _Response:
|
|
503
|
-
try:
|
|
504
|
-
service = self.infra.with_user(user).with_parent(parent_id).build()
|
|
505
|
-
except DoesNotExistError as e:
|
|
506
|
-
return JSONResponse(
|
|
507
|
-
RestfulResponse(RestfulName(self.parent)).not_found(e), 404
|
|
508
|
-
)
|
|
509
|
-
try:
|
|
510
|
-
service.replace_one(item)
|
|
511
|
-
except DoesNotExistError as e:
|
|
512
|
-
return JSONResponse(self.response.not_found(e), 404)
|
|
513
|
-
except ForbiddenError as e:
|
|
514
|
-
return JSONResponse(self.response.forbidden(e), 403)
|
|
515
|
-
|
|
516
|
-
return self.response.ok()
|
|
517
|
-
|
|
518
|
-
return endpoint
|
|
519
|
-
|
|
520
305
|
def with_replace_many_endpoint(
|
|
521
306
|
self,
|
|
522
307
|
is_documented: bool = True,
|
|
@@ -524,7 +309,7 @@ class RestfulRouter:
|
|
|
524
309
|
) -> Self:
|
|
525
310
|
self.router.add_api_route(
|
|
526
311
|
"/batch",
|
|
527
|
-
self.replace_many(
|
|
312
|
+
self.resource.replace_many(
|
|
528
313
|
User=Annotated[
|
|
529
314
|
Any,
|
|
530
315
|
Depends(extract_user),
|
|
@@ -548,25 +333,6 @@ class RestfulRouter:
|
|
|
548
333
|
|
|
549
334
|
return self
|
|
550
335
|
|
|
551
|
-
def replace_many(self, User, ParentId, Collection) -> Callable[..., _Response]: # type: ignore
|
|
552
|
-
def endpoint(user: User, parent_id: ParentId, items: Collection) -> _Response:
|
|
553
|
-
try:
|
|
554
|
-
service = self.infra.with_user(user).with_parent(parent_id).build()
|
|
555
|
-
except DoesNotExistError as e:
|
|
556
|
-
return JSONResponse(
|
|
557
|
-
RestfulResponse(RestfulName(self.parent)).not_found(e), 404
|
|
558
|
-
)
|
|
559
|
-
try:
|
|
560
|
-
service.replace_many(items)
|
|
561
|
-
except DoesNotExistError as e:
|
|
562
|
-
return JSONResponse(self.response.not_found(e), 404)
|
|
563
|
-
except ForbiddenError as e:
|
|
564
|
-
return JSONResponse(self.response.forbidden(e), 403)
|
|
565
|
-
|
|
566
|
-
return self.response.ok()
|
|
567
|
-
|
|
568
|
-
return endpoint
|
|
569
|
-
|
|
570
336
|
def with_delete_one_endpoint(
|
|
571
337
|
self,
|
|
572
338
|
is_documented: bool = True,
|
|
@@ -574,7 +340,7 @@ class RestfulRouter:
|
|
|
574
340
|
) -> Self:
|
|
575
341
|
self.router.add_api_route(
|
|
576
342
|
self.item_path,
|
|
577
|
-
self.delete_one(
|
|
343
|
+
self.resource.delete_one(
|
|
578
344
|
User=Annotated[
|
|
579
345
|
Any,
|
|
580
346
|
Depends(extract_user),
|
|
@@ -598,26 +364,6 @@ class RestfulRouter:
|
|
|
598
364
|
|
|
599
365
|
return self
|
|
600
366
|
|
|
601
|
-
def delete_one(self, User, ParentId, ItemId) -> Callable[..., _Response]: # type: ignore
|
|
602
|
-
def endpoint(user: User, parent_id: ParentId, item_id: ItemId) -> _Response:
|
|
603
|
-
try:
|
|
604
|
-
service = self.infra.with_user(user).with_parent(parent_id).build()
|
|
605
|
-
except DoesNotExistError as e:
|
|
606
|
-
return JSONResponse(
|
|
607
|
-
RestfulResponse(RestfulName(self.parent)).not_found(e), 404
|
|
608
|
-
)
|
|
609
|
-
|
|
610
|
-
try:
|
|
611
|
-
service.delete_one(item_id)
|
|
612
|
-
except DoesNotExistError as e:
|
|
613
|
-
return JSONResponse(self.response.not_found(e), 404)
|
|
614
|
-
except ForbiddenError as e:
|
|
615
|
-
return JSONResponse(self.response.forbidden(e), 403)
|
|
616
|
-
|
|
617
|
-
return self.response.ok()
|
|
618
|
-
|
|
619
|
-
return endpoint
|
|
620
|
-
|
|
621
367
|
def with_sub_resource(self, **names: APIRouter) -> Self:
|
|
622
368
|
for name, router in names.items():
|
|
623
369
|
self.router.include_router(router, prefix=f"{self.item_path}/{name}")
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
|
-
from fastapi import status
|
|
6
|
-
from fastapi.responses import JSONResponse
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class SuccessResponse(dict[str, Any]):
|
|
10
|
-
def __init__(self, status_code: int, **kwargs: Any):
|
|
11
|
-
super().__init__(
|
|
12
|
-
{
|
|
13
|
-
"status": "success",
|
|
14
|
-
"code": status_code,
|
|
15
|
-
"data": {**kwargs},
|
|
16
|
-
}
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class ResourceFound(SuccessResponse):
|
|
21
|
-
def __init__(self, **kwargs: Any):
|
|
22
|
-
super().__init__(status_code=status.HTTP_200_OK, **kwargs)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class ResourceCreated(SuccessResponse):
|
|
26
|
-
def __init__(self, **kwargs: Any):
|
|
27
|
-
super().__init__(status_code=status.HTTP_201_CREATED, **kwargs)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class ErrorResponse(JSONResponse):
|
|
31
|
-
def __init__(self, status_code: int, message: str, **kwargs: Any):
|
|
32
|
-
content = {
|
|
33
|
-
"status": "fail",
|
|
34
|
-
"code": status_code,
|
|
35
|
-
"error": {"message": message},
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if kwargs:
|
|
39
|
-
content["data"] = {**kwargs}
|
|
40
|
-
|
|
41
|
-
super().__init__(status_code=status_code, content=content)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
class BadRequest(ErrorResponse):
|
|
45
|
-
def __init__(self, message: str, **kwargs: Any) -> None:
|
|
46
|
-
super().__init__(
|
|
47
|
-
status_code=status.HTTP_400_BAD_REQUEST,
|
|
48
|
-
message=message,
|
|
49
|
-
**kwargs,
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
class ResourceNotFound(ErrorResponse):
|
|
54
|
-
def __init__(self, message: str):
|
|
55
|
-
super().__init__(status_code=status.HTTP_404_NOT_FOUND, message=message)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
class ResourceExists(ErrorResponse):
|
|
59
|
-
def __init__(self, message: str, **kwargs: Any):
|
|
60
|
-
super().__init__(
|
|
61
|
-
status_code=status.HTTP_409_CONFLICT,
|
|
62
|
-
message=message,
|
|
63
|
-
**kwargs,
|
|
64
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|