codeabc 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.
- backend/__init__.py +0 -0
- backend/app.py +95 -0
- backend/desktop_server.py +46 -0
- backend/models.py +629 -0
- backend/prompts/__init__.py +0 -0
- backend/prompts/annotate.py +44 -0
- backend/prompts/edit.py +62 -0
- backend/prompts/overview.py +57 -0
- backend/prompts/qa.py +45 -0
- backend/routers/__init__.py +0 -0
- backend/routers/analyze.py +271 -0
- backend/routers/project.py +872 -0
- backend/services/__init__.py +0 -0
- backend/services/action_plan.py +366 -0
- backend/services/activity.py +313 -0
- backend/services/apimap.py +391 -0
- backend/services/async_surface.py +123 -0
- backend/services/cache.py +183 -0
- backend/services/churn.py +244 -0
- backend/services/ci_checks.py +317 -0
- backend/services/codemap_export.py +136 -0
- backend/services/commands.py +276 -0
- backend/services/complexity.py +111 -0
- backend/services/config_files.py +190 -0
- backend/services/contributing.py +308 -0
- backend/services/coverage.py +283 -0
- backend/services/datamodels.py +190 -0
- backend/services/deep_nesting.py +110 -0
- backend/services/dependencies.py +270 -0
- backend/services/docs.py +255 -0
- backend/services/docstrings.py +163 -0
- backend/services/duplicate_code.py +165 -0
- backend/services/entrypoints.py +152 -0
- backend/services/envscan.py +167 -0
- backend/services/error_handling.py +239 -0
- backend/services/filenames.py +261 -0
- backend/services/github_clone.py +129 -0
- backend/services/glossary.py +96 -0
- backend/services/health_score.py +381 -0
- backend/services/importgraph.py +986 -0
- backend/services/integrations.py +263 -0
- backend/services/licenses.py +505 -0
- backend/services/llm.py +117 -0
- backend/services/long_functions.py +101 -0
- backend/services/ownership.py +194 -0
- backend/services/package_purposes.py +112 -0
- backend/services/pr_reading.py +181 -0
- backend/services/release_map.py +402 -0
- backend/services/report_export.py +232 -0
- backend/services/risk.py +88 -0
- backend/services/scanner.py +546 -0
- backend/services/schedules.py +369 -0
- backend/services/security.py +445 -0
- backend/services/settings_map.py +234 -0
- backend/services/symbols.py +605 -0
- backend/services/techdebt.py +124 -0
- backend/services/too_many_params.py +102 -0
- backend/services/typing_coverage.py +184 -0
- codeabc-0.1.0.dist-info/METADATA +283 -0
- codeabc-0.1.0.dist-info/RECORD +62 -0
- codeabc-0.1.0.dist-info/WHEEL +4 -0
- codeabc-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""External services: which live third-party services this code talks to.
|
|
2
|
+
|
|
3
|
+
``dependencies`` lists the packages a project *declares*; ``envscan`` lists the
|
|
4
|
+
environment variables it *reads*. This answers the question a non-programmer
|
|
5
|
+
actually asks before they can run an inherited project: "what outside services
|
|
6
|
+
does this thing need, and which of them will cost me money or an account?"
|
|
7
|
+
|
|
8
|
+
A 200-line file that imports ``openai`` and ``boto3`` means you can't run it
|
|
9
|
+
without an OpenAI API key and an AWS account, no matter how clean the code is.
|
|
10
|
+
That fact doesn't show up in a dependency list in any obvious way.
|
|
11
|
+
|
|
12
|
+
:func:`detect_external_services` scans the already-read file contents for the
|
|
13
|
+
import statements (and a few tell-tale hostnames) of well-known services, maps
|
|
14
|
+
them to a plain-language note, and groups them by category. Pure functions over
|
|
15
|
+
the file-content map, so it is unit-testable with plain strings. Detection is a
|
|
16
|
+
curated dictionary, not an exhaustive registry: it aims to catch the services
|
|
17
|
+
that matter for "can I run this", and to stay quiet rather than guess.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
|
|
24
|
+
# module/import token -> (display name, category, plain-language note)
|
|
25
|
+
# Keyed by the root import token (python) or package name (js/go path tail).
|
|
26
|
+
_SERVICE_BY_MODULE: dict[str, tuple[str, str, str]] = {
|
|
27
|
+
# --- AI / LLM ---
|
|
28
|
+
"openai": ("OpenAI", "AI / 大模型", "调用 OpenAI 接口,需要 OpenAI API key(按用量计费)。"),
|
|
29
|
+
"anthropic": (
|
|
30
|
+
"Anthropic",
|
|
31
|
+
"AI / 大模型",
|
|
32
|
+
"调用 Anthropic Claude 接口,需要 API key(按用量计费)。",
|
|
33
|
+
),
|
|
34
|
+
"cohere": ("Cohere", "AI / 大模型", "调用 Cohere 接口,需要 API key。"),
|
|
35
|
+
"google.generativeai": ("Google Gemini", "AI / 大模型", "调用 Google Gemini,需要 API key。"),
|
|
36
|
+
"replicate": (
|
|
37
|
+
"Replicate",
|
|
38
|
+
"AI / 大模型",
|
|
39
|
+
"在 Replicate 上跑模型,需要 API token(按用量计费)。",
|
|
40
|
+
),
|
|
41
|
+
"huggingface_hub": ("Hugging Face", "AI / 大模型", "访问 Hugging Face,私有模型需要 token。"),
|
|
42
|
+
"transformers": ("Hugging Face", "AI / 大模型", "用 Hugging Face 模型,可能联网下载权重。"),
|
|
43
|
+
# --- Cloud ---
|
|
44
|
+
"boto3": ("AWS", "云服务", "调用 AWS(S3/等),需要 AWS 账号与密钥(按用量计费)。"),
|
|
45
|
+
"botocore": ("AWS", "云服务", "调用 AWS,需要 AWS 账号与密钥。"),
|
|
46
|
+
"google.cloud": ("Google Cloud", "云服务", "调用 Google Cloud,需要 GCP 账号与凭据。"),
|
|
47
|
+
"azure": ("Azure", "云服务", "调用 Microsoft Azure,需要 Azure 账号与凭据。"),
|
|
48
|
+
# --- Databases / cache ---
|
|
49
|
+
"psycopg2": ("PostgreSQL", "数据库", "连接 PostgreSQL 数据库,需要一个可用的数据库实例。"),
|
|
50
|
+
"psycopg": ("PostgreSQL", "数据库", "连接 PostgreSQL 数据库,需要一个可用的数据库实例。"),
|
|
51
|
+
"asyncpg": ("PostgreSQL", "数据库", "连接 PostgreSQL 数据库,需要一个可用的数据库实例。"),
|
|
52
|
+
"pymysql": ("MySQL", "数据库", "连接 MySQL 数据库,需要一个可用的数据库实例。"),
|
|
53
|
+
"mysqlclient": ("MySQL", "数据库", "连接 MySQL 数据库,需要一个可用的数据库实例。"),
|
|
54
|
+
"pymongo": ("MongoDB", "数据库", "连接 MongoDB,需要一个可用的 MongoDB 实例。"),
|
|
55
|
+
"redis": ("Redis", "数据库 / 缓存", "连接 Redis,需要一个可用的 Redis 实例。"),
|
|
56
|
+
"elasticsearch": ("Elasticsearch", "数据库 / 搜索", "连接 Elasticsearch,需要一个可用的集群。"),
|
|
57
|
+
"pinecone": ("Pinecone", "数据库 / 向量库", "调用 Pinecone 向量库,需要 API key。"),
|
|
58
|
+
# --- Payments ---
|
|
59
|
+
"stripe": ("Stripe", "支付", "调用 Stripe 收款,需要 Stripe 账号与密钥。"),
|
|
60
|
+
# --- Messaging / comms ---
|
|
61
|
+
"twilio": ("Twilio", "通讯", "发短信/打电话走 Twilio,需要账号(按用量计费)。"),
|
|
62
|
+
"sendgrid": ("SendGrid", "邮件", "发邮件走 SendGrid,需要 API key。"),
|
|
63
|
+
"slack_sdk": ("Slack", "通讯", "对接 Slack,需要 Slack token。"),
|
|
64
|
+
"kafka": ("Kafka", "消息队列", "用 Kafka 收发消息,需要一个 Kafka 集群。"),
|
|
65
|
+
"pika": (
|
|
66
|
+
"RabbitMQ",
|
|
67
|
+
"消息队列",
|
|
68
|
+
"用 RabbitMQ 在不同程序间传消息,需要先搭一台 RabbitMQ 服务。",
|
|
69
|
+
),
|
|
70
|
+
"celery": (
|
|
71
|
+
"Celery",
|
|
72
|
+
"任务队列",
|
|
73
|
+
"用 Celery 跑后台任务,还需要一台帮它排队的中转服务(通常是 Redis)。",
|
|
74
|
+
),
|
|
75
|
+
# --- Auth / monitoring / dev ---
|
|
76
|
+
"firebase_admin": (
|
|
77
|
+
"Firebase",
|
|
78
|
+
"后端服务",
|
|
79
|
+
"对接 Google 的 Firebase,需要一个 Firebase 项目和它的密钥文件。",
|
|
80
|
+
),
|
|
81
|
+
"sentry_sdk": (
|
|
82
|
+
"Sentry",
|
|
83
|
+
"监控",
|
|
84
|
+
"把程序报错自动上报到 Sentry 看板,需要先注册 Sentry 拿一个上报地址。",
|
|
85
|
+
),
|
|
86
|
+
"github": ("GitHub API", "开发者平台", "调用 GitHub API,私有操作需要 token。"),
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# A few high-signal hostnames for when the SDK isn't imported but the API is hit.
|
|
90
|
+
_SERVICE_BY_HOST: dict[str, tuple[str, str, str]] = {
|
|
91
|
+
"api.openai.com": ("OpenAI", "AI / 大模型", "直接请求 OpenAI 接口,需要 OpenAI API key。"),
|
|
92
|
+
"api.anthropic.com": ("Anthropic", "AI / 大模型", "直接请求 Anthropic 接口,需要 API key。"),
|
|
93
|
+
"api.stripe.com": ("Stripe", "支付", "直接请求 Stripe 接口,需要密钥。"),
|
|
94
|
+
"amazonaws.com": ("AWS", "云服务", "请求 AWS 服务,需要 AWS 账号与密钥。"),
|
|
95
|
+
"hooks.slack.com": ("Slack", "通讯", "把消息自动发到 Slack 群里。"),
|
|
96
|
+
"api.telegram.org": (
|
|
97
|
+
"Telegram",
|
|
98
|
+
"通讯",
|
|
99
|
+
"用 Telegram 机器人发消息,需要先找 Telegram 申请一个机器人密钥。",
|
|
100
|
+
),
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
_CODE_EXTS = {"py", "pyi", "js", "jsx", "ts", "tsx", "mjs", "cjs", "go", "java", "rb"}
|
|
104
|
+
_TEST_SEGMENTS = {"test", "tests", "__tests__", "spec", "specs"}
|
|
105
|
+
|
|
106
|
+
# python `import a.b` / `from a.b import` ; js `from 'x'` / require('x')
|
|
107
|
+
_PY_IMPORT = re.compile(r"^\s*(?:from|import)\s+([a-zA-Z0-9_.]+)")
|
|
108
|
+
_JS_IMPORT = re.compile(r"""(?:from|require\(|import)\s*['"]([^'"]+)['"]""")
|
|
109
|
+
_GO_IMPORT = re.compile(r"""^\s*(?:_\s+)?["']([^"']+)["']""")
|
|
110
|
+
_HOST = re.compile(r"https?://([a-zA-Z0-9.\-]+)")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _ext(path: str) -> str:
|
|
114
|
+
name = path.rsplit("/", 1)[-1].rsplit("\\", 1)[-1]
|
|
115
|
+
return name.rsplit(".", 1)[-1].lower() if "." in name else ""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _is_test(path: str) -> bool:
|
|
119
|
+
segs = [s for s in path.replace("\\", "/").split("/") if s]
|
|
120
|
+
if any(s in _TEST_SEGMENTS for s in segs):
|
|
121
|
+
return True
|
|
122
|
+
name = segs[-1] if segs else ""
|
|
123
|
+
stem = name.rsplit(".", 1)[0] if "." in name else name
|
|
124
|
+
return stem.startswith("test_") or stem.endswith("_test") or ".test" in name or ".spec" in name
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _module_root(token: str) -> str:
|
|
128
|
+
"""Normalize an import token to a lookup key.
|
|
129
|
+
|
|
130
|
+
`openai.types.chat` -> tries `openai.types.chat`, `openai.types`, `openai`.
|
|
131
|
+
For JS, `@slack/web-api` -> `slack`; `aws-sdk` -> `aws-sdk`.
|
|
132
|
+
"""
|
|
133
|
+
token = token.strip()
|
|
134
|
+
# JS scoped/path packages: take a meaningful segment
|
|
135
|
+
if token.startswith("@"):
|
|
136
|
+
token = token[1:]
|
|
137
|
+
token = token.split("/")[0] if "/" in token and "." not in token else token
|
|
138
|
+
return token
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _lookup(token: str) -> tuple[str, str, str] | None:
|
|
142
|
+
# try progressively shorter dotted prefixes (python style)
|
|
143
|
+
parts = token.split(".")
|
|
144
|
+
for i in range(len(parts), 0, -1):
|
|
145
|
+
key = ".".join(parts[:i])
|
|
146
|
+
if key in _SERVICE_BY_MODULE:
|
|
147
|
+
return _SERVICE_BY_MODULE[key]
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _scan_file(content: str, ext: str) -> list[tuple[str, tuple[str, str, str]]]:
|
|
152
|
+
"""Return (evidence, service-tuple) hits for one file."""
|
|
153
|
+
hits: list[tuple[str, tuple[str, str, str]]] = []
|
|
154
|
+
for raw in content.splitlines():
|
|
155
|
+
line = raw.strip()
|
|
156
|
+
if not line:
|
|
157
|
+
continue
|
|
158
|
+
tokens: list[str] = []
|
|
159
|
+
if ext in {"py", "pyi"}:
|
|
160
|
+
m = _PY_IMPORT.match(line)
|
|
161
|
+
if m:
|
|
162
|
+
tokens.append(_module_root(m.group(1)))
|
|
163
|
+
elif ext in {"js", "jsx", "ts", "tsx", "mjs", "cjs"}:
|
|
164
|
+
m = _JS_IMPORT.search(line)
|
|
165
|
+
if m and not m.group(1).startswith("."):
|
|
166
|
+
tokens.append(_module_root(m.group(1)))
|
|
167
|
+
elif ext == "go":
|
|
168
|
+
m = _GO_IMPORT.match(line)
|
|
169
|
+
if m:
|
|
170
|
+
# github.com/aws/aws-sdk-go -> check each path segment
|
|
171
|
+
tokens.extend(seg for seg in m.group(1).split("/") if seg)
|
|
172
|
+
for tok in tokens:
|
|
173
|
+
svc = _lookup(tok)
|
|
174
|
+
if svc:
|
|
175
|
+
hits.append((line[:80], svc))
|
|
176
|
+
# hostname signal (any line)
|
|
177
|
+
if "http" in line:
|
|
178
|
+
for host in _HOST.findall(line):
|
|
179
|
+
for known, svc in _SERVICE_BY_HOST.items():
|
|
180
|
+
if host.endswith(known):
|
|
181
|
+
hits.append((line[:80], svc))
|
|
182
|
+
return hits
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def detect_external_services(file_contents: dict[str, str], *, limit: int = 20) -> dict:
|
|
186
|
+
"""Detect the external services a codebase talks to.
|
|
187
|
+
|
|
188
|
+
Returns::
|
|
189
|
+
|
|
190
|
+
{
|
|
191
|
+
"total": int, # distinct services
|
|
192
|
+
"services": [ # one entry per distinct service
|
|
193
|
+
{"name", "category", "note", "file_count", "example"}, ...
|
|
194
|
+
],
|
|
195
|
+
"categories": {cat: count},
|
|
196
|
+
"notes": [str, ...],
|
|
197
|
+
}
|
|
198
|
+
"""
|
|
199
|
+
# name -> {category, note, files:set, example}
|
|
200
|
+
found: dict[str, dict] = {}
|
|
201
|
+
for path, content in file_contents.items():
|
|
202
|
+
if not content or _is_test(path):
|
|
203
|
+
continue
|
|
204
|
+
ext = _ext(path)
|
|
205
|
+
if ext not in _CODE_EXTS:
|
|
206
|
+
continue
|
|
207
|
+
for evidence, (name, category, note) in _scan_file(content, ext):
|
|
208
|
+
entry = found.setdefault(
|
|
209
|
+
name, {"category": category, "note": note, "files": set(), "example": evidence}
|
|
210
|
+
)
|
|
211
|
+
entry["files"].add(path)
|
|
212
|
+
|
|
213
|
+
services = [
|
|
214
|
+
{
|
|
215
|
+
"name": name,
|
|
216
|
+
"category": info["category"],
|
|
217
|
+
"note": info["note"],
|
|
218
|
+
"file_count": len(info["files"]),
|
|
219
|
+
"example": info["example"],
|
|
220
|
+
}
|
|
221
|
+
for name, info in found.items()
|
|
222
|
+
]
|
|
223
|
+
# most-used first, then name for stability
|
|
224
|
+
services.sort(key=lambda s: (-s["file_count"], s["name"]))
|
|
225
|
+
|
|
226
|
+
categories: dict[str, int] = {}
|
|
227
|
+
for s in services:
|
|
228
|
+
categories[s["category"]] = categories.get(s["category"], 0) + 1
|
|
229
|
+
|
|
230
|
+
notes: list[str] = []
|
|
231
|
+
if not services:
|
|
232
|
+
notes.append("没有发现明显的外部服务依赖,项目大概率能离线/本地跑起来。")
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
"total": len(services),
|
|
236
|
+
"services": services[:limit],
|
|
237
|
+
"categories": categories,
|
|
238
|
+
"notes": notes,
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def render_integrations_markdown(project_name: str, data: dict | None) -> str:
|
|
243
|
+
"""Render the external-services map as a Markdown section, or ``""``."""
|
|
244
|
+
d = data or {}
|
|
245
|
+
total = d.get("total", 0)
|
|
246
|
+
if not total:
|
|
247
|
+
return ""
|
|
248
|
+
|
|
249
|
+
lines = [
|
|
250
|
+
f"# {project_name} — 外部服务依赖",
|
|
251
|
+
"",
|
|
252
|
+
f"> 这个项目用到了 {total} 个外部服务。想在自己电脑上把它跑起来,你大概率得先去注册"
|
|
253
|
+
"下面这些服务、拿到它们给的密钥或账号(「密钥 / API key」就是一串用来证明身份的字符;"
|
|
254
|
+
"有些服务会按使用量收费)。",
|
|
255
|
+
"",
|
|
256
|
+
"## 需要准备的外部服务",
|
|
257
|
+
"",
|
|
258
|
+
]
|
|
259
|
+
for s in d.get("services") or []:
|
|
260
|
+
where = f"({s['file_count']} 个文件用到)" if s["file_count"] > 1 else ""
|
|
261
|
+
lines.append(f"- **{s['name']}**({s['category']}){where} — {s['note']}")
|
|
262
|
+
|
|
263
|
+
return "\n".join(lines).rstrip() + "\n"
|