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
|
@@ -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"]
|
flashapi/inspectors/__init__.py
CHANGED
|
@@ -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"]
|
flashapi/inspectors/base.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from abc import ABC, abstractmethod
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
+
...
|
flashapi/inspectors/dataclass.py
CHANGED
|
@@ -1,39 +1,50 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from dataclasses import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
from flashapi.core.pluralize import pluralize
|
|
7
|
-
from flashapi.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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)
|
flashapi/inspectors/detect.py
CHANGED
|
@@ -1,48 +1,54 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from dataclasses import is_dataclass
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
+
|
flashapi/inspectors/django.py
CHANGED
|
@@ -1,83 +1,93 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from flashapi.core.
|
|
4
|
-
from flashapi.core.
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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)
|