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,114 @@
|
|
|
1
|
+
"""
|
|
2
|
+
map_code(): a CodeMap of how code works from one entry point, read with ast from its files.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from typing import Iterable
|
|
9
|
+
|
|
10
|
+
from ..kit.text import _clip
|
|
11
|
+
|
|
12
|
+
from .terms import _LIBRARIES, _RUNTIME_MODULES
|
|
13
|
+
from .model import CodeMap, Deployment
|
|
14
|
+
from .reader import _CodeReader
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _python_version(runtime: str | None) -> tuple[int, int] | None:
|
|
18
|
+
"""
|
|
19
|
+
'python3.12' -> (3, 12).
|
|
20
|
+
"""
|
|
21
|
+
match = re.fullmatch(r"python(\d+)\.(\d+)", runtime or "")
|
|
22
|
+
return (int(match.group(1)), int(match.group(2))) if match else None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def entry_file(entry: str, names: Iterable[str]) -> tuple[str, str] | None:
|
|
26
|
+
"""
|
|
27
|
+
The file an entry point is in, and the folder its modules are named from: 'pkg.cli.main' -> ('src/pkg/cli.py',
|
|
28
|
+
'src/'); 'pkg.cli.App.run' (a method) is found in 'pkg/cli.py' too. None when no file matches.
|
|
29
|
+
"""
|
|
30
|
+
parts = entry.split(".")[:-1]
|
|
31
|
+
names = list(names)
|
|
32
|
+
for cut in (len(parts), len(parts) - 1):
|
|
33
|
+
if cut < 1:
|
|
34
|
+
continue
|
|
35
|
+
stem = "/".join(parts[:cut])
|
|
36
|
+
for suffix in (f"{stem}.py", f"{stem}/__init__.py"):
|
|
37
|
+
for name in sorted(names, key=len):
|
|
38
|
+
if name == suffix or name.endswith("/" + suffix):
|
|
39
|
+
return name, name[: -len(suffix)]
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _given_file(entry: str, file: str) -> tuple[str, str] | None:
|
|
44
|
+
"""
|
|
45
|
+
The entry point's file as given, and the folder its modules are named from: ('pkg.cli.main', 'src/pkg/cli.py')
|
|
46
|
+
-> ('src/pkg/cli.py', 'src/').
|
|
47
|
+
"""
|
|
48
|
+
for cut in (1, 2):
|
|
49
|
+
module = entry.rsplit(".", cut)[0]
|
|
50
|
+
for stem in (module.replace(".", "/") + ".py", module.replace(".", "/") + "/__init__.py"):
|
|
51
|
+
if file == stem or file.endswith("/" + stem):
|
|
52
|
+
return file, file[: -len(stem)]
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def map_code(
|
|
57
|
+
files: dict[str, str | bytes],
|
|
58
|
+
entry: str,
|
|
59
|
+
*,
|
|
60
|
+
names: Iterable[str] | None = None,
|
|
61
|
+
libraries: Iterable[str] = (),
|
|
62
|
+
file: str | None = None,
|
|
63
|
+
python: str | None = None,
|
|
64
|
+
max_depth: int | None = None,
|
|
65
|
+
platform: str = "",
|
|
66
|
+
deployment: Deployment | None = None,
|
|
67
|
+
) -> CodeMap:
|
|
68
|
+
"""
|
|
69
|
+
How Python code works, read with Python's ast module and never run: files is {path: source}, entry the function
|
|
70
|
+
to start from as module.function ('pkg.cli.main') or module.Class.method. Starting there, it follows the code into
|
|
71
|
+
its own functions and modules, keeping track of what each name holds (a boto3 client, part of its input, an
|
|
72
|
+
environment variable...), and returns a CodeMap: what runs at import and on each call, step by step, its AWS calls
|
|
73
|
+
with the IAM permission each needs, the environment variables it reads, the libraries it imports. file is the
|
|
74
|
+
entry point's file when it's known (else the first file whose path matches its module); platform names where it
|
|
75
|
+
runs (a code_node.platforms name: 'lambda' adds the checks only a Lambda handler needs), '' for plain Python.
|
|
76
|
+
deployment is what a deploy file says about where it runs (code_node.deploys: a function in template.yaml): its
|
|
77
|
+
runtime, environment variables and role are checked against the code, and it sets the platform when none is given.
|
|
78
|
+
|
|
79
|
+
map_code({"app.py": open("app.py").read()}, "app.handler")
|
|
80
|
+
"""
|
|
81
|
+
texts = {
|
|
82
|
+
path: text.decode("utf-8", "replace") if isinstance(text, bytes) else str(text) for path, text in files.items()
|
|
83
|
+
}
|
|
84
|
+
names = list(names) if names is not None else list(texts)
|
|
85
|
+
if deployment is not None:
|
|
86
|
+
platform = platform or deployment.platform
|
|
87
|
+
python = python or deployment.runtime
|
|
88
|
+
cmap = CodeMap(deployment, entry, platform=platform)
|
|
89
|
+
if not entry or "." not in entry:
|
|
90
|
+
cmap.note = f"The entry point ({entry or 'none'}) doesn't name a module and a function, like app.main."
|
|
91
|
+
return cmap
|
|
92
|
+
found = _given_file(entry, file) if file else entry_file(entry, names)
|
|
93
|
+
if found is None:
|
|
94
|
+
module = entry.rsplit(".", 1)[0].replace(".", "/")
|
|
95
|
+
cmap.note = f"No file matches the entry point {entry} (looked for {module}.py)."
|
|
96
|
+
return cmap
|
|
97
|
+
cmap.handler_file, root = found
|
|
98
|
+
libs = set(libraries) | _RUNTIME_MODULES | _LIBRARIES
|
|
99
|
+
libs.discard(entry.split(".")[0]) # its own top package is never a library
|
|
100
|
+
reader = _CodeReader(
|
|
101
|
+
texts, names, root, cmap, _python_version(python), libs, bool(deployment is not None and deployment.layers)
|
|
102
|
+
)
|
|
103
|
+
if max_depth is not None:
|
|
104
|
+
reader.max_depth = max_depth
|
|
105
|
+
try:
|
|
106
|
+
reader.read(entry)
|
|
107
|
+
except RecursionError:
|
|
108
|
+
cmap.limited = "stopped where the code nests too deep to follow"
|
|
109
|
+
except Exception as exc: # noqa: BLE001 code it can't follow: what it found so far, said to be partial
|
|
110
|
+
cmap.limited = f"stopped partway, at code it couldn't follow ({type(exc).__name__}: {_clip(str(exc), 80)})"
|
|
111
|
+
if cmap.limited.startswith("stopped"):
|
|
112
|
+
cmap.functions, cmap.event = list(reader.reached.values()), list(reader.event_reads.values())
|
|
113
|
+
cmap.routes = [(method, path, key) for method, path, key, _ in reader.routes]
|
|
114
|
+
return cmap
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The dataclasses a CodeMap is made of: the steps of the outline, the AWS calls, the
|
|
3
|
+
environment and event it reads, its functions and imports, and the lines findings
|
|
4
|
+
point at.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any, Iterable
|
|
11
|
+
|
|
12
|
+
from .aws_model import _event_label, _service_name
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class CodeStep:
|
|
17
|
+
"""
|
|
18
|
+
A line of how_it_works()'s outline: a statement of the function's own code that does something worth knowing
|
|
19
|
+
(calls AWS, the network or one of its own functions, reads the event or an environment variable, logs, returns or
|
|
20
|
+
raises), or a loop, branch, `with` or `try` around such lines, with what it does in plain English.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
file: str
|
|
24
|
+
line: int
|
|
25
|
+
code: str # the statement on one line: "for record in event['Records']:"
|
|
26
|
+
kind: str = "do" # 'do', 'loop', 'if', 'else', 'with', 'try', 'except', 'return', 'raise', 'def', 'module'
|
|
27
|
+
notes: list[tuple[str, str]] = field(default_factory=list) # (kind, what): ('aws-reads', 'S3 GetObject'), ...
|
|
28
|
+
steps: list[CodeStep] = field(default_factory=list) # the lines under it: a loop's body, a function's steps
|
|
29
|
+
calls: list[CodeStep] = field(default_factory=list) # 'def' steps: its own functions this line calls
|
|
30
|
+
name: str = "" # a 'def' step: the function ('db.connect'); a 'module' step: the module ('db')
|
|
31
|
+
seen: bool = False # a 'def' step whose steps are shown where it was first called, not again here
|
|
32
|
+
holds: list[tuple[str, str]] = field(default_factory=list) # the names it sets and what they hold, in words:
|
|
33
|
+
# [('bucket', 'event Records[].s3.bucket.name'), ('self.table', 'a DynamoDB Table')]
|
|
34
|
+
given: list[tuple[str, str]] = field(default_factory=list) # what goes into its AWS calls, by call and argument:
|
|
35
|
+
# [('S3 PutObject Bucket', 'env ARCHIVE_BUCKET')]
|
|
36
|
+
when: list[str] = field(default_factory=list) # a 'def' step: the tests of the ifs around the line calling it,
|
|
37
|
+
# outermost first: ['args.command == "init"'], ['check(order)'] after `if not check(order): continue`
|
|
38
|
+
|
|
39
|
+
def walk(self) -> Iterable[CodeStep]:
|
|
40
|
+
"""
|
|
41
|
+
This step and every step under it, in the order they're shown.
|
|
42
|
+
"""
|
|
43
|
+
yield self
|
|
44
|
+
for step in [*self.calls, *self.steps]:
|
|
45
|
+
yield from step.walk()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class AwsCall:
|
|
50
|
+
"""
|
|
51
|
+
An AWS API call the code makes, found by following its boto3 clients and resources.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
service: str # boto3's name: 's3', 'dynamodb'
|
|
55
|
+
operation: str # 'GetObject'
|
|
56
|
+
access: str # 'reads', 'writes', 'sends' (a message or event), 'runs' (invokes or starts something) or 'signs'
|
|
57
|
+
# (a presigned link: no request)
|
|
58
|
+
permission: str # the IAM action its role needs: 's3:GetObject' ('' when it needs none)
|
|
59
|
+
file: str
|
|
60
|
+
line: int
|
|
61
|
+
function: str # where: 'etl.handler', or the module ('etl') for code that runs when it's imported
|
|
62
|
+
code: str # the call: "s3.get_object(Bucket=body['bucket'], Key=body['key'])"
|
|
63
|
+
loop: str = "" # the loop it's in: 'for row in rows:'
|
|
64
|
+
pages: str = "" # a list call: 'all' (a paginator, or it follows the next-page token), 'one' (its first page only)
|
|
65
|
+
guessed: bool = False # from the method's name alone: its client couldn't be followed
|
|
66
|
+
startup: bool = False # made when the code is imported (on Lambda, once per cold start), not on each call
|
|
67
|
+
args: list[tuple[str, str]] = field(default_factory=list) # where its arguments come from, the ones that can be
|
|
68
|
+
# told: [('Bucket', 'env ARCHIVE_BUCKET'), ('Key', "'orders.json'")]
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def label(self) -> str:
|
|
72
|
+
"""
|
|
73
|
+
'S3 GetObject'.
|
|
74
|
+
"""
|
|
75
|
+
return f"{_service_name(self.service)} {self.operation}"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class LibraryCall:
|
|
80
|
+
"""
|
|
81
|
+
A call through a library a library plugin knows (code_node.libraries): a Redis GET, a SQL INSERT, an OpenAI
|
|
82
|
+
chat completion. Drawn as a box for its service, like an AWS service's.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
library: str # the plugin's name: 'redis', 'sql'
|
|
86
|
+
service: str # what it reaches, as people name it: 'Redis', 'PostgreSQL', 'OpenAI'
|
|
87
|
+
operation: str # 'GET', 'INSERT', 'chat.completions.create'
|
|
88
|
+
access: str # 'reads', 'writes', 'sends' or 'runs', as for AWS calls
|
|
89
|
+
file: str
|
|
90
|
+
line: int
|
|
91
|
+
function: str # where: 'cart.app.handler', or the module for code that runs when it's imported
|
|
92
|
+
code: str # the call: "r.get(f'cart:{user}')"
|
|
93
|
+
target: str = "" # what on: a key, a table, a collection, a model: 'cart:{user}', 'orders', 'gpt-4o'
|
|
94
|
+
icon: str = "" # its box's icon: 'database', 'queue', 'key', 'send', 'spark', 'bucket', 'cloud'
|
|
95
|
+
loop: str = "" # the loop it's in: 'for item in items:'
|
|
96
|
+
startup: bool = False # made when the code is imported
|
|
97
|
+
flag: str = "" # a warning about this call, in a few words: 'SQL built from the event'
|
|
98
|
+
why: str = "" # ...and as a sentence, for the findings
|
|
99
|
+
batch: str = "" # in a loop: the call that does many at once ('executemany()', 'a pipeline()')
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def label(self) -> str:
|
|
103
|
+
"""
|
|
104
|
+
'Redis GET', 'PostgreSQL INSERT'.
|
|
105
|
+
"""
|
|
106
|
+
return f"{self.service} {self.operation}"
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def row(self) -> str:
|
|
110
|
+
"""
|
|
111
|
+
How its box lists it: 'GET cart:{user}', 'INSERT orders'.
|
|
112
|
+
"""
|
|
113
|
+
return f"{self.operation} {self.target}".strip()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class Deployment:
|
|
118
|
+
"""
|
|
119
|
+
What a deploy file says about where an entry point runs, read by a deploy plugin (code_node.deploys): a Lambda
|
|
120
|
+
function in a SAM template or serverless.yml, with its settings and what its role may call. Its platform's
|
|
121
|
+
checks run against it: variables that aren't set, AWS calls its role doesn't allow, the Python it runs on.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
name: str # the function's name in the file: 'CartFunction', 'checkout'
|
|
125
|
+
source: str # the file that says so: 'template.yaml'
|
|
126
|
+
reader: str # the deploy plugin that read it: 'sam'
|
|
127
|
+
file: str # the Python file of its entry point: 'cart/app.py'
|
|
128
|
+
function: str # the entry point's function in that file: 'on_checkout'
|
|
129
|
+
platform: str = "lambda" # where it runs, a code_node.platforms name
|
|
130
|
+
line: int = 0 # where the file says so
|
|
131
|
+
runtime: str | None = None # 'python3.12'
|
|
132
|
+
timeout: int | None = None # seconds
|
|
133
|
+
memory: int | None = None # MB
|
|
134
|
+
env_names: set[str] = field(default_factory=set) # the environment variables it's given (names only)
|
|
135
|
+
env_error: str | None = None # why its variables can't be known, when they can't (env_names is then partial)
|
|
136
|
+
layers: list[str] = field(default_factory=list) # 'arn:aws:lambda:...:layer:pandas:3', 'DepsLayer'
|
|
137
|
+
permissions: set[str] | None = None # the IAM actions its role allows ('s3:GetObject', 'dynamodb:*'); None when
|
|
138
|
+
# the file can't tell (a role made elsewhere, a policy it doesn't know)
|
|
139
|
+
policies: list[str] = field(default_factory=list) # what gives them, as the file says: 'DynamoDBReadPolicy'
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass
|
|
143
|
+
class EnvRead:
|
|
144
|
+
"""
|
|
145
|
+
An environment variable the code reads. Its value is never read: only its name.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
name: str
|
|
149
|
+
file: str
|
|
150
|
+
line: int
|
|
151
|
+
function: str
|
|
152
|
+
required: bool # os.environ['X']: a KeyError when it isn't set; os.environ.get / os.getenv give a default
|
|
153
|
+
default: str = "" # the default, as code
|
|
154
|
+
startup: bool = False # read when the code is imported
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@dataclass
|
|
158
|
+
class EventField:
|
|
159
|
+
"""
|
|
160
|
+
A field the code reads from its input: what its platform passes the entry point (a Lambda handler's event).
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
path: tuple[str, ...] # ('Records', '[]', 'body', '{json}', 'bucket')
|
|
164
|
+
file: str
|
|
165
|
+
line: int
|
|
166
|
+
required: bool # event['x'] (a KeyError when it's missing), not event.get('x')
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def label(self) -> str:
|
|
170
|
+
"""
|
|
171
|
+
'Records[].body (JSON) bucket'.
|
|
172
|
+
"""
|
|
173
|
+
return _event_label(self.path)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@dataclass
|
|
177
|
+
class CodeFunction:
|
|
178
|
+
"""
|
|
179
|
+
One of the code's own functions that runs when the entry point does.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
name: str # 'db.connect', 'repo.Orders.save'
|
|
183
|
+
file: str
|
|
184
|
+
line: int
|
|
185
|
+
lines: int
|
|
186
|
+
args: str # 'host, password'
|
|
187
|
+
doc: str = "" # its docstring's first line
|
|
188
|
+
called_from: list[str] = field(default_factory=list) # 'etl.handler line 19'
|
|
189
|
+
when: dict[str, str] = field(default_factory=dict) # a place in called_from -> the test it's called under: the if
|
|
190
|
+
# nearest around the call, else the nearest `if x: return` above it ('args.serve', not every check before it)
|
|
191
|
+
depth: int = 0 # calls from the entry point (the entry point is 0)
|
|
192
|
+
is_async: bool = False # async def: a call that blocks in it holds up everything else on its event loop
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@dataclass
|
|
196
|
+
class ImportUse:
|
|
197
|
+
"""
|
|
198
|
+
A module the code imports, and where Python finds it.
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
module: str # as written: 'requests', 'db', 'pkg.sub'
|
|
202
|
+
source: str # 'own' (its own files), 'stdlib', 'runtime' (boto3, botocore...), 'package' (a library packed in
|
|
203
|
+
# it), 'layer' (none of those, and the function has layers), 'missing', or 'removed' (from the runtime's Python)
|
|
204
|
+
file: str
|
|
205
|
+
line: int
|
|
206
|
+
optional: bool = False # in a try that catches ImportError
|
|
207
|
+
startup: bool = True # imported when the module loads, not inside a function
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def top(self) -> str:
|
|
211
|
+
return self.module.split(".")[0]
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@dataclass
|
|
215
|
+
class CodeSpot:
|
|
216
|
+
"""
|
|
217
|
+
A line how_it_works()'s findings are about.
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
kind: str # 'http', 'sleep', 'write', 'secret', 'swallow', 'client', 'connect', 'command', 'syntax'
|
|
221
|
+
file: str
|
|
222
|
+
line: int
|
|
223
|
+
function: str
|
|
224
|
+
code: str
|
|
225
|
+
detail: str = "" # http: the method; write: the path; secret: its name (never the value); client: the service;
|
|
226
|
+
# connect: the service a library's client is for ('Redis'); command: what goes into it from the input
|
|
227
|
+
ok: bool = True # http: has a timeout; client, connect: made once and reused; command: nothing from the input
|
|
228
|
+
loop: str = ""
|
|
229
|
+
startup: bool = False # when the code is imported
|
|
230
|
+
waits: str = "" # connect: how long a call through it waits when the service doesn't answer, when that's too long
|
|
231
|
+
# ('as long as Redis takes': no timeout); '' when it's given one
|
|
232
|
+
fix: str = "" # connect: how to give it a timeout: 'Redis(..., socket_timeout=5)'
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@dataclass
|
|
236
|
+
class CodeMap:
|
|
237
|
+
"""
|
|
238
|
+
How Python code works from one entry point, read with Python's ast module (nothing in it is run): what runs when
|
|
239
|
+
it's imported, the steps of each call from the entry point on, following its own functions, and what they reach
|
|
240
|
+
outside: AWS calls, environment variables, the fields of its input, libraries and HTTP.
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
function: Deployment | Any # what the findings check against: where a deploy file says it runs (a Lambda function
|
|
244
|
+
# in template.yaml), else None
|
|
245
|
+
handler: str | None # the entry point as given: 'etl.handler'
|
|
246
|
+
platform: str = "" # the platform it runs on, by name ('lambda'): its checks and wording. '' is plain Python
|
|
247
|
+
handler_file: str | None = None # 'etl.py'
|
|
248
|
+
entry: str | None = None # the function it starts from, as module.name ('etl.handler'); None when it isn't there
|
|
249
|
+
entry_line: int = 0
|
|
250
|
+
params: list[str] = field(default_factory=list) # the entry point's parameters: 'event', 'ctx=…', '*args'
|
|
251
|
+
is_async: bool = False # async def: the Python runtime doesn't await it
|
|
252
|
+
startup: list[CodeStep] = field(default_factory=list) # what runs when the code is imported
|
|
253
|
+
steps: list[CodeStep] = field(default_factory=list) # one 'def' step: the entry point's, with every step under it
|
|
254
|
+
functions: list[CodeFunction] = field(default_factory=list) # its own functions the entry point reaches, in order
|
|
255
|
+
aws: list[AwsCall] = field(default_factory=list)
|
|
256
|
+
library_calls: list[LibraryCall] = field(default_factory=list) # calls through the libraries plugins know
|
|
257
|
+
env: list[EnvRead] = field(default_factory=list)
|
|
258
|
+
event: list[EventField] = field(default_factory=list)
|
|
259
|
+
imports: list[ImportUse] = field(default_factory=list)
|
|
260
|
+
spots: list[CodeSpot] = field(default_factory=list)
|
|
261
|
+
returns: list[tuple[str, str, int]] = field(
|
|
262
|
+
default_factory=list
|
|
263
|
+
) # what the entry point returns: (code, file, line)
|
|
264
|
+
routes: list[tuple[str, str, str]] = field(default_factory=list) # web routes: (method, path, function)
|
|
265
|
+
files: int = 0 # Python files read
|
|
266
|
+
lines: int = 0 # their lines
|
|
267
|
+
other_files: int = 0 # its own Python files the entry point's code never imports (tests, scripts, other handlers)
|
|
268
|
+
unreached: int = 0 # functions in the files it read that the entry point never reaches
|
|
269
|
+
skipped: list[tuple[str, str]] = field(default_factory=list) # (file, why) for files that couldn't be read
|
|
270
|
+
limited: str = "" # what was cut short, when something was
|
|
271
|
+
note: str | None = None # why there's nothing to map: not Python, a container image, no entry point file
|
|
272
|
+
handlers: list[str] = field(default_factory=list) # when the entry point isn't in its file: the functions there
|
|
273
|
+
# that could be (that take what its platform passes)
|
|
274
|
+
plugins: list[str] = field(default_factory=list) # the library plugins whose calls it found, by title: 'Redis'
|
|
275
|
+
|
|
276
|
+
@property
|
|
277
|
+
def services(self) -> list[str]:
|
|
278
|
+
"""
|
|
279
|
+
The AWS services it calls, as people name them ('S3', 'DynamoDB'), in the order it first calls them.
|
|
280
|
+
"""
|
|
281
|
+
return list(dict.fromkeys(_service_name(call.service) for call in self.aws))
|
|
282
|
+
|
|
283
|
+
def to_df(self):
|
|
284
|
+
"""
|
|
285
|
+
Its AWS calls, a row each.
|
|
286
|
+
"""
|
|
287
|
+
import pandas as pd
|
|
288
|
+
|
|
289
|
+
return pd.DataFrame([{**vars(call), "label": call.label} for call in self.aws])
|