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
@@ -0,0 +1,129 @@
1
+ """WebSocket real-time events — broadcasts CRUD events to connected clients.
2
+
3
+ Protocol: raw WebSocket with JSON messages (compatible with the spec's event format).
4
+ Clients subscribe to topics via a SUBSCRIBE message, server pushes events.
5
+
6
+ Message format (server → client):
7
+ {
8
+ "type": "ENTITY_CREATED" | "ENTITY_UPDATED" | "ENTITY_DELETED" | "ENTITY_RESTORED",
9
+ "entity": "Eleve",
10
+ "data": { ... },
11
+ "timestamp": "2026-07-14T15:30:00Z"
12
+ }
13
+
14
+ Subscribe message (client → server):
15
+ {
16
+ "action": "subscribe",
17
+ "topic": "/topic/entities" | "/topic/{entity}"
18
+ }
19
+
20
+ Unsubscribe message (client → server):
21
+ {
22
+ "action": "unsubscribe",
23
+ "topic": "/topic/entities" | "/topic/{entity}"
24
+ }
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import json
30
+ from datetime import datetime, timezone
31
+
32
+
33
+ class WebSocketHub:
34
+ """In-memory pub/sub hub for WebSocket connections.
35
+
36
+ Framework adapters register connections and call broadcast() on CRUD events.
37
+ """
38
+
39
+ def __init__(self) -> None:
40
+ self._subscribers: dict[str, set] = {}
41
+
42
+ def subscribe(self, topic: str, connection) -> None:
43
+ if topic not in self._subscribers:
44
+ self._subscribers[topic] = set()
45
+ self._subscribers[topic].add(connection)
46
+
47
+ def unsubscribe(self, topic: str, connection) -> None:
48
+ if topic in self._subscribers:
49
+ self._subscribers[topic].discard(connection)
50
+ if not self._subscribers[topic]:
51
+ del self._subscribers[topic]
52
+
53
+ def remove_connection(self, connection) -> None:
54
+ for topic in list(self._subscribers.keys()):
55
+ self._subscribers[topic].discard(connection)
56
+ if not self._subscribers[topic]:
57
+ del self._subscribers[topic]
58
+
59
+ def get_subscribers(self, entity: str) -> set:
60
+ global_subs = self._subscribers.get("/topic/entities", set())
61
+ entity_subs = self._subscribers.get(f"/topic/{entity.lower()}", set())
62
+ return global_subs | entity_subs
63
+
64
+ def build_message(self, event_type: str, entity: str, data: dict | None = None) -> str:
65
+ message = {
66
+ "type": event_type,
67
+ "entity": entity,
68
+ "data": data or {},
69
+ "timestamp": datetime.now(timezone.utc).isoformat(),
70
+ }
71
+ return json.dumps(message)
72
+
73
+
74
+ _hub = WebSocketHub()
75
+
76
+
77
+ def get_hub() -> WebSocketHub:
78
+ return _hub
79
+
80
+
81
+ def broadcast_event(entity: str, event_type: str, data: dict | None = None) -> None:
82
+ """Broadcast an event to all subscribers. Called by adapters after CRUD ops.
83
+
84
+ This is a no-op if no WebSocket connections are active (zero overhead).
85
+ """
86
+ hub = get_hub()
87
+ subscribers = hub.get_subscribers(entity)
88
+ if not subscribers:
89
+ return
90
+
91
+ message = hub.build_message(event_type, entity, data)
92
+
93
+ dead = set()
94
+ for conn in subscribers:
95
+ try:
96
+ conn.send_message(message)
97
+ except Exception:
98
+ dead.add(conn)
99
+
100
+ for conn in dead:
101
+ hub.remove_connection(conn)
102
+
103
+
104
+ async def broadcast_event_async(entity: str, event_type: str, data: dict | None = None) -> None:
105
+ """Async version of broadcast_event for FastAPI/async frameworks."""
106
+ hub = get_hub()
107
+ subscribers = hub.get_subscribers(entity)
108
+ if not subscribers:
109
+ return
110
+
111
+ message = hub.build_message(event_type, entity, data)
112
+
113
+ dead = set()
114
+ for conn in subscribers:
115
+ try:
116
+ await conn.send_message(message)
117
+ except Exception:
118
+ dead.add(conn)
119
+
120
+ for conn in dead:
121
+ hub.remove_connection(conn)
122
+
123
+
124
+ EVENT_MAP = {
125
+ "CREATE": "ENTITY_CREATED",
126
+ "UPDATE": "ENTITY_UPDATED",
127
+ "DELETE": "ENTITY_DELETED",
128
+ "RESTORE": "ENTITY_RESTORED",
129
+ }
flashapi/flask.py CHANGED
@@ -1,5 +1,5 @@
1
- """FlashAPI Flask adapter — public entry point."""
2
-
3
- from flashapi.adapters.flask import register_models
4
-
5
- __all__ = ["register_models"]
1
+ """FlashAPI Flask adapter — public entry point."""
2
+
3
+ from flashapi.adapters.flask import register_models
4
+
5
+ __all__ = ["register_models"]
@@ -1,3 +1,3 @@
1
- from flashapi.inspectors.detect import inspect_model
2
-
3
- __all__ = ["inspect_model"]
1
+ from flashapi.inspectors.detect import inspect_model
2
+
3
+ __all__ = ["inspect_model"]
@@ -1,11 +1,13 @@
1
- from __future__ import annotations
2
-
3
- from abc import ABC, abstractmethod
4
-
5
- from flashapi.core.schema import ModelSchema
6
-
7
-
8
- class Inspector(ABC):
9
- @abstractmethod
10
- def inspect(self, model_class: type, plural: str | None = None) -> ModelSchema:
11
- ...
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from flashapi.core.schema import ModelSchema
8
+
9
+
10
+ class Inspector(ABC):
11
+ @abstractmethod
12
+ def inspect(self, model_class: type, plural: str | None = None) -> ModelSchema:
13
+ ...
@@ -1,39 +1,50 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import fields as dc_fields, MISSING
4
-
5
- from flashapi.core.schema import FieldSchema, FieldType, ModelSchema
6
- from flashapi.core.pluralize import pluralize
7
- from flashapi.inspectors.base import Inspector
8
-
9
- TYPE_MAP: dict[type, FieldType] = {
10
- str: FieldType.STRING,
11
- int: FieldType.INTEGER,
12
- float: FieldType.FLOAT,
13
- bool: FieldType.BOOLEAN,
14
- }
15
-
16
-
17
- class DataclassInspector(Inspector):
18
- def inspect(self, model_class: type, plural: str | None = None) -> ModelSchema:
19
- schema_fields: list[FieldSchema] = []
20
- schema_fields.append(
21
- FieldSchema(name="id", type=FieldType.INTEGER, required=False, primary_key=True, auto_generated=True)
22
- )
23
-
24
- for f in dc_fields(model_class):
25
- field_type = TYPE_MAP.get(f.type, FieldType.STRING)
26
- required = f.default is MISSING and f.default_factory is MISSING
27
- default = None if required else f.default
28
-
29
- schema_fields.append(FieldSchema(
30
- name=f.name,
31
- type=field_type,
32
- required=required,
33
- default=default,
34
- ))
35
-
36
- model_name = model_class.__name__
37
- plural_name = plural or pluralize(model_name)
38
-
39
- return ModelSchema(name=model_name, plural=plural_name, fields=schema_fields)
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import MISSING
4
+ from dataclasses import fields as dc_fields
5
+
6
+ from flashapi.core.pluralize import pluralize
7
+ from flashapi.core.schema import FieldSchema, FieldType, ModelSchema
8
+ from flashapi.inspectors.base import Inspector
9
+
10
+ TYPE_MAP: dict[type, FieldType] = {
11
+ str: FieldType.STRING,
12
+ int: FieldType.INTEGER,
13
+ float: FieldType.FLOAT,
14
+ bool: FieldType.BOOLEAN,
15
+ }
16
+
17
+
18
+ class DataclassInspector(Inspector):
19
+ def _extract_visibility(self, f) -> dict:
20
+ visibility = {}
21
+ meta = f.metadata or {}
22
+ for key in ("readonly", "writeonly", "hidden", "export_exclude"):
23
+ if meta.get(key):
24
+ visibility[key] = True
25
+ return visibility
26
+
27
+ def inspect(self, model_class: type, plural: str | None = None) -> ModelSchema:
28
+ schema_fields: list[FieldSchema] = []
29
+ schema_fields.append(
30
+ FieldSchema(name="id", type=FieldType.INTEGER, required=False, primary_key=True, auto_generated=True),
31
+ )
32
+
33
+ for f in dc_fields(model_class):
34
+ field_type = TYPE_MAP.get(f.type, FieldType.STRING)
35
+ required = f.default is MISSING and f.default_factory is MISSING
36
+ default = None if required else f.default
37
+ visibility = self._extract_visibility(f)
38
+
39
+ schema_fields.append(FieldSchema(
40
+ name=f.name,
41
+ type=field_type,
42
+ required=required,
43
+ default=default,
44
+ **visibility,
45
+ ))
46
+
47
+ model_name = model_class.__name__
48
+ plural_name = plural or pluralize(model_name)
49
+
50
+ return ModelSchema(name=model_name, plural=plural_name, fields=schema_fields)
@@ -1,48 +1,54 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import is_dataclass
4
-
5
- from flashapi.core.schema import ModelSchema
6
-
7
-
8
- def inspect_model(model_class: type, plural: str | None = None) -> ModelSchema:
9
- if _is_django_model(model_class):
10
- from flashapi.inspectors.django import DjangoInspector
11
- return DjangoInspector().inspect(model_class, plural)
12
-
13
- if _is_sqlalchemy_model(model_class):
14
- from flashapi.inspectors.sqlalchemy import SQLAlchemyInspector
15
- return SQLAlchemyInspector().inspect(model_class, plural)
16
-
17
- if _is_pydantic_model(model_class):
18
- from flashapi.inspectors.pydantic import PydanticInspector
19
- return PydanticInspector().inspect(model_class, plural)
20
-
21
- if is_dataclass(model_class):
22
- from flashapi.inspectors.dataclass import DataclassInspector
23
- return DataclassInspector().inspect(model_class, plural)
24
-
25
- raise TypeError(
26
- f"Unsupported model type: {model_class}. "
27
- "FlashAPI supports Django models, SQLAlchemy models, Pydantic models, and dataclasses."
28
- )
29
-
30
-
31
- def _is_django_model(cls: type) -> bool:
32
- try:
33
- from django.db import models
34
- return issubclass(cls, models.Model)
35
- except ImportError:
36
- return False
37
-
38
-
39
- def _is_sqlalchemy_model(cls: type) -> bool:
40
- return hasattr(cls, "__table__") and hasattr(cls, "__tablename__")
41
-
42
-
43
- def _is_pydantic_model(cls: type) -> bool:
44
- try:
45
- from pydantic import BaseModel
46
- return issubclass(cls, BaseModel)
47
- except ImportError:
48
- return False
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import is_dataclass
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from flashapi.core.schema import ModelSchema
8
+
9
+
10
+ def inspect_model(model_class: type, plural: str | None = None) -> ModelSchema:
11
+ if _is_django_model(model_class):
12
+ from flashapi.inspectors.django import DjangoInspector
13
+ return DjangoInspector().inspect(model_class, plural)
14
+
15
+ if _is_sqlalchemy_model(model_class):
16
+ from flashapi.inspectors.sqlalchemy import SQLAlchemyInspector
17
+ return SQLAlchemyInspector().inspect(model_class, plural)
18
+
19
+ if _is_pydantic_model(model_class):
20
+ from flashapi.inspectors.pydantic import PydanticInspector
21
+ return PydanticInspector().inspect(model_class, plural)
22
+
23
+ if is_dataclass(model_class):
24
+ from flashapi.inspectors.dataclass import DataclassInspector
25
+ return DataclassInspector().inspect(model_class, plural)
26
+
27
+ msg = (
28
+ f"Unsupported model type: {model_class}. "
29
+ "FlashAPI supports Django models, SQLAlchemy models, Pydantic models, and dataclasses."
30
+ )
31
+ raise TypeError(
32
+ msg,
33
+ )
34
+
35
+
36
+ def _is_django_model(cls: type) -> bool:
37
+ try:
38
+ from django.db import models
39
+ return isinstance(cls, type) and issubclass(cls, models.Model)
40
+ except ImportError:
41
+ return False
42
+
43
+
44
+ def _is_sqlalchemy_model(cls: type) -> bool:
45
+ return isinstance(cls, type) and hasattr(cls, "__table__") and hasattr(cls, "__tablename__")
46
+
47
+
48
+ def _is_pydantic_model(cls: type) -> bool:
49
+ try:
50
+ from pydantic import BaseModel
51
+ return isinstance(cls, type) and issubclass(cls, BaseModel)
52
+ except ImportError:
53
+ return False
54
+
@@ -1,83 +1,93 @@
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
- DJANGO_TYPE_MAP = {
8
- "AutoField": FieldType.INTEGER,
9
- "BigAutoField": FieldType.INTEGER,
10
- "SmallAutoField": FieldType.INTEGER,
11
- "CharField": FieldType.STRING,
12
- "TextField": FieldType.TEXT,
13
- "IntegerField": FieldType.INTEGER,
14
- "BigIntegerField": FieldType.INTEGER,
15
- "SmallIntegerField": FieldType.INTEGER,
16
- "PositiveIntegerField": FieldType.INTEGER,
17
- "FloatField": FieldType.FLOAT,
18
- "DecimalField": FieldType.FLOAT,
19
- "BooleanField": FieldType.BOOLEAN,
20
- "DateField": FieldType.DATE,
21
- "DateTimeField": FieldType.DATETIME,
22
- "TimeField": FieldType.TIME,
23
- "UUIDField": FieldType.UUID,
24
- "JSONField": FieldType.JSON,
25
- "BinaryField": FieldType.BINARY,
26
- "EmailField": FieldType.STRING,
27
- "URLField": FieldType.STRING,
28
- "SlugField": FieldType.STRING,
29
- "FileField": FieldType.STRING,
30
- "ImageField": FieldType.STRING,
31
- }
32
-
33
-
34
- class DjangoInspector(Inspector):
35
- def inspect(self, model_class: type, plural: str | None = None) -> ModelSchema:
36
- meta = model_class._meta
37
- fields: list[FieldSchema] = []
38
-
39
- for f in meta.get_fields():
40
- if f.many_to_many or f.one_to_many:
41
- continue
42
-
43
- field_type_name = type(f).__name__
44
- field_type = DJANGO_TYPE_MAP.get(field_type_name, FieldType.STRING)
45
-
46
- constraints = {}
47
- if hasattr(f, "max_length") and f.max_length:
48
- constraints["max_length"] = f.max_length
49
-
50
- relation = None
51
- is_fk = hasattr(f, "related_model") and f.related_model
52
- if is_fk:
53
- relation = RelationSchema(
54
- type="one_to_one" if f.one_to_one else "many_to_one",
55
- target=f.related_model.__name__,
56
- )
57
- field_type = FieldType.INTEGER
58
-
59
- is_pk = getattr(f, "primary_key", False)
60
- auto_generated = field_type_name in ("AutoField", "BigAutoField", "SmallAutoField")
61
- has_default = hasattr(f, "default") and f.default is not None
62
- if is_pk and has_default and not auto_generated:
63
- auto_generated = True
64
- required = not getattr(f, "blank", False) and not getattr(f, "null", False) and not has_default
65
- default = f.default if has_default else None
66
-
67
- field_name = getattr(f, "attname", f.name) if is_fk else f.name
68
-
69
- fields.append(FieldSchema(
70
- name=field_name,
71
- type=field_type,
72
- required=required and not is_pk,
73
- default=default,
74
- constraints=constraints,
75
- primary_key=is_pk,
76
- auto_generated=auto_generated,
77
- relation=relation,
78
- ))
79
-
80
- model_name = model_class.__name__
81
- plural_name = plural or pluralize(model_name)
82
-
83
- return ModelSchema(name=model_name, plural=plural_name, fields=fields)
1
+ from __future__ import annotations
2
+
3
+ from flashapi.core.pluralize import pluralize
4
+ from flashapi.core.schema import FieldSchema, FieldType, ModelSchema, RelationSchema
5
+ from flashapi.inspectors.base import Inspector
6
+
7
+ DJANGO_TYPE_MAP = {
8
+ "AutoField": FieldType.INTEGER,
9
+ "BigAutoField": FieldType.INTEGER,
10
+ "SmallAutoField": FieldType.INTEGER,
11
+ "CharField": FieldType.STRING,
12
+ "TextField": FieldType.TEXT,
13
+ "IntegerField": FieldType.INTEGER,
14
+ "BigIntegerField": FieldType.INTEGER,
15
+ "SmallIntegerField": FieldType.INTEGER,
16
+ "PositiveIntegerField": FieldType.INTEGER,
17
+ "FloatField": FieldType.FLOAT,
18
+ "DecimalField": FieldType.FLOAT,
19
+ "BooleanField": FieldType.BOOLEAN,
20
+ "DateField": FieldType.DATE,
21
+ "DateTimeField": FieldType.DATETIME,
22
+ "TimeField": FieldType.TIME,
23
+ "UUIDField": FieldType.UUID,
24
+ "JSONField": FieldType.JSON,
25
+ "BinaryField": FieldType.BINARY,
26
+ "EmailField": FieldType.STRING,
27
+ "URLField": FieldType.STRING,
28
+ "SlugField": FieldType.STRING,
29
+ "FileField": FieldType.STRING,
30
+ "ImageField": FieldType.STRING,
31
+ }
32
+
33
+
34
+ class DjangoInspector(Inspector):
35
+ def inspect(self, model_class: type, plural: str | None = None) -> ModelSchema:
36
+ from django.db.models.fields import NOT_PROVIDED
37
+
38
+ meta = model_class._meta
39
+ fields: list[FieldSchema] = []
40
+
41
+ for f in meta.get_fields():
42
+ if f.many_to_many or f.one_to_many:
43
+ continue
44
+
45
+ field_type_name = type(f).__name__
46
+ field_type = DJANGO_TYPE_MAP.get(field_type_name, FieldType.STRING)
47
+
48
+ constraints = {}
49
+ if hasattr(f, "max_length") and f.max_length:
50
+ constraints["max_length"] = f.max_length
51
+
52
+ relation = None
53
+ is_fk = hasattr(f, "related_model") and f.related_model
54
+ if is_fk:
55
+ relation = RelationSchema(
56
+ type="one_to_one" if f.one_to_one else "many_to_one",
57
+ target=f.related_model.__name__,
58
+ )
59
+ target_pk = f.related_model._meta.pk
60
+ target_type_name = type(target_pk).__name__
61
+ field_type = DJANGO_TYPE_MAP.get(target_type_name, FieldType.INTEGER)
62
+
63
+ is_pk = getattr(f, "primary_key", False)
64
+ auto_generated = field_type_name in ("AutoField", "BigAutoField", "SmallAutoField")
65
+ raw_default = getattr(f, "default", NOT_PROVIDED)
66
+ has_real_default = raw_default is not NOT_PROVIDED and raw_default is not None
67
+ has_callable_default = has_real_default and callable(raw_default)
68
+ auto_now_add = getattr(f, "auto_now_add", False)
69
+ auto_now = getattr(f, "auto_now", False)
70
+ if is_pk and has_real_default and not auto_generated:
71
+ auto_generated = True
72
+ if has_callable_default or auto_now_add or auto_now:
73
+ auto_generated = True
74
+ required = not getattr(f, "blank", False) and not getattr(f, "null", False) and not has_real_default
75
+ default = raw_default if has_real_default else None
76
+
77
+ field_name = getattr(f, "attname", f.name) if is_fk else f.name
78
+
79
+ fields.append(FieldSchema(
80
+ name=field_name,
81
+ type=field_type,
82
+ required=required and not is_pk,
83
+ default=default,
84
+ constraints=constraints,
85
+ primary_key=is_pk,
86
+ auto_generated=auto_generated,
87
+ relation=relation,
88
+ ))
89
+
90
+ model_name = model_class.__name__
91
+ plural_name = plural or pluralize(model_name)
92
+
93
+ return ModelSchema(name=model_name, plural=plural_name, fields=fields)