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,3 @@
1
+ """
2
+ Extractors for parsing Django views and extracting documentation information.
3
+ """
@@ -0,0 +1,229 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """Query parameter extraction utilities for Django views."""
4
+
5
+ from typing import Any
6
+
7
+ from drf_to_mkdoc.utils.common import extract_viewset_from_operation_id
8
+
9
+
10
+ def extract_query_parameters_from_view(operation_id: str) -> dict[str, Any]:
11
+ """Extract query parameters from a Django view class"""
12
+ view_class = extract_viewset_from_operation_id(operation_id)
13
+ if not view_class:
14
+ return {
15
+ "search_fields": [],
16
+ "filter_fields": [],
17
+ "ordering_fields": [],
18
+ "filter_backends": [],
19
+ "pagination_fields": [],
20
+ }
21
+
22
+ return {
23
+ "search_fields": extract_query_parameters_from_view_search_fields(view_class),
24
+ "filter_fields": extract_query_parameters_from_view_filter_fields(view_class),
25
+ "ordering_fields": extract_query_parameters_from_view_ordering_fields(view_class),
26
+ "filter_backends": extract_query_parameters_from_view_filter_backends(view_class),
27
+ "pagination_fields": extract_query_parameters_from_view_pagination_fields(view_class),
28
+ }
29
+
30
+
31
+ def extract_query_parameters_from_view_search_fields(view_class: Any) -> list[str]:
32
+ """Extract search fields from a Django view class"""
33
+ if not view_class:
34
+ return []
35
+
36
+ search_fields = []
37
+ if hasattr(view_class, "search_fields") and view_class.search_fields:
38
+ search_fields = sorted(view_class.search_fields)
39
+
40
+ return search_fields
41
+
42
+
43
+ def extract_query_parameters_from_view_filter_fields(view_class: Any) -> list[str]:
44
+ """Extract filter fields from a Django view class"""
45
+ if not view_class:
46
+ return []
47
+
48
+ filter_fields = []
49
+ if hasattr(view_class, "filterset_class"):
50
+ filter_fields = extract_filterset_fields(view_class.filterset_class)
51
+ elif hasattr(view_class, "filterset_fields") and view_class.filterset_fields:
52
+ filter_fields = sorted(view_class.filterset_fields)
53
+
54
+ return filter_fields
55
+
56
+
57
+ def extract_query_parameters_from_view_ordering_fields(view_class: Any) -> list[str]:
58
+ """Extract ordering fields from a Django view class"""
59
+ if not view_class:
60
+ return []
61
+
62
+ ordering_fields = []
63
+ if hasattr(view_class, "ordering_fields") and view_class.ordering_fields:
64
+ ordering_fields = sorted(view_class.ordering_fields)
65
+
66
+ return ordering_fields
67
+
68
+
69
+ def extract_query_parameters_from_view_filter_backends(view_class: Any) -> list[str]:
70
+ """Extract filter backends from a Django view class"""
71
+ if not view_class:
72
+ return []
73
+
74
+ filter_backends = []
75
+ if hasattr(view_class, "filter_backends") and view_class.filter_backends:
76
+ for backend in view_class.filter_backends:
77
+ filter_backends.append(getattr(backend, "__name__", str(backend)))
78
+
79
+ return filter_backends
80
+
81
+
82
+ def extract_query_parameters_from_view_pagination_fields(view_class: Any) -> list[str]:
83
+ """Extract pagination fields from a Django view class"""
84
+ if not view_class:
85
+ return []
86
+
87
+ pagination_fields = []
88
+ if hasattr(view_class, "pagination_class") and view_class.pagination_class:
89
+ pagination_class = view_class.pagination_class
90
+ if hasattr(pagination_class, "get_schema_fields"):
91
+ try:
92
+ # Get pagination fields from the pagination class
93
+ schema_fields = pagination_class().get_schema_fields(view_class())
94
+ pagination_fields = sorted([field.name for field in schema_fields])
95
+ except Exception as e:
96
+ # Check if it's specifically the coreapi missing error
97
+ if "coreapi must be installed" in str(e):
98
+ raise ValueError(
99
+ "coreapi is required for pagination schema extraction. "
100
+ "Install it with: pip install coreapi"
101
+ ) from e
102
+ raise ValueError(
103
+ "Failed to get schema fields from pagination class "
104
+ f"{pagination_class.__name__}: {e}"
105
+ ) from e
106
+ else:
107
+ raise ValueError(
108
+ f"Pagination class {pagination_class.__name__} "
109
+ "must implement get_schema_fields() method"
110
+ )
111
+
112
+ return pagination_fields
113
+
114
+
115
+ def _extract_filterset_fields_from_class_attributes(filterset_class: Any) -> list[str]:
116
+ fields = []
117
+
118
+ try:
119
+ import django_filters
120
+
121
+ # Get all class attributes, including inherited ones
122
+ for attr_name in dir(filterset_class):
123
+ # Skip private attributes and known non-filter attributes
124
+ if attr_name.startswith("_") or attr_name in [
125
+ "Meta",
126
+ "form",
127
+ "queryset",
128
+ "request",
129
+ "errors",
130
+ "qs",
131
+ "is_valid",
132
+ ]:
133
+ continue
134
+
135
+ try:
136
+ attr = getattr(filterset_class, attr_name)
137
+ if isinstance(attr, django_filters.Filter):
138
+ if attr_name not in fields:
139
+ fields.append(attr_name)
140
+ except (AttributeError, TypeError):
141
+ continue
142
+
143
+ except ImportError:
144
+ # django_filters not available, skip this strategy
145
+ pass
146
+
147
+ return fields
148
+
149
+
150
+ def _extract_filterset_fields_from_meta(filterset_class: Any) -> list[str]:
151
+ fields = []
152
+
153
+ if hasattr(filterset_class, "Meta") and hasattr(filterset_class.Meta, "fields"):
154
+ meta_fields = filterset_class.Meta.fields
155
+ if isinstance(meta_fields, list | tuple):
156
+ # List/tuple format: ['field1', 'field2']
157
+ for field in meta_fields:
158
+ if field not in fields:
159
+ fields.append(field)
160
+ elif isinstance(meta_fields, dict):
161
+ # Dictionary format: {'field1': ['exact'], 'field2': ['icontains']}
162
+ for field in meta_fields:
163
+ if field not in fields:
164
+ fields.append(field)
165
+
166
+ return fields
167
+
168
+
169
+ def _extract_filterset_fields_from_internal_attrs(filterset_class: Any) -> list[str]:
170
+ fields = []
171
+
172
+ # Use Django's internal FilterSet attributes as fallback
173
+ # This handles cases where the above strategies might miss some filters
174
+ for internal_attr in ["declared_filters", "base_filters"]:
175
+ if hasattr(filterset_class, internal_attr):
176
+ try:
177
+ internal_filters = getattr(filterset_class, internal_attr)
178
+ if hasattr(internal_filters, "keys"):
179
+ for field in internal_filters:
180
+ if field not in fields:
181
+ fields.append(field)
182
+ except (AttributeError, TypeError):
183
+ continue
184
+
185
+ return fields
186
+
187
+
188
+ def _extract_filterset_fields_from_get_fields(filterset_class: Any) -> list[str]:
189
+ fields = []
190
+
191
+ # Try get_fields() method if available (for dynamic filters)
192
+ if hasattr(filterset_class, "get_fields"):
193
+ filterset_instance = filterset_class()
194
+ filterset_fields = filterset_instance.get_fields()
195
+ if filterset_fields and hasattr(filterset_fields, "keys"):
196
+ for field in filterset_fields:
197
+ if field not in fields:
198
+ fields.append(field)
199
+
200
+ return fields
201
+
202
+
203
+ def extract_filterset_fields(filterset_class: Any) -> list[str]:
204
+ """Extract field names from a Django FilterSet class
205
+
206
+ This function uses multiple strategies to comprehensively detect all filter fields:
207
+ 1. Check class attributes for django_filters.Filter instances (declared filters)
208
+ 2. Check Meta.fields (both dict and list formats)
209
+ 3. Use Django's internal declared_filters and base_filters as fallback
210
+ 4. Handle edge cases and inheritance
211
+ """
212
+ if not filterset_class:
213
+ return []
214
+
215
+ fields = []
216
+
217
+ # Strategy 1: Check class attributes for Filter instances
218
+ fields.extend(_extract_filterset_fields_from_class_attributes(filterset_class))
219
+
220
+ # Strategy 2: Check Meta.fields (handles both dict and list formats)
221
+ fields.extend(_extract_filterset_fields_from_meta(filterset_class))
222
+
223
+ # Strategy 3: Use Django's internal FilterSet attributes as fallback
224
+ fields.extend(_extract_filterset_fields_from_internal_attrs(filterset_class))
225
+
226
+ # Strategy 4: Try get_fields() method if available (for dynamic filters)
227
+ fields.extend(_extract_filterset_fields_from_get_fields(filterset_class))
228
+
229
+ return sorted(fields)
File without changes
@@ -0,0 +1,72 @@
1
+ from typing import Any
2
+
3
+
4
+ def _md_query_parameters_format_error_section(query_params: dict[str, Any]) -> str:
5
+ if "error" in query_params:
6
+ return f"**Error:** {query_params['error']}\n\n"
7
+ return ""
8
+
9
+
10
+ def _md_query_parameters_format_search_section(query_params: dict[str, Any]) -> str:
11
+ search_fields = query_params.get("search_fields", [])
12
+ if search_fields:
13
+ content = "### Search Parameters\n\n"
14
+ for field in search_fields:
15
+ content += f"- `{field}`\n"
16
+ content += "\n"
17
+ return content
18
+ return ""
19
+
20
+
21
+ def _md_query_parameters_format_filter_section(query_params: dict[str, Any]) -> str:
22
+ filter_fields = query_params.get("filter_fields", [])
23
+ if filter_fields:
24
+ content = "### Filter Parameters\n\n"
25
+ for field in filter_fields:
26
+ content += f"- `{field}`\n"
27
+ content += "\n"
28
+ return content
29
+ return ""
30
+
31
+
32
+ def _md_query_parameters_format_ordering_section(query_params: dict[str, Any]) -> str:
33
+ ordering_fields = query_params.get("ordering_fields", [])
34
+ if ordering_fields:
35
+ content = "### Ordering Parameters\n\n"
36
+ for field in ordering_fields:
37
+ content += f"- `{field}`\n"
38
+ content += "\n"
39
+ return content
40
+ return ""
41
+
42
+
43
+ def _md_query_parameters_format_pagination_section(query_params: dict[str, Any]) -> str:
44
+ pagination_fields = query_params.get("pagination_fields", [])
45
+ if pagination_fields:
46
+ content = "### Pagination Parameters\n\n"
47
+ for field in pagination_fields:
48
+ content += f"- `{field}`\n"
49
+ content += "\n"
50
+ return content
51
+ return ""
52
+
53
+
54
+ def _md_query_parameters_format_filter_backends_section(query_params: dict[str, Any]) -> str:
55
+ filter_backends = query_params.get("filter_backends", [])
56
+ if filter_backends:
57
+ return f"**Filter Backends:** {', '.join(filter_backends)}\n\n"
58
+ return ""
59
+
60
+
61
+ def generate_query_parameters_md(query_params: dict[str, Any]) -> str:
62
+ """Format query parameters into markdown documentation"""
63
+ if "error" in query_params:
64
+ return _md_query_parameters_format_error_section(query_params)
65
+
66
+ content = ""
67
+ content += _md_query_parameters_format_search_section(query_params)
68
+ content += _md_query_parameters_format_filter_section(query_params)
69
+ content += _md_query_parameters_format_ordering_section(query_params)
70
+ content += _md_query_parameters_format_pagination_section(query_params)
71
+ content += _md_query_parameters_format_filter_backends_section(query_params)
72
+ return content
@@ -0,0 +1,269 @@
1
+ from pathlib import Path
2
+ from typing import Any
3
+
4
+ from drf_to_mkdoc.utils.common import get_app_descriptions, get_model_description, write_file
5
+
6
+
7
+ def create_models_index(models_data: dict[str, Any], docs_dir: Path) -> None:
8
+ models_by_app = {}
9
+ for model_name, model_info in models_data.items():
10
+ app_name = model_info.get("app_label", model_name.split(".")[0])
11
+ class_name = model_info.get("name", model_name.split(".")[-1])
12
+ if app_name not in models_by_app:
13
+ models_by_app[app_name] = []
14
+ models_by_app[app_name].append((class_name, model_name, model_info))
15
+
16
+ content = """# Django Models\n\nThis section contains documentation for
17
+ all Django models in the system, organized by Django application.\n\n"""
18
+
19
+ app_descriptions = get_app_descriptions()
20
+
21
+ for app_name in sorted(models_by_app.keys()):
22
+ app_desc = app_descriptions.get(app_name, f"{app_name.title()} application models")
23
+ content += f'<div class="app-header">{app_name.title()} App</div>\n\n'
24
+ content += f"*{app_desc}*\n\n"
25
+
26
+ content += '<div class="model-cards">\n'
27
+
28
+ for class_name, _model_name, _model_info in sorted(models_by_app[app_name]):
29
+ content += f"""
30
+ <a href="{app_name}/{class_name.lower()}/"
31
+ class="model-card">{class_name}</a>\n
32
+ """
33
+
34
+ content += "</div>\n\n"
35
+
36
+ content += """## Model Relationships\n\nThe models are interconnected through foreign keys
37
+ and many-to-many relationships:\n\n- **Users** can be associated
38
+ with multiple **Clinics** through **ClinicUser**
39
+ \n- **Doctors** belong to **Clinics**
40
+ and offer **Services** through **DoctorService**
41
+ \n- **Appointments** connect **Patients**
42
+ with **Doctors** and **Services**
43
+ \n- **Schedules** define **Doctor** availability in specific **Rooms**
44
+ \n- **Rooms** belong to **Clinics** and host **Appointments**\n
45
+ \nEach model page contains detailed field documentation,
46
+ method signatures, and relationships to other models.\n"""
47
+
48
+ models_index_path = docs_dir / "models" / "index.md"
49
+ models_index_path.parent.mkdir(parents=True, exist_ok=True)
50
+
51
+ with models_index_path.open("w", encoding="utf-8") as f:
52
+ f.write(content)
53
+
54
+
55
+ def generate_model_docs(models_data: dict[str, Any]) -> None:
56
+ """Generate model documentation from JSON data"""
57
+ for model_name, model_info in models_data.items():
58
+ app_name = model_info.get("app_label", model_name.split(".")[0])
59
+ class_name = model_info.get("name", model_name.split(".")[-1])
60
+
61
+ # Create the model page content
62
+ content = create_model_page(model_info)
63
+
64
+ # Write the file in app subdirectory
65
+ file_path = f"models/{app_name}/{class_name.lower()}.md"
66
+ write_file(file_path, content)
67
+
68
+
69
+ def render_fields_table(fields: dict[str, Any]) -> str:
70
+ content = "## Fields\n\n"
71
+ content += "| Field | Type | Description | Extra |\n"
72
+ content += "|-------|------|-------------|-------|\n"
73
+
74
+ for field_name, field_info in fields.items():
75
+ field_type = field_info.get("type", "Unknown")
76
+ verbose_name = field_info.get("verbose_name", field_name)
77
+ help_text = field_info.get("help_text", "")
78
+
79
+ extra_info = []
80
+ if field_info.get("null"):
81
+ extra_info.append("null=True")
82
+ if field_info.get("blank"):
83
+ extra_info.append("blank=True")
84
+ if field_info.get("unique"):
85
+ extra_info.append("unique=True")
86
+ if field_info.get("primary_key"):
87
+ extra_info.append("primary_key=True")
88
+ if field_info.get("default"):
89
+ extra_info.append(f"default={field_info['default']}")
90
+
91
+ field_specific = field_info.get("field_specific", {})
92
+ for key, value in field_specific.items():
93
+ if key not in ["related_name", "related_query_name", "to"]:
94
+ extra_info.append(f"{key}={value}")
95
+
96
+ extra_str = ", ".join(extra_info) if extra_info else ""
97
+ description_str = help_text or verbose_name
98
+
99
+ content += f"| `{field_name}` | {field_type} | {description_str} | {extra_str} |\n"
100
+
101
+ return content
102
+
103
+
104
+ def render_choices_tables(fields: dict[str, Any]) -> str:
105
+ choice_tables = []
106
+
107
+ for field_name, field_info in fields.items():
108
+ choices = field_info.get("choices")
109
+ if choices:
110
+ table = f"### {field_name} Choices\n\n"
111
+ table += "| Label | Value |\n"
112
+ table += "|-------|--------|\n"
113
+ for choice in choices:
114
+ table += f"| {choice['display']} | `{choice['value']}` |\n"
115
+ table += "\n"
116
+ choice_tables.append(table)
117
+
118
+ if choice_tables:
119
+ return "## Choices\n\n" + "\n".join(choice_tables)
120
+ return ""
121
+
122
+
123
+ def create_model_page(model_info: dict[str, Any]) -> str:
124
+ """Create a model documentation page from model info"""
125
+ name = model_info.get("name", "Unknown")
126
+ app_label = model_info.get("app_label", "unknown")
127
+ table_name = model_info.get("table_name", "")
128
+ description = get_model_description(name)
129
+
130
+ content = _create_model_header(name, app_label, table_name, description)
131
+ content += _add_fields_section(model_info)
132
+ content += _add_relationships_section(model_info)
133
+ content += _add_methods_section(model_info)
134
+ content += _add_meta_options_section(model_info)
135
+
136
+ return content
137
+
138
+
139
+ def _create_model_header(name: str, app_label: str, table_name: str, description: str) -> str:
140
+ """Create the header section of the model documentation."""
141
+ return f"""# {name}
142
+
143
+ **App:** {app_label}\n
144
+ **Table:** `{table_name}`\n
145
+
146
+ ## Description
147
+
148
+ {description}
149
+
150
+ """
151
+
152
+
153
+ def _add_fields_section(model_info: dict[str, Any]) -> str:
154
+ """Add the fields section to the model documentation."""
155
+ fields = model_info.get("fields", {})
156
+ non_relationship_fields = {
157
+ name: info
158
+ for name, info in fields.items()
159
+ if info.get("type", "") not in ["ForeignKey", "OneToOneField", "ManyToManyField"]
160
+ }
161
+
162
+ if not non_relationship_fields:
163
+ return ""
164
+
165
+ content = render_fields_table(non_relationship_fields)
166
+ content += "\n"
167
+ content += render_choices_tables(non_relationship_fields)
168
+ content += "\n"
169
+ return content
170
+
171
+
172
+ def _add_relationships_section(model_info: dict[str, Any]) -> str:
173
+ """Add the relationships section to the model documentation."""
174
+ fields = model_info.get("fields", {})
175
+ relationships = model_info.get("relationships", {})
176
+
177
+ relationship_fields = {
178
+ name: info
179
+ for name, info in fields.items()
180
+ if info.get("type", "") in ["ForeignKey", "OneToOneField", "ManyToManyField"]
181
+ }
182
+
183
+ if not (relationships or relationship_fields):
184
+ return ""
185
+
186
+ content = "## Relationships\n\n"
187
+ content += "| Field | Type | Related Model |\n"
188
+ content += "|-------|------|---------------|\n"
189
+
190
+ content += _render_relationship_fields(relationship_fields)
191
+ content += _render_relationships_from_section(relationships)
192
+ content += "\n"
193
+
194
+ return content
195
+
196
+
197
+ def _render_relationship_fields(relationship_fields: dict[str, Any]) -> str:
198
+ """Render relationship fields from the fields section."""
199
+ content = ""
200
+ for field_name, field_info in relationship_fields.items():
201
+ field_type = field_info.get("type", "Unknown")
202
+ field_specific = field_info.get("field_specific", {})
203
+ to_model = field_specific.get("to", "")
204
+
205
+ if to_model:
206
+ model_link = _create_model_link(to_model)
207
+ content += f"| `{field_name}` | {field_type} | {model_link}|\n"
208
+
209
+ return content
210
+
211
+
212
+ def _render_relationships_from_section(relationships: dict[str, Any]) -> str:
213
+ """Render relationships from the relationships section."""
214
+ content = ""
215
+ for rel_name, rel_info in relationships.items():
216
+ rel_type = rel_info.get("type", "Unknown")
217
+ related_model_full = rel_info.get("related_model", "")
218
+
219
+ if related_model_full and "." in related_model_full:
220
+ related_app, related_model = related_model_full.split(".", 1)
221
+ model_link = f"[{related_model}](../../{related_app}/{related_model.lower()}/)"
222
+ else:
223
+ model_link = related_model_full
224
+
225
+ content += f"| `{rel_name}` | {rel_type} | {model_link} | \n"
226
+
227
+ return content
228
+
229
+
230
+ def _create_model_link(to_model: str) -> str:
231
+ """Create a link to a related model."""
232
+ if "." in to_model:
233
+ related_app, related_model = to_model.split(".", 1)
234
+ return f"[{related_model}](../{related_app}/{related_model.lower()}/)"
235
+ return f"[{to_model}]({to_model.lower()}/)"
236
+
237
+
238
+ def _add_methods_section(model_info: dict[str, Any]) -> str:
239
+ """Add the methods section to the model documentation."""
240
+ methods = model_info.get("methods", [])
241
+ if not methods:
242
+ return ""
243
+
244
+ content = "## Methods\n\n"
245
+ for method in methods:
246
+ method_name = method.get("name", "")
247
+ docstring = method.get("docstring", "")
248
+
249
+ content += f"### `{method_name}()`\n\n"
250
+ if docstring:
251
+ content += f"{docstring}\n\n"
252
+ else:
253
+ content += "No documentation available.\n\n"
254
+
255
+ return content
256
+
257
+
258
+ def _add_meta_options_section(model_info: dict[str, Any]) -> str:
259
+ """Add the meta options section to the model documentation."""
260
+ meta_options = model_info.get("meta_options", {})
261
+ if not meta_options:
262
+ return ""
263
+
264
+ content = "## Meta Options\n\n"
265
+ for option, value in meta_options.items():
266
+ content += f"- **{option}:** {value}\n"
267
+ content += "\n"
268
+
269
+ return content