griffe-fastapi 0.1.0__tar.gz
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.
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: griffe-fastapi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Griffe extension for FastAPI.
|
|
5
|
+
Author: fbraem
|
|
6
|
+
Author-email: franky.braem@gmail.com
|
|
7
|
+
Requires-Python: >=3.12,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Requires-Dist: griffe (>=1.5.1,<2.0.0)
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
Griffe FastAPI Extension
|
|
14
|
+
========================
|
|
15
|
+
|
|
16
|
+
This extension will search for functions that are decorated with an APIRouter and adds the following extra
|
|
17
|
+
fields to a function:
|
|
18
|
+
|
|
19
|
+
+ method: the HTTP method
|
|
20
|
+
+ responses: A dictionary with the responses
|
|
21
|
+
|
|
22
|
+
These fields are stored in the extra property of the function. The extra property is a dictionary and `griffe_fastapi`
|
|
23
|
+
is the key for the fields of this extension.
|
|
24
|
+
|
|
25
|
+
Create a custom function template to handle these extra fields in your documentation.
|
|
26
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Griffe FastAPI Extension
|
|
2
|
+
========================
|
|
3
|
+
|
|
4
|
+
This extension will search for functions that are decorated with an APIRouter and adds the following extra
|
|
5
|
+
fields to a function:
|
|
6
|
+
|
|
7
|
+
+ method: the HTTP method
|
|
8
|
+
+ responses: A dictionary with the responses
|
|
9
|
+
|
|
10
|
+
These fields are stored in the extra property of the function. The extra property is a dictionary and `griffe_fastapi`
|
|
11
|
+
is the key for the fields of this extension.
|
|
12
|
+
|
|
13
|
+
Create a custom function template to handle these extra fields in your documentation.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "griffe-fastapi"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Griffe extension for FastAPI."
|
|
5
|
+
authors = ["fbraem <franky.braem@gmail.com>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
|
|
8
|
+
[tool.poetry.dependencies]
|
|
9
|
+
python = "^3.12"
|
|
10
|
+
griffe = "^1.5.1"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
[tool.poetry.group.test.dependencies]
|
|
14
|
+
pytest = "^8.3.3"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
[tool.poetry.group.dev.dependencies]
|
|
18
|
+
ruff = "^0.7.4"
|
|
19
|
+
black = "^24.10.0"
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["poetry-core"]
|
|
23
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""griffe_fastapi extension."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from typing import Any
|
|
5
|
+
from griffe import (
|
|
6
|
+
Decorator,
|
|
7
|
+
ExprAttribute,
|
|
8
|
+
ExprDict,
|
|
9
|
+
ExprKeyword,
|
|
10
|
+
ExprName,
|
|
11
|
+
Extension,
|
|
12
|
+
Function,
|
|
13
|
+
Inspector,
|
|
14
|
+
ObjectNode,
|
|
15
|
+
Visitor,
|
|
16
|
+
get_logger,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
self_namespace = "griffe_fastapi"
|
|
20
|
+
|
|
21
|
+
logger = get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _search_decorator(decorators: list[Decorator]) -> Decorator | None:
|
|
25
|
+
"""Search for a APIRouter decorator."""
|
|
26
|
+
decorators = list(
|
|
27
|
+
filter(
|
|
28
|
+
lambda d: d.value.canonical_name in ("get", "post", "patch", "delete"),
|
|
29
|
+
decorators,
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
if len(decorators) == 0:
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
for decorator in decorators:
|
|
36
|
+
module = decorator.value.function.first.parent
|
|
37
|
+
if decorator.value.function.first.name not in module.members:
|
|
38
|
+
logger.warning(
|
|
39
|
+
f"Cannot find {decorator.value.function.first.name} in module {module.name}"
|
|
40
|
+
)
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
type_ = module.members[decorator.value.function.first.name].value.canonical_name
|
|
44
|
+
if type_ == "APIRouter":
|
|
45
|
+
return decorator
|
|
46
|
+
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _process_responses(
|
|
51
|
+
func: Function,
|
|
52
|
+
http_code_attribute: str | ExprAttribute,
|
|
53
|
+
open_api_response: ExprName | ExprDict,
|
|
54
|
+
):
|
|
55
|
+
"""Process the response code and the response object."""
|
|
56
|
+
http_code = None
|
|
57
|
+
# When a constant is used, resolve the value
|
|
58
|
+
if isinstance(http_code_attribute, ExprAttribute):
|
|
59
|
+
if http_code_attribute.canonical_path.startswith("fastapi.status."):
|
|
60
|
+
http_code = http_code_attribute.last.name.split("_")[1]
|
|
61
|
+
if http_code is None:
|
|
62
|
+
logger.warning(
|
|
63
|
+
f"Could not resolve http code {http_code_attribute.canonical_path} "
|
|
64
|
+
f"for function {func.canonical_path}"
|
|
65
|
+
)
|
|
66
|
+
return
|
|
67
|
+
else:
|
|
68
|
+
http_code = http_code_attribute
|
|
69
|
+
|
|
70
|
+
func.extra[self_namespace]["responses"][http_code] = {
|
|
71
|
+
ast.literal_eval(str(key)): ast.literal_eval(str(value))
|
|
72
|
+
for key, value in zip(
|
|
73
|
+
open_api_response.keys,
|
|
74
|
+
open_api_response.values,
|
|
75
|
+
strict=True,
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class FastAPIExtension(Extension):
|
|
81
|
+
def __init__(self, *, paths: list[str] | None = None):
|
|
82
|
+
"""Initialize the extension.
|
|
83
|
+
|
|
84
|
+
When paths are set, the extension will only process the modules of the given
|
|
85
|
+
path.
|
|
86
|
+
"""
|
|
87
|
+
super().__init__()
|
|
88
|
+
self._paths = paths or []
|
|
89
|
+
|
|
90
|
+
def on_function_instance(
|
|
91
|
+
self,
|
|
92
|
+
*,
|
|
93
|
+
node: ast.AST | ObjectNode,
|
|
94
|
+
func: Function,
|
|
95
|
+
agent: Visitor | Inspector,
|
|
96
|
+
**kwargs: Any,
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Implement the function instance handler."""
|
|
99
|
+
|
|
100
|
+
# When paths is set, skip functions that are not part of the path.
|
|
101
|
+
if self._paths:
|
|
102
|
+
if not any(func.path.startswith(path) for path in self._paths):
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
decorator = _search_decorator(func.decorators)
|
|
106
|
+
if decorator is None:
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
func.extra[self_namespace] = {"method": decorator.value.canonical_name}
|
|
110
|
+
|
|
111
|
+
# Search the "responses" keyword in the arguments of the function.
|
|
112
|
+
responses = next(
|
|
113
|
+
(
|
|
114
|
+
x
|
|
115
|
+
for x in decorator.value.arguments
|
|
116
|
+
if isinstance(x, ExprKeyword) and x.name == "responses"
|
|
117
|
+
),
|
|
118
|
+
None,
|
|
119
|
+
)
|
|
120
|
+
if responses is None:
|
|
121
|
+
logger.warning(
|
|
122
|
+
f"No responses argument found for function {func.canonical_path}"
|
|
123
|
+
)
|
|
124
|
+
return
|
|
125
|
+
if not isinstance(responses.value, ExprDict):
|
|
126
|
+
logger.warning(
|
|
127
|
+
f"responses argument is not a dict for function {func.canonical_path}"
|
|
128
|
+
)
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
resolved_responses = {}
|
|
132
|
+
|
|
133
|
+
for http_code_variable, open_api_response_obj in zip(
|
|
134
|
+
responses.value.keys, responses.value.values, strict=True
|
|
135
|
+
):
|
|
136
|
+
# When the response contains a variable, try to resolve it.
|
|
137
|
+
if isinstance(open_api_response_obj, ExprName):
|
|
138
|
+
module_attribute = func.module.members[open_api_response_obj.name]
|
|
139
|
+
if isinstance(module_attribute.value, ExprDict):
|
|
140
|
+
resolved_responses = {
|
|
141
|
+
**resolved_responses,
|
|
142
|
+
**{
|
|
143
|
+
k: v
|
|
144
|
+
for k, v in zip(
|
|
145
|
+
module_attribute.value.keys,
|
|
146
|
+
module_attribute.value.values,
|
|
147
|
+
)
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
else:
|
|
151
|
+
resolved_responses[http_code_variable] = open_api_response_obj
|
|
152
|
+
|
|
153
|
+
func.extra[self_namespace]["responses"] = {}
|
|
154
|
+
for key, value in resolved_responses.items():
|
|
155
|
+
_process_responses(func, key, value)
|