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.

@@ -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
+ )
@@ -0,0 +1,3 @@
1
+ """
2
+ Utility modules for DRF to MkDocs documentation generation.
3
+ """