apexdevkit 1.0.1__tar.gz → 1.1.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 (30) hide show
  1. {apexdevkit-1.0.1 → apexdevkit-1.1.1}/PKG-INFO +1 -1
  2. apexdevkit-1.1.1/apexdevkit/fastapi/router.py +251 -0
  3. apexdevkit-1.1.1/apexdevkit/fastapi/schema.py +116 -0
  4. apexdevkit-1.1.1/apexdevkit/fastapi/service.py +85 -0
  5. {apexdevkit-1.0.1 → apexdevkit-1.1.1}/pyproject.toml +2 -1
  6. {apexdevkit-1.0.1 → apexdevkit-1.1.1}/LICENSE +0 -0
  7. {apexdevkit-1.0.1 → apexdevkit-1.1.1}/README.md +0 -0
  8. {apexdevkit-1.0.1 → apexdevkit-1.1.1}/apexdevkit/__init__.py +0 -0
  9. {apexdevkit-1.0.1 → apexdevkit-1.1.1}/apexdevkit/annotation/__init__.py +0 -0
  10. {apexdevkit-1.0.1 → apexdevkit-1.1.1}/apexdevkit/annotation/deprecate.py +0 -0
  11. {apexdevkit-1.0.1 → apexdevkit-1.1.1}/apexdevkit/error.py +0 -0
  12. {apexdevkit-1.0.1 → apexdevkit-1.1.1}/apexdevkit/fastapi/__init__.py +0 -0
  13. {apexdevkit-1.0.1 → apexdevkit-1.1.1}/apexdevkit/fastapi/builder.py +0 -0
  14. {apexdevkit-1.0.1 → apexdevkit-1.1.1}/apexdevkit/fastapi/dependable.py +0 -0
  15. {apexdevkit-1.0.1 → apexdevkit-1.1.1}/apexdevkit/fastapi/docs.py +0 -0
  16. {apexdevkit-1.0.1 → apexdevkit-1.1.1}/apexdevkit/fastapi/response.py +0 -0
  17. {apexdevkit-1.0.1 → apexdevkit-1.1.1}/apexdevkit/http/__init__.py +0 -0
  18. {apexdevkit-1.0.1 → apexdevkit-1.1.1}/apexdevkit/http/fake.py +0 -0
  19. {apexdevkit-1.0.1 → apexdevkit-1.1.1}/apexdevkit/http/fluent.py +0 -0
  20. {apexdevkit-1.0.1 → apexdevkit-1.1.1}/apexdevkit/http/httpx.py +0 -0
  21. {apexdevkit-1.0.1 → apexdevkit-1.1.1}/apexdevkit/http/json.py +0 -0
  22. {apexdevkit-1.0.1 → apexdevkit-1.1.1}/apexdevkit/http/url.py +0 -0
  23. {apexdevkit-1.0.1 → apexdevkit-1.1.1}/apexdevkit/py.typed +0 -0
  24. {apexdevkit-1.0.1 → apexdevkit-1.1.1}/apexdevkit/repository/__init__.py +0 -0
  25. {apexdevkit-1.0.1 → apexdevkit-1.1.1}/apexdevkit/repository/connector.py +0 -0
  26. {apexdevkit-1.0.1 → apexdevkit-1.1.1}/apexdevkit/repository/database.py +0 -0
  27. {apexdevkit-1.0.1 → apexdevkit-1.1.1}/apexdevkit/repository/in_memory.py +0 -0
  28. {apexdevkit-1.0.1 → apexdevkit-1.1.1}/apexdevkit/testing/__init__.py +0 -0
  29. {apexdevkit-1.0.1 → apexdevkit-1.1.1}/apexdevkit/testing/database.py +0 -0
  30. {apexdevkit-1.0.1 → apexdevkit-1.1.1}/apexdevkit/testing/rest.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: apexdevkit
3
- Version: 1.0.1
3
+ Version: 1.1.1
4
4
  Summary: Apex Development Tools for python.
5
5
  Author: Apex Dev
6
6
  Author-email: dev@apex.ge
