apexdevkit 1.5.15__tar.gz → 1.5.16__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.15 → apexdevkit-1.5.16}/PKG-INFO +1 -1
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/apexdevkit/fastapi/router.py +101 -14
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/apexdevkit/fastapi/schema.py +20 -10
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/apexdevkit/fastapi/service.py +18 -15
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/apexdevkit/testing/rest.py +42 -2
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/pyproject.toml +1 -1
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/LICENSE +0 -0
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/README.md +0 -0
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/apexdevkit/__init__.py +0 -0
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/apexdevkit/annotation/__init__.py +0 -0
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/apexdevkit/annotation/deprecate.py +0 -0
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/apexdevkit/error.py +0 -0
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/apexdevkit/fastapi/__init__.py +0 -0
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/apexdevkit/fastapi/builder.py +0 -0
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/apexdevkit/fastapi/dependable.py +0 -0
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/apexdevkit/fastapi/docs.py +0 -0
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/apexdevkit/fastapi/response.py +0 -0
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/apexdevkit/formatter.py +0 -0
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/apexdevkit/http/__init__.py +0 -0
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/apexdevkit/http/fake.py +0 -0
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/apexdevkit/http/fluent.py +0 -0
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/apexdevkit/http/httpx.py +0 -0
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/apexdevkit/http/json.py +0 -0
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/apexdevkit/http/url.py +0 -0
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/apexdevkit/py.typed +0 -0
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/apexdevkit/repository/__init__.py +0 -0
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/apexdevkit/repository/base.py +0 -0
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/apexdevkit/repository/connector.py +0 -0
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/apexdevkit/repository/database.py +0 -0
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/apexdevkit/repository/in_memory.py +0 -0
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/apexdevkit/repository/interface.py +0 -0
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/apexdevkit/testing/__init__.py +0 -0
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/apexdevkit/testing/database.py +0 -0
- {apexdevkit-1.5.15 → apexdevkit-1.5.16}/apexdevkit/testing/fake.py +0 -0
|
@@ -6,9 +6,8 @@ from typing import Annotated, Any, Callable, Iterable, Self, TypeVar
|
|
|
6
6
|
from fastapi import APIRouter, Depends, Path
|
|
7
7
|
from fastapi.responses import JSONResponse
|
|
8
8
|
|
|
9
|
-
from apexdevkit.annotation import deprecated
|
|
10
9
|
from apexdevkit.error import DoesNotExistError, ExistsError, ForbiddenError
|
|
11
|
-
from apexdevkit.fastapi.schema import
|
|
10
|
+
from apexdevkit.fastapi.schema import RestfulSchema, SchemaFields
|
|
12
11
|
from apexdevkit.fastapi.service import RawCollection, RawItem, RestfulService
|
|
13
12
|
from apexdevkit.testing import RestfulName
|
|
14
13
|
|
|
@@ -148,12 +147,6 @@ class RestfulRouter:
|
|
|
148
147
|
def item_path(self) -> str:
|
|
149
148
|
return "/{" + self.id_alias + "}"
|
|
150
149
|
|
|
151
|
-
@deprecated("Use with_name and with_fields instead")
|
|
152
|
-
def with_dataclass(self, value: Any) -> Self: # pragma: no cover
|
|
153
|
-
return self.with_name(RestfulName(value.__name__.lower())).with_fields(
|
|
154
|
-
DataclassFields(value)
|
|
155
|
-
)
|
|
156
|
-
|
|
157
150
|
def with_name(self, value: RestfulName) -> Self:
|
|
158
151
|
self.name = value
|
|
159
152
|
|
|
@@ -169,12 +162,6 @@ class RestfulRouter:
|
|
|
169
162
|
|
|
170
163
|
return self
|
|
171
164
|
|
|
172
|
-
@deprecated("Use with_infra instead")
|
|
173
|
-
def with_service(self, value: RestfulService) -> Self: # pragma: no cover
|
|
174
|
-
self.infra = PreBuiltRestfulService(value)
|
|
175
|
-
|
|
176
|
-
return self
|
|
177
|
-
|
|
178
165
|
def with_infra(self, value: RestfulServiceBuilder) -> Self:
|
|
179
166
|
self.infra = value
|
|
180
167
|
|
|
@@ -480,6 +467,106 @@ class RestfulRouter:
|
|
|
480
467
|
|
|
481
468
|
return endpoint
|
|
482
469
|
|
|
470
|
+
def with_replace_one_endpoint(
|
|
471
|
+
self,
|
|
472
|
+
is_documented: bool = True,
|
|
473
|
+
extract_user: Callable[..., Any] = no_user,
|
|
474
|
+
) -> Self:
|
|
475
|
+
self.router.add_api_route(
|
|
476
|
+
"",
|
|
477
|
+
self.replace_one(
|
|
478
|
+
User=Annotated[
|
|
479
|
+
Any,
|
|
480
|
+
Depends(extract_user),
|
|
481
|
+
],
|
|
482
|
+
ParentId=Annotated[
|
|
483
|
+
str,
|
|
484
|
+
Path(alias=self.parent_id_alias, default_factory=str),
|
|
485
|
+
],
|
|
486
|
+
Item=Annotated[
|
|
487
|
+
RawItem,
|
|
488
|
+
Depends(self.schema.for_replace_one()),
|
|
489
|
+
],
|
|
490
|
+
),
|
|
491
|
+
methods=["PUT"],
|
|
492
|
+
status_code=200,
|
|
493
|
+
responses={404: {}},
|
|
494
|
+
response_model=self.schema.for_no_data(),
|
|
495
|
+
include_in_schema=is_documented,
|
|
496
|
+
summary="Replace One",
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
return self
|
|
500
|
+
|
|
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
|
+
def with_replace_many_endpoint(
|
|
521
|
+
self,
|
|
522
|
+
is_documented: bool = True,
|
|
523
|
+
extract_user: Callable[..., Any] = no_user,
|
|
524
|
+
) -> Self:
|
|
525
|
+
self.router.add_api_route(
|
|
526
|
+
"/batch",
|
|
527
|
+
self.replace_many(
|
|
528
|
+
User=Annotated[
|
|
529
|
+
Any,
|
|
530
|
+
Depends(extract_user),
|
|
531
|
+
],
|
|
532
|
+
ParentId=Annotated[
|
|
533
|
+
str,
|
|
534
|
+
Path(alias=self.parent_id_alias, default_factory=str),
|
|
535
|
+
],
|
|
536
|
+
Collection=Annotated[
|
|
537
|
+
RawCollection,
|
|
538
|
+
Depends(self.schema.for_replace_many()),
|
|
539
|
+
],
|
|
540
|
+
),
|
|
541
|
+
methods=["PUT"],
|
|
542
|
+
status_code=200,
|
|
543
|
+
responses={},
|
|
544
|
+
response_model=self.schema.for_no_data(),
|
|
545
|
+
include_in_schema=is_documented,
|
|
546
|
+
summary="Replace Many",
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
return self
|
|
550
|
+
|
|
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
|
+
|
|
483
570
|
def with_delete_one_endpoint(
|
|
484
571
|
self,
|
|
485
572
|
is_documented: bool = True,
|
|
@@ -5,7 +5,6 @@ from typing import Any, Callable, Iterable, List
|
|
|
5
5
|
|
|
6
6
|
from pydantic import BaseModel, create_model
|
|
7
7
|
|
|
8
|
-
from apexdevkit.annotation import deprecated
|
|
9
8
|
from apexdevkit.http import JsonDict
|
|
10
9
|
from apexdevkit.testing import RestfulName
|
|
11
10
|
|
|
@@ -25,15 +24,6 @@ class SchemaFields(ABC):
|
|
|
25
24
|
pass
|
|
26
25
|
|
|
27
26
|
|
|
28
|
-
@deprecated("Use custom schema fields instead")
|
|
29
|
-
@dataclass
|
|
30
|
-
class DataclassFields(SchemaFields): # pragma: no cover
|
|
31
|
-
source: Any
|
|
32
|
-
|
|
33
|
-
def readable(self) -> JsonDict:
|
|
34
|
-
return JsonDict(self.source.__annotations__)
|
|
35
|
-
|
|
36
|
-
|
|
37
27
|
@dataclass
|
|
38
28
|
class RestfulSchema:
|
|
39
29
|
name: RestfulName
|
|
@@ -43,6 +33,7 @@ class RestfulSchema:
|
|
|
43
33
|
schema = self._schema_for("", self.fields.readable())
|
|
44
34
|
create_schema = self._schema_for("Create", self.fields.writable())
|
|
45
35
|
self._schema_for("Update", self.fields.editable())
|
|
36
|
+
replace_schema = self._schema_for("Replace", self.fields.readable())
|
|
46
37
|
update_many_item = self._schema_for(
|
|
47
38
|
"UpdateManyItem", self.fields.editable().merge(self.fields.id())
|
|
48
39
|
)
|
|
@@ -64,6 +55,9 @@ class RestfulSchema:
|
|
|
64
55
|
"UpdateMany",
|
|
65
56
|
JsonDict({self.name.plural: List[update_many_item]}),
|
|
66
57
|
)
|
|
58
|
+
self._schema_for(
|
|
59
|
+
"ReplaceMany", JsonDict({self.name.plural: List[replace_schema]})
|
|
60
|
+
)
|
|
67
61
|
|
|
68
62
|
def _schema_for(self, action: str, fields: dict[str, Any]) -> type[BaseModel]:
|
|
69
63
|
if action not in self.schemas:
|
|
@@ -139,3 +133,19 @@ class RestfulSchema:
|
|
|
139
133
|
return [dict(item) for item in request.model_dump()[self.name.plural]]
|
|
140
134
|
|
|
141
135
|
return _
|
|
136
|
+
|
|
137
|
+
def for_replace_one(self) -> Callable[[BaseModel], dict[str, Any]]:
|
|
138
|
+
schema = self.schemas["Replace"]
|
|
139
|
+
|
|
140
|
+
def _(request: schema) -> dict[str, Any]:
|
|
141
|
+
return request.model_dump()
|
|
142
|
+
|
|
143
|
+
return _
|
|
144
|
+
|
|
145
|
+
def for_replace_many(self) -> Callable[[BaseModel], Iterable[dict[str, Any]]]:
|
|
146
|
+
schema = self.schemas["ReplaceMany"]
|
|
147
|
+
|
|
148
|
+
def _(request: schema) -> Iterable[dict[str, Any]]:
|
|
149
|
+
return [dict(item) for item in request.model_dump()[self.name.plural]]
|
|
150
|
+
|
|
151
|
+
return _
|
|
@@ -2,8 +2,7 @@ from abc import ABC
|
|
|
2
2
|
from dataclasses import dataclass, field
|
|
3
3
|
from typing import Any, Dict, Generic, Iterable, Self, TypeVar
|
|
4
4
|
|
|
5
|
-
from apexdevkit.
|
|
6
|
-
from apexdevkit.formatter import DataclassFormatter, Formatter
|
|
5
|
+
from apexdevkit.formatter import Formatter
|
|
7
6
|
from apexdevkit.repository.interface import Repository
|
|
8
7
|
|
|
9
8
|
RawItem = dict[str, Any]
|
|
@@ -37,6 +36,12 @@ class RestfulService(ABC): # pragma: no cover
|
|
|
37
36
|
def update_many(self, items: RawCollectionWithId) -> RawCollection:
|
|
38
37
|
raise NotImplementedError(self.update_many.__name__)
|
|
39
38
|
|
|
39
|
+
def replace_one(self, item: RawItem) -> RawItem:
|
|
40
|
+
raise NotImplementedError(self.replace_one.__name__)
|
|
41
|
+
|
|
42
|
+
def replace_many(self, items: RawCollection) -> RawCollection:
|
|
43
|
+
raise NotImplementedError(self.replace_many.__name__)
|
|
44
|
+
|
|
40
45
|
def delete_one(self, item_id: str) -> None:
|
|
41
46
|
raise NotImplementedError(self.delete_one.__name__)
|
|
42
47
|
|
|
@@ -46,16 +51,9 @@ ItemT = TypeVar("ItemT")
|
|
|
46
51
|
|
|
47
52
|
@dataclass
|
|
48
53
|
class RestfulRepositoryBuilder(Generic[ItemT]):
|
|
49
|
-
|
|
50
|
-
formatter: Formatter[ItemT] | None = field(init=False, default=None)
|
|
54
|
+
formatter: Formatter[ItemT] = field(init=False)
|
|
51
55
|
repository: Repository[Any, ItemT] = field(init=False)
|
|
52
56
|
|
|
53
|
-
@deprecated("Pass formatter instead")
|
|
54
|
-
def with_resource(self, resource: type[ItemT]) -> Self:
|
|
55
|
-
self.resource = resource
|
|
56
|
-
|
|
57
|
-
return self
|
|
58
|
-
|
|
59
57
|
def with_formatter(self, formatter: Formatter[ItemT]) -> Self:
|
|
60
58
|
self.formatter = formatter
|
|
61
59
|
|
|
@@ -67,11 +65,6 @@ class RestfulRepositoryBuilder(Generic[ItemT]):
|
|
|
67
65
|
return self
|
|
68
66
|
|
|
69
67
|
def build(self) -> RestfulService:
|
|
70
|
-
if not self.formatter and self.resource:
|
|
71
|
-
self.with_formatter(DataclassFormatter(self.resource))
|
|
72
|
-
|
|
73
|
-
assert self.formatter, "Must provide either resource or formatter"
|
|
74
|
-
|
|
75
68
|
return _RestfulNestedRepository(self.formatter, self.repository)
|
|
76
69
|
|
|
77
70
|
|
|
@@ -117,5 +110,15 @@ class _RestfulNestedRepository(RestfulService, Generic[ItemT]):
|
|
|
117
110
|
|
|
118
111
|
return [self.formatter.dump(item) for item in updates]
|
|
119
112
|
|
|
113
|
+
def replace_one(self, item: RawItem) -> RawItem:
|
|
114
|
+
self.repository.update(self.formatter.load(item))
|
|
115
|
+
|
|
116
|
+
return item
|
|
117
|
+
|
|
118
|
+
def replace_many(self, items: RawCollection) -> RawCollection:
|
|
119
|
+
self.repository.update_many([self.formatter.load(item) for item in items])
|
|
120
|
+
|
|
121
|
+
return items
|
|
122
|
+
|
|
120
123
|
def delete_one(self, item_id: str) -> None:
|
|
121
124
|
self.repository.delete(item_id)
|
|
@@ -19,6 +19,9 @@ class RestResource:
|
|
|
19
19
|
def create_one(self) -> CreateOne:
|
|
20
20
|
return CreateOne(self.name, self.http)
|
|
21
21
|
|
|
22
|
+
def create_many(self) -> CreateMany:
|
|
23
|
+
return CreateMany(self.name, self.http)
|
|
24
|
+
|
|
22
25
|
def read_one(self) -> ReadOne:
|
|
23
26
|
return ReadOne(self.name, self.http)
|
|
24
27
|
|
|
@@ -31,8 +34,11 @@ class RestResource:
|
|
|
31
34
|
def update_many(self) -> UpdateMany:
|
|
32
35
|
return UpdateMany(self.name, self.http)
|
|
33
36
|
|
|
34
|
-
def
|
|
35
|
-
return
|
|
37
|
+
def replace_one(self) -> ReplaceOne:
|
|
38
|
+
return ReplaceOne(self.name, self.http)
|
|
39
|
+
|
|
40
|
+
def replace_many(self) -> ReplaceMany:
|
|
41
|
+
return ReplaceMany(self.name, self.http)
|
|
36
42
|
|
|
37
43
|
def delete_one(self) -> DeleteOne:
|
|
38
44
|
return DeleteOne(self.name, self.http)
|
|
@@ -182,6 +188,20 @@ class UpdateOne(RestRequest):
|
|
|
182
188
|
return self
|
|
183
189
|
|
|
184
190
|
|
|
191
|
+
@dataclass
|
|
192
|
+
class ReplaceOne(RestRequest):
|
|
193
|
+
data: JsonDict = field(init=False)
|
|
194
|
+
|
|
195
|
+
@cached_property
|
|
196
|
+
def response(self) -> httpx.Response:
|
|
197
|
+
return self.http.put(self.resource + "", json=dict(self.data))
|
|
198
|
+
|
|
199
|
+
def from_data(self, value: JsonDict) -> Self:
|
|
200
|
+
self.data = value
|
|
201
|
+
|
|
202
|
+
return self
|
|
203
|
+
|
|
204
|
+
|
|
185
205
|
@dataclass
|
|
186
206
|
class CreateMany(RestRequest):
|
|
187
207
|
data: list[JsonDict] = field(default_factory=list)
|
|
@@ -222,6 +242,26 @@ class UpdateMany(RestRequest):
|
|
|
222
242
|
return self.from_data(value)
|
|
223
243
|
|
|
224
244
|
|
|
245
|
+
@dataclass
|
|
246
|
+
class ReplaceMany(RestRequest):
|
|
247
|
+
data: list[JsonDict] = field(default_factory=list)
|
|
248
|
+
|
|
249
|
+
@cached_property
|
|
250
|
+
def response(self) -> httpx.Response:
|
|
251
|
+
return self.http.put(
|
|
252
|
+
self.resource + "batch",
|
|
253
|
+
json={self.resource.plural: [dict(data) for data in self.data]},
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def from_data(self, value: JsonDict) -> Self:
|
|
257
|
+
self.data.append(value)
|
|
258
|
+
|
|
259
|
+
return self
|
|
260
|
+
|
|
261
|
+
def and_data(self, value: JsonDict) -> Self:
|
|
262
|
+
return self.from_data(value)
|
|
263
|
+
|
|
264
|
+
|
|
225
265
|
@dataclass
|
|
226
266
|
class DeleteOne(RestRequest):
|
|
227
267
|
item_id: str = field(init=False)
|
|
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
|