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,8 @@
1
+ """griffe-fastapi package.
2
+
3
+ Griffe extension for FastAPI.
4
+ """
5
+
6
+ from griffe_fastapi._extension import FastAPIExtension
7
+
8
+ __all__: list[str] = ["FastAPIExtension"]
@@ -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)