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.
Files changed (54) hide show
  1. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/PKG-INFO +1 -1
  2. apexdevkit-2.3.1/apexdevkit/fastapi/__init__.py +15 -0
  3. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/fastapi/builder.py +4 -39
  4. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/fastapi/dependable.py +6 -14
  5. apexdevkit-2.3.1/apexdevkit/fastapi/endpoints.py +103 -0
  6. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/fastapi/router.py +35 -30
  7. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/fastapi/schema.py +0 -3
  8. apexdevkit-2.3.1/apexdevkit/fastapi/service.py +137 -0
  9. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/formatter.py +8 -0
  10. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/testing.py +3 -2
  11. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/pyproject.toml +3 -2
  12. apexdevkit-2.1.4/apexdevkit/fastapi/__init__.py +0 -13
  13. apexdevkit-2.1.4/apexdevkit/fastapi/docs.py +0 -15
  14. apexdevkit-2.1.4/apexdevkit/fastapi/resource.py +0 -319
  15. apexdevkit-2.1.4/apexdevkit/fastapi/service.py +0 -120
  16. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/LICENSE +0 -0
  17. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/README.md +0 -0
  18. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/__init__.py +0 -0
  19. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/annotation/__init__.py +0 -0
  20. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/annotation/deprecate.py +0 -0
  21. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/date.py +0 -0
  22. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/error.py +0 -0
  23. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/fastapi/name.py +0 -0
  24. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/fastapi/response.py +0 -0
  25. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/key_fn.py +0 -0
  26. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/py.typed +0 -0
  27. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/query/__init__.py +0 -0
  28. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/query/generator.py +0 -0
  29. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/query/query.py +0 -0
  30. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/__init__.py +0 -0
  31. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/core/__init__.py +0 -0
  32. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/core/database.py +0 -0
  33. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/core/decorator.py +0 -0
  34. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/core/interface.py +0 -0
  35. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/core/mixin.py +0 -0
  36. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/in_memory/__init__.py +0 -0
  37. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/in_memory/mixin.py +0 -0
  38. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/in_memory/repository.py +0 -0
  39. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/in_memory/store.py +0 -0
  40. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/sql/__init__.py +0 -0
  41. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/sql/connector.py +0 -0
  42. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/sql/field.py +0 -0
  43. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/sql/mssql/__init__.py +0 -0
  44. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/sql/mssql/repository.py +0 -0
  45. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/sql/mssql/table.py +0 -0
  46. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/sql/sqlite/__init__.py +0 -0
  47. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/sql/sqlite/repository.py +0 -0
  48. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/repository/sql/sqlite/table.py +0 -0
  49. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/server.py +0 -0
  50. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/sync/__init__.py +0 -0
  51. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/sync/observer.py +0 -0
  52. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/sync/source.py +0 -0
  53. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/sync/sync.py +0 -0
  54. {apexdevkit-2.1.4 → apexdevkit-2.3.1}/apexdevkit/sync/target.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apexdevkit
3
- Version: 2.1.4
3
+ Version: 2.3.1
4
4
  Summary: Apex Development Tools for python.
5
5
  License-File: LICENSE
6
6
  Author: Apex Dev
@@ -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).normilized(),
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).normilized(), app)
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 normilized(self) -> str:
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.normilized()
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 RestfulService
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) -> RestfulService: # pragma: no cover
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[RestfulService]:
33
+ def as_dependable(self) -> type[RestfulResource]:
42
34
  Builder = self.dependency.as_dependable()
43
35
 
44
- def _(builder: Builder) -> RestfulService:
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[RestfulService, Depends(_)]
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[RestfulService]:
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, TypeVar
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 RawItem, RestfulService
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[RestfulService]:
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 resource(self) -> RestfulResource:
85
- return RestfulResource(RestfulResponse(self.name))
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.resource.create_one(
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.resource.read_one(
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.resource.read_many(
182
+ self.endpoints.read_many_endpoint(
183
183
  Service=self._resolve(dependency),
184
184
  QueryParams=Annotated[
185
- Schema(self.name.singular).optional_schema_for("ReadMany", query),
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.resource.read_all(Service=self._resolve(dependency)),
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.resource.update_one(
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.resource.replace_one(
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.resource.delete_one(
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[RestfulService]:
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
 
@@ -11,9 +11,6 @@ from apexdevkit.fastapi.name import RestfulName
11
11
 
12
12
 
13
13
  class SchemaFields(ABC):
14
- def id(self) -> FluentDict[type]:
15
- return self.readable().select("id")
16
-
17
14
  def writable(self) -> FluentDict[type]:
18
15
  return self.readable().drop("id")
19
16
 
@@ -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
- assert self.response.item() == value, self.response.raw
130
+ if self.response.item() == value:
131
+ return self
131
132
 
132
- return self
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.4"
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/resource.py" = [ "N803" ]
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,15 +0,0 @@
1
- from typing import TypeVar
2
-
3
- from pydantic import BaseModel
4
-
5
- PayloadT = TypeVar("PayloadT")
6
-
7
-
8
- class Response[PayloadT](BaseModel):
9
- status: str
10
- code: int
11
- data: PayloadT
12
-
13
-
14
- class NoData(BaseModel):
15
- pass
@@ -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