drf-to-mkdoc 0.1.6__py3-none-any.whl → 1.0.7__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 CHANGED
@@ -4,4 +4,4 @@ DRF to MkDocs - Generate Markdown API docs from Django/DRF OpenAPI schema for Mk
4
4
 
5
5
  __version__ = "0.1.0"
6
6
  __author__ = "ShayestehHs"
7
- __email__ = "shayestehhs1@gmail.com"
7
+ __email__ = "shayestehhs1@gmail.com"
drf_to_mkdoc/apps.py CHANGED
@@ -1,15 +1,19 @@
1
+ import logging
2
+
1
3
  from django.apps import AppConfig
2
4
 
5
+ logger = logging.getLogger()
6
+
3
7
 
4
8
  class DrfToMkdocConfig(AppConfig):
5
9
  default_auto_field = "django.db.models.BigAutoField"
6
10
  name = "drf_to_mkdoc"
7
11
  verbose_name = "DRF to MkDocs Documentation Generator"
8
-
12
+
9
13
  def ready(self):
10
14
  """Initialize the app when Django starts."""
11
15
  # Import management commands to register them
12
16
  try:
13
17
  import drf_to_mkdoc.management.commands # noqa
14
18
  except ImportError:
15
- pass
19
+ logger.exception("Failed to import drf_to_mkdoc commands")
@@ -5,7 +5,6 @@ DEFAULTS = {
5
5
  "MODEL_DOCS_FILE": "docs/model-docs.json", # Path to model documentation JSON file
6
6
  "DOC_CONFIG_FILE": "docs/configs/doc_config.json", # Path to documentation configuration file
7
7
  "CUSTOM_SCHEMA_FILE": "docs/configs/custom_schema.json", # Path to custom schema file
8
-
9
8
  # Django apps - required, no default
10
9
  "DJANGO_APPS": None, # List of Django app names to process
11
10
  }
@@ -1,6 +1,8 @@
1
1
  from django.conf import settings
2
+
2
3
  from drf_to_mkdoc.conf.defaults import DEFAULTS
3
4
 
5
+
4
6
  class DRFToMkDocSettings:
5
7
  required_settings = ["DJANGO_APPS"]
6
8
 
@@ -12,15 +14,15 @@ class DRFToMkDocSettings:
12
14
  def get(self, key):
13
15
  if key not in self.defaults:
14
16
  raise AttributeError(f"Invalid DRF_TO_MKDOC setting: '{key}'")
15
-
17
+
16
18
  value = self._user_settings.get(key, self.defaults[key])
17
-
19
+
18
20
  if value is None and key in self.required_settings:
19
21
  raise ValueError(
20
22
  f"DRF_TO_MKDOC setting '{key}' is required but not configured. "
21
23
  f"Please add it to your Django settings under {self.user_settings_key}."
22
24
  )
23
-
25
+
24
26
  return value
25
27
 
26
28
  def __getattr__(self, key):
@@ -28,17 +30,18 @@ class DRFToMkDocSettings:
28
30
 
29
31
  def validate_required_settings(self):
30
32
  missing_settings = []
31
-
33
+
32
34
  for setting in self.required_settings:
33
35
  try:
34
36
  self.get(setting)
35
37
  except ValueError:
36
38
  missing_settings.append(setting)
37
-
39
+
38
40
  if missing_settings:
39
41
  raise ValueError(
40
42
  f"Missing required settings: {', '.join(missing_settings)}. "
41
43
  f"Please configure these in your Django settings under {self.user_settings_key}."
42
44
  )
43
45
 
46
+
44
47
  drf_to_mkdoc_settings = DRFToMkDocSettings(defaults=DEFAULTS)
@@ -1,11 +1,12 @@
1
+ import shutil
1
2
  import subprocess
2
3
  from pathlib import Path
3
4
 
4
- from django.conf import settings
5
- from django.core.management.base import BaseCommand, CommandError
6
5
  from django.apps import apps
7
- from drf_to_mkdoc.conf.settings import drf_to_mkdoc_settings
6
+ from django.conf import settings
8
7
  from django.core.management import call_command
8
+ from django.core.management.base import BaseCommand, CommandError
9
+ from your_app import drf_to_mkdoc_settings # Replace with your actual import
9
10
 
10
11
 
11
12
  class Command(BaseCommand):
@@ -18,13 +19,13 @@ class Command(BaseCommand):
18
19
  try:
19
20
  apps.check_apps_ready()
20
21
  except Exception as e:
21
- raise CommandError(f"Django apps not properly configured: {e}")
22
+ raise CommandError(f"Django apps not properly configured: {e}") from e
22
23
 
23
24
  base_dir = Path(settings.BASE_DIR)
24
25
  site_dir = base_dir / "site"
25
26
  mkdocs_config = base_dir / "mkdocs.yml"
26
27
  mkdocs_config_alt = base_dir / "mkdocs.yaml"
27
-
28
+
28
29
  if not mkdocs_config.exists() and not mkdocs_config_alt.exists():
