apexdevkit 1.17.5__tar.gz → 1.17.7__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 (48) hide show
  1. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/PKG-INFO +1 -1
  2. apexdevkit-1.17.7/apexdevkit/repository/mssql.py +467 -0
  3. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/repository/sqlite.py +101 -16
  4. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/pyproject.toml +1 -1
  5. apexdevkit-1.17.5/apexdevkit/repository/mssql.py +0 -104
  6. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/LICENSE +0 -0
  7. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/README.md +0 -0
  8. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/__init__.py +0 -0
  9. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/annotation/__init__.py +0 -0
  10. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/annotation/deprecate.py +0 -0
  11. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/environment.py +0 -0
  12. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/error.py +0 -0
  13. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/fastapi/__init__.py +0 -0
  14. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/fastapi/builder.py +0 -0
  15. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/fastapi/dependable.py +0 -0
  16. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/fastapi/docs.py +0 -0
  17. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/fastapi/name.py +0 -0
  18. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/fastapi/request.py +0 -0
  19. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/fastapi/resource.py +0 -0
  20. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/fastapi/response.py +0 -0
  21. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/fastapi/router.py +0 -0
  22. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/fastapi/schema.py +0 -0
  23. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/fastapi/service.py +0 -0
  24. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/fluent.py +0 -0
  25. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/formatter.py +0 -0
  26. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/http/__init__.py +0 -0
  27. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/http/fake.py +0 -0
  28. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/http/fluent.py +0 -0
  29. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/http/httpx.py +0 -0
  30. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/http/json.py +0 -0
  31. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/http/url.py +0 -0
  32. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/key_fn.py +0 -0
  33. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/py.typed +0 -0
  34. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/repository/__init__.py +0 -0
  35. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/repository/base.py +0 -0
  36. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/repository/connector.py +0 -0
  37. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/repository/database.py +0 -0
  38. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/repository/decorator.py +0 -0
  39. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/repository/in_memory.py +0 -0
  40. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/repository/interface.py +0 -0
  41. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/repository/mongo.py +0 -0
  42. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/server.py +0 -0
  43. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/synchronization.py +0 -0
  44. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/testing/__init__.py +0 -0
  45. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/testing/database.py +0 -0
  46. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/testing/fake.py +0 -0
  47. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/testing/rest.py +0 -0
  48. {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/value.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: apexdevkit
3
- Version: 1.17.5
3
+ Version: 1.17.7
4
4
  Summary: Apex Development Tools for python.
5
5
  Author: Apex Dev
6
6
  Author-email: dev@apex.ge
@@ -0,0 +1,467 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Generic, Iterable, Iterator, TypeVar
5
+
6
+ from pymssql.exceptions import DatabaseError
7
+
8
+ from apexdevkit.error import DoesNotExistError, ExistsError
9
+ from apexdevkit.formatter import Formatter
10
+ from apexdevkit.repository import Database, DatabaseCommand, RepositoryBase
11
+
12
+ ItemT = TypeVar("ItemT")
13
+
14
+
15
+ @dataclass
16
+ class MsSqlRepository(RepositoryBase[ItemT]):
17
+ db: Database
18
+ table: SqlTable[ItemT]
19
+
20
+ def __iter__(self) -> Iterator[ItemT]:
21
+ for raw in self.db.execute(self.table.select_all()).fetch_all():
22
+ yield self.table.load(raw)
23
+
24
+ def __len__(self) -> int:
25
+ raw = self.db.execute(self.table.count_all()).fetch_one()
26
+
27
+ try:
28
+ return int(raw["n_items"])
29
+ except KeyError:
30
+ raise UnknownError(raw)
31
+
32
+ def delete(self, item_id: str) -> None:
33
+ self.db.execute(self.table.delete(item_id)).fetch_none()
34
+
35
+ def delete_all(self) -> None:
36
+ self.db.execute(self.table.delete_all()).fetch_none()
37
+
38
+ def create(self, item: ItemT) -> ItemT:
39
+ try:
40
+ return self.table.load(self.db.execute(self.table.insert(item)).fetch_one())
41
+ except DatabaseError as e:
42
+ e = MssqlException(e)
43
+
44
+ if e.is_duplication():
45
+ raise self.table.exists(item)
46
+
47
+ raise UnknownError(e.message)
48
+
49
+ def read(self, item_id: str) -> ItemT:
50
+ raw = self.db.execute(self.table.select(item_id)).fetch_one()
51
+
52
+ if not raw:
53
+ raise DoesNotExistError(item_id)
54
+
55
+ return self.table.load(raw)
56
+
57
+ def update(self, item: ItemT) -> None:
58
+ self.db.execute(self.table.update(item)).fetch_none()
59
+
60
+
61
+ class SqlTable(Generic[ItemT]): # pragma: no cover
62
+ def count_all(self) -> DatabaseCommand:
63
+ raise NotImplementedError
64
+
65
+ def insert(self, item: ItemT) -> DatabaseCommand:
66
+ raise NotImplementedError
67
+
68
+ def select(self, item_id: str) -> DatabaseCommand:
69
+ raise NotImplementedError
70
+
71
+ def select_all(self) -> DatabaseCommand:
72
+ raise NotImplementedError
73
+
74
+ def delete(self, item_id: str) -> DatabaseCommand:
75
+ raise NotImplementedError
76
+
77
+ def delete_all(self) -> DatabaseCommand:
78
+ raise NotImplementedError
79
+
80
+ def update(self, item: ItemT) -> DatabaseCommand:
81
+ raise NotImplementedError
82
+
83
+ def load(self, data: dict[str, Any]) -> ItemT:
84
+ raise NotImplementedError
85
+
86
+ def exists(self, duplicate: ItemT) -> ExistsError:
87
+ raise NotImplementedError
88
+
89
+
90
+ @dataclass
91
+ class SqlTableDecorator(Generic[ItemT]):
92
+ table: DefaultSqlTable[ItemT]
93
+
94
+ def count_all(self) -> DatabaseCommand:
95
+ return self.table.count_all()
96
+
97
+ def insert(self, item: ItemT) -> DatabaseCommand:
98
+ return self.table.insert(item)
99
+
100
+ def select(self, item_id: str) -> DatabaseCommand:
101
+ return self.table.select(item_id)
102
+
103
+ def select_all(self) -> DatabaseCommand:
104
+ return self.table.select_all()
105
+
106
+ def delete(self, item_id: str) -> DatabaseCommand:
107
+ return self.table.delete(item_id)
108
+
109
+ def delete_all(self) -> DatabaseCommand:
110
+ return self.table.delete_all()
111
+
112
+ def update(self, item: ItemT) -> DatabaseCommand:
113
+ return self.table.update(item)
114
+
115
+ def load(self, data: dict[str, Any]) -> ItemT:
116
+ return self.table.load(data)
117
+
118
+ def exists(self, duplicate: ItemT) -> ExistsError:
119
+ return self.table.exists(duplicate)
120
+
121
+
122
+ @dataclass
123
+ class MssqlException:
124
+ code: int
125
+ message: str
126
+
127
+ def __init__(self, e: DatabaseError):
128
+ self.code = e.args[0]
129
+ self.message = e.args[1].decode()
130
+
131
+ def is_duplication(self) -> bool:
132
+ return self.code in [2627, 70003]
133
+
134
+
135
+ @dataclass
136
+ class UnknownError(Exception):
137
+ raw: Any
138
+
139
+
140
+ @dataclass(frozen=True)
141
+ class MsSqlTableBuilder(Generic[ItemT]):
142
+ username: str | None = None
143
+ schema: str | None = None
144
+ table: str | None = None
145
+ formatter: Formatter[dict[str, Any], ItemT] | None = None
146
+ fields: list[MsSqlField] | None = None
147
+ ordering: list[str] | None = None
148
+ parent_field: str | None = None
149
+ parent_value: Any | None = None
150
+
151
+ def with_username(self, value: str) -> MsSqlTableBuilder[ItemT]:
152
+ return MsSqlTableBuilder[ItemT](
153
+ value,
154
+ self.schema,
155
+ self.table,
156
+ self.formatter,
157
+ self.fields,
158
+ self.ordering,
159
+ self.parent_field,
160
+ self.parent_value,
161
+ )
162
+
163
+ def with_schema(self, value: str) -> MsSqlTableBuilder[ItemT]:
164
+ return MsSqlTableBuilder[ItemT](
165
+ self.username,
166
+ value,
167
+ self.table,
168
+ self.formatter,
169
+ self.fields,
170
+ self.ordering,
171
+ self.parent_field,
172
+ self.parent_value,
173
+ )
174
+
175
+ def with_table(self, value: str) -> MsSqlTableBuilder[ItemT]:
176
+ return MsSqlTableBuilder[ItemT](
177
+ self.username,
178
+ self.schema,
179
+ value,
180
+ self.formatter,
181
+ self.fields,
182
+ self.ordering,
183
+ self.parent_field,
184
+ self.parent_value,
185
+ )
186
+
187
+ def with_formatter(
188
+ self, value: Formatter[dict[str, Any], ItemT]
189
+ ) -> MsSqlTableBuilder[ItemT]:
190
+ return MsSqlTableBuilder[ItemT](
191
+ self.username,
192
+ self.schema,
193
+ self.table,
194
+ value,
195
+ self.fields,
196
+ self.ordering,
197
+ self.parent_field,
198
+ self.parent_value,
199
+ )
200
+
201
+ def with_fields(self, fields: Iterable[str]) -> MsSqlTableBuilder[ItemT]:
202
+ return MsSqlTableBuilder[ItemT](
203
+ self.username,
204
+ self.schema,
205
+ self.table,
206
+ self.formatter,
207
+ [MsSqlField(field, field == "id") for field in list(fields)],
208
+ self.ordering,
209
+ self.parent_field,
210
+ self.parent_value,
211
+ )
212
+
213
+ def with_order_fields(self, ordering: Iterable[str]) -> MsSqlTableBuilder[ItemT]:
214
+ ordered = list(ordering)
215
+ assert self.fields is not None, "Set fields first."
216
+
217
+ for key in ordered:
218
+ if key not in [field.name for field in self.fields]:
219
+ raise ValueError("Missing fields in the table.")
220
+ return MsSqlTableBuilder[ItemT](
221
+ self.username,
222
+ self.schema,
223
+ self.table,
224
+ self.formatter,
225
+ self.fields,
226
+ ordered,
227
+ self.parent_field,
228
+ self.parent_value,
229
+ )
230
+
231
+ def with_id(self, identifier: str) -> MsSqlTableBuilder[ItemT]:
232
+ assert self.fields is not None, "Set fields first."
233
+ if identifier not in [field.name for field in self.fields]:
234
+ raise ValueError("Missing fields in the table.")
235
+
236
+ return MsSqlTableBuilder[ItemT](
237
+ self.username,
238
+ self.schema,
239
+ self.table,
240
+ self.formatter,
241
+ [MsSqlField(field.name, field.name == identifier) for field in self.fields],
242
+ self.ordering,
243
+ self.parent_field,
244
+ self.parent_value,
245
+ )
246
+
247
+ def with_parent(
248
+ self, parent_field: str, parent_value: Any
249
+ ) -> MsSqlTableBuilder[ItemT]:
250
+ assert self.fields is not None, "Set fields first."
251
+ if parent_field not in [field.name for field in self.fields]:
252
+ raise ValueError("Missing fields in the table.")
253
+
254
+ return MsSqlTableBuilder[ItemT](
255
+ self.username,
256
+ self.schema,
257
+ self.table,
258
+ self.formatter,
259
+ self.fields,
260
+ self.ordering,
261
+ parent_field,
262
+ parent_value,
263
+ )
264
+
265
+ def build(self) -> SqlTable[ItemT]:
266
+ if not self.schema or not self.table or not self.formatter or not self.fields:
267
+ raise ValueError("Cannot build sql table.")
268
+
269
+ return DefaultSqlTable(
270
+ self.username,
271
+ self.schema,
272
+ self.table,
273
+ self.formatter,
274
+ self.fields,
275
+ self.ordering,
276
+ self.parent_field,
277
+ self.parent_value,
278
+ )
279
+
280
+
281
+ @dataclass(frozen=True)
282
+ class DefaultSqlTable(SqlTable[ItemT]):
283
+ username: str | None
284
+ schema: str
285
+ table: str
286
+ formatter: Formatter[dict[str, Any], ItemT]
287
+ fields: list[MsSqlField]
288
+ ordering: list[str] | None
289
+ parent_key: str | None
290
+ parent_value: Any | None
291
+
292
+ def count_all(self) -> DatabaseCommand:
293
+ where_statement = ""
294
+ if self.parent_key is not None:
295
+ where_statement += (
296
+ "WHERE [" + self.parent_key + "] = " + self._parent_value_sql
297
+ )
298
+
299
+ return DatabaseCommand(f"""
300
+ {self._user_check}
301
+ SELECT count(*) AS n_items
302
+ FROM [{self.schema}].[{self.table}]
303
+ {where_statement}
304
+ REVERT
305
+ """)
306
+
307
+ def insert(self, item: ItemT) -> DatabaseCommand:
308
+ dumped = self.formatter.dump(item)
309
+ if self.parent_key is not None:
310
+ dumped[self.parent_key] = self.parent_value
311
+
312
+ columns = ", ".join(["[" + field.name + "]" for field in self.fields])
313
+ placeholders = ", ".join([f"%({key.name})s" for key in self.fields])
314
+ output = ", ".join(["INSERTED." + field.name for field in self.fields])
315
+
316
+ return DatabaseCommand(f"""
317
+ {self._user_check}
318
+ INSERT INTO [{self.schema}].[{self.table}] (
319
+ {columns}
320
+ ) OUTPUT
321
+ {output}
322
+ VALUES (
323
+ {placeholders}
324
+ )
325
+ REVERT
326
+ """).with_data(dumped)
327
+
328
+ def select(self, item_id: str) -> DatabaseCommand:
329
+ raw: dict[str, Any] = {self._id: item_id}
330
+ where_statement = f"WHERE [{self._id}] = %({self._id})s"
331
+ if self.parent_key is not None:
332
+ raw[self.parent_key] = self.parent_value
333
+ where_statement += (
334
+ " AND [" + self.parent_key + "] = %(" + self.parent_key + ")s"
335
+ )
336
+
337
+ columns = ", ".join(["[" + field.name + "]" for field in self.fields])
338
+
339
+ return DatabaseCommand(f"""
340
+ {self._user_check}
341
+ SELECT
342
+ {columns}
343
+ FROM [{self.schema}].[{self.table}]
344
+ {where_statement}
345
+ REVERT
346
+ """).with_data(raw)
347
+
348
+ def select_all(self) -> DatabaseCommand:
349
+ columns = ", ".join(["[" + field.name + "]" for field in self.fields])
350
+ where_statement = ""
351
+ if self.parent_key is not None:
352
+ where_statement += (
353
+ "WHERE [" + self.parent_key + "] = " + self._parent_value_sql
354
+ )
355
+
356
+ return DatabaseCommand(f"""
357
+ {self._user_check}
358
+ SELECT
359
+ {columns}
360
+ FROM [{self.schema}].[{self.table}]
361
+ {where_statement}
362
+ {self._order}
363
+ REVERT
364
+ """)
365
+
366
+ def update(self, item: ItemT) -> DatabaseCommand:
367
+ dumped = self.formatter.dump(item)
368
+ if self.parent_key is not None:
369
+ dumped[self.parent_key] = self.parent_value
370
+
371
+ where_statement = f"WHERE [{self._id}] = %({self._id})s"
372
+ if self.parent_key is not None:
373
+ where_statement += (
374
+ " AND [" + self.parent_key + "] = %(" + self.parent_key + ")s"
375
+ )
376
+
377
+ updates = ", ".join(
378
+ [
379
+ f"{field.name} = %({field.name})s"
380
+ for field in self.fields
381
+ if not field.is_id and field.name != self.parent_key
382
+ ]
383
+ )
384
+
385
+ return DatabaseCommand(f"""
386
+ {self._user_check}
387
+ UPDATE [{self.schema}].[{self.table}]
388
+ SET
389
+ {updates}
390
+ {where_statement}
391
+ REVERT
392
+ """).with_data(dumped)
393
+
394
+ def delete(self, item_id: str) -> DatabaseCommand:
395
+ raw: dict[str, Any] = {self._id: item_id}
396
+ where_statement = f"WHERE [{self._id}] = %({self._id})s"
397
+ if self.parent_key is not None:
398
+ raw[self.parent_key] = self.parent_value
399
+ where_statement += (
400
+ " AND [" + self.parent_key + "] = %(" + self.parent_key + ")s"
401
+ )
402
+
403
+ return DatabaseCommand(f"""
404
+ {self._user_check}
405
+ DELETE
406
+ FROM [{self.schema}].[{self.table}]
407
+ {where_statement}
408
+ REVERT
409
+ """).with_data(raw)
410
+
411
+ def delete_all(self) -> DatabaseCommand:
412
+ where_statement = ""
413
+ if self.parent_key is not None:
414
+ where_statement += (
415
+ "WHERE [" + self.parent_key + "] = " + self._parent_value_sql
416
+ )
417
+
418
+ return DatabaseCommand(f"""
419
+ {self._user_check}
420
+ DELETE
421
+ FROM [{self.schema}].[{self.table}]
422
+ {where_statement}
423
+ REVERT
424
+ """)
425
+
426
+ def load(self, data: dict[str, Any]) -> ItemT:
427
+ return self.formatter.load(data)
428
+
429
+ def exists(self, duplicate: ItemT) -> ExistsError:
430
+ raw = self.formatter.dump(duplicate)
431
+ return ExistsError(duplicate).with_duplicate(
432
+ lambda i: f"{self._id}<{raw[self._id]}>"
433
+ )
434
+
435
+ @property
436
+ def _id(self) -> str:
437
+ result = next((field for field in self.fields if field.is_id), None)
438
+ if result is None:
439
+ raise ValueError("Id field is required.")
440
+ return result.name
441
+
442
+ @property
443
+ def _user_check(self) -> str:
444
+ if self.username is not None:
445
+ return f"EXECUTE AS USER = '{self.username}'"
446
+ else:
447
+ return ""
448
+
449
+ @property
450
+ def _order(self) -> str:
451
+ if self.ordering is not None and len(self.ordering) > 0:
452
+ return "ORDER BY " + ", ".join(self.ordering)
453
+ else:
454
+ return ""
455
+
456
+ @property
457
+ def _parent_value_sql(self) -> str:
458
+ if isinstance(self.parent_value, str):
459
+ return "'" + self.parent_value + "'"
460
+ else:
461
+ return str(self.parent_value)
462
+
463
+
464
+ @dataclass(frozen=True)
465
+ class MsSqlField:
466
+ name: str
467
+ is_id: bool
@@ -103,20 +103,28 @@ class SqliteTableBuilder(Generic[ItemT]):
103
103
  table_name: str | None = None
104
104
  formatter: Formatter[dict[str, Any], ItemT] | None = None
105
105
  fields: list[SqliteField] | None = None
106
+ parent_field: str | None = None
107
+ parent_value: Any | None = None
106
108
 
107
109
  def with_name(self, value: str) -> SqliteTableBuilder[ItemT]:
108
- return SqliteTableBuilder[ItemT](value, self.formatter, self.fields)
110
+ return SqliteTableBuilder[ItemT](
111
+ value, self.formatter, self.fields, self.parent_field, self.parent_value
112
+ )
109
113
 
110
114
  def with_formatter(
111
115
  self, value: Formatter[dict[str, Any], ItemT]
112
116
  ) -> SqliteTableBuilder[ItemT]:
113
- return SqliteTableBuilder[ItemT](self.table_name, value, self.fields)
117
+ return SqliteTableBuilder[ItemT](
118
+ self.table_name, value, self.fields, self.parent_field, self.parent_value
119
+ )
114
120
 
115
121
  def with_fields(self, fields: Iterable[str]) -> SqliteTableBuilder[ItemT]:
116
122
  return SqliteTableBuilder[ItemT](
117
123
  self.table_name,
118
124
  self.formatter,
119
125
  [SqliteField(field, field == "id", False) for field in list(fields)],
126
+ self.parent_field,
127
+ self.parent_value,
120
128
  )
121
129
 
122
130
  def with_id(self, identifier: str) -> SqliteTableBuilder[ItemT]:
@@ -131,6 +139,8 @@ class SqliteTableBuilder(Generic[ItemT]):
131
139
  SqliteField(field.name, field.name == identifier, field.is_composite)
132
140
  for field in self.fields
133
141
  ],
