gitinstall 1.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 (59) hide show
  1. gitinstall/__init__.py +61 -0
  2. gitinstall/_sdk.py +541 -0
  3. gitinstall/academic.py +831 -0
  4. gitinstall/admin.html +327 -0
  5. gitinstall/auto_update.py +384 -0
  6. gitinstall/autopilot.py +349 -0
  7. gitinstall/badge.py +476 -0
  8. gitinstall/checkpoint.py +330 -0
  9. gitinstall/cicd.py +499 -0
  10. gitinstall/clawhub.html +718 -0
  11. gitinstall/config_schema.py +353 -0
  12. gitinstall/db.py +984 -0
  13. gitinstall/db_backend.py +445 -0
  14. gitinstall/dep_chain.py +337 -0
  15. gitinstall/dependency_audit.py +1153 -0
  16. gitinstall/detector.py +542 -0
  17. gitinstall/doctor.py +493 -0
  18. gitinstall/education.py +869 -0
  19. gitinstall/enterprise.py +802 -0
  20. gitinstall/error_fixer.py +953 -0
  21. gitinstall/event_bus.py +251 -0
  22. gitinstall/executor.py +577 -0
  23. gitinstall/feature_flags.py +138 -0
  24. gitinstall/fetcher.py +921 -0
  25. gitinstall/huggingface.py +922 -0
  26. gitinstall/hw_detect.py +988 -0
  27. gitinstall/i18n.py +664 -0
  28. gitinstall/installer_registry.py +362 -0
  29. gitinstall/knowledge_base.py +379 -0
  30. gitinstall/license_check.py +605 -0
  31. gitinstall/llm.py +569 -0
  32. gitinstall/log.py +236 -0
  33. gitinstall/main.py +1408 -0
  34. gitinstall/mcp_agent.py +841 -0
  35. gitinstall/mcp_server.py +386 -0
  36. gitinstall/monorepo.py +810 -0
  37. gitinstall/multi_source.py +425 -0
  38. gitinstall/onboard.py +276 -0
  39. gitinstall/planner.py +222 -0
  40. gitinstall/planner_helpers.py +323 -0
  41. gitinstall/planner_known_projects.py +1010 -0
  42. gitinstall/planner_templates.py +996 -0
  43. gitinstall/remote_gpu.py +633 -0
  44. gitinstall/resilience.py +608 -0
  45. gitinstall/run_tests.py +572 -0
  46. gitinstall/skills.py +476 -0
  47. gitinstall/tool_schemas.py +324 -0
  48. gitinstall/trending.py +279 -0
  49. gitinstall/uninstaller.py +415 -0
  50. gitinstall/validate_top100.py +607 -0
  51. gitinstall/watchdog.py +180 -0
  52. gitinstall/web.py +1277 -0
  53. gitinstall/web_ui.html +2277 -0
  54. gitinstall-1.1.0.dist-info/METADATA +275 -0
  55. gitinstall-1.1.0.dist-info/RECORD +59 -0
  56. gitinstall-1.1.0.dist-info/WHEEL +5 -0
  57. gitinstall-1.1.0.dist-info/entry_points.txt +3 -0
  58. gitinstall-1.1.0.dist-info/licenses/LICENSE +21 -0
  59. gitinstall-1.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,425 @@
