codeframe-ai 0.9.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.
- codeframe/__init__.py +11 -0
- codeframe/__main__.py +20 -0
- codeframe/adapters/__init__.py +5 -0
- codeframe/adapters/e2b/__init__.py +13 -0
- codeframe/adapters/e2b/adapter.py +342 -0
- codeframe/adapters/e2b/budget.py +71 -0
- codeframe/adapters/e2b/credential_scanner.py +134 -0
- codeframe/adapters/llm/__init__.py +92 -0
- codeframe/adapters/llm/anthropic.py +414 -0
- codeframe/adapters/llm/base.py +444 -0
- codeframe/adapters/llm/mock.py +281 -0
- codeframe/adapters/llm/openai.py +483 -0
- codeframe/agents/__init__.py +8 -0
- codeframe/agents/dependency_resolver.py +714 -0
- codeframe/auth/__init__.py +16 -0
- codeframe/auth/api_key_router.py +238 -0
- codeframe/auth/api_keys.py +156 -0
- codeframe/auth/dependencies.py +358 -0
- codeframe/auth/manager.py +178 -0
- codeframe/auth/models.py +30 -0
- codeframe/auth/router.py +93 -0
- codeframe/auth/schemas.py +15 -0
- codeframe/auth/scopes.py +53 -0
- codeframe/cli/__init__.py +12 -0
- codeframe/cli/__main__.py +20 -0
- codeframe/cli/api_client.py +275 -0
- codeframe/cli/app.py +5688 -0
- codeframe/cli/auth.py +122 -0
- codeframe/cli/auth_commands.py +958 -0
- codeframe/cli/commands/__init__.py +5 -0
- codeframe/cli/config_commands.py +79 -0
- codeframe/cli/dashboard_commands.py +67 -0
- codeframe/cli/engines_commands.py +205 -0
- codeframe/cli/env_commands.py +409 -0
- codeframe/cli/helpers.py +56 -0
- codeframe/cli/hooks_commands.py +208 -0
- codeframe/cli/import_commands.py +129 -0
- codeframe/cli/pr_commands.py +549 -0
- codeframe/cli/proof_commands.py +415 -0
- codeframe/cli/stats_commands.py +311 -0
- codeframe/cli/telemetry_runtime.py +153 -0
- codeframe/cli/validators.py +123 -0
- codeframe/config/rate_limits.py +165 -0
- codeframe/core/__init__.py +15 -0
- codeframe/core/adapters/__init__.py +43 -0
- codeframe/core/adapters/agent_adapter.py +114 -0
- codeframe/core/adapters/builtin.py +326 -0
- codeframe/core/adapters/claude_code.py +62 -0
- codeframe/core/adapters/codex.py +393 -0
- codeframe/core/adapters/git_utils.py +40 -0
- codeframe/core/adapters/kilocode.py +126 -0
- codeframe/core/adapters/opencode.py +48 -0
- codeframe/core/adapters/streaming_chat.py +483 -0
- codeframe/core/adapters/subprocess_adapter.py +213 -0
- codeframe/core/adapters/verification_wrapper.py +269 -0
- codeframe/core/agent.py +2183 -0
- codeframe/core/agents_config.py +569 -0
- codeframe/core/api_key_service.py +211 -0
- codeframe/core/artifacts.py +428 -0
- codeframe/core/blocker_detection.py +218 -0
- codeframe/core/blockers.py +433 -0
- codeframe/core/checkpoints.py +481 -0
- codeframe/core/conductor.py +2255 -0
- codeframe/core/config.py +827 -0
- codeframe/core/config_watcher.py +268 -0
- codeframe/core/context.py +542 -0
- codeframe/core/context_packager.py +234 -0
- codeframe/core/credentials.py +735 -0
- codeframe/core/dependency_analyzer.py +229 -0
- codeframe/core/dependency_graph.py +290 -0
- codeframe/core/diagnostic_agent.py +712 -0
- codeframe/core/diagnostics.py +616 -0
- codeframe/core/editor.py +556 -0
- codeframe/core/engine_registry.py +256 -0
- codeframe/core/engine_stats.py +231 -0
- codeframe/core/environment.py +697 -0
- codeframe/core/events.py +375 -0
- codeframe/core/executor.py +1005 -0
- codeframe/core/fix_tracker.py +480 -0
- codeframe/core/gates.py +1322 -0
- codeframe/core/git.py +477 -0
- codeframe/core/github_connect_service.py +178 -0
- codeframe/core/github_integration_config.py +118 -0
- codeframe/core/github_issues_service.py +449 -0
- codeframe/core/hooks.py +184 -0
- codeframe/core/importers/__init__.py +1 -0
- codeframe/core/importers/ralph.py +540 -0
- codeframe/core/installer.py +650 -0
- codeframe/core/models.py +1026 -0
- codeframe/core/notifications_config.py +183 -0
- codeframe/core/planner.py +437 -0
- codeframe/core/prd.py +670 -0
- codeframe/core/prd_discovery.py +1118 -0
- codeframe/core/prd_stress_test.py +499 -0
- codeframe/core/progress.py +126 -0
- codeframe/core/proof/__init__.py +34 -0
- codeframe/core/proof/capture.py +79 -0
- codeframe/core/proof/evidence.py +56 -0
- codeframe/core/proof/ledger.py +574 -0
- codeframe/core/proof/models.py +162 -0
- codeframe/core/proof/obligations.py +103 -0
- codeframe/core/proof/runner.py +233 -0
- codeframe/core/proof/scope.py +81 -0
- codeframe/core/proof/stubs.py +156 -0
- codeframe/core/quick_fixes.py +558 -0
- codeframe/core/react_agent.py +1650 -0
- codeframe/core/reconciliation.py +183 -0
- codeframe/core/replay.py +788 -0
- codeframe/core/review.py +285 -0
- codeframe/core/runtime.py +1134 -0
- codeframe/core/sandbox/__init__.py +27 -0
- codeframe/core/sandbox/context.py +98 -0
- codeframe/core/sandbox/worktree.py +20 -0
- codeframe/core/schedule.py +396 -0
- codeframe/core/stall_detector.py +71 -0
- codeframe/core/stall_monitor.py +134 -0
- codeframe/core/state_machine.py +121 -0
- codeframe/core/streaming.py +502 -0
- codeframe/core/task_tree.py +400 -0
- codeframe/core/tasks.py +1022 -0
- codeframe/core/telemetry.py +232 -0
- codeframe/core/templates.py +221 -0
- codeframe/core/tools.py +942 -0
- codeframe/core/workspace.py +887 -0
- codeframe/core/worktrees.py +276 -0
- codeframe/git/__init__.py +5 -0
- codeframe/git/github_integration.py +505 -0
- codeframe/lib/__init__.py +0 -0
- codeframe/lib/audit_logger.py +248 -0
- codeframe/lib/metrics_tracker.py +800 -0
- codeframe/lib/quality/__init__.py +7 -0
- codeframe/lib/quality/complexity_analyzer.py +316 -0
- codeframe/lib/quality/owasp_patterns.py +284 -0
- codeframe/lib/quality/security_scanner.py +250 -0
- codeframe/lib/rate_limiter.py +312 -0
- codeframe/notifications/__init__.py +0 -0
- codeframe/notifications/webhook.py +380 -0
- codeframe/planning/__init__.py +30 -0
- codeframe/planning/issue_generator.py +219 -0
- codeframe/planning/prd_template_functions.py +137 -0
- codeframe/planning/prd_templates.py +975 -0
- codeframe/planning/task_scheduler.py +511 -0
- codeframe/planning/task_templates.py +533 -0
- codeframe/platform_store/__init__.py +5 -0
- codeframe/platform_store/database.py +277 -0
- codeframe/platform_store/repositories/__init__.py +24 -0
- codeframe/platform_store/repositories/api_key_repository.py +245 -0
- codeframe/platform_store/repositories/audit_repository.py +67 -0
- codeframe/platform_store/repositories/base.py +295 -0
- codeframe/platform_store/repositories/interactive_sessions.py +165 -0
- codeframe/platform_store/repositories/token_repository.py +598 -0
- codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
- codeframe/platform_store/schema_manager.py +321 -0
- codeframe/templates/AGENTS.md.default +94 -0
- codeframe/tui/__init__.py +5 -0
- codeframe/tui/app.py +256 -0
- codeframe/tui/data_service.py +103 -0
- codeframe/ui/__init__.py +0 -0
- codeframe/ui/dependencies.py +103 -0
- codeframe/ui/models.py +999 -0
- codeframe/ui/response_models.py +201 -0
- codeframe/ui/routers/__init__.py +5 -0
- codeframe/ui/routers/_helpers.py +29 -0
- codeframe/ui/routers/batches_v2.py +315 -0
- codeframe/ui/routers/blockers_v2.py +320 -0
- codeframe/ui/routers/checkpoints_v2.py +310 -0
- codeframe/ui/routers/costs_v2.py +322 -0
- codeframe/ui/routers/diagnose_v2.py +225 -0
- codeframe/ui/routers/discovery_v2.py +417 -0
- codeframe/ui/routers/environment_v2.py +284 -0
- codeframe/ui/routers/events_v2.py +75 -0
- codeframe/ui/routers/gates_v2.py +166 -0
- codeframe/ui/routers/git_v2.py +284 -0
- codeframe/ui/routers/github_integrations_v2.py +532 -0
- codeframe/ui/routers/interactive_sessions_v2.py +238 -0
- codeframe/ui/routers/pr_v2.py +709 -0
- codeframe/ui/routers/prd_v2.py +695 -0
- codeframe/ui/routers/proof_v2.py +755 -0
- codeframe/ui/routers/review_v2.py +360 -0
- codeframe/ui/routers/schedule_v2.py +214 -0
- codeframe/ui/routers/session_chat_ws.py +354 -0
- codeframe/ui/routers/settings_v2.py +562 -0
- codeframe/ui/routers/streaming_v2.py +155 -0
- codeframe/ui/routers/tasks_v2.py +1098 -0
- codeframe/ui/routers/templates_v2.py +232 -0
- codeframe/ui/routers/terminal_ws.py +267 -0
- codeframe/ui/routers/workspace_v2.py +527 -0
- codeframe/ui/server.py +568 -0
- codeframe/ui/shared.py +241 -0
- codeframe/workspace/__init__.py +5 -0
- codeframe/workspace/manager.py +249 -0
- codeframe_ai-0.9.0.dist-info/METADATA +517 -0
- codeframe_ai-0.9.0.dist-info/RECORD +197 -0
- codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
- codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
- codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
- codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""Worktree-per-task isolation for parallel batch execution.
|
|
2
|
+
|
|
3
|
+
Creates isolated git worktrees so parallel agents don't modify files in the
|
|
4
|
+
same working directory. Each task gets its own branch and working tree,
|
|
5
|
+
then merges back to the base branch on completion.
|
|
6
|
+
|
|
7
|
+
Lifecycle:
|
|
8
|
+
1. create(workspace_path, task_id) → worktree path
|
|
9
|
+
2. Agent runs with cwd set to worktree
|
|
10
|
+
3. merge_back(workspace_path, task_id) → MergeResult
|
|
11
|
+
4. cleanup(workspace_path, task_id)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
import os
|
|
19
|
+
import subprocess
|
|
20
|
+
import threading
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Optional
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
WORKTREE_DIR = ".codeframe/worktrees"
|
|
29
|
+
_REGISTRY_FILE = ".codeframe/worktrees.json"
|
|
30
|
+
_registry_lock = threading.Lock()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class MergeResult:
|
|
35
|
+
"""Result from merging a worktree branch back to base."""
|
|
36
|
+
|
|
37
|
+
task_id: str
|
|
38
|
+
success: bool
|
|
39
|
+
conflict_details: str
|
|
40
|
+
merge_commit: Optional[str]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TaskWorktree:
|
|
44
|
+
"""Manages git worktrees for isolated parallel task execution."""
|
|
45
|
+
|
|
46
|
+
def create(
|
|
47
|
+
self,
|
|
48
|
+
workspace_path: Path,
|
|
49
|
+
task_id: str,
|
|
50
|
+
base_branch: str = "main",
|
|
51
|
+
) -> Path:
|
|
52
|
+
"""Create an isolated worktree for a task.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
workspace_path: Root of the git repository
|
|
56
|
+
task_id: Task identifier (used for branch and directory name)
|
|
57
|
+
base_branch: Branch to base the worktree on
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Path to the created worktree directory
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
subprocess.CalledProcessError: If git worktree creation fails
|
|
64
|
+
"""
|
|
65
|
+
worktree_path = workspace_path / WORKTREE_DIR / task_id
|
|
66
|
+
worktree_path.parent.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
branch_name = f"cf/{task_id}"
|
|
68
|
+
|
|
69
|
+
subprocess.run(
|
|
70
|
+
["git", "worktree", "add", str(worktree_path), "-b", branch_name, base_branch],
|
|
71
|
+
cwd=str(workspace_path),
|
|
72
|
+
capture_output=True,
|
|
73
|
+
text=True,
|
|
74
|
+
check=True,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
logger.info("Created worktree for %s at %s", task_id, worktree_path)
|
|
78
|
+
return worktree_path
|
|
79
|
+
|
|
80
|
+
def merge_back(
|
|
81
|
+
self,
|
|
82
|
+
workspace_path: Path,
|
|
83
|
+
task_id: str,
|
|
84
|
+
base_branch: str = "main",
|
|
85
|
+
) -> MergeResult:
|
|
86
|
+
"""Merge worktree branch back to base branch.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
workspace_path: Root of the git repository
|
|
90
|
+
task_id: Task identifier
|
|
91
|
+
base_branch: Branch to merge into
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
MergeResult with success status and optional conflict details
|
|
95
|
+
"""
|
|
96
|
+
branch_name = f"cf/{task_id}"
|
|
97
|
+
|
|
98
|
+
# Checkout base branch
|
|
99
|
+
subprocess.run(
|
|
100
|
+
["git", "checkout", base_branch],
|
|
101
|
+
cwd=str(workspace_path),
|
|
102
|
+
capture_output=True,
|
|
103
|
+
text=True,
|
|
104
|
+
check=True,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Attempt merge
|
|
108
|
+
result = subprocess.run(
|
|
109
|
+
["git", "merge", branch_name, "--no-ff", "-m", f"Merge {branch_name} into {base_branch}"],
|
|
110
|
+
cwd=str(workspace_path),
|
|
111
|
+
capture_output=True,
|
|
112
|
+
text=True,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if result.returncode == 0:
|
|
116
|
+
# Get merge commit hash
|
|
117
|
+
head = subprocess.run(
|
|
118
|
+
["git", "rev-parse", "HEAD"],
|
|
119
|
+
cwd=str(workspace_path),
|
|
120
|
+
capture_output=True,
|
|
121
|
+
text=True,
|
|
122
|
+
)
|
|
123
|
+
merge_commit = head.stdout.strip() if head.returncode == 0 else None
|
|
124
|
+
|
|
125
|
+
logger.info("Merged %s back to %s", branch_name, base_branch)
|
|
126
|
+
return MergeResult(
|
|
127
|
+
task_id=task_id,
|
|
128
|
+
success=True,
|
|
129
|
+
conflict_details="",
|
|
130
|
+
merge_commit=merge_commit,
|
|
131
|
+
)
|
|
132
|
+
else:
|
|
133
|
+
# Merge conflict — abort and report
|
|
134
|
+
conflict_output = result.stdout + result.stderr
|
|
135
|
+
subprocess.run(
|
|
136
|
+
["git", "merge", "--abort"],
|
|
137
|
+
cwd=str(workspace_path),
|
|
138
|
+
capture_output=True,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
logger.warning("Merge conflict for %s: %s", branch_name, conflict_output[:200])
|
|
142
|
+
return MergeResult(
|
|
143
|
+
task_id=task_id,
|
|
144
|
+
success=False,
|
|
145
|
+
conflict_details=conflict_output[:2000],
|
|
146
|
+
merge_commit=None,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def cleanup(
|
|
150
|
+
self,
|
|
151
|
+
workspace_path: Path,
|
|
152
|
+
task_id: str,
|
|
153
|
+
) -> None:
|
|
154
|
+
"""Remove worktree and delete task branch.
|
|
155
|
+
|
|
156
|
+
Never raises — cleanup failures are logged as warnings.
|
|
157
|
+
"""
|
|
158
|
+
worktree_path = workspace_path / WORKTREE_DIR / task_id
|
|
159
|
+
branch_name = f"cf/{task_id}"
|
|
160
|
+
|
|
161
|
+
# Remove worktree
|
|
162
|
+
try:
|
|
163
|
+
subprocess.run(
|
|
164
|
+
["git", "worktree", "remove", str(worktree_path), "--force"],
|
|
165
|
+
cwd=str(workspace_path),
|
|
166
|
+
capture_output=True,
|
|
167
|
+
text=True,
|
|
168
|
+
)
|
|
169
|
+
except Exception as exc:
|
|
170
|
+
logger.warning("Failed to remove worktree for %s: %s", task_id, exc)
|
|
171
|
+
|
|
172
|
+
# Delete branch
|
|
173
|
+
try:
|
|
174
|
+
subprocess.run(
|
|
175
|
+
["git", "branch", "-D", branch_name],
|
|
176
|
+
cwd=str(workspace_path),
|
|
177
|
+
capture_output=True,
|
|
178
|
+
text=True,
|
|
179
|
+
)
|
|
180
|
+
except Exception as exc:
|
|
181
|
+
logger.warning("Failed to delete branch %s: %s", branch_name, exc)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def get_base_branch(workspace_path: Path) -> str:
|
|
185
|
+
"""Return the current HEAD branch name, defaulting to 'main' on failure.
|
|
186
|
+
|
|
187
|
+
Returns 'main' when git is unavailable, the directory is not a repo,
|
|
188
|
+
or HEAD is detached (rev-parse returns 'HEAD' literally).
|
|
189
|
+
"""
|
|
190
|
+
result = subprocess.run(
|
|
191
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
192
|
+
cwd=str(workspace_path),
|
|
193
|
+
capture_output=True,
|
|
194
|
+
text=True,
|
|
195
|
+
)
|
|
196
|
+
branch = result.stdout.strip()
|
|
197
|
+
if result.returncode != 0 or not branch or branch == "HEAD":
|
|
198
|
+
return "main"
|
|
199
|
+
return branch
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def list_worktrees(workspace_path: Path) -> list[dict]:
|
|
203
|
+
"""Return all entries in the worktree registry, or [] if absent/corrupt/malformed."""
|
|
204
|
+
registry_file = workspace_path / _REGISTRY_FILE
|
|
205
|
+
if not registry_file.exists():
|
|
206
|
+
return []
|
|
207
|
+
try:
|
|
208
|
+
data = json.loads(registry_file.read_text())
|
|
209
|
+
return data if isinstance(data, list) else []
|
|
210
|
+
except Exception:
|
|
211
|
+
return []
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class WorktreeRegistry:
|
|
215
|
+
"""Atomic registry of active worktrees for orphan detection.
|
|
216
|
+
|
|
217
|
+
Stores entries in ``.codeframe/worktrees.json``. All mutations are
|
|
218
|
+
protected by a module-level threading.Lock and written atomically
|
|
219
|
+
(write to .tmp then os.replace).
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
def register(self, workspace_path: Path, task_id: str, batch_id: str) -> None:
|
|
223
|
+
"""Add or refresh a worktree entry for task_id."""
|
|
224
|
+
with _registry_lock:
|
|
225
|
+
entries = list_worktrees(workspace_path)
|
|
226
|
+
# Remove any existing entry for this task_id (idempotent)
|
|
227
|
+
entries = [e for e in entries if e["task_id"] != task_id]
|
|
228
|
+
entries.append({
|
|
229
|
+
"task_id": task_id,
|
|
230
|
+
"batch_id": batch_id,
|
|
231
|
+
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
232
|
+
"pid": os.getpid(),
|
|
233
|
+
})
|
|
234
|
+
self._write(workspace_path, entries)
|
|
235
|
+
|
|
236
|
+
def unregister(self, workspace_path: Path, task_id: str) -> None:
|
|
237
|
+
"""Remove the worktree entry for task_id. Safe if absent."""
|
|
238
|
+
with _registry_lock:
|
|
239
|
+
entries = list_worktrees(workspace_path)
|
|
240
|
+
entries = [e for e in entries if e["task_id"] != task_id]
|
|
241
|
+
self._write(workspace_path, entries)
|
|
242
|
+
|
|
243
|
+
def list_stale(self, workspace_path: Path) -> list[dict]:
|
|
244
|
+
"""Return entries whose registered PID is no longer alive."""
|
|
245
|
+
stale = []
|
|
246
|
+
for entry in list_worktrees(workspace_path):
|
|
247
|
+
pid = entry.get("pid")
|
|
248
|
+
if pid is None:
|
|
249
|
+
continue
|
|
250
|
+
try:
|
|
251
|
+
os.kill(pid, 0)
|
|
252
|
+
except ProcessLookupError:
|
|
253
|
+
stale.append(entry)
|
|
254
|
+
except PermissionError:
|
|
255
|
+
pass # Process is alive but owned by another user — not stale
|
|
256
|
+
return stale
|
|
257
|
+
|
|
258
|
+
def cleanup_stale(self, workspace_path: Path) -> None:
|
|
259
|
+
"""Remove stale worktree directories and unregister their entries."""
|
|
260
|
+
stale = self.list_stale(workspace_path)
|
|
261
|
+
if not stale:
|
|
262
|
+
return
|
|
263
|
+
wt = TaskWorktree()
|
|
264
|
+
for entry in stale:
|
|
265
|
+
task_id = entry["task_id"]
|
|
266
|
+
logger.info("Cleaning up orphaned worktree for task %s (pid %s)", task_id, entry.get("pid"))
|
|
267
|
+
wt.cleanup(workspace_path, task_id)
|
|
268
|
+
self.unregister(workspace_path, task_id)
|
|
269
|
+
|
|
270
|
+
@staticmethod
|
|
271
|
+
def _write(workspace_path: Path, entries: list[dict]) -> None:
|
|
272
|
+
registry_file = workspace_path / _REGISTRY_FILE
|
|
273
|
+
registry_file.parent.mkdir(parents=True, exist_ok=True)
|
|
274
|
+
tmp_file = registry_file.with_suffix(".json.tmp")
|
|
275
|
+
tmp_file.write_text(json.dumps(entries, indent=2))
|
|
276
|
+
os.replace(tmp_file, registry_file)
|