142
+ self.parent_field,
143
+ self.parent_value,
134
144
  )
135
145
 
136
146
  def with_composite_key(
@@ -149,13 +159,36 @@ class SqliteTableBuilder(Generic[ItemT]):
149
159
  SqliteField(field.name, field.is_id, field.name in list(composites))
150
160
  for field in self.fields
151
161
  ],
162
+ self.parent_field,
163
+ self.parent_value,
164
+ )
165
+
166
+ def with_parent(
167
+ self, parent_field: str, parent_value: Any
168
+ ) -> SqliteTableBuilder[ItemT]:
169
+ assert self.fields is not None, "Set fields first."
170
+ if parent_field not in [field.name for field in self.fields]:
171
+ raise ValueError("Missing fields in the table.")
172
+
173
+ return SqliteTableBuilder[ItemT](
174
+ self.table_name,
175
+ self.formatter,
176
+ self.fields,
177
+ parent_field,
178
+ parent_value,
152
179
  )
153
180
 
154
181
  def build(self) -> SqlTable[ItemT]:
155
182
  if not self.table_name or not self.formatter or not self.fields:
156
183
  raise ValueError("Cannot build sql table.")
157
184
 
158
- return _DefaultSqlTable(self.table_name, self.formatter, self.fields)
185
+ return _DefaultSqlTable(
186
+ self.table_name,
187
+ self.formatter,
188
+ self.fields,
189
+ self.parent_field,
190
+ self.parent_value,
191
+ )
159
192
 
