drf-to-mkdoc 0.1.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.
Potentially problematic release.
This version of drf-to-mkdoc might be problematic. Click here for more details.
- drf_to_mkdoc/__init__.py +7 -0
- drf_to_mkdoc/apps.py +15 -0
- drf_to_mkdoc/conf/__init__.py +0 -0
- drf_to_mkdoc/conf/defaults.py +11 -0
- drf_to_mkdoc/conf/settings.py +44 -0
- drf_to_mkdoc/management/__init__.py +0 -0
- drf_to_mkdoc/management/commands/__init__.py +0 -0
- drf_to_mkdoc/management/commands/build_docs.py +76 -0
- drf_to_mkdoc/management/commands/generate_doc_json.py +512 -0
- drf_to_mkdoc/management/commands/generate_docs.py +138 -0
- drf_to_mkdoc/management/commands/generate_model_docs.py +327 -0
- drf_to_mkdoc/management/commands/update_doc_schema.py +53 -0
- drf_to_mkdoc/utils/__init__.py +3 -0
- drf_to_mkdoc/utils/common.py +292 -0
- drf_to_mkdoc/utils/endpoint_generator.py +945 -0
- drf_to_mkdoc/utils/extractors/__init__.py +3 -0
- drf_to_mkdoc/utils/extractors/query_parameter_extractors.py +229 -0
- drf_to_mkdoc/utils/md_generators/__init__.py +0 -0
- drf_to_mkdoc/utils/md_generators/query_parameters_generators.py +72 -0
- drf_to_mkdoc/utils/model_generator.py +269 -0
- drf_to_mkdoc-0.1.0.dist-info/METADATA +247 -0
- drf_to_mkdoc-0.1.0.dist-info/RECORD +25 -0
- drf_to_mkdoc-0.1.0.dist-info/WHEEL +5 -0
- drf_to_mkdoc-0.1.0.dist-info/licenses/LICENSE +21 -0
- drf_to_mkdoc-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from django.core.management.base import BaseCommand
|
|
6
|
+
|
|
7
|
+
from drf_to_mkdoc.utils.common import get_schema, load_model_json_data
|
|
8
|
+
from drf_to_mkdoc.utils.endpoint_generator import (
|
|
9
|
+
create_endpoints_index,
|
|
10
|
+
generate_endpoint_files,
|
|
11
|
+
parse_endpoints_from_schema,
|
|
12
|
+
)
|
|
13
|
+
from drf_to_mkdoc.utils.model_generator import create_models_index, generate_model_docs
|
|
14
|
+
from drf_to_mkdoc.conf.settings import drf_to_mkdoc_settings
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Command(BaseCommand):
|
|
19
|
+
help = "Generate complete API documentation (models + endpoints + navigation)"
|
|
20
|
+
|
|
21
|
+
def add_arguments(self, parser):
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--endpoints-only",
|
|
24
|
+
action="store_true",
|
|
25
|
+
help="Generate only endpoint documentation",
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"--models-only",
|
|
29
|
+
action="store_true",
|
|
30
|
+
help="Generate only model documentation",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def handle(self, *args, **options):
|
|
34
|
+
self.stdout.write(self.style.SUCCESS("🚀 Starting documentation generation..."))
|
|
35
|
+
|
|
36
|
+
docs_dir = Path(drf_to_mkdoc_settings.DOCS_DIR)
|
|
37
|
+
docs_dir.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
|
|
39
|
+
if options["models_only"]:
|
|
40
|
+
self._generate_models_only()
|
|
41
|
+
elif options["endpoints_only"]:
|
|
42
|
+
self._generate_endpoints_only()
|
|
43
|
+
else:
|
|
44
|
+
self._generate_all()
|
|
45
|
+
|
|
46
|
+
self.stdout.write(self.style.SUCCESS("✅ Documentation generation complete!"))
|
|
47
|
+
|
|
48
|
+
def _generate_models_only(self):
|
|
49
|
+
"""Generate only model documentation"""
|
|
50
|
+
self.stdout.write("📋 Generating model documentation...")
|
|
51
|
+
|
|
52
|
+
# Load model data
|
|
53
|
+
json_data = load_model_json_data()
|
|
54
|
+
models_data = json_data.get("models", {}) if json_data else {}
|
|
55
|
+
|
|
56
|
+
if not models_data:
|
|
57
|
+
self.stdout.write(self.style.WARNING("⚠️ No model data found"))
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
docs_dir = Path(drf_to_mkdoc_settings.DOCS_DIR)
|
|
61
|
+
|
|
62
|
+
# Generate model documentation
|
|
63
|
+
generate_model_docs(models_data, docs_dir)
|
|
64
|
+
create_models_index(models_data, docs_dir)
|
|
65
|
+
|
|
66
|
+
self.stdout.write(self.style.SUCCESS("✅ Model documentation generated"))
|
|
67
|
+
|
|
68
|
+
def _generate_endpoints_only(self):
|
|
69
|
+
"""Generate only endpoint documentation"""
|
|
70
|
+
self.stdout.write("🔗 Generating endpoint documentation...")
|
|
71
|
+
|
|
72
|
+
# Load schema
|
|
73
|
+
schema = get_schema()
|
|
74
|
+
if not schema:
|
|
75
|
+
self.stdout.write(self.style.ERROR("❌ Failed to load OpenAPI schema"))
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
paths = schema.get("paths", {})
|
|
79
|
+
components = schema.get("components", {})
|
|
80
|
+
|
|
81
|
+
self.stdout.write(f"📊 Loaded {len(paths)} API paths")
|
|
82
|
+
|
|
83
|
+
docs_dir = Path(drf_to_mkdoc_settings.DOCS_DIR)
|
|
84
|
+
|
|
85
|
+
# Parse and generate endpoints
|
|
86
|
+
endpoints_by_app = parse_endpoints_from_schema(paths)
|
|
87
|
+
total_endpoints = generate_endpoint_files(endpoints_by_app, components)
|
|
88
|
+
create_endpoints_index(endpoints_by_app, docs_dir)
|
|
89
|
+
|
|
90
|
+
self.stdout.write(
|
|
91
|
+
self.style.SUCCESS(
|
|
92
|
+
f"✅ Generated {total_endpoints} endpoint files with Django view introspection"
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def _generate_all(self):
|
|
97
|
+
"""Generate complete documentation"""
|
|
98
|
+
self.stdout.write("📚 Generating complete documentation...")
|
|
99
|
+
|
|
100
|
+
docs_dir = Path(drf_to_mkdoc_settings.DOCS_DIR)
|
|
101
|
+
|
|
102
|
+
# Load data
|
|
103
|
+
json_data = load_model_json_data()
|
|
104
|
+
models_data = json_data.get("models", {}) if json_data else {}
|
|
105
|
+
schema = get_schema()
|
|
106
|
+
|
|
107
|
+
if not schema:
|
|
108
|
+
self.stdout.write(self.style.ERROR("❌ Failed to load OpenAPI schema"))
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
paths = schema.get("paths", {})
|
|
112
|
+
components = schema.get("components", {})
|
|
113
|
+
|
|
114
|
+
self.stdout.write(f"📊 Loaded {len(paths)} API paths")
|
|
115
|
+
|
|
116
|
+
# Generate model documentation
|
|
117
|
+
if models_data:
|
|
118
|
+
self.stdout.write("📋 Generating model documentation...")
|
|
119
|
+
try:
|
|
120
|
+
generate_model_docs(models_data)
|
|
121
|
+
create_models_index(models_data, docs_dir)
|
|
122
|
+
except Exception as e:
|
|
123
|
+
self.stdout.write(self.style.WARNING(f"⚠️ Failed to generate model docs: {e}"))
|
|
124
|
+
self.stdout.write(self.style.WARNING("Continuing with endpoint generation..."))
|
|
125
|
+
else:
|
|
126
|
+
self.stdout.write(self.style.WARNING("⚠️ No model data found"))
|
|
127
|
+
|
|
128
|
+
# Generate endpoint documentation
|
|
129
|
+
self.stdout.write("🔗 Generating endpoint documentation...")
|
|
130
|
+
endpoints_by_app = parse_endpoints_from_schema(paths)
|
|
131
|
+
total_endpoints = generate_endpoint_files(endpoints_by_app, components)
|
|
132
|
+
create_endpoints_index(endpoints_by_app, docs_dir)
|
|
133
|
+
|
|
134
|
+
self.stdout.write(
|
|
135
|
+
self.style.SUCCESS(
|
|
136
|
+
f"✅ Generated {total_endpoints} endpoint files with Django view introspection"
|
|
137
|
+
)
|
|
138
|
+
)
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from django.apps import apps
|
|
7
|
+
from django.core.management.base import BaseCommand
|
|
8
|
+
from django.db import models
|
|
9
|
+
|
|
10
|
+
from drf_to_mkdoc.conf.settings import drf_to_mkdoc_settings
|
|
11
|
+
|
|
12
|
+
class Command(BaseCommand):
|
|
13
|
+
help = "Generate model documentation JSON from Django model introspection"
|
|
14
|
+
|
|
15
|
+
def add_arguments(self, parser):
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
"--output",
|
|
18
|
+
type=str,
|
|
19
|
+
default=drf_to_mkdoc_settings.MODEL_DOCS_FILE,
|
|
20
|
+
help=f"Output JSON file name (default: {drf_to_mkdoc_settings.MODEL_DOCS_FILE})",
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--exclude-apps",
|
|
24
|
+
type=str,
|
|
25
|
+
nargs="*",
|
|
26
|
+
default=["admin", "auth", "contenttypes", "sessions", "messages", "staticfiles"],
|
|
27
|
+
help="Apps to exclude from documentation",
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument("--pretty", action="store_true", help="Pretty print JSON output")
|
|
30
|
+
|
|
31
|
+
def handle(self, *args, **options):
|
|
32
|
+
output_file = options["output"]
|
|
33
|
+
exclude_apps = options["exclude_apps"]
|
|
34
|
+
pretty = options["pretty"]
|
|
35
|
+
|
|
36
|
+
self.stdout.write(self.style.SUCCESS("🔍 Scanning Django models..."))
|
|
37
|
+
|
|
38
|
+
model_docs = self.generate_model_documentation(exclude_apps)
|
|
39
|
+
|
|
40
|
+
json_data = {
|
|
41
|
+
"models": model_docs,
|
|
42
|
+
"stats": {
|
|
43
|
+
"total_models": len(model_docs),
|
|
44
|
+
"total_apps": len({model["app_label"] for model in model_docs.values()}),
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
output_path = Path(output_file)
|
|
49
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
with output_path.open("w", encoding="utf-8") as f:
|
|
51
|
+
if pretty:
|
|
52
|
+
json.dump(json_data, f, indent=2, ensure_ascii=False, default=str)
|
|
53
|
+
else:
|
|
54
|
+
json.dump(json_data, f, ensure_ascii=False, default=str)
|
|
55
|
+
|
|
56
|
+
self.stdout.write(
|
|
57
|
+
self.style.SUCCESS(f"✅ Generated model documentation: {output_path.absolute()}")
|
|
58
|
+
)
|
|
59
|
+
self.stdout.write(f"📊 Total models: {len(model_docs)}")
|
|
60
|
+
self.stdout.write(
|
|
61
|
+
f"📦 Total apps: {len({model['app_label'] for model in model_docs.values()})}"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def generate_model_documentation(self, exclude_apps):
|
|
65
|
+
"""Generate documentation for all Django models"""
|
|
66
|
+
model_docs = {}
|
|
67
|
+
|
|
68
|
+
for app_config in apps.get_app_configs():
|
|
69
|
+
app_label = app_config.label
|
|
70
|
+
|
|
71
|
+
# Skip excluded apps
|
|
72
|
+
if app_label in exclude_apps:
|
|
73
|
+
self.stdout.write(f"⏭️ Skipping app: {app_label}")
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
self.stdout.write(f"📱 Processing app: {app_label}")
|
|
77
|
+
|
|
78
|
+
for model in app_config.get_models():
|
|
79
|
+
model_name = model.__name__
|
|
80
|
+
model_key = f"{app_label}.{model_name}"
|
|
81
|
+
|
|
82
|
+
self.stdout.write(f" 📋 Processing model: {model_name}")
|
|
83
|
+
|
|
84
|
+
model_docs[model_key] = self.introspect_model(model, app_label)
|
|
85
|
+
|
|
86
|
+
return model_docs
|
|
87
|
+
|
|
88
|
+
def introspect_model(self, model, app_label):
|
|
89
|
+
"""Introspect a single Django model"""
|
|
90
|
+
meta = model._meta
|
|
91
|
+
|
|
92
|
+
# Basic model information
|
|
93
|
+
model_doc = {
|
|
94
|
+
"name": model.__name__,
|
|
95
|
+
"app_label": app_label,
|
|
96
|
+
"table_name": meta.db_table,
|
|
97
|
+
"verbose_name": str(meta.verbose_name),
|
|
98
|
+
"verbose_name_plural": str(meta.verbose_name_plural),
|
|
99
|
+
"description": self.get_model_description(model),
|
|
100
|
+
"abstract": meta.abstract,
|
|
101
|
+
"proxy": meta.proxy,
|
|
102
|
+
"fields": {},
|
|
103
|
+
"relationships": {},
|
|
104
|
+
"meta_options": self.get_meta_options(meta),
|
|
105
|
+
"methods": self.get_model_methods(model),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Process fields
|
|
109
|
+
for field in meta.get_fields():
|
|
110
|
+
if field.many_to_many or field.one_to_many or field.many_to_one or field.one_to_one:
|
|
111
|
+
# Handle relationships separately
|
|
112
|
+
model_doc["relationships"][field.name] = self.introspect_relationship(field)
|
|
113
|
+
else:
|
|
114
|
+
# Handle regular fields
|
|
115
|
+
model_doc["fields"][field.name] = self.introspect_field(field)
|
|
116
|
+
|
|
117
|
+
return model_doc
|
|
118
|
+
|
|
119
|
+
def introspect_field(self, field):
|
|
120
|
+
"""Introspect a single model field"""
|
|
121
|
+
return {
|
|
122
|
+
"name": field.name,
|
|
123
|
+
"type": field.__class__.__name__,
|
|
124
|
+
"verbose_name": (
|
|
125
|
+
str(field.verbose_name) if hasattr(field, "verbose_name") else field.name
|
|
126
|
+
),
|
|
127
|
+
"help_text": field.help_text if hasattr(field, "help_text") else "",
|
|
128
|
+
"null": getattr(field, "null", False),
|
|
129
|
+
"blank": getattr(field, "blank", False),
|
|
130
|
+
"editable": getattr(field, "editable", True),
|
|
131
|
+
"primary_key": getattr(field, "primary_key", False),
|
|
132
|
+
"unique": getattr(field, "unique", False),
|
|
133
|
+
"db_index": getattr(field, "db_index", False),
|
|
134
|
+
"default": self.get_field_default(field),
|
|
135
|
+
"choices": self.get_field_choices(field),
|
|
136
|
+
"validators": self.get_field_validators(field),
|
|
137
|
+
"field_specific": self.get_field_specific_attrs(field),
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
def introspect_relationship(self, field):
|
|
141
|
+
"""Introspect relationship fields"""
|
|
142
|
+
return {
|
|
143
|
+
"name": field.name,
|
|
144
|
+
"type": field.__class__.__name__,
|
|
145
|
+
"related_model": f"{field.related_model._meta.app_label}."
|
|
146
|
+
f"{field.related_model.__name__}",
|
|
147
|
+
"related_name": getattr(field, "related_name", None),
|
|
148
|
+
"on_delete": self.get_on_delete_name(field),
|
|
149
|
+
"null": getattr(field, "null", False),
|
|
150
|
+
"blank": getattr(field, "blank", False),
|
|
151
|
+
"many_to_many": field.many_to_many,
|
|
152
|
+
"one_to_many": field.one_to_many,
|
|
153
|
+
"many_to_one": field.many_to_one,
|
|
154
|
+
"one_to_one": field.one_to_one,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
def get_on_delete_name(self, field):
|
|
158
|
+
"""Get readable name for on_delete option"""
|
|
159
|
+
if not hasattr(field, "on_delete") or field.on_delete is None:
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
# Import Django's on_delete functions
|
|
163
|
+
|
|
164
|
+
# Map function objects to their readable names
|
|
165
|
+
on_delete_mapping = {
|
|
166
|
+
models.CASCADE: "CASCADE",
|
|
167
|
+
models.PROTECT: "PROTECT",
|
|
168
|
+
models.SET_NULL: "SET_NULL",
|
|
169
|
+
models.SET_DEFAULT: "SET_DEFAULT",
|
|
170
|
+
models.SET: "SET",
|
|
171
|
+
models.DO_NOTHING: "DO_NOTHING",
|
|
172
|
+
models.RESTRICT: "RESTRICT",
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
on_delete_func = field.on_delete
|
|
176
|
+
|
|
177
|
+
# Handle SET() callable
|
|
178
|
+
if hasattr(on_delete_func, "__name__") and on_delete_func.__name__ == "SET":
|
|
179
|
+
return "SET"
|
|
180
|
+
|
|
181
|
+
# Check if it's one of the standard Django on_delete options
|
|
182
|
+
for func, name in on_delete_mapping.items():
|
|
183
|
+
if on_delete_func is func:
|
|
184
|
+
return name
|
|
185
|
+
|
|
186
|
+
# Fallback for custom functions or unknown cases
|
|
187
|
+
return getattr(on_delete_func, "__name__", str(on_delete_func))
|
|
188
|
+
|
|
189
|
+
def get_model_description(self, model):
|
|
190
|
+
"""Get model description from docstring or generate one"""
|
|
191
|
+
if model.__doc__ and not model.__doc__.startswith(model.__name__ + "("):
|
|
192
|
+
# Only use docstring if it's not just the auto-generated field list
|
|
193
|
+
return model.__doc__.strip()
|
|
194
|
+
return ""
|
|
195
|
+
|
|
196
|
+
def get_meta_options(self, meta):
|
|
197
|
+
"""Extract Meta class options"""
|
|
198
|
+
options = {}
|
|
199
|
+
|
|
200
|
+
# Common Meta options
|
|
201
|
+
meta_attrs = [
|
|
202
|
+
"ordering",
|
|
203
|
+
"unique_together",
|
|
204
|
+
"index_together",
|
|
205
|
+
"constraints",
|
|
206
|
+
"indexes",
|
|
207
|
+
"permissions",
|
|
208
|
+
"default_permissions",
|
|
209
|
+
"get_latest_by",
|
|
210
|
+
"order_with_respect_to",
|
|
211
|
+
"managed",
|
|
212
|
+
"default_manager_name",
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
for attr in meta_attrs:
|
|
216
|
+
if hasattr(meta, attr):
|
|
217
|
+
value = getattr(meta, attr)
|
|
218
|
+
if value:
|
|
219
|
+
options[attr] = str(value)
|
|
220
|
+
|
|
221
|
+
return options
|
|
222
|
+
|
|
223
|
+
def get_model_methods(self, model):
|
|
224
|
+
"""Get custom model methods (excluding built-in Django methods)"""
|
|
225
|
+
methods = []
|
|
226
|
+
|
|
227
|
+
# Get all methods that don't start with underscore and aren't Django built-ins
|
|
228
|
+
django_methods = {
|
|
229
|
+
"save",
|
|
230
|
+
"delete",
|
|
231
|
+
"clean",
|
|
232
|
+
"full_clean",
|
|
233
|
+
"validate_unique",
|
|
234
|
+
"get_absolute_url",
|
|
235
|
+
"get_next_by_",
|
|
236
|
+
"get_previous_by_",
|
|
237
|
+
"refresh_from_db",
|
|
238
|
+
"serializable_value",
|
|
239
|
+
"check",
|
|
240
|
+
"from_db",
|
|
241
|
+
"clean_fields",
|
|
242
|
+
"get_deferred_fields",
|
|
243
|
+
"pk",
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
model_field_names = {field.name for field in model._meta.get_fields()}
|
|
247
|
+
|
|
248
|
+
for attr_name in dir(model):
|
|
249
|
+
if (
|
|
250
|
+
not attr_name.startswith("_")
|
|
251
|
+
and not attr_name.startswith("get_next_by_")
|
|
252
|
+
and not attr_name.startswith("get_previous_by_")
|
|
253
|
+
and attr_name not in django_methods
|
|
254
|
+
and callable(getattr(model, attr_name))
|
|
255
|
+
):
|
|
256
|
+
display_method_match = re.match(r"^get_(.+)_display$", attr_name)
|
|
257
|
+
if display_method_match:
|
|
258
|
+
field_name = display_method_match.group(1)
|
|
259
|
+
if field_name in model_field_names:
|
|
260
|
+
# Exclude if the field doesn't exist in the model
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
method = getattr(model, attr_name)
|
|
264
|
+
|
|
265
|
+
# Check if it's a method defined in this class (not inherited from Django)
|
|
266
|
+
if (
|
|
267
|
+
inspect.ismethod(method)
|
|
268
|
+
or inspect.isfunction(method)
|
|
269
|
+
or (hasattr(method, "__func__") and inspect.isfunction(method.__func__))
|
|
270
|
+
):
|
|
271
|
+
# Check if method is defined in the model class itself
|
|
272
|
+
if hasattr(model, attr_name) and attr_name in model.__dict__:
|
|
273
|
+
methods.append(
|
|
274
|
+
{
|
|
275
|
+
"name": attr_name,
|
|
276
|
+
"docstring": method.__doc__.strip() if method.__doc__ else "",
|
|
277
|
+
}
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
return methods
|
|
281
|
+
|
|
282
|
+
def get_field_default(self, field):
|
|
283
|
+
"""Get field default value"""
|
|
284
|
+
if hasattr(field, "default") and field.default is not models.NOT_PROVIDED:
|
|
285
|
+
default = field.default
|
|
286
|
+
if callable(default):
|
|
287
|
+
return f"<callable: {default.__name__}>"
|
|
288
|
+
return str(default)
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
def get_field_choices(self, field):
|
|
292
|
+
"""Get field choices"""
|
|
293
|
+
if hasattr(field, "choices") and field.choices:
|
|
294
|
+
return [{"value": choice[0], "display": choice[1]} for choice in field.choices]
|
|
295
|
+
return []
|
|
296
|
+
|
|
297
|
+
def get_field_validators(self, field):
|
|
298
|
+
"""Get field validators"""
|
|
299
|
+
if hasattr(field, "validators") and field.validators:
|
|
300
|
+
return [validator.__class__.__name__ for validator in field.validators]
|
|
301
|
+
return []
|
|
302
|
+
|
|
303
|
+
def get_field_specific_attrs(self, field):
|
|
304
|
+
"""Get field-specific attributes"""
|
|
305
|
+
specific_attrs = {}
|
|
306
|
+
|
|
307
|
+
# CharField, TextField
|
|
308
|
+
if hasattr(field, "max_length") and field.max_length:
|
|
309
|
+
specific_attrs["max_length"] = field.max_length
|
|
310
|
+
|
|
311
|
+
# DecimalField
|
|
312
|
+
if hasattr(field, "max_digits") and field.max_digits:
|
|
313
|
+
specific_attrs["max_digits"] = field.max_digits
|
|
314
|
+
if hasattr(field, "decimal_places") and field.decimal_places:
|
|
315
|
+
specific_attrs["decimal_places"] = field.decimal_places
|
|
316
|
+
|
|
317
|
+
# FileField, ImageField
|
|
318
|
+
if hasattr(field, "upload_to") and field.upload_to:
|
|
319
|
+
specific_attrs["upload_to"] = str(field.upload_to)
|
|
320
|
+
|
|
321
|
+
# DateTimeField
|
|
322
|
+
if hasattr(field, "auto_now") and field.auto_now:
|
|
323
|
+
specific_attrs["auto_now"] = field.auto_now
|
|
324
|
+
if hasattr(field, "auto_now_add") and field.auto_now_add:
|
|
325
|
+
specific_attrs["auto_now_add"] = field.auto_now_add
|
|
326
|
+
|
|
327
|
+
return specific_attrs
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
|
|
4
|
+
import yaml
|
|
5
|
+
from django.conf import settings
|
|
6
|
+
from django.core.management.base import BaseCommand
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Command(BaseCommand):
|
|
10
|
+
help = "Updates the final schema by copying the documented schema."
|
|
11
|
+
|
|
12
|
+
def handle(self, *args, **options):
|
|
13
|
+
self.stdout.write("Starting the schema update process...")
|
|
14
|
+
|
|
15
|
+
# Load documented schema
|
|
16
|
+
doc_schema_path = os.path.join(settings.BASE_DIR, "docs/configs/doc-schema.yaml")
|
|
17
|
+
try:
|
|
18
|
+
with open(doc_schema_path) as f:
|
|
19
|
+
documented_schema = yaml.safe_load(f)
|
|
20
|
+
self.stdout.write(f"Successfully loaded documented schema from {doc_schema_path}")
|
|
21
|
+
except FileNotFoundError:
|
|
22
|
+
self.stderr.write(
|
|
23
|
+
self.style.ERROR(f"'{doc_schema_path}' not found. Please create it first.")
|
|
24
|
+
)
|
|
25
|
+
return
|
|
26
|
+
except yaml.YAMLError as e:
|
|
27
|
+
self.stderr.write(self.style.ERROR(f"Error parsing '{doc_schema_path}': {e}"))
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
# Save to final schema location
|
|
31
|
+
output_path = os.path.join(settings.BASE_DIR, "schema.yaml")
|
|
32
|
+
|
|
33
|
+
# Create backup if schema.yaml exists
|
|
34
|
+
if os.path.exists(output_path):
|
|
35
|
+
backup_path = f"{output_path}.bak"
|
|
36
|
+
shutil.copyfile(output_path, backup_path)
|
|
37
|
+
self.stdout.write(f"Created backup at '{backup_path}'")
|
|
38
|
+
|
|
39
|
+
# Write the documented schema
|
|
40
|
+
with open(output_path, "w") as f:
|
|
41
|
+
yaml.dump(documented_schema, f, default_flow_style=False, sort_keys=False)
|
|
42
|
+
|
|
43
|
+
# Count documented endpoints
|
|
44
|
+
paths_count = len(documented_schema.get("paths", {}))
|
|
45
|
+
|
|
46
|
+
self.stdout.write(
|
|
47
|
+
self.style.SUCCESS(f"Successfully updated schema and saved to '{output_path}'.")
|
|
48
|
+
)
|
|
49
|
+
self.stdout.write(f"Schema now contains {paths_count} documented endpoint(s).")
|
|
50
|
+
self.stdout.write(
|
|
51
|
+
"You can now use this schema for API documentation or import"
|
|
52
|
+
" it into tools like Swagger UI."
|
|
53
|
+
)
|