-
Notifications
You must be signed in to change notification settings - Fork 12
/
Copy pathdocs_blueprint.py
166 lines (143 loc) · 5.81 KB
/
docs_blueprint.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
from enum import Enum
import flask
from flask import Blueprint, current_app, jsonify
from flask_parameter_validation import ValidateParameters
docs_blueprint = Blueprint(
"docs", __name__, url_prefix="/docs", template_folder="./templates"
)
def get_route_docs():
"""
Generate documentation for all Flask routes that use the ValidateParameters decorator.
Returns a list of dictionaries, each containing documentation for a particular route.
"""
docs = []
for rule in current_app.url_map.iter_rules(): # Iterate through all Flask Routes
rule_func = current_app.view_functions[
rule.endpoint
] # Get the associated function
fn_docs = get_function_docs(rule_func)
if fn_docs:
fn_docs["rule"] = str(rule)
fn_docs["methods"] = [str(method) for method in rule.methods]
docs.append(fn_docs)
return docs
def get_function_docs(func):
"""
Get documentation for a specific function that uses the ValidateParameters decorator.
Returns a dictionary containing documentation details, or None if the decorator is not used.
"""
fn_list = ValidateParameters().get_fn_list()
for fsig, fdocs in fn_list.items():
if hasattr(func, "__fpv_discriminated_sig__") and func.__fpv_discriminated_sig__ == fsig:
return {
"docstring": format_docstring(fdocs.get("docstring")),
"decorators": fdocs.get("decorators"),
"args": extract_argument_details(fdocs),
}
return None
def format_docstring(docstring):
"""
Format a function's docstring for HTML display.
"""
if not docstring:
return None
docstring = docstring.strip().replace("\n", "<br/>")
return docstring.replace(" ", " " * 4)
def extract_argument_details(fdocs):
"""
Extract details about a function's arguments, including type hints and ValidateParameters details.
"""
args_data = {}
for idx, arg_name in enumerate(fdocs["argspec"].args):
arg_data = {
"name": arg_name,
"type": get_arg_type_hint(fdocs, arg_name),
"loc": get_arg_location(fdocs, idx),
"loc_args": get_arg_location_details(fdocs, idx),
}
args_data.setdefault(arg_data["loc"], []).append(arg_data)
return args_data
def get_arg_type_hint(fdocs, arg_name):
"""
Extract the type hint for a specific argument.
"""
arg_type = fdocs["argspec"].annotations[arg_name]
def recursively_resolve_type_hint(type_to_resolve):
if hasattr(type_to_resolve, "__name__"): # In Python 3.9, Optional and Union do not have __name__
type_base_name = type_to_resolve.__name__
elif hasattr(type_to_resolve, "_name") and type_to_resolve._name is not None:
# In Python 3.9, _name exists on list[whatever] and has a non-None value
type_base_name = type_to_resolve._name
else:
# But, in Python 3.9, Optional[whatever] has _name of None - but its __origin__ is Union
type_base_name = type_to_resolve.__origin__._name
if hasattr(type_to_resolve, "__args__"):
return (
f"{type_base_name}[{', '.join([recursively_resolve_type_hint(a) for a in type_to_resolve.__args__])}]"
)
return type_base_name
return recursively_resolve_type_hint(arg_type)
def get_arg_location(fdocs, idx):
"""
Determine where in the request the argument comes from (e.g., Route, Json, Query).
"""
return type(fdocs["argspec"].defaults[idx]).__name__
def get_arg_location_details(fdocs, idx):
"""
Extract additional details about the location of an argument in the request.
"""
loc_details = {}
location = fdocs["argspec"].defaults[idx]
for param, value in location.__dict__.items():
if value is not None:
if callable(value):
loc_details[param] = f"{value.__module__}.{value.__name__}"
elif issubclass(type(value), Enum):
loc_details[param] = f"{type(value).__name__}.{value.name}: "
if issubclass(type(value), int):
loc_details[param] += f"{value.value}"
elif issubclass(type(value), str):
loc_details[param] += f"'{value.value}'"
else:
loc_details[param] = f"FPV: Unsupported Enum type"
elif type(value).__name__ == 'time':
loc_details[param] = value.isoformat()
elif param == 'sources':
loc_details[param] = [type(source).__name__ for source in value]
else:
loc_details[param] = value
return loc_details
@docs_blueprint.app_template_filter()
def http_badge_bg(http_method):
"""
Provide a color badge for various HTTP methods.
"""
color_map = {"GET": "bg-primary", "POST": "bg-success", "DELETE": "bg-danger"}
return color_map.get(http_method, "bg-warning")
@docs_blueprint.route("/")
def docs_html():
"""
Render the documentation as an HTML page.
"""
config = flask.current_app.config
return flask.render_template(
"fpv_default_docs.html",
site_name=config.get("FPV_DOCS_SITE_NAME", "Site"),
docs=get_route_docs(),
custom_blocks=config.get("FPV_DOCS_CUSTOM_BLOCKS", []),
default_theme=config.get("FPV_DOCS_DEFAULT_THEME", "light"),
)
@docs_blueprint.route("/json")
def docs_json():
"""
Provide the documentation as a JSON response.
"""
config = flask.current_app.config
return jsonify(
{
"site_name": config.get("FPV_DOCS_SITE_NAME", "Site"),
"docs": get_route_docs(),
"custom_blocks": config.get("FPV_DOCS_CUSTOM_BLOCKS", []),
"default_theme": config.get("FPV_DOCS_DEFAULT_THEME", "light"),
}
)