160
193
 
161
194
  @dataclass(frozen=True)
@@ -163,14 +196,27 @@ class _DefaultSqlTable(SqlTable[ItemT]):
163
196
  table_name: str
164
197
  formatter: Formatter[dict[str, Any], ItemT]
165
198
  fields: list[SqliteField]
199
+ parent_key: str | None
200
+ parent_value: Any | None
166
201
 
167
202
  def count_all(self) -> DatabaseCommand:
203
+ where_statement = ""
204
+ if self.parent_key is not None:
205
+ where_statement += (
206
+ "WHERE " + self.parent_key + " = " + self._parent_value_sql
207
+ )
208
+
168
209
  return DatabaseCommand(f"""
169
210
  SELECT count(*) as n_items
170
- FROM {self.table_name.capitalize()};
211
+ FROM {self.table_name.capitalize()}
212
+ {where_statement};
171
213
  """)
172
214
 
173
215
  def insert(self, item: ItemT) -> DatabaseCommand:
216
+ dumped = self.formatter.dump(item)
217
+ if self.parent_key is not None:
218
+ dumped[self.parent_key] = self.parent_value
219
+
174
220
  columns = ", ".join([field.name for field in self.fields])
175
221
  placeholders = ", ".join([f":{key.name}" for key in self.fields])
