python-flashapi 0.1.2__py3-none-any.whl → 0.3.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 +808 -165
- flashapi/adapters/fastapi.py +911 -293
- flashapi/adapters/flask.py +762 -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/health.py +69 -0
- 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.3.0.dist-info/METADATA +319 -0
- python_flashapi-0.3.0.dist-info/RECORD +49 -0
- {python_flashapi-0.1.2.dist-info → python_flashapi-0.3.0.dist-info}/WHEEL +1 -1
- python_flashapi-0.3.0.dist-info/licenses/LICENSE +190 -0
- python_flashapi-0.3.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
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Field visibility rules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from flashapi.core.schema import ModelSchema
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def response_fields(schema: ModelSchema) -> set[str]:
|
|
12
|
+
"""Fields visible in GET responses."""
|
|
13
|
+
return {
|
|
14
|
+
f.name for f in schema.fields
|
|
15
|
+
if not f.hidden and not f.writeonly
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def writable_fields(schema: ModelSchema) -> set[str]:
|
|
20
|
+
"""Fields accepted in POST/PUT bodies."""
|
|
21
|
+
return {
|
|
22
|
+
f.name for f in schema.fields
|
|
23
|
+
if not f.hidden and not f.readonly and not f.primary_key
|
|
24
|
+
and not f.auto_generated and not f.auto
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def export_fields(schema: ModelSchema) -> set[str]:
|
|
29
|
+
"""Fields included in exports."""
|
|
30
|
+
return {
|
|
31
|
+
f.name for f in schema.fields
|
|
32
|
+
if not f.hidden and not f.writeonly and not f.export_exclude
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def filter_response(data: dict, schema: ModelSchema) -> dict:
|
|
37
|
+
"""Remove hidden/writeonly fields from a response dict."""
|
|
38
|
+
visible = response_fields(schema)
|
|
39
|
+
schema_field_names = {f.name for f in schema.fields}
|
|
40
|
+
return {k: v for k, v in data.items() if k in visible or k == "id" or k not in schema_field_names}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def filter_input(data: dict, schema: ModelSchema) -> dict:
|
|
44
|
+
"""Remove readonly/hidden fields from input dict."""
|
|
45
|
+
allowed = writable_fields(schema)
|
|
46
|
+
return {k: v for k, v in data.items() if k in allowed}
|
flashapi/django.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
"""FlashAPI Django adapter — public entry point."""
|
|
2
|
-
|
|
3
|
-
from flashapi.adapters.django import generate_urls
|
|
4
|
-
|
|
5
|
-
__all__ = ["generate_urls"]
|
|
1
|
+
"""FlashAPI Django adapter — public entry point."""
|
|
2
|
+
|
|
3
|
+
from flashapi.adapters.django import generate_urls
|
|
4
|
+
|
|
5
|
+
__all__ = ["generate_urls"]
|
flashapi/docs/openapi.py
CHANGED
|
@@ -1,223 +1,298 @@
|
|
|
1
|
-
"""OpenAPI schema generation and Swagger UI for Flask and Django."""
|
|
2
|
-
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
|
-
from flashapi.core.schema import
|
|
6
|
-
|
|
7
|
-
FIELD_TYPE_TO_OPENAPI = {
|
|
8
|
-
FieldType.STRING: {"type": "string"},
|
|
9
|
-
FieldType.INTEGER: {"type": "integer"},
|
|
10
|
-
FieldType.FLOAT: {"type": "number"},
|
|
11
|
-
FieldType.BOOLEAN: {"type": "boolean"},
|
|
12
|
-
FieldType.DATE: {"type": "string", "format": "date"},
|
|
13
|
-
FieldType.DATETIME: {"type": "string", "format": "date-time"},
|
|
14
|
-
FieldType.TIME: {"type": "string", "format": "time"},
|
|
15
|
-
FieldType.UUID: {"type": "string", "format": "uuid"},
|
|
16
|
-
FieldType.JSON: {"type": "object"},
|
|
17
|
-
FieldType.TEXT: {"type": "string"},
|
|
18
|
-
FieldType.BINARY: {"type": "string", "format": "binary"},
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
SWAGGER_UI_HTML = """<!DOCTYPE html>
|
|
22
|
-
<html>
|
|
23
|
-
<head>
|
|
24
|
-
<title>{title} - Docs</title>
|
|
25
|
-
<meta charset="utf-8"/>
|
|
26
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
27
|
-
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
|
|
28
|
-
</head>
|
|
29
|
-
<body>
|
|
30
|
-
<div id="swagger-ui"></div>
|
|
31
|
-
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
|
|
32
|
-
<script>
|
|
33
|
-
SwaggerUIBundle({{
|
|
34
|
-
url: "{openapi_url}",
|
|
35
|
-
dom_id: '#swagger-ui',
|
|
36
|
-
presets: [
|
|
37
|
-
SwaggerUIBundle.presets.apis,
|
|
38
|
-
SwaggerUIBundle.SwaggerUIStandalonePreset
|
|
39
|
-
],
|
|
40
|
-
layout: "BaseLayout"
|
|
41
|
-
}})
|
|
42
|
-
</script>
|
|
43
|
-
</body>
|
|
44
|
-
</html>"""
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def generate_openapi_schema(
|
|
48
|
-
schemas: list[ModelSchema],
|
|
49
|
-
title: str = "FlashAPI",
|
|
50
|
-
version: str = "0.1.0",
|
|
51
|
-
description: str = "Define your models. FlashAPI does the rest.",
|
|
52
|
-
trailing_slash: bool = False,
|
|
53
|
-
) -> dict[str, Any]:
|
|
54
|
-
"""Generate a full OpenAPI 3.1.0 schema from model schemas."""
|
|
55
|
-
paths = {}
|
|
56
|
-
components_schemas = {}
|
|
57
|
-
|
|
58
|
-
for schema in schemas:
|
|
59
|
-
components_schemas[schema.name] = _build_model_schema(schema)
|
|
60
|
-
components_schemas[f"{schema.name}Create"] = _build_model_schema(schema,
|
|
61
|
-
model_paths = _build_paths(schema, trailing_slash=trailing_slash)
|
|
62
|
-
paths.update(model_paths)
|
|
63
|
-
|
|
64
|
-
return {
|
|
65
|
-
"openapi": "3.1.0",
|
|
66
|
-
"info": {
|
|
67
|
-
"title": title,
|
|
68
|
-
"version": version,
|
|
69
|
-
"description": description,
|
|
70
|
-
},
|
|
71
|
-
"paths": paths,
|
|
72
|
-
"components": {
|
|
73
|
-
"schemas": components_schemas,
|
|
74
|
-
},
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def _build_model_schema(schema: ModelSchema, *,
|
|
79
|
-
properties = {}
|
|
80
|
-
required = []
|
|
81
|
-
|
|
82
|
-
for field in schema.fields:
|
|
83
|
-
if
|
|
84
|
-
continue
|
|
85
|
-
prop = dict(FIELD_TYPE_TO_OPENAPI.get(field.type, {"type": "string"}))
|
|
86
|
-
if field.constraints.get("max_length"):
|
|
87
|
-
prop["maxLength"] = field.constraints["max_length"]
|
|
88
|
-
if field.constraints.get("min_length"):
|
|
89
|
-
prop["minLength"] = field.constraints["min_length"]
|
|
90
|
-
if field.constraints.get("min_value") is not None:
|
|
91
|
-
prop["minimum"] = field.constraints["min_value"]
|
|
92
|
-
if field.constraints.get("max_value") is not None:
|
|
93
|
-
prop["maximum"] = field.constraints["max_value"]
|
|
94
|
-
properties[field.name] = prop
|
|
95
|
-
if field.required and not field.primary_key:
|
|
96
|
-
required.append(field.name)
|
|
97
|
-
|
|
98
|
-
result = {"type": "object", "properties": properties}
|
|
99
|
-
if required:
|
|
100
|
-
result["required"] = required
|
|
101
|
-
return result
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
},
|
|
174
|
-
"
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
"
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
1
|
+
"""OpenAPI schema generation and Swagger UI for Flask and Django."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from flashapi.core.schema import FieldType, ModelSchema
|
|
6
|
+
|
|
7
|
+
FIELD_TYPE_TO_OPENAPI = {
|
|
8
|
+
FieldType.STRING: {"type": "string"},
|
|
9
|
+
FieldType.INTEGER: {"type": "integer"},
|
|
10
|
+
FieldType.FLOAT: {"type": "number"},
|
|
11
|
+
FieldType.BOOLEAN: {"type": "boolean"},
|
|
12
|
+
FieldType.DATE: {"type": "string", "format": "date"},
|
|
13
|
+
FieldType.DATETIME: {"type": "string", "format": "date-time"},
|
|
14
|
+
FieldType.TIME: {"type": "string", "format": "time"},
|
|
15
|
+
FieldType.UUID: {"type": "string", "format": "uuid"},
|
|
16
|
+
FieldType.JSON: {"type": "object"},
|
|
17
|
+
FieldType.TEXT: {"type": "string"},
|
|
18
|
+
FieldType.BINARY: {"type": "string", "format": "binary"},
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
SWAGGER_UI_HTML = """<!DOCTYPE html>
|
|
22
|
+
<html>
|
|
23
|
+
<head>
|
|
24
|
+
<title>{title} - Docs</title>
|
|
25
|
+
<meta charset="utf-8"/>
|
|
26
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
27
|
+
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
|
|
28
|
+
</head>
|
|
29
|
+
<body>
|
|
30
|
+
<div id="swagger-ui"></div>
|
|
31
|
+
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
|
|
32
|
+
<script>
|
|
33
|
+
SwaggerUIBundle({{
|
|
34
|
+
url: "{openapi_url}",
|
|
35
|
+
dom_id: '#swagger-ui',
|
|
36
|
+
presets: [
|
|
37
|
+
SwaggerUIBundle.presets.apis,
|
|
38
|
+
SwaggerUIBundle.SwaggerUIStandalonePreset
|
|
39
|
+
],
|
|
40
|
+
layout: "BaseLayout"
|
|
41
|
+
}})
|
|
42
|
+
</script>
|
|
43
|
+
</body>
|
|
44
|
+
</html>"""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def generate_openapi_schema(
|
|
48
|
+
schemas: list[ModelSchema],
|
|
49
|
+
title: str = "FlashAPI",
|
|
50
|
+
version: str = "0.1.0",
|
|
51
|
+
description: str = "Define your models. FlashAPI does the rest.",
|
|
52
|
+
trailing_slash: bool = False,
|
|
53
|
+
) -> dict[str, Any]:
|
|
54
|
+
"""Generate a full OpenAPI 3.1.0 schema from model schemas."""
|
|
55
|
+
paths = {}
|
|
56
|
+
components_schemas = {}
|
|
57
|
+
|
|
58
|
+
for schema in schemas:
|
|
59
|
+
components_schemas[schema.name] = _build_model_schema(schema)
|
|
60
|
+
components_schemas[f"{schema.name}Create"] = _build_model_schema(schema, exclude_auto=True)
|
|
61
|
+
model_paths = _build_paths(schema, trailing_slash=trailing_slash)
|
|
62
|
+
paths.update(model_paths)
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
"openapi": "3.1.0",
|
|
66
|
+
"info": {
|
|
67
|
+
"title": title,
|
|
68
|
+
"version": version,
|
|
69
|
+
"description": description,
|
|
70
|
+
},
|
|
71
|
+
"paths": paths,
|
|
72
|
+
"components": {
|
|
73
|
+
"schemas": components_schemas,
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _build_model_schema(schema: ModelSchema, *, exclude_auto: bool = False) -> dict:
|
|
79
|
+
properties = {}
|
|
80
|
+
required = []
|
|
81
|
+
|
|
82
|
+
for field in schema.fields:
|
|
83
|
+
if exclude_auto and field.auto_generated:
|
|
84
|
+
continue
|
|
85
|
+
prop = dict(FIELD_TYPE_TO_OPENAPI.get(field.type, {"type": "string"}))
|
|
86
|
+
if field.constraints.get("max_length"):
|
|
87
|
+
prop["maxLength"] = field.constraints["max_length"]
|
|
88
|
+
if field.constraints.get("min_length"):
|
|
89
|
+
prop["minLength"] = field.constraints["min_length"]
|
|
90
|
+
if field.constraints.get("min_value") is not None:
|
|
91
|
+
prop["minimum"] = field.constraints["min_value"]
|
|
92
|
+
if field.constraints.get("max_value") is not None:
|
|
93
|
+
prop["maximum"] = field.constraints["max_value"]
|
|
94
|
+
properties[field.name] = prop
|
|
95
|
+
if field.required and not field.primary_key:
|
|
96
|
+
required.append(field.name)
|
|
97
|
+
|
|
98
|
+
result = {"type": "object", "properties": properties}
|
|
99
|
+
if required:
|
|
100
|
+
result["required"] = required
|
|
101
|
+
return result
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _resolve_lookup_type(schema: ModelSchema) -> dict:
|
|
105
|
+
"""Get the OpenAPI type for the lookup field (item_id parameter)."""
|
|
106
|
+
lf = schema.lookup_field or "id"
|
|
107
|
+
for field in schema.fields:
|
|
108
|
+
if field.name == lf or (field.primary_key and lf == "id"):
|
|
109
|
+
return dict(FIELD_TYPE_TO_OPENAPI.get(field.type, {"type": "string"}))
|
|
110
|
+
return {"type": "string"}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _build_paths(schema: ModelSchema, trailing_slash: bool = False) -> dict:
|
|
114
|
+
paths = {}
|
|
115
|
+
table = schema.plural
|
|
116
|
+
tag = schema.name
|
|
117
|
+
suffix = "/" if trailing_slash else ""
|
|
118
|
+
lookup_schema = _resolve_lookup_type(schema)
|
|
119
|
+
|
|
120
|
+
collection_ops = {}
|
|
121
|
+
detail_ops = {}
|
|
122
|
+
|
|
123
|
+
if "list" in schema.permissions:
|
|
124
|
+
collection_ops["get"] = {
|
|
125
|
+
"tags": [tag],
|
|
126
|
+
"summary": f"List all {table}",
|
|
127
|
+
"parameters": [
|
|
128
|
+
{"name": "page", "in": "query", "schema": {"type": "integer", "default": 0}},
|
|
129
|
+
{"name": "size", "in": "query", "schema": {"type": "integer", "default": 20}},
|
|
130
|
+
{"name": "sort", "in": "query", "schema": {"type": "string"}},
|
|
131
|
+
{"name": "search", "in": "query", "schema": {"type": "string"}},
|
|
132
|
+
{"name": "deleted", "in": "query", "schema": {"type": "boolean", "default": False}},
|
|
133
|
+
{"name": "expand", "in": "query", "schema": {"type": "string"}},
|
|
134
|
+
],
|
|
135
|
+
"responses": {
|
|
136
|
+
"200": {
|
|
137
|
+
"description": "Paginated list",
|
|
138
|
+
"content": {"application/json": {"schema": {
|
|
139
|
+
"type": "object",
|
|
140
|
+
"properties": {
|
|
141
|
+
"data": {"type": "array", "items": {"$ref": f"#/components/schemas/{schema.name}"}},
|
|
142
|
+
"meta": {"type": "object"},
|
|
143
|
+
},
|
|
144
|
+
}}},
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if "create" in schema.permissions:
|
|
150
|
+
collection_ops["post"] = {
|
|
151
|
+
"tags": [tag],
|
|
152
|
+
"summary": f"Create a {schema.name.lower()}",
|
|
153
|
+
"requestBody": {
|
|
154
|
+
"required": True,
|
|
155
|
+
"content": {"application/json": {"schema": {"$ref": f"#/components/schemas/{schema.name}Create"}}},
|
|
156
|
+
},
|
|
157
|
+
"responses": {
|
|
158
|
+
"201": {
|
|
159
|
+
"description": "Created",
|
|
160
|
+
"content": {"application/json": {"schema": {
|
|
161
|
+
"type": "object",
|
|
162
|
+
"properties": {"data": {"$ref": f"#/components/schemas/{schema.name}"}},
|
|
163
|
+
}}},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if "read" in schema.permissions:
|
|
169
|
+
detail_ops["get"] = {
|
|
170
|
+
"tags": [tag],
|
|
171
|
+
"summary": f"Get a {schema.name.lower()} by ID",
|
|
172
|
+
"parameters": [
|
|
173
|
+
{"name": "item_id", "in": "path", "required": True, "schema": lookup_schema},
|
|
174
|
+
{"name": "expand", "in": "query", "schema": {"type": "string"}},
|
|
175
|
+
],
|
|
176
|
+
"responses": {
|
|
177
|
+
"200": {
|
|
178
|
+
"description": "Item detail",
|
|
179
|
+
"content": {"application/json": {"schema": {
|
|
180
|
+
"type": "object",
|
|
181
|
+
"properties": {"data": {"$ref": f"#/components/schemas/{schema.name}"}},
|
|
182
|
+
}}},
|
|
183
|
+
},
|
|
184
|
+
"404": {"description": "Not found"},
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if "update" in schema.permissions:
|
|
189
|
+
detail_ops["put"] = {
|
|
190
|
+
"tags": [tag],
|
|
191
|
+
"summary": f"Update a {schema.name.lower()}",
|
|
192
|
+
"parameters": [
|
|
193
|
+
{"name": "item_id", "in": "path", "required": True, "schema": lookup_schema},
|
|
194
|
+
],
|
|
195
|
+
"requestBody": {
|
|
196
|
+
"required": True,
|
|
197
|
+
"content": {"application/json": {"schema": {"$ref": f"#/components/schemas/{schema.name}Create"}}},
|
|
198
|
+
},
|
|
199
|
+
"responses": {
|
|
200
|
+
"200": {
|
|
201
|
+
"description": "Updated",
|
|
202
|
+
"content": {"application/json": {"schema": {
|
|
203
|
+
"type": "object",
|
|
204
|
+
"properties": {"data": {"$ref": f"#/components/schemas/{schema.name}"}},
|
|
205
|
+
}}},
|
|
206
|
+
},
|
|
207
|
+
"404": {"description": "Not found"},
|
|
208
|
+
},
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if "delete" in schema.permissions:
|
|
212
|
+
detail_ops["delete"] = {
|
|
213
|
+
"tags": [tag],
|
|
214
|
+
"summary": f"{'Soft delete' if schema.soft_delete else 'Delete'} a {schema.name.lower()}",
|
|
215
|
+
"parameters": [
|
|
216
|
+
{"name": "item_id", "in": "path", "required": True, "schema": lookup_schema},
|
|
217
|
+
],
|
|
218
|
+
"responses": {
|
|
219
|
+
"204": {"description": "Deleted"},
|
|
220
|
+
"404": {"description": "Not found"},
|
|
221
|
+
},
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if collection_ops:
|
|
225
|
+
paths[f"/{table}{suffix}"] = collection_ops
|
|
226
|
+
if detail_ops:
|
|
227
|
+
paths[f"/{table}/{{item_id}}{suffix}"] = detail_ops
|
|
228
|
+
|
|
229
|
+
if "delete" in schema.permissions and schema.soft_delete:
|
|
230
|
+
paths[f"/{table}/{{item_id}}/restore{suffix}"] = {
|
|
231
|
+
"post": {
|
|
232
|
+
"tags": [tag],
|
|
233
|
+
"summary": f"Restore soft-deleted {schema.name.lower()}",
|
|
234
|
+
"parameters": [
|
|
235
|
+
{"name": "item_id", "in": "path", "required": True, "schema": lookup_schema},
|
|
236
|
+
],
|
|
237
|
+
"responses": {
|
|
238
|
+
"204": {"description": "Restored"},
|
|
239
|
+
"404": {"description": "Not found"},
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if "create" in schema.permissions:
|
|
245
|
+
paths[f"/{table}/bulk{suffix}"] = {
|
|
246
|
+
"post": {
|
|
247
|
+
"tags": [tag],
|
|
248
|
+
"summary": f"Bulk create {table}",
|
|
249
|
+
"requestBody": {
|
|
250
|
+
"required": True,
|
|
251
|
+
"content": {"application/json": {"schema": {
|
|
252
|
+
"type": "array",
|
|
253
|
+
"items": {"$ref": f"#/components/schemas/{schema.name}Create"},
|
|
254
|
+
}}},
|
|
255
|
+
},
|
|
256
|
+
"responses": {
|
|
257
|
+
"201": {
|
|
258
|
+
"description": "Bulk created",
|
|
259
|
+
"content": {"application/json": {"schema": {"type": "object"}}},
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if "list" in schema.permissions:
|
|
266
|
+
paths[f"/{table}/export{suffix}"] = {
|
|
267
|
+
"get": {
|
|
268
|
+
"tags": [tag],
|
|
269
|
+
"summary": f"Export {table}",
|
|
270
|
+
"parameters": [
|
|
271
|
+
{"name": "format", "in": "query", "schema": {"type": "string", "default": "csv"}},
|
|
272
|
+
],
|
|
273
|
+
"responses": {
|
|
274
|
+
"200": {"description": "Export file"},
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if "read" in schema.permissions and schema.audit:
|
|
280
|
+
paths[f"/{table}/{{item_id}}/history{suffix}"] = {
|
|
281
|
+
"get": {
|
|
282
|
+
"tags": [tag],
|
|
283
|
+
"summary": f"Audit history for {schema.name.lower()}",
|
|
284
|
+
"parameters": [
|
|
285
|
+
{"name": "item_id", "in": "path", "required": True, "schema": lookup_schema},
|
|
286
|
+
],
|
|
287
|
+
"responses": {
|
|
288
|
+
"200": {"description": "Audit history"},
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return paths
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def get_swagger_html(title: str = "FlashAPI", openapi_url: str = "/openapi.json") -> str:
|
|
298
|
+
return SWAGGER_UI_HTML.format(title=title, openapi_url=openapi_url)
|
flashapi/fastapi.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
"""FlashAPI FastAPI adapter — public entry point."""
|
|
2
|
-
|
|
3
|
-
from flashapi.adapters.fastapi import FlashAPI
|
|
4
|
-
|
|
5
|
-
__all__ = ["FlashAPI"]
|
|
1
|
+
"""FlashAPI FastAPI adapter — public entry point."""
|
|
2
|
+
|
|
3
|
+
from flashapi.adapters.fastapi import FlashAPI
|
|
4
|
+
|
|
5
|
+
__all__ = ["FlashAPI"]
|
flashapi/features/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
from flashapi.features.
|
|
2
|
-
from flashapi.features.
|
|
3
|
-
from flashapi.features.
|
|
4
|
-
from flashapi.features.
|
|
5
|
-
|
|
6
|
-
__all__ = ["
|
|
1
|
+
from flashapi.features.filtering import apply_filters
|
|
2
|
+
from flashapi.features.pagination import paginate
|
|
3
|
+
from flashapi.features.search import apply_search
|
|
4
|
+
from flashapi.features.sorting import apply_sorting
|
|
5
|
+
|
|
6
|
+
__all__ = ["apply_filters", "apply_search", "apply_sorting", "paginate"]
|