codegraph-engine 2.2.1__py3-none-any.whl → 3.0.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.
- codegraph/__init__.py +1 -1
- codegraph/agent_brain.py +172 -2
- codegraph/agent_capabilities.py +564 -6
- codegraph/agent_rules.py +1 -1
- codegraph/api_drift.py +455 -0
- codegraph/async_queue/__init__.py +50 -0
- codegraph/async_queue/detectors/__init__.py +48 -0
- codegraph/async_queue/detectors/celery.py +160 -0
- codegraph/async_queue/detectors/kafka.py +168 -0
- codegraph/async_queue/detectors/rabbitmq.py +116 -0
- codegraph/async_queue/detectors/redis.py +130 -0
- codegraph/async_queue/detectors/sqs.py +135 -0
- codegraph/async_queue/models.py +161 -0
- codegraph/async_queue/otel_models.py +74 -0
- codegraph/async_queue/otel_parser.py +337 -0
- codegraph/async_queue/reconciler.py +427 -0
- codegraph/async_queue/scanner.py +291 -0
- codegraph/async_queue/traversal.py +447 -0
- codegraph/branch_comparison.py +197 -0
- codegraph/change_impact.py +547 -0
- codegraph/cli.py +992 -3
- codegraph/cli_output.py +17 -2
- codegraph/context.py +38 -11
- codegraph/context_freshness.py +351 -0
- codegraph/database/__init__.py +14 -0
- codegraph/database/extractor.py +712 -17
- codegraph/database/interrogation.py +388 -2
- codegraph/database/schema_drift.py +862 -0
- codegraph/epistemic.py +7 -0
- codegraph/evidence_contract.py +21 -0
- codegraph/frameworks.py +171 -6
- codegraph/frameworks_nestjs.py +502 -0
- codegraph/frameworks_nextjs.py +337 -0
- codegraph/frameworks_react.py +457 -0
- codegraph/freshness.py +2 -0
- codegraph/frontend_html.py +109 -0
- codegraph/git.py +2 -0
- codegraph/git_state.py +545 -0
- codegraph/indexing/__init__.py +7 -1
- codegraph/indexing/indexer.py +291 -38
- codegraph/indexing/models.py +6 -0
- codegraph/indexing/parser.py +239 -0
- codegraph/indexing/pool.py +136 -0
- codegraph/indexing/scanner.py +6 -0
- codegraph/indexing/templates.py +418 -0
- codegraph/interrogation.py +128 -20
- codegraph/mcp/server.py +258 -27
- codegraph/models.py +7 -0
- codegraph/monorepo.py +3 -0
- codegraph/optimizer.py +42 -5
- codegraph/process_lifecycle.py +31 -13
- codegraph/refactor/__init__.py +36 -0
- codegraph/refactor/models.py +114 -0
- codegraph/refactor/renamer.py +844 -0
- codegraph/refactor/transactions.py +403 -0
- codegraph/resolver.py +1202 -159
- codegraph/retrieval_policy.py +6 -0
- codegraph/route_topology.py +172 -0
- codegraph/runtime/__init__.py +19 -0
- codegraph/runtime/collector.py +155 -0
- codegraph/runtime/db_watcher.py +191 -0
- codegraph/runtime/ingestor.py +30 -10
- codegraph/runtime/port_inspector.py +304 -0
- codegraph/runtime/runner.py +155 -6
- codegraph/runtime/trace_linker.py +175 -0
- codegraph/search/hybrid.py +26 -0
- codegraph/semantic_conflicts.py +270 -0
- codegraph/semantic_decorators.py +34 -3
- codegraph/structural_diff.py +542 -0
- codegraph/styles.py +144 -0
- codegraph/symbol_history.py +277 -0
- codegraph/tsconfig.py +236 -0
- codegraph/ui/__init__.py +6 -0
- codegraph/ui/server.py +715 -0
- codegraph/watcher.py +323 -0
- {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/METADATA +257 -39
- codegraph_engine-3.0.0.dist-info/RECORD +129 -0
- codegraph_engine-2.2.1.dist-info/RECORD +0 -87
- {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/WHEEL +0 -0
- {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/entry_points.txt +0 -0
- {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {codegraph_engine-2.2.1.dist-info → codegraph_engine-3.0.0.dist-info}/top_level.txt +0 -0
codegraph/indexing/parser.py
CHANGED
|
@@ -27,6 +27,9 @@ from codegraph.frameworks import (
|
|
|
27
27
|
analyze_js_ts_mounts,
|
|
28
28
|
get_python_analyzers,
|
|
29
29
|
)
|
|
30
|
+
from codegraph.frameworks_nestjs import analyze_nestjs_file
|
|
31
|
+
from codegraph.frameworks_nextjs import analyze_nextjs_file
|
|
32
|
+
from codegraph.frameworks_react import analyze_react_file
|
|
30
33
|
from codegraph.semantic_decorators import analyze_symbol_decorators
|
|
31
34
|
|
|
32
35
|
from .models import (
|
|
@@ -215,6 +218,9 @@ def parse(content: str, language: str, file_path: str) -> ParseResult:
|
|
|
215
218
|
"""Parse a source file in a single scoped pass."""
|
|
216
219
|
if language == "python":
|
|
217
220
|
return _parse_python(content, file_path)
|
|
221
|
+
if language == "html" or file_path.lower().endswith((".html", ".htm", ".jinja", ".jinja2", ".njk", ".ejs")):
|
|
222
|
+
from codegraph.indexing.templates import parse_html_template
|
|
223
|
+
return parse_html_template(content, file_path)
|
|
218
224
|
if language in ("sql", "prisma"):
|
|
219
225
|
source_hash = hashlib.sha256(content.encode("utf-8", errors="replace")).hexdigest()
|
|
220
226
|
return ParseResult(
|
|
@@ -1360,6 +1366,48 @@ class _PythonScopeVisitor(ast.NodeVisitor):
|
|
|
1360
1366
|
self._extract_binding_from_assign(
|
|
1361
1367
|
target.id, node.value, node.lineno, getattr(node, "col_offset", None)
|
|
1362
1368
|
)
|
|
1369
|
+
if target.id in ("template_name", "template") and isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):
|
|
1370
|
+
self.bindings.append(
|
|
1371
|
+
BindingRef(
|
|
1372
|
+
target_name=node.value.value,
|
|
1373
|
+
file_path=self.file_path,
|
|
1374
|
+
line=node.lineno,
|
|
1375
|
+
column=getattr(node, "col_offset", None),
|
|
1376
|
+
scope=self.current_scope_qname or "",
|
|
1377
|
+
expr_kind="RENDERS_TEMPLATE",
|
|
1378
|
+
base_expr=self.current_scope_canonical_id or "",
|
|
1379
|
+
source_expr=node.value.value,
|
|
1380
|
+
)
|
|
1381
|
+
)
|
|
1382
|
+
elif isinstance(node.value, ast.BinOp) and isinstance(node.value.op, ast.BitOr):
|
|
1383
|
+
# Unfold bitwise OR chain (e.g. prompt | llm | output_parser)
|
|
1384
|
+
pipe_steps: list[str] = []
|
|
1385
|
+
curr: ast.expr = node.value
|
|
1386
|
+
is_valid_pipe = True
|
|
1387
|
+
while isinstance(curr, ast.BinOp) and isinstance(curr.op, ast.BitOr):
|
|
1388
|
+
r_text = ast.unparse(curr.right).strip() if hasattr(ast, "unparse") else ""
|
|
1389
|
+
if not r_text or isinstance(curr.right, ast.Constant):
|
|
1390
|
+
is_valid_pipe = False
|
|
1391
|
+
break
|
|
1392
|
+
pipe_steps.append(r_text)
|
|
1393
|
+
curr = curr.left
|
|
1394
|
+
l_text = ast.unparse(curr).strip() if hasattr(ast, "unparse") else ""
|
|
1395
|
+
if is_valid_pipe and l_text and not isinstance(curr, ast.Constant):
|
|
1396
|
+
pipe_steps.append(l_text)
|
|
1397
|
+
pipe_steps.reverse()
|
|
1398
|
+
if len(pipe_steps) >= 2:
|
|
1399
|
+
self.bindings.append(
|
|
1400
|
+
BindingRef(
|
|
1401
|
+
target_name=target.id,
|
|
1402
|
+
file_path=self.file_path,
|
|
1403
|
+
line=node.lineno,
|
|
1404
|
+
column=getattr(node, "col_offset", None),
|
|
1405
|
+
scope=self.current_scope_qname or "",
|
|
1406
|
+
expr_kind="LCEL_PIPELINE",
|
|
1407
|
+
base_expr="|".join(pipe_steps),
|
|
1408
|
+
source_expr=json.dumps(pipe_steps),
|
|
1409
|
+
)
|
|
1410
|
+
)
|
|
1363
1411
|
elif (
|
|
1364
1412
|
isinstance(target, ast.Attribute)
|
|
1365
1413
|
and isinstance(target.value, ast.Name)
|
|
@@ -1575,6 +1623,39 @@ class _PythonScopeVisitor(ast.NodeVisitor):
|
|
|
1575
1623
|
caller_canon = self.current_scope_canonical_id
|
|
1576
1624
|
end_ln = node.end_lineno or node.lineno
|
|
1577
1625
|
|
|
1626
|
+
# Python View -> Template rendering (Flask render_template, Django render, FastAPI TemplateResponse)
|
|
1627
|
+
template_name: str | None = None
|
|
1628
|
+
if isinstance(node.func, ast.Name):
|
|
1629
|
+
if node.func.id == "render_template" and node.args:
|
|
1630
|
+
if isinstance(node.args[0], ast.Constant) and isinstance(node.args[0].value, str):
|
|
1631
|
+
template_name = node.args[0].value
|
|
1632
|
+
elif node.func.id == "render" and len(node.args) >= 2:
|
|
1633
|
+
if isinstance(node.args[1], ast.Constant) and isinstance(node.args[1].value, str):
|
|
1634
|
+
template_name = node.args[1].value
|
|
1635
|
+
elif isinstance(node.func, ast.Attribute):
|
|
1636
|
+
if node.func.attr == "TemplateResponse" and node.args:
|
|
1637
|
+
if isinstance(node.args[0], ast.Constant) and isinstance(node.args[0].value, str):
|
|
1638
|
+
template_name = node.args[0].value
|
|
1639
|
+
elif len(node.args) >= 2 and isinstance(node.args[1], ast.Constant) and isinstance(node.args[1].value, str):
|
|
1640
|
+
template_name = node.args[1].value
|
|
1641
|
+
for kw in node.keywords:
|
|
1642
|
+
if kw.arg in ("name", "template_name") and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
|
|
1643
|
+
template_name = kw.value.value
|
|
1644
|
+
|
|
1645
|
+
if template_name:
|
|
1646
|
+
self.bindings.append(
|
|
1647
|
+
BindingRef(
|
|
1648
|
+
target_name=template_name,
|
|
1649
|
+
file_path=self.file_path,
|
|
1650
|
+
line=node.lineno,
|
|
1651
|
+
column=getattr(node, "col_offset", None),
|
|
1652
|
+
scope=caller_qname or "",
|
|
1653
|
+
expr_kind="RENDERS_TEMPLATE",
|
|
1654
|
+
base_expr=caller_canon or "",
|
|
1655
|
+
source_expr=template_name,
|
|
1656
|
+
)
|
|
1657
|
+
)
|
|
1658
|
+
|
|
1578
1659
|
if isinstance(node.func, ast.Name):
|
|
1579
1660
|
callee_name = node.func.id
|
|
1580
1661
|
if callee_name == "subscribe" and len(node.args) >= 2 and hasattr(ast, "unparse"):
|
|
@@ -2010,6 +2091,106 @@ class _PythonScopeVisitor(ast.NodeVisitor):
|
|
|
2010
2091
|
subscript_key=ev_str,
|
|
2011
2092
|
)
|
|
2012
2093
|
)
|
|
2094
|
+
# Celery / Background task / Ray remote task invocation (.delay(), .apply_async(), .s(), .si(), .signature(), .remote())
|
|
2095
|
+
if attr_name in ("delay", "apply_async", "s", "si", "signature", "remote"):
|
|
2096
|
+
task_callee = recv_clean.split(".")[-1]
|
|
2097
|
+
if task_callee and task_callee not in ("self", "cls"):
|
|
2098
|
+
self.calls.append(
|
|
2099
|
+
CallRef(
|
|
2100
|
+
callee=task_callee,
|
|
2101
|
+
qualified_callee=recv_clean,
|
|
2102
|
+
line=node.lineno,
|
|
2103
|
+
end_line=end_ln,
|
|
2104
|
+
source_file=self.file_path,
|
|
2105
|
+
confidence="HIGH",
|
|
2106
|
+
caller_symbol=caller_qname,
|
|
2107
|
+
caller_canonical_id=caller_canon,
|
|
2108
|
+
receiver=recv_clean if "." in recv_clean else None,
|
|
2109
|
+
)
|
|
2110
|
+
)
|
|
2111
|
+
dispatch_kind = "RAY_REMOTE" if attr_name == "remote" else "CELERY_DISPATCH"
|
|
2112
|
+
self.bindings.append(
|
|
2113
|
+
BindingRef(
|
|
2114
|
+
target_name=task_callee,
|
|
2115
|
+
file_path=self.file_path,
|
|
2116
|
+
line=node.lineno,
|
|
2117
|
+
column=getattr(node, "col_offset", None),
|
|
2118
|
+
scope=caller_qname or "",
|
|
2119
|
+
expr_kind=dispatch_kind,
|
|
2120
|
+
base_expr=attr_name,
|
|
2121
|
+
source_expr=recv_clean,
|
|
2122
|
+
)
|
|
2123
|
+
)
|
|
2124
|
+
elif attr_name == "send_task" and node.args and isinstance(node.args[0], ast.Constant) and isinstance(node.args[0].value, str):
|
|
2125
|
+
full_task = node.args[0].value
|
|
2126
|
+
task_short = full_task.split(".")[-1]
|
|
2127
|
+
self.calls.append(
|
|
2128
|
+
CallRef(
|
|
2129
|
+
callee=task_short,
|
|
2130
|
+
qualified_callee=full_task,
|
|
2131
|
+
line=node.lineno,
|
|
2132
|
+
end_line=end_ln,
|
|
2133
|
+
source_file=self.file_path,
|
|
2134
|
+
confidence="HIGH",
|
|
2135
|
+
caller_symbol=caller_qname,
|
|
2136
|
+
caller_canonical_id=caller_canon,
|
|
2137
|
+
receiver=full_task if "." in full_task else None,
|
|
2138
|
+
)
|
|
2139
|
+
)
|
|
2140
|
+
self.bindings.append(
|
|
2141
|
+
BindingRef(
|
|
2142
|
+
target_name=task_short,
|
|
2143
|
+
file_path=self.file_path,
|
|
2144
|
+
line=node.lineno,
|
|
2145
|
+
column=getattr(node, "col_offset", None),
|
|
2146
|
+
scope=caller_qname or "",
|
|
2147
|
+
expr_kind="CELERY_DISPATCH",
|
|
2148
|
+
base_expr="send_task",
|
|
2149
|
+
source_expr=full_task,
|
|
2150
|
+
)
|
|
2151
|
+
)
|
|
2152
|
+
elif attr_name == "connect" and node.args and hasattr(ast, "unparse"):
|
|
2153
|
+
receiver_node = node.args[0]
|
|
2154
|
+
receiver_name = ast.unparse(receiver_node).strip()
|
|
2155
|
+
sender_name = ""
|
|
2156
|
+
for kw in node.keywords:
|
|
2157
|
+
if kw.arg == "sender":
|
|
2158
|
+
if isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
|
|
2159
|
+
sender_name = kw.value.value
|
|
2160
|
+
elif isinstance(kw.value, (ast.Name, ast.Attribute)):
|
|
2161
|
+
sender_name = ast.unparse(kw.value).strip()
|
|
2162
|
+
self.bindings.append(
|
|
2163
|
+
BindingRef(
|
|
2164
|
+
target_name=receiver_name,
|
|
2165
|
+
file_path=self.file_path,
|
|
2166
|
+
line=node.lineno,
|
|
2167
|
+
column=getattr(node, "col_offset", None),
|
|
2168
|
+
scope=self.current_scope_qname or "",
|
|
2169
|
+
expr_kind="DJANGO_SIGNAL_RECEIVER",
|
|
2170
|
+
base_expr=recv_clean,
|
|
2171
|
+
source_expr=sender_name,
|
|
2172
|
+
)
|
|
2173
|
+
)
|
|
2174
|
+
elif attr_name in ("send", "send_robust") and hasattr(ast, "unparse"):
|
|
2175
|
+
sender_name = ""
|
|
2176
|
+
for kw in node.keywords:
|
|
2177
|
+
if kw.arg == "sender":
|
|
2178
|
+
if isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
|
|
2179
|
+
sender_name = kw.value.value
|
|
2180
|
+
elif isinstance(kw.value, (ast.Name, ast.Attribute)):
|
|
2181
|
+
sender_name = ast.unparse(kw.value).strip()
|
|
2182
|
+
self.bindings.append(
|
|
2183
|
+
BindingRef(
|
|
2184
|
+
target_name=recv_clean,
|
|
2185
|
+
file_path=self.file_path,
|
|
2186
|
+
line=node.lineno,
|
|
2187
|
+
column=getattr(node, "col_offset", None),
|
|
2188
|
+
scope=self.current_scope_qname or "",
|
|
2189
|
+
expr_kind="DJANGO_SIGNAL_SEND",
|
|
2190
|
+
base_expr=attr_name,
|
|
2191
|
+
source_expr=sender_name,
|
|
2192
|
+
)
|
|
2193
|
+
)
|
|
2013
2194
|
|
|
2014
2195
|
q_callee = f"{resolved_recv}.{attr_name}" if resolved_recv else attr_name
|
|
2015
2196
|
self.calls.append(
|
|
@@ -2042,6 +2223,26 @@ class _PythonScopeVisitor(ast.NodeVisitor):
|
|
|
2042
2223
|
)
|
|
2043
2224
|
)
|
|
2044
2225
|
|
|
2226
|
+
# Agent tool registration (e.g. AgentExecutor(tools=[...]), create_openai_tools_agent(..., tools=[...]))
|
|
2227
|
+
for kw in node.keywords:
|
|
2228
|
+
if kw.arg in ("tools", "functions") and isinstance(kw.value, (ast.List, ast.Tuple)):
|
|
2229
|
+
func_label = ast.unparse(node.func).strip() if hasattr(ast, "unparse") else ""
|
|
2230
|
+
for elt in kw.value.elts:
|
|
2231
|
+
t_name = ast.unparse(elt).strip() if hasattr(ast, "unparse") else ""
|
|
2232
|
+
if t_name:
|
|
2233
|
+
self.bindings.append(
|
|
2234
|
+
BindingRef(
|
|
2235
|
+
target_name=t_name,
|
|
2236
|
+
file_path=self.file_path,
|
|
2237
|
+
line=node.lineno,
|
|
2238
|
+
column=getattr(node, "col_offset", None),
|
|
2239
|
+
scope=caller_qname or "",
|
|
2240
|
+
expr_kind="AGENT_TOOL_REGISTRATION",
|
|
2241
|
+
base_expr=func_label,
|
|
2242
|
+
source_expr=t_name,
|
|
2243
|
+
)
|
|
2244
|
+
)
|
|
2245
|
+
|
|
2045
2246
|
self.generic_visit(node)
|
|
2046
2247
|
|
|
2047
2248
|
|
|
@@ -3013,6 +3214,44 @@ def _parse_js_ts(content: str, language: str, file_path: str) -> ParseResult:
|
|
|
3013
3214
|
)
|
|
3014
3215
|
)
|
|
3015
3216
|
|
|
3217
|
+
# Framework file-level enhancements (NestJS and Next.js)
|
|
3218
|
+
if "@" in content:
|
|
3219
|
+
nest_res = analyze_nestjs_file(content, file_path, module)
|
|
3220
|
+
if nest_res.routes:
|
|
3221
|
+
routes.extend(nest_res.routes)
|
|
3222
|
+
if nest_res.mounts:
|
|
3223
|
+
mounts.extend(nest_res.mounts)
|
|
3224
|
+
if nest_res.bindings:
|
|
3225
|
+
bindings.extend(nest_res.bindings)
|
|
3226
|
+
|
|
3227
|
+
next_res = analyze_nextjs_file(content, file_path, module)
|
|
3228
|
+
if next_res.routes:
|
|
3229
|
+
seen_ep_ids = {r.endpoint_id for r in routes}
|
|
3230
|
+
for nr in next_res.routes:
|
|
3231
|
+
if nr.endpoint_id not in seen_ep_ids:
|
|
3232
|
+
routes.append(nr)
|
|
3233
|
+
seen_ep_ids.add(nr.endpoint_id)
|
|
3234
|
+
if next_res.bindings:
|
|
3235
|
+
bindings.extend(next_res.bindings)
|
|
3236
|
+
|
|
3237
|
+
# React and JSX component architecture (Pillar 4)
|
|
3238
|
+
react_res = analyze_react_file(content, file_path, module)
|
|
3239
|
+
sym_indices = {s.canonical_id: i for i, s in enumerate(symbols)}
|
|
3240
|
+
for sym in react_res.components:
|
|
3241
|
+
if sym.canonical_id in sym_indices:
|
|
3242
|
+
symbols[sym_indices[sym.canonical_id]] = sym
|
|
3243
|
+
else:
|
|
3244
|
+
seen_canonical.add(sym.canonical_id)
|
|
3245
|
+
symbols.append(sym)
|
|
3246
|
+
for h_sym in react_res.hooks:
|
|
3247
|
+
if h_sym.canonical_id in sym_indices:
|
|
3248
|
+
symbols[sym_indices[h_sym.canonical_id]] = h_sym
|
|
3249
|
+
else:
|
|
3250
|
+
seen_canonical.add(h_sym.canonical_id)
|
|
3251
|
+
symbols.append(h_sym)
|
|
3252
|
+
if react_res.bindings:
|
|
3253
|
+
bindings.extend(react_res.bindings)
|
|
3254
|
+
|
|
3016
3255
|
symbols.sort(key=lambda s: (s.start_line, s.canonical_id))
|
|
3017
3256
|
return ParseResult(
|
|
3018
3257
|
path=file_path,
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Thread-safe SQLite connection pool for concurrent MCP and CLI execution."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import queue
|
|
5
|
+
import sqlite3
|
|
6
|
+
import threading
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .classifier import make_compat_row
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SQLiteConnectionPool:
|
|
14
|
+
"""Thread-safe, re-entrant connection pool for SQLite databases.
|
|
15
|
+
|
|
16
|
+
Features:
|
|
17
|
+
- Thread-safe LIFO pool to maximize SQLite cache reuse.
|
|
18
|
+
- WAL mode, synchronous=NORMAL, foreign_keys=ON, busy_timeout=30000ms.
|
|
19
|
+
- check_same_thread=False for cross-thread pool handoff.
|
|
20
|
+
- Re-entrant per-thread acquisition (nesting depth tracking via thread-local state).
|
|
21
|
+
- Optional initializer hook for idempotent schema / setup execution.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
db_path: Path | str,
|
|
27
|
+
max_size: int = 16,
|
|
28
|
+
timeout: float = 30.0,
|
|
29
|
+
initializer: Callable[[sqlite3.Connection], None] | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
self.db_path = Path(db_path) if isinstance(db_path, str) else db_path
|
|
32
|
+
self.max_size = max(1, max_size)
|
|
33
|
+
self.timeout = timeout
|
|
34
|
+
self.initializer = initializer
|
|
35
|
+
self._pool: queue.LifoQueue[sqlite3.Connection] = queue.LifoQueue()
|
|
36
|
+
self._created = 0
|
|
37
|
+
self._lock = threading.Lock()
|
|
38
|
+
self._local = threading.local()
|
|
39
|
+
|
|
40
|
+
def _create_connection(self) -> sqlite3.Connection:
|
|
41
|
+
con = sqlite3.connect(
|
|
42
|
+
self.db_path,
|
|
43
|
+
check_same_thread=False,
|
|
44
|
+
timeout=self.timeout,
|
|
45
|
+
)
|
|
46
|
+
con.row_factory = make_compat_row
|
|
47
|
+
con.execute("PRAGMA journal_mode = WAL")
|
|
48
|
+
con.execute("PRAGMA synchronous = NORMAL")
|
|
49
|
+
con.execute("PRAGMA foreign_keys = ON")
|
|
50
|
+
con.execute("PRAGMA busy_timeout = 30000")
|
|
51
|
+
con.execute("PRAGMA cache_size = -64000")
|
|
52
|
+
con.execute("PRAGMA temp_store = MEMORY")
|
|
53
|
+
if self.initializer is not None:
|
|
54
|
+
self.initializer(con)
|
|
55
|
+
return con
|
|
56
|
+
|
|
57
|
+
def acquire(self) -> tuple[sqlite3.Connection, bool]:
|
|
58
|
+
"""Acquire a connection. Returns (connection, is_owner).
|
|
59
|
+
|
|
60
|
+
If the current thread already holds a connection from this pool,
|
|
61
|
+
it increments the re-entrancy depth and returns (connection, False).
|
|
62
|
+
"""
|
|
63
|
+
active_con = getattr(self._local, "con", None)
|
|
64
|
+
if active_con is not None:
|
|
65
|
+
self._local.nesting = getattr(self._local, "nesting", 0) + 1
|
|
66
|
+
return active_con, False
|
|
67
|
+
|
|
68
|
+
con: sqlite3.Connection | None = None
|
|
69
|
+
try:
|
|
70
|
+
con = self._pool.get_nowait()
|
|
71
|
+
except queue.Empty:
|
|
72
|
+
with self._lock:
|
|
73
|
+
if self._created < self.max_size:
|
|
74
|
+
con = self._create_connection()
|
|
75
|
+
self._created += 1
|
|
76
|
+
|
|
77
|
+
if con is None:
|
|
78
|
+
con = self._pool.get(timeout=self.timeout)
|
|
79
|
+
|
|
80
|
+
self._local.con = con
|
|
81
|
+
self._local.nesting = 1
|
|
82
|
+
return con, True
|
|
83
|
+
|
|
84
|
+
def release(
|
|
85
|
+
self,
|
|
86
|
+
con: sqlite3.Connection,
|
|
87
|
+
is_owner: bool,
|
|
88
|
+
rollback: bool = False,
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Release a connection back to the pool."""
|
|
91
|
+
if not is_owner:
|
|
92
|
+
cur_nesting = getattr(self._local, "nesting", 1) - 1
|
|
93
|
+
self._local.nesting = max(0, cur_nesting)
|
|
94
|
+
if rollback:
|
|
95
|
+
try:
|
|
96
|
+
con.rollback()
|
|
97
|
+
except Exception:
|
|
98
|
+
pass
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
self._local.con = None
|
|
102
|
+
self._local.nesting = 0
|
|
103
|
+
if rollback:
|
|
104
|
+
try:
|
|
105
|
+
con.rollback()
|
|
106
|
+
except Exception:
|
|
107
|
+
pass
|
|
108
|
+
else:
|
|
109
|
+
try:
|
|
110
|
+
con.commit()
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
self._pool.put_nowait(con)
|
|
116
|
+
except Exception:
|
|
117
|
+
try:
|
|
118
|
+
con.close()
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
def close_all(self) -> None:
|
|
123
|
+
"""Close and dispose all connections in the pool."""
|
|
124
|
+
with self._lock:
|
|
125
|
+
while True:
|
|
126
|
+
try:
|
|
127
|
+
con = self._pool.get_nowait()
|
|
128
|
+
try:
|
|
129
|
+
con.close()
|
|
130
|
+
except Exception:
|
|
131
|
+
pass
|
|
132
|
+
except queue.Empty:
|
|
133
|
+
break
|
|
134
|
+
self._created = 0
|
|
135
|
+
self._local.con = None
|
|
136
|
+
self._local.nesting = 0
|