176
222
 
@@ -181,17 +227,23 @@ class _DefaultSqlTable(SqlTable[ItemT]):
181
227
  {placeholders}
182
228
  )
183
229
  RETURNING {columns};
184
- """).with_data(self.formatter.dump(item))
230
+ """).with_data(dumped)
185
231
 
186
232
  def select(self, item_id: str) -> DatabaseCommand:
233
+ raw: dict[str, Any] = {self._id: item_id}
234
+ where_statement = f"WHERE {self._id} = :{self._id}"
235
+ if self.parent_key is not None:
236
+ raw[self.parent_key] = self.parent_value
237
+ where_statement += " AND " + self.parent_key + " = :" + self.parent_key
238
+
187
239
  columns = ", ".join([field.name for field in self.fields])
188
240
 
189
241
  return DatabaseCommand(f"""
190
242
  SELECT
191
243
  {columns}
192
244
  FROM {self.table_name.capitalize()}
193
- WHERE {self._id} = :{self._id};
194
- """).with_data({self._id: item_id})
245
+ {where_statement};
246
+ """).with_data(raw)
195
247
 
196
248
  def select_duplicate(self, item: ItemT) -> DatabaseCommand:
197
249
  raw = self.formatter.dump(item)
@@ -207,20 +259,35 @@ class _DefaultSqlTable(SqlTable[ItemT]):
207
259
  """).with_data({key: raw[key] for key in raw if key in self._composite})
