python-flashapi 0.1.0__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.
- flashapi/__init__.py +7 -0
- flashapi/adapters/__init__.py +0 -0
- flashapi/adapters/base.py +12 -0
- flashapi/adapters/django.py +165 -0
- flashapi/adapters/fastapi.py +293 -0
- flashapi/adapters/flask.py +232 -0
- flashapi/core/__init__.py +3 -0
- flashapi/core/custom_routes.py +261 -0
- flashapi/core/pluralize.py +80 -0
- flashapi/core/relations.py +61 -0
- flashapi/core/response.py +33 -0
- flashapi/core/schema.py +83 -0
- flashapi/django.py +5 -0
- flashapi/docs/__init__.py +0 -0
- flashapi/docs/openapi.py +223 -0
- flashapi/fastapi.py +5 -0
- flashapi/features/__init__.py +6 -0
- flashapi/features/filtering.py +33 -0
- flashapi/features/pagination.py +20 -0
- flashapi/features/search.py +25 -0
- flashapi/features/sorting.py +21 -0
- flashapi/flask.py +5 -0
- flashapi/inspectors/__init__.py +3 -0
- flashapi/inspectors/base.py +11 -0
- flashapi/inspectors/dataclass.py +39 -0
- flashapi/inspectors/detect.py +48 -0
- flashapi/inspectors/django.py +83 -0
- flashapi/inspectors/pydantic.py +84 -0
- flashapi/inspectors/sqlalchemy.py +75 -0
- flashapi/py.typed +0 -0
- flashapi/storage/__init__.py +4 -0
- flashapi/storage/auto.py +106 -0
- flashapi/storage/base.py +26 -0
- flashapi/storage/orm.py +85 -0
- flashapi/storage/sqlalchemy.py +137 -0
- python_flashapi-0.1.0.dist-info/METADATA +259 -0
- python_flashapi-0.1.0.dist-info/RECORD +39 -0
- python_flashapi-0.1.0.dist-info/WHEEL +4 -0
- python_flashapi-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import date, datetime, time
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
from typing import Any, get_origin, get_args
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from flashapi.core.schema import FieldSchema, FieldType, ModelSchema
|
|
9
|
+
from flashapi.core.pluralize import pluralize
|
|
10
|
+
from flashapi.inspectors.base import Inspector
|
|
11
|
+
|
|
12
|
+
TYPE_MAP: dict[type, FieldType] = {
|
|
13
|
+
str: FieldType.STRING,
|
|
14
|
+
int: FieldType.INTEGER,
|
|
15
|
+
float: FieldType.FLOAT,
|
|
16
|
+
bool: FieldType.BOOLEAN,
|
|
17
|
+
datetime: FieldType.DATETIME,
|
|
18
|
+
date: FieldType.DATE,
|
|
19
|
+
time: FieldType.TIME,
|
|
20
|
+
UUID: FieldType.UUID,
|
|
21
|
+
Decimal: FieldType.FLOAT,
|
|
22
|
+
bytes: FieldType.BINARY,
|
|
23
|
+
dict: FieldType.JSON,
|
|
24
|
+
list: FieldType.JSON,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PydanticInspector(Inspector):
|
|
29
|
+
def inspect(self, model_class: type, plural: str | None = None) -> ModelSchema:
|
|
30
|
+
from pydantic import BaseModel
|
|
31
|
+
|
|
32
|
+
if not issubclass(model_class, BaseModel):
|
|
33
|
+
raise TypeError(f"{model_class} is not a Pydantic model")
|
|
34
|
+
|
|
35
|
+
fields: list[FieldSchema] = []
|
|
36
|
+
fields.append(FieldSchema(name="id", type=FieldType.INTEGER, required=False, primary_key=True, auto_generated=True))
|
|
37
|
+
|
|
38
|
+
for name, field_info in model_class.model_fields.items():
|
|
39
|
+
field_type = self._resolve_type(field_info.annotation)
|
|
40
|
+
constraints = self._extract_constraints(field_info)
|
|
41
|
+
required = field_info.is_required()
|
|
42
|
+
default = field_info.default if not required else None
|
|
43
|
+
|
|
44
|
+
fields.append(FieldSchema(
|
|
45
|
+
name=name,
|
|
46
|
+
type=field_type,
|
|
47
|
+
required=required,
|
|
48
|
+
default=default,
|
|
49
|
+
constraints=constraints,
|
|
50
|
+
))
|
|
51
|
+
|
|
52
|
+
model_name = model_class.__name__
|
|
53
|
+
plural_name = plural or pluralize(model_name)
|
|
54
|
+
|
|
55
|
+
return ModelSchema(name=model_name, plural=plural_name, fields=fields)
|
|
56
|
+
|
|
57
|
+
def _resolve_type(self, annotation: Any) -> FieldType:
|
|
58
|
+
if annotation is None:
|
|
59
|
+
return FieldType.STRING
|
|
60
|
+
|
|
61
|
+
origin = get_origin(annotation)
|
|
62
|
+
if origin is not None:
|
|
63
|
+
args = get_args(annotation)
|
|
64
|
+
non_none = [a for a in args if a is not type(None)]
|
|
65
|
+
if non_none:
|
|
66
|
+
annotation = non_none[0]
|
|
67
|
+
else:
|
|
68
|
+
return FieldType.STRING
|
|
69
|
+
|
|
70
|
+
return TYPE_MAP.get(annotation, FieldType.STRING)
|
|
71
|
+
|
|
72
|
+
def _extract_constraints(self, field_info: Any) -> dict:
|
|
73
|
+
constraints = {}
|
|
74
|
+
metadata = getattr(field_info, "metadata", [])
|
|
75
|
+
for m in metadata:
|
|
76
|
+
if hasattr(m, "max_length"):
|
|
77
|
+
constraints["max_length"] = m.max_length
|
|
78
|
+
if hasattr(m, "min_length"):
|
|
79
|
+
constraints["min_length"] = m.min_length
|
|
80
|
+
if hasattr(m, "ge"):
|
|
81
|
+
constraints["min_value"] = m.ge
|
|
82
|
+
if hasattr(m, "le"):
|
|
83
|
+
constraints["max_value"] = m.le
|
|
84
|
+
return constraints
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from flashapi.core.schema import FieldSchema, FieldType, ModelSchema, RelationSchema
|
|
4
|
+
from flashapi.core.pluralize import pluralize
|
|
5
|
+
from flashapi.inspectors.base import Inspector
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _map_sa_type(col_type) -> FieldType:
|
|
9
|
+
type_name = type(col_type).__name__.upper()
|
|
10
|
+
mapping = {
|
|
11
|
+
"VARCHAR": FieldType.STRING,
|
|
12
|
+
"STRING": FieldType.STRING,
|
|
13
|
+
"TEXT": FieldType.TEXT,
|
|
14
|
+
"INTEGER": FieldType.INTEGER,
|
|
15
|
+
"BIGINTEGER": FieldType.INTEGER,
|
|
16
|
+
"SMALLINTEGER": FieldType.INTEGER,
|
|
17
|
+
"FLOAT": FieldType.FLOAT,
|
|
18
|
+
"NUMERIC": FieldType.FLOAT,
|
|
19
|
+
"BOOLEAN": FieldType.BOOLEAN,
|
|
20
|
+
"DATE": FieldType.DATE,
|
|
21
|
+
"DATETIME": FieldType.DATETIME,
|
|
22
|
+
"TIME": FieldType.TIME,
|
|
23
|
+
"UUID": FieldType.UUID,
|
|
24
|
+
"JSON": FieldType.JSON,
|
|
25
|
+
"BLOB": FieldType.BINARY,
|
|
26
|
+
"LARGEBINARY": FieldType.BINARY,
|
|
27
|
+
}
|
|
28
|
+
return mapping.get(type_name, FieldType.STRING)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SQLAlchemyInspector(Inspector):
|
|
32
|
+
def inspect(self, model_class: type, plural: str | None = None) -> ModelSchema:
|
|
33
|
+
table = model_class.__table__
|
|
34
|
+
fields: list[FieldSchema] = []
|
|
35
|
+
|
|
36
|
+
for col in table.columns:
|
|
37
|
+
field_type = _map_sa_type(col.type)
|
|
38
|
+
constraints = {}
|
|
39
|
+
|
|
40
|
+
if hasattr(col.type, "length") and col.type.length:
|
|
41
|
+
constraints["max_length"] = col.type.length
|
|
42
|
+
|
|
43
|
+
relation = None
|
|
44
|
+
if col.foreign_keys:
|
|
45
|
+
fk = next(iter(col.foreign_keys))
|
|
46
|
+
target_table = fk.column.table.name
|
|
47
|
+
relation = RelationSchema(type="many_to_one", target=target_table)
|
|
48
|
+
|
|
49
|
+
has_default = col.default is not None or col.server_default is not None
|
|
50
|
+
is_auto_int = (
|
|
51
|
+
col.primary_key
|
|
52
|
+
and col.autoincrement is not False
|
|
53
|
+
and field_type == FieldType.INTEGER
|
|
54
|
+
)
|
|
55
|
+
auto_generated = is_auto_int or (col.primary_key and has_default)
|
|
56
|
+
|
|
57
|
+
default_value = None
|
|
58
|
+
if col.default and not callable(getattr(col.default, "arg", None)):
|
|
59
|
+
default_value = col.default.arg
|
|
60
|
+
|
|
61
|
+
fields.append(FieldSchema(
|
|
62
|
+
name=col.name,
|
|
63
|
+
type=field_type,
|
|
64
|
+
required=not col.nullable and not col.primary_key and not has_default,
|
|
65
|
+
default=default_value,
|
|
66
|
+
constraints=constraints,
|
|
67
|
+
primary_key=col.primary_key,
|
|
68
|
+
auto_generated=auto_generated,
|
|
69
|
+
relation=relation,
|
|
70
|
+
))
|
|
71
|
+
|
|
72
|
+
model_name = model_class.__name__
|
|
73
|
+
plural_name = plural or pluralize(model_name)
|
|
74
|
+
|
|
75
|
+
return ModelSchema(name=model_name, plural=plural_name, fields=fields)
|
flashapi/py.typed
ADDED
|
File without changes
|
flashapi/storage/auto.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sqlite3
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from flashapi.core.schema import FieldType, ModelSchema
|
|
7
|
+
from flashapi.storage.base import Storage
|
|
8
|
+
|
|
9
|
+
FIELD_TYPE_TO_SQL = {
|
|
10
|
+
FieldType.STRING: "TEXT",
|
|
11
|
+
FieldType.INTEGER: "INTEGER",
|
|
12
|
+
FieldType.FLOAT: "REAL",
|
|
13
|
+
FieldType.BOOLEAN: "INTEGER",
|
|
14
|
+
FieldType.DATE: "TEXT",
|
|
15
|
+
FieldType.DATETIME: "TEXT",
|
|
16
|
+
FieldType.TIME: "TEXT",
|
|
17
|
+
FieldType.UUID: "TEXT",
|
|
18
|
+
FieldType.JSON: "TEXT",
|
|
19
|
+
FieldType.TEXT: "TEXT",
|
|
20
|
+
FieldType.BINARY: "BLOB",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _validate_identifier(name: str) -> str:
|
|
25
|
+
"""Validate and quote a SQL identifier to prevent injection."""
|
|
26
|
+
import re
|
|
27
|
+
if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", name):
|
|
28
|
+
raise ValueError(f"Invalid SQL identifier: {name!r}")
|
|
29
|
+
return f'"{name}"'
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AutoStorage(Storage):
|
|
33
|
+
"""SQLite-backed automatic storage for Pydantic/dataclass models."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, database: str = "flashapi.db"):
|
|
36
|
+
self._db_path = database
|
|
37
|
+
self._conn = sqlite3.connect(database, check_same_thread=False)
|
|
38
|
+
self._conn.row_factory = sqlite3.Row
|
|
39
|
+
self._conn.execute("PRAGMA journal_mode=WAL")
|
|
40
|
+
|
|
41
|
+
def ensure_table(self, schema: ModelSchema) -> None:
|
|
42
|
+
table = _validate_identifier(schema.plural)
|
|
43
|
+
columns = []
|
|
44
|
+
for field in schema.fields:
|
|
45
|
+
col_name = _validate_identifier(field.name)
|
|
46
|
+
if field.primary_key:
|
|
47
|
+
columns.append(f"{col_name} INTEGER PRIMARY KEY AUTOINCREMENT")
|
|
48
|
+
else:
|
|
49
|
+
sql_type = FIELD_TYPE_TO_SQL.get(field.type, "TEXT")
|
|
50
|
+
not_null = " NOT NULL" if field.required else ""
|
|
51
|
+
columns.append(f"{col_name} {sql_type}{not_null}")
|
|
52
|
+
|
|
53
|
+
sql = f"CREATE TABLE IF NOT EXISTS {table} ({', '.join(columns)})"
|
|
54
|
+
self._conn.execute(sql)
|
|
55
|
+
self._conn.commit()
|
|
56
|
+
|
|
57
|
+
def create(self, table: str, data: dict[str, Any]) -> dict[str, Any]:
|
|
58
|
+
safe_table = _validate_identifier(table)
|
|
59
|
+
columns = [_validate_identifier(k) for k in data.keys()]
|
|
60
|
+
placeholders = ", ".join(["?"] * len(columns))
|
|
61
|
+
col_names = ", ".join(columns)
|
|
62
|
+
values = list(data.values())
|
|
63
|
+
|
|
64
|
+
cursor = self._conn.execute(
|
|
65
|
+
f"INSERT INTO {safe_table} ({col_names}) VALUES ({placeholders})", values
|
|
66
|
+
)
|
|
67
|
+
self._conn.commit()
|
|
68
|
+
|
|
69
|
+
item_id = cursor.lastrowid
|
|
70
|
+
return self.get(table, item_id)
|
|
71
|
+
|
|
72
|
+
def get(self, table: str, item_id: int | str) -> dict[str, Any] | None:
|
|
73
|
+
safe_table = _validate_identifier(table)
|
|
74
|
+
cursor = self._conn.execute(f"SELECT * FROM {safe_table} WHERE id = ?", (item_id,))
|
|
75
|
+
row = cursor.fetchone()
|
|
76
|
+
if row is None:
|
|
77
|
+
return None
|
|
78
|
+
return dict(row)
|
|
79
|
+
|
|
80
|
+
def list_all(self, table: str) -> list[dict[str, Any]]:
|
|
81
|
+
safe_table = _validate_identifier(table)
|
|
82
|
+
cursor = self._conn.execute(f"SELECT * FROM {safe_table}")
|
|
83
|
+
return [dict(row) for row in cursor.fetchall()]
|
|
84
|
+
|
|
85
|
+
def update(self, table: str, item_id: int | str, data: dict[str, Any]) -> dict[str, Any] | None:
|
|
86
|
+
if not self.get(table, item_id):
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
safe_table = _validate_identifier(table)
|
|
90
|
+
set_clause = ", ".join([f"{_validate_identifier(k)} = ?" for k in data.keys()])
|
|
91
|
+
values = list(data.values()) + [item_id]
|
|
92
|
+
|
|
93
|
+
self._conn.execute(f"UPDATE {safe_table} SET {set_clause} WHERE id = ?", values)
|
|
94
|
+
self._conn.commit()
|
|
95
|
+
return self.get(table, item_id)
|
|
96
|
+
|
|
97
|
+
def delete(self, table: str, item_id: int | str) -> bool:
|
|
98
|
+
if not self.get(table, item_id):
|
|
99
|
+
return False
|
|
100
|
+
safe_table = _validate_identifier(table)
|
|
101
|
+
self._conn.execute(f"DELETE FROM {safe_table} WHERE id = ?", (item_id,))
|
|
102
|
+
self._conn.commit()
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
def close(self) -> None:
|
|
106
|
+
self._conn.close()
|
flashapi/storage/base.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Storage(ABC):
|
|
8
|
+
@abstractmethod
|
|
9
|
+
def create(self, table: str, data: dict[str, Any]) -> dict[str, Any]:
|
|
10
|
+
...
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def get(self, table: str, item_id: int | str) -> dict[str, Any] | None:
|
|
14
|
+
...
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def list_all(self, table: str) -> list[dict[str, Any]]:
|
|
18
|
+
...
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def update(self, table: str, item_id: int | str, data: dict[str, Any]) -> dict[str, Any] | None:
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def delete(self, table: str, item_id: int | str) -> bool:
|
|
26
|
+
...
|
flashapi/storage/orm.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from flashapi.storage.base import Storage
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DjangoORMStorage(Storage):
|
|
9
|
+
"""Storage backend that delegates to Django's ORM."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, model_class: type):
|
|
12
|
+
self._model = model_class
|
|
13
|
+
|
|
14
|
+
def create(self, table: str, data: dict[str, Any]) -> dict[str, Any]:
|
|
15
|
+
instance = self._model.objects.create(**data)
|
|
16
|
+
return self._to_dict(instance)
|
|
17
|
+
|
|
18
|
+
def get(self, table: str, item_id: int | str) -> dict[str, Any] | None:
|
|
19
|
+
try:
|
|
20
|
+
instance = self._model.objects.get(pk=item_id)
|
|
21
|
+
return self._to_dict(instance)
|
|
22
|
+
except self._model.DoesNotExist:
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
def list_all(self, table: str) -> list[dict[str, Any]]:
|
|
26
|
+
return [self._to_dict(obj) for obj in self._model.objects.all()]
|
|
27
|
+
|
|
28
|
+
def update(self, table: str, item_id: int | str, data: dict[str, Any]) -> dict[str, Any] | None:
|
|
29
|
+
try:
|
|
30
|
+
instance = self._model.objects.get(pk=item_id)
|
|
31
|
+
except self._model.DoesNotExist:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
for key, value in data.items():
|
|
35
|
+
setattr(instance, key, value)
|
|
36
|
+
instance.save()
|
|
37
|
+
return self._to_dict(instance)
|
|
38
|
+
|
|
39
|
+
def delete(self, table: str, item_id: int | str) -> bool:
|
|
40
|
+
try:
|
|
41
|
+
instance = self._model.objects.get(pk=item_id)
|
|
42
|
+
instance.delete()
|
|
43
|
+
return True
|
|
44
|
+
except self._model.DoesNotExist:
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
def _to_dict(self, instance) -> dict[str, Any]:
|
|
48
|
+
|
|
49
|
+
data = {}
|
|
50
|
+
for field in instance._meta.get_fields():
|
|
51
|
+
if field.many_to_many or field.one_to_many:
|
|
52
|
+
continue
|
|
53
|
+
if hasattr(field, "related_model") and field.related_model:
|
|
54
|
+
name = field.attname
|
|
55
|
+
else:
|
|
56
|
+
name = field.name
|
|
57
|
+
value = getattr(instance, name, None)
|
|
58
|
+
data[name] = self._serialize_value(value)
|
|
59
|
+
return data
|
|
60
|
+
|
|
61
|
+
def _serialize_value(self, value) -> Any:
|
|
62
|
+
from datetime import date, datetime, time
|
|
63
|
+
from decimal import Decimal
|
|
64
|
+
import uuid
|
|
65
|
+
|
|
66
|
+
if value is None:
|
|
67
|
+
return None
|
|
68
|
+
if isinstance(value, (str, int, float, bool)):
|
|
69
|
+
return value
|
|
70
|
+
if isinstance(value, Decimal):
|
|
71
|
+
return float(value)
|
|
72
|
+
if isinstance(value, datetime):
|
|
73
|
+
return value.isoformat()
|
|
74
|
+
if isinstance(value, date):
|
|
75
|
+
return value.isoformat()
|
|
76
|
+
if isinstance(value, time):
|
|
77
|
+
return value.isoformat()
|
|
78
|
+
if isinstance(value, uuid.UUID):
|
|
79
|
+
return str(value)
|
|
80
|
+
if hasattr(value, "field") and hasattr(value, "name"):
|
|
81
|
+
try:
|
|
82
|
+
return value.name or None
|
|
83
|
+
except (ValueError, AttributeError):
|
|
84
|
+
return None
|
|
85
|
+
return str(value)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import date, datetime, time
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from flashapi.storage.base import Storage
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SQLAlchemyStorage(Storage):
|
|
10
|
+
"""Storage backend that delegates to a SQLAlchemy session."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, session_factory, model_class: type):
|
|
13
|
+
self._session_factory = session_factory
|
|
14
|
+
self._model = model_class
|
|
15
|
+
self._column_types = {
|
|
16
|
+
col.name: col.type for col in model_class.__table__.columns
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
def _coerce_values(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
20
|
+
"""Convert string values to proper Python types based on column definitions."""
|
|
21
|
+
from sqlalchemy import Date, DateTime, Time, Boolean
|
|
22
|
+
|
|
23
|
+
coerced = {}
|
|
24
|
+
for key, value in data.items():
|
|
25
|
+
if value is None:
|
|
26
|
+
coerced[key] = None
|
|
27
|
+
continue
|
|
28
|
+
|
|
29
|
+
col_type = self._column_types.get(key)
|
|
30
|
+
if col_type is None:
|
|
31
|
+
coerced[key] = value
|
|
32
|
+
continue
|
|
33
|
+
|
|
34
|
+
if isinstance(col_type, DateTime) and isinstance(value, str):
|
|
35
|
+
coerced[key] = datetime.fromisoformat(value)
|
|
36
|
+
elif isinstance(col_type, Date) and isinstance(value, str):
|
|
37
|
+
coerced[key] = date.fromisoformat(value)
|
|
38
|
+
elif isinstance(col_type, Time) and isinstance(value, str):
|
|
39
|
+
coerced[key] = time.fromisoformat(value)
|
|
40
|
+
elif isinstance(col_type, Boolean) and not isinstance(value, bool):
|
|
41
|
+
coerced[key] = bool(value)
|
|
42
|
+
else:
|
|
43
|
+
coerced[key] = value
|
|
44
|
+
|
|
45
|
+
return coerced
|
|
46
|
+
|
|
47
|
+
def create(self, table: str, data: dict[str, Any]) -> dict[str, Any]:
|
|
48
|
+
session = self._session_factory()
|
|
49
|
+
try:
|
|
50
|
+
instance = self._model(**self._coerce_values(data))
|
|
51
|
+
session.add(instance)
|
|
52
|
+
session.commit()
|
|
53
|
+
session.refresh(instance)
|
|
54
|
+
return self._to_dict(instance)
|
|
55
|
+
except Exception:
|
|
56
|
+
session.rollback()
|
|
57
|
+
raise
|
|
58
|
+
finally:
|
|
59
|
+
session.close()
|
|
60
|
+
|
|
61
|
+
def get(self, table: str, item_id: int | str) -> dict[str, Any] | None:
|
|
62
|
+
session = self._session_factory()
|
|
63
|
+
try:
|
|
64
|
+
instance = session.get(self._model, item_id)
|
|
65
|
+
if instance is None:
|
|
66
|
+
return None
|
|
67
|
+
return self._to_dict(instance)
|
|
68
|
+
finally:
|
|
69
|
+
session.close()
|
|
70
|
+
|
|
71
|
+
def list_all(self, table: str) -> list[dict[str, Any]]:
|
|
72
|
+
session = self._session_factory()
|
|
73
|
+
try:
|
|
74
|
+
instances = session.query(self._model).all()
|
|
75
|
+
return [self._to_dict(obj) for obj in instances]
|
|
76
|
+
finally:
|
|
77
|
+
session.close()
|
|
78
|
+
|
|
79
|
+
def update(self, table: str, item_id: int | str, data: dict[str, Any]) -> dict[str, Any] | None:
|
|
80
|
+
session = self._session_factory()
|
|
81
|
+
try:
|
|
82
|
+
instance = session.get(self._model, item_id)
|
|
83
|
+
if instance is None:
|
|
84
|
+
return None
|
|
85
|
+
for key, value in self._coerce_values(data).items():
|
|
86
|
+
setattr(instance, key, value)
|
|
87
|
+
session.commit()
|
|
88
|
+
session.refresh(instance)
|
|
89
|
+
return self._to_dict(instance)
|
|
90
|
+
except Exception:
|
|
91
|
+
session.rollback()
|
|
92
|
+
raise
|
|
93
|
+
finally:
|
|
94
|
+
session.close()
|
|
95
|
+
|
|
96
|
+
def delete(self, table: str, item_id: int | str) -> bool:
|
|
97
|
+
session = self._session_factory()
|
|
98
|
+
try:
|
|
99
|
+
instance = session.get(self._model, item_id)
|
|
100
|
+
if instance is None:
|
|
101
|
+
return False
|
|
102
|
+
session.delete(instance)
|
|
103
|
+
session.commit()
|
|
104
|
+
return True
|
|
105
|
+
except Exception:
|
|
106
|
+
session.rollback()
|
|
107
|
+
raise
|
|
108
|
+
finally:
|
|
109
|
+
session.close()
|
|
110
|
+
|
|
111
|
+
def _to_dict(self, instance) -> dict[str, Any]:
|
|
112
|
+
data = {}
|
|
113
|
+
for col in instance.__table__.columns:
|
|
114
|
+
value = getattr(instance, col.name, None)
|
|
115
|
+
data[col.name] = self._serialize_value(value)
|
|
116
|
+
return data
|
|
117
|
+
|
|
118
|
+
def _serialize_value(self, value) -> Any:
|
|
119
|
+
from datetime import date, datetime, time
|
|
120
|
+
from decimal import Decimal
|
|
121
|
+
import uuid as uuid_mod
|
|
122
|
+
|
|
123
|
+
if value is None:
|
|
124
|
+
return None
|
|
125
|
+
if isinstance(value, (str, int, float, bool)):
|
|
126
|
+
return value
|
|
127
|
+
if isinstance(value, Decimal):
|
|
128
|
+
return float(value)
|
|
129
|
+
if isinstance(value, datetime):
|
|
130
|
+
return value.isoformat()
|
|
131
|
+
if isinstance(value, date):
|
|
132
|
+
return value.isoformat()
|
|
133
|
+
if isinstance(value, time):
|
|
134
|
+
return value.isoformat()
|
|
135
|
+
if isinstance(value, uuid_mod.UUID):
|
|
136
|
+
return str(value)
|
|
137
|
+
return str(value)
|