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
code_node/project.py
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A folder of Python code: its files, the names Python imports them by, and the entry points worth starting from.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import ast
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from .analysis.syntax import _parse
|
|
15
|
+
from .deploys import all_deployments, deploy_file_names
|
|
16
|
+
from .platforms import platforms
|
|
17
|
+
|
|
18
|
+
_SKIPPED_FOLDERS = frozenset(
|
|
19
|
+
{
|
|
20
|
+
".git",
|
|
21
|
+
".hg",
|
|
22
|
+
".svn",
|
|
23
|
+
".venv",
|
|
24
|
+
"venv",
|
|
25
|
+
"env",
|
|
26
|
+
".env",
|
|
27
|
+
"node_modules",
|
|
28
|
+
"__pycache__",
|
|
29
|
+
".tox",
|
|
30
|
+
".nox",
|
|
31
|
+
".mypy_cache",
|
|
32
|
+
".pytest_cache",
|
|
33
|
+
".ruff_cache",
|
|
34
|
+
"build",
|
|
35
|
+
"dist",
|
|
36
|
+
"site-packages",
|
|
37
|
+
".eggs",
|
|
38
|
+
".idea",
|
|
39
|
+
".vscode",
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
_MAX_FILES = 5_000 # Python files read from one folder, at most
|
|
44
|
+
|
|
45
|
+
_MAX_FILE_BYTES = 2 * 1024 * 1024 # a bigger .py file is generated code: skipped
|
|
46
|
+
|
|
47
|
+
_MAIN_NAMES = ("main", "run", "cli", "app")
|
|
48
|
+
|
|
49
|
+
_IMPORT_RE = re.compile(r"^[ \t]*(?:from[ \t]+([\w.]+)[ \t]+import|import[ \t]+([\w., \t]+))", re.M)
|
|
50
|
+
|
|
51
|
+
_CLI_APPS = ("Typer",) # web apps are a platform's (FastAPI for the web one, Chalice for Lambda's)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class EntryPoint:
|
|
56
|
+
"""
|
|
57
|
+
A place to start reading the code from: 'pkg.cli.main', and why it's a good one.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
name: str # module.function, module.Class.method, or module.app (a web app object)
|
|
61
|
+
why: str # 'a console script (code-node)', 'runs under if __name__ == "__main__"', 'a Lambda handler'
|
|
62
|
+
file: str
|
|
63
|
+
line: int
|
|
64
|
+
platform: str = "" # the platform that found it ('lambda' for a Lambda handler): its checks run on the code
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class Project:
|
|
69
|
+
"""
|
|
70
|
+
The Python files under a folder, read as text: {path from the folder, with /: source}.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
root: Path
|
|
74
|
+
files: dict[str, str] = field(default_factory=dict)
|
|
75
|
+
modules: dict[str, str] = field(default_factory=dict) # file -> the module name Python imports it by
|
|
76
|
+
skipped: list[str] = field(default_factory=list)
|
|
77
|
+
limited: bool = False
|
|
78
|
+
configs: list[str] = field(default_factory=list) # its pyproject.toml files, the root's first
|
|
79
|
+
deploy_files: dict[str, str] = field(default_factory=dict) # files deploy plugins read (template.yaml,
|
|
80
|
+
# serverless.yml): {path from the folder: text}
|
|
81
|
+
_trees: dict[str, ast.Module] | None = None
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def trees(self) -> dict[str, ast.Module]:
|
|
85
|
+
"""
|
|
86
|
+
Each file's syntax tree, parsed the first time it's asked for (files that don't parse are left out).
|
|
87
|
+
"""
|
|
88
|
+
if self._trees is None:
|
|
89
|
+
self._trees = {}
|
|
90
|
+
for rel, text in self.files.items():
|
|
91
|
+
try:
|
|
92
|
+
self._trees[rel] = _parse(text, rel)
|
|
93
|
+
except (SyntaxError, ValueError):
|
|
94
|
+
self.skipped.append(rel)
|
|
95
|
+
return self._trees
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def own_tops(self) -> set[str]:
|
|
99
|
+
"""
|
|
100
|
+
The top-level packages and modules that are this folder's own code.
|
|
101
|
+
"""
|
|
102
|
+
return {module.split(".")[0] for module in self.modules.values()}
|
|
103
|
+
|
|
104
|
+
def libraries(self) -> set[str]:
|
|
105
|
+
"""
|
|
106
|
+
Every top-level module the code imports that isn't its own: read as installed libraries, never as missing.
|
|
107
|
+
"""
|
|
108
|
+
own = self.own_tops
|
|
109
|
+
found: set[str] = set()
|
|
110
|
+
for text in self.files.values():
|
|
111
|
+
for match in _IMPORT_RE.finditer(text):
|
|
112
|
+
names = match.group(1) or match.group(2)
|
|
113
|
+
found |= {name.strip().split(".")[0] for name in names.split(",")}
|
|
114
|
+
found = {name.split(" ")[0] for name in found if name}
|
|
115
|
+
return {name for name in found if name not in own and name not in sys.stdlib_module_names}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _module_name(path: Path, root: Path) -> str:
|
|
119
|
+
"""
|
|
120
|
+
The name Python imports a file by: up through the folders that hold an __init__.py, the way a package is laid
|
|
121
|
+
out. 'src/pkg/sub/mod.py' (pkg and sub with __init__.py) -> 'pkg.sub.mod'.
|
|
122
|
+
"""
|
|
123
|
+
parts = [path.stem] if path.name != "__init__.py" else []
|
|
124
|
+
folder = path.parent
|
|
125
|
+
while folder != root and (folder / "__init__.py").exists():
|
|
126
|
+
parts.insert(0, folder.name)
|
|
127
|
+
folder = folder.parent
|
|
128
|
+
return ".".join(parts)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def read_project(root: str | os.PathLike[str]) -> Project:
|
|
132
|
+
"""
|
|
133
|
+
Every .py file under root, skipping virtual environments, caches and build output.
|
|
134
|
+
"""
|
|
135
|
+
base = Path(root).resolve()
|
|
136
|
+
project = Project(base)
|
|
137
|
+
deploy_names = deploy_file_names()
|
|
138
|
+
for folder, subfolders, names in os.walk(base):
|
|
139
|
+
subfolders[:] = sorted(d for d in subfolders if d not in _SKIPPED_FOLDERS and not d.endswith(".egg-info"))
|
|
140
|
+
for name in sorted(names):
|
|
141
|
+
if name == "pyproject.toml":
|
|
142
|
+
project.configs.append((Path(folder) / name).relative_to(base).as_posix())
|
|
143
|
+
if name in deploy_names and (Path(folder) / name).stat().st_size <= _MAX_FILE_BYTES:
|
|
144
|
+
rel = (Path(folder) / name).relative_to(base).as_posix()
|
|
145
|
+
project.deploy_files[rel] = (Path(folder) / name).read_text("utf-8", "replace")
|
|
146
|
+
if not name.endswith(".py"):
|
|
147
|
+
continue
|
|
148
|
+
path = Path(folder) / name
|
|
149
|
+
rel = path.relative_to(base).as_posix()
|
|
150
|
+
if len(project.files) >= _MAX_FILES:
|
|
151
|
+
project.limited = True
|
|
152
|
+
return project
|
|
153
|
+
if path.stat().st_size > _MAX_FILE_BYTES:
|
|
154
|
+
project.skipped.append(rel)
|
|
155
|
+
continue
|
|
156
|
+
text = path.read_text("utf-8", "replace")
|
|
157
|
+
project.files[rel] = text
|
|
158
|
+
project.modules[rel] = _module_name(path, base)
|
|
159
|
+
return project
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _console_scripts(path: Path) -> dict[str, str]:
|
|
163
|
+
"""
|
|
164
|
+
A pyproject.toml's console scripts: {'code-node': 'code_node.cli:main'}.
|
|
165
|
+
"""
|
|
166
|
+
if not path.exists():
|
|
167
|
+
return {}
|
|
168
|
+
text = path.read_text("utf-8", "replace")
|
|
169
|
+
try:
|
|
170
|
+
import tomllib
|
|
171
|
+
except ImportError: # Python 3.10: read the [project.scripts] table by hand
|
|
172
|
+
table = re.search(r"^\[(?:project\.scripts|tool\.poetry\.scripts)\]\s*$(.*?)(?=^\[|\Z)", text, re.M | re.S)
|
|
173
|
+
lines = table.group(1).splitlines() if table else []
|
|
174
|
+
pairs = (re.match(r"\s*['\"]?([\w.-]+)['\"]?\s*=\s*['\"]([\w.:]+)['\"]", line) for line in lines)
|
|
175
|
+
return {match.group(1): match.group(2) for match in pairs if match}
|
|
176
|
+
try:
|
|
177
|
+
data = tomllib.loads(text)
|
|
178
|
+
except tomllib.TOMLDecodeError:
|
|
179
|
+
return {}
|
|
180
|
+
scripts = dict(data.get("project", {}).get("scripts", {}))
|
|
181
|
+
scripts.update(data.get("tool", {}).get("poetry", {}).get("scripts", {}))
|
|
182
|
+
return {name: str(target) for name, target in scripts.items()}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _main_guard_calls(tree: ast.Module) -> list[str]:
|
|
186
|
+
"""
|
|
187
|
+
The functions an `if __name__ == "__main__":` block calls: ['main'].
|
|
188
|
+
"""
|
|
189
|
+
called = []
|
|
190
|
+
for node in tree.body:
|
|
191
|
+
is_guard = (
|
|
192
|
+
isinstance(node, ast.If)
|
|
193
|
+
and isinstance(node.test, ast.Compare)
|
|
194
|
+
and isinstance(node.test.left, ast.Name)
|
|
195
|
+
and node.test.left.id == "__name__"
|
|
196
|
+
)
|
|
197
|
+
if not is_guard:
|
|
198
|
+
continue
|
|
199
|
+
for inner in ast.walk(node):
|
|
200
|
+
if isinstance(inner, ast.Call) and isinstance(inner.func, ast.Name):
|
|
201
|
+
called.append(inner.func.id)
|
|
202
|
+
return called
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _module_functions(tree: ast.Module) -> dict[str, ast.FunctionDef | ast.AsyncFunctionDef]:
|
|
206
|
+
return {node.name: node for node in tree.body if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _web_apps(tree: ast.Module) -> list[tuple[str, int, str, str]]:
|
|
210
|
+
"""
|
|
211
|
+
Module-level web and CLI apps, with the platform whose app it is: `app = FastAPI()` -> [('app', 3, 'a web app:
|
|
212
|
+
each route', 'web')].
|
|
213
|
+
"""
|
|
214
|
+
found = []
|
|
215
|
+
for node in tree.body:
|
|
216
|
+
if not (isinstance(node, ast.Assign) and isinstance(node.value, ast.Call)):
|
|
217
|
+
continue
|
|
218
|
+
func = node.value.func
|
|
219
|
+
made = func.attr if isinstance(func, ast.Attribute) else getattr(func, "id", "")
|
|
220
|
+
web = next((platform.name for platform in platforms() if made in platform.web_apps), None)
|
|
221
|
+
if web is not None or made in _CLI_APPS:
|
|
222
|
+
kind = "a CLI app: each command" if web is None else "a web app: each route"
|
|
223
|
+
found += [(t.id, node.lineno, kind, web or "") for t in node.targets if isinstance(t, ast.Name)]
|
|
224
|
+
return found
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _calls(node: ast.AST) -> set[str]:
|
|
228
|
+
"""
|
|
229
|
+
The plain names a function calls: {'app', 'print'}.
|
|
230
|
+
"""
|
|
231
|
+
return {n.func.id for n in ast.walk(node) if isinstance(n, ast.Call) and isinstance(n.func, ast.Name)}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _is_test(rel: str) -> bool:
|
|
235
|
+
"""
|
|
236
|
+
A test file: under tests/ or named test_*.py / *_test.py / conftest.py.
|
|
237
|
+
"""
|
|
238
|
+
name = rel.rsplit("/", 1)[-1]
|
|
239
|
+
return (
|
|
240
|
+
"tests/" in f"/{rel}"
|
|
241
|
+
or "/test/" in f"/{rel}"
|
|
242
|
+
or name.startswith("test_")
|
|
243
|
+
or name.endswith("_test.py")
|
|
244
|
+
or name == "conftest.py"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def entry_points(project: Project) -> list[EntryPoint]:
|
|
249
|
+
"""
|
|
250
|
+
Where the code starts, best first: console scripts, the entry points platforms find (Lambda handlers), web apps,
|
|
251
|
+
then what runs under `if __name__ == "__main__":` and functions named main.
|
|
252
|
+
"""
|
|
253
|
+
found: dict[str, EntryPoint] = {}
|
|
254
|
+
by_module = {module: rel for rel, module in project.modules.items()}
|
|
255
|
+
|
|
256
|
+
def add(entry: EntryPoint) -> None:
|
|
257
|
+
found.setdefault(entry.name, entry)
|
|
258
|
+
|
|
259
|
+
scripts = [
|
|
260
|
+
(script, target, config)
|
|
261
|
+
for config in project.configs
|
|
262
|
+
for script, target in _console_scripts(project.root / config).items()
|
|
263
|
+
]
|
|
264
|
+
for script, target, config in scripts:
|
|
265
|
+
module, _, function = target.partition(":")
|
|
266
|
+
rel = by_module.get(module)
|
|
267
|
+
if not (rel and function and rel in project.trees):
|
|
268
|
+
continue
|
|
269
|
+
tree = project.trees[rel]
|
|
270
|
+
node = _module_functions(tree).get(function.split(".")[0])
|
|
271
|
+
apps = {name: (line, kind, runs_on) for name, line, kind, runs_on in _web_apps(tree)}
|
|
272
|
+
runs = [name for name in (_calls(node) if node is not None else set()) if name in apps]
|
|
273
|
+
command = f"the {script} command" + (f" in {config}" if "/" in config else "")
|
|
274
|
+
if runs: # main() just runs the app: start from the app, so each command is followed
|
|
275
|
+
line, kind, runs_on = apps[runs[0]]
|
|
276
|
+
add(EntryPoint(f"{module}.{runs[0]}", f"{command} ({kind})", rel, line, runs_on))
|
|
277
|
+
add(EntryPoint(f"{module}.{function}", command, rel, getattr(node, "lineno", 0)))
|
|
278
|
+
code = {rel: tree for rel, tree in project.trees.items() if not _is_test(rel)}
|
|
279
|
+
for deployed in all_deployments(project.deploy_files, project.files):
|
|
280
|
+
tree = project.trees.get(deployed.file)
|
|
281
|
+
node = _module_functions(tree).get(deployed.function) if tree is not None else None
|
|
282
|
+
if node is not None:
|
|
283
|
+
add(
|
|
284
|
+
EntryPoint(
|
|
285
|
+
f"{project.modules[deployed.file]}.{deployed.function}",
|
|
286
|
+
f"{deployed.name} in {deployed.source}",
|
|
287
|
+
deployed.file,
|
|
288
|
+
node.lineno,
|
|
289
|
+
deployed.platform,
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
for rel, tree in code.items():
|
|
293
|
+
module = project.modules[rel]
|
|
294
|
+
for platform in platforms():
|
|
295
|
+
for name, line, why in platform.find_entries(module, tree):
|
|
296
|
+
add(EntryPoint(f"{module}.{name}", why, rel, line, platform.name))
|
|
297
|
+
for name, line, kind, runs_on in _web_apps(tree):
|
|
298
|
+
add(EntryPoint(f"{module}.{name}", kind, rel, line, runs_on))
|
|
299
|
+
for rel, tree in code.items():
|
|
300
|
+
module = project.modules[rel]
|
|
301
|
+
functions = _module_functions(tree)
|
|
302
|
+
for name in _main_guard_calls(tree):
|
|
303
|
+
if name in functions:
|
|
304
|
+
add(EntryPoint(f"{module}.{name}", 'runs under if __name__ == "__main__"', rel, functions[name].lineno))
|
|
305
|
+
for rel, tree in code.items():
|
|
306
|
+
functions = _module_functions(tree)
|
|
307
|
+
for name in _MAIN_NAMES:
|
|
308
|
+
if name in functions:
|
|
309
|
+
add(
|
|
310
|
+
EntryPoint(
|
|
311
|
+
f"{project.modules[rel]}.{name}", f"a function named {name}()", rel, functions[name].lineno
|
|
312
|
+
)
|
|
313
|
+
)
|
|
314
|
+
# one another of its kind names, as include('orders.urls') does, comes after it: the site's URLs before an app's
|
|
315
|
+
named = {
|
|
316
|
+
e.name
|
|
317
|
+
for e in found.values()
|
|
318
|
+
for other in found.values()
|
|
319
|
+
if other.why == e.why
|
|
320
|
+
and other.file != e.file
|
|
321
|
+
and re.search(rf"['\"]{re.escape(e.name.rsplit('.', 1)[0])}['\"]", project.files.get(other.file, ""))
|
|
322
|
+
}
|
|
323
|
+
return [e for e in found.values() if e.name not in named] + [e for e in found.values() if e.name in named]
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def entry_in_files(project: Project, entry: str) -> EntryPoint | None:
|
|
327
|
+
"""
|
|
328
|
+
The entry point as given, when its module (or, for a method, the module before its class) is a file here.
|
|
329
|
+
"""
|
|
330
|
+
by_module = {module: rel for rel, module in project.modules.items()}
|
|
331
|
+
for cut in (1, 2):
|
|
332
|
+
module = entry.rsplit(".", cut)[0]
|
|
333
|
+
if module in by_module and entry.count(".") >= cut:
|
|
334
|
+
return EntryPoint(entry, "given", by_module[module], 0)
|
|
335
|
+
return None
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def functions_matching(project: Project, text: str) -> list[EntryPoint]:
|
|
339
|
+
"""
|
|
340
|
+
Functions and methods whose name contains text ('save' finds repo.Orders.save), for a partly typed entry point.
|
|
341
|
+
"""
|
|
342
|
+
found = []
|
|
343
|
+
needle = text.lower()
|
|
344
|
+
for rel, tree in project.trees.items():
|
|
345
|
+
module = project.modules[rel]
|
|
346
|
+
for node in tree.body:
|
|
347
|
+
members = (
|
|
348
|
+
[(f"{module}.{node.name}", node)] if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) else []
|
|
349
|
+
)
|
|
350
|
+
if isinstance(node, ast.ClassDef):
|
|
351
|
+
members = [
|
|
352
|
+
(f"{module}.{node.name}.{m.name}", m)
|
|
353
|
+
for m in node.body
|
|
354
|
+
if isinstance(m, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
355
|
+
]
|
|
356
|
+
found += [EntryPoint(name, "matches", rel, fn.lineno) for name, fn in members if needle in name.lower()]
|
|
357
|
+
return found
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def project_root(file: Path) -> Path:
|
|
361
|
+
"""
|
|
362
|
+
The folder a file's project starts at: the nearest one above it with .git, pyproject.toml, setup.py or setup.cfg,
|
|
363
|
+
else the file's own folder.
|
|
364
|
+
"""
|
|
365
|
+
for folder in [file.parent, *file.parent.parents]:
|
|
366
|
+
if any((folder / marker).exists() for marker in (".git", "pyproject.toml", "setup.py", "setup.cfg")):
|
|
367
|
+
return folder
|
|
368
|
+
return file.parent
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def entries_in_file(project: Project, rel: str, function: str = "") -> list[EntryPoint]:
|
|
372
|
+
"""
|
|
373
|
+
Where to start in one file, best first: the function named (function or Class.method), else what its
|
|
374
|
+
`if __name__ == "__main__":` block calls, its platforms' entry points (Lambda handlers) and apps, functions named
|
|
375
|
+
main, then every other function in it from the top.
|
|
376
|
+
"""
|
|
377
|
+
module = project.modules.get(rel)
|
|
378
|
+
tree = project.trees.get(rel)
|
|
379
|
+
if module is None or tree is None:
|
|
380
|
+
return []
|
|
381
|
+
found = [e for e in entry_points(project) if e.file == rel]
|
|
382
|
+
if function:
|
|
383
|
+
known = next((e for e in found if e.name == f"{module}.{function}"), None) # a handler keeps its platform
|
|
384
|
+
return [EntryPoint(f"{module}.{function}", "named", rel, 0, known.platform if known else "")]
|
|
385
|
+
named = {e.name for e in found}
|
|
386
|
+
functions = _module_functions(tree)
|
|
387
|
+
for name in _main_guard_calls(tree):
|
|
388
|
+
if name in functions and f"{module}.{name}" not in named:
|
|
389
|
+
found.insert(
|
|
390
|
+
0, EntryPoint(f"{module}.{name}", 'runs under if __name__ == "__main__"', rel, functions[name].lineno)
|
|
391
|
+
)
|
|
392
|
+
named.add(f"{module}.{name}")
|
|
393
|
+
for name, node in functions.items():
|
|
394
|
+
if f"{module}.{name}" not in named:
|
|
395
|
+
found.append(EntryPoint(f"{module}.{name}", "a function in this file", rel, node.lineno))
|
|
396
|
+
for cls in [node for node in tree.body if isinstance(node, ast.ClassDef)]:
|
|
397
|
+
methods = [m for m in cls.body if isinstance(m, (ast.FunctionDef, ast.AsyncFunctionDef))]
|
|
398
|
+
for method in sorted(methods, key=lambda m: m.name.startswith("_")):
|
|
399
|
+
found.append(
|
|
400
|
+
EntryPoint(f"{module}.{cls.name}.{method.name}", f"a method of {cls.name}", rel, method.lineno)
|
|
401
|
+
)
|
|
402
|
+
return found
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Real logos for the graph's boxes, in two sizes, as favicons come: `full`, the detailed art, for 40 pixels and up
|
|
3
|
+
(a 2x screen's 32), and, for AWS's service icons, `small`, a simple mark that stays sharp at 16 to 32 (an ordinary
|
|
4
|
+
screen's badge). logo_for(name, pixels) picks one; the page's graph has both and picks by the screen's density.
|
|
5
|
+
|
|
6
|
+
Real logos for the graph's boxes: an AWS service's (S3, DynamoDB, SNS...) and those of the services libraries reach
|
|
7
|
+
(Redis, PostgreSQL, OpenAI...). The SVGs are gilbarbara/logos, CC0, through Iconify's @iconify-json/logos, kept as
|
|
8
|
+
files in logos/: scripts/logos.py downloads the pinned version and writes the ones named here. The page embeds the
|
|
9
|
+
ones it uses, so it stays one file that works offline. Each logo is its company's trademark, shown to name the
|
|
10
|
+
service the code reaches.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
from functools import lru_cache
|
|
17
|
+
from importlib.resources import files
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from ..platforms.web import FRAMEWORK_LOGOS
|
|
21
|
+
|
|
22
|
+
AWS_LOGOS = { # boto3's service name -> its logo
|
|
23
|
+
"s3": "aws-s3",
|
|
24
|
+
"s3control": "aws-s3",
|
|
25
|
+
"dynamodb": "aws-dynamodb",
|
|
26
|
+
"dynamodbstreams": "aws-dynamodb",
|
|
27
|
+
"sns": "aws-sns",
|
|
28
|
+
"sqs": "aws-sqs",
|
|
29
|
+
"lambda": "aws-lambda",
|
|
30
|
+
"kinesis": "aws-kinesis",
|
|
31
|
+
"events": "aws-eventbridge",
|
|
32
|
+
"stepfunctions": "aws-step-functions",
|
|
33
|
+
"secretsmanager": "aws-secrets-manager",
|
|
34
|
+
"ssm": "aws-systems-manager",
|
|
35
|
+
"kms": "aws-kms",
|
|
36
|
+
"ses": "aws-ses",
|
|
37
|
+
"sesv2": "aws-ses",
|
|
38
|
+
"rds": "aws-rds",
|
|
39
|
+
"rds-data": "aws-rds",
|
|
40
|
+
"redshift": "aws-redshift",
|
|
41
|
+
"redshift-data": "aws-redshift",
|
|
42
|
+
"athena": "aws-athena",
|
|
43
|
+
"glue": "aws-glue",
|
|
44
|
+
"ecs": "aws-ecs",
|
|
45
|
+
"eks": "aws-eks",
|
|
46
|
+
"ec2": "aws-ec2",
|
|
47
|
+
"cloudwatch": "aws-cloudwatch",
|
|
48
|
+
"logs": "aws-cloudwatch",
|
|
49
|
+
"cognito-idp": "aws-cognito",
|
|
50
|
+
"cognito-identity": "aws-cognito",
|
|
51
|
+
"iam": "aws-iam",
|
|
52
|
+
"sts": "aws-iam",
|
|
53
|
+
"apigateway": "aws-api-gateway",
|
|
54
|
+
"apigatewayv2": "aws-api-gateway",
|
|
55
|
+
"apigatewaymanagementapi": "aws-api-gateway",
|
|
56
|
+
"cloudfront": "aws-cloudfront",
|
|
57
|
+
"route53": "aws-route53",
|
|
58
|
+
"elasticache": "aws-elasticache",
|
|
59
|
+
"docdb": "aws-documentdb",
|
|
60
|
+
"neptune": "aws-neptune",
|
|
61
|
+
"timestream-write": "aws-timestream",
|
|
62
|
+
"timestream-query": "aws-timestream",
|
|
63
|
+
"keyspaces": "aws-keyspaces",
|
|
64
|
+
"batch": "aws-batch",
|
|
65
|
+
"codebuild": "aws-codebuild",
|
|
66
|
+
"codepipeline": "aws-codepipeline",
|
|
67
|
+
"codedeploy": "aws-codedeploy",
|
|
68
|
+
"codecommit": "aws-codecommit",
|
|
69
|
+
"cloudformation": "aws-cloudformation",
|
|
70
|
+
"xray": "aws-xray",
|
|
71
|
+
"appsync": "aws-appsync",
|
|
72
|
+
"mq": "aws-mq",
|
|
73
|
+
"kafka": "aws-msk",
|
|
74
|
+
"opensearch": "aws-open-search",
|
|
75
|
+
"es": "aws-open-search",
|
|
76
|
+
"acm": "aws-certificate-manager",
|
|
77
|
+
"backup": "aws-backup",
|
|
78
|
+
"glacier": "aws-glacier",
|
|
79
|
+
"quicksight": "aws-quicksight",
|
|
80
|
+
"wafv2": "aws-waf",
|
|
81
|
+
"waf": "aws-waf",
|
|
82
|
+
"cloudtrail": "aws-cloudtrail",
|
|
83
|
+
"config": "aws-config",
|
|
84
|
+
"lightsail": "aws-lightsail",
|
|
85
|
+
"amplify": "aws-amplify",
|
|
86
|
+
"appflow": "aws-appflow",
|
|
87
|
+
"lakeformation": "aws-lake-formation",
|
|
88
|
+
"shield": "aws-shield",
|
|
89
|
+
"elbv2": "aws-elb",
|
|
90
|
+
"elb": "aws-elb",
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
SERVICE_LOGOS = { # a service a library reaches, as its box names it (lower case) -> its logo
|
|
94
|
+
"redis": "redis",
|
|
95
|
+
"postgresql": "postgresql",
|
|
96
|
+
"mysql": "mysql-icon",
|
|
97
|
+
"sqlite": "sqlite",
|
|
98
|
+
"mongodb": "mongodb-icon",
|
|
99
|
+
"openai": "openai-icon",
|
|
100
|
+
"azure openai": "azure-icon",
|
|
101
|
+
"anthropic": "anthropic-icon",
|
|
102
|
+
"anthropic on bedrock": "anthropic-icon",
|
|
103
|
+
"anthropic on vertex ai": "anthropic-icon",
|
|
104
|
+
"kafka": "kafka-icon",
|
|
105
|
+
"rabbitmq": "rabbitmq-icon",
|
|
106
|
+
"snowflake": "snowflake-icon",
|
|
107
|
+
"redshift": "aws-redshift",
|
|
108
|
+
"elasticsearch": "elasticsearch",
|
|
109
|
+
"mariadb": "mariadb-icon",
|
|
110
|
+
"stripe": "stripe",
|
|
111
|
+
"slack": "slack-icon",
|
|
112
|
+
"twilio": "twilio-icon",
|
|
113
|
+
"supabase": "supabase-icon",
|
|
114
|
+
"firebase": "firebase-icon",
|
|
115
|
+
"dynamodb": "aws-dynamodb",
|
|
116
|
+
"s3": "aws-s3",
|
|
117
|
+
"google cloud": "google-cloud",
|
|
118
|
+
"google cloud storage": "google-cloud",
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
DEPLOY_LOGOS = {"sam": "aws-cloudformation", "serverless": "serverless"} # a deploy plugin's name -> its logo
|
|
123
|
+
|
|
124
|
+
PLATFORM_LOGOS = {"lambda": "aws-lambda", "": "python"} # a platform's name -> its logo ('' is plain Python); the
|
|
125
|
+
# web one's are by framework (platforms/web.py FRAMEWORK_LOGOS), shown on an app's box
|
|
126
|
+
|
|
127
|
+
LIBRARY_LOGOS = { # a library plugin's name -> its logo, and boto3's, which the reader knows itself
|
|
128
|
+
"boto3": "aws",
|
|
129
|
+
"redis": "redis",
|
|
130
|
+
"sql": "postgresql",
|
|
131
|
+
"mongodb": "mongodb-icon",
|
|
132
|
+
"llm": "openai-icon",
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
AWS_GLYPHS = { # an AWS icon -> the symbol its small mark has, in white on the service's colour (AWS's own 16 px
|
|
137
|
+
# icons are drawn that way); one not here gets 'cloud'
|
|
138
|
+
"aws-s3": "bucket",
|
|
139
|
+
"aws-glacier": "bucket",
|
|
140
|
+
"aws-backup": "bucket",
|
|
141
|
+
"aws-dynamodb": "database",
|
|
142
|
+
"aws-rds": "database",
|
|
143
|
+
"aws-aurora": "database",
|
|
144
|
+
"aws-redshift": "database",
|
|
145
|
+
"aws-timestream": "database",
|
|
146
|
+
"aws-documentdb": "database",
|
|
147
|
+
"aws-neptune": "database",
|
|
148
|
+
"aws-elasticache": "database",
|
|
149
|
+
"aws-keyspaces": "database",
|
|
150
|
+
"aws-athena": "database",
|
|
151
|
+
"aws-sqs": "queue",
|
|
152
|
+
"aws-kinesis": "queue",
|
|
153
|
+
"aws-mq": "queue",
|
|
154
|
+
"aws-msk": "queue",
|
|
155
|
+
"aws-sns": "send",
|
|
156
|
+
"aws-eventbridge": "send",
|
|
157
|
+
"aws-ses": "send",
|
|
158
|
+
"aws-secrets-manager": "key",
|
|
159
|
+
"aws-kms": "key",
|
|
160
|
+
"aws-iam": "key",
|
|
161
|
+
"aws-cognito": "key",
|
|
162
|
+
"aws-systems-manager": "key",
|
|
163
|
+
"aws-certificate-manager": "key",
|
|
164
|
+
"aws-shield": "key",
|
|
165
|
+
"aws-waf": "key",
|
|
166
|
+
"aws-lambda": "function",
|
|
167
|
+
"aws-step-functions": "flow",
|
|
168
|
+
"aws-glue": "flow",
|
|
169
|
+
"aws-batch": "flow",
|
|
170
|
+
"aws-ecs": "flow",
|
|
171
|
+
"aws-eks": "flow",
|
|
172
|
+
"aws-codebuild": "flow",
|
|
173
|
+
"aws-codepipeline": "flow",
|
|
174
|
+
"aws-codedeploy": "flow",
|
|
175
|
+
"aws-appflow": "flow",
|
|
176
|
+
"aws-cloudformation": "flow",
|
|
177
|
+
"aws-api-gateway": "http",
|
|
178
|
+
"aws-cloudfront": "http",
|
|
179
|
+
"aws-route53": "http",
|
|
180
|
+
"aws-appsync": "http",
|
|
181
|
+
"aws-elb": "http",
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
SMALL_UNDER = 40 # pixels: a logo drawn smaller than this uses its small mark, when it has one
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _read(path: Any) -> tuple[int, int, str] | None:
|
|
188
|
+
if not path.is_file():
|
|
189
|
+
return None
|
|
190
|
+
found = re.fullmatch(r'\s*<svg[^>]*viewBox="0 0 (\d+) (\d+)"[^>]*>(.*)</svg>\s*', path.read_text("utf-8"), re.S)
|
|
191
|
+
return (int(found.group(1)), int(found.group(2)), found.group(3)) if found else None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@lru_cache(maxsize=None)
|
|
195
|
+
def small_logo(name: str) -> tuple[int, int, str] | None:
|
|
196
|
+
"""
|
|
197
|
+
A logo's small mark, from logos/small/<name>.svg: (width, height, its SVG body). None when it has none: only
|
|
198
|
+
AWS's service icons do, the others being simple enough already.
|
|
199
|
+
"""
|
|
200
|
+
if not re.fullmatch(r"[a-z0-9-]+", name or ""):
|
|
201
|
+
return None
|
|
202
|
+
return _read(files(__package__).joinpath("logos", "small", f"{name}.svg"))
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def logo_for(name: str, pixels: float) -> tuple[int, int, str] | None:
|
|
206
|
+
"""
|
|
207
|
+
The version of a logo for the pixels it's drawn at, as a browser picks a favicon: the small mark under
|
|
208
|
+
SMALL_UNDER, when there is one, else the full art.
|
|
209
|
+
"""
|
|
210
|
+
return (small_logo(name) if pixels < SMALL_UNDER else None) or logo(name)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@lru_cache(maxsize=None)
|
|
214
|
+
def logo(name: str) -> tuple[int, int, str] | None:
|
|
215
|
+
"""
|
|
216
|
+
A logo by its name ('aws-s3'), or a plugin's icon when it names one: (width, height, its SVG body), read from
|
|
217
|
+
logos/<name>.svg. None when there's no such logo.
|
|
218
|
+
"""
|
|
219
|
+
if not re.fullmatch(r"[a-z0-9-]+", name or ""):
|
|
220
|
+
return None
|
|
221
|
+
return _read(files(__package__).joinpath("logos", f"{name}.svg"))
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def fills(found: tuple[int, int, str]) -> bool:
|
|
225
|
+
"""
|
|
226
|
+
Whether a logo is a coloured square of its own (AWS's service icons): drawn filling its badge, not on a tile,
|
|
227
|
+
so it gets every pixel there is. Their fine lines blur together any smaller on a 1x screen.
|
|
228
|
+
"""
|
|
229
|
+
width, height, body = found
|
|
230
|
+
return width == height and f'd="M0 0h{width}v{height}H0z"' in body[:600]
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def logo_names() -> set[str]:
|
|
234
|
+
"""
|
|
235
|
+
Every logo the graph and the docs' figures draw: what scripts/logos.py keeps.
|
|
236
|
+
"""
|
|
237
|
+
return (
|
|
238
|
+
set(AWS_LOGOS.values())
|
|
239
|
+
| set(SERVICE_LOGOS.values())
|
|
240
|
+
| set(DEPLOY_LOGOS.values())
|
|
241
|
+
| set(PLATFORM_LOGOS.values())
|
|
242
|
+
| set(LIBRARY_LOGOS.values())
|
|
243
|
+
| set(FRAMEWORK_LOGOS.values())
|
|
244
|
+
)
|