208
260
 
209
261
  def select_all(self) -> DatabaseCommand:
262
+ where_statement = ""
263
+ if self.parent_key is not None:
264
+ where_statement += (
265
+ "WHERE " + self.parent_key + " = " + self._parent_value_sql
266
+ )
267
+
210
268
  columns = ", ".join([field.name for field in self.fields])
211
269
 
212
270
  return DatabaseCommand(f"""
213
271
  SELECT
214
272
  {columns}
215
- FROM {self.table_name.capitalize()};
273
+ FROM {self.table_name.capitalize()}
274
+ {where_statement};
216
275
  """)
217
276
 
218
277
  def update(self, item: ItemT) -> DatabaseCommand:
278
+ dumped = self.formatter.dump(item)
279
+ if self.parent_key is not None:
280
+ dumped[self.parent_key] = self.parent_value
281
+
282
+ where_statement = f"WHERE {self._id} = :{self._id}"
283
+ if self.parent_key is not None:
284
+ where_statement += " AND " + self.parent_key + " = :" + self.parent_key
285
+
219
286
  updates = ", ".join(
220
287
  [
221
288
  f"{field.name} = :{field.name}"
222
289
  for field in self.fields
223
- if not field.is_id
290
+ if not field.is_id and field.name != self.parent_key
224
291
  ]
225
292
  )
