apexdevkit 0.2.1__py3-none-any.whl

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.
@@ -0,0 +1,90 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, ContextManager, Iterable, Protocol, Self
5
+
6
+ _RawData = dict[str, Any]
7
+
8
+
9
+ @dataclass
10
+ class Database:
11
+ connector: Connector
12
+
13
+ command: DatabaseCommand = field(init=False)
14
+
15
+ def execute(self, command: DatabaseCommand) -> Self:
16
+ self.command = command
17
+
18
+ return self
19
+
20
+ def fetch_none(self) -> None:
21
+ with self.connector.connect() as connection:
22
+ cursor: Cursor = connection.cursor()
23
+ cursor.execute(self.command.value, self.command.payload)
24
+ cursor.close()
25
+
26
+ def fetch_one(self) -> _RawData:
27
+ with self.connector.connect() as connection:
28
+ cursor: Cursor = connection.cursor()
29
+ cursor.execute(self.command.value, self.command.payload)
30
+ raw = cursor.fetchone()
31
+ cursor.close()
32
+
33
+ return dict(raw or {})
34
+
35
+ def fetch_all(self) -> Iterable[_RawData]:
36
+ with self.connector.connect() as connection:
37
+ cursor: Cursor = connection.cursor()
38
+ cursor.execute(self.command.value, self.command.payload)
39
+ raw = cursor.fetchall()
40
+ cursor.close()
41
+
42
+ return list(raw)
43
+
44
+
45
+ class Connector(Protocol): # pragma: no cover
46
+ def connect(self) -> ContextManager[Connection]:
47
+ pass
48
+
49
+
50
+ class Connection(Protocol): # pragma: no cover
51
+ def cursor(self) -> Cursor:
52
+ pass
53
+
54
+
55
+ class Cursor(Protocol): # pragma: no cover
56
+ def execute(self, *args: Any, **kwargs: Any) -> Any:
57
+ pass
58
+
59
+ def executemany(self, *args: Any, **kwargs: Any) -> Any:
60
+ pass
61
+
62
+ def fetchone(self, *args: Any, **kwargs: Any) -> Any:
63
+ pass
64
+
65
+ def fetchall(self, *args: Any, **kwargs: Any) -> Any:
66
+ pass
67
+
68
+ def close(self) -> None:
69
+ pass
70
+
71
+
72
+ @dataclass
73
+ class DatabaseCommand:
74
+ value: str
75
+ payload: _RawData | list[_RawData] = field(init=False, default_factory=dict)
76
+
77
+ def with_data(self, value: _RawData | None = None, **fields: Any) -> Self:
78
+ assert isinstance(self.payload, dict)
79
+ self.payload.update(value or {})
80
+ self.payload.update(fields)
81
+
82
+ return self
83
+
84
+ def with_collection(self, value: list[_RawData]) -> Self:
85
+ self.payload = value
86
+
87
+ return self
88
+
89
+ def __str__(self) -> str: # pragma: no cover
90
+ return self.value
@@ -0,0 +1,106 @@
1
+ from copy import deepcopy
2
+ from dataclasses import dataclass, field
3
+ from typing import Any, Generic, Iterable, Iterator, Protocol, Self, TypeVar
4
+
5
+ from apexdevkit.error import Criteria, DoesNotExistError, ExistsError
6
+
7
+
8
+ class _Item(Protocol):
9
+ @property
10
+ def id(self) -> Any:
11
+ pass
12
+
13
+
14
+ ItemT = TypeVar("ItemT", bound=_Item)
15
+
16
+
17
+ class _Formatter(Protocol[ItemT]):
18
+ def load(self, raw: dict[str, Any]) -> ItemT:
19
+ pass
20
+
21
+ def dump(self, item: ItemT) -> dict[str, Any]:
22
+ pass
23
+
24
+
25
+ @dataclass
26
+ class InMemoryRepository(Generic[ItemT]):
27
+ formatter: _Formatter[ItemT]
28
+ items: dict[str, dict[str, Any]] = field(default_factory=dict)
29
+
30
+ _uniques: list[Criteria] = field(init=False, default_factory=list)
31
+ _search_by: list[str] = field(init=False, default_factory=list)
32
+
33
+ def __post_init__(self) -> None:
34
+ self._search_by = ["id", *self._search_by]
35
+
36
+ def with_searchable(self, attribute: str) -> Self:
37
+ self._search_by.append(attribute)
38
+
39
+ return self
40
+
41
+ def with_unique(self, criteria: Criteria) -> Self:
42
+ self._uniques.append(criteria)
43
+
44
+ return self
45
+
46
+ def with_seeded(self, *items: ItemT) -> Self:
47
+ for item in items:
48
+ self.create(item)
49
+ return self
50
+
51
+ def create_many(self, items: list[ItemT]) -> None:
52
+ for item in items:
53
+ self.create(item)
54
+
55
+ def create(self, item: ItemT) -> None:
56
+ self._ensure_does_not_exist(item)
57
+ self.items[str(item.id)] = deepcopy(self.formatter.dump(item))
58
+
59
+ def _ensure_does_not_exist(self, new: ItemT) -> None:
60
+ for existing in self:
61
+ error = ExistsError(existing)
62
+
63
+ for criteria in self._uniques:
64
+ if criteria(new) == criteria(existing):
65
+ error.with_duplicate(criteria)
66
+
67
+ error.fire()
68
+
69
+ assert str(new.id) not in self.items, f"Item with id<{new.id}> already exists"
70
+
71
+ def read(self, item_id: Any) -> ItemT:
72
+ for item in self:
73
+ for attribute in self._search_by:
74
+ if getattr(item, attribute) == item_id:
75
+ return item
76
+
77
+ raise DoesNotExistError(item_id)
78
+
79
+ def update(self, item: ItemT) -> None:
80
+ self.delete(item.id)
81
+ self.create(item)
82
+
83
+ def update_many(self, items: list[ItemT]) -> None:
84
+ for item in items:
85
+ self.update(item)
86
+
87
+ def delete(self, item_id: Any) -> None:
88
+ try:
89
+ del self.items[str(item_id)]
90
+ except KeyError:
91
+ raise DoesNotExistError(item_id)
92
+
93
+ def search(self, **kwargs: Any) -> Iterable[ItemT]:
94
+ items = []
95
+
96
+ for item in self.items.values():
97
+ if kwargs.items() <= item.items():
98
+ items.append(self.formatter.load(item))
99
+
100
+ return items
101
+
102
+ def __iter__(self) -> Iterator[ItemT]:
103
+ yield from [self.formatter.load(deepcopy(raw)) for raw in self.items.values()]
104
+
105
+ def __len__(self) -> int:
106
+ return len(self.items)
@@ -0,0 +1,9 @@
1
+ from apexdevkit.testing.database import FakeConnector
2
+ from apexdevkit.testing.rest import RestCollection, RestfulName, RestResource
3
+
4
+ __all__ = [
5
+ "FakeConnector",
6
+ "RestCollection",
7
+ "RestfulName",
8
+ "RestResource",
9
+ ]
@@ -0,0 +1,42 @@
1
+ from contextlib import nullcontext
2
+ from dataclasses import dataclass, field
3
+ from typing import Any, ContextManager, Self
4
+
5
+ from apexdevkit.repository import DatabaseCommand
6
+
7
+
8
+ @dataclass
9
+ class FakeConnector:
10
+ commands: list[tuple[str, Any]] = field(default_factory=list)
11
+ results: list[Any] = field(init=False, default_factory=list)
12
+
13
+ def with_result(self, values: Any) -> Self:
14
+ self.results = [values, *self.results]
15
+
16
+ return self
17
+
18
+ def execute(self, command: str, data: Any) -> None:
19
+ self.commands.append((command, data))
20
+
21
+ def executemany(self, command: str, data_list: list[Any]) -> None:
22
+ self.commands.extend([(command, data) for data in data_list])
23
+
24
+ def fetchone(self) -> dict[str, Any]:
25
+ return self.results.pop() # type: ignore
26
+
27
+ def fetchall(self) -> list[dict[str, Any]]:
28
+ return self.results.pop() # type: ignore
29
+
30
+ def connect(self) -> ContextManager[Self]:
31
+ return nullcontext(self)
32
+
33
+ def cursor(self) -> Self:
34
+ return self
35
+
36
+ def close(self) -> None:
37
+ pass
38
+
39
+ def assert_contains(self, command: DatabaseCommand, at_index: int = 0) -> None:
40
+ query, data = self.commands[at_index]
41
+
42
+ assert command == DatabaseCommand(query).with_data(data)
@@ -0,0 +1,310 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import abstractmethod
4
+ from dataclasses import dataclass, field
5
+ from functools import cached_property
6
+ from typing import Any, Iterable, Self
7
+ from uuid import UUID
8
+ from warnings import warn
9
+
10
+ import httpx
11
+ from fastapi.testclient import TestClient
12
+
13
+ from apexdevkit.http import HttpUrl, JsonDict
14
+
15
+
16
+ @dataclass
17
+ class RestResource:
18
+ http: TestClient
19
+ name: RestfulName
20
+
21
+ def create_one(self) -> CreateOne:
22
+ return CreateOne(self.name, self.http)
23
+
24
+ def read_one(self) -> ReadOne:
25
+ return ReadOne(self.name, self.http)
26
+
27
+ def read_all(self) -> ReadAll:
28
+ return ReadAll(self.name, self.http)
29
+
30
+ def update_one(self) -> UpdateOne:
31
+ return UpdateOne(self.name, self.http)
32
+
33
+ def update_many(self) -> UpdateMany:
34
+ return UpdateMany(self.name, self.http)
35
+
36
+ def create_many(self) -> CreateMany:
37
+ return CreateMany(self.name, self.http)
38
+
39
+ def delete_one(self) -> DeleteOne:
40
+ return DeleteOne(self.name, self.http)
41
+
42
+
43
+ @dataclass
44
+ class RestCollection(RestResource):
45
+ def sub_resource(self, name: str) -> RestItem:
46
+ return RestItem(
47
+ TestClient(
48
+ self.http.app,
49
+ base_url=HttpUrl(str(self.http.base_url)) + self.name.plural,
50
+ ),
51
+ RestfulName(name),
52
+ )
53
+
54
+
55
+ @dataclass
56
+ class RestItem(RestResource):
57
+ def sub_resource(self, name: str) -> RestItem:
58
+ return RestItem(
59
+ TestClient(
60
+ self.http.app,
61
+ base_url=HttpUrl(str(self.http.base_url)) + self.name.singular,
62
+ ),
63
+ RestfulName(name),
64
+ )
65
+
66
+
67
+ @dataclass
68
+ class RestfulName:
69
+ singular: str
70
+
71
+ plural: str = ""
72
+
73
+ def __post_init__(self) -> None:
74
+ self.plural = self.plural or as_plural(self.singular)
75
+
76
+ def __add__(self, other: str) -> str:
77
+ return HttpUrl(self.plural) + other
78
+
79
+
80
+ def as_plural(singular: str) -> str:
81
+ if singular.endswith("y"):
82
+ return singular[:-1] + "ies"
83
+
84
+ return singular + "s"
85
+
86
+
87
+ @dataclass
88
+ class RestRequest:
89
+ resource: RestfulName
90
+ http: TestClient
91
+
92
+ @abstractmethod
93
+ @cached_property
94
+ def response(self) -> httpx.Response: # pragma: no cover
95
+ pass
96
+
97
+ def unpack(self) -> JsonDict:
98
+ return JsonDict(self.response.json()["data"][self.resource.singular])
99
+
100
+ def unpack_many(self) -> Iterable[JsonDict]:
101
+ items = self.response.json()["data"][self.resource.plural]
102
+
103
+ return [JsonDict(item) for item in items]
104
+
105
+ def ensure(self) -> RestResponse:
106
+ return RestResponse(
107
+ resource=self.resource,
108
+ json=JsonDict(self.response.json()),
109
+ http_code=self.response.status_code,
110
+ )
111
+
112
+
113
+ @dataclass
114
+ class CreateOne(RestRequest):
115
+ data: JsonDict = field(init=False)
116
+
117
+ def from_data(self, value: JsonDict) -> Self:
118
+ self.data = value
119
+
120
+ return self
121
+
122
+ @cached_property
123
+ def response(self) -> httpx.Response:
124
+ return self.http.post(self.resource + "", json=dict(self.data))
125
+
126
+
127
+ @dataclass
128
+ class ReadOne(RestRequest):
129
+ item_id: str | UUID = field(init=False)
130
+
131
+ @cached_property
132
+ def response(self) -> httpx.Response:
133
+ return self.http.get(self.resource + str(self.item_id))
134
+
135
+ def with_id(self, value: str | UUID) -> Self:
136
+ self.item_id = str(value)
137
+
138
+ return self
139
+
140
+
141
+ @dataclass
142
+ class ReadAll(RestRequest):
143
+ params: dict[str, Any] = field(init=False, default_factory=dict)
144
+
145
+ @cached_property
146
+ def response(self) -> httpx.Response:
147
+ return self.http.get(self.resource + "", params=self.params)
148
+
149
+ def with_params(self, **kwargs: Any) -> Self:
150
+ self.params = {**kwargs}
151
+
152
+ return self
153
+
154
+
155
+ @dataclass
156
+ class UpdateOne(RestRequest):
157
+ item_id: str | UUID = field(init=False)
158
+ data: JsonDict = field(init=False)
159
+
160
+ @cached_property
161
+ def response(self) -> httpx.Response:
162
+ return self.http.patch(self.resource + str(self.item_id), json=dict(self.data))
163
+
164
+ def with_id(self, value: str | UUID) -> Self:
165
+ self.item_id = str(value)
166
+
167
+ return self
168
+
169
+ def and_data(self, value: JsonDict) -> Self:
170
+ self.data = value
171
+
172
+ return self
173
+
174
+
175
+ @dataclass
176
+ class CreateMany(RestRequest):
177
+ data: list[JsonDict] = field(default_factory=list)
178
+
179
+ @cached_property
180
+ def response(self) -> httpx.Response:
181
+ return self.http.post(
182
+ self.resource + "batch",
183
+ json={self.resource.plural: [dict(data) for data in self.data]},
184
+ )
185
+
186
+ def from_data(self, value: JsonDict) -> Self:
187
+ self.data.append(value)
188
+
189
+ return self
190
+
191
+ def and_data(self, value: JsonDict) -> Self:
192
+ return self.from_data(value)
193
+
194
+
195
+ @dataclass
196
+ class UpdateMany(RestRequest):
197
+ data: list[JsonDict] = field(default_factory=list)
198
+
199
+ @cached_property
200
+ def response(self) -> httpx.Response:
201
+ return self.http.patch(
202
+ self.resource + "",
203
+ json={self.resource.plural: [dict(data) for data in self.data]},
204
+ )
205
+
206
+ def from_data(self, value: JsonDict) -> Self:
207
+ self.data.append(value)
208
+
209
+ return self
210
+
211
+ def and_data(self, value: JsonDict) -> Self:
212
+ return self.from_data(value)
213
+
214
+
215
+ @dataclass
216
+ class DeleteOne(RestRequest):
217
+ item_id: str | UUID = field(init=False)
218
+
219
+ @cached_property
220
+ def response(self) -> httpx.Response:
221
+ return self.http.delete(self.resource + str(self.item_id))
222
+
223
+ def with_id(self, value: str | UUID) -> Self:
224
+ self.item_id = str(value)
225
+
226
+ return self
227
+
228
+
229
+ @dataclass
230
+ class RestResponse:
231
+ resource: RestfulName
232
+ json: JsonDict
233
+ http_code: int
234
+
235
+ def fail(self) -> Self:
236
+ return self.with_status("fail")
237
+
238
+ def success(self) -> Self:
239
+ return self.with_status("success")
240
+
241
+ def with_status(self, value: str) -> Self:
242
+ assert self.json.value_of("status").to(str) == value
243
+
244
+ return self
245
+
246
+ def with_code(self, value: int) -> Self:
247
+ assert self.http_code == value
248
+ assert self.json.value_of("code").to(int) == value
249
+
250
+ return self
251
+
252
+ def message(self, value: str) -> Self:
253
+ return self.with_message(value)
254
+
255
+ def and_message(self, value: str) -> Self:
256
+ return self.with_message(value)
257
+
258
+ def with_message(self, value: str) -> Self:
259
+ assert self.json.value_of("error").to(dict) == {"message": value}
260
+
261
+ return self
262
+
263
+ def and_data(self, *values: JsonDict) -> Self: # pragma: no cover
264
+ warn(
265
+ (
266
+ "The 'and_data' method is deprecated. "
267
+ "Please use 'and_item' or 'and_collection' instead."
268
+ )
269
+ )
270
+ if len(values) == 1:
271
+ return self.with_item(values[0])
272
+
273
+ return self.with_collection(list(values))
274
+
275
+ def and_item(self, value: Any) -> Self:
276
+ return self.with_item(value)
277
+
278
+ def with_item(self, value: Any) -> Self:
279
+ return self.with_data(
280
+ **{
281
+ self.resource.singular: dict(value)
282
+ if isinstance(value, JsonDict)
283
+ else value
284
+ }
285
+ )
286
+
287
+ def and_collection(self, value: list[Any]) -> Self:
288
+ return self.with_collection(value)
289
+
290
+ def with_collection(self, values: list[Any]) -> Self:
291
+ return self.with_data(
292
+ **{
293
+ self.resource.plural: [
294
+ dict(value) if isinstance(value, JsonDict) else value
295
+ for value in values
296
+ ],
297
+ },
298
+ count=len(values),
299
+ )
300
+
301
+ def and_no_data(self) -> Self:
302
+ return self.no_data()
303
+
304
+ def no_data(self) -> Self:
305
+ return self.with_data()
306
+
307
+ def with_data(self, **kwargs: Any) -> Self:
308
+ assert self.json.value_of("data").to(dict) == {**kwargs}
309
+
310
+ return self