devsync 0.5.5__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.
- aiconfigkit/__init__.py +0 -0
- aiconfigkit/__main__.py +6 -0
- aiconfigkit/ai_tools/__init__.py +0 -0
- aiconfigkit/ai_tools/base.py +236 -0
- aiconfigkit/ai_tools/capability_registry.py +262 -0
- aiconfigkit/ai_tools/claude.py +91 -0
- aiconfigkit/ai_tools/claude_desktop.py +97 -0
- aiconfigkit/ai_tools/cline.py +92 -0
- aiconfigkit/ai_tools/copilot.py +92 -0
- aiconfigkit/ai_tools/cursor.py +109 -0
- aiconfigkit/ai_tools/detector.py +169 -0
- aiconfigkit/ai_tools/kiro.py +85 -0
- aiconfigkit/ai_tools/mcp_syncer.py +291 -0
- aiconfigkit/ai_tools/roo.py +110 -0
- aiconfigkit/ai_tools/translator.py +390 -0
- aiconfigkit/ai_tools/winsurf.py +102 -0
- aiconfigkit/cli/__init__.py +0 -0
- aiconfigkit/cli/delete.py +118 -0
- aiconfigkit/cli/download.py +274 -0
- aiconfigkit/cli/install.py +237 -0
- aiconfigkit/cli/install_new.py +937 -0
- aiconfigkit/cli/list.py +275 -0
- aiconfigkit/cli/main.py +454 -0
- aiconfigkit/cli/mcp_configure.py +232 -0
- aiconfigkit/cli/mcp_install.py +166 -0
- aiconfigkit/cli/mcp_sync.py +165 -0
- aiconfigkit/cli/package.py +383 -0
- aiconfigkit/cli/package_create.py +323 -0
- aiconfigkit/cli/package_install.py +472 -0
- aiconfigkit/cli/template.py +19 -0
- aiconfigkit/cli/template_backup.py +261 -0
- aiconfigkit/cli/template_init.py +499 -0
- aiconfigkit/cli/template_install.py +261 -0
- aiconfigkit/cli/template_list.py +172 -0
- aiconfigkit/cli/template_uninstall.py +146 -0
- aiconfigkit/cli/template_update.py +225 -0
- aiconfigkit/cli/template_validate.py +234 -0
- aiconfigkit/cli/tools.py +47 -0
- aiconfigkit/cli/uninstall.py +125 -0
- aiconfigkit/cli/update.py +309 -0
- aiconfigkit/core/__init__.py +0 -0
- aiconfigkit/core/checksum.py +211 -0
- aiconfigkit/core/component_detector.py +905 -0
- aiconfigkit/core/conflict_resolution.py +329 -0
- aiconfigkit/core/git_operations.py +539 -0
- aiconfigkit/core/mcp/__init__.py +1 -0
- aiconfigkit/core/mcp/credentials.py +279 -0
- aiconfigkit/core/mcp/manager.py +308 -0
- aiconfigkit/core/mcp/set_manager.py +1 -0
- aiconfigkit/core/mcp/validator.py +1 -0
- aiconfigkit/core/models.py +1661 -0
- aiconfigkit/core/package_creator.py +743 -0
- aiconfigkit/core/package_manifest.py +248 -0
- aiconfigkit/core/repository.py +298 -0
- aiconfigkit/core/secret_detector.py +438 -0
- aiconfigkit/core/template_manifest.py +283 -0
- aiconfigkit/core/version.py +201 -0
- aiconfigkit/storage/__init__.py +0 -0
- aiconfigkit/storage/library.py +429 -0
- aiconfigkit/storage/mcp_tracker.py +1 -0
- aiconfigkit/storage/package_tracker.py +234 -0
- aiconfigkit/storage/template_library.py +229 -0
- aiconfigkit/storage/template_tracker.py +296 -0
- aiconfigkit/storage/tracker.py +416 -0
- aiconfigkit/tui/__init__.py +5 -0
- aiconfigkit/tui/installer.py +511 -0
- aiconfigkit/utils/__init__.py +0 -0
- aiconfigkit/utils/atomic_write.py +90 -0
- aiconfigkit/utils/backup.py +169 -0
- aiconfigkit/utils/dotenv.py +128 -0
- aiconfigkit/utils/git_helpers.py +187 -0
- aiconfigkit/utils/logging.py +60 -0
- aiconfigkit/utils/namespace.py +134 -0
- aiconfigkit/utils/paths.py +205 -0
- aiconfigkit/utils/project.py +109 -0
- aiconfigkit/utils/streaming.py +216 -0
- aiconfigkit/utils/ui.py +194 -0
- aiconfigkit/utils/validation.py +187 -0
- devsync-0.5.5.dist-info/LICENSE +21 -0
- devsync-0.5.5.dist-info/METADATA +477 -0
- devsync-0.5.5.dist-info/RECORD +84 -0
- devsync-0.5.5.dist-info/WHEEL +5 -0
- devsync-0.5.5.dist-info/entry_points.txt +2 -0
- devsync-0.5.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""Update command for refreshing library instructions."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from git import Repo
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
12
|
+
|
|
13
|
+
from aiconfigkit.core.checksum import calculate_file_checksum
|
|
14
|
+
from aiconfigkit.core.git_operations import GitOperations, RepositoryOperationError
|
|
15
|
+
from aiconfigkit.core.models import LibraryInstruction, RefType
|
|
16
|
+
from aiconfigkit.core.repository import RepositoryParser
|
|
17
|
+
from aiconfigkit.storage.library import LibraryManager
|
|
18
|
+
from aiconfigkit.storage.tracker import InstallationTracker
|
|
19
|
+
from aiconfigkit.utils.project import find_project_root
|
|
20
|
+
from aiconfigkit.utils.ui import print_error, print_info, print_success
|
|
21
|
+
|
|
22
|
+
console = Console()
|
|
23
|
+
|
|
24
|
+
app = typer.Typer()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def update_repository(
|
|
28
|
+
namespace: Optional[str] = None,
|
|
29
|
+
all_repos: bool = False,
|
|
30
|
+
) -> int:
|
|
31
|
+
"""
|
|
32
|
+
Update instructions from their source repositories.
|
|
33
|
+
|
|
34
|
+
Only updates mutable references (branches). Tags and commits are immutable and will be skipped.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
namespace: Specific repository namespace to update
|
|
38
|
+
all_repos: Update all repositories
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Exit code (0 = success)
|
|
42
|
+
"""
|
|
43
|
+
library = LibraryManager()
|
|
44
|
+
tracker = InstallationTracker()
|
|
45
|
+
|
|
46
|
+
if not all_repos and not namespace:
|
|
47
|
+
print_error("Must specify either --namespace or --all")
|
|
48
|
+
return 1
|
|
49
|
+
|
|
50
|
+
# Get repositories to update
|
|
51
|
+
if all_repos:
|
|
52
|
+
repositories = library.list_repositories()
|
|
53
|
+
if not repositories:
|
|
54
|
+
print_info("No repositories in library to update")
|
|
55
|
+
return 0
|
|
56
|
+
else:
|
|
57
|
+
assert namespace is not None, "namespace should not be None here"
|
|
58
|
+
repo = library.get_repository(namespace)
|
|
59
|
+
if not repo:
|
|
60
|
+
print_error(f"Repository not found: {namespace}")
|
|
61
|
+
print_info("Use 'inskit list library' to see available repositories")
|
|
62
|
+
return 1
|
|
63
|
+
repositories = [repo]
|
|
64
|
+
|
|
65
|
+
console.print(f"\n[bold]Updating {len(repositories)} repository(ies)...[/bold]\n")
|
|
66
|
+
|
|
67
|
+
updated_count = 0
|
|
68
|
+
skipped_count = 0
|
|
69
|
+
error_count = 0
|
|
70
|
+
|
|
71
|
+
for repo in repositories:
|
|
72
|
+
# Extract ref info from namespace
|
|
73
|
+
ref, ref_type = _extract_ref_from_namespace(repo.namespace)
|
|
74
|
+
|
|
75
|
+
# Skip immutable refs (tags and commits)
|
|
76
|
+
if ref and ref_type in (RefType.TAG, RefType.COMMIT):
|
|
77
|
+
ref_type_name = "tag" if ref_type == RefType.TAG else "commit"
|
|
78
|
+
console.print(
|
|
79
|
+
f"[yellow]⊘ Skipped:[/yellow] {repo.name} " f"({ref_type_name} [cyan]{ref}[/cyan] is immutable)"
|
|
80
|
+
)
|
|
81
|
+
skipped_count += 1
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
# Update branch-based or non-versioned repositories
|
|
85
|
+
console.print(f"[cyan]Updating:[/cyan] {repo.name} ", end="")
|
|
86
|
+
if ref:
|
|
87
|
+
console.print(f"(branch [green]{ref}[/green])")
|
|
88
|
+
else:
|
|
89
|
+
console.print("(default branch)")
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
# Get the repository directory
|
|
93
|
+
repo_dir = library.library_dir / repo.namespace
|
|
94
|
+
|
|
95
|
+
if not repo_dir.exists():
|
|
96
|
+
print_error(f" Repository directory not found: {repo_dir}")
|
|
97
|
+
error_count += 1
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
# Check if it's a git repository (skip local non-git repos)
|
|
101
|
+
git_dir = repo_dir / ".git"
|
|
102
|
+
if not git_dir.exists():
|
|
103
|
+
console.print(" [yellow]⊘ Skipped:[/yellow] Not a Git repository (local source)")
|
|
104
|
+
skipped_count += 1
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
with Progress(
|
|
108
|
+
SpinnerColumn(),
|
|
109
|
+
TextColumn("[progress.description]{task.description}"),
|
|
110
|
+
console=console,
|
|
111
|
+
transient=True,
|
|
112
|
+
) as progress:
|
|
113
|
+
task = progress.add_task(" Checking for updates...", total=None)
|
|
114
|
+
|
|
115
|
+
# Open repository and check for updates
|
|
116
|
+
git_repo = Repo(repo_dir)
|
|
117
|
+
branch_name = ref if ref else git_repo.active_branch.name
|
|
118
|
+
|
|
119
|
+
# Check if updates are available
|
|
120
|
+
has_updates = GitOperations.check_for_updates(git_repo, branch_name)
|
|
121
|
+
|
|
122
|
+
if not has_updates:
|
|
123
|
+
console.print(" [green]✓[/green] Already up to date")
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
# Pull updates
|
|
127
|
+
progress.update(task, description=" Pulling updates...")
|
|
128
|
+
result = GitOperations.pull_repository_updates(git_repo, branch_name)
|
|
129
|
+
|
|
130
|
+
if not result.get("success"):
|
|
131
|
+
error_type = result.get("error", "unknown")
|
|
132
|
+
if error_type == "local_modifications":
|
|
133
|
+
console.print(f" [red]✗[/red] {result.get('message')}")
|
|
134
|
+
elif error_type == "conflict":
|
|
135
|
+
console.print(" [red]✗[/red] Merge conflict detected")
|
|
136
|
+
else:
|
|
137
|
+
console.print(f" [red]✗[/red] Update failed: {result.get('message')}")
|
|
138
|
+
error_count += 1
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
# Re-parse repository to update library index
|
|
142
|
+
parser = RepositoryParser(repo_dir)
|
|
143
|
+
repository = parser.parse()
|
|
144
|
+
|
|
145
|
+
# Update library instructions
|
|
146
|
+
library_instructions = []
|
|
147
|
+
instructions_dir = repo_dir / "instructions"
|
|
148
|
+
|
|
149
|
+
for instruction in repository.instructions:
|
|
150
|
+
source_file = repo_dir / instruction.file_path
|
|
151
|
+
if not source_file.exists():
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
dest_file = instructions_dir / f"{instruction.name}.md"
|
|
155
|
+
if source_file != dest_file:
|
|
156
|
+
shutil.copy2(source_file, dest_file)
|
|
157
|
+
|
|
158
|
+
checksum = calculate_file_checksum(str(dest_file))
|
|
159
|
+
|
|
160
|
+
lib_inst = LibraryInstruction(
|
|
161
|
+
id=f"{repo.namespace}/{instruction.name}",
|
|
162
|
+
name=instruction.name,
|
|
163
|
+
description=instruction.description,
|
|
164
|
+
repo_namespace=repo.namespace,
|
|
165
|
+
repo_url=repo.url,
|
|
166
|
+
repo_name=repo.name,
|
|
167
|
+
author=repository.metadata.get("author", "Unknown"),
|
|
168
|
+
version=repository.metadata.get("version", "1.0.0"),
|
|
169
|
+
file_path=str(dest_file),
|
|
170
|
+
tags=instruction.tags,
|
|
171
|
+
downloaded_at=datetime.now(),
|
|
172
|
+
checksum=checksum,
|
|
173
|
+
)
|
|
174
|
+
library_instructions.append(lib_inst)
|
|
175
|
+
|
|
176
|
+
# Update library index
|
|
177
|
+
library.add_repository(
|
|
178
|
+
repo_name=repo.name,
|
|
179
|
+
repo_description=repository.metadata.get("description", ""),
|
|
180
|
+
repo_url=repo.url,
|
|
181
|
+
repo_author=repository.metadata.get("author", "Unknown"),
|
|
182
|
+
repo_version=repository.metadata.get("version", "1.0.0"),
|
|
183
|
+
instructions=library_instructions,
|
|
184
|
+
alias=repo.alias,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Update installed instructions
|
|
188
|
+
_update_installed_instructions(repo.namespace, library_instructions, tracker)
|
|
189
|
+
|
|
190
|
+
console.print(f" [green]✓[/green] Updated successfully ({len(library_instructions)} instructions)")
|
|
191
|
+
updated_count += 1
|
|
192
|
+
|
|
193
|
+
except RepositoryOperationError as e:
|
|
194
|
+
console.print(f" [red]✗[/red] Failed: {e}")
|
|
195
|
+
error_count += 1
|
|
196
|
+
except Exception as e:
|
|
197
|
+
console.print(f" [red]✗[/red] Unexpected error: {e}")
|
|
198
|
+
error_count += 1
|
|
199
|
+
|
|
200
|
+
# Summary
|
|
201
|
+
console.print()
|
|
202
|
+
if updated_count > 0:
|
|
203
|
+
print_success(f"✓ Updated: {updated_count} repository(ies)")
|
|
204
|
+
if skipped_count > 0:
|
|
205
|
+
print_info(f"⊘ Skipped: {skipped_count} immutable reference(s)")
|
|
206
|
+
if error_count > 0:
|
|
207
|
+
print_error(f"✗ Failed: {error_count} repository(ies)")
|
|
208
|
+
|
|
209
|
+
return 0 if error_count == 0 else 1
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _extract_ref_from_namespace(namespace: str) -> tuple[Optional[str], Optional[RefType]]:
|
|
213
|
+
"""Extract Git reference from versioned namespace."""
|
|
214
|
+
if "@" not in namespace:
|
|
215
|
+
return (None, None)
|
|
216
|
+
|
|
217
|
+
ref = namespace.split("@", 1)[1]
|
|
218
|
+
|
|
219
|
+
import re
|
|
220
|
+
|
|
221
|
+
# Tags typically start with 'v' followed by numbers
|
|
222
|
+
if re.match(r"^v?\d+\.\d+", ref):
|
|
223
|
+
return (ref, RefType.TAG)
|
|
224
|
+
# Commit hashes are hex strings
|
|
225
|
+
elif re.match(r"^[0-9a-f]{7,40}$", ref):
|
|
226
|
+
return (ref, RefType.COMMIT)
|
|
227
|
+
# Everything else is likely a branch
|
|
228
|
+
else:
|
|
229
|
+
return (ref, RefType.BRANCH)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _update_installed_instructions(
|
|
233
|
+
repo_namespace: str, library_instructions: list[LibraryInstruction], tracker: InstallationTracker
|
|
234
|
+
) -> None:
|
|
235
|
+
"""Update files for installed instructions from this repository."""
|
|
236
|
+
project_root = find_project_root()
|
|
237
|
+
|
|
238
|
+
# Get all installed instructions from this repository
|
|
239
|
+
all_records = tracker.get_installed_instructions(project_root=project_root)
|
|
240
|
+
repo_records = [r for r in all_records if repo_namespace in r.source_repo or repo_namespace in r.instruction_name]
|
|
241
|
+
|
|
242
|
+
if not repo_records:
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
# Update each installed instruction file
|
|
246
|
+
for record in repo_records:
|
|
247
|
+
# Find matching library instruction
|
|
248
|
+
matching_inst = None
|
|
249
|
+
for lib_inst in library_instructions:
|
|
250
|
+
if lib_inst.name in record.instruction_name or record.instruction_name in lib_inst.id:
|
|
251
|
+
matching_inst = lib_inst
|
|
252
|
+
break
|
|
253
|
+
|
|
254
|
+
if not matching_inst:
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
# Update the installed file
|
|
258
|
+
try:
|
|
259
|
+
source_path = Path(matching_inst.file_path)
|
|
260
|
+
if not source_path.exists():
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
installed_path = Path(record.installed_path)
|
|
264
|
+
# Handle relative paths for project-scoped installations
|
|
265
|
+
if not installed_path.is_absolute() and project_root:
|
|
266
|
+
installed_path = project_root / installed_path
|
|
267
|
+
|
|
268
|
+
if installed_path.exists():
|
|
269
|
+
# Read new content and write to installed location
|
|
270
|
+
content = source_path.read_text(encoding="utf-8")
|
|
271
|
+
installed_path.write_text(content, encoding="utf-8")
|
|
272
|
+
except Exception:
|
|
273
|
+
# Silently skip if update fails (file might have been manually removed)
|
|
274
|
+
continue
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@app.command(name="update")
|
|
278
|
+
def update_command(
|
|
279
|
+
namespace: Optional[str] = typer.Option(
|
|
280
|
+
None,
|
|
281
|
+
"--namespace",
|
|
282
|
+
"-n",
|
|
283
|
+
help="Repository namespace to update",
|
|
284
|
+
),
|
|
285
|
+
all_repos: bool = typer.Option(
|
|
286
|
+
False,
|
|
287
|
+
"--all",
|
|
288
|
+
"-a",
|
|
289
|
+
help="Update all repositories in library",
|
|
290
|
+
),
|
|
291
|
+
) -> None:
|
|
292
|
+
"""
|
|
293
|
+
Update downloaded instructions to their latest versions.
|
|
294
|
+
|
|
295
|
+
This re-downloads instructions from their source repositories,
|
|
296
|
+
ensuring you have the latest versions in your library.
|
|
297
|
+
|
|
298
|
+
Examples:
|
|
299
|
+
# Update a specific repository
|
|
300
|
+
instructionkit update --namespace github.com_company_instructions
|
|
301
|
+
|
|
302
|
+
# Update all repositories
|
|
303
|
+
instructionkit update --all
|
|
304
|
+
|
|
305
|
+
# List repositories to find namespace
|
|
306
|
+
instructionkit list library
|
|
307
|
+
"""
|
|
308
|
+
exit_code = update_repository(namespace=namespace, all_repos=all_repos)
|
|
309
|
+
raise typer.Exit(code=exit_code)
|
|
File without changes
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Checksum verification for instruction files and templates."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ChecksumError(Exception):
|
|
9
|
+
"""Raised when checksum verification fails."""
|
|
10
|
+
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def calculate_checksum(content: str, algorithm: str = "sha256") -> str:
|
|
15
|
+
"""
|
|
16
|
+
Calculate checksum of content.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
content: Content to hash
|
|
20
|
+
algorithm: Hash algorithm (sha256, sha1, md5)
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Hex digest of checksum
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
ValueError: If algorithm is not supported
|
|
27
|
+
"""
|
|
28
|
+
algorithms = {
|
|
29
|
+
"sha256": hashlib.sha256,
|
|
30
|
+
"sha1": hashlib.sha1,
|
|
31
|
+
"md5": hashlib.md5,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if algorithm.lower() not in algorithms:
|
|
35
|
+
raise ValueError(f"Unsupported hash algorithm: {algorithm}")
|
|
36
|
+
|
|
37
|
+
hash_func = algorithms[algorithm.lower()]
|
|
38
|
+
return hash_func(content.encode("utf-8")).hexdigest()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def verify_checksum(content: str, expected_checksum: str, algorithm: str = "sha256") -> bool:
|
|
42
|
+
"""
|
|
43
|
+
Verify content matches expected checksum.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
content: Content to verify
|
|
47
|
+
expected_checksum: Expected checksum value
|
|
48
|
+
algorithm: Hash algorithm used
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
True if checksum matches
|
|
52
|
+
"""
|
|
53
|
+
actual_checksum = calculate_checksum(content, algorithm)
|
|
54
|
+
return actual_checksum.lower() == expected_checksum.lower()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def verify_checksum_strict(content: str, expected_checksum: str, algorithm: str = "sha256") -> None:
|
|
58
|
+
"""
|
|
59
|
+
Verify content matches expected checksum, raise on mismatch.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
content: Content to verify
|
|
63
|
+
expected_checksum: Expected checksum value
|
|
64
|
+
algorithm: Hash algorithm used
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
ChecksumError: If checksum does not match
|
|
68
|
+
"""
|
|
69
|
+
if not verify_checksum(content, expected_checksum, algorithm):
|
|
70
|
+
actual = calculate_checksum(content, algorithm)
|
|
71
|
+
raise ChecksumError(f"Checksum mismatch! Expected: {expected_checksum}, " f"Actual: {actual}")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def calculate_file_checksum(file_path: str, algorithm: str = "sha256") -> str:
|
|
75
|
+
"""
|
|
76
|
+
Calculate checksum of a file.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
file_path: Path to file
|
|
80
|
+
algorithm: Hash algorithm (sha256, sha1, md5)
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Hex digest of checksum
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
ValueError: If algorithm is not supported
|
|
87
|
+
FileNotFoundError: If file does not exist
|
|
88
|
+
"""
|
|
89
|
+
algorithms = {
|
|
90
|
+
"sha256": hashlib.sha256,
|
|
91
|
+
"sha1": hashlib.sha1,
|
|
92
|
+
"md5": hashlib.md5,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if algorithm.lower() not in algorithms:
|
|
96
|
+
raise ValueError(f"Unsupported hash algorithm: {algorithm}")
|
|
97
|
+
|
|
98
|
+
hash_func = algorithms[algorithm.lower()]()
|
|
99
|
+
|
|
100
|
+
with open(file_path, "rb") as f:
|
|
101
|
+
# Read file in chunks for memory efficiency
|
|
102
|
+
for chunk in iter(lambda: f.read(8192), b""):
|
|
103
|
+
hash_func.update(chunk)
|
|
104
|
+
|
|
105
|
+
return hash_func.hexdigest()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def verify_file_checksum(file_path: str, expected_checksum: str, algorithm: str = "sha256") -> bool:
|
|
109
|
+
"""
|
|
110
|
+
Verify file matches expected checksum.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
file_path: Path to file
|
|
114
|
+
expected_checksum: Expected checksum value
|
|
115
|
+
algorithm: Hash algorithm used
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
True if checksum matches
|
|
119
|
+
"""
|
|
120
|
+
actual_checksum = calculate_file_checksum(file_path, algorithm)
|
|
121
|
+
return actual_checksum.lower() == expected_checksum.lower()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ChecksumValidator:
|
|
125
|
+
"""Helper class for checksum validation with configuration."""
|
|
126
|
+
|
|
127
|
+
def __init__(self, algorithm: str = "sha256", strict: bool = True):
|
|
128
|
+
"""
|
|
129
|
+
Initialize validator.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
algorithm: Hash algorithm to use
|
|
133
|
+
strict: Whether to raise exceptions on mismatch
|
|
134
|
+
"""
|
|
135
|
+
self.algorithm = algorithm
|
|
136
|
+
self.strict = strict
|
|
137
|
+
|
|
138
|
+
def validate(self, content: str, expected_checksum: Optional[str]) -> bool:
|
|
139
|
+
"""
|
|
140
|
+
Validate content against expected checksum.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
content: Content to validate
|
|
144
|
+
expected_checksum: Expected checksum (None to skip validation)
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
True if valid or checksum not provided
|
|
148
|
+
|
|
149
|
+
Raises:
|
|
150
|
+
ChecksumError: If strict mode and checksum mismatch
|
|
151
|
+
"""
|
|
152
|
+
# Skip validation if no checksum provided
|
|
153
|
+
if expected_checksum is None:
|
|
154
|
+
return True
|
|
155
|
+
|
|
156
|
+
matches = verify_checksum(content, expected_checksum, self.algorithm)
|
|
157
|
+
|
|
158
|
+
if not matches and self.strict:
|
|
159
|
+
actual = calculate_checksum(content, self.algorithm)
|
|
160
|
+
raise ChecksumError(
|
|
161
|
+
f"Checksum validation failed!\n"
|
|
162
|
+
f"Expected: {expected_checksum}\n"
|
|
163
|
+
f"Actual: {actual}\n"
|
|
164
|
+
f"Algorithm: {self.algorithm}"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
return matches
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# Template-specific helper functions (for Template Sync System)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def sha256_file(file_path: Path) -> str:
|
|
174
|
+
"""
|
|
175
|
+
Calculate SHA-256 checksum of a file.
|
|
176
|
+
|
|
177
|
+
Template-specific wrapper around calculate_file_checksum for Path objects.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
file_path: Path to file
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
64-character hexadecimal string (SHA-256 hash)
|
|
184
|
+
|
|
185
|
+
Example:
|
|
186
|
+
>>> from pathlib import Path
|
|
187
|
+
>>> checksum = sha256_file(Path("template.md"))
|
|
188
|
+
>>> len(checksum)
|
|
189
|
+
64
|
|
190
|
+
"""
|
|
191
|
+
return calculate_file_checksum(str(file_path), "sha256")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def sha256_string(content: str) -> str:
|
|
195
|
+
"""
|
|
196
|
+
Calculate SHA-256 checksum of a string.
|
|
197
|
+
|
|
198
|
+
Template-specific wrapper around calculate_checksum.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
content: String content to hash
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
64-character hexadecimal string (SHA-256 hash)
|
|
205
|
+
|
|
206
|
+
Example:
|
|
207
|
+
>>> checksum = sha256_string("Hello, world!")
|
|
208
|
+
>>> len(checksum)
|
|
209
|
+
64
|
|
210
|
+
"""
|
|
211
|
+
return calculate_checksum(content, "sha256")
|