python-flashapi 0.1.2__py3-none-any.whl → 0.2.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 +8 -7
- flashapi/adapters/base.py +13 -12
- flashapi/adapters/django.py +771 -165
- flashapi/adapters/fastapi.py +879 -293
- flashapi/adapters/flask.py +728 -232
- flashapi/core/__init__.py +11 -3
- flashapi/core/custom_routes.py +5 -5
- flashapi/core/pluralize.py +80 -80
- flashapi/core/relations.py +59 -61
- flashapi/core/response.py +36 -33
- flashapi/core/schema.py +145 -83
- flashapi/core/visibility.py +46 -0
- flashapi/django.py +5 -5
- flashapi/docs/openapi.py +298 -223
- flashapi/fastapi.py +5 -5
- flashapi/features/__init__.py +6 -6
- flashapi/features/audit.py +84 -0
- flashapi/features/auth.py +134 -0
- flashapi/features/dashboard.py +412 -0
- flashapi/features/export.py +143 -0
- flashapi/features/filtering.py +124 -33
- flashapi/features/pagination.py +20 -20
- flashapi/features/rate_limit.py +45 -0
- flashapi/features/search.py +25 -25
- flashapi/features/sorting.py +23 -21
- flashapi/features/webhooks.py +84 -0
- flashapi/features/websocket.py +129 -0
- flashapi/flask.py +5 -5
- flashapi/inspectors/__init__.py +3 -3
- flashapi/inspectors/base.py +13 -11
- flashapi/inspectors/dataclass.py +50 -39
- flashapi/inspectors/detect.py +54 -48
- flashapi/inspectors/django.py +93 -83
- flashapi/inspectors/pydantic.py +99 -84
- flashapi/inspectors/sqlalchemy.py +83 -75
- flashapi/storage/__init__.py +4 -4
- flashapi/storage/auto.py +189 -106
- flashapi/storage/base.py +32 -26
- flashapi/storage/orm.py +125 -85
- flashapi/storage/sqlalchemy.py +56 -13
- python_flashapi-0.2.0.dist-info/METADATA +314 -0
- python_flashapi-0.2.0.dist-info/RECORD +48 -0
- python_flashapi-0.2.0.dist-info/licenses/LICENSE +190 -0
- python_flashapi-0.2.0.dist-info/licenses/NOTICE +5 -0
- python_flashapi-0.1.2.dist-info/METADATA +0 -259
- python_flashapi-0.1.2.dist-info/RECORD +0 -39
- python_flashapi-0.1.2.dist-info/licenses/LICENSE +0 -21
- {python_flashapi-0.1.2.dist-info → python_flashapi-0.2.0.dist-info}/WHEEL +0 -0
flashapi/storage/base.py
CHANGED
|
@@ -1,26 +1,32 @@
|
|
|
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
|
-
...
|
|
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, *, lookup_field: str = "id") -> dict[str, Any] | None:
|
|
14
|
+
...
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def list_all(self, table: str, *, include_deleted: bool = False, only_deleted: bool = False) -> list[dict[str, Any]]:
|
|
18
|
+
...
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def update(self, table: str, item_id: int | str, data: dict[str, Any], *, lookup_field: str = "id") -> dict[str, Any] | None:
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def delete(self, table: str, item_id: int | str, *, soft: bool = True, lookup_field: str = "id") -> bool:
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
def restore(self, table: str, item_id: int | str, *, lookup_field: str = "id") -> bool:
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
def bulk_create(self, table: str, items: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
32
|
+
return [self.create(table, item) for item in items]
|
flashapi/storage/orm.py
CHANGED
|
@@ -1,85 +1,125 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def
|
|
19
|
-
try:
|
|
20
|
-
|
|
21
|
-
return
|
|
22
|
-
except
|
|
23
|
-
return
|
|
24
|
-
|
|
25
|
-
def
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
for
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if
|
|
77
|
-
return
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from flashapi.storage.base import Storage
|
|
7
|
+
|
|
8
|
+
SOFT_DELETE_FIELD = "deleted_at"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DjangoORMStorage(Storage):
|
|
12
|
+
"""Storage backend that delegates to Django's ORM."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, model_class: type) -> None:
|
|
15
|
+
self._model = model_class
|
|
16
|
+
self._has_deleted_at = self._check_has_field(SOFT_DELETE_FIELD)
|
|
17
|
+
|
|
18
|
+
def _check_has_field(self, field_name: str) -> bool:
|
|
19
|
+
try:
|
|
20
|
+
self._model._meta.get_field(field_name)
|
|
21
|
+
return True
|
|
22
|
+
except Exception:
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
def create(self, table: str, data: dict[str, Any]) -> dict[str, Any]:
|
|
26
|
+
instance = self._model.objects.create(**data)
|
|
27
|
+
return self._to_dict(instance)
|
|
28
|
+
|
|
29
|
+
def _get_instance(self, item_id, lookup_field="id"):
|
|
30
|
+
try:
|
|
31
|
+
if lookup_field == "id":
|
|
32
|
+
return self._model.objects.get(pk=item_id)
|
|
33
|
+
return self._model.objects.get(**{lookup_field: item_id})
|
|
34
|
+
except self._model.DoesNotExist:
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
def get(self, table: str, item_id: int | str, *, lookup_field: str = "id") -> dict[str, Any] | None:
|
|
38
|
+
instance = self._get_instance(item_id, lookup_field)
|
|
39
|
+
if instance is None:
|
|
40
|
+
return None
|
|
41
|
+
return self._to_dict(instance)
|
|
42
|
+
|
|
43
|
+
def list_all(self, table: str, *, include_deleted: bool = False, only_deleted: bool = False) -> list[dict[str, Any]]:
|
|
44
|
+
qs = self._model.objects.all()
|
|
45
|
+
if self._has_deleted_at:
|
|
46
|
+
if only_deleted:
|
|
47
|
+
qs = qs.filter(**{SOFT_DELETE_FIELD + "__isnull": False})
|
|
48
|
+
elif not include_deleted:
|
|
49
|
+
qs = qs.filter(**{SOFT_DELETE_FIELD + "__isnull": True})
|
|
50
|
+
return [self._to_dict(obj) for obj in qs]
|
|
51
|
+
|
|
52
|
+
def update(self, table: str, item_id: int | str, data: dict[str, Any], *, lookup_field: str = "id") -> dict[str, Any] | None:
|
|
53
|
+
instance = self._get_instance(item_id, lookup_field)
|
|
54
|
+
if instance is None:
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
for key, value in data.items():
|
|
58
|
+
setattr(instance, key, value)
|
|
59
|
+
instance.save()
|
|
60
|
+
return self._to_dict(instance)
|
|
61
|
+
|
|
62
|
+
def delete(self, table: str, item_id: int | str, *, soft: bool = True, lookup_field: str = "id") -> bool:
|
|
63
|
+
instance = self._get_instance(item_id, lookup_field)
|
|
64
|
+
if instance is None:
|
|
65
|
+
return False
|
|
66
|
+
if soft and self._has_deleted_at:
|
|
67
|
+
if getattr(instance, SOFT_DELETE_FIELD, None) is not None:
|
|
68
|
+
return False
|
|
69
|
+
setattr(instance, SOFT_DELETE_FIELD, datetime.now(timezone.utc))
|
|
70
|
+
instance.save()
|
|
71
|
+
else:
|
|
72
|
+
instance.delete()
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
def restore(self, table: str, item_id: int | str, *, lookup_field: str = "id") -> bool:
|
|
76
|
+
if not self._has_deleted_at:
|
|
77
|
+
return False
|
|
78
|
+
instance = self._get_instance(item_id, lookup_field)
|
|
79
|
+
if instance is None:
|
|
80
|
+
return False
|
|
81
|
+
if getattr(instance, SOFT_DELETE_FIELD, None) is None:
|
|
82
|
+
return False
|
|
83
|
+
setattr(instance, SOFT_DELETE_FIELD, None)
|
|
84
|
+
instance.save()
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
def _to_dict(self, instance) -> dict[str, Any]:
|
|
88
|
+
|
|
89
|
+
data = {}
|
|
90
|
+
for field in instance._meta.get_fields():
|
|
91
|
+
if field.many_to_many or field.one_to_many:
|
|
92
|
+
continue
|
|
93
|
+
if hasattr(field, "related_model") and field.related_model:
|
|
94
|
+
name = field.attname
|
|
95
|
+
else:
|
|
96
|
+
name = field.name
|
|
97
|
+
value = getattr(instance, name, None)
|
|
98
|
+
data[name] = self._serialize_value(value)
|
|
99
|
+
return data
|
|
100
|
+
|
|
101
|
+
def _serialize_value(self, value) -> Any:
|
|
102
|
+
import uuid
|
|
103
|
+
from datetime import date, datetime, time
|
|
104
|
+
from decimal import Decimal
|
|
105
|
+
|
|
106
|
+
if value is None:
|
|
107
|
+
return None
|
|
108
|
+
if isinstance(value, (str, int, float, bool)):
|
|
109
|
+
return value
|
|
110
|
+
if isinstance(value, Decimal):
|
|
111
|
+
return float(value)
|
|
112
|
+
if isinstance(value, datetime):
|
|
113
|
+
return value.isoformat()
|
|
114
|
+
if isinstance(value, date):
|
|
115
|
+
return value.isoformat()
|
|
116
|
+
if isinstance(value, time):
|
|
117
|
+
return value.isoformat()
|
|
118
|
+
if isinstance(value, uuid.UUID):
|
|
119
|
+
return str(value)
|
|
120
|
+
if hasattr(value, "field") and hasattr(value, "name"):
|
|
121
|
+
try:
|
|
122
|
+
return value.name or None
|
|
123
|
+
except (ValueError, AttributeError):
|
|
124
|
+
return None
|
|
125
|
+
return str(value)
|
flashapi/storage/sqlalchemy.py
CHANGED
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from datetime import date, datetime, time
|
|
3
|
+
from datetime import date, datetime, time, timezone
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
6
|
from flashapi.storage.base import Storage
|
|
7
7
|
|
|
8
|
+
SOFT_DELETE_FIELD = "deleted_at"
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
class SQLAlchemyStorage(Storage):
|
|
10
12
|
"""Storage backend that delegates to a SQLAlchemy session."""
|
|
11
13
|
|
|
12
|
-
def __init__(self, session_factory, model_class: type):
|
|
14
|
+
def __init__(self, session_factory, model_class: type) -> None:
|
|
13
15
|
self._session_factory = session_factory
|
|
14
16
|
self._model = model_class
|
|
15
17
|
self._column_types = {
|
|
16
18
|
col.name: col.type for col in model_class.__table__.columns
|
|
17
19
|
}
|
|
20
|
+
self._has_deleted_at = SOFT_DELETE_FIELD in self._column_types
|
|
18
21
|
|
|
19
22
|
def _coerce_values(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
20
23
|
"""Convert string values to proper Python types based on column definitions."""
|
|
21
|
-
from sqlalchemy import Date, DateTime, Time
|
|
24
|
+
from sqlalchemy import Boolean, Date, DateTime, Time
|
|
22
25
|
|
|
23
26
|
coerced = {}
|
|
24
27
|
for key, value in data.items():
|
|
@@ -58,28 +61,43 @@ class SQLAlchemyStorage(Storage):
|
|
|
58
61
|
finally:
|
|
59
62
|
session.close()
|
|
60
63
|
|
|
61
|
-
def
|
|
64
|
+
def _get_by_lookup(self, session, item_id, lookup_field="id"):
|
|
65
|
+
if lookup_field == "id":
|
|
66
|
+
return session.get(self._model, item_id)
|
|
67
|
+
col = getattr(self._model, lookup_field, None)
|
|
68
|
+
if col is None:
|
|
69
|
+
return None
|
|
70
|
+
return session.query(self._model).filter(col == item_id).first()
|
|
71
|
+
|
|
72
|
+
def get(self, table: str, item_id: int | str, *, lookup_field: str = "id") -> dict[str, Any] | None:
|
|
62
73
|
session = self._session_factory()
|
|
63
74
|
try:
|
|
64
|
-
instance =
|
|
75
|
+
instance = self._get_by_lookup(session, item_id, lookup_field)
|
|
65
76
|
if instance is None:
|
|
66
77
|
return None
|
|
67
78
|
return self._to_dict(instance)
|
|
68
79
|
finally:
|
|
69
80
|
session.close()
|
|
70
81
|
|
|
71
|
-
def list_all(self, table: str) -> list[dict[str, Any]]:
|
|
82
|
+
def list_all(self, table: str, *, include_deleted: bool = False, only_deleted: bool = False) -> list[dict[str, Any]]:
|
|
72
83
|
session = self._session_factory()
|
|
73
84
|
try:
|
|
74
|
-
|
|
85
|
+
query = session.query(self._model)
|
|
86
|
+
if self._has_deleted_at:
|
|
87
|
+
col = getattr(self._model, SOFT_DELETE_FIELD)
|
|
88
|
+
if only_deleted:
|
|
89
|
+
query = query.filter(col.isnot(None))
|
|
90
|
+
elif not include_deleted:
|
|
91
|
+
query = query.filter(col.is_(None))
|
|
92
|
+
instances = query.all()
|
|
75
93
|
return [self._to_dict(obj) for obj in instances]
|
|
76
94
|
finally:
|
|
77
95
|
session.close()
|
|
78
96
|
|
|
79
|
-
def update(self, table: str, item_id: int | str, data: dict[str, Any]) -> dict[str, Any] | None:
|
|
97
|
+
def update(self, table: str, item_id: int | str, data: dict[str, Any], *, lookup_field: str = "id") -> dict[str, Any] | None:
|
|
80
98
|
session = self._session_factory()
|
|
81
99
|
try:
|
|
82
|
-
instance =
|
|
100
|
+
instance = self._get_by_lookup(session, item_id, lookup_field)
|
|
83
101
|
if instance is None:
|
|
84
102
|
return None
|
|
85
103
|
for key, value in self._coerce_values(data).items():
|
|
@@ -93,13 +111,38 @@ class SQLAlchemyStorage(Storage):
|
|
|
93
111
|
finally:
|
|
94
112
|
session.close()
|
|
95
113
|
|
|
96
|
-
def delete(self, table: str, item_id: int | str) -> bool:
|
|
114
|
+
def delete(self, table: str, item_id: int | str, *, soft: bool = True, lookup_field: str = "id") -> bool:
|
|
115
|
+
session = self._session_factory()
|
|
116
|
+
try:
|
|
117
|
+
instance = self._get_by_lookup(session, item_id, lookup_field)
|
|
118
|
+
if instance is None:
|
|
119
|
+
return False
|
|
120
|
+
if soft and self._has_deleted_at:
|
|
121
|
+
if getattr(instance, SOFT_DELETE_FIELD, None) is not None:
|
|
122
|
+
return False
|
|
123
|
+
setattr(instance, SOFT_DELETE_FIELD, datetime.now(timezone.utc))
|
|
124
|
+
session.commit()
|
|
125
|
+
else:
|
|
126
|
+
session.delete(instance)
|
|
127
|
+
session.commit()
|
|
128
|
+
return True
|
|
129
|
+
except Exception:
|
|
130
|
+
session.rollback()
|
|
131
|
+
raise
|
|
132
|
+
finally:
|
|
133
|
+
session.close()
|
|
134
|
+
|
|
135
|
+
def restore(self, table: str, item_id: int | str, *, lookup_field: str = "id") -> bool:
|
|
136
|
+
if not self._has_deleted_at:
|
|
137
|
+
return False
|
|
97
138
|
session = self._session_factory()
|
|
98
139
|
try:
|
|
99
|
-
instance =
|
|
140
|
+
instance = self._get_by_lookup(session, item_id, lookup_field)
|
|
100
141
|
if instance is None:
|
|
101
142
|
return False
|
|
102
|
-
|
|
143
|
+
if getattr(instance, SOFT_DELETE_FIELD, None) is None:
|
|
144
|
+
return False
|
|
145
|
+
setattr(instance, SOFT_DELETE_FIELD, None)
|
|
103
146
|
session.commit()
|
|
104
147
|
return True
|
|
105
148
|
except Exception:
|
|
@@ -116,9 +159,9 @@ class SQLAlchemyStorage(Storage):
|
|
|
116
159
|
return data
|
|
117
160
|
|
|
118
161
|
def _serialize_value(self, value) -> Any:
|
|
162
|
+
import uuid as uuid_mod
|
|
119
163
|
from datetime import date, datetime, time
|
|
120
164
|
from decimal import Decimal
|
|
121
|
-
import uuid as uuid_mod
|
|
122
165
|
|
|
123
166
|
if value is None:
|
|
124
167
|
return None
|