29
30
  raise CommandError(
30
31
  "MkDocs configuration file not found. Please create either 'mkdocs.yml' or 'mkdocs.yaml' "
@@ -34,10 +35,7 @@ class Command(BaseCommand):
34
35
  try:
35
36
  # Generate the model documentation JSON first
36
37
  self.stdout.write("Generating model documentation...")
37
-
38
- call_command(
39
- "generate_model_docs", "--pretty"
40
- )
38
+ call_command("generate_model_docs", "--pretty")
41
39
  self.stdout.write(self.style.SUCCESS("Model documentation generated."))
42
40
 
43
41
  # Generate the documentation content
@@ -47,17 +45,7 @@ class Command(BaseCommand):
47
45
 
48
46
  # Build the MkDocs site
49
47
  self.stdout.write("Building MkDocs site...")
50
- result = subprocess.run(
51
- ["mkdocs", "build", "--clean"],
52
- check=False,
53
- cwd=base_dir,
54
- capture_output=True,
55
- text=True,
56
- )
57
-
58
- if result.returncode != 0:
59
- raise CommandError(f"MkDocs build failed: {result.stderr}")
60
-
48
+ self._build_mkdocs_site(base_dir, site_dir)
61
49
  self.stdout.write(self.style.SUCCESS("Documentation built successfully!"))
62
50
  self.stdout.write(f"Site built in: {site_dir}")
63
51
 
@@ -65,3 +53,56 @@ class Command(BaseCommand):
65
53
  raise CommandError(
66
54
  "MkDocs not found. Please install it with: pip install mkdocs mkdocs-material"
67
55
  ) from e
56
+
57
+ def _build_mkdocs_site(self, base_dir: Path, site_dir: Path) -> None:
58
+ """
59
+ Build the MkDocs site with proper security checks.
60
+
61
+ Args:
62
+ base_dir: The base directory of the Django project
63
+ site_dir: The directory where the site will be built
64
+
65
+ Raises:
66
+ FileNotFoundError: If mkdocs executable is not found
67
+ CommandError: If the build process fails
68
+ """
69
+ mkdocs_path = shutil.which("mkdocs")
70
+ if not mkdocs_path:
71
+ raise FileNotFoundError("mkdocs executable not found in PATH")
72
+
73
+ mkdocs_path_obj = Path(mkdocs_path)
74
+ if not mkdocs_path_obj.exists() or not mkdocs_path_obj.is_file():
75
+ raise CommandError(f"Invalid mkdocs executable path: {mkdocs_path}")
76
+
77
+ if not base_dir.is_absolute():
78
+ base_dir = base_dir.resolve()
79
+
80
+ if not base_dir.exists():
81
+ raise CommandError(f"Base directory does not exist: {base_dir}")
82
+
83
+ cmd = [
84
+ str(mkdocs_path_obj), # Convert to string for subprocess
85
+ "build",
86
+ "--clean",
87
+ ]
88
+
89
+ try:
90
+ result = subprocess.run( # noqa S603
91
+ cmd,
92
+ check=True,
93
+ cwd=str(base_dir),
94
+ capture_output=True,
95
+ text=True,
96
+ timeout=300,
97
+ )
98
+
99
+ if result.stdout:
100
+ self.stdout.write(f"MkDocs output: {result.stdout}")
101
+
102
+ except subprocess.TimeoutExpired as e:
103
+ raise CommandError("MkDocs build timed out after 5 minutes") from e
104
+ except subprocess.CalledProcessError as e:
105
+ error_msg = f"MkDocs build failed (exit code {e.returncode})"
106
+ if e.stderr:
107
+ error_msg += f": {e.stderr}"
108
+ raise CommandError(error_msg) from e
@@ -4,6 +4,7 @@ from pathlib import Path
4
4
 
5
5
  from django.core.management.base import BaseCommand
6
6
 
7
+ from drf_to_mkdoc.conf.settings import drf_to_mkdoc_settings
7
8
  from drf_to_mkdoc.utils.common import get_schema, load_model_json_data
8
9
  from drf_to_mkdoc.utils.endpoint_generator import (
9
10
  create_endpoints_index,
@@ -11,8 +12,6 @@ from drf_to_mkdoc.utils.endpoint_generator import (
11
12
  parse_endpoints_from_schema,
12
13
  )
13
14
  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
15
 
17
16
 
18
17
  class Command(BaseCommand):
@@ -9,6 +9,7 @@ from django.db import models
9
9
 
10
10
  from drf_to_mkdoc.conf.settings import drf_to_mkdoc_settings
11
11
 
12
+
12
13
  class Command(BaseCommand):
13
14
  help = "Generate model documentation JSON from Django model introspection"
14
15
 
@@ -1,19 +1,21 @@
1
- from asyncio.log import logger
2
1
  import importlib
3
- import yaml
4
2
  import json
5
3
  import re
4
+ from asyncio.log import logger
6
5
  from functools import lru_cache
7
6
  from pathlib import Path
8
- from typing import Any, Optional
7
+ from typing import Any
9
8
 
9
+ import yaml
10
10
  from django.apps import apps
11
11
  from django.core.exceptions import AppRegistryNotReady
12
12
  from django.urls import resolve
13
13
  from django.utils.module_loading import import_string
14
14
  from drf_spectacular.generators import SchemaGenerator
15
+
15
16
  from drf_to_mkdoc.conf.settings import drf_to_mkdoc_settings
16
17
 
18
+
17
19
  class SchemaValidationError(Exception):
18
20
  """Custom exception for schema validation errors."""
19
21
 
@@ -37,9 +39,10 @@ def substitute_path_params(path: str, parameters: list[dict[str, Any]]) -> str:
37
39
  django_path = re.sub(r"<path:[^>]+>", "dummy/path", django_path)
38
40
  django_path = re.sub(r"<[^:>]+>", "dummy", django_path) # Catch remaining simple params
39
41
 
40
- return django_path
42
+ return django_path # noqa: RET504
43
+
41
44
 
42
- def load_schema() -> Optional[dict[str, Any]]:
45
+ def load_schema() -> dict[str, Any] | None:
43
46
  """Load the OpenAPI schema from doc-schema.yaml"""
44
47
  schema_file = Path(drf_to_mkdoc_settings.CONFIG_DIR) / "doc-schema.yaml"
45
48
  if not schema_file.exists():
@@ -49,7 +52,7 @@ def load_schema() -> Optional[dict[str, Any]]:
49
52
  return yaml.safe_load(f)
50
53
 
51
54
 
52
- def load_model_json_data() -> Optional[dict[str, Any]]:
55
+ def load_model_json_data() -> dict[str, Any] | None:
53
56
  """Load the JSON mapping data for model information"""
54
57
  json_file = Path(drf_to_mkdoc_settings.MODEL_DOCS_FILE)
55
58
  if not json_file.exists():
@@ -59,7 +62,7 @@ def load_model_json_data() -> Optional[dict[str, Any]]:
59
62
  return json.load(f)
60
63
 
61
64
 
62
- def load_doc_config() -> Optional[dict[str, Any]]:
65
+ def load_doc_config() -> dict[str, Any] | None:
63
66
  """Load the documentation configuration file"""
64
67
  config_file = Path(drf_to_mkdoc_settings.DOC_CONFIG_FILE)
65
68
  if not config_file.exists():
@@ -69,7 +72,7 @@ def load_doc_config() -> Optional[dict[str, Any]]:
69
72
  return json.load(f)
70
73
 
71
74
 
72
- def get_model_docstring(class_name: str) -> Optional[str]:
75
+ def get_model_docstring(class_name: str) -> str | None:
73
76
  """Extract docstring from Django model class"""
74
77
  try:
75
78
  # Check if Django is properly initialized
@@ -167,6 +170,7 @@ def get_custom_schema():
167
170
  raise QueryParamTypeError("Invalid queryparam_type")
168
171
  return data
169
172
 
173
+
170
174
  def convert_to_django_path(path: str, parameters: list[dict[str, Any]]) -> str:
171
175
  """
172
176
  Convert a path with {param} to a Django-style path with <type:param>.
@@ -193,26 +197,27 @@ def convert_to_django_path(path: str, parameters: list[dict[str, Any]]) -> str:
193
197
  # Default Django path conversion
194
198
  def replacement(match):
195
199
  param_name = match.group(1)
196
- param_info = next((p for p in parameters if p.get('name') == param_name), {})
197
- param_type = param_info.get('schema', {}).get('type')
198
- param_format = param_info.get('schema', {}).get('format')
199
-
200
- if param_type == 'integer':
201
- converter = 'int'
202
- elif param_type == 'string' and param_format == 'uuid':
203
- converter = 'uuid'
200
+ param_info = next((p for p in parameters if p.get("name") == param_name), {})
201
+ param_type = param_info.get("schema", {}).get("type")
202
+ param_format = param_info.get("schema", {}).get("format")
203
+
204
+ if param_type == "integer":
205
+ converter = "int"
206
+ elif param_type == "string" and param_format == "uuid":
207
+ converter = "uuid"
204
208
  else:
205
- converter = 'str'
209
+ converter = "str"
206
210
 
207
- return f'<{converter}:{param_name}>'
211
+ return f"<{converter}:{param_name}>"
208
212
 
209
- django_path = re.sub(r'{(\w+)}', replacement, path)
213
+ django_path = re.sub(r"{(\w+)}", replacement, path)
210
214
 
211
- if not django_path.endswith('/'):
212
- django_path += '/'
215
+ if not django_path.endswith("/"):
216
+ django_path += "/"
213
217
 
214
218
  return django_path
215
219
 
220
+
216
221
  @lru_cache
217
222
  def get_schema():
218
223
  base_schema = SchemaGenerator().get_schema(request=None, public=True)
@@ -291,7 +296,7 @@ def extract_viewset_from_operation_id(operation_id: str):
291
296
  else:
292
297
  return view_func
293
298
 
294
- except Exception as e:
299
+ except Exception:
295
300
  logger.error(f"Failed to resolve path {path}")
296
301
 
297
302
 
@@ -1,12 +1,14 @@
1
- #!/usr/bin/env python3
2
-
3
1
  import ast
4
2
  import inspect
5
3
  import json
4
+ import logging
6
5
  from collections import defaultdict
7
6
  from pathlib import Path
8
7
  from typing import Any
9
8
 
9
+ from django.apps import apps
10
+ from rest_framework import serializers
11
+
10
12
  from drf_to_mkdoc.conf.settings import drf_to_mkdoc_settings
11
13
  from drf_to_mkdoc.utils.common import (
12
14
  create_safe_filename,
@@ -17,30 +19,35 @@ from drf_to_mkdoc.utils.common import (
17
19
  get_custom_schema,
18
20
  write_file,
19
21
  )
20
- from drf_to_mkdoc.utils.extractors.query_parameter_extractors import extract_query_parameters_from_view
21
- from drf_to_mkdoc.utils.md_generators.query_parameters_generators import generate_query_parameters_md
22
+ from drf_to_mkdoc.utils.extractors.query_parameter_extractors import (
23
+ extract_query_parameters_from_view,
24
+ )
25
+ from drf_to_mkdoc.utils.md_generators.query_parameters_generators import (
26
+ generate_query_parameters_md,
27
+ )
28
+
29
+ logger = logging.getLogger()
22
30
 
23
31
 
24
32
  def analyze_serializer_method_field_schema(serializer_class, field_name: str) -> dict:
25
33
  """Analyze a SerializerMethodField to determine its actual return type schema."""
26
34
  method_name = f"get_{field_name}"
27
35
 
28
-
29
36
  # Strategy 2: Check type annotations
30
37
  schema_from_annotations = _extract_schema_from_type_hints(serializer_class, method_name)
31
38
  if schema_from_annotations:
32
39
  return schema_from_annotations
33
-
40
+
34
41
  # Strategy 3: Analyze method source code
35
42
  schema_from_source = _analyze_method_source_code(serializer_class, method_name)
36
43
  if schema_from_source:
37
44
  return schema_from_source
38
-
45
+
39
46
  # Strategy 4: Runtime analysis (sample execution)
40
47
  schema_from_runtime = _analyze_method_runtime(serializer_class, method_name)
41
48
  if schema_from_runtime:
42
49
  return schema_from_runtime
43
-
50
+
44
51
  # Fallback to string
45
52
  return {"type": "string"}
46
53
 
@@ -51,24 +58,24 @@ def _extract_schema_from_decorator(serializer_class, method_name: str) -> dict:
51
58
  method = getattr(serializer_class, method_name, None)
52
59
  if not method:
53
60
  return None
54
-
61
+
55
62
  # Check if method has the decorator attribute (drf-spectacular)
56
- if hasattr(method, '_spectacular_annotation'):
63
+ if hasattr(method, "_spectacular_annotation"):
57
64
  annotation = method._spectacular_annotation
58
65
  # Handle OpenApiTypes
59
- if hasattr(annotation, 'type'):
66
+ if hasattr(annotation, "type"):
60
67
  return {"type": annotation.type}
61
- elif isinstance(annotation, dict):
68
+ if isinstance(annotation, dict):
62
69
  return annotation
63
-
70
+
64
71
  # Check for drf-yasg decorator
65
- if hasattr(method, '_swagger_serializer_method'):
72
+ if hasattr(method, "_swagger_serializer_method"):
66
73
  swagger_info = method._swagger_serializer_method
67
- if hasattr(swagger_info, 'many') and hasattr(swagger_info, 'child'):
74
+ if hasattr(swagger_info, "many") and hasattr(swagger_info, "child"):
68
75
  return {"type": "array", "items": {"type": "object"}}
69
-
76
+
70
77
  except Exception:
71
- pass
78
+ logger.exception("Failed to extract schema from decorator")
72
79
  return None
73
80
 
74
81
 
@@ -78,30 +85,30 @@ def _extract_schema_from_type_hints(serializer_class, method_name: str) -> dict:
78
85
  method = getattr(serializer_class, method_name, None)
79
86
  if not method:
80
87
  return None
81
-
88
+
82
89
  signature = inspect.signature(method)
83
90
  return_annotation = signature.return_annotation
84
-
91
+
85
92
  if return_annotation and return_annotation != inspect.Signature.empty:
86
93
  # Handle common type hints
87
- if return_annotation == int:
88
- return {"type": "integer"}
89
- elif return_annotation == str:
90
- return {"type": "string"}
91
- elif return_annotation == bool:
92
- return {"type": "boolean"}
93
- elif return_annotation == float:
94
- return {"type": "number"}
95
- elif hasattr(return_annotation, '__origin__'):
94
+ if return_annotation in (int, str, bool, float):
95
+ return {
96
+ int: {"type": "integer"},
97
+ str: {"type": "string"},
98
+ bool: {"type": "boolean"},
99
+ float: {"type": "number"},
100
+ }[return_annotation]
101
+
102
+ if hasattr(return_annotation, "__origin__"):
96
103
  # Handle generic types like List[str], Dict[str, Any]
97
104
  origin = return_annotation.__origin__
98
105
  if origin is list:
99
106
  return {"type": "array", "items": {"type": "string"}}
100
- elif origin is dict:
107
+ if origin is dict:
101
108
  return {"type": "object"}
102
-
109
+
103
110
  except Exception:
104
- pass
111
+ logger.exception("Failed to extract schema from type hints")
105
112
  return None
106
113
 
107
114
 
@@ -111,18 +118,18 @@ def _analyze_method_source_code(serializer_class, method_name: str) -> dict:
111
118
  method = getattr(serializer_class, method_name, None)
112
119
  if not method:
113
120
  return None
114
-
121
+
115
122
  source = inspect.getsource(method)
116
123
  tree = ast.parse(source)
117
-
124
+
118
125
  # Find return statements and analyze them
119
126
  return_analyzer = ReturnStatementAnalyzer()
120
127
  return_analyzer.visit(tree)
121
-
128
+
122
129
  return _infer_schema_from_return_patterns(return_analyzer.return_patterns)
123
-
130
+
124
131
  except Exception:
125
- pass
132
+ logger.exception("Failed to analyze method source code")
126
133
  return None
127
134
 
128
135
 
@@ -130,32 +137,39 @@ def _analyze_method_runtime(serializer_class, method_name: str) -> dict:
130
137
  """Analyze method by creating mock instances and examining return values."""
131
138
  try:
132
139
  # Create a basic mock object with common attributes
133
- mock_obj = type('MockObj', (), {
134
- 'id': 1, 'pk': 1, 'name': 'test', 'count': lambda: 5,
135
- 'items': type('items', (), {'count': lambda: 3, 'all': lambda: []})()
136
- })()
137
-
140
+ mock_obj = type(
141
+ "MockObj",
142
+ (),
143
+ {
144
+ "id": 1,
145
+ "pk": 1,
146
+ "name": "test",
147
+ "count": lambda: 5,
148
+ "items": type("items", (), {"count": lambda: 3, "all": lambda: []})(),
149
+ },
150
+ )()
151
+
138
152
  serializer_instance = serializer_class()
139
153
  method = getattr(serializer_instance, method_name, None)
140
-
154
+
141
155
  if not method:
142
156
  return None
143
-
157
+
144
158
  # Execute method with mock data
145
159
  result = method(mock_obj)
146
160
  return _infer_schema_from_value(result)
147
-
161
+
148
162
  except Exception:
149
- pass
163
+ logger.exception("Failed to analyse method runtime")
150
164
  return None
151
165
 
152
166
 
153
167
  class ReturnStatementAnalyzer(ast.NodeVisitor):
154
168
  """AST visitor to analyze return statements in method source code."""
155
-
169
+
156
170
  def __init__(self):
157
171
  self.return_patterns = []
158
-
172
+
159
173
  def visit_Return(self, node):
160
174
  """Visit return statements and extract patterns."""
161
175
  if node.value:
@@ -163,92 +177,86 @@ class ReturnStatementAnalyzer(ast.NodeVisitor):
163
177
  if pattern:
164
178
  self.return_patterns.append(pattern)
165
179
  self.generic_visit(node)
166
-
180
+
167
181
  def _analyze_return_value(self, node) -> dict:
168
182
  """Analyze different types of return value patterns."""
169
183
  if isinstance(node, ast.Dict):
170
184
  return self._analyze_dict_return(node)
171
- elif isinstance(node, ast.List):
185
+ if isinstance(node, ast.List):
172
186
  return self._analyze_list_return(node)
173
- elif isinstance(node, ast.Constant):
187
+ if isinstance(node, ast.Constant):
174
188
  return self._analyze_constant_return(node)
175
- elif isinstance(node, ast.Call):
189
+ if isinstance(node, ast.Call):
176
190
  return self._analyze_method_call_return(node)
177
- elif isinstance(node, ast.Attribute):
191
+ if isinstance(node, ast.Attribute):
178
192
  return self._analyze_attribute_return(node)
179
193
  return None
180
-
194
+
181
195
  def _analyze_dict_return(self, node) -> dict:
182
196
  """Analyze dictionary return patterns."""
183
197
  properties = {}
184
- for key, value in zip(node.keys, node.values):
198
+ for key, value in zip(node.keys, node.values, strict=False):
185
199
  if isinstance(key, ast.Constant) and isinstance(key.value, str):
186
200
  prop_schema = self._infer_value_type(value)
187
201
  if prop_schema:
188
202
  properties[key.value] = prop_schema
189
-
190
- return {
191
- "type": "object",
192
- "properties": properties
193
- }
194
-
203
+
204
+ return {"type": "object", "properties": properties}
205
+
195
206
  def _analyze_list_return(self, node) -> dict:
196
207
  """Analyze list return patterns."""
197
208
  if node.elts:
198
209
  # Analyze first element to determine array item type
199
210
  first_element_schema = self._infer_value_type(node.elts[0])
200
- return {
201
- "type": "array",
202
- "items": first_element_schema or {"type": "string"}
203
- }
211
+ return {"type": "array", "items": first_element_schema or {"type": "string"}}
204
212
  return {"type": "array", "items": {"type": "string"}}
205
-
213
+
206
214
  def _analyze_constant_return(self, node) -> dict:
207
215
  """Analyze constant return values."""
208
216
  return self._python_type_to_schema(type(node.value))
209
-
217
+
210
218
  def _analyze_method_call_return(self, node) -> dict:
211
219
  """Analyze method call returns (like obj.count(), obj.items.all())."""
212
220
  if isinstance(node.func, ast.Attribute):
213
221
  method_name = node.func.attr
214
-
222
+
215
223
  # Common Django ORM patterns
216
- if method_name in ['count']:
224
+ if method_name in ["count"]:
217
225
  return {"type": "integer"}
218
- elif method_name in ['all', 'filter', 'exclude']:
226
+ if method_name in ["all", "filter", "exclude"]:
219
227
  return {"type": "array", "items": {"type": "object"}}
220
- elif method_name in ['first', 'last', 'get']:
228
+ if method_name in ["first", "last", "get"]:
221
229
  return {"type": "object"}
222
- elif method_name in ['exists']:
230
+ if method_name in ["exists"]:
223
231
  return {"type": "boolean"}
224
-
232
+
225
233
  return None
226
-
234
+
227
235
  def _analyze_attribute_return(self, node) -> dict:
228
236
  """Analyze attribute access returns (like obj.name, obj.id)."""
229
237
  if isinstance(node, ast.Attribute):
230
238
  attr_name = node.attr
231
-
239
+
232
240
  # Common field name patterns
233
- if attr_name in ['id', 'pk', 'count']:
241
+ if attr_name in ["id", "pk", "count"]:
234
242
  return {"type": "integer"}
235
- elif attr_name in ['name', 'title', 'description', 'slug']:
243
+ if attr_name in ["name", "title", "description", "slug"]:
236
244
  return {"type": "string"}
237
- elif attr_name in ['is_active', 'is_published', 'enabled']:
245
+ if attr_name in ["is_active", "is_published", "enabled"]:
238
246
  return {"type": "boolean"}
239
-
247
+
240
248
  return None
241
-
249
+
242
250
  def _infer_value_type(self, node) -> dict:
243
251
  """Infer schema type from AST node."""
244
252
  if isinstance(node, ast.Constant):
245
253
  return self._python_type_to_schema(type(node.value))
246
- elif isinstance(node, ast.Call):
254
+ if isinstance(node, ast.Call):
247
255
  return self._analyze_method_call_return(node)
248
- elif isinstance(node, ast.Attribute):
256
+ if isinstance(node, ast.Attribute):
249
257
  return self._analyze_attribute_return(node)
250
258
  return {"type": "string"} # Default fallback
251
-
259
+
252
260
  def _python_type_to_schema(self, python_type) -> dict:
253
261
  """Convert Python type to OpenAPI schema."""
254
262
  type_mapping = {
@@ -266,20 +274,17 @@ def _infer_schema_from_return_patterns(patterns: list) -> dict:
266
274
  """Infer final schema from collected return patterns."""
267
275
  if not patterns:
268
276
  return None
269
-
277
+
270
278
  # If all patterns are the same type, use that
271
- if len(set(p.get("type") for p in patterns)) == 1:
279
+ if all(p.get("type") == patterns[0].get("type") for p in patterns):
272
280
  # Merge object properties if multiple object returns
273
281
  if patterns[0]["type"] == "object":
274
282
  merged_properties = {}
275
283
  for pattern in patterns:
276
284
  merged_properties.update(pattern.get("properties", {}))
277
- return {
278
- "type": "object",
279
- "properties": merged_properties
280
- }
285
+ return {"type": "object", "properties": merged_properties}
281
286
  return patterns[0]
282
-
287
+
283
288
  # Mixed types - could be union, but default to string for OpenAPI compatibility
284
289
  return {"type": "string"}
285
290
 
@@ -290,55 +295,47 @@ def _infer_schema_from_value(value: Any) -> dict:
290
295
  properties = {}
291
296
  for key, val in value.items():
292
297
  properties[str(key)] = _infer_schema_from_value(val)
293
- return {
294
- "type": "object",
295
- "properties": properties
296
- }
297
- elif isinstance(value, list):
298
+ return {"type": "object", "properties": properties}
299
+ if isinstance(value, list):
298
300
  if value:
299
- return {
300
- "type": "array",
301
- "items": _infer_schema_from_value(value[0])
302
- }
301
+ return {"type": "array", "items": _infer_schema_from_value(value[0])}
303
302
  return {"type": "array", "items": {"type": "string"}}
304
- elif isinstance(value, (int, float, str, bool)):
303
+ if type(value) in (int, float, str, bool):
305
304
  return {
306
305
  int: {"type": "integer"},
307
306
  float: {"type": "number"},
308
307
  str: {"type": "string"},
309
- bool: {"type": "boolean"}
308
+ bool: {"type": "boolean"},
310
309
  }[type(value)]
311
- else:
312
- return {"type": "string"}
310
+ return {"type": "string"}
313
311
 
314
312
 
315
313
  def _get_serializer_class_from_schema_name(schema_name: str):
316
314
  """Try to get the serializer class from schema name."""
317
315
  try:
318
- # Import Django and get all installed apps
319
- import django
320
- from django.apps import apps
321
- from rest_framework import serializers
322
-
323
316
  # Search through all apps for the serializer
324
317
  for app in apps.get_app_configs():
325
318
  app_module = app.module
326
319
  try:
327
320
  # Try to import serializers module from the app
328
- serializers_module = __import__(f"{app_module.__name__}.serializers", fromlist=[''])
329
-
321
+ serializers_module = __import__(
322
+ f"{app_module.__name__}.serializers", fromlist=[""]
323
+ )
324
+
330
325
  # Look for serializer class matching the schema name
331
326
  for attr_name in dir(serializers_module):
332
327
  attr = getattr(serializers_module, attr_name)
333
- if (isinstance(attr, type) and
334
- issubclass(attr, serializers.Serializer) and
335
- attr.__name__.replace('Serializer', '') in schema_name):
328
+ if (
329
+ isinstance(attr, type)
330
+ and issubclass(attr, serializers.Serializer)
331
+ and attr.__name__.replace("Serializer", "") in schema_name
332
+ ):
336
333
  return attr
337
334
  except ImportError:
338
335
  continue
339
-
336
+
340
337
  except Exception:
341
- pass
338
+ logger.exception("Failed to get serializser.")
342
339
  return None
343
340
 
344
341
 
@@ -362,35 +359,38 @@ def schema_to_example_json(schema: dict, components: dict, for_response: bool =
362
359
  return _generate_example_by_type(schema, components, for_response)
363
360
 
364
361
 
365
- def _enhance_method_field_schema(schema: dict, components: dict) -> dict:
362
+ def _enhance_method_field_schema(schema: dict, _components: dict) -> dict:
366
363
  """Enhance schema by analyzing SerializerMethodField types."""
367
- if not isinstance(schema, dict) or 'properties' not in schema:
364
+ if not isinstance(schema, dict) or "properties" not in schema:
368
365
  return schema
369
-
366
+
370
367
  # Try to get serializer class from schema title or other hints
371
- schema_title = schema.get('title', '')
368
+ schema_title = schema.get("title", "")
372
369
  serializer_class = _get_serializer_class_from_schema_name(schema_title)
373
-
370
+
374
371
  if not serializer_class:
375
372
  return schema
376
-
373
+
377
374
  enhanced_properties = {}
378
- for prop_name, prop_schema in schema['properties'].items():
375
+ for prop_name, prop_schema in schema["properties"].items():
379
376
  # Check if this looks like an unanalyzed SerializerMethodField
380
- if (isinstance(prop_schema, dict) and
381
- prop_schema.get('type') == 'string' and
382
- not prop_schema.get('enum') and
383
- not prop_schema.get('format') and
384
- not prop_schema.get('example')):
385
-
377
+ if (
378
+ isinstance(prop_schema, dict)
379
+ and prop_schema.get("type") == "string"
380
+ and not prop_schema.get("enum")
381
+ and not prop_schema.get("format")
382
+ and not prop_schema.get("example")
383
+ ):
386
384
  # Try to analyze the method field
387
- analyzed_schema = analyze_serializer_method_field_schema(serializer_class, prop_name)
385
+ analyzed_schema = analyze_serializer_method_field_schema(
386
+ serializer_class, prop_name
387
+ )
388
388
  enhanced_properties[prop_name] = analyzed_schema
389
389
  else:
390
390
  enhanced_properties[prop_name] = prop_schema
391
-
391
+
392
392
  enhanced_schema = schema.copy()
393
- enhanced_schema['properties'] = enhanced_properties
393
+ enhanced_schema["properties"] = enhanced_properties
394
394
  return enhanced_schema
395
395
 
396
396
 
@@ -942,4 +942,3 @@ def create_endpoints_index(
942
942
  ]
943
943
  )
944
944
  generator.create_endpoints_index(endpoints_by_app, docs_dir)
945
-
@@ -1,9 +1,7 @@
1
- #!/usr/bin/env python3
2
-
3
- """Query parameter extraction utilities for Django views."""
4
-
5
1
  from typing import Any
6
2
 
3
+ import django_filters
4
+
7
5
  from drf_to_mkdoc.utils.common import extract_viewset_from_operation_id
8
6
 
9
7
 
@@ -51,7 +49,7 @@ def extract_query_parameters_from_view_filter_fields(view_class: Any) -> list[st
51
49
  elif hasattr(view_class, "filterset_fields") and view_class.filterset_fields:
52
50
  filter_fields = sorted(view_class.filterset_fields)
53
51
 
54
- return filter_fields
52
+ return list(set(filter_fields))
55
53
 
56
54
 
57
55
  def extract_query_parameters_from_view_ordering_fields(view_class: Any) -> list[str]:
@@ -116,8 +114,6 @@ def _extract_filterset_fields_from_class_attributes(filterset_class: Any) -> lis
116
114
  fields = []
117
115
 
118
116
  try:
119
- import django_filters
120
-
121
117
  # Get all class attributes, including inherited ones
122
118
  for attr_name in dir(filterset_class):
123
119
  # Skip private attributes and known non-filter attributes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drf-to-mkdoc
3
- Version: 0.1.6
3
+ Version: 1.0.7
4
4
  Summary: Generate Markdown API docs from Django/DRF OpenAPI schema for MkDocs
5
5
  Author-email: Hossein Shayesteh <shayestehhs1@gmail.com>
6
6
  Maintainer-email: Hossein Shayesteh <shayestehhs1@gmail.com>
@@ -1,14 +1,14 @@
1
- drf_to_mkdoc/__init__.py,sha256=j7qOxIbkDy7oit4Tb3NZUqbqkrxKz07PeN9QuF3Qp9s,179
2
- drf_to_mkdoc/apps.py,sha256=0TLecPHZ8vf0IhAVFh1oIIHQbhx5lVto7qrvStx3R1Y,464
1
+ drf_to_mkdoc/__init__.py,sha256=IbTW5uKQvJRG9ncHRuk_AGKHPn4ruxs5LqDpUFgUhws,180
2
+ drf_to_mkdoc/apps.py,sha256=-NrC_dRr6GmLmNQhkNh819B7V1SS4DYDv5JOR0TtuJM,560
3
3
  drf_to_mkdoc/conf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- drf_to_mkdoc/conf/defaults.py,sha256=9OK65SeP4aLZbuRJBAE_QeC-OhXkh0cACBqax6wYXnM,576
5
- drf_to_mkdoc/conf/settings.py,sha256=OgB3MCn4Z5F4xqWP34kwzMs50kRn3qF0gE1zS2SHS2M,1550
4
+ drf_to_mkdoc/conf/defaults.py,sha256=hEjQdN3WsT539UWhFkylI60piykV_7BDudLPNd_E8PE,575
5
+ drf_to_mkdoc/conf/settings.py,sha256=Gz2Ye5AmPRtOZ3R71IraeK2xBndqEezos9W9xPWYRPY,1513
6
6
  drf_to_mkdoc/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  drf_to_mkdoc/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- drf_to_mkdoc/management/commands/build_docs.py,sha256=p--TajkpYU-hmthXp5eVmVtJnXLxZrfQWQmuzwOgl0c,2491
8
+ drf_to_mkdoc/management/commands/build_docs.py,sha256=QUDE6ROwT6LYAfCc4MkzJZANkGF1BdAQIHfvUwuLYG4,4053
9
9
  drf_to_mkdoc/management/commands/generate_doc_json.py,sha256=mWdYgMbSeLP4iQZeUm2DxwYQmdGy8w05XTEfbT_nOJo,19833
10
- drf_to_mkdoc/management/commands/generate_docs.py,sha256=YGdejd-b1Wn_e5ru9orwp1b9H5PZwVWkuWxAY1JyG88,4897
11
- drf_to_mkdoc/management/commands/generate_model_docs.py,sha256=rtgg77StIg0MomWx8ZAZY6SuFZ_m5ZFQmc0K-RrMw4I,13420
10
+ drf_to_mkdoc/management/commands/generate_docs.py,sha256=JWoMYvfvjYq_Rlmi7RRFfjJ6qsRjI1K-7B5w-qmjMe8,4896
11
+ drf_to_mkdoc/management/commands/generate_model_docs.py,sha256=A_Q6o10kfy-GN_ZDD9YS6jv3RTyxBy28DEsi5qKZZoU,13421
12
12
  drf_to_mkdoc/management/commands/update_doc_schema.py,sha256=TtHVQxnVpB_VELRkVcdsDXDU5zXdguFleB1mXCDMAbg,2009
13
13
  drf_to_mkdoc/static/drf-to-mkdoc/javascripts/endpoints-filter.js,sha256=KtfWroqsXg-wwmk36hpoH--M9WIW85EFNGeswMjFu4g,6138
14
14
  drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/extra.css,sha256=jR52Kyld8fbHCUSehgU0DONcGnjTkqihQqiAXkAvflU,8024
@@ -29,15 +29,15 @@ drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/tags.css,sha256=dOw13qsvV
29
29
  drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/theme-toggle.css,sha256=atnaTx7Ed6YZ0sIfofYBf8wOfUzlLCynrBDMAjHJF58,676
30
30
  drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/variables.css,sha256=JJpvo4MoEsPEiFfugNDbPWvYAGDh0kE1jEl804dX3sk,830
31
31
  drf_to_mkdoc/utils/__init__.py,sha256=6dFTb07S6yIf-INMy0Mlgf5purNir687ZU9WZtITh4k,68
32
- drf_to_mkdoc/utils/common.py,sha256=FnqnEgQkIvXy05zG_xGFm4rmJ1pXURszHGbkuNoNa-4,11750
33
- drf_to_mkdoc/utils/endpoint_generator.py,sha256=oGHQXJB5VFlGOq6W8a3q96CwF3conjBe_tkYj6m2mlg,35849
32
+ drf_to_mkdoc/utils/common.py,sha256=LHq2MjqURLK6VOXNfjWXPADXPDYxxYbcmLZs9jIg36Y,11744
33
+ drf_to_mkdoc/utils/endpoint_generator.py,sha256=2V63lvik_dlxLZ0P4WrNe77iboFLKpUZ9VJAL758tC4,35716
34
34
  drf_to_mkdoc/utils/model_generator.py,sha256=O1ibaw7KmL_fQ1OTebuk6Tt2yTjyElpyF7bN8gk5LBE,9588
35
35
  drf_to_mkdoc/utils/extractors/__init__.py,sha256=BvC8gKOPVI9gU1Piw0jRhKQ2ER5s1moAxgq7ZYkJWNI,86
36
- drf_to_mkdoc/utils/extractors/query_parameter_extractors.py,sha256=9t6NbafXIsbKxquh7VrpnsYOJ70YAsL9fgu_7dSXr5Y,8577
36
+ drf_to_mkdoc/utils/extractors/query_parameter_extractors.py,sha256=-u9TG1hDKVExanypyWM7wQGG9vo80WrfVLbaPXv-Bc4,8494
37
37
  drf_to_mkdoc/utils/md_generators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
38
  drf_to_mkdoc/utils/md_generators/query_parameters_generators.py,sha256=N-XqZ_FUODSR5V4xM9oEA3aaIiNGNmNwpvrWbQTx6RI,2566
39
- drf_to_mkdoc-0.1.6.dist-info/licenses/LICENSE,sha256=3n9_ckIREsH8ogCxWW6dFsw_WfGcluG2mHcgl9i_UU0,1068
40
- drf_to_mkdoc-0.1.6.dist-info/METADATA,sha256=GR296N3kUCyXgQGfkvu1AS1_UnM283eCVHUuEfKTUys,7304
41
- drf_to_mkdoc-0.1.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
42
- drf_to_mkdoc-0.1.6.dist-info/top_level.txt,sha256=ZzJecR6j_tvLZiubUBEgawHflozC4DQy9ooNf1yDJ3Q,13
43
- drf_to_mkdoc-0.1.6.dist-info/RECORD,,
39
+ drf_to_mkdoc-1.0.7.dist-info/licenses/LICENSE,sha256=3n9_ckIREsH8ogCxWW6dFsw_WfGcluG2mHcgl9i_UU0,1068
40
+ drf_to_mkdoc-1.0.7.dist-info/METADATA,sha256=VBPSLEF8EX1LaRDMe31AhZoihyEPBTQcW_50z9lL2lM,7304
41
+ drf_to_mkdoc-1.0.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
42
+ drf_to_mkdoc-1.0.7.dist-info/top_level.txt,sha256=ZzJecR6j_tvLZiubUBEgawHflozC4DQy9ooNf1yDJ3Q,13
43
+ drf_to_mkdoc-1.0.7.dist-info/RECORD,,