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 +1 -1
- drf_to_mkdoc/apps.py +6 -2
- drf_to_mkdoc/conf/defaults.py +0 -1
- drf_to_mkdoc/conf/settings.py +8 -5
- drf_to_mkdoc/management/commands/build_docs.py +61 -20
- drf_to_mkdoc/management/commands/generate_docs.py +1 -2
- drf_to_mkdoc/management/commands/generate_model_docs.py +1 -0
- drf_to_mkdoc/utils/common.py +27 -22
- drf_to_mkdoc/utils/endpoint_generator.py +130 -131
- drf_to_mkdoc/utils/extractors/query_parameter_extractors.py +3 -7
- {drf_to_mkdoc-0.1.6.dist-info → drf_to_mkdoc-1.0.7.dist-info}/METADATA +1 -1
- {drf_to_mkdoc-0.1.6.dist-info → drf_to_mkdoc-1.0.7.dist-info}/RECORD +15 -15
- {drf_to_mkdoc-0.1.6.dist-info → drf_to_mkdoc-1.0.7.dist-info}/WHEEL +0 -0
- {drf_to_mkdoc-0.1.6.dist-info → drf_to_mkdoc-1.0.7.dist-info}/licenses/LICENSE +0 -0
- {drf_to_mkdoc-0.1.6.dist-info → drf_to_mkdoc-1.0.7.dist-info}/top_level.txt +0 -0
drf_to_mkdoc/__init__.py
CHANGED
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
|
-
|
|
19
|
+
logger.exception("Failed to import drf_to_mkdoc commands")
|
drf_to_mkdoc/conf/defaults.py
CHANGED
|
@@ -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
|
}
|
drf_to_mkdoc/conf/settings.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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):
|
drf_to_mkdoc/utils/common.py
CHANGED
|
@@ -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
|
|
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() ->
|
|
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() ->
|
|
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() ->
|
|
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) ->
|
|
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(
|
|
197
|
-
param_type = param_info.get(
|
|
198
|
-
param_format = param_info.get(
|
|
199
|
-
|
|
200
|
-
if param_type ==
|
|
201
|
-
converter =
|
|
202
|
-
elif param_type ==
|
|
203
|
-
converter =
|
|
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 =
|
|
209
|
+
converter = "str"
|
|
206
210
|
|
|
207
|
-
return f
|
|
211
|
+
return f"<{converter}:{param_name}>"
|
|
208
212
|
|
|
209
|
-
django_path = re.sub(r
|
|
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
|
|
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
|
|
21
|
-
|
|
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,
|
|
63
|
+
if hasattr(method, "_spectacular_annotation"):
|
|
57
64
|
annotation = method._spectacular_annotation
|
|
58
65
|
# Handle OpenApiTypes
|
|
59
|
-
if hasattr(annotation,
|
|
66
|
+
if hasattr(annotation, "type"):
|
|
60
67
|
return {"type": annotation.type}
|
|
61
|
-
|
|
68
|
+
if isinstance(annotation, dict):
|
|
62
69
|
return annotation
|
|
63
|
-
|
|
70
|
+
|
|
64
71
|
# Check for drf-yasg decorator
|
|
65
|
-
if hasattr(method,
|
|
72
|
+
if hasattr(method, "_swagger_serializer_method"):
|
|
66
73
|
swagger_info = method._swagger_serializer_method
|
|
67
|
-
if hasattr(swagger_info,
|
|
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
|
-
|
|
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
|
|
88
|
-
return {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
107
|
+
if origin is dict:
|
|
101
108
|
return {"type": "object"}
|
|
102
|
-
|
|
109
|
+
|
|
103
110
|
except Exception:
|
|
104
|
-
|
|
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
|
-
|
|
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(
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
185
|
+
if isinstance(node, ast.List):
|
|
172
186
|
return self._analyze_list_return(node)
|
|
173
|
-
|
|
187
|
+
if isinstance(node, ast.Constant):
|
|
174
188
|
return self._analyze_constant_return(node)
|
|
175
|
-
|
|
189
|
+
if isinstance(node, ast.Call):
|
|
176
190
|
return self._analyze_method_call_return(node)
|
|
177
|
-
|
|
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
|
-
|
|
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 [
|
|
224
|
+
if method_name in ["count"]:
|
|
217
225
|
return {"type": "integer"}
|
|
218
|
-
|
|
226
|
+
if method_name in ["all", "filter", "exclude"]:
|
|
219
227
|
return {"type": "array", "items": {"type": "object"}}
|
|
220
|
-
|
|
228
|
+
if method_name in ["first", "last", "get"]:
|
|
221
229
|
return {"type": "object"}
|
|
222
|
-
|
|
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 [
|
|
241
|
+
if attr_name in ["id", "pk", "count"]:
|
|
234
242
|
return {"type": "integer"}
|
|
235
|
-
|
|
243
|
+
if attr_name in ["name", "title", "description", "slug"]:
|
|
236
244
|
return {"type": "string"}
|
|
237
|
-
|
|
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
|
-
|
|
254
|
+
if isinstance(node, ast.Call):
|
|
247
255
|
return self._analyze_method_call_return(node)
|
|
248
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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__(
|
|
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 (
|
|
334
|
-
|
|
335
|
-
attr.
|
|
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
|
-
|
|
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,
|
|
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
|
|
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(
|
|
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[
|
|
375
|
+
for prop_name, prop_schema in schema["properties"].items():
|
|
379
376
|
# Check if this looks like an unanalyzed SerializerMethodField
|
|
380
|
-
if (
|
|
381
|
-
prop_schema
|
|
382
|
-
|
|
383
|
-
not prop_schema.get(
|
|
384
|
-
not prop_schema.get(
|
|
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(
|
|
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[
|
|
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.
|
|
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=
|
|
2
|
-
drf_to_mkdoc/apps.py,sha256
|
|
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=
|
|
5
|
-
drf_to_mkdoc/conf/settings.py,sha256=
|
|
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=
|
|
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=
|
|
11
|
-
drf_to_mkdoc/management/commands/generate_model_docs.py,sha256=
|
|
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=
|
|
33
|
-
drf_to_mkdoc/utils/endpoint_generator.py,sha256=
|
|
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
|
|
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.
|
|
40
|
-
drf_to_mkdoc-0.
|
|
41
|
-
drf_to_mkdoc-0.
|
|
42
|
-
drf_to_mkdoc-0.
|
|
43
|
-
drf_to_mkdoc-0.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|