code-node-graph 0.1.0__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.
- code_node/__init__.py +40 -0
- code_node/__main__.py +7 -0
- code_node/analysis/__init__.py +3 -0
- code_node/analysis/aws_model.py +197 -0
- code_node/analysis/findings.py +462 -0
- code_node/analysis/graph.py +780 -0
- code_node/analysis/mapping.py +114 -0
- code_node/analysis/model.py +289 -0
- code_node/analysis/reader.py +1926 -0
- code_node/analysis/syntax.py +220 -0
- code_node/analysis/terms.py +505 -0
- code_node/cli.py +316 -0
- code_node/deploys/__init__.py +321 -0
- code_node/deploys/sam.py +371 -0
- code_node/deploys/serverless.py +117 -0
- code_node/kit/__init__.py +4 -0
- code_node/kit/blocks.py +184 -0
- code_node/kit/fmt.py +389 -0
- code_node/kit/highlight.py +131 -0
- code_node/kit/render.py +1066 -0
- code_node/kit/text.py +81 -0
- code_node/libraries/__init__.py +333 -0
- code_node/libraries/llm.py +124 -0
- code_node/libraries/mongo.py +169 -0
- code_node/libraries/redis.py +253 -0
- code_node/libraries/sql.py +232 -0
- code_node/pages.py +92 -0
- code_node/platforms/__init__.py +203 -0
- code_node/platforms/aws_lambda.py +290 -0
- code_node/platforms/web.py +301 -0
- code_node/project.py +402 -0
- code_node/report/__init__.py +3 -0
- code_node/report/brands.py +244 -0
- code_node/report/build.py +119 -0
- code_node/report/editor.py +40 -0
- code_node/report/graph.py +1411 -0
- code_node/report/logos/README.md +8 -0
- code_node/report/logos/anthropic-icon.svg +1 -0
- code_node/report/logos/aws-amplify.svg +1 -0
- code_node/report/logos/aws-api-gateway.svg +1 -0
- code_node/report/logos/aws-appflow.svg +1 -0
- code_node/report/logos/aws-appsync.svg +1 -0
- code_node/report/logos/aws-athena.svg +1 -0
- code_node/report/logos/aws-backup.svg +1 -0
- code_node/report/logos/aws-batch.svg +1 -0
- code_node/report/logos/aws-certificate-manager.svg +1 -0
- code_node/report/logos/aws-cloudformation.svg +1 -0
- code_node/report/logos/aws-cloudfront.svg +1 -0
- code_node/report/logos/aws-cloudtrail.svg +1 -0
- code_node/report/logos/aws-cloudwatch.svg +1 -0
- code_node/report/logos/aws-codebuild.svg +1 -0
- code_node/report/logos/aws-codecommit.svg +1 -0
- code_node/report/logos/aws-codedeploy.svg +1 -0
- code_node/report/logos/aws-codepipeline.svg +1 -0
- code_node/report/logos/aws-cognito.svg +1 -0
- code_node/report/logos/aws-config.svg +1 -0
- code_node/report/logos/aws-documentdb.svg +1 -0
- code_node/report/logos/aws-dynamodb.svg +1 -0
- code_node/report/logos/aws-ec2.svg +1 -0
- code_node/report/logos/aws-ecs.svg +1 -0
- code_node/report/logos/aws-eks.svg +1 -0
- code_node/report/logos/aws-elasticache.svg +1 -0
- code_node/report/logos/aws-elb.svg +1 -0
- code_node/report/logos/aws-eventbridge.svg +1 -0
- code_node/report/logos/aws-glacier.svg +1 -0
- code_node/report/logos/aws-glue.svg +1 -0
- code_node/report/logos/aws-iam.svg +1 -0
- code_node/report/logos/aws-keyspaces.svg +1 -0
- code_node/report/logos/aws-kinesis.svg +1 -0
- code_node/report/logos/aws-kms.svg +1 -0
- code_node/report/logos/aws-lake-formation.svg +1 -0
- code_node/report/logos/aws-lambda.svg +1 -0
- code_node/report/logos/aws-lightsail.svg +1 -0
- code_node/report/logos/aws-mq.svg +1 -0
- code_node/report/logos/aws-msk.svg +1 -0
- code_node/report/logos/aws-neptune.svg +1 -0
- code_node/report/logos/aws-open-search.svg +1 -0
- code_node/report/logos/aws-quicksight.svg +1 -0
- code_node/report/logos/aws-rds.svg +1 -0
- code_node/report/logos/aws-redshift.svg +1 -0
- code_node/report/logos/aws-route53.svg +1 -0
- code_node/report/logos/aws-s3.svg +1 -0
- code_node/report/logos/aws-secrets-manager.svg +1 -0
- code_node/report/logos/aws-ses.svg +1 -0
- code_node/report/logos/aws-shield.svg +1 -0
- code_node/report/logos/aws-sns.svg +1 -0
- code_node/report/logos/aws-sqs.svg +1 -0
- code_node/report/logos/aws-step-functions.svg +1 -0
- code_node/report/logos/aws-systems-manager.svg +1 -0
- code_node/report/logos/aws-timestream.svg +1 -0
- code_node/report/logos/aws-waf.svg +1 -0
- code_node/report/logos/aws-xray.svg +1 -0
- code_node/report/logos/aws.svg +1 -0
- code_node/report/logos/azure-icon.svg +1 -0
- code_node/report/logos/django-icon.svg +1 -0
- code_node/report/logos/elasticsearch.svg +1 -0
- code_node/report/logos/fastapi-icon.svg +1 -0
- code_node/report/logos/firebase-icon.svg +1 -0
- code_node/report/logos/flask.svg +1 -0
- code_node/report/logos/google-cloud.svg +1 -0
- code_node/report/logos/kafka-icon.svg +1 -0
- code_node/report/logos/mariadb-icon.svg +1 -0
- code_node/report/logos/mongodb-icon.svg +1 -0
- code_node/report/logos/mysql-icon.svg +1 -0
- code_node/report/logos/openai-icon.svg +1 -0
- code_node/report/logos/postgresql.svg +1 -0
- code_node/report/logos/python.svg +1 -0
- code_node/report/logos/rabbitmq-icon.svg +1 -0
- code_node/report/logos/redis.svg +1 -0
- code_node/report/logos/serverless.svg +1 -0
- code_node/report/logos/slack-icon.svg +1 -0
- code_node/report/logos/small/aws-api-gateway.svg +1 -0
- code_node/report/logos/small/aws-appflow.svg +1 -0
- code_node/report/logos/small/aws-appsync.svg +1 -0
- code_node/report/logos/small/aws-athena.svg +1 -0
- code_node/report/logos/small/aws-backup.svg +1 -0
- code_node/report/logos/small/aws-batch.svg +1 -0
- code_node/report/logos/small/aws-certificate-manager.svg +1 -0
- code_node/report/logos/small/aws-cloudformation.svg +1 -0
- code_node/report/logos/small/aws-cloudfront.svg +1 -0
- code_node/report/logos/small/aws-cloudtrail.svg +1 -0
- code_node/report/logos/small/aws-cloudwatch.svg +1 -0
- code_node/report/logos/small/aws-codebuild.svg +1 -0
- code_node/report/logos/small/aws-codecommit.svg +1 -0
- code_node/report/logos/small/aws-codedeploy.svg +1 -0
- code_node/report/logos/small/aws-codepipeline.svg +1 -0
- code_node/report/logos/small/aws-cognito.svg +1 -0
- code_node/report/logos/small/aws-config.svg +1 -0
- code_node/report/logos/small/aws-documentdb.svg +1 -0
- code_node/report/logos/small/aws-dynamodb.svg +1 -0
- code_node/report/logos/small/aws-ec2.svg +1 -0
- code_node/report/logos/small/aws-ecs.svg +1 -0
- code_node/report/logos/small/aws-eks.svg +1 -0
- code_node/report/logos/small/aws-elasticache.svg +1 -0
- code_node/report/logos/small/aws-elb.svg +1 -0
- code_node/report/logos/small/aws-eventbridge.svg +1 -0
- code_node/report/logos/small/aws-glacier.svg +1 -0
- code_node/report/logos/small/aws-glue.svg +1 -0
- code_node/report/logos/small/aws-iam.svg +1 -0
- code_node/report/logos/small/aws-keyspaces.svg +1 -0
- code_node/report/logos/small/aws-kinesis.svg +1 -0
- code_node/report/logos/small/aws-kms.svg +1 -0
- code_node/report/logos/small/aws-lake-formation.svg +1 -0
- code_node/report/logos/small/aws-lambda.svg +1 -0
- code_node/report/logos/small/aws-mq.svg +1 -0
- code_node/report/logos/small/aws-msk.svg +1 -0
- code_node/report/logos/small/aws-neptune.svg +1 -0
- code_node/report/logos/small/aws-open-search.svg +1 -0
- code_node/report/logos/small/aws-quicksight.svg +1 -0
- code_node/report/logos/small/aws-rds.svg +1 -0
- code_node/report/logos/small/aws-redshift.svg +1 -0
- code_node/report/logos/small/aws-route53.svg +1 -0
- code_node/report/logos/small/aws-s3.svg +1 -0
- code_node/report/logos/small/aws-secrets-manager.svg +1 -0
- code_node/report/logos/small/aws-ses.svg +1 -0
- code_node/report/logos/small/aws-shield.svg +1 -0
- code_node/report/logos/small/aws-sns.svg +1 -0
- code_node/report/logos/small/aws-sqs.svg +1 -0
- code_node/report/logos/small/aws-step-functions.svg +1 -0
- code_node/report/logos/small/aws-systems-manager.svg +1 -0
- code_node/report/logos/small/aws-timestream.svg +1 -0
- code_node/report/logos/small/aws-waf.svg +1 -0
- code_node/report/logos/small/aws-xray.svg +1 -0
- code_node/report/logos/snowflake-icon.svg +1 -0
- code_node/report/logos/sqlite.svg +1 -0
- code_node/report/logos/stripe.svg +1 -0
- code_node/report/logos/supabase-icon.svg +1 -0
- code_node/report/logos/twilio-icon.svg +1 -0
- code_node/report/outline.py +219 -0
- code_node/report/style.py +358 -0
- code_node/report/tabs.py +73 -0
- code_node/report/walk.py +624 -0
- code_node/serve.py +277 -0
- code_node_graph-0.1.0.dist-info/METADATA +165 -0
- code_node_graph-0.1.0.dist-info/RECORD +178 -0
- code_node_graph-0.1.0.dist-info/WHEEL +4 -0
- code_node_graph-0.1.0.dist-info/entry_points.txt +2 -0
- code_node_graph-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A web app as a platform: FastAPI, Flask and Django. Each route is an entry point, and what it's called with is the
|
|
3
|
+
request: a route function's parameters (FastAPI's path, query and body, a Flask or Django view's URL values, a Django
|
|
4
|
+
view's request) and Flask's request object are the input, so text built from them is followed into SQL and shell
|
|
5
|
+
commands. Code at import runs once per worker process, and a call that blocks in an async route holds up every other
|
|
6
|
+
request on its event loop.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import ast
|
|
12
|
+
import re
|
|
13
|
+
from collections import defaultdict
|
|
14
|
+
|
|
15
|
+
from ..analysis.findings import _listed, _short_name, _where
|
|
16
|
+
from ..analysis.model import AwsCall, CodeMap, CodeSpot
|
|
17
|
+
from . import Platform
|
|
18
|
+
|
|
19
|
+
_PLACEHOLDER_RE = re.compile(r"\{(\w+)(?::[^}]*)?\}|<(?:[^:<>]+:)?(\w+)>|\(\?P<(\w+)>") # {id}, <int:id>, (?P<id>...)
|
|
20
|
+
|
|
21
|
+
_MARKERS = {
|
|
22
|
+
"Query": "query",
|
|
23
|
+
"Path": "path",
|
|
24
|
+
"Body": "body",
|
|
25
|
+
"Header": "headers",
|
|
26
|
+
"Cookie": "cookies",
|
|
27
|
+
"Form": "form",
|
|
28
|
+
"File": "files",
|
|
29
|
+
} # FastAPI's: where a parameter comes from in the request
|
|
30
|
+
|
|
31
|
+
_NOT_INPUT = {
|
|
32
|
+
"Depends",
|
|
33
|
+
"Security",
|
|
34
|
+
"BackgroundTasks",
|
|
35
|
+
"Response",
|
|
36
|
+
"SecurityScopes",
|
|
37
|
+
"Session",
|
|
38
|
+
"AsyncSession",
|
|
39
|
+
"Connection",
|
|
40
|
+
"AsyncConnection",
|
|
41
|
+
} # FastAPI fills these itself, not from the request
|
|
42
|
+
|
|
43
|
+
_WHOLE = {"Request", "WebSocket", "HTTPConnection"} # the request itself
|
|
44
|
+
|
|
45
|
+
_SCALARS = {
|
|
46
|
+
"int",
|
|
47
|
+
"str",
|
|
48
|
+
"float",
|
|
49
|
+
"bool",
|
|
50
|
+
"bytes",
|
|
51
|
+
"UUID",
|
|
52
|
+
"date",
|
|
53
|
+
"datetime",
|
|
54
|
+
"time",
|
|
55
|
+
"timedelta",
|
|
56
|
+
"Decimal",
|
|
57
|
+
"EmailStr",
|
|
58
|
+
"HttpUrl",
|
|
59
|
+
"AnyUrl",
|
|
60
|
+
"Any",
|
|
61
|
+
"Literal",
|
|
62
|
+
"list",
|
|
63
|
+
"List",
|
|
64
|
+
"set",
|
|
65
|
+
"Set",
|
|
66
|
+
"tuple",
|
|
67
|
+
"Tuple",
|
|
68
|
+
"Sequence",
|
|
69
|
+
"frozenset",
|
|
70
|
+
"FrozenSet",
|
|
71
|
+
"StrictStr",
|
|
72
|
+
"StrictInt",
|
|
73
|
+
"conint",
|
|
74
|
+
"constr",
|
|
75
|
+
} # a query parameter's
|
|
76
|
+
|
|
77
|
+
_WRAPPERS = {
|
|
78
|
+
"Optional",
|
|
79
|
+
"Union",
|
|
80
|
+
"Annotated",
|
|
81
|
+
"list",
|
|
82
|
+
"List",
|
|
83
|
+
"set",
|
|
84
|
+
"Set",
|
|
85
|
+
"tuple",
|
|
86
|
+
"Tuple",
|
|
87
|
+
"Sequence",
|
|
88
|
+
"frozenset",
|
|
89
|
+
"FrozenSet",
|
|
90
|
+
} # look inside them for the type that says where it comes from
|
|
91
|
+
|
|
92
|
+
FRAMEWORK_LOGOS = { # a module the code imports -> its framework's logo (report/logos/), the first one found
|
|
93
|
+
"fastapi": "fastapi-icon",
|
|
94
|
+
"flask": "flask",
|
|
95
|
+
"quart": "flask",
|
|
96
|
+
"rest_framework": "django-icon",
|
|
97
|
+
"django": "django-icon",
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
_BLOCKING_HTTP = ("requests.", "urllib.request.", "urlopen(") # HTTP that waits for the answer, as code starts
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _named(node: ast.expr | None) -> str:
|
|
104
|
+
"""
|
|
105
|
+
The name a type or a call is written with: Query(...) -> 'Query', fastapi.Request -> 'Request'.
|
|
106
|
+
"""
|
|
107
|
+
if isinstance(node, ast.Call):
|
|
108
|
+
node = node.func
|
|
109
|
+
if isinstance(node, ast.Name):
|
|
110
|
+
return node.id
|
|
111
|
+
if isinstance(node, ast.Attribute):
|
|
112
|
+
return node.attr
|
|
113
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str): # a forward reference: 'Order'
|
|
114
|
+
return node.value.rsplit(".", 1)[-1].split("[")[0].strip()
|
|
115
|
+
return ""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _marker(node: ast.expr | None) -> ast.expr | None:
|
|
119
|
+
"""
|
|
120
|
+
FastAPI's Query(), Body(), Depends()... when that's what node is.
|
|
121
|
+
"""
|
|
122
|
+
return node if _named(node) in _MARKERS or _named(node) in _NOT_INPUT else None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _core(node: ast.expr | None) -> tuple[str, ast.expr | None]:
|
|
126
|
+
"""
|
|
127
|
+
A parameter's annotation as (its type, the FastAPI marker in it): Annotated[str, Query()] -> ('str', Query()),
|
|
128
|
+
list[Item] | None -> ('Item', None), int -> ('int', None).
|
|
129
|
+
"""
|
|
130
|
+
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr): # X | None
|
|
131
|
+
left, right = _core(node.left), _core(node.right)
|
|
132
|
+
return left if left[0] != "None" else right
|
|
133
|
+
if isinstance(node, ast.Subscript) and _named(node.value) in _WRAPPERS:
|
|
134
|
+
inner = node.slice.elts if isinstance(node.slice, ast.Tuple) else [node.slice]
|
|
135
|
+
if _named(node.value) == "Annotated":
|
|
136
|
+
return _core(inner[0])[0], next((m for m in map(_marker, inner[1:]) if m is not None), None)
|
|
137
|
+
found = [_core(e) for e in inner]
|
|
138
|
+
return next((f for f in found if f[0] not in ("None", "")), ("", None))
|
|
139
|
+
if isinstance(node, ast.Constant) and node.value is None:
|
|
140
|
+
return "None", None
|
|
141
|
+
return _named(node), None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _fastapi(names: set[str], params: list[tuple[ast.arg, ast.expr | None]]) -> dict[str, tuple[str, ...]]:
|
|
145
|
+
"""
|
|
146
|
+
FastAPI's rules: a parameter named in the path is from the path, one marked Query(), Body(), Header()... from
|
|
147
|
+
there, a Request is the whole request, a plain type (int, str, list[str]) is a query parameter, and a model
|
|
148
|
+
(Order) is the body.
|
|
149
|
+
"""
|
|
150
|
+
found: dict[str, tuple[str, ...]] = {}
|
|
151
|
+
bodies = []
|
|
152
|
+
for param, default in params:
|
|
153
|
+
name = param.arg
|
|
154
|
+
kind, marked = _core(param.annotation)
|
|
155
|
+
call = _marker(default) or marked
|
|
156
|
+
marker = _named(call)
|
|
157
|
+
if marker in _NOT_INPUT or kind in _NOT_INPUT:
|
|
158
|
+
continue
|
|
159
|
+
if kind in _WHOLE:
|
|
160
|
+
found[name] = ()
|
|
161
|
+
elif kind == "UploadFile" or marker == "File":
|
|
162
|
+
found[name] = ("files", name)
|
|
163
|
+
elif marker == "Body" or (not marker and name not in names and kind and kind not in _SCALARS):
|
|
164
|
+
bodies.append(name)
|
|
165
|
+
elif marker:
|
|
166
|
+
alias = next(
|
|
167
|
+
(
|
|
168
|
+
kw.value.value
|
|
169
|
+
for kw in getattr(call, "keywords", [])
|
|
170
|
+
if kw.arg == "alias" and isinstance(kw.value, ast.Constant)
|
|
171
|
+
),
|
|
172
|
+
None,
|
|
173
|
+
)
|
|
174
|
+
found[name] = (_MARKERS[marker], str(alias or (name.replace("_", "-") if marker == "Header" else name)))
|
|
175
|
+
else:
|
|
176
|
+
found[name] = ("path" if name in names else "query", name)
|
|
177
|
+
for name in bodies: # one body model is the whole body; more are each a field of it
|
|
178
|
+
found[name] = ("body",) if len(bodies) == 1 else ("body", name)
|
|
179
|
+
return found
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class WebPlatform(Platform):
|
|
183
|
+
"""
|
|
184
|
+
A web app (FastAPI, Flask, Django): each route is an entry point, called with the request.
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
name = "web"
|
|
188
|
+
title = "Web app"
|
|
189
|
+
entry_kind = "a FastAPI, Flask or Django app: each route"
|
|
190
|
+
input_name = "the request"
|
|
191
|
+
input_word = "request"
|
|
192
|
+
draws_input = True
|
|
193
|
+
started = "When the server starts it (once in each worker process)"
|
|
194
|
+
web_apps = ("FastAPI", "Flask", "APIRouter", "Blueprint", "Quart")
|
|
195
|
+
input_objects = ("flask.request", "quart.request")
|
|
196
|
+
input_aliases = {"get_json": "json", "get_data": "data"}
|
|
197
|
+
|
|
198
|
+
def find_entries(self, module: str, tree: ast.Module) -> list[tuple[str, int, str]]:
|
|
199
|
+
if module.rsplit(".", 1)[-1] != "urls":
|
|
200
|
+
return []
|
|
201
|
+
for node in tree.body:
|
|
202
|
+
targets = (
|
|
203
|
+
node.targets
|
|
204
|
+
if isinstance(node, ast.Assign)
|
|
205
|
+
else [node.target]
|
|
206
|
+
if isinstance(node, ast.AnnAssign)
|
|
207
|
+
else []
|
|
208
|
+
)
|
|
209
|
+
if any(isinstance(t, ast.Name) and t.id == "urlpatterns" for t in targets):
|
|
210
|
+
return [("urlpatterns", node.lineno, "Django's URLs: each view")]
|
|
211
|
+
return []
|
|
212
|
+
|
|
213
|
+
def route_inputs(
|
|
214
|
+
self, function: ast.FunctionDef | ast.AsyncFunctionDef, path: str, imports: set[str], bound: bool = False
|
|
215
|
+
) -> dict[str, tuple[str, ...]]:
|
|
216
|
+
a = function.args
|
|
217
|
+
positional = a.posonlyargs + a.args
|
|
218
|
+
defaults: list[ast.expr | None] = [None] * (len(positional) - len(a.defaults)) + list(a.defaults)
|
|
219
|
+
params = list(zip(positional, defaults))[1 if bound else 0 :] + list(zip(a.kwonlyargs, a.kw_defaults))
|
|
220
|
+
names = {name for match in _PLACEHOLDER_RE.finditer(path) for name in match.groups() if name}
|
|
221
|
+
if "fastapi" in imports or ("{" in path and "(?P" not in path):
|
|
222
|
+
return _fastapi(names, params)
|
|
223
|
+
if imports & {"flask", "quart"}: # a view is called with its URL's values only; the rest is flask.request
|
|
224
|
+
return {p.arg: ("path", p.arg) for p, _ in params if p.arg in names}
|
|
225
|
+
# Django: the request first, then the URL's values
|
|
226
|
+
return {p.arg: () if i == 0 else ("path", p.arg) for i, (p, _) in enumerate(params)}
|
|
227
|
+
|
|
228
|
+
def entry_icon(self, cmap: CodeMap) -> str:
|
|
229
|
+
imported = {i.top for i in cmap.imports}
|
|
230
|
+
return next((logo for module, logo in FRAMEWORK_LOGOS.items() if module in imported), "")
|
|
231
|
+
|
|
232
|
+
def hang(self, cmap: CodeMap) -> str:
|
|
233
|
+
return (
|
|
234
|
+
"the request waits as long as the connection stays open, holding one of the server's workers, and "
|
|
235
|
+
"the client gives up first."
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
def lost_error(self) -> str:
|
|
239
|
+
return (
|
|
240
|
+
"answers with a success status, so the client thinks it worked and the server's error log never shows it."
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def findings(self, cmap: CodeMap) -> list[tuple[str, str]]:
|
|
244
|
+
found: list[tuple[str, str]] = []
|
|
245
|
+
if cmap.note:
|
|
246
|
+
return found
|
|
247
|
+
for function, lines in self._blocking(cmap).items():
|
|
248
|
+
name = _short_name(function)
|
|
249
|
+
found.append(
|
|
250
|
+
(
|
|
251
|
+
"warn",
|
|
252
|
+
(
|
|
253
|
+
f"{_listed([f'{what} at {_where(file, line)}' for what, file, line in lines], 2)} "
|
|
254
|
+
f"{'waits' if len(lines) == 1 else 'wait'} without await inside async def {name}(): while "
|
|
255
|
+
f"{'it waits' if len(lines) == 1 else 'they wait'}, the server's event loop can't run anything else, "
|
|
256
|
+
f"so every other request this worker is serving waits too. Make {name}() a plain def (FastAPI runs "
|
|
257
|
+
"those in a thread pool), or use an async client (httpx.AsyncClient, aioboto3, await asyncio.sleep)."
|
|
258
|
+
),
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
for spot in [s for s in cmap.spots if s.kind == "connect" and not s.ok and not s.startup]:
|
|
262
|
+
found.append(
|
|
263
|
+
(
|
|
264
|
+
"info",
|
|
265
|
+
(
|
|
266
|
+
f"It opens a new {spot.detail} connection on every request, at "
|
|
267
|
+
f"{_where(spot.file, spot.line, spot.function)}"
|
|
268
|
+
+ (" inside a loop" if spot.loop else "")
|
|
269
|
+
+ ": that's a round trip or more before any work, and many requests at once can use up the server's "
|
|
270
|
+
"connections. Make a pool once, at module level or when the app starts, and take a connection from it."
|
|
271
|
+
),
|
|
272
|
+
)
|
|
273
|
+
)
|
|
274
|
+
return found
|
|
275
|
+
|
|
276
|
+
def marks(self, cmap: CodeMap) -> dict[str, list[str]]:
|
|
277
|
+
return {
|
|
278
|
+
function: [f"{what} blocks its event loop (line {line})" for what, _, line in lines]
|
|
279
|
+
for function, lines in self._blocking(cmap).items()
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
@staticmethod
|
|
283
|
+
def _blocking(cmap: CodeMap) -> dict[str, list[tuple[str, str, int]]]:
|
|
284
|
+
"""
|
|
285
|
+
Calls that block inside an async def, by function: [('requests.get()', 'api.py', 9)]. HTTP through
|
|
286
|
+
requests or urllib, time.sleep() and boto3, which all wait for their answer without giving the loop back.
|
|
287
|
+
"""
|
|
288
|
+
async_functions = {f.name for f in cmap.functions if f.is_async}
|
|
289
|
+
lines: dict[str, list[tuple[str, str, int]]] = defaultdict(list)
|
|
290
|
+
spots: list[CodeSpot | AwsCall] = [
|
|
291
|
+
s
|
|
292
|
+
for s in cmap.spots
|
|
293
|
+
if (s.kind == "http" and s.code.startswith(_BLOCKING_HTTP))
|
|
294
|
+
or (s.kind == "sleep" and s.code.startswith("time.sleep"))
|
|
295
|
+
]
|
|
296
|
+
spots += [c for c in cmap.aws if not c.guessed]
|
|
297
|
+
for spot in spots:
|
|
298
|
+
if spot.function in async_functions and not spot.startup:
|
|
299
|
+
what = spot.code.split("(", 1)[0] + "()" if isinstance(spot, CodeSpot) else spot.label
|
|
300
|
+
lines[spot.function].append((what, spot.file, spot.line))
|
|
301
|
+
return lines
|