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.
Files changed (178) hide show
  1. code_node/__init__.py +40 -0
  2. code_node/__main__.py +7 -0
  3. code_node/analysis/__init__.py +3 -0
  4. code_node/analysis/aws_model.py +197 -0
  5. code_node/analysis/findings.py +462 -0
  6. code_node/analysis/graph.py +780 -0
  7. code_node/analysis/mapping.py +114 -0
  8. code_node/analysis/model.py +289 -0
  9. code_node/analysis/reader.py +1926 -0
  10. code_node/analysis/syntax.py +220 -0
  11. code_node/analysis/terms.py +505 -0
  12. code_node/cli.py +316 -0
  13. code_node/deploys/__init__.py +321 -0
  14. code_node/deploys/sam.py +371 -0
  15. code_node/deploys/serverless.py +117 -0
  16. code_node/kit/__init__.py +4 -0
  17. code_node/kit/blocks.py +184 -0
  18. code_node/kit/fmt.py +389 -0
  19. code_node/kit/highlight.py +131 -0
  20. code_node/kit/render.py +1066 -0
  21. code_node/kit/text.py +81 -0
  22. code_node/libraries/__init__.py +333 -0
  23. code_node/libraries/llm.py +124 -0
  24. code_node/libraries/mongo.py +169 -0
  25. code_node/libraries/redis.py +253 -0
  26. code_node/libraries/sql.py +232 -0
  27. code_node/pages.py +92 -0
  28. code_node/platforms/__init__.py +203 -0
  29. code_node/platforms/aws_lambda.py +290 -0
  30. code_node/platforms/web.py +301 -0
  31. code_node/project.py +402 -0
  32. code_node/report/__init__.py +3 -0
  33. code_node/report/brands.py +244 -0
  34. code_node/report/build.py +119 -0
  35. code_node/report/editor.py +40 -0
  36. code_node/report/graph.py +1411 -0
  37. code_node/report/logos/README.md +8 -0
  38. code_node/report/logos/anthropic-icon.svg +1 -0
  39. code_node/report/logos/aws-amplify.svg +1 -0
  40. code_node/report/logos/aws-api-gateway.svg +1 -0
  41. code_node/report/logos/aws-appflow.svg +1 -0
  42. code_node/report/logos/aws-appsync.svg +1 -0
  43. code_node/report/logos/aws-athena.svg +1 -0
  44. code_node/report/logos/aws-backup.svg +1 -0
  45. code_node/report/logos/aws-batch.svg +1 -0
  46. code_node/report/logos/aws-certificate-manager.svg +1 -0
  47. code_node/report/logos/aws-cloudformation.svg +1 -0
  48. code_node/report/logos/aws-cloudfront.svg +1 -0
  49. code_node/report/logos/aws-cloudtrail.svg +1 -0
  50. code_node/report/logos/aws-cloudwatch.svg +1 -0
  51. code_node/report/logos/aws-codebuild.svg +1 -0
  52. code_node/report/logos/aws-codecommit.svg +1 -0
  53. code_node/report/logos/aws-codedeploy.svg +1 -0
  54. code_node/report/logos/aws-codepipeline.svg +1 -0
  55. code_node/report/logos/aws-cognito.svg +1 -0
  56. code_node/report/logos/aws-config.svg +1 -0
  57. code_node/report/logos/aws-documentdb.svg +1 -0
  58. code_node/report/logos/aws-dynamodb.svg +1 -0
  59. code_node/report/logos/aws-ec2.svg +1 -0
  60. code_node/report/logos/aws-ecs.svg +1 -0
  61. code_node/report/logos/aws-eks.svg +1 -0
  62. code_node/report/logos/aws-elasticache.svg +1 -0
  63. code_node/report/logos/aws-elb.svg +1 -0
  64. code_node/report/logos/aws-eventbridge.svg +1 -0
  65. code_node/report/logos/aws-glacier.svg +1 -0
  66. code_node/report/logos/aws-glue.svg +1 -0
  67. code_node/report/logos/aws-iam.svg +1 -0
  68. code_node/report/logos/aws-keyspaces.svg +1 -0
  69. code_node/report/logos/aws-kinesis.svg +1 -0
  70. code_node/report/logos/aws-kms.svg +1 -0
  71. code_node/report/logos/aws-lake-formation.svg +1 -0
  72. code_node/report/logos/aws-lambda.svg +1 -0
  73. code_node/report/logos/aws-lightsail.svg +1 -0
  74. code_node/report/logos/aws-mq.svg +1 -0
  75. code_node/report/logos/aws-msk.svg +1 -0
  76. code_node/report/logos/aws-neptune.svg +1 -0
  77. code_node/report/logos/aws-open-search.svg +1 -0
  78. code_node/report/logos/aws-quicksight.svg +1 -0
  79. code_node/report/logos/aws-rds.svg +1 -0
  80. code_node/report/logos/aws-redshift.svg +1 -0
  81. code_node/report/logos/aws-route53.svg +1 -0
  82. code_node/report/logos/aws-s3.svg +1 -0
  83. code_node/report/logos/aws-secrets-manager.svg +1 -0
  84. code_node/report/logos/aws-ses.svg +1 -0
  85. code_node/report/logos/aws-shield.svg +1 -0
  86. code_node/report/logos/aws-sns.svg +1 -0
  87. code_node/report/logos/aws-sqs.svg +1 -0
  88. code_node/report/logos/aws-step-functions.svg +1 -0
  89. code_node/report/logos/aws-systems-manager.svg +1 -0
  90. code_node/report/logos/aws-timestream.svg +1 -0
  91. code_node/report/logos/aws-waf.svg +1 -0
  92. code_node/report/logos/aws-xray.svg +1 -0
  93. code_node/report/logos/aws.svg +1 -0
  94. code_node/report/logos/azure-icon.svg +1 -0
  95. code_node/report/logos/django-icon.svg +1 -0
  96. code_node/report/logos/elasticsearch.svg +1 -0
  97. code_node/report/logos/fastapi-icon.svg +1 -0
  98. code_node/report/logos/firebase-icon.svg +1 -0
  99. code_node/report/logos/flask.svg +1 -0
  100. code_node/report/logos/google-cloud.svg +1 -0
  101. code_node/report/logos/kafka-icon.svg +1 -0
  102. code_node/report/logos/mariadb-icon.svg +1 -0
  103. code_node/report/logos/mongodb-icon.svg +1 -0
  104. code_node/report/logos/mysql-icon.svg +1 -0
  105. code_node/report/logos/openai-icon.svg +1 -0
  106. code_node/report/logos/postgresql.svg +1 -0
  107. code_node/report/logos/python.svg +1 -0
  108. code_node/report/logos/rabbitmq-icon.svg +1 -0
  109. code_node/report/logos/redis.svg +1 -0
  110. code_node/report/logos/serverless.svg +1 -0
  111. code_node/report/logos/slack-icon.svg +1 -0
  112. code_node/report/logos/small/aws-api-gateway.svg +1 -0
  113. code_node/report/logos/small/aws-appflow.svg +1 -0
  114. code_node/report/logos/small/aws-appsync.svg +1 -0
  115. code_node/report/logos/small/aws-athena.svg +1 -0
  116. code_node/report/logos/small/aws-backup.svg +1 -0
  117. code_node/report/logos/small/aws-batch.svg +1 -0
  118. code_node/report/logos/small/aws-certificate-manager.svg +1 -0
  119. code_node/report/logos/small/aws-cloudformation.svg +1 -0
  120. code_node/report/logos/small/aws-cloudfront.svg +1 -0
  121. code_node/report/logos/small/aws-cloudtrail.svg +1 -0
  122. code_node/report/logos/small/aws-cloudwatch.svg +1 -0
  123. code_node/report/logos/small/aws-codebuild.svg +1 -0
  124. code_node/report/logos/small/aws-codecommit.svg +1 -0
  125. code_node/report/logos/small/aws-codedeploy.svg +1 -0
  126. code_node/report/logos/small/aws-codepipeline.svg +1 -0
  127. code_node/report/logos/small/aws-cognito.svg +1 -0
  128. code_node/report/logos/small/aws-config.svg +1 -0
  129. code_node/report/logos/small/aws-documentdb.svg +1 -0
  130. code_node/report/logos/small/aws-dynamodb.svg +1 -0
  131. code_node/report/logos/small/aws-ec2.svg +1 -0
  132. code_node/report/logos/small/aws-ecs.svg +1 -0
  133. code_node/report/logos/small/aws-eks.svg +1 -0
  134. code_node/report/logos/small/aws-elasticache.svg +1 -0
  135. code_node/report/logos/small/aws-elb.svg +1 -0
  136. code_node/report/logos/small/aws-eventbridge.svg +1 -0
  137. code_node/report/logos/small/aws-glacier.svg +1 -0
  138. code_node/report/logos/small/aws-glue.svg +1 -0
  139. code_node/report/logos/small/aws-iam.svg +1 -0
  140. code_node/report/logos/small/aws-keyspaces.svg +1 -0
  141. code_node/report/logos/small/aws-kinesis.svg +1 -0
  142. code_node/report/logos/small/aws-kms.svg +1 -0
  143. code_node/report/logos/small/aws-lake-formation.svg +1 -0
  144. code_node/report/logos/small/aws-lambda.svg +1 -0
  145. code_node/report/logos/small/aws-mq.svg +1 -0
  146. code_node/report/logos/small/aws-msk.svg +1 -0
  147. code_node/report/logos/small/aws-neptune.svg +1 -0
  148. code_node/report/logos/small/aws-open-search.svg +1 -0
  149. code_node/report/logos/small/aws-quicksight.svg +1 -0
  150. code_node/report/logos/small/aws-rds.svg +1 -0
  151. code_node/report/logos/small/aws-redshift.svg +1 -0
  152. code_node/report/logos/small/aws-route53.svg +1 -0
  153. code_node/report/logos/small/aws-s3.svg +1 -0
  154. code_node/report/logos/small/aws-secrets-manager.svg +1 -0
  155. code_node/report/logos/small/aws-ses.svg +1 -0
  156. code_node/report/logos/small/aws-shield.svg +1 -0
  157. code_node/report/logos/small/aws-sns.svg +1 -0
  158. code_node/report/logos/small/aws-sqs.svg +1 -0
  159. code_node/report/logos/small/aws-step-functions.svg +1 -0
  160. code_node/report/logos/small/aws-systems-manager.svg +1 -0
  161. code_node/report/logos/small/aws-timestream.svg +1 -0
  162. code_node/report/logos/small/aws-waf.svg +1 -0
  163. code_node/report/logos/small/aws-xray.svg +1 -0
  164. code_node/report/logos/snowflake-icon.svg +1 -0
  165. code_node/report/logos/sqlite.svg +1 -0
  166. code_node/report/logos/stripe.svg +1 -0
  167. code_node/report/logos/supabase-icon.svg +1 -0
  168. code_node/report/logos/twilio-icon.svg +1 -0
  169. code_node/report/outline.py +219 -0
  170. code_node/report/style.py +358 -0
  171. code_node/report/tabs.py +73 -0
  172. code_node/report/walk.py +624 -0
  173. code_node/serve.py +277 -0
  174. code_node_graph-0.1.0.dist-info/METADATA +165 -0
  175. code_node_graph-0.1.0.dist-info/RECORD +178 -0
  176. code_node_graph-0.1.0.dist-info/WHEEL +4 -0
  177. code_node_graph-0.1.0.dist-info/entry_points.txt +2 -0
  178. 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