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,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])