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,445 @@
1
+ """
2
+ db_backend.py - 数据库抽象后端
3
+ =====================================
4
+
5
+ 支持 SQLite(默认)→ PostgreSQL 等后端无缝切换。
6
+ 环境变量 GITINSTALL_DB_BACKEND 控制后端类型:
7
+ - "sqlite" (默认) 使用本地文件 ~/.gitinstall/data.db
8
+ - "postgresql" 连接 GITINSTALL_DATABASE_URL
9
+
10
+ 面向大众原则:零配置即可用 SQLite,生产部署时切换 PostgreSQL。
11
+ 零外部依赖:SQLite 使用 stdlib,PostgreSQL 需安装 psycopg2。
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ import re
18
+ import sqlite3
19
+ import threading
20
+ from abc import ABC, abstractmethod
21
+ from contextlib import contextmanager
22
+ from pathlib import Path
23
+ from typing import Any, Optional
24
+
25
+
26
+ # ─────────────────────────────────────────────
27
+ # 抽象后端接口
28
+ # ─────────────────────────────────────────────
29
+
30
+ class DatabaseBackend(ABC):
31
+ """数据库后端抽象接口。
32
+
33
+ 所有操作通过 execute/executemany/executescript 统一调用。
34
+ 使用 '?' 占位符(SQLite 风格),PostgreSQL 后端自动转换为 '%s'。
35
+ """
36
+
37
+ @abstractmethod
38
+ def get_connection(self) -> Any:
39
+ """获取当前线程的数据库连接"""
40
+
41
+ @abstractmethod
42
+ @contextmanager
43
+ def transaction(self):
44
+ """事务上下文管理器,yield connection"""
45
+
46
+ @abstractmethod
47
+ def execute(self, sql: str, params: tuple = ()) -> Any:
48
+ """执行单条 SQL,返回 cursor"""
49
+
50
+ @abstractmethod
51
+ def executemany(self, sql: str, params_list: list[tuple]) -> Any:
52
+ """批量执行 SQL"""
53
+
54
+ @abstractmethod
55
+ def executescript(self, sql: str) -> None:
56
+ """执行多条 SQL 脚本(DDL 初始化用)"""
57
+
58
+ @abstractmethod
59
+ def fetchone(self, sql: str, params: tuple = ()) -> Optional[dict]:
60
+ """执行查询返回单行(dict 或 None)"""
61
+
62
+ @abstractmethod
63
+ def fetchall(self, sql: str, params: tuple = ()) -> list[dict]:
64
+ """执行查询返回所有行(list[dict])"""
65
+
66
+ @abstractmethod
67
+ def close(self) -> None:
68
+ """关闭连接"""
69
+
70
+ @abstractmethod
71
+ def integrity_check(self) -> str:
72
+ """数据库完整性检查,返回 'ok' 或错误描述"""
73
+
74
+ @abstractmethod
75
+ def table_row_count(self, table: str) -> int:
76
+ """返回指定表的行数(仅诊断用)"""
77
+
78
+ @property
79
+ @abstractmethod
80
+ def backend_type(self) -> str:
81
+ """返回后端类型标识,如 'sqlite' 或 'postgresql'"""
82
+
83
+ def adapt_schema(self, sqlite_schema: str) -> str:
84
+ """将 SQLite 风格 schema 转换为当前后端语法。
85
+ 默认原样返回(SQLite 无需转换)。"""
86
+ return sqlite_schema
87
+
88
+
89
+ # ─────────────────────────────────────────────
90
+ # SQLite 后端实现
91
+ # ─────────────────────────────────────────────
92
+
93
+ class SQLiteBackend(DatabaseBackend):
94
+ """线程安全 SQLite 后端,每线程一个连接。"""
95
+
96
+ def __init__(self, db_path: Optional[str] = None):
97
+ if db_path is None:
98
+ db_dir = Path.home() / ".gitinstall"
99
+ db_dir.mkdir(parents=True, exist_ok=True)
100
+ try:
101
+ os.chmod(db_dir, 0o700)
102
+ except OSError:
103
+ pass
104
+ self._db_path = str(db_dir / "data.db")
105
+ else:
106
+ self._db_path = db_path
107
+ self._local = threading.local()
108
+
109
+ @property
110
+ def backend_type(self) -> str:
111
+ return "sqlite"
112
+
113
+ def get_connection(self) -> sqlite3.Connection:
114
+ conn = getattr(self._local, "conn", None)
115
+ if conn is None:
116
+ conn = sqlite3.connect(self._db_path, timeout=10)
117
+ conn.row_factory = sqlite3.Row
118
+ conn.execute("PRAGMA journal_mode=WAL")
119
+ conn.execute("PRAGMA busy_timeout=5000")
120
+ try:
121
+ if os.path.exists(self._db_path):
122
+ os.chmod(self._db_path, 0o600)
123
+ except OSError:
124
+ pass
125
+ self._local.conn = conn
126
+ return conn
127
+
128
+ @contextmanager
129
+ def transaction(self):
130
+ conn = self.get_connection()
131
+ try:
132
+ yield conn
133
+ conn.commit()
134
+ except Exception:
135
+ conn.rollback()
136
+ raise
137
+
138
+ def execute(self, sql: str, params: tuple = ()) -> Any:
139
+ return self.get_connection().execute(sql, params)
140
+
141
+ def executemany(self, sql: str, params_list: list[tuple]) -> Any:
142
+ return self.get_connection().executemany(sql, params_list)
143
+
144
+ def executescript(self, sql: str) -> None:
145
+ self.get_connection().executescript(sql)
146
+
147
+ def fetchone(self, sql: str, params: tuple = ()) -> Optional[dict]:
148
+ row = self.get_connection().execute(sql, params).fetchone()
149
+ return dict(row) if row else None
150
+
151
+ def fetchall(self, sql: str, params: tuple = ()) -> list[dict]:
152
+ rows = self.get_connection().execute(sql, params).fetchall()
153
+ return [dict(r) for r in rows]
154
+
155
+ def close(self) -> None:
156
+ conn = getattr(self._local, "conn", None)
157
+ if conn:
158
+ conn.close()
159
+ self._local.conn = None
160
+
161
+ def integrity_check(self) -> str:
162
+ try:
163
+ row = self.get_connection().execute("PRAGMA integrity_check").fetchone()
164
+ return row[0] if row else "unknown"
165
+ except Exception as e:
166
+ return str(e)
167
+
168
+ def table_row_count(self, table: str) -> int:
169
+ # 防注入:仅允许字母、数字、下划线
170
+ import re
171
+ if not re.match(r'^[a-zA-Z_]\w*$', table):
172
+ raise ValueError(f"Invalid table name: {table}")
173
+ row = self.get_connection().execute(
174
+ f"SELECT COUNT(*) FROM {table}"
175
+ ).fetchone()
176
+ return row[0] if row else 0
177
+
178
+
179
+ # ─────────────────────────────────────────────
180
+ # PostgreSQL 后端实现(占位,需要 psycopg2)
181
+ # ─────────────────────────────────────────────
182
+
183
+ class _PGCursorProxy:
184
+ """包装 psycopg2 cursor,使 fetchone 返回 dict(兼容 sqlite3.Row)"""
185
+
186
+ def __init__(self, cursor):
187
+ self._cur = cursor
188
+
189
+ def fetchone(self):
190
+ row = self._cur.fetchone()
191
+ return dict(row) if row else None
192
+
193
+ def fetchall(self):
194
+ return [dict(r) for r in self._cur.fetchall()]
195
+
196
+ @property
197
+ def rowcount(self):
198
+ return self._cur.rowcount
199
+
200
+ @property
201
+ def lastrowid(self):
202
+ return self._cur.lastrowid
203
+
204
+ def __iter__(self):
205
+ return iter(self._cur)
206
+
207
+
208
+ class _PGConnectionProxy:
209
+ """包装 psycopg2 connection,提供 sqlite3 兼容 API。
210
+
211
+ 使 db.py 中的 conn.execute() / conn.executemany() / conn.commit()
212
+ 无需修改即可同时支持 SQLite 和 PostgreSQL。
213
+ """
214
+
215
+ def __init__(self, raw_conn, extras_module):
216
+ self._conn = raw_conn
217
+ self._extras = extras_module
218
+
219
+ def execute(self, sql: str, params: tuple = ()) -> _PGCursorProxy:
220
+ sql = _pg_adapt_sql(sql)
221
+ cur = self._conn.cursor(cursor_factory=self._extras.RealDictCursor)
222
+ cur.execute(sql, params)
223
+ return _PGCursorProxy(cur)
224
+
225
+ def executemany(self, sql: str, params_list) -> _PGCursorProxy:
226
+ sql = _pg_adapt_sql(sql)
227
+ cur = self._conn.cursor(cursor_factory=self._extras.RealDictCursor)
228
+ cur.executemany(sql, params_list)
229
+ return _PGCursorProxy(cur)
230
+
231
+ def commit(self):
232
+ self._conn.commit()
233
+
234
+ def rollback(self):
235
+ self._conn.rollback()
236
+
237
+ def close(self):
238
+ self._conn.close()
239
+
240
+ @property
241
+ def closed(self):
242
+ return self._conn.closed
243
+
244
+
245
+ def _pg_adapt_sql(sql: str) -> str:
246
+ """将 SQLite 风格 SQL 实时转为 PostgreSQL 兼容语法"""
247
+ # strftime('%s','now') → EXTRACT(EPOCH FROM NOW()) (先转,避免 % 转义干扰)
248
+ sql = re.sub(
249
+ r"strftime\('%s',\s*'now'\)",
250
+ "EXTRACT(EPOCH FROM NOW())",
251
+ sql,
252
+ )
253
+ # date(ts, 'unixepoch', 'localtime') → TO_CHAR(TO_TIMESTAMP(ts), 'YYYY-MM-DD')
254
+ sql = re.sub(
255
+ r"date\((\w+),\s*'unixepoch',\s*'localtime'\)",
256
+ r"TO_CHAR(TO_TIMESTAMP(\1), 'YYYY-MM-DD')",
257
+ sql,
258
+ )
259
+ # 转义已有的 % 为 %%(防止 LIKE 'session:%' 被误解析)
260
+ sql = sql.replace("%", "%%")
261
+ # ? → %s 占位符
262
+ sql = sql.replace("?", "%s")
263
+ # INSERT OR IGNORE → INSERT ... ON CONFLICT DO NOTHING
264
+ has_or_ignore = bool(re.search(r"INSERT\s+OR\s+IGNORE\s+INTO", sql, re.IGNORECASE))
265
+ sql = re.sub(
266
+ r"INSERT\s+OR\s+IGNORE\s+INTO",
267
+ "INSERT INTO",
268
+ sql, flags=re.IGNORECASE,
269
+ )
270
+ if has_or_ignore:
271
+ # 在 VALUES(...) 后追加 ON CONFLICT DO NOTHING
272
+ sql = re.sub(r"(\)\s*)$", r"\1 ON CONFLICT DO NOTHING", sql.rstrip())
273
+ if "ON CONFLICT DO NOTHING" not in sql:
274
+ sql = sql.rstrip(";").rstrip() + " ON CONFLICT DO NOTHING"
275
+ return sql
276
+
277
+
278
+ class PostgreSQLBackend(DatabaseBackend):
279
+ """PostgreSQL 后端。需要安装 psycopg2-binary。
280
+
281
+ 使用 GITINSTALL_DATABASE_URL 环境变量配置连接字符串,例如:
282
+ postgresql://user:pass@localhost:5432/gitinstall
283
+
284
+ get_connection() 返回 _PGConnectionProxy,提供 sqlite3 兼容 API,
285
+ 使 db.py 无需修改即可运行在 PostgreSQL 上。
286
+ """
287
+
288
+ def __init__(self, database_url: Optional[str] = None):
289
+ self._url = database_url or os.getenv("GITINSTALL_DATABASE_URL", "")
290
+ if not self._url:
291
+ raise ValueError(
292
+ "PostgreSQL backend requires GITINSTALL_DATABASE_URL environment variable"
293
+ )
294
+ self._local = threading.local()
295
+
296
+ @property
297
+ def backend_type(self) -> str:
298
+ return "postgresql"
299
+
300
+ def _import_psycopg2(self):
301
+ try:
302
+ import psycopg2
303
+ import psycopg2.extras
304
+ return psycopg2, psycopg2.extras
305
+ except ImportError:
306
+ raise ImportError(
307
+ "PostgreSQL backend requires psycopg2. "
308
+ "Install with: pip install psycopg2-binary"
309
+ )
310
+
311
+ def _raw_connection(self):
312
+ """获取原始 psycopg2 connection(内部用)"""
313
+ raw = getattr(self._local, "raw_conn", None)
314
+ if raw is None or raw.closed:
315
+ psycopg2, _ = self._import_psycopg2()
316
+ raw = psycopg2.connect(self._url)
317
+ raw.autocommit = False
318
+ self._local.raw_conn = raw
319
+ return raw
320
+
321
+ def get_connection(self) -> _PGConnectionProxy:
322
+ """返回 SQLite 兼容的连接代理"""
323
+ proxy = getattr(self._local, "conn_proxy", None)
324
+ if proxy is None or proxy.closed:
325
+ _, extras = self._import_psycopg2()
326
+ raw = self._raw_connection()
327
+ proxy = _PGConnectionProxy(raw, extras)
328
+ self._local.conn_proxy = proxy
329
+ return proxy
330
+
331
+ @contextmanager
332
+ def transaction(self):
333
+ conn = self.get_connection()
334
+ try:
335
+ yield conn
336
+ conn.commit()
337
+ except Exception:
338
+ conn.rollback()
339
+ raise
340
+
341
+ def _convert_placeholders(self, sql: str) -> str:
342
+ """将 SQLite 的 ? 占位符转换为 PostgreSQL 的 %s"""
343
+ return sql.replace("?", "%s")
344
+
345
+ def execute(self, sql: str, params: tuple = ()) -> Any:
346
+ proxy = self.get_connection()
347
+ return proxy.execute(sql, params)
348
+
349
+ def executemany(self, sql: str, params_list: list[tuple]) -> Any:
350
+ proxy = self.get_connection()
351
+ return proxy.executemany(sql, params_list)
352
+
353
+ def executescript(self, sql: str) -> None:
354
+ adapted = self.adapt_schema(sql)
355
+ raw = self._raw_connection()
356
+ cur = raw.cursor()
357
+ cur.execute(adapted)
358
+ raw.commit()
359
+
360
+ def fetchone(self, sql: str, params: tuple = ()) -> Optional[dict]:
361
+ proxy = self.get_connection()
362
+ result = proxy.execute(sql, params)
363
+ return result.fetchone()
364
+
365
+ def fetchall(self, sql: str, params: tuple = ()) -> list[dict]:
366
+ proxy = self.get_connection()
367
+ result = proxy.execute(sql, params)
368
+ return result.fetchall()
369
+
370
+ def close(self) -> None:
371
+ raw = getattr(self._local, "raw_conn", None)
372
+ if raw:
373
+ raw.close()
374
+ self._local.raw_conn = None
375
+ self._local.conn_proxy = None
376
+
377
+ def integrity_check(self) -> str:
378
+ try:
379
+ self.execute("SELECT 1")
380
+ return "ok"
381
+ except Exception as e:
382
+ return str(e)
383
+
384
+ def table_row_count(self, table: str) -> int:
385
+ import re
386
+ if not re.match(r'^[a-zA-Z_]\w*$', table):
387
+ raise ValueError(f"Invalid table name: {table}")
388
+ row = self.fetchone(f"SELECT COUNT(*) as cnt FROM {table}")
389
+ return row["cnt"] if row else 0
390
+
391
+ def adapt_schema(self, sqlite_schema: str) -> str:
392
+ """将 SQLite schema SQL 转为 PostgreSQL 兼容语法"""
393
+ import re
394
+ sql = sqlite_schema
395
+ # INTEGER PRIMARY KEY AUTOINCREMENT → SERIAL PRIMARY KEY
396
+ sql = re.sub(
397
+ r'INTEGER\s+PRIMARY\s+KEY\s+AUTOINCREMENT',
398
+ 'SERIAL PRIMARY KEY',
399
+ sql, flags=re.IGNORECASE,
400
+ )
401
+ # REAL DEFAULT (strftime('%s','now')) → DOUBLE PRECISION DEFAULT EXTRACT(EPOCH FROM NOW())
402
+ sql = re.sub(
403
+ r"REAL\s+NOT\s+NULL\s+DEFAULT\s+\(strftime\('%s','now'\)\)",
404
+ "DOUBLE PRECISION NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())",
405
+ sql, flags=re.IGNORECASE,
406
+ )
407
+ # REAL → DOUBLE PRECISION (remaining)
408
+ sql = re.sub(r'\bREAL\b', 'DOUBLE PRECISION', sql, flags=re.IGNORECASE)
409
+ # INTEGER → INTEGER (compatible, no change needed)
410
+ return sql
411
+
412
+
413
+ # ─────────────────────────────────────────────
414
+ # 后端工厂
415
+ # ─────────────────────────────────────────────
416
+
417
+ _backend: Optional[DatabaseBackend] = None
418
+ _backend_lock = threading.Lock()
419
+
420
+
421
+ def get_backend() -> DatabaseBackend:
422
+ """获取当前数据库后端(单例)。
423
+
424
+ 通过环境变量 GITINSTALL_DB_BACKEND 选择:
425
+ - "sqlite" (默认) 本地 SQLite
426
+ - "postgresql" PostgreSQL(需要 psycopg2 + GITINSTALL_DATABASE_URL)
427
+ """
428
+ global _backend
429
+ if _backend is not None:
430
+ return _backend
431
+ with _backend_lock:
432
+ if _backend is not None:
433
+ return _backend
434
+ backend_type = os.getenv("GITINSTALL_DB_BACKEND", "sqlite").lower()
435
+ if backend_type == "postgresql":
436
+ _backend = PostgreSQLBackend()
437
+ else:
438
+ _backend = SQLiteBackend()
439
+ return _backend
440
+
441
+
442
+ def set_backend(backend: DatabaseBackend) -> None:
443
+ """替换数据库后端(测试用)"""
444
+ global _backend
445
+ _backend = backend