|
1
|
""" |
|
2
|
FastAPI framework enricher. |
|
3
|
|
|
4
|
Promotes generic Function/Class nodes to FastAPI semantic types: |
|
5
|
- Route — functions decorated with @app.<method> or @router.<method> |
|
6
|
- Dependency — functions with Depends( in their signature |
|
7
|
- PydanticModel — classes that inherit from BaseModel |
|
8
|
- BackgroundTask — functions decorated with @app.on_event or using BackgroundTasks |
|
9
|
""" |
|
10
|
|
|
11
|
from navegador.enrichment.base import EnrichmentResult, FrameworkEnricher |
|
12
|
from navegador.graph.store import GraphStore |
|
13
|
|
|
14
|
# HTTP verbs recognised as route decorators |
|
15
|
_HTTP_METHODS = ("get", "post", "put", "delete", "patch", "head", "options") |
|
16
|
|
|
17
|
|
|
18
|
class FastAPIEnricher(FrameworkEnricher): |
|
19
|
"""Enriches a navegador graph with FastAPI-specific semantic types.""" |
|
20
|
|
|
21
|
def __init__(self, store: GraphStore) -> None: |
|
22
|
super().__init__(store) |
|
23
|
|
|
24
|
# ── Identity ────────────────────────────────────────────────────────────── |
|
25
|
|
|
26
|
@property |
|
27
|
def framework_name(self) -> str: |
|
28
|
return "fastapi" |
|
29
|
|
|
30
|
@property |
|
31
|
def detection_patterns(self) -> list[str]: |
|
32
|
return ["fastapi"] |
|
33
|
|
|
34
|
# ── Enrichment ──────────────────────────────────────────────────────────── |
|
35
|
|
|
36
|
def enrich(self) -> EnrichmentResult: |
|
37
|
result = EnrichmentResult() |
|
38
|
|
|
39
|
routes = self._enrich_routes() |
|
40
|
result.promoted += routes |
|
41
|
result.patterns_found["routes"] = routes |
|
42
|
|
|
43
|
dependencies = self._enrich_dependencies() |
|
44
|
result.promoted += dependencies |
|
45
|
result.patterns_found["dependencies"] = dependencies |
|
46
|
|
|
47
|
pydantic_models = self._enrich_pydantic_models() |
|
48
|
result.promoted += pydantic_models |
|
49
|
result.patterns_found["pydantic_models"] = pydantic_models |
|
50
|
|
|
51
|
background_tasks = self._enrich_background_tasks() |
|
52
|
result.promoted += background_tasks |
|
53
|
result.patterns_found["background_tasks"] = background_tasks |
|
54
|
|
|
55
|
return result |
|
56
|
|
|
57
|
# ── Pattern helpers ─────────────────────────────────────────────────────── |
|
58
|
|
|
59
|
def _enrich_routes(self) -> int: |
|
60
|
""" |
|
61
|
Find Function/Method nodes linked to Decorator nodes whose names match |
|
62
|
@app.<method> or @router.<method> patterns (e.g. app.get, router.post). |
|
63
|
|
|
64
|
Falls back to querying the ``signature`` and ``docstring`` properties for |
|
65
|
route decorator patterns when no Decorator nodes are present in the graph. |
|
66
|
""" |
|
67
|
promoted = 0 |
|
68
|
|
|
69
|
# Strategy 1: Decorator nodes connected via DECORATES edges |
|
70
|
for http_method in _HTTP_METHODS: |
|
71
|
result = self.store.query( |
|
72
|
"MATCH (d:Decorator)-[:DECORATES]->(n) " |
|
73
|
"WHERE d.name CONTAINS $pattern " |
|
74
|
"RETURN n.name, n.file_path", |
|
75
|
{"pattern": f".{http_method}"}, |
|
76
|
) |
|
77
|
rows = result.result_set or [] |
|
78
|
for row in rows: |
|
79
|
name, file_path = row[0], row[1] |
|
80
|
if name and file_path: |
|
81
|
self._promote_node(name, file_path, "Route", {"http_method": http_method}) |
|
82
|
promoted += 1 |
|
83
|
|
|
84
|
# Strategy 2: signature / docstring heuristics (no Decorator nodes) |
|
85
|
for http_method in _HTTP_METHODS: |
|
86
|
for prop in ("signature", "docstring"): |
|
87
|
result = self.store.query( |
|
88
|
f"MATCH (n) WHERE (n:Function OR n:Method) " |
|
89
|
f"AND n.{prop} IS NOT NULL " |
|
90
|
f"AND n.{prop} CONTAINS $pattern " |
|
91
|
"RETURN n.name, n.file_path", |
|
92
|
{"pattern": f".{http_method}("}, |
|
93
|
) |
|
94
|
rows = result.result_set or [] |
|
95
|
for row in rows: |
|
96
|
name, file_path = row[0], row[1] |
|
97
|
if name and file_path: |
|
98
|
self._promote_node(name, file_path, "Route", {"http_method": http_method}) |
|
99
|
promoted += 1 |
|
100
|
|
|
101
|
return promoted |
|
102
|
|
|
103
|
def _enrich_dependencies(self) -> int: |
|
104
|
""" |
|
105
|
Find Function/Method nodes whose signature contains ``Depends(``. |
|
106
|
These are FastAPI dependency-injection callables. |
|
107
|
""" |
|
108
|
promoted = 0 |
|
109
|
|
|
110
|
for prop in ("signature", "docstring"): |
|
111
|
result = self.store.query( |
|
112
|
f"MATCH (n) WHERE (n:Function OR n:Method) " |
|
113
|
f"AND n.{prop} IS NOT NULL " |
|
114
|
f"AND n.{prop} CONTAINS $pattern " |
|
115
|
"RETURN n.name, n.file_path", |
|
116
|
{"pattern": "Depends("}, |
|
117
|
) |
|
118
|
rows = result.result_set or [] |
|
119
|
for row in rows: |
|
120
|
name, file_path = row[0], row[1] |
|
121
|
if name and file_path: |
|
122
|
self._promote_node(name, file_path, "Dependency") |
|
123
|
promoted += 1 |
|
124
|
|
|
125
|
# Also match via Decorator nodes named "Depends" |
|
126
|
result = self.store.query( |
|
127
|
"MATCH (d:Decorator)-[:DECORATES]->(n) " |
|
128
|
"WHERE d.name CONTAINS $pattern " |
|
129
|
"RETURN n.name, n.file_path", |
|
130
|
{"pattern": "Depends"}, |
|
131
|
) |
|
132
|
rows = result.result_set or [] |
|
133
|
for row in rows: |
|
134
|
name, file_path = row[0], row[1] |
|
135
|
if name and file_path: |
|
136
|
self._promote_node(name, file_path, "Dependency") |
|
137
|
promoted += 1 |
|
138
|
|
|
139
|
return promoted |
|
140
|
|
|
141
|
def _enrich_pydantic_models(self) -> int: |
|
142
|
""" |
|
143
|
Find Class nodes that inherit from ``BaseModel`` via INHERITS edges, |
|
144
|
or whose name appears as a base in the raw INHERITS graph. |
|
145
|
""" |
|
146
|
promoted = 0 |
|
147
|
|
|
148
|
# Primary: INHERITS edges pointing to a node named "BaseModel" |
|
149
|
result = self.store.query( |
|
150
|
"MATCH (n:Class)-[:INHERITS]->(base) " |
|
151
|
"WHERE base.name = $base_name " |
|
152
|
"RETURN n.name, n.file_path", |
|
153
|
{"base_name": "BaseModel"}, |
|
154
|
) |
|
155
|
rows = result.result_set or [] |
|
156
|
for row in rows: |
|
157
|
name, file_path = row[0], row[1] |
|
158
|
if name and file_path: |
|
159
|
self._promote_node(name, file_path, "PydanticModel") |
|
160
|
promoted += 1 |
|
161
|
|
|
162
|
# Fallback: docstring or source mentions BaseModel |
|
163
|
result = self.store.query( |
|
164
|
"MATCH (n:Class) " |
|
165
|
"WHERE n.docstring IS NOT NULL AND n.docstring CONTAINS $pattern " |
|
166
|
"RETURN n.name, n.file_path", |
|
167
|
{"pattern": "BaseModel"}, |
|
168
|
) |
|
169
|
rows = result.result_set or [] |
|
170
|
for row in rows: |
|
171
|
name, file_path = row[0], row[1] |
|
172
|
if name and file_path: |
|
173
|
self._promote_node(name, file_path, "PydanticModel") |
|
174
|
promoted += 1 |
|
175
|
|
|
176
|
return promoted |
|
177
|
|
|
178
|
def _enrich_background_tasks(self) -> int: |
|
179
|
""" |
|
180
|
Find Function/Method nodes that: |
|
181
|
- are decorated with @app.on_event (via Decorator nodes or signature), or |
|
182
|
- reference ``BackgroundTasks`` in their signature / docstring. |
|
183
|
""" |
|
184
|
promoted = 0 |
|
185
|
|
|
186
|
# Strategy 1: Decorator nodes matching on_event |
|
187
|
result = self.store.query( |
|
188
|
"MATCH (d:Decorator)-[:DECORATES]->(n) " |
|
189
|
"WHERE d.name CONTAINS $pattern " |
|
190
|
"RETURN n.name, n.file_path", |
|
191
|
{"pattern": "on_event"}, |
|
192
|
) |
|
193
|
rows = result.result_set or [] |
|
194
|
for row in rows: |
|
195
|
name, file_path = row[0], row[1] |
|
196
|
if name and file_path: |
|
197
|
self._promote_node(name, file_path, "BackgroundTask") |
|
198
|
promoted += 1 |
|
199
|
|
|
200
|
# Strategy 2: signature / docstring heuristics for on_event and BackgroundTasks |
|
201
|
for pattern in ("on_event(", "BackgroundTasks"): |
|
202
|
for prop in ("signature", "docstring"): |
|
203
|
result = self.store.query( |
|
204
|
f"MATCH (n) WHERE (n:Function OR n:Method) " |
|
205
|
f"AND n.{prop} IS NOT NULL " |
|
206
|
f"AND n.{prop} CONTAINS $pattern " |
|
207
|
"RETURN n.name, n.file_path", |
|
208
|
{"pattern": pattern}, |
|
209
|
) |
|
210
|
rows = result.result_set or [] |
|
211
|
for row in rows: |
|
212
|
name, file_path = row[0], row[1] |
|
213
|
if name and file_path: |
|
214
|
self._promote_node(name, file_path, "BackgroundTask") |
|
215
|
promoted += 1 |
|
216
|
|
|
217
|
return promoted |
|
218
|
|