226
293
 
@@ -228,22 +295,33 @@ class _DefaultSqlTable(SqlTable[ItemT]):
228
295
  UPDATE {self.table_name.capitalize()}
229
296
  SET
230
297
  {updates}
231
- WHERE
232
- {self._id} = :{self._id};
233
- """).with_data(self.formatter.dump(item))
298
+ {where_statement};
299
+ """).with_data(dumped)
234
300
 
235
301
  def delete(self, item_id: str) -> DatabaseCommand:
302
+ raw: dict[str, Any] = {self._id: item_id}
303
+ where_statement = f"WHERE {self._id} = :{self._id}"
304
+ if self.parent_key is not None:
305
+ raw[self.parent_key] = self.parent_value
306
+ where_statement += " AND " + self.parent_key + " = :" + self.parent_key
307
+
236
308
  return DatabaseCommand(f"""
237
309
  DELETE
238
310
  FROM {self.table_name.capitalize()}
239
- WHERE
240
- {self._id} = :{self._id};
241
- """).with_data({self._id: item_id})
311
+ {where_statement};
312
+ """).with_data(raw)
242
313
 
243
314
  def delete_all(self) -> DatabaseCommand:
315
+ where_statement = ""
316
+ if self.parent_key is not None:
317
+ where_statement += (
318
+ "WHERE " + self.parent_key + " = " + self._parent_value_sql
319
+ )
320
+
244
321
  return DatabaseCommand(f"""
