apexdevkit 2.1.4__tar.gz → 2.3.1__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-2.1.4 → apexdevkit-2.3.1}/PKG-INFO +1 -1
- apexdevkit-2.3.1/apexdevkit/fastapi/__init__.py +15 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/fastapi/builder.py +4 -39
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/fastapi/dependable.py +6 -14
- apexdevkit-2.3.1/apexdevkit/fastapi/endpoints.py +103 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/fastapi/router.py +35 -30
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/fastapi/schema.py +0 -3
- apexdevkit-2.3.1/apexdevkit/fastapi/service.py +137 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/formatter.py +8 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/testing.py +3 -2
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/pyproject.toml +3 -2
- apexdevkit-2.1.4/apexdevkit/fastapi/__init__.py +0 -13
- apexdevkit-2.1.4/apexdevkit/fastapi/docs.py +0 -15
- apexdevkit-2.1.4/apexdevkit/fastapi/resource.py +0 -319
- apexdevkit-2.1.4/apexdevkit/fastapi/service.py +0 -120
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/LICENSE +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/README.md +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/__init__.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/annotation/__init__.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/annotation/deprecate.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/date.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/error.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/fastapi/name.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/fastapi/response.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/key_fn.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/py.typed +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/query/__init__.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/query/generator.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/query/query.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/__init__.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/core/__init__.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/core/database.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/core/decorator.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/core/interface.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/core/mixin.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/in_memory/__init__.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/in_memory/mixin.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/in_memory/repository.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/in_memory/store.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/sql/__init__.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/sql/connector.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/sql/field.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/sql/mssql/__init__.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/sql/mssql/repository.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/sql/mssql/table.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/sql/sqlite/__init__.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/sql/sqlite/repository.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/sql/sqlite/table.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/server.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/sync/__init__.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/sync/observer.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/sync/source.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/sync/sync.py +0 -0
- {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/sync/target.py +0 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from apexdevkit.fastapi.builder import FastApiBuilder
|
|
2
|
+
from apexdevkit.fastapi.router import RestfulRouter
|
|
3
|
+
from apexdevkit.fastapi.service import (
|
|
4
|
+
RestfulRepository,
|
|
5
|
+
RestfulResource,
|
|
6
|
+
RestfulService,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"FastApiBuilder",
|
|
11
|
+
"RestfulRepository",
|
|
12
|
+
"RestfulResource",
|
|
13
|
+
"RestfulRouter",
|
|
14
|
+
"RestfulService",
|
|
15
|
+
]
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from abc import ABC, abstractmethod
|
|
4
3
|
from dataclasses import dataclass, field
|
|
5
4
|
from typing import TYPE_CHECKING, Any, Protocol, Self
|
|
6
5
|
|
|
@@ -10,13 +9,10 @@ from fastapi.responses import JSONResponse
|
|
|
10
9
|
|
|
11
10
|
from apexdevkit.annotation import deprecated
|
|
12
11
|
from apexdevkit.error import ApiError
|
|
13
|
-
from apexdevkit.fastapi.dependable import DependableBuilder
|
|
14
12
|
|
|
15
13
|
if TYPE_CHECKING:
|
|
16
14
|
from collections.abc import Mapping, MutableSet
|
|
17
15
|
|
|
18
|
-
from apexdevkit.fastapi.service import RestfulService
|
|
19
|
-
|
|
20
16
|
|
|
21
17
|
@dataclass
|
|
22
18
|
class FastApiBuilder:
|
|
@@ -57,7 +53,7 @@ class FastApiBuilder:
|
|
|
57
53
|
for key, value in values.items():
|
|
58
54
|
self.app.include_router(
|
|
59
55
|
value,
|
|
60
|
-
prefix=_Endpoint(key).
|
|
56
|
+
prefix=_Endpoint(key).normalized(),
|
|
61
57
|
tags=value.tags or [key.title()],
|
|
62
58
|
)
|
|
63
59
|
|
|
@@ -65,7 +61,7 @@ class FastApiBuilder:
|
|
|
65
61
|
|
|
66
62
|
def with_mounted(self, **apps: FastAPI) -> Self:
|
|
67
63
|
for path, app in apps.items():
|
|
68
|
-
self.app.mount(_Endpoint(path).
|
|
64
|
+
self.app.mount(_Endpoint(path).normalized(), app)
|
|
69
65
|
|
|
70
66
|
return self
|
|
71
67
|
|
|
@@ -125,43 +121,12 @@ class CORSApplicator:
|
|
|
125
121
|
)
|
|
126
122
|
|
|
127
123
|
|
|
128
|
-
@dataclass
|
|
129
|
-
class RestfulServiceBuilder(ABC):
|
|
130
|
-
parent_id: str = field(init=False)
|
|
131
|
-
user: Any = field(init=False)
|
|
132
|
-
|
|
133
|
-
def as_dependable(self) -> DependableBuilder:
|
|
134
|
-
return DependableBuilder.from_builder(self)
|
|
135
|
-
|
|
136
|
-
def with_user(self, user: Any) -> RestfulServiceBuilder:
|
|
137
|
-
self.user = user
|
|
138
|
-
|
|
139
|
-
return self
|
|
140
|
-
|
|
141
|
-
def with_parent(self, identity: str) -> RestfulServiceBuilder:
|
|
142
|
-
self.parent_id = identity
|
|
143
|
-
|
|
144
|
-
return self
|
|
145
|
-
|
|
146
|
-
@abstractmethod
|
|
147
|
-
def build(self) -> RestfulService: # pragma: no cover
|
|
148
|
-
pass
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
@dataclass
|
|
152
|
-
class PreBuilt(RestfulServiceBuilder): # pragma: no cover
|
|
153
|
-
service: RestfulService
|
|
154
|
-
|
|
155
|
-
def build(self) -> RestfulService:
|
|
156
|
-
return self.service
|
|
157
|
-
|
|
158
|
-
|
|
159
124
|
@dataclass(frozen=True)
|
|
160
125
|
class _Endpoint:
|
|
161
126
|
raw: str
|
|
162
127
|
|
|
163
|
-
def
|
|
128
|
+
def normalized(self) -> str:
|
|
164
129
|
return f"/{self.raw.replace('_', '-')}"
|
|
165
130
|
|
|
166
131
|
def __str__(self) -> str: # pragma: no cover
|
|
167
|
-
return self.
|
|
132
|
+
return self.normalized()
|
|
@@ -3,19 +3,11 @@ from dataclasses import dataclass
|
|
|
3
3
|
from typing import Annotated, Any, Protocol
|
|
4
4
|
|
|
5
5
|
from fastapi import Depends, Path
|
|
6
|
-
from fastapi.requests import Request
|
|
7
6
|
|
|
8
7
|
from apexdevkit.error import ApiError, DoesNotExistError, ForbiddenError
|
|
9
8
|
from apexdevkit.fastapi.name import RestfulName
|
|
10
9
|
from apexdevkit.fastapi.response import RestfulResponse
|
|
11
|
-
from apexdevkit.fastapi.service import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def inject(dependency: str) -> Any: # pragma: no cover
|
|
15
|
-
def get(request: Request) -> Any:
|
|
16
|
-
return getattr(request.app.state, dependency)
|
|
17
|
-
|
|
18
|
-
return Depends(get)
|
|
10
|
+
from apexdevkit.fastapi.service import RestfulResource
|
|
19
11
|
|
|
20
12
|
|
|
21
13
|
class _ServiceBuilder(Protocol):
|
|
@@ -25,7 +17,7 @@ class _ServiceBuilder(Protocol):
|
|
|
25
17
|
def with_parent(self, identity: str) -> "_ServiceBuilder":
|
|
26
18
|
pass
|
|
27
19
|
|
|
28
|
-
def build(self) ->
|
|
20
|
+
def build(self) -> RestfulResource: # pragma: no cover
|
|
29
21
|
pass
|
|
30
22
|
|
|
31
23
|
|
|
@@ -38,10 +30,10 @@ class _Dependency(Protocol):
|
|
|
38
30
|
class ServiceDependency:
|
|
39
31
|
dependency: _Dependency
|
|
40
32
|
|
|
41
|
-
def as_dependable(self) -> type[
|
|
33
|
+
def as_dependable(self) -> type[RestfulResource]:
|
|
42
34
|
Builder = self.dependency.as_dependable()
|
|
43
35
|
|
|
44
|
-
def _(builder: Builder) ->
|
|
36
|
+
def _(builder: Builder) -> RestfulResource:
|
|
45
37
|
try:
|
|
46
38
|
return builder.build()
|
|
47
39
|
except ForbiddenError as e:
|
|
@@ -49,7 +41,7 @@ class ServiceDependency:
|
|
|
49
41
|
403, RestfulResponse(RestfulName("unknown")).forbidden(e)
|
|
50
42
|
) from e
|
|
51
43
|
|
|
52
|
-
return Annotated[
|
|
44
|
+
return Annotated[RestfulResource, Depends(_)]
|
|
53
45
|
|
|
54
46
|
|
|
55
47
|
@dataclass(frozen=True)
|
|
@@ -119,5 +111,5 @@ class DependableBuilder:
|
|
|
119
111
|
def with_user(self, extract_user: Callable[..., Any]) -> "DependableBuilder":
|
|
120
112
|
return DependableBuilder(UserDependency(extract_user, self.dependency))
|
|
121
113
|
|
|
122
|
-
def as_dependable(self) -> type[
|
|
114
|
+
def as_dependable(self) -> type[RestfulResource]:
|
|
123
115
|
return ServiceDependency(self.dependency).as_dependable()
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from fastapi.responses import JSONResponse
|
|
6
|
+
|
|
7
|
+
from apexdevkit.error import DoesNotExistError, ExistsError, ForbiddenError
|
|
8
|
+
from apexdevkit.fastapi.response import RestfulResponse
|
|
9
|
+
|
|
10
|
+
_Response = JSONResponse | dict[str, Any]
|
|
11
|
+
_Endpoint = Callable[..., _Response]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class RestfulEndpoints:
|
|
16
|
+
response: RestfulResponse
|
|
17
|
+
|
|
18
|
+
def create_one_endpoint(self, Service, Item) -> _Endpoint:
|
|
19
|
+
def endpoint(service: Service, item: Item) -> _Response:
|
|
20
|
+
try:
|
|
21
|
+
result = service.create_one(item)
|
|
22
|
+
except ExistsError as e:
|
|
23
|
+
return JSONResponse(self.response.exists(e), 409)
|
|
24
|
+
except ForbiddenError as e:
|
|
25
|
+
return JSONResponse(self.response.forbidden(e), 403)
|
|
26
|
+
|
|
27
|
+
return self.response.created_one(result)
|
|
28
|
+
|
|
29
|
+
return endpoint
|
|
30
|
+
|
|
31
|
+
def read_one_endpoint(self, Service, ItemId) -> _Endpoint:
|
|
32
|
+
def endpoint(service: Service, item_id: ItemId) -> _Response:
|
|
33
|
+
try:
|
|
34
|
+
result = service.read_one(item_id)
|
|
35
|
+
except DoesNotExistError as e:
|
|
36
|
+
return JSONResponse(self.response.not_found(e), 404)
|
|
37
|
+
except ForbiddenError as e:
|
|
38
|
+
return JSONResponse(self.response.forbidden(e), 403)
|
|
39
|
+
|
|
40
|
+
return self.response.found_one(result)
|
|
41
|
+
|
|
42
|
+
return endpoint
|
|
43
|
+
|
|
44
|
+
def read_many_endpoint(self, Service, QueryParams) -> _Endpoint:
|
|
45
|
+
def endpoint(service: Service, params: QueryParams) -> _Response:
|
|
46
|
+
try:
|
|
47
|
+
result = list(service.read_many(**dict(params)))
|
|
48
|
+
except ForbiddenError as e:
|
|
49
|
+
return JSONResponse(self.response.forbidden(e), 403)
|
|
50
|
+
|
|
51
|
+
return self.response.found_many(result)
|
|
52
|
+
|
|
53
|
+
return endpoint
|
|
54
|
+
|
|
55
|
+
def read_all_endpoint(self, Service) -> _Endpoint:
|
|
56
|
+
def endpoint(service: Service) -> _Response:
|
|
57
|
+
try:
|
|
58
|
+
result = list(service.read_all())
|
|
59
|
+
except ForbiddenError as e:
|
|
60
|
+
return JSONResponse(self.response.forbidden(e), 403)
|
|
61
|
+
|
|
62
|
+
return self.response.found_many(result)
|
|
63
|
+
|
|
64
|
+
return endpoint
|
|
65
|
+
|
|
66
|
+
def update_one_endpoint(self, Service, ItemId, Updates) -> _Endpoint:
|
|
67
|
+
def endpoint(service: Service, item_id: ItemId, updates: Updates) -> _Response:
|
|
68
|
+
try:
|
|
69
|
+
service.update_one(item_id, **updates)
|
|
70
|
+
except DoesNotExistError as e:
|
|
71
|
+
return JSONResponse(self.response.not_found(e), 404)
|
|
72
|
+
except ForbiddenError as e:
|
|
73
|
+
return JSONResponse(self.response.forbidden(e), 403)
|
|
74
|
+
|
|
75
|
+
return self.response.ok()
|
|
76
|
+
|
|
77
|
+
return endpoint
|
|
78
|
+
|
|
79
|
+
def replace_one_endpoint(self, Service, Item) -> _Endpoint:
|
|
80
|
+
def endpoint(service: Service, item: Item) -> _Response:
|
|
81
|
+
try:
|
|
82
|
+
service.replace_one(item)
|
|
83
|
+
except DoesNotExistError as e:
|
|
84
|
+
return JSONResponse(self.response.not_found(e), 404)
|
|
85
|
+
except ForbiddenError as e:
|
|
86
|
+
return JSONResponse(self.response.forbidden(e), 403)
|
|
87
|
+
|
|
88
|
+
return self.response.ok()
|
|
89
|
+
|
|
90
|
+
return endpoint
|
|
91
|
+
|
|
92
|
+
def delete_one_endpoint(self, Service, ItemId) -> _Endpoint:
|
|
93
|
+
def endpoint(service: Service, item_id: ItemId) -> _Response:
|
|
94
|
+
try:
|
|
95
|
+
service.delete_one(item_id)
|
|
96
|
+
except DoesNotExistError as e:
|
|
97
|
+
return JSONResponse(self.response.not_found(e), 404)
|
|
98
|
+
except ForbiddenError as e:
|
|
99
|
+
return JSONResponse(self.response.forbidden(e), 403)
|
|
100
|
+
|
|
101
|
+
return self.response.ok()
|
|
102
|
+
|
|
103
|
+
return endpoint
|
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
from collections.abc import Callable
|
|
1
|
+
from collections.abc import Callable, Mapping
|
|
2
2
|
from dataclasses import dataclass, field
|
|
3
3
|
from enum import Enum
|
|
4
|
-
from typing import Annotated, Any, Protocol, Self
|
|
4
|
+
from typing import Annotated, Any, Protocol, Self
|
|
5
5
|
|
|
6
6
|
from fastapi import APIRouter, Depends, Path, Query
|
|
7
7
|
from fastapi.responses import JSONResponse
|
|
8
8
|
from pypebbles import FluentDict
|
|
9
9
|
|
|
10
|
+
from apexdevkit.fastapi.endpoints import RestfulEndpoints
|
|
10
11
|
from apexdevkit.fastapi.name import RestfulName
|
|
11
|
-
from apexdevkit.fastapi.resource import RestfulResource
|
|
12
12
|
from apexdevkit.fastapi.response import RestfulResponse
|
|
13
13
|
from apexdevkit.fastapi.schema import RestfulSchema, Schema, SchemaFields
|
|
14
|
-
from apexdevkit.fastapi.service import
|
|
14
|
+
from apexdevkit.fastapi.service import RestfulResource
|
|
15
|
+
|
|
16
|
+
RawItem = Mapping[str, Any]
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
class RouterWithHiddenUnderscoreRoutes(APIRouter):
|
|
@@ -47,26 +49,24 @@ class RouterWithHiddenUnderscoreRoutes(APIRouter):
|
|
|
47
49
|
|
|
48
50
|
_Response = JSONResponse | dict[str, Any]
|
|
49
51
|
|
|
50
|
-
T = TypeVar("T")
|
|
51
|
-
|
|
52
52
|
|
|
53
|
-
class Dependency(Protocol): # pragma: no cover
|
|
54
|
-
def as_dependable(self) -> type[
|
|
53
|
+
class Dependency[T](Protocol): # pragma: no cover
|
|
54
|
+
def as_dependable(self) -> type[RestfulResource[T]]:
|
|
55
55
|
pass
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
@dataclass
|
|
59
|
-
class RestfulRouter:
|
|
59
|
+
class RestfulRouter[T]:
|
|
60
60
|
name: RestfulName
|
|
61
61
|
|
|
62
62
|
parent: RestfulName | None = None
|
|
63
63
|
router: APIRouter = field(default_factory=RouterWithHiddenUnderscoreRoutes)
|
|
64
64
|
|
|
65
65
|
_schema: RestfulSchema = field(init=False)
|
|
66
|
-
_dependency: Dependency | None = field(init=False, default=None)
|
|
66
|
+
_dependency: Dependency[T] | None = field(init=False, default=None)
|
|
67
67
|
|
|
68
68
|
@classmethod
|
|
69
|
-
def named(cls, singular: str, *, plural: str | None = None) -> "RestfulRouter":
|
|
69
|
+
def named(cls, singular: str, *, plural: str | None = None) -> "RestfulRouter[T]":
|
|
70
70
|
if plural:
|
|
71
71
|
return cls(RestfulName(singular, plural))
|
|
72
72
|
|
|
@@ -81,8 +81,8 @@ class RestfulRouter:
|
|
|
81
81
|
return self
|
|
82
82
|
|
|
83
83
|
@property
|
|
84
|
-
def
|
|
85
|
-
return
|
|
84
|
+
def endpoints(self) -> RestfulEndpoints:
|
|
85
|
+
return RestfulEndpoints(RestfulResponse(self.name))
|
|
86
86
|
|
|
87
87
|
@property
|
|
88
88
|
def id_alias(self) -> str:
|
|
@@ -115,20 +115,20 @@ class RestfulRouter:
|
|
|
115
115
|
|
|
116
116
|
return self
|
|
117
117
|
|
|
118
|
-
def with_default_dependency(self, value: Dependency) -> Self:
|
|
118
|
+
def with_default_dependency(self, value: Dependency[T]) -> Self:
|
|
119
119
|
self._dependency = value
|
|
120
120
|
|
|
121
121
|
return self
|
|
122
122
|
|
|
123
123
|
def with_create_one(
|
|
124
124
|
self,
|
|
125
|
-
dependency: Dependency | None = None,
|
|
125
|
+
dependency: Dependency[T] | None = None,
|
|
126
126
|
*,
|
|
127
127
|
is_documented: bool = True,
|
|
128
128
|
) -> Self:
|
|
129
129
|
self.router.add_api_route(
|
|
130
130
|
"",
|
|
131
|
-
self.
|
|
131
|
+
self.endpoints.create_one_endpoint(
|
|
132
132
|
Service=self._resolve(dependency),
|
|
133
133
|
Item=Annotated[
|
|
134
134
|
RawItem,
|
|
@@ -147,13 +147,13 @@ class RestfulRouter:
|
|
|
147
147
|
|
|
148
148
|
def with_read_one(
|
|
149
149
|
self,
|
|
150
|
-
dependency: Dependency | None = None,
|
|
150
|
+
dependency: Dependency[T] | None = None,
|
|
151
151
|
*,
|
|
152
152
|
is_documented: bool = True,
|
|
153
153
|
) -> Self:
|
|
154
154
|
self.router.add_api_route(
|
|
155
155
|
self.item_path,
|
|
156
|
-
self.
|
|
156
|
+
self.endpoints.read_one_endpoint(
|
|
157
157
|
Service=self._resolve(dependency),
|
|
158
158
|
ItemId=Annotated[
|
|
159
159
|
str,
|
|
@@ -173,16 +173,19 @@ class RestfulRouter:
|
|
|
173
173
|
def with_read_many(
|
|
174
174
|
self,
|
|
175
175
|
query: FluentDict[Any],
|
|
176
|
-
dependency: Dependency | None = None,
|
|
176
|
+
dependency: Dependency[T] | None = None,
|
|
177
177
|
*,
|
|
178
178
|
is_documented: bool = True,
|
|
179
179
|
) -> Self:
|
|
180
180
|
self.router.add_api_route(
|
|
181
181
|
"",
|
|
182
|
-
self.
|
|
182
|
+
self.endpoints.read_many_endpoint(
|
|
183
183
|
Service=self._resolve(dependency),
|
|
184
184
|
QueryParams=Annotated[
|
|
185
|
-
Schema(self.name.singular).optional_schema_for(
|
|
185
|
+
Schema(self.name.singular).optional_schema_for(
|
|
186
|
+
action="ReadMany",
|
|
187
|
+
fields=query,
|
|
188
|
+
),
|
|
186
189
|
Query(),
|
|
187
190
|
],
|
|
188
191
|
),
|
|
@@ -198,13 +201,15 @@ class RestfulRouter:
|
|
|
198
201
|
|
|
199
202
|
def with_read_all(
|
|
200
203
|
self,
|
|
201
|
-
dependency: Dependency | None = None,
|
|
204
|
+
dependency: Dependency[T] | None = None,
|
|
202
205
|
*,
|
|
203
206
|
is_documented: bool = True,
|
|
204
207
|
) -> Self:
|
|
205
208
|
self.router.add_api_route(
|
|
206
209
|
"",
|
|
207
|
-
self.
|
|
210
|
+
self.endpoints.read_all_endpoint(
|
|
211
|
+
Service=self._resolve(dependency),
|
|
212
|
+
),
|
|
208
213
|
methods=["GET"],
|
|
209
214
|
status_code=200,
|
|
210
215
|
responses={},
|
|
@@ -217,13 +222,13 @@ class RestfulRouter:
|
|
|
217
222
|
|
|
218
223
|
def with_update_one(
|
|
219
224
|
self,
|
|
220
|
-
dependency: Dependency | None = None,
|
|
225
|
+
dependency: Dependency[T] | None = None,
|
|
221
226
|
*,
|
|
222
227
|
is_documented: bool = True,
|
|
223
228
|
) -> Self:
|
|
224
229
|
self.router.add_api_route(
|
|
225
230
|
self.item_path,
|
|
226
|
-
self.
|
|
231
|
+
self.endpoints.update_one_endpoint(
|
|
227
232
|
Service=self._resolve(dependency),
|
|
228
233
|
ItemId=Annotated[
|
|
229
234
|
str,
|
|
@@ -246,13 +251,13 @@ class RestfulRouter:
|
|
|
246
251
|
|
|
247
252
|
def with_replace_one(
|
|
248
253
|
self,
|
|
249
|
-
dependency: Dependency | None = None,
|
|
254
|
+
dependency: Dependency[T] | None = None,
|
|
250
255
|
*,
|
|
251
256
|
is_documented: bool = True,
|
|
252
257
|
) -> Self:
|
|
253
258
|
self.router.add_api_route(
|
|
254
259
|
"",
|
|
255
|
-
self.
|
|
260
|
+
self.endpoints.replace_one_endpoint(
|
|
256
261
|
Service=self._resolve(dependency),
|
|
257
262
|
Item=Annotated[
|
|
258
263
|
RawItem,
|
|
@@ -271,13 +276,13 @@ class RestfulRouter:
|
|
|
271
276
|
|
|
272
277
|
def with_delete_one(
|
|
273
278
|
self,
|
|
274
|
-
dependency: Dependency | None = None,
|
|
279
|
+
dependency: Dependency[T] | None = None,
|
|
275
280
|
*,
|
|
276
281
|
is_documented: bool = True,
|
|
277
282
|
) -> Self:
|
|
278
283
|
self.router.add_api_route(
|
|
279
284
|
self.item_path,
|
|
280
|
-
self.
|
|
285
|
+
self.endpoints.delete_one_endpoint(
|
|
281
286
|
Service=self._resolve(dependency),
|
|
282
287
|
ItemId=Annotated[
|
|
283
288
|
str,
|
|
@@ -312,7 +317,7 @@ class RestfulRouter:
|
|
|
312
317
|
def build(self) -> APIRouter:
|
|
313
318
|
return self.router
|
|
314
319
|
|
|
315
|
-
def _resolve(self, dependency: Dependency | None) -> type[
|
|
320
|
+
def _resolve(self, dependency: Dependency[T] | None) -> type[RestfulResource[T]]:
|
|
316
321
|
resolved = dependency or self._dependency
|
|
317
322
|
assert resolved, "One of default or endpoint dependency must be specified"
|
|
318
323
|
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from pypebbles import JsonDict
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Iterable, Mapping
|
|
11
|
+
|
|
12
|
+
from apexdevkit.formatter import Formatter
|
|
13
|
+
from apexdevkit.repository import Entity, Repository
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, kw_only=True)
|
|
17
|
+
class RestfulResource[T]:
|
|
18
|
+
service: RestfulService[T]
|
|
19
|
+
formatter: Formatter[Mapping[str, Any], T]
|
|
20
|
+
|
|
21
|
+
def create_one(self, item: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
22
|
+
result = self.service.create_one(self.formatter.load(item))
|
|
23
|
+
|
|
24
|
+
return self.formatter.dump(result)
|
|
25
|
+
|
|
26
|
+
def read_one(self, item_id: str) -> Mapping[str, Any]:
|
|
27
|
+
result = self.service.read_one(with_id=item_id)
|
|
28
|
+
|
|
29
|
+
return self.formatter.dump(result)
|
|
30
|
+
|
|
31
|
+
def read_all(self) -> Iterable[Mapping[str, Any]]:
|
|
32
|
+
result = self.service.read_all()
|
|
33
|
+
|
|
34
|
+
return [self.formatter.dump(item) for item in result]
|
|
35
|
+
|
|
36
|
+
def read_many(self, **params: Any) -> Iterable[Mapping[str, Any]]:
|
|
37
|
+
result = self.service.read_many(JsonDict(params))
|
|
38
|
+
|
|
39
|
+
return [self.formatter.dump(item) for item in result]
|
|
40
|
+
|
|
41
|
+
def update_one(self, item_id: str, **with_fields: Any) -> Mapping[str, Any]:
|
|
42
|
+
current = self.service.read_one(with_id=item_id)
|
|
43
|
+
updated = {**self.formatter.dump(current), **with_fields}
|
|
44
|
+
result = self.service.update_one(self.formatter.load(updated))
|
|
45
|
+
|
|
46
|
+
return self.formatter.dump(result)
|
|
47
|
+
|
|
48
|
+
def replace_one(self, item: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
49
|
+
result = self.service.update_one(self.formatter.load(item))
|
|
50
|
+
|
|
51
|
+
return self.formatter.dump(result)
|
|
52
|
+
|
|
53
|
+
def delete_one(self, item_id: str) -> None:
|
|
54
|
+
self.service.delete_one(with_id=item_id)
|
|
55
|
+
|
|
56
|
+
@dataclass(kw_only=True)
|
|
57
|
+
class Builder[BT](ABC):
|
|
58
|
+
formatter: Formatter[Mapping[str, Any], BT]
|
|
59
|
+
|
|
60
|
+
parent_id: str = field(init=False)
|
|
61
|
+
user: Any = field(init=False)
|
|
62
|
+
|
|
63
|
+
def with_user(self, user: Any) -> RestfulResource.Builder[BT]:
|
|
64
|
+
self.user = user
|
|
65
|
+
|
|
66
|
+
return self
|
|
67
|
+
|
|
68
|
+
def with_parent(self, identity: str) -> RestfulResource.Builder[BT]:
|
|
69
|
+
self.parent_id = identity
|
|
70
|
+
|
|
71
|
+
return self
|
|
72
|
+
|
|
73
|
+
def build(self) -> RestfulResource[BT]:
|
|
74
|
+
return RestfulResource(
|
|
75
|
+
service=self.service(),
|
|
76
|
+
formatter=self.formatter,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
@abstractmethod
|
|
80
|
+
def service(self) -> RestfulService[BT]:
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
@dataclass(kw_only=True)
|
|
84
|
+
class PreBuilt[PBT](Builder[PBT]): # pragma: no cover
|
|
85
|
+
instance: RestfulService[PBT]
|
|
86
|
+
|
|
87
|
+
def service(self) -> RestfulService[PBT]:
|
|
88
|
+
return self.instance
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class RestfulService[T]:
|
|
92
|
+
def create_one(self, item: T) -> T:
|
|
93
|
+
raise NotImplementedError
|
|
94
|
+
|
|
95
|
+
def read_one(self, *, with_id: str) -> T:
|
|
96
|
+
raise NotImplementedError
|
|
97
|
+
|
|
98
|
+
def read_all(self) -> Iterable[T]:
|
|
99
|
+
raise NotImplementedError
|
|
100
|
+
|
|
101
|
+
def read_many(self, query: JsonDict) -> Iterable[T]:
|
|
102
|
+
raise NotImplementedError
|
|
103
|
+
|
|
104
|
+
def update_one(self, item: T) -> T:
|
|
105
|
+
raise NotImplementedError
|
|
106
|
+
|
|
107
|
+
def delete_one(self, with_id: str) -> T:
|
|
108
|
+
raise NotImplementedError
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(frozen=True)
|
|
112
|
+
class RestfulRepository[T: Entity](RestfulService[T]):
|
|
113
|
+
repository: Repository[T]
|
|
114
|
+
|
|
115
|
+
def create_one(self, item: T) -> T:
|
|
116
|
+
return self.repository.create(item)
|
|
117
|
+
|
|
118
|
+
def read_one(self, *, with_id: str) -> T:
|
|
119
|
+
return self.repository.read(with_id)
|
|
120
|
+
|
|
121
|
+
def read_all(self) -> Iterable[T]:
|
|
122
|
+
yield from self.repository
|
|
123
|
+
|
|
124
|
+
def read_many(self, query: JsonDict) -> Iterable[T]:
|
|
125
|
+
raise NotImplementedError
|
|
126
|
+
|
|
127
|
+
def update_one(self, item: T) -> T:
|
|
128
|
+
self.repository.update(item)
|
|
129
|
+
|
|
130
|
+
return item
|
|
131
|
+
|
|
132
|
+
def delete_one(self, with_id: str) -> T:
|
|
133
|
+
result = self.repository.read(with_id)
|
|
134
|
+
|
|
135
|
+
self.repository.delete(with_id)
|
|
136
|
+
|
|
137
|
+
return result
|
|
@@ -17,6 +17,14 @@ class Formatter[SourceT, TargetT](Protocol): # pragma: no cover
|
|
|
17
17
|
pass
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
class NoFormatter[S, T]:
|
|
21
|
+
def load(self, source: S) -> T:
|
|
22
|
+
raise NotImplementedError
|
|
23
|
+
|
|
24
|
+
def dump(self, target: T) -> S:
|
|
25
|
+
raise NotImplementedError
|
|
26
|
+
|
|
27
|
+
|
|
20
28
|
@dataclass(frozen=True)
|
|
21
29
|
class AliasFormatter[TargetT](Formatter[Mapping[str, Any], TargetT]):
|
|
22
30
|
inner: Formatter[Mapping[str, Any], TargetT]
|
|
@@ -127,9 +127,10 @@ class ResponseProbe:
|
|
|
127
127
|
return self.with_item(value)
|
|
128
128
|
|
|
129
129
|
def with_item(self, value: Any) -> Self:
|
|
130
|
-
|
|
130
|
+
if self.response.item() == value:
|
|
131
|
+
return self
|
|
131
132
|
|
|
132
|
-
|
|
133
|
+
raise AssertionError("expected=", self.response.item(), "actual=", value)
|
|
133
134
|
|
|
134
135
|
def and_collection(self, values: Collection[Any]) -> Self:
|
|
135
136
|
return self.with_collection(values)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "apexdevkit"
|
|
3
|
-
version = "2.1
|
|
3
|
+
version = "2.3.1"
|
|
4
4
|
description = "Apex Development Tools for python."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -31,7 +31,7 @@ pytest-env = "*"
|
|
|
31
31
|
pytest-recording = "*"
|
|
32
32
|
|
|
33
33
|
[tool.ruff.lint.per-file-ignores]
|
|
34
|
-
"apexdevkit/fastapi/
|
|
34
|
+
"apexdevkit/fastapi/endpoints.py" = [ "N803" ]
|
|
35
35
|
"apexdevkit/fastapi/dependable.py" = [ "N806" ]
|
|
36
36
|
|
|
37
37
|
[tool.ruff.lint.mccabe]
|
|
@@ -80,6 +80,7 @@ ignore = [
|
|
|
80
80
|
[tool.mypy]
|
|
81
81
|
exclude = [
|
|
82
82
|
"apexdevkit/fastapi/dependable.py",
|
|
83
|
+
"apexdevkit/fastapi/endpoints.py",
|
|
83
84
|
"apexdevkit/fastapi/schema.py",
|
|
84
85
|
]
|
|
85
86
|
ignore_missing_imports = true
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
from apexdevkit.fastapi.builder import FastApiBuilder, RestfulServiceBuilder
|
|
2
|
-
from apexdevkit.fastapi.dependable import inject
|
|
3
|
-
from apexdevkit.fastapi.router import RestfulRouter, RouterWithHiddenUnderscoreRoutes
|
|
4
|
-
from apexdevkit.fastapi.service import RestfulRepository
|
|
5
|
-
|
|
6
|
-
__all__ = [
|
|
7
|
-
"FastApiBuilder",
|
|
8
|
-
"RestfulRepository",
|
|
9
|
-
"RestfulRouter",
|
|
10
|
-
"RestfulServiceBuilder",
|
|
11
|
-
"RouterWithHiddenUnderscoreRoutes",
|
|
12
|
-
"inject",
|
|
13
|
-
]
|
|
@@ -1,319 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
from collections.abc import Callable
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from typing import Any, Union
|
|
5
|
-
|
|
6
|
-
from fastapi.responses import JSONResponse
|
|
7
|
-
from pydantic import BaseModel
|
|
8
|
-
|
|
9
|
-
from apexdevkit.error import DoesNotExistError, ExistsError, ForbiddenError
|
|
10
|
-
from apexdevkit.fastapi.response import RestfulResponse
|
|
11
|
-
from apexdevkit.query.query import (
|
|
12
|
-
Aggregation,
|
|
13
|
-
AggregationOption,
|
|
14
|
-
DateValue,
|
|
15
|
-
Filter,
|
|
16
|
-
FooterOptions,
|
|
17
|
-
Leaf,
|
|
18
|
-
ListValue,
|
|
19
|
-
NullValue,
|
|
20
|
-
NumericValue,
|
|
21
|
-
Operation,
|
|
22
|
-
Operator,
|
|
23
|
-
Page,
|
|
24
|
-
QueryOptions,
|
|
25
|
-
Sort,
|
|
26
|
-
StringValue,
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
_Response = JSONResponse | dict[str, Any]
|
|
30
|
-
_Endpoint = Callable[..., _Response]
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
@dataclass(frozen=True)
|
|
34
|
-
class RestfulResource:
|
|
35
|
-
response: RestfulResponse
|
|
36
|
-
|
|
37
|
-
def create_one(self, Service, Item) -> _Endpoint: # type: ignore
|
|
38
|
-
def endpoint(service: Service, item: Item) -> _Response:
|
|
39
|
-
try:
|
|
40
|
-
item = service.create_one(item)
|
|
41
|
-
except ExistsError as e:
|
|
42
|
-
return JSONResponse(self.response.exists(e), 409)
|
|
43
|
-
except ForbiddenError as e:
|
|
44
|
-
return JSONResponse(self.response.forbidden(e), 403)
|
|
45
|
-
|
|
46
|
-
return self.response.created_one(item)
|
|
47
|
-
|
|
48
|
-
return endpoint
|
|
49
|
-
|
|
50
|
-
def create_many(self, Service, Collection) -> _Endpoint: # type: ignore
|
|
51
|
-
def endpoint(service: Service, items: Collection) -> _Response:
|
|
52
|
-
try:
|
|
53
|
-
return self.response.created_many(service.create_many(items))
|
|
54
|
-
except ExistsError as e:
|
|
55
|
-
return JSONResponse(self.response.exists(e), 409)
|
|
56
|
-
except ForbiddenError as e:
|
|
57
|
-
return JSONResponse(self.response.forbidden(e), 403)
|
|
58
|
-
|
|
59
|
-
return endpoint
|
|
60
|
-
|
|
61
|
-
def read_one(self, Service, ItemId) -> _Endpoint: # type: ignore
|
|
62
|
-
def endpoint(service: Service, item_id: ItemId) -> _Response:
|
|
63
|
-
try:
|
|
64
|
-
return self.response.found_one(service.read_one(item_id))
|
|
65
|
-
except DoesNotExistError as e:
|
|
66
|
-
return JSONResponse(self.response.not_found(e), 404)
|
|
67
|
-
except ForbiddenError as e:
|
|
68
|
-
return JSONResponse(self.response.forbidden(e), 403)
|
|
69
|
-
|
|
70
|
-
return endpoint
|
|
71
|
-
|
|
72
|
-
def read_many(self, Service, QueryParams) -> _Endpoint: # type: ignore
|
|
73
|
-
def endpoint(service: Service, params: QueryParams) -> _Response:
|
|
74
|
-
try:
|
|
75
|
-
return self.response.found_many(list(service.read_many(**dict(params))))
|
|
76
|
-
except ForbiddenError as e:
|
|
77
|
-
return JSONResponse(self.response.forbidden(e), 403)
|
|
78
|
-
|
|
79
|
-
return endpoint
|
|
80
|
-
|
|
81
|
-
def filter_with(self, Service, QueryOptions) -> _Endpoint: # type: ignore
|
|
82
|
-
def endpoint(service: Service, options: QueryOptions) -> _Response:
|
|
83
|
-
try:
|
|
84
|
-
return self.response.found_many(list(service.filter_with(options)))
|
|
85
|
-
except ForbiddenError as e:
|
|
86
|
-
return JSONResponse(self.response.forbidden(e), 403)
|
|
87
|
-
|
|
88
|
-
return endpoint
|
|
89
|
-
|
|
90
|
-
def aggregation_with(self, Service, FilterOptions) -> _Endpoint: # type: ignore
|
|
91
|
-
def endpoint(service: Service, options: FilterOptions) -> _Response:
|
|
92
|
-
try:
|
|
93
|
-
return {
|
|
94
|
-
"status": "success",
|
|
95
|
-
"code": 200,
|
|
96
|
-
"aggregations": service.aggregation_with(options),
|
|
97
|
-
}
|
|
98
|
-
except ForbiddenError as e:
|
|
99
|
-
return JSONResponse(self.response.forbidden(e), 403)
|
|
100
|
-
|
|
101
|
-
return endpoint
|
|
102
|
-
|
|
103
|
-
def read_all(self, Service) -> _Endpoint: # type: ignore
|
|
104
|
-
def endpoint(service: Service) -> _Response:
|
|
105
|
-
try:
|
|
106
|
-
return self.response.found_many(list(service.read_all()))
|
|
107
|
-
except ForbiddenError as e:
|
|
108
|
-
return JSONResponse(self.response.forbidden(e), 403)
|
|
109
|
-
|
|
110
|
-
return endpoint
|
|
111
|
-
|
|
112
|
-
def update_one(self, Service, ItemId, Updates) -> _Endpoint: # type: ignore
|
|
113
|
-
def endpoint(service: Service, item_id: ItemId, updates: Updates) -> _Response:
|
|
114
|
-
try:
|
|
115
|
-
service.update_one(item_id, **updates)
|
|
116
|
-
except DoesNotExistError as e:
|
|
117
|
-
return JSONResponse(self.response.not_found(e), 404)
|
|
118
|
-
except ForbiddenError as e:
|
|
119
|
-
return JSONResponse(self.response.forbidden(e), 403)
|
|
120
|
-
|
|
121
|
-
return self.response.ok()
|
|
122
|
-
|
|
123
|
-
return endpoint
|
|
124
|
-
|
|
125
|
-
def update_many(self, Service, Collection) -> _Endpoint: # type: ignore
|
|
126
|
-
def endpoint(service: Service, items: Collection) -> _Response:
|
|
127
|
-
try:
|
|
128
|
-
service.update_many(items)
|
|
129
|
-
except DoesNotExistError as e:
|
|
130
|
-
return JSONResponse(self.response.not_found(e), 404)
|
|
131
|
-
except ForbiddenError as e:
|
|
132
|
-
return JSONResponse(self.response.forbidden(e), 403)
|
|
133
|
-
|
|
134
|
-
return self.response.ok()
|
|
135
|
-
|
|
136
|
-
return endpoint
|
|
137
|
-
|
|
138
|
-
def replace_one(self, Service, Item) -> _Endpoint: # type: ignore
|
|
139
|
-
def endpoint(service: Service, item: Item) -> _Response:
|
|
140
|
-
try:
|
|
141
|
-
service.replace_one(item)
|
|
142
|
-
except DoesNotExistError as e:
|
|
143
|
-
return JSONResponse(self.response.not_found(e), 404)
|
|
144
|
-
except ForbiddenError as e:
|
|
145
|
-
return JSONResponse(self.response.forbidden(e), 403)
|
|
146
|
-
|
|
147
|
-
return self.response.ok()
|
|
148
|
-
|
|
149
|
-
return endpoint
|
|
150
|
-
|
|
151
|
-
def replace_many(self, Service, Collection) -> _Endpoint: # type: ignore
|
|
152
|
-
def endpoint(service: Service, items: Collection) -> _Response:
|
|
153
|
-
try:
|
|
154
|
-
service.replace_many(items)
|
|
155
|
-
except DoesNotExistError as e:
|
|
156
|
-
return JSONResponse(self.response.not_found(e), 404)
|
|
157
|
-
except ForbiddenError as e:
|
|
158
|
-
return JSONResponse(self.response.forbidden(e), 403)
|
|
159
|
-
|
|
160
|
-
return self.response.ok()
|
|
161
|
-
|
|
162
|
-
return endpoint
|
|
163
|
-
|
|
164
|
-
def delete_one(self, Service, ItemId) -> _Endpoint: # type: ignore
|
|
165
|
-
def endpoint(service: Service, item_id: ItemId) -> _Response:
|
|
166
|
-
try:
|
|
167
|
-
service.delete_one(item_id)
|
|
168
|
-
except DoesNotExistError as e:
|
|
169
|
-
return JSONResponse(self.response.not_found(e), 404)
|
|
170
|
-
except ForbiddenError as e:
|
|
171
|
-
return JSONResponse(self.response.forbidden(e), 403)
|
|
172
|
-
|
|
173
|
-
return self.response.ok()
|
|
174
|
-
|
|
175
|
-
return endpoint
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
@dataclass
|
|
179
|
-
class _ColumnName:
|
|
180
|
-
raw: str
|
|
181
|
-
|
|
182
|
-
def __str__(self) -> str:
|
|
183
|
-
return re.sub(r"(?<!^)(?=[A-Z])", "_", self.raw).lower()
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
class _NumericItem(BaseModel):
|
|
187
|
-
value: int
|
|
188
|
-
exponent: int
|
|
189
|
-
|
|
190
|
-
def to_value(self) -> NumericValue:
|
|
191
|
-
return NumericValue(self.value, self.exponent)
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
class _StringItem(BaseModel):
|
|
195
|
-
value: str
|
|
196
|
-
|
|
197
|
-
def to_value(self) -> StringValue:
|
|
198
|
-
return StringValue(self.value)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
class _DateItem(BaseModel):
|
|
202
|
-
date: str
|
|
203
|
-
|
|
204
|
-
def to_value(self) -> DateValue:
|
|
205
|
-
return DateValue(self.date)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
class _ListItem(BaseModel):
|
|
209
|
-
values: list[_NumericItem | _StringItem | _DateItem]
|
|
210
|
-
|
|
211
|
-
def to_value(self) -> ListValue:
|
|
212
|
-
return ListValue([item.to_value() for item in self.values])
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
class _LeafItem(BaseModel):
|
|
216
|
-
name: str
|
|
217
|
-
values: list[_NumericItem | _StringItem | _DateItem]
|
|
218
|
-
|
|
219
|
-
def to_value(self) -> Leaf:
|
|
220
|
-
return Leaf(
|
|
221
|
-
name=str(_ColumnName(self.name)),
|
|
222
|
-
values=[item.to_value() for item in self.values],
|
|
223
|
-
)
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
class _OperatorItem(BaseModel):
|
|
227
|
-
operation: Operation
|
|
228
|
-
operands: list[Union["_LeafItem", "_OperatorItem"]]
|
|
229
|
-
|
|
230
|
-
def to_value(self) -> Operator:
|
|
231
|
-
return Operator(self.operation, [item.to_value() for item in self.operands])
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
class _SortItem(BaseModel):
|
|
235
|
-
name: str
|
|
236
|
-
is_descending: bool
|
|
237
|
-
|
|
238
|
-
def to_value(self) -> Sort:
|
|
239
|
-
return Sort(str(_ColumnName(self.name)), self.is_descending)
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
class _PageItem(BaseModel):
|
|
243
|
-
page: int | None
|
|
244
|
-
length: int | None
|
|
245
|
-
offset: int | None
|
|
246
|
-
|
|
247
|
-
def to_value(self) -> Page:
|
|
248
|
-
return Page(self.page, self.length, self.offset)
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
class _FilterItem(BaseModel):
|
|
252
|
-
args: list[_NumericItem | _StringItem | _DateItem | _ListItem | None]
|
|
253
|
-
|
|
254
|
-
def to_value(self) -> Filter:
|
|
255
|
-
return Filter(
|
|
256
|
-
[arg.to_value() if arg is not None else None for arg in self.args]
|
|
257
|
-
)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
class _AggregationOptionItem(BaseModel):
|
|
261
|
-
name: str | None
|
|
262
|
-
aggregation: Aggregation
|
|
263
|
-
|
|
264
|
-
def to_value(self) -> AggregationOption:
|
|
265
|
-
return AggregationOption(
|
|
266
|
-
str(_ColumnName(self.name)) if self.name else None,
|
|
267
|
-
self.aggregation,
|
|
268
|
-
)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
class _FooterOptions(BaseModel):
|
|
272
|
-
filter: _FilterItem | None
|
|
273
|
-
condition: _OperatorItem | None
|
|
274
|
-
aggregations: list[_AggregationOptionItem]
|
|
275
|
-
|
|
276
|
-
def to_footer_options(self) -> FooterOptions:
|
|
277
|
-
return FooterOptions(
|
|
278
|
-
filter=self.filter.to_value() if self.filter else None,
|
|
279
|
-
condition=self.condition.to_value() if self.condition else None,
|
|
280
|
-
aggregations=[item.to_value() for item in self.aggregations],
|
|
281
|
-
)
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
class _QueryOptions(BaseModel):
|
|
285
|
-
filter: _FilterItem | None
|
|
286
|
-
condition: _OperatorItem | None
|
|
287
|
-
ordering: list[_SortItem]
|
|
288
|
-
paging: _PageItem
|
|
289
|
-
|
|
290
|
-
def to_query_options(self) -> QueryOptions:
|
|
291
|
-
return QueryOptions(
|
|
292
|
-
filter=self.filter.to_value() if self.filter else None,
|
|
293
|
-
condition=self.condition.to_value() if self.condition else None,
|
|
294
|
-
ordering=[item.to_value() for item in self.ordering],
|
|
295
|
-
paging=self.paging.to_value(),
|
|
296
|
-
)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
class _SummaryItem(BaseModel):
|
|
300
|
-
name: str | None
|
|
301
|
-
aggregation: Aggregation
|
|
302
|
-
result: NumericValue | StringValue | DateValue | NullValue
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
class _SummaryListEnvelope(BaseModel):
|
|
306
|
-
count: int
|
|
307
|
-
summaries: list[_SummaryItem]
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
class SummaryResponse(BaseModel):
|
|
311
|
-
status: str
|
|
312
|
-
code: int
|
|
313
|
-
data: _SummaryListEnvelope
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
class SumResponse(BaseModel):
|
|
317
|
-
status: str
|
|
318
|
-
code: int
|
|
319
|
-
count: int
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from collections.abc import Iterable, Mapping
|
|
4
|
-
from dataclasses import dataclass
|
|
5
|
-
from functools import cached_property
|
|
6
|
-
from typing import TYPE_CHECKING, Any
|
|
7
|
-
|
|
8
|
-
from apexdevkit.repository import BruteForceBatch, Entity, Repository
|
|
9
|
-
|
|
10
|
-
if TYPE_CHECKING:
|
|
11
|
-
from apexdevkit.formatter import Formatter
|
|
12
|
-
from apexdevkit.repository.core.interface import BatchRepository
|
|
13
|
-
|
|
14
|
-
RawItem = Mapping[str, Any]
|
|
15
|
-
RawCollection = Iterable[RawItem]
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class _RawItemWithId(dict[str, Any]):
|
|
19
|
-
def __post_init__(self) -> None:
|
|
20
|
-
assert "id" in self
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
RawCollectionWithId = Iterable[_RawItemWithId]
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class RestfulService: # pragma: no cover
|
|
27
|
-
def create_one(self, item: RawItem) -> RawItem:
|
|
28
|
-
raise NotImplementedError(self.create_one.__name__)
|
|
29
|
-
|
|
30
|
-
def create_many(self, items: RawCollection) -> RawCollection:
|
|
31
|
-
raise NotImplementedError(self.create_many.__name__)
|
|
32
|
-
|
|
33
|
-
def read_one(self, item_id: str) -> RawItem:
|
|
34
|
-
raise NotImplementedError(self.read_one.__name__)
|
|
35
|
-
|
|
36
|
-
def read_many(self, **params: Any) -> RawCollection:
|
|
37
|
-
raise NotImplementedError(self.read_many.__name__)
|
|
38
|
-
|
|
39
|
-
def filter_with(self, options: RawItem) -> RawCollection:
|
|
40
|
-
raise NotImplementedError(self.filter_with.__name__)
|
|
41
|
-
|
|
42
|
-
def aggregation_with(self, options: RawItem) -> RawItem:
|
|
43
|
-
raise NotImplementedError(self.filter_with.__name__)
|
|
44
|
-
|
|
45
|
-
def read_all(self) -> RawCollection:
|
|
46
|
-
raise NotImplementedError(self.read_all.__name__)
|
|
47
|
-
|
|
48
|
-
def update_one(self, item_id: str, **with_fields: Any) -> RawItem:
|
|
49
|
-
raise NotImplementedError(self.update_one.__name__)
|
|
50
|
-
|
|
51
|
-
def update_many(self, items: RawCollectionWithId) -> RawCollection:
|
|
52
|
-
raise NotImplementedError(self.update_many.__name__)
|
|
53
|
-
|
|
54
|
-
def replace_one(self, item: RawItem) -> RawItem:
|
|
55
|
-
raise NotImplementedError(self.replace_one.__name__)
|
|
56
|
-
|
|
57
|
-
def replace_many(self, items: RawCollection) -> RawCollection:
|
|
58
|
-
raise NotImplementedError(self.replace_many.__name__)
|
|
59
|
-
|
|
60
|
-
def delete_one(self, item_id: str) -> None:
|
|
61
|
-
raise NotImplementedError(self.delete_one.__name__)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
@dataclass(frozen=True)
|
|
65
|
-
class RestfulRepository[ItemT: Entity](RestfulService):
|
|
66
|
-
repository: Repository[ItemT]
|
|
67
|
-
|
|
68
|
-
formatter: Formatter[Mapping[str, Any], ItemT]
|
|
69
|
-
|
|
70
|
-
@cached_property
|
|
71
|
-
def _batch(self) -> BatchRepository[ItemT]:
|
|
72
|
-
return BruteForceBatch(inner=self.repository)
|
|
73
|
-
|
|
74
|
-
def create_one(self, item: RawItem) -> RawItem:
|
|
75
|
-
return self.formatter.dump(self.repository.create(self.formatter.load(item)))
|
|
76
|
-
|
|
77
|
-
def create_many(self, items: RawCollection) -> RawCollection:
|
|
78
|
-
return [
|
|
79
|
-
self.formatter.dump(item)
|
|
80
|
-
for item in self._batch.load(
|
|
81
|
-
[self.formatter.load(fields) for fields in items]
|
|
82
|
-
)
|
|
83
|
-
]
|
|
84
|
-
|
|
85
|
-
def read_one(self, item_id: str) -> RawItem:
|
|
86
|
-
return self.formatter.dump(self.repository.read(item_id))
|
|
87
|
-
|
|
88
|
-
def read_all(self) -> RawCollection:
|
|
89
|
-
return [self.formatter.dump(item) for item in self.repository]
|
|
90
|
-
|
|
91
|
-
def update_one(self, item_id: str, **with_fields: Any) -> RawItem:
|
|
92
|
-
data = dict(self.formatter.dump(self.repository.read(item_id)))
|
|
93
|
-
data.update(**with_fields)
|
|
94
|
-
|
|
95
|
-
self.repository.update(self.formatter.load(data))
|
|
96
|
-
|
|
97
|
-
return data
|
|
98
|
-
|
|
99
|
-
def update_many(self, items: RawCollectionWithId) -> RawCollection:
|
|
100
|
-
updates = []
|
|
101
|
-
for fields in items:
|
|
102
|
-
data = dict(self.formatter.dump(self.repository.read(fields["id"])))
|
|
103
|
-
data.update(**fields)
|
|
104
|
-
|
|
105
|
-
updates.append(self.formatter.load(data))
|
|
106
|
-
|
|
107
|
-
return [self.formatter.dump(item) for item in self._batch.renew(updates)]
|
|
108
|
-
|
|
109
|
-
def replace_one(self, item: RawItem) -> RawItem:
|
|
110
|
-
self.repository.update(self.formatter.load(item))
|
|
111
|
-
|
|
112
|
-
return item
|
|
113
|
-
|
|
114
|
-
def replace_many(self, items: RawCollection) -> RawCollection:
|
|
115
|
-
updated = self._batch.renew([self.formatter.load(item) for item in items])
|
|
116
|
-
|
|
117
|
-
return [self.formatter.dump(item) for item in updated]
|
|
118
|
-
|
|
119
|
-
def delete_one(self, item_id: str) -> None:
|
|
120
|
-
self.repository.delete(item_id)
|
|
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
|
|
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
|