@@ -0,0 +1,251 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Annotated, Any, Iterable, Self, TypeVar
3
+
4
+ from fastapi import APIRouter, Depends
5
+ from fastapi.responses import JSONResponse
6
+
7
+ from apexdevkit.error import DoesNotExistError, ExistsError
8
+ from apexdevkit.fastapi.schema import RestfulSchema
9
+ from apexdevkit.fastapi.service import RestfulService, _RawCollection, _RawItem
10
+ from apexdevkit.testing import RestfulName
11
+
12
+ _Response = JSONResponse | dict[str, Any]
13
+
14
+
15
+ @dataclass
16
+ class RestfulResponse:
17
+ name: RestfulName
18
+
19
+ @classmethod
20
+ def from_dataclass(cls, value: type[Any]) -> "RestfulResponse":
21
+ return cls(name=RestfulName(value.__name__.lower()))
22
+
23
+ def _response(self, code: int, data: Any, error: str = "") -> dict[str, Any]:
24
+ content: dict[str, Any] = {"code": code, "status": "success"}
25
+
26
+ if error:
27
+ content["status"] = "fail"
28
+ content["error"] = {"message": error}
29
+
30
+ match data:
31
+ case None:
32
+ content["data"] = {}
33
+ case list():
34
+ content["data"] = {self.name.plural: data, "count": len(data)}
35
+ case _:
36
+ content["data"] = {self.name.singular: data}
37
+
38
+ return content
39
+
40
+ def ok(self) -> dict[str, Any]:
41
+ return self._response(200, data=None)
42
+
43
+ def not_found(self, e: DoesNotExistError) -> dict[str, Any]:
44
+ name = self.name.singular.capitalize()
45
+
46
+ return self._response(
47
+ 404,
48
+ data={"id": str(e.id)},
49
+ error=f"An item<{name}> with id<{e.id}> does not exist.",
50
+ )
51
+
52
+ def exists(self, e: ExistsError) -> dict[str, Any]:
53
+ name = self.name.singular.capitalize()
54
+
55
+ return self._response(
56
+ 409,
57
+ data={"id": str(e.id)},
58
+ error=f"An item<{name}> with the {e} already exists.",
59
+ )
60
+
61
+ def created_one(self, item: Any) -> dict[str, Any]:
62
+ return self._response(201, item)
63
+
64
+ def created_many(self, items: Iterable[Any]) -> dict[str, Any]:
65
+ return self._response(201, list(items))
66
+
67
+ def found_one(self, item: Any) -> dict[str, Any]:
68
+ return self._response(200, item)
69
+
70
+ def found_many(self, items: list[Any]) -> dict[str, Any]:
71
+ return self._response(200, items)
72
+
73
+
74
+ T = TypeVar("T")
75
+
76
+
77
+ @dataclass
78
+ class RestfulRouter:
79
+ schema: RestfulSchema = field(init=False)
80
+ response: RestfulResponse = field(init=False)
81
+ service: RestfulService = field(init=False)
82
+
83
+ router: APIRouter = field(init=False, default_factory=APIRouter)
84
+
85
+ @classmethod
86
+ def from_dataclass(cls, value: Any) -> "RestfulRouter":
87
+ return (
88
+ cls()
89
+ .with_schema(RestfulSchema.from_dataclass(value))
90
+ .with_response(RestfulResponse.from_dataclass(value))
91
+ )
92
+
93
+ def with_schema(self, value: RestfulSchema) -> Self:
94
+ self.schema = value
95
+
96
+ return self
97
+
98
+ def with_response(self, value: RestfulResponse) -> Self:
99
+ self.response = value
100
+
101
+ return self
102
+
103
+ def with_service(self, value: RestfulService) -> Self:
104
+ self.service = value
105
+
106
+ return self
107
+
108
+ def with_create_one_endpoint(self, is_documented: bool = True) -> Self:
109
+ schema = self.schema.for_create_one()
110
+
111
+ @self.router.post(
112
+ "",
113
+ status_code=201,
114
+ responses={409: {}},
115
+ response_model=self.schema.for_item(),
116
+ include_in_schema=is_documented,
117
+ )
118
+ def create_one(item: Annotated[_RawItem, Depends(schema)]) -> _Response:
119
+ try:
120
+ item = self.service.create_one(item)
121
+ except ExistsError as e:
122
+ return JSONResponse(self.response.exists(e), 409)
123
+
124
+ return self.response.created_one(item)
125
+
126
+ return self
127
+
128
+ def with_create_many_endpoint(self, is_documented: bool = True) -> Self:
129
+ schema = self.schema.for_create_many()
130
+
131
+ @self.router.post(
132
+ "/batch",
133
+ status_code=201,
134
+ responses={409: {}},
135
+ response_model=self.schema.for_collection(),
136
+ include_in_schema=is_documented,
137
+ )
138
+ def create_many(items: Annotated[_RawCollection, Depends(schema)]) -> _Response:
139
+ try:
140
+ return self.response.created_many(self.service.create_many(items))
141
+ except ExistsError as e:
142
+ return JSONResponse(self.response.exists(e), 409)
143
+
144
+ return self
145
+
146
+ def with_read_one_endpoint(self, is_documented: bool = True) -> Self:
147
+ schema = self.schema.for_item()
148
+
149
+ @self.router.get(
150
+ "/{item_id}",
151
+ status_code=200,
152
+ responses={404: {}},
153
+ response_model=schema,
154
+ include_in_schema=is_documented,
155
+ )
156
+ def read_one(item_id: str) -> _Response:
157
+ try:
158
+ return self.response.found_one(self.service.read_one(item_id))
159
+ except DoesNotExistError as e:
160
+ return JSONResponse(self.response.not_found(e), 404)
161
+
162
+ return self
163
+
164
+ def with_read_all_endpoint(self, is_documented: bool = True) -> Self:
165
+ schema = self.schema.for_collection()
166
+
167
+ @self.router.get(
168
+ "",
169
+ status_code=200,
170
+ responses={},
171
+ response_model=schema,
172
+ include_in_schema=is_documented,
173
+ )
174
+ def read_all() -> _Response:
175
+ return self.response.found_many(list(self.service.read_all()))
176
+
177
+ return self
178
+
179
+ def with_update_one_endpoint(self, is_documented: bool = True) -> Self:
180
+ schema = self.schema.for_update_one()
181
+
182
+ @self.router.patch(
183
+ "/{item_id}",
184
+ status_code=200,
185
+ responses={404: {}},
186
+ response_model=self.schema.for_no_data(),
187
+ include_in_schema=is_documented,
188
+ )
189
+ def update_one(
190
+ item_id: str,
191
+ updates: Annotated[_RawItem, Depends(schema)],
192
+ ) -> _Response:
193
+ try:
194
+ self.service.update_one(item_id, **updates)
195
+ except DoesNotExistError as e:
196
+ return JSONResponse(self.response.not_found(e), 404)
197
+
198
+ return self.response.ok()
199
+
200
+ return self
201
+
202
+ def with_update_many_endpoint(self, is_documented: bool = True) -> Self:
203
+ schema = self.schema.for_update_many()
204
+
205
+ @self.router.patch(
206
+ "",
207
+ status_code=200,
208
+ responses={},
209
+ response_model=self.schema.for_no_data(),
210
+ include_in_schema=is_documented,
211
+ )
212
+ def update_many(items: Annotated[_RawCollection, Depends(schema)]) -> _Response:
213
+ self.service.update_many(items)
214
+
215
+ return self.response.ok()
216
+
217
+ return self
218
+
219
+ def with_delete_one_endpoint(self, is_documented: bool = True) -> Self:
220
+ schema = self.schema.for_no_data()
221
+
222
+ @self.router.delete(
223
+ "/{item_id}",
224
+ status_code=200,
225
+ responses={404: {}},
226
+ response_model=schema,
227
+ include_in_schema=is_documented,
228
+ )
229
+ def delete_one(item_id: str) -> _Response:
230
+ try:
231
+ self.service.delete_one(item_id)
232
+ except DoesNotExistError as e:
233
+ return JSONResponse(self.response.not_found(e), 404)
234
+
235
+ return self.response.ok()
236
+
237
+ return self
238
+
239
+ def default(self) -> Self:
240
+ return (
241
+ self.with_create_one_endpoint()
242
+ .with_create_many_endpoint()
243
+ .with_read_one_endpoint()
244
+ .with_read_all_endpoint()
245
+ .with_update_one_endpoint()
246
+ .with_update_many_endpoint()
247
+ .with_delete_one_endpoint()
248
+ )
249
+
250
+ def build(self) -> APIRouter:
251
+ return self.router
@@ -0,0 +1,116 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Any, Callable, Iterable, List
3
+
4
+ from pydantic import BaseModel, create_model
5
+
6
+ from apexdevkit.http import JsonDict
7
+ from apexdevkit.testing import RestfulName
8
+
9
+
10
+ @dataclass
11
+ class RestfulSchema:
12
+ name: RestfulName
13
+ fields: JsonDict
14
+
15
+ schemas: dict[str, type[BaseModel]] = field(default_factory=dict)
16
+
17
+ def __post_init__(self) -> None:
18
+ schema = self._schema_for("", self.fields)
19
+ create_schema = self._schema_for("Create", self.fields.drop("id"))
20
+ self._schema_for("Update", self.fields.drop("id"))
21
+
22
+ self._schema_for(
23
+ "Item",
24
+ JsonDict({self.name.singular: schema}),
25
+ )
26
+ self._schema_for(
27
+ "Collection",
28
+ JsonDict({self.name.plural: List[schema]}).with_a(count=int),
29
+ )
30
+
31
+ self._schema_for(
32
+ "CreateMany",
33
+ JsonDict({self.name.plural: List[create_schema]}),
34
+ )
35
+ self._schema_for(
36
+ "UpdateMany",
37
+ JsonDict({self.name.plural: List[schema]}),
38
+ )
39
+
40
+ @classmethod
41
+ def from_dataclass(cls, value: Any) -> "RestfulSchema":
42
+ return cls(
43
+ name=RestfulName(value.__name__.lower()),
44
+ fields=JsonDict(value.__annotations__),
45
+ )
46
+
47
+ def for_no_data(self) -> type[BaseModel]:
48
+ class NoData(BaseModel):
49
+ pass
50
+
51
+ return self._schema_for(
52
+ "NoDataResponse",
53
+ JsonDict().with_a(status=str).and_a(code=int).and_a(data=NoData),
54
+ )
55
+
56
+ def for_item(self) -> type[BaseModel]:
57
+ return self._schema_for(
58
+ "ItemResponse",
59
+ JsonDict()
60
+ .with_a(status=str)
61
+ .and_a(code=int)
62
+ .and_a(data=self.schemas["Item"]),
63
+ )
64
+
65
+ def for_collection(self) -> type[BaseModel]:
66
+ return self._schema_for(
67
+ "CollectionResponse",
68
+ JsonDict()
69
+ .with_a(status=str)
70
+ .and_a(code=int)
71
+ .and_a(data=self.schemas["Collection"]),
72
+ )
73
+
74
+ def for_create_one(self) -> Callable[[BaseModel], dict[str, Any]]:
75
+ schema = self.schemas["Create"]
76
+
77
+ def _(request: schema) -> dict[str, Any]:
78
+ return request.model_dump()
79
+
80
+ return _
81
+
82
+ def for_create_many(self) -> Callable[[BaseModel], Iterable[dict[str, Any]]]:
83
+ schema = self.schemas["CreateMany"]
84
+
85
+ def _(request: schema) -> Iterable[dict[str, Any]]:
86
+ return [dict(item) for item in request.model_dump()[self.name.plural]]
87
+
88
+ return _
89
+
90
+ def for_update_one(self) -> Callable[[BaseModel], dict[str, Any]]:
91
+ schema = self.schemas["Update"]
92
+
93
+ def _(request: schema):
94
+ return request.model_dump()
95
+
96
+ return _
97
+
98
+ def for_update_many(self) -> Callable[[BaseModel], Iterable[dict[str, Any]]]:
99
+ schema = self.schemas["UpdateMany"]
100
+
101
+ def _(request: schema) -> Iterable[dict[str, Any]]:
102
+ return [dict(item) for item in request.model_dump()[self.name.plural]]
103
+
104
+ return _
105
+
106
+ def _schema_for(self, action: str, fields: dict[str, Any]) -> type[BaseModel]:
107
+ if action not in self.schemas:
108
+ self.schemas[action] = create_model(
109
+ self.name.singular.capitalize() + action,
110
+ **{
111
+ field_name: (field_type, ...)
112
+ for field_name, field_type in fields.items()
113
+ },
114
+ )
115
+
116
+ return self.schemas[action]
@@ -0,0 +1,85 @@
1
+ from dataclasses import asdict, dataclass, replace
2
+ from typing import Any, Iterable, Protocol
3
+
4
+ from apexdevkit.repository import InMemoryRepository
5
+
6
+ _RawItem = dict[str, Any]
7
+ _RawCollection = Iterable[_RawItem]
8
+
9
+
10
+ class RestfulService(Protocol):
11
+ def create_one(self, item: _RawItem) -> _RawItem:
12
+ pass
13
+
14
+ def create_many(self, items: _RawCollection) -> _RawCollection:
15
+ pass
16
+
17
+ def read_one(self, item_id: str) -> _RawItem:
18
+ pass
19
+
20
+ def read_all(self) -> _RawCollection:
21
+ pass
22
+
23
+ def update_one(self, item_id: str, **with_fields: Any) -> _RawItem:
24
+ pass
25
+
26
+ def update_many(self, items: _RawCollection) -> _RawCollection:
27
+ pass
28
+
29
+ def delete_one(self, item_id: str) -> None:
30
+ pass
31
+
32
+
33
+ def _as_raw_collection(value: Iterable[Any]) -> _RawCollection:
34
+ return [_as_raw_item(item) for item in value]
35
+
36
+
37
+ def _as_raw_item(value: Any) -> _RawItem:
38
+ return asdict(value)
39
+
40
+
41
+ @dataclass
42
+ class InMemoryRestfulService:
43
+ resource: type[Any]
44
+ repository: InMemoryRepository[Any]
45
+
46
+ def create_one(self, item: _RawItem) -> _RawItem:
47
+ result = self.resource(**item)
48
+
49
+ self.repository.create(result)
50
+
51
+ return _as_raw_item(result)
52
+
53
+ def create_many(self, items: _RawCollection) -> _RawCollection:
54
+ result = [self.resource(**fields) for fields in items]
55
+
56
+ self.repository.create_many(result)
57
+
58
+ return _as_raw_collection(result)
59
+
60
+ def read_one(self, item_id: str) -> _RawItem:
61
+ result = self.repository.read(item_id)
62
+
63
+ return _as_raw_item(result)
64
+
65
+ def read_all(self) -> _RawCollection:
66
+ result = self.repository
67
+
68
+ return _as_raw_collection(result)
69
+
70
+ def update_one(self, item_id: str, **with_fields: Any) -> _RawItem:
71
+ result = replace(self.repository.read(item_id), **with_fields)
72
+
73
+ self.repository.update(result)
74
+
75
+ return _as_raw_item(result)
76
+
77
+ def update_many(self, items: _RawCollection) -> _RawCollection:
78
+ result = [self.resource(**fields) for fields in items]
79
+
80
+ self.repository.update_many(result)
81
+
82
+ return _as_raw_collection(result)
83
+
84
+ def delete_one(self, item_id: str) -> None:
85
+ self.repository.delete(item_id)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "apexdevkit"
3
- version = "1.0.1"
3
+ version = "1.1.1"
4
4
  description = "Apex Development Tools for python."
5
5
  authors = ["Apex Dev <dev@apex.ge>"]
6
6
  readme = "README.md"
@@ -25,6 +25,7 @@ ruff = "*"
25
25
  python_version = "3.11"
26
26
  ignore_missing_imports = true
27
27
  strict = true
28
+ exclude = ["apexdevkit/fastapi/schema.py"]
28
29
 
29
30
  [tool.ruff]
30
31
  target-version = "py311"
File without changes
File without changes