245
322
  DELETE
246
- FROM {self.table_name.capitalize()};
323
+ FROM {self.table_name.capitalize()}
324
+ {where_statement};
247
325
  """)
248
326
 
249
327
  def load(self, data: dict[str, Any]) -> ItemT:
@@ -269,6 +347,13 @@ class _DefaultSqlTable(SqlTable[ItemT]):
269
347
  names = [field.name for field in self.fields if field.is_composite]
270
348
  return [self._id] if len(names) == 0 else names
271
349
 
350
+ @property
351
+ def _parent_value_sql(self) -> str:
352
+ if isinstance(self.parent_value, str):
353
+ return "'" + self.parent_value + "'"
354
+ else:
355
+ return str(self.parent_value)
356
+
272
357
 
273
358
  @dataclass(frozen=True)
274
359
  class SqliteField:
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "apexdevkit"
3
- version = "1.17.5"
3
+ version = "1.17.7"
4
4
  description = "Apex Development Tools for python."
5
5
  authors = ["Apex Dev <dev@apex.ge>"]
6
6
  readme = "README.md"
@@ -1,104 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from typing import Any, Generic, Iterator, TypeVar
5
-
6
- from pymssql.exceptions import DatabaseError
7
-
8
- from apexdevkit.error import DoesNotExistError, ExistsError
9
- from apexdevkit.repository import Database, DatabaseCommand, RepositoryBase
10
-
11
- ItemT = TypeVar("ItemT")
12
-
13
-
14
- @dataclass
15
- class MsSqlRepository(RepositoryBase[ItemT]):
16
- db: Database
17
- table: SqlTable[ItemT]
18
-
19
- def __iter__(self) -> Iterator[ItemT]:
20
- for raw in self.db.execute(self.table.select_all()).fetch_all():
21
- yield self.table.load(raw)
22
-
23
- def __len__(self) -> int:
24
- raw = self.db.execute(self.table.count_all()).fetch_one()
25
-
26
- try:
27
- return int(raw["n_items"])
28
- except KeyError:
29
- raise UnknownError(raw)
30
-
31
- def delete(self, item_id: str) -> None:
32
- self.db.execute(self.table.delete(item_id)).fetch_none()
33
-
34
- def delete_all(self) -> None:
35
- self.db.execute(self.table.delete_all()).fetch_none()
36
-
37
- def create(self, item: ItemT) -> ItemT:
38
- try:
39
- return self.table.load(self.db.execute(self.table.insert(item)).fetch_one())
40
- except DatabaseError as e:
41
- e = MssqlException(e)
42
-
43
- if e.is_duplication():
44
- raise self.table.exists(item)
45
-
46
- raise UnknownError(e.message)
47
-
48
- def read(self, item_id: str) -> ItemT:
49
- raw = self.db.execute(self.table.select(item_id)).fetch_one()
50
-
51
- if not raw:
52
- raise DoesNotExistError(item_id)
53
-
54
- return self.table.load(raw)
55
-
56
- def update(self, item: ItemT) -> None:
57
- self.db.execute(self.table.update(item)).fetch_none()
58
-
59
-
60
- class SqlTable(Generic[ItemT]): # pragma: no cover
61
- def count_all(self) -> DatabaseCommand:
62
- raise NotImplementedError
63
-
64
- def insert(self, item: ItemT) -> DatabaseCommand:
65
- raise NotImplementedError
66
-
67
- def select(self, item_id: str) -> DatabaseCommand:
68
- raise NotImplementedError
69
-
70
- def select_all(self) -> DatabaseCommand:
71
- raise NotImplementedError
72
-
73
- def delete(self, item_id: str) -> DatabaseCommand:
74
- raise NotImplementedError
75
-
76
- def delete_all(self) -> DatabaseCommand:
77
- raise NotImplementedError
78
-
79
- def update(self, item: ItemT) -> DatabaseCommand:
80
- raise NotImplementedError
81
-
82
- def load(self, data: dict[str, Any]) -> ItemT:
83
- raise NotImplementedError
84
-
85
- def exists(self, duplicate: ItemT) -> ExistsError:
86
- raise NotImplementedError
87
-
88
-
89
- @dataclass
90
- class MssqlException:
91
- code: int
92
- message: str
93
-
94
- def __init__(self, e: DatabaseError):
95
- self.code = e.args[0]
96
- self.message = e.args[1].decode()
97
-
98
- def is_duplication(self) -> bool:
99
- return self.code in [2627, 70003]
100
-
101
-
102
- @dataclass
103
- class UnknownError(Exception):
104
- raw: Any
File without changes
File without changes