1
+ """
2
+ multi_source.py - 多源代码托管平台支持
3
+ ========================================
4
+
5
+ 灵感来源:OpenClaw 22+ 渠道集成模式
6
+
7
+ 支持从多个代码托管平台安装项目:
8
+ 1. GitHub (github.com) — 主力支持
9
+ 2. GitLab (gitlab.com) — 完整支持
10
+ 3. Bitbucket (bitbucket.org) — 完整支持
11
+ 4. Gitee (gitee.com) — 国内镜像
12
+ 5. Codeberg (codeberg.org) — 开源替代
13
+ 6. 自定义 Git URL — 通用 git clone
14
+
15
+ 架构:
16
+ 统一的 SourceProvider 接口,每个平台一个实现。
17
+ 自动从 URL 识别平台 → 路由到对应 Provider。
18
+
19
+ 零外部依赖,纯 Python 标准库。
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import os
26
+ import re
27
+ import urllib.error
28
+ import urllib.parse
29
+ import urllib.request
30
+ from abc import ABC, abstractmethod
31
+ from dataclasses import dataclass, field
32
+ from typing import Optional
33
+
34
+
35
+ @dataclass
36
+ class RepoMetadata:
37
+ """统一的仓库元数据"""
38
+ platform: str # github, gitlab, bitbucket, gitee, codeberg
39
+ owner: str
40
+ repo: str
41
+ full_name: str # owner/repo
42
+ description: str = ""
43
+ stars: int = 0
44
+ language: str = ""
45
+ clone_url: str = ""
46
+ homepage: str = ""
47
+ license: str = ""
48
+ default_branch: str = "main"
49
+ topics: list[str] = field(default_factory=list)
50
+ is_fork: bool = False
51
+ is_archived: bool = False
52
+ last_push: str = ""
53
+
54
+
55
+ class SourceProvider(ABC):
56
+ """代码托管平台统一接口"""
57
+
58
+ platform_name: str = ""
59
+ api_base: str = ""
60
+ raw_base: str = ""
61
+
62
+ @abstractmethod
63
+ def get_repo_metadata(self, owner: str, repo: str) -> RepoMetadata:
64
+ """获取仓库元数据"""
65
+
66
+ @abstractmethod
67
+ def get_readme(self, owner: str, repo: str, branch: str = None) -> str:
68
+ """获取 README 内容"""
69
+
70
+ @abstractmethod
71
+ def get_file_content(self, owner: str, repo: str, path: str, branch: str = None) -> str:
72
+ """获取指定文件内容"""
73
+
74
+ def get_clone_url(self, owner: str, repo: str) -> str:
75
+ """获取 clone URL"""
76
+ return f"https://{self.platform_name}/{owner}/{repo}.git"
77
+
78
+ def _api_get(self, url: str, headers: dict = None) -> dict:
79
+ """通用 API GET 请求"""
80
+ hdrs = {"User-Agent": "gitinstall/1.0", "Accept": "application/json"}
81
+ if headers:
82
+ hdrs.update(headers)
83
+ req = urllib.request.Request(url, headers=hdrs)
84
+ try:
85
+ with urllib.request.urlopen(req, timeout=15) as resp:
86
+ return json.loads(resp.read().decode())
87
+ except urllib.error.HTTPError as e:
88
+ if e.code == 404:
89
+ raise FileNotFoundError(f"仓库不存在: {url}")
90
+ raise ConnectionError(f"API 请求失败 ({e.code}): {url}")
91
+ except urllib.error.URLError as e:
92
+ raise ConnectionError(f"网络错误: {e}")
93
+
94
+ def _raw_get(self, url: str) -> str:
95
+ """获取原始文本内容"""
96
+ hdrs = {"User-Agent": "gitinstall/1.0"}
97
+ req = urllib.request.Request(url, headers=hdrs)
98
+ try:
99
+ with urllib.request.urlopen(req, timeout=15) as resp:
100
+ return resp.read().decode(errors="replace")
101
+ except Exception:
102
+ return ""
103
+
104
+
105
+ # ─────────────────────────────────────────────
106
+ # GitHub Provider
107
+ # ─────────────────────────────────────────────
108
+
109
+ class GitHubProvider(SourceProvider):
110
+ platform_name = "github.com"
111
+ api_base = "https://api.github.com"
112
+ raw_base = "https://raw.githubusercontent.com"
113
+
114
+ def __init__(self):
115
+ self._token = os.getenv("GITHUB_TOKEN", "").strip()
116
+
117
+ def _auth_headers(self) -> dict:
118
+ h = {}
119
+ if self._token:
120
+ h["Authorization"] = f"token {self._token}"
121
+ return h
122
+
123
+ def get_repo_metadata(self, owner: str, repo: str) -> RepoMetadata:
124
+ url = f"{self.api_base}/repos/{owner}/{repo}"
125
+ data = self._api_get(url, self._auth_headers())
126
+ return RepoMetadata(
127
+ platform="github",
128
+ owner=data.get("owner", {}).get("login", owner),
129
+ repo=data.get("name", repo),
130
+ full_name=data.get("full_name", f"{owner}/{repo}"),
131
+ description=data.get("description", "") or "",
132
+ stars=data.get("stargazers_count", 0),
133
+ language=data.get("language", "") or "",
134
+ clone_url=data.get("clone_url", self.get_clone_url(owner, repo)),
135
+ homepage=data.get("homepage", "") or "",
136
+ license=(data.get("license") or {}).get("spdx_id", ""),
137
+ default_branch=data.get("default_branch", "main"),
138
+ topics=data.get("topics", []),
139
+ is_fork=data.get("fork", False),
140
+ is_archived=data.get("archived", False),
141
+ last_push=data.get("pushed_at", ""),
142
+ )
143
+
144
+ def get_readme(self, owner: str, repo: str, branch: str = None) -> str:
145
+ branch = branch or "main"
146
+ for fname in ["README.md", "README.rst", "README.txt", "README", "readme.md"]:
147
+ content = self._raw_get(f"{self.raw_base}/{owner}/{repo}/{branch}/{fname}")
148
+ if content:
149
+ return content[:50000]
150
+ return ""
151
+
152
+ def get_file_content(self, owner: str, repo: str, path: str, branch: str = None) -> str:
153
+ branch = branch or "main"
154
+ return self._raw_get(f"{self.raw_base}/{owner}/{repo}/{branch}/{path}")
155
+
156
+
157
+ # ─────────────────────────────────────────────
158
+ # GitLab Provider
159
+ # ─────────────────────────────────────────────
160
+
161
+ class GitLabProvider(SourceProvider):
162
+ platform_name = "gitlab.com"
163
+ api_base = "https://gitlab.com/api/v4"
164
+
165
+ def __init__(self):
166
+ self._token = os.getenv("GITLAB_TOKEN", "").strip()
167
+
168
+ def _auth_headers(self) -> dict:
169
+ h = {}
170
+ if self._token:
171
+ h["PRIVATE-TOKEN"] = self._token
172
+ return h
173
+
174
+ def _project_id(self, owner: str, repo: str) -> str:
175
+ return urllib.parse.quote(f"{owner}/{repo}", safe="")
176
+
177
+ def get_repo_metadata(self, owner: str, repo: str) -> RepoMetadata:
178
+ pid = self._project_id(owner, repo)
179
+ url = f"{self.api_base}/projects/{pid}"
180
+ data = self._api_get(url, self._auth_headers())
181
+ ns = data.get("namespace", {})
182
+ return RepoMetadata(
183
+ platform="gitlab",
184
+ owner=ns.get("path", owner),
185
+ repo=data.get("path", repo),
186
+ full_name=data.get("path_with_namespace", f"{owner}/{repo}"),
187
+ description=data.get("description", "") or "",
188
+ stars=data.get("star_count", 0),
189
+ language="", # GitLab API 需要额外请求获取语言
190
+ clone_url=data.get("http_url_to_repo", self.get_clone_url(owner, repo)),
191
+ homepage=data.get("web_url", ""),
192
+ license="",
193
+ default_branch=data.get("default_branch", "main"),
194
+ topics=data.get("topics", []) or data.get("tag_list", []),
195
+ is_fork=data.get("forked_from_project") is not None,
196
+ is_archived=data.get("archived", False),
197
+ last_push=data.get("last_activity_at", ""),
198
+ )
199
+
200
+ def get_readme(self, owner: str, repo: str, branch: str = None) -> str:
201
+ pid = self._project_id(owner, repo)
202
+ branch = branch or "main"
203
+ for fname in ["README.md", "README.rst", "README.txt", "README"]:
204
+ encoded = urllib.parse.quote(fname, safe="")
205
+ url = f"{self.api_base}/projects/{pid}/repository/files/{encoded}/raw?ref={branch}"
206
+ content = self._raw_get(url)
207
+ if content:
208
+ return content[:50000]
209
+ return ""
210
+
211
+ def get_file_content(self, owner: str, repo: str, path: str, branch: str = None) -> str:
212
+ pid = self._project_id(owner, repo)
213
+ branch = branch or "main"
214
+ encoded = urllib.parse.quote(path, safe="")
215
+ url = f"{self.api_base}/projects/{pid}/repository/files/{encoded}/raw?ref={branch}"
216
+ return self._raw_get(url)
217
+
218
+
219
+ # ─────────────────────────────────────────────
220
+ # Bitbucket Provider
221
+ # ─────────────────────────────────────────────
222
+
223
+ class BitbucketProvider(SourceProvider):
224
+ platform_name = "bitbucket.org"
225
+ api_base = "https://api.bitbucket.org/2.0"
226
+
227
+ def get_repo_metadata(self, owner: str, repo: str) -> RepoMetadata:
228
+ url = f"{self.api_base}/repositories/{owner}/{repo}"
229
+ data = self._api_get(url)
230
+ return RepoMetadata(
231
+ platform="bitbucket",
232
+ owner=owner,
233
+ repo=repo,
234
+ full_name=data.get("full_name", f"{owner}/{repo}"),
235
+ description=data.get("description", "") or "",
236
+ stars=0, # Bitbucket 不公开 star 数
237
+ language=data.get("language", "") or "",
238
+ clone_url=f"https://bitbucket.org/{owner}/{repo}.git",
239
+ homepage=data.get("website", "") or "",
240
+ license="",
241
+ default_branch=data.get("mainbranch", {}).get("name", "main"),
242
+ is_fork=data.get("parent") is not None,
243
+ )
244
+
245
+ def get_readme(self, owner: str, repo: str, branch: str = None) -> str:
246
+ branch = branch or "main"
247
+ for fname in ["README.md", "README.rst", "README.txt", "README"]:
248
+ url = f"https://bitbucket.org/{owner}/{repo}/raw/{branch}/{fname}"
249
+ content = self._raw_get(url)
250
+ if content:
251
+ return content[:50000]
252
+ return ""
253
+
254
+ def get_file_content(self, owner: str, repo: str, path: str, branch: str = None) -> str:
255
+ branch = branch or "main"
256
+ url = f"https://bitbucket.org/{owner}/{repo}/raw/{branch}/{path}"
257
+ return self._raw_get(url)
258
+
259
+
260
+ # ─────────────────────────────────────────────
261
+ # Gitee Provider (国内镜像)
262
+ # ─────────────────────────────────────────────
263
+
264
+ class GiteeProvider(SourceProvider):
265
+ platform_name = "gitee.com"
266
+ api_base = "https://gitee.com/api/v5"
267
+
268
+ def __init__(self):
269
+ self._token = os.getenv("GITEE_TOKEN", "").strip()
270
+
271
+ def _auth_params(self) -> str:
272
+ if self._token:
273
+ return f"?access_token={self._token}"
274
+ return ""
275
+
276
+ def get_repo_metadata(self, owner: str, repo: str) -> RepoMetadata:
277
+ url = f"{self.api_base}/repos/{owner}/{repo}{self._auth_params()}"
278
+ data = self._api_get(url)
279
+ return RepoMetadata(
280
+ platform="gitee",
281
+ owner=data.get("owner", {}).get("login", owner),
282
+ repo=data.get("path", repo),
283
+ full_name=data.get("full_name", f"{owner}/{repo}"),
284
+ description=data.get("description", "") or "",
285
+ stars=data.get("stargazers_count", 0),
286
+ language=data.get("language", "") or "",
287
+ clone_url=data.get("html_url", "") + ".git" if data.get("html_url") else self.get_clone_url(owner, repo),
288
+ homepage=data.get("homepage", "") or "",
289
+ license=(data.get("license") or ""),
290
+ default_branch=data.get("default_branch", "master"),
291
+ )
292
+
293
+ def get_readme(self, owner: str, repo: str, branch: str = None) -> str:
294
+ branch = branch or "master"
295
+ for fname in ["README.md", "README.rst", "README.txt", "README"]:
296
+ url = f"https://gitee.com/{owner}/{repo}/raw/{branch}/{fname}"
297
+ content = self._raw_get(url)
298
+ if content:
299
+ return content[:50000]
300
+ return ""
301
+
302
+ def get_file_content(self, owner: str, repo: str, path: str, branch: str = None) -> str:
303
+ branch = branch or "master"
304
+ url = f"https://gitee.com/{owner}/{repo}/raw/{branch}/{path}"
305
+ return self._raw_get(url)
306
+
307
+
308
+ # ─────────────────────────────────────────────
309
+ # Codeberg Provider
310
+ # ─────────────────────────────────────────────
311
+
312
+ class CodebergProvider(SourceProvider):
313
+ platform_name = "codeberg.org"
314
+ api_base = "https://codeberg.org/api/v1"
315
+
316
+ def get_repo_metadata(self, owner: str, repo: str) -> RepoMetadata:
317
+ url = f"{self.api_base}/repos/{owner}/{repo}"
318
+ data = self._api_get(url)
319
+ return RepoMetadata(
320
+ platform="codeberg",
321
+ owner=data.get("owner", {}).get("login", owner),
322
+ repo=data.get("name", repo),
323
+ full_name=data.get("full_name", f"{owner}/{repo}"),
324
+ description=data.get("description", "") or "",
325
+ stars=data.get("stars_count", 0),
326
+ language=data.get("language", "") or "",
327
+ clone_url=data.get("clone_url", self.get_clone_url(owner, repo)),
328
+ homepage=data.get("website", "") or "",
329
+ default_branch=data.get("default_branch", "main"),
330
+ is_fork=data.get("fork", False),
331
+ is_archived=data.get("archived", False),
332
+ )
333
+
334
+ def get_readme(self, owner: str, repo: str, branch: str = None) -> str:
335
+ branch = branch or "main"
336
+ url = f"https://codeberg.org/{owner}/{repo}/raw/branch/{branch}/README.md"
337
+ content = self._raw_get(url)
338
+ if content:
339
+ return content[:50000]
340
+ return ""
341
+
342
+ def get_file_content(self, owner: str, repo: str, path: str, branch: str = None) -> str:
343
+ branch = branch or "main"
344
+ url = f"https://codeberg.org/{owner}/{repo}/raw/branch/{branch}/{path}"
345
+ return self._raw_get(url)
346
+
347
+
348
+ # ─────────────────────────────────────────────
349
+ # 平台自动识别 + 路由
350
+ # ─────────────────────────────────────────────
351
+
352
+ # 平台 URL 模式 → Provider 映射
353
+ _PLATFORM_PATTERNS = [
354
+ (r'github\.com[:/]([^/]+)/([^/\s\.]+)', "github"),
355
+ (r'gitlab\.com[:/]([^/]+)/([^/\s\.]+)', "gitlab"),
356
+ (r'bitbucket\.org[:/]([^/]+)/([^/\s\.]+)', "bitbucket"),
357
+ (r'gitee\.com[:/]([^/]+)/([^/\s\.]+)', "gitee"),
358
+ (r'codeberg\.org[:/]([^/]+)/([^/\s\.]+)', "codeberg"),
359
+ ]
360
+
361
+ _PROVIDERS = {
362
+ "github": GitHubProvider,
363
+ "gitlab": GitLabProvider,
364
+ "bitbucket": BitbucketProvider,
365
+ "gitee": GiteeProvider,
366
+ "codeberg": CodebergProvider,
367
+ }
368
+
369
+
370
+ def detect_platform(identifier: str) -> tuple[str, str, str]:
371
+ """
372
+ 从 URL 或标识符自动检测平台。
373
+
374
+ 返回:(platform, owner, repo)
375
+ platform 为 "github", "gitlab", "bitbucket", "gitee", "codeberg" 之一,
376
+ 未识别时默认 "github"。
377
+ """
378
+ identifier = identifier.strip().rstrip("/")
379
+
380
+ for pattern, platform in _PLATFORM_PATTERNS:
381
+ match = re.search(pattern, identifier, re.IGNORECASE)
382
+ if match:
383
+ owner = match.group(1)
384
+ repo = match.group(2).removesuffix(".git")
385
+ return platform, owner, repo
386
+
387
+ # 没有匹配任何平台 URL → 假设是 GitHub 的 owner/repo
388
+ if "/" in identifier and not identifier.startswith("http"):
389
+ parts = identifier.split("/")
390
+ if len(parts) >= 2:
391
+ return "github", parts[0], parts[1]
392
+
393
+ # 仅项目名
394
+ return "github", "", identifier
395
+
396
+
397
+ def get_provider(platform: str) -> SourceProvider:
398
+ """获取对应平台的 Provider 实例"""
399
+ cls = _PROVIDERS.get(platform)
400
+ if not cls:
401
+ raise ValueError(f"不支持的平台: {platform}(支持: {', '.join(_PROVIDERS.keys())})")
402
+ return cls()
403
+
404
+
405
+ def fetch_from_any_source(identifier: str) -> RepoMetadata:
406
+ """
407
+ 从任意源获取仓库元数据。
408
+
409
+ 自动检测平台并路由到对应 Provider。
410
+ 支持: GitHub, GitLab, Bitbucket, Gitee, Codeberg URL 或 owner/repo 格式。
411
+ """
412
+ platform, owner, repo = detect_platform(identifier)
413
+ provider = get_provider(platform)
414
+ return provider.get_repo_metadata(owner, repo)
415
+
416
+
417
+ def get_supported_platforms() -> list[dict]:
418
+ """返回支持的平台列表"""
419
+ return [
420
+ {"name": "GitHub", "domain": "github.com", "key": "github", "env_token": "GITHUB_TOKEN"},
421
+ {"name": "GitLab", "domain": "gitlab.com", "key": "gitlab", "env_token": "GITLAB_TOKEN"},
422
+ {"name": "Bitbucket", "domain": "bitbucket.org", "key": "bitbucket", "env_token": ""},
423
+ {"name": "Gitee", "domain": "gitee.com", "key": "gitee", "env_token": "GITEE_TOKEN"},
424
+ {"name": "Codeberg", "domain": "codeberg.org", "key": "codeberg", "env_token": ""},
425
+ ]