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,462 @@
1
+ """
2
+ What the code map finds wrong, and the one-paragraph summary of what the code does
3
+ and returns.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import ast
9
+ import re
10
+ import sys
11
+ from collections import defaultdict
12
+
13
+ from ..kit.fmt import _plural
14
+ from ..kit.text import _clip
15
+
16
+ from .terms import LATEST_RUNTIMES
17
+ from .terms import _BATCHES, _REMOVED_MODULES
18
+ from .syntax import _a, _const, _parse
19
+ from .aws_model import _service_name
20
+ from .model import AwsCall, CodeMap, CodeSpot, LibraryCall
21
+ from .mapping import _python_version
22
+ from ..libraries import get_library
23
+ from ..platforms import get_platform
24
+
25
+
26
+ _PIP_NAMES = {
27
+ "yaml": "pyyaml",
28
+ "PIL": "pillow",
29
+ "sklearn": "scikit-learn",
30
+ "cv2": "opencv-python-headless",
31
+ "dateutil": "python-dateutil",
32
+ "jwt": "PyJWT",
33
+ "dotenv": "python-dotenv",
34
+ "bs4": "beautifulsoup4",
35
+ "psycopg2": "psycopg2-binary",
36
+ "Crypto": "pycryptodome",
37
+ "jose": "python-jose",
38
+ "magic": "python-magic",
39
+ }
40
+
41
+ _REPLACEMENTS = {
42
+ "distutils": "setuptools, or shutil and sysconfig",
43
+ "imp": "importlib",
44
+ "asyncore": "asyncio",
45
+ "asynchat": "asyncio",
46
+ "smtpd": "aiosmtpd",
47
+ "cgi": "email.message and urllib.parse",
48
+ "pipes": "shlex.quote",
49
+ "crypt": "hashlib",
50
+ "telnetlib": "telnetlib3",
51
+ "imghdr": "Pillow or filetype",
52
+ "uu": "base64",
53
+ "xdrlib": "struct",
54
+ }
55
+
56
+
57
+ def _short_name(key: str) -> str:
58
+ """
59
+ One of its functions as people say it: 'app.main.handler' -> 'handler', 'app.store.Orders.save' ->
60
+ 'Orders.save', 'app.store.Orders.__init__' -> 'Orders' (what you call to run it).
61
+ """
62
+ parts = key.split(".")
63
+ if len(parts) >= 2 and parts[-2][:1].isupper():
64
+ return parts[-2] if parts[-1] == "__init__" else f"{parts[-2]}.{parts[-1]}"
65
+ return parts[-1]
66
+
67
+
68
+ def _where(file: str, line: int, function: str = "") -> str:
69
+ """
70
+ 'etl.py line 17', or 'etl.py line 17 in handler()'.
71
+ """
72
+ return f"{file} line {line}" + (f" in {_short_name(function)}()" if function else "")
73
+
74
+
75
+ def _listed(items: list[str], most: int = 3) -> str:
76
+ """
77
+ 'a', 'a and b', 'a, b and c', 'a, b, c and 2 more'.
78
+ """
79
+ if len(items) > most:
80
+ return ", ".join(items[:most]) + f" and {len(items) - most} more"
81
+ return " and ".join([", ".join(items[:-1]), items[-1]]) if len(items) > 1 else (items[0] if items else "")
82
+
83
+
84
+ def code_map_findings(cmap: CodeMap) -> list[tuple[str, str]]:
85
+ """
86
+ What's worth fixing in the code, from its CodeMap: an entry point that isn't there, imports that can't be found
87
+ or that newer Pythons removed, HTTP requests without a timeout, list calls that read only their first page,
88
+ errors it drops, secrets written in the code, and AWS calls in a loop that a batch call would do in fewer
89
+ requests. Then what its platform adds (a Lambda handler's own checks), worded for where it runs.
90
+ """
91
+ found: list[tuple[str, str]] = []
92
+ if cmap.note:
93
+ return found
94
+ platform = get_platform(cmap.platform)
95
+ version = platform.python_version(cmap)
96
+ entry_name = (cmap.handler or "").rsplit(".", 1)[-1]
97
+ spots: dict[str, list[CodeSpot]] = defaultdict(list)
98
+ for spot in cmap.spots:
99
+ spots[spot.kind].append(spot)
100
+
101
+ for spot in spots["syntax"]:
102
+ mine = sys.version_info[:2]
103
+ if version and version > mine:
104
+ found.append(
105
+ (
106
+ "info",
107
+ (
108
+ f"{spot.file} doesn't parse with this Python {mine[0]}.{mine[1]} ({spot.detail}): it may use syntax "
109
+ f"only Python {version[0]}.{version[1]} knows, so code-node couldn't follow it. Run code-node with "
110
+ f"Python {version[0]}.{version[1]} to read it."
111
+ ),
112
+ )
113
+ )
114
+ else:
115
+ found.append(
116
+ (
117
+ "warn",
118
+ (
119
+ f"{spot.file} doesn't parse ({spot.detail}): Python can't load it either, so code that imports it fails "
120
+ "with SyntaxError, and the map stops there. Fix that line."
121
+ ),
122
+ )
123
+ )
124
+ if cmap.entry is None and cmap.handler_file and not any(s.file == cmap.handler_file for s in spots["syntax"]):
125
+ others = f" ({_listed(cmap.handlers)} look like entry points)" if cmap.handlers else ""
126
+ found.append(("warn", platform.missing_entry(cmap, entry_name, others)))
127
+ found += platform.entry_findings(cmap)
128
+
129
+ gone = [i for i in cmap.imports if i.source == "missing" and not i.optional]
130
+ if gone:
131
+ tops = list(dict.fromkeys(i.top for i in gone))
132
+ startup = any(i.startup for i in gone)
133
+ pip = " ".join(_PIP_NAMES.get(top, top) for top in tops[:5])
134
+ install = platform.install(pip, "it" if len(tops) == 1 else "them")
135
+ found.append(
136
+ (
137
+ "warn",
138
+ (
139
+ f"It imports {_listed(tops)}, which {'isn' if len(tops) == 1 else 'aren'}'t in the code, Python's "
140
+ f"standard library or the Python environment ({gone[0].file} line {gone[0].line}): "
141
+ + ("every call fails" if startup else "calls that reach that line fail")
142
+ + f" with {platform.import_error(tops[0])}. {install[0].upper()}{install[1:]}."
143
+ ),
144
+ )
145
+ )
146
+ for i in [i for i in cmap.imports if i.source == "removed"]:
147
+ removed = _REMOVED_MODULES[i.top]
148
+ found.append(
149
+ (
150
+ "warn",
151
+ (
152
+ f"It imports {i.top} ({i.file} line {i.line}), which Python {removed[0]}.{removed[1]} removed: on the "
153
+ f"Python it runs on that fails with {platform.import_error(i.top)}. Use "
154
+ f"{_REPLACEMENTS.get(i.top, 'a replacement')} instead."
155
+ ),
156
+ )
157
+ )
158
+ family_latest = LATEST_RUNTIMES["python"]
159
+ latest = _python_version(family_latest)
160
+ later = [
161
+ i
162
+ for i in cmap.imports
163
+ if i.source == "stdlib" and i.top in _REMOVED_MODULES and latest and _REMOVED_MODULES[i.top] <= latest
164
+ ]
165
+ if later:
166
+ tops = list(dict.fromkeys(i.top for i in later))
167
+ removed = _REMOVED_MODULES[tops[0]]
168
+ swaps = _listed([f"{top} by {_REPLACEMENTS.get(top, 'a replacement')}" for top in tops], 2)
169
+ found.append(
170
+ (
171
+ "info",
172
+ (
173
+ f"It imports {_listed(tops)}, which Python {removed[0]}.{removed[1]} removed: on Python "
174
+ f"{removed[0]}.{removed[1]} or later it fails with {platform.import_error(tops[0])} until "
175
+ f"{'that is' if len(tops) == 1 else 'they are'} replaced ({swaps})."
176
+ ),
177
+ )
178
+ )
179
+
180
+ slow = [s for s in spots["http"] if not s.ok]
181
+ if slow:
182
+ found.append(
183
+ (
184
+ "warn",
185
+ (
186
+ f"{_plural(len(slow), 'HTTP request')} {'has' if len(slow) == 1 else 'have'} no timeout "
187
+ f"({_listed([f'{s.detail} at {_where(s.file, s.line)}' for s in slow], 2)}): if the server doesn't "
188
+ f"answer, {platform.hang(cmap)} Pass timeout=10 (or what the call needs)."
189
+ ),
190
+ )
191
+ )
192
+ for call in cmap.aws:
193
+ if call.pages == "one" and call.operation.startswith(("List", "Scan", "Query")) and not call.guessed:
194
+ first = {"ListObjectsV2": "1,000 keys", "ListObjects": "1,000 keys", "Scan": "1 MB", "Query": "1 MB"}.get(
195
+ call.operation, "the first page"
196
+ )
197
+ fix = (
198
+ "loop while the answer has LastEvaluatedKey, passing it back as ExclusiveStartKey"
199
+ if call.service == "dynamodb"
200
+ else f"use get_paginator('{_snake(call.operation)}').paginate(...) on its client"
201
+ )
202
+ found.append(
203
+ (
204
+ "warn",
205
+ (
206
+ f"{call.label} at {_where(call.file, call.line)} reads only its first page: past {first} the rest is "
207
+ f"left out without an error. To read every page, {fix}."
208
+ ),
209
+ )
210
+ )
211
+ for spot in spots["swallow"]:
212
+ found.append(
213
+ (
214
+ "warn",
215
+ (
216
+ f"Errors are dropped at {_where(spot.file, spot.line, spot.function)} ({spot.code}): a run that failed "
217
+ f"{platform.lost_error()} Log it (logger.exception(...)) and raise it again, or report the failed item."
218
+ ),
219
+ )
220
+ )
221
+ secrets = spots["secret"]
222
+ if secrets:
223
+ where = _listed([f"{s.detail} at {s.file} line {s.line}" for s in secrets], 2)
224
+ found.append(
225
+ (
226
+ "warn",
227
+ (
228
+ f"Secret-looking values are written in the code ({where}; "
229
+ "the values aren't shown here): anyone who can read the code can read them. "
230
+ "Keep them in Secrets Manager or Parameter Store, and read them when the code starts."
231
+ ),
232
+ )
233
+ )
234
+
235
+ batched = [c for c in cmap.aws if c.loop and (c.service, c.operation) in _BATCHES and not c.guessed]
236
+ for call in list({(c.service, c.operation): c for c in batched}.values()):
237
+ alternative, size = _BATCHES[(call.service, call.operation)]
238
+ found.append(
239
+ (
240
+ "info",
241
+ (
242
+ f"{call.label} runs once per item in {call.loop.rstrip(':')} ({_where(call.file, call.line)}): a request "
243
+ f"for each. {alternative} sends up to {size:,} at a time, so a run takes fewer round trips."
244
+ ),
245
+ )
246
+ )
247
+ found += _library_findings(cmap)
248
+ for spot in [s for s in spots["command"] if not s.ok]:
249
+ found.append(
250
+ (
251
+ "warn",
252
+ (
253
+ f"It runs a shell command with {spot.detail} in it, at {_where(spot.file, spot.line, spot.function)}: "
254
+ f"whoever sends {platform.input_name} can make it run commands of their own (shell injection). Pass the "
255
+ "command as a list of arguments, without shell=True, and check the value before it goes in."
256
+ ),
257
+ )
258
+ )
259
+ return found + platform.findings(cmap)
260
+
261
+
262
+ def _library_findings(cmap: CodeMap) -> list[tuple[str, str]]:
263
+ """
264
+ What's worth fixing in the calls through libraries plugins know (Redis, SQL, MongoDB, OpenAI...): the calls a
265
+ plugin flagged, calls made once per item that a batch call would do together, and clients that wait too long
266
+ when their service doesn't answer. Then each plugin's own.
267
+ """
268
+ platform = get_platform(cmap.platform)
269
+ found: list[tuple[str, str]] = []
270
+ for call in list({(c.service, c.line, c.why): c for c in cmap.library_calls if c.why}.values()):
271
+ where = _where(call.file, call.line, "" if call.startup else call.function)
272
+ found.append(("warn", f"{call.label} at {where} {call.why}"))
273
+ looped = [c for c in cmap.library_calls if c.loop and c.batch]
274
+ for call in list({(c.service, c.operation, c.target): c for c in looped}.values()):
275
+ found.append(
276
+ (
277
+ "info",
278
+ (
279
+ f"{call.label} runs once per item in {call.loop.rstrip(':')} ({_where(call.file, call.line)}), a round "
280
+ f"trip for each, where {call.batch} would need fewer."
281
+ ),
282
+ )
283
+ )
284
+ for spot in [s for s in cmap.spots if s.kind == "connect"]:
285
+ if spot.waits:
286
+ found.append(
287
+ (
288
+ "info" if not platform.name else "warn",
289
+ (
290
+ f"The {spot.detail} client made at {_where(spot.file, spot.line, '' if spot.startup else spot.function)}"
291
+ f" has no timeout, so a call through it waits {spot.waits}: if {spot.detail} doesn't answer, "
292
+ f"{platform.hang(cmap)} Give it one where it's made" + (f": {spot.fix}." if spot.fix else ".")
293
+ ),
294
+ )
295
+ )
296
+ if spot.loop and not spot.ok:
297
+ found.append(
298
+ (
299
+ "info",
300
+ (
301
+ f"It makes a new {spot.detail} client for each item in {spot.loop.rstrip(':')}, at "
302
+ f"{_where(spot.file, spot.line, spot.function)}: each one opens its own connection. Make it once, "
303
+ "before the loop."
304
+ ),
305
+ )
306
+ )
307
+ by_library: dict[str, list[LibraryCall]] = defaultdict(list)
308
+ for call in cmap.library_calls:
309
+ by_library[call.library].append(call)
310
+ for name, calls in by_library.items():
311
+ library = get_library(name)
312
+ if library is not None:
313
+ found += library.findings(cmap, calls)
314
+ return found
315
+
316
+
317
+ def _snake(operation: str) -> str:
318
+ """
319
+ 'ListObjectsV2' -> 'list_objects_v2', boto3's method name.
320
+ """
321
+ try:
322
+ from botocore import xform_name
323
+ except ImportError:
324
+ return re.sub(r"(?<=[a-z0-9])(?=[A-Z])", "_", operation).lower()
325
+ return xform_name(operation)
326
+
327
+
328
+ def _code_summary(cmap: CodeMap) -> list[str]:
329
+ """
330
+ How the code works in a few sentences: what it does when it's imported (when Lambda starts it, for a handler),
331
+ then what each call reads from its input, which AWS services it reaches and how, its own functions it calls, and
332
+ what it returns.
333
+ """
334
+ platform = get_platform(cmap.platform)
335
+ out = []
336
+ file = cmap.handler_file or ""
337
+ early = []
338
+ names = list(
339
+ dict.fromkeys(
340
+ _service_name(s.detail) for s in cmap.spots if s.kind == "client" and s.startup and s.detail != "?"
341
+ )
342
+ )
343
+ if names:
344
+ early.append(
345
+ f"makes {_a(names[0])} {names[0]} client" if len(names) == 1 else f"makes {_listed(names)} clients"
346
+ )
347
+ env = list(dict.fromkeys(e.name for e in cmap.env if e.startup))
348
+ if env:
349
+ early.append(f"reads {_listed(env)} from its environment")
350
+ early += _aws_phrase([c for c in cmap.aws if c.startup])
351
+ if early:
352
+ out.append(f"{platform.started}, the code {_listed(early, 4)}.")
353
+ if cmap.entry is None:
354
+ return out
355
+ entry = cmap.entry.rsplit(".", 1)[-1]
356
+ if any(f.name == cmap.entry for f in cmap.functions): # not an app object: its routes are said below
357
+ out.append(f"Each call runs {entry}() in {file}.")
358
+ labels = [f.label for f in cmap.event]
359
+ leaves = [
360
+ label
361
+ for label in labels
362
+ if not any(other.startswith((label + "[", label + ".", label + " (")) for other in labels)
363
+ ]
364
+ if leaves:
365
+ kind = platform.input_kind(cmap.event)
366
+ out.append(
367
+ f"It reads {_listed(leaves, 4)} from {platform.input_name}"
368
+ + (f", which looks like {kind}" if kind else "")
369
+ + "."
370
+ )
371
+ phrase = _aws_phrase([c for c in cmap.aws if not c.startup])
372
+ phrase += _library_phrase([c for c in cmap.library_calls if not c.startup])
373
+ http = [s for s in cmap.spots if s.kind == "http" and not s.startup]
374
+ more = list(phrase)
375
+ if http:
376
+ more.append(f"sends {_plural(len(http), 'HTTP request')}")
377
+ commands = [path for method, path, _ in cmap.routes if method == "CLI" and not path.startswith("(")]
378
+ web = [f"{method} {path}" for method, path, _ in cmap.routes if method != "CLI"]
379
+ if commands:
380
+ more.append(f"has {_plural(len(commands), 'command')} ({_listed(commands)})")
381
+ if web:
382
+ more.append(f"answers {_plural(len(web), 'web route')} ({_listed(web)})")
383
+ if more:
384
+ out.append(f"It {_listed(more, 6) if len(more) < 3 else '; '.join(more[:-1]) + '; and ' + more[-1]}.")
385
+ called = [f for f in cmap.functions if f.depth == 1]
386
+ returns = list(dict.fromkeys(code for code, _, _ in cmap.returns))
387
+ last = []
388
+ if called:
389
+ last.append(
390
+ "calls "
391
+ + _listed([f"{_short_name(f.name)}()" + (f" from {f.file}" if f.file != file else "") for f in called], 4)
392
+ )
393
+ if returns:
394
+ last.append(
395
+ f"returns {_returned(returns[0])}"
396
+ + (f" (or {_plural(len(returns) - 1, 'other value')})" if len(returns) > 1 else "")
397
+ )
398
+ if last:
399
+ out.append(f"It {' and '.join(last)}.")
400
+ if not (leaves or more or last):
401
+ out.append(
402
+ f"It does nothing this can follow: no AWS calls, own functions, HTTP requests or fields of "
403
+ f"{platform.input_name}."
404
+ )
405
+ return out
406
+
407
+
408
+ def _returned(code: str) -> str:
409
+ """
410
+ What a return statement gives back, said shortly: "{'statusCode': 200, 'body': ...}" -> 'a dict with
411
+ statusCode and body'; short code as it is.
412
+ """
413
+ try:
414
+ node = _parse(code, mode="eval").body
415
+ except SyntaxError:
416
+ return _clip(code, 60)
417
+ if isinstance(node, ast.Dict) and node.keys and all(isinstance(_const(k), str) for k in node.keys):
418
+ keys = [str(_const(k)) for k in node.keys]
419
+ return f"a dict with {_listed(keys, 4)}" if len(code) > 50 else code
420
+ return _clip(code, 60)
421
+
422
+
423
+ def _library_phrase(calls: list[LibraryCall]) -> list[str]:
424
+ """
425
+ ['reads from Redis (GET) and PostgreSQL (SELECT orders)', 'runs OpenAI (chat.completions.create gpt-4o)'].
426
+ """
427
+ grouped: dict[str, dict[str, list[LibraryCall]]] = {}
428
+ for call in calls:
429
+ grouped.setdefault(call.access, {}).setdefault(call.service, []).append(call)
430
+ verbs = {"reads": "reads from", "writes": "writes to", "sends": "sends to", "runs": "calls"}
431
+ parts = []
432
+ for access, services in grouped.items():
433
+ each = []
434
+ for service, group in services.items():
435
+ rows = list(dict.fromkeys(c.row for c in group))
436
+ each.append(f"{service} ({', '.join(rows[:3])}{', …' if len(rows) > 3 else ''})")
437
+ parts.append(f"{verbs.get(access, 'calls')} {_listed(each, 4)}")
438
+ return parts
439
+
440
+
441
+ def _aws_phrase(calls: list[AwsCall]) -> list[str]:
442
+ """
443
+ ['reads from S3 (GetObject) and SQS (ListQueues)', 'writes to DynamoDB (PutItem)'].
444
+ """
445
+ grouped: dict[str, dict[str, list[AwsCall]]] = {}
446
+ for call in calls:
447
+ grouped.setdefault(call.access, {}).setdefault(call.service, []).append(call)
448
+ verbs = {
449
+ "reads": "reads from",
450
+ "writes": "writes to",
451
+ "sends": "sends to",
452
+ "runs": "calls",
453
+ "signs": "signs links for",
454
+ }
455
+ parts = []
456
+ for access, services in grouped.items():
457
+ each = []
458
+ for service, group in services.items():
459
+ ops = list(dict.fromkeys(c.operation for c in group))
460
+ each.append(f"{_service_name(service)} ({', '.join(ops[:3])}{', …' if len(ops) > 3 else ''})")
461
+ parts.append(f"{verbs[access]} {_listed(each, 4)}")
462
+ return parts