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.
Files changed (48) hide show
  1. flashapi/__init__.py +8 -7
  2. flashapi/adapters/base.py +13 -12
  3. flashapi/adapters/django.py +771 -165
  4. flashapi/adapters/fastapi.py +879 -293
  5. flashapi/adapters/flask.py +728 -232
  6. flashapi/core/__init__.py +11 -3
  7. flashapi/core/custom_routes.py +5 -5
  8. flashapi/core/pluralize.py +80 -80
  9. flashapi/core/relations.py +59 -61
  10. flashapi/core/response.py +36 -33
  11. flashapi/core/schema.py +145 -83
  12. flashapi/core/visibility.py +46 -0
  13. flashapi/django.py +5 -5
  14. flashapi/docs/openapi.py +298 -223
  15. flashapi/fastapi.py +5 -5
  16. flashapi/features/__init__.py +6 -6
  17. flashapi/features/audit.py +84 -0
  18. flashapi/features/auth.py +134 -0
  19. flashapi/features/dashboard.py +412 -0
  20. flashapi/features/export.py +143 -0
  21. flashapi/features/filtering.py +124 -33
  22. flashapi/features/pagination.py +20 -20
  23. flashapi/features/rate_limit.py +45 -0
  24. flashapi/features/search.py +25 -25
  25. flashapi/features/sorting.py +23 -21
  26. flashapi/features/webhooks.py +84 -0
  27. flashapi/features/websocket.py +129 -0
  28. flashapi/flask.py +5 -5
  29. flashapi/inspectors/__init__.py +3 -3
  30. flashapi/inspectors/base.py +13 -11
  31. flashapi/inspectors/dataclass.py +50 -39
  32. flashapi/inspectors/detect.py +54 -48
  33. flashapi/inspectors/django.py +93 -83
  34. flashapi/inspectors/pydantic.py +99 -84
  35. flashapi/inspectors/sqlalchemy.py +83 -75
  36. flashapi/storage/__init__.py +4 -4
  37. flashapi/storage/auto.py +189 -106
  38. flashapi/storage/base.py +32 -26
  39. flashapi/storage/orm.py +125 -85
  40. flashapi/storage/sqlalchemy.py +56 -13
  41. python_flashapi-0.2.0.dist-info/METADATA +314 -0
  42. python_flashapi-0.2.0.dist-info/RECORD +48 -0
  43. python_flashapi-0.2.0.dist-info/licenses/LICENSE +190 -0
  44. python_flashapi-0.2.0.dist-info/licenses/NOTICE +5 -0
  45. python_flashapi-0.1.2.dist-info/METADATA +0 -259
  46. python_flashapi-0.1.2.dist-info/RECORD +0 -39
  47. python_flashapi-0.1.2.dist-info/licenses/LICENSE +0 -21
  48. {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 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)
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)
@@ -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, Boolean
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 get(self, table: str, item_id: int | str) -> dict[str, Any] | None:
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 = session.get(self._model, item_id)
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
- instances = session.query(self._model).all()
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 = session.get(self._model, item_id)
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 = session.get(self._model, item_id)
140
+ instance = self._get_by_lookup(session, item_id, lookup_field)
100
141
  if instance is None:
101
142
  return False
102
- session.delete(instance)
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