cicada-mcp 0.1.4__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.

Potentially problematic release.


This version of cicada-mcp might be problematic. Click here for more details.

Files changed (48) hide show
  1. cicada/__init__.py +30 -0
  2. cicada/clean.py +297 -0
  3. cicada/command_logger.py +293 -0
  4. cicada/dead_code_analyzer.py +282 -0
  5. cicada/extractors/__init__.py +36 -0
  6. cicada/extractors/base.py +66 -0
  7. cicada/extractors/call.py +176 -0
  8. cicada/extractors/dependency.py +361 -0
  9. cicada/extractors/doc.py +179 -0
  10. cicada/extractors/function.py +246 -0
  11. cicada/extractors/module.py +123 -0
  12. cicada/extractors/spec.py +151 -0
  13. cicada/find_dead_code.py +270 -0
  14. cicada/formatter.py +918 -0
  15. cicada/git_helper.py +646 -0
  16. cicada/indexer.py +629 -0
  17. cicada/install.py +724 -0
  18. cicada/keyword_extractor.py +364 -0
  19. cicada/keyword_search.py +553 -0
  20. cicada/lightweight_keyword_extractor.py +298 -0
  21. cicada/mcp_server.py +1559 -0
  22. cicada/mcp_tools.py +291 -0
  23. cicada/parser.py +124 -0
  24. cicada/pr_finder.py +435 -0
  25. cicada/pr_indexer/__init__.py +20 -0
  26. cicada/pr_indexer/cli.py +62 -0
  27. cicada/pr_indexer/github_api_client.py +431 -0
  28. cicada/pr_indexer/indexer.py +297 -0
  29. cicada/pr_indexer/line_mapper.py +209 -0
  30. cicada/pr_indexer/pr_index_builder.py +253 -0
  31. cicada/setup.py +339 -0
  32. cicada/utils/__init__.py +52 -0
  33. cicada/utils/call_site_formatter.py +95 -0
  34. cicada/utils/function_grouper.py +57 -0
  35. cicada/utils/hash_utils.py +173 -0
  36. cicada/utils/index_utils.py +290 -0
  37. cicada/utils/path_utils.py +240 -0
  38. cicada/utils/signature_builder.py +106 -0
  39. cicada/utils/storage.py +111 -0
  40. cicada/utils/subprocess_runner.py +182 -0
  41. cicada/utils/text_utils.py +90 -0
  42. cicada/version_check.py +116 -0
  43. cicada_mcp-0.1.4.dist-info/METADATA +619 -0
  44. cicada_mcp-0.1.4.dist-info/RECORD +48 -0
  45. cicada_mcp-0.1.4.dist-info/WHEEL +5 -0
  46. cicada_mcp-0.1.4.dist-info/entry_points.txt +8 -0
  47. cicada_mcp-0.1.4.dist-info/licenses/LICENSE +21 -0
  48. cicada_mcp-0.1.4.dist-info/top_level.txt +1 -0
cicada/git_helper.py ADDED
@@ -0,0 +1,646 @@
1
+ """
2
+ Git integration - extract commit history and file changes
3
+
4
+ This module provides access to git commit history using GitPython.
5
+ It complements pr_finder.py (which provides PR attribution) by
6
+ offering comprehensive commit history for files and functions.
7
+
8
+ Author: Cursor(Auto)
9
+ """
10
+
11
+ import git
12
+ import subprocess
13
+ from datetime import datetime
14
+ from typing import List, Dict, Optional
15
+ from pathlib import Path
16
+
17
+
18
+ class GitHelper:
19
+ """Helper class for extracting git commit history"""
20
+
21
+ def __init__(self, repo_path: str):
22
+ """
23
+ Initialize GitHelper with a repository path
24
+
25
+ Args:
26
+ repo_path: Path to git repository
27
+
28
+ Raises:
29
+ git.InvalidGitRepositoryError: If path is not a git repository
30
+ """
31
+ self.repo = git.Repo(repo_path)
32
+ self.repo_path = Path(repo_path)
33
+
34
+ def get_file_history(self, file_path: str, max_commits: int = 10) -> List[Dict]:
35
+ """
36
+ Get commit history for a specific file
37
+
38
+ Args:
39
+ file_path: Relative path to file from repo root
40
+ max_commits: Maximum number of commits to return
41
+
42
+ Returns:
43
+ List of commit information dictionaries with keys:
44
+ - sha: Short commit SHA (8 chars)
45
+ - full_sha: Full commit SHA
46
+ - author: Author name
47
+ - author_email: Author email
48
+ - date: Commit date in ISO format
49
+ - message: Full commit message
50
+ - summary: First line of commit message
51
+ """
52
+ commits = []
53
+
54
+ try:
55
+ # Get commits that touched this file
56
+ for commit in self.repo.iter_commits(
57
+ paths=file_path, max_count=max_commits
58
+ ):
59
+ commits.append(
60
+ {
61
+ "sha": commit.hexsha[:8], # Short SHA
62
+ "full_sha": commit.hexsha,
63
+ "author": str(commit.author),
64
+ "author_email": commit.author.email,
65
+ "date": commit.committed_datetime.isoformat(),
66
+ "message": commit.message.strip(),
67
+ "summary": commit.summary,
68
+ }
69
+ )
70
+ except Exception as e:
71
+ print(f"Error getting history for {file_path}: {e}")
72
+
73
+ return commits
74
+
75
+ def get_function_history_heuristic(
76
+ self,
77
+ file_path: str,
78
+ function_name: str,
79
+ _line_number: int,
80
+ max_commits: int = 5,
81
+ ) -> List[Dict]:
82
+ """
83
+ Get commit history for a specific function using heuristics.
84
+
85
+ This is a heuristic-based approach that returns commits that:
86
+ 1. Modified the file near the function's location, OR
87
+ 2. Mention the function name in the commit message
88
+
89
+ A more sophisticated version would use git blame to track
90
+ exact line changes over time.
91
+
92
+ Args:
93
+ file_path: Relative path to file
94
+ function_name: Name of the function
95
+ line_number: Line number where function is defined
96
+ max_commits: Maximum commits to return
97
+
98
+ Returns:
99
+ List of relevant commits with 'relevance' field:
100
+ - 'mentioned': Function name in commit message
101
+ - 'file_change': Recent change to the file
102
+ """
103
+ # Get file history with more commits than requested
104
+ file_commits = self.get_file_history(file_path, max_commits * 2)
105
+
106
+ # Filter for commits mentioning the function or likely relevant
107
+ relevant_commits = []
108
+ for commit in file_commits:
109
+ # Include if function name in commit message
110
+ if function_name.lower() in commit["message"].lower():
111
+ commit["relevance"] = "mentioned"
112
+ relevant_commits.append(commit)
113
+ # Or if it's a recent commit to the file
114
+ elif len(relevant_commits) < max_commits:
115
+ commit["relevance"] = "file_change"
116
+ relevant_commits.append(commit)
117
+
118
+ if len(relevant_commits) >= max_commits:
119
+ break
120
+
121
+ return relevant_commits
122
+
123
+ def get_function_history_precise(
124
+ self,
125
+ file_path: str,
126
+ start_line: Optional[int] = None,
127
+ end_line: Optional[int] = None,
128
+ function_name: Optional[str] = None,
129
+ max_commits: int = 5,
130
+ ) -> List[Dict]:
131
+ """
132
+ Get precise commit history for a function using git log -L.
133
+
134
+ This method uses git's native function tracking (when function_name is provided)
135
+ or line tracking (when start_line/end_line are provided) to find commits that
136
+ actually modified the function, even as it moves within the file.
137
+
138
+ Args:
139
+ file_path: Relative path to file from repo root
140
+ start_line: Starting line number (optional, for line-based tracking)
141
+ end_line: Ending line number (optional, for line-based tracking)
142
+ function_name: Function name to track (e.g., "create_user")
143
+ max_commits: Maximum number of commits to return
144
+
145
+ Returns:
146
+ List of commit information dictionaries with keys:
147
+ - sha: Short commit SHA (8 chars)
148
+ - full_sha: Full commit SHA
149
+ - author: Author name
150
+ - author_email: Author email
151
+ - date: Commit date in ISO format
152
+ - message: Full commit message
153
+ - summary: First line of commit message
154
+
155
+ Note:
156
+ - Provide either function_name OR (start_line, end_line)
157
+ - If function_name is provided and fails, falls back to line-based tracking
158
+ - Requires .gitattributes with "*.ex diff=elixir" for function tracking
159
+ """
160
+ commits = []
161
+ import subprocess
162
+
163
+ # Determine tracking mode
164
+ use_function_tracking = function_name is not None
165
+ use_line_tracking = start_line is not None and end_line is not None
166
+
167
+ if not use_function_tracking and not use_line_tracking:
168
+ print(f"Error: Must provide either function_name or (start_line, end_line)")
169
+ return []
170
+
171
+ try:
172
+ # Build git log -L command
173
+ if use_function_tracking:
174
+ # Try function-based tracking first: git log -L :funcname:file
175
+ line_spec = f":{function_name}:{file_path}"
176
+ else:
177
+ # Use line-based tracking: git log -L start,end:file
178
+ line_spec = f"{start_line},{end_line}:{file_path}"
179
+
180
+ cmd = [
181
+ "git",
182
+ "log",
183
+ f"-L{line_spec}",
184
+ f"--max-count={max_commits}",
185
+ "--format=%H|%an|%ae|%aI|%s",
186
+ "--no-patch", # Don't show diffs, just commits
187
+ ]
188
+
189
+ # Run command in repo directory
190
+ result = subprocess.run(
191
+ cmd, cwd=str(self.repo_path), capture_output=True, text=True, check=True
192
+ )
193
+
194
+ # Parse output
195
+ for line in result.stdout.strip().split("\n"):
196
+ if not line or line.startswith("diff"):
197
+ continue
198
+
199
+ parts = line.split("|")
200
+ if len(parts) >= 5:
201
+ full_sha = parts[0]
202
+ commits.append(
203
+ {
204
+ "sha": full_sha[:8],
205
+ "full_sha": full_sha,
206
+ "author": parts[1],
207
+ "author_email": parts[2],
208
+ "date": parts[3],
209
+ "summary": parts[4],
210
+ "message": parts[4], # Summary for now, can enhance later
211
+ }
212
+ )
213
+
214
+ except subprocess.CalledProcessError as e:
215
+ # git log -L failed
216
+ error_msg = e.stderr if e.stderr else str(e)
217
+
218
+ # If function tracking failed and we have line numbers, try fallback
219
+ if use_function_tracking and start_line and end_line:
220
+ print(
221
+ f"Function tracking failed for {function_name}, falling back to line tracking"
222
+ )
223
+ return self.get_function_history_precise(
224
+ file_path,
225
+ start_line=start_line,
226
+ end_line=end_line,
227
+ max_commits=max_commits,
228
+ )
229
+ else:
230
+ print(f"Warning: git log -L failed for {file_path}: {error_msg}")
231
+ return []
232
+
233
+ except Exception as e:
234
+ print(f"Error getting precise history for {file_path}: {e}")
235
+ return []
236
+
237
+ return commits
238
+
239
+ def get_function_evolution(
240
+ self,
241
+ file_path: str,
242
+ start_line: Optional[int] = None,
243
+ end_line: Optional[int] = None,
244
+ function_name: Optional[str] = None,
245
+ ) -> Optional[Dict]:
246
+ """
247
+ Get evolution metadata for a function (creation, last modification, change count).
248
+
249
+ Uses git log -L to track the complete history of a function and
250
+ extract key lifecycle information.
251
+
252
+ Args:
253
+ file_path: Relative path to file from repo root
254
+ start_line: Starting line number (optional, for line-based tracking)
255
+ end_line: Ending line number (optional, for line-based tracking)
256
+ function_name: Function name to track (e.g., "create_user")
257
+
258
+ Returns:
259
+ Dictionary with evolution information:
260
+ - created_at: {sha, date, author, message} for first commit
261
+ - last_modified: {sha, date, author, message} for most recent commit
262
+ - total_modifications: Total number of commits that touched this function
263
+ - modification_frequency: Average commits per month (if > 1 month of history)
264
+ Returns None if no history found or on error.
265
+
266
+ Note:
267
+ - Provide either function_name OR (start_line, end_line)
268
+ """
269
+ try:
270
+ # Get all commits that touched this function (no limit)
271
+ commits = self.get_function_history_precise(
272
+ file_path,
273
+ start_line=start_line,
274
+ end_line=end_line,
275
+ function_name=function_name,
276
+ max_commits=1000, # Get all commits
277
+ )
278
+
279
+ if not commits:
280
+ return None
281
+
282
+ # Calculate evolution metadata
283
+ created_at = commits[-1] # Oldest commit (last in list)
284
+ last_modified = commits[0] # Most recent commit (first in list)
285
+ total_modifications = len(commits)
286
+
287
+ # Calculate modification frequency (commits per month)
288
+ modification_frequency = None
289
+ if len(commits) > 1:
290
+ try:
291
+ from datetime import datetime
292
+
293
+ first_date = datetime.fromisoformat(created_at["date"])
294
+ last_date = datetime.fromisoformat(last_modified["date"])
295
+ days_between = (last_date - first_date).days
296
+
297
+ if days_between > 0:
298
+ months = days_between / 30.0
299
+ modification_frequency = (
300
+ total_modifications / months
301
+ if months > 0
302
+ else total_modifications
303
+ )
304
+ except Exception:
305
+ # If date parsing fails, skip frequency calculation
306
+ pass
307
+
308
+ return {
309
+ "created_at": {
310
+ "sha": created_at["sha"],
311
+ "full_sha": created_at["full_sha"],
312
+ "date": created_at["date"],
313
+ "author": created_at["author"],
314
+ "author_email": created_at["author_email"],
315
+ "message": created_at["summary"],
316
+ },
317
+ "last_modified": {
318
+ "sha": last_modified["sha"],
319
+ "full_sha": last_modified["full_sha"],
320
+ "date": last_modified["date"],
321
+ "author": last_modified["author"],
322
+ "author_email": last_modified["author_email"],
323
+ "message": last_modified["summary"],
324
+ },
325
+ "total_modifications": total_modifications,
326
+ "modification_frequency": modification_frequency,
327
+ }
328
+
329
+ except Exception as e:
330
+ print(f"Error getting function evolution for {file_path}: {e}")
331
+ return None
332
+
333
+ def get_function_history(
334
+ self, file_path: str, start_line: int, end_line: int
335
+ ) -> List[Dict]:
336
+ """
337
+ Get line-by-line authorship for a function using git blame.
338
+
339
+ Shows who wrote each line of code, with consecutive lines by the
340
+ same author grouped together for easier reading.
341
+
342
+ Args:
343
+ file_path: Relative path to file from repo root
344
+ start_line: Starting line number of the function
345
+ end_line: Ending line number of the function
346
+
347
+ Returns:
348
+ List of blame groups, each containing:
349
+ - author: Author name
350
+ - author_email: Author email
351
+ - sha: Commit SHA (short)
352
+ - full_sha: Full commit SHA
353
+ - date: Commit date
354
+ - line_start: First line number in this group
355
+ - line_end: Last line number in this group
356
+ - line_count: Number of consecutive lines by this author
357
+ - lines: List of {number, content} for each line
358
+ """
359
+ blame_groups = []
360
+ import subprocess
361
+
362
+ try:
363
+ # Use git blame with line range
364
+ cmd = [
365
+ "git",
366
+ "blame",
367
+ f"-L{start_line},{end_line}",
368
+ "--porcelain", # Machine-readable format
369
+ file_path,
370
+ ]
371
+
372
+ result = subprocess.run(
373
+ cmd, cwd=str(self.repo_path), capture_output=True, text=True, check=True
374
+ )
375
+
376
+ # Parse porcelain format
377
+ lines_data = []
378
+ current_commit = {}
379
+
380
+ for line in result.stdout.split("\n"):
381
+ if not line:
382
+ continue
383
+
384
+ # Commit SHA line (40 char hex)
385
+ if len(line) >= 40 and line[0:40].isalnum():
386
+ parts = line.split()
387
+ if len(parts) >= 3:
388
+ current_commit = {
389
+ "sha": parts[0][:8],
390
+ "full_sha": parts[0],
391
+ "line_number": int(parts[2]),
392
+ }
393
+ # Author name
394
+ elif line.startswith("author "):
395
+ current_commit["author"] = line[7:]
396
+ # Author email
397
+ elif line.startswith("author-mail "):
398
+ email = line[12:].strip("<>")
399
+ current_commit["author_email"] = email
400
+ # Author time
401
+ elif line.startswith("author-time "):
402
+ try:
403
+ timestamp = int(line[12:])
404
+ current_commit["date"] = datetime.fromtimestamp(
405
+ timestamp
406
+ ).isoformat()
407
+ except:
408
+ current_commit["date"] = line[12:]
409
+ # Actual code line (starts with tab)
410
+ elif line.startswith("\t"):
411
+ code_line = line[1:] # Remove leading tab
412
+ line_info = {**current_commit, "content": code_line}
413
+ lines_data.append(line_info)
414
+
415
+ # Group consecutive lines by same author and commit
416
+ if lines_data:
417
+ current_group = {
418
+ "author": lines_data[0]["author"],
419
+ "author_email": lines_data[0]["author_email"],
420
+ "sha": lines_data[0]["sha"],
421
+ "full_sha": lines_data[0]["full_sha"],
422
+ "date": lines_data[0]["date"],
423
+ "line_start": lines_data[0]["line_number"],
424
+ "line_end": lines_data[0]["line_number"],
425
+ "lines": [
426
+ {
427
+ "number": lines_data[0]["line_number"],
428
+ "content": lines_data[0]["content"],
429
+ }
430
+ ],
431
+ }
432
+
433
+ for line_info in lines_data[1:]:
434
+ # Same author and commit? Extend current group
435
+ if (
436
+ line_info["author"] == current_group["author"]
437
+ and line_info["sha"] == current_group["sha"]
438
+ ):
439
+ current_group["line_end"] = line_info["line_number"]
440
+ _ = current_group["lines"].append(
441
+ {
442
+ "number": line_info["line_number"],
443
+ "content": line_info["content"],
444
+ }
445
+ )
446
+ else:
447
+ # Different author/commit - save current group and start new one
448
+ current_group["line_count"] = len(current_group["lines"])
449
+ blame_groups.append(current_group)
450
+ current_group = {
451
+ "author": line_info["author"],
452
+ "author_email": line_info["author_email"],
453
+ "sha": line_info["sha"],
454
+ "full_sha": line_info["full_sha"],
455
+ "date": line_info["date"],
456
+ "line_start": line_info["line_number"],
457
+ "line_end": line_info["line_number"],
458
+ "lines": [
459
+ {
460
+ "number": line_info["line_number"],
461
+ "content": line_info["content"],
462
+ }
463
+ ],
464
+ }
465
+
466
+ # Add the last group
467
+ current_group["line_count"] = len(current_group["lines"])
468
+ blame_groups.append(current_group)
469
+
470
+ except subprocess.CalledProcessError as e:
471
+ error_msg = e.stderr if e.stderr else str(e)
472
+ print(
473
+ f"Warning: git blame failed for {file_path}:{start_line}-{end_line}: {error_msg}"
474
+ )
475
+ return []
476
+ except Exception as e:
477
+ print(f"Error getting blame for {file_path}: {e}")
478
+ return []
479
+
480
+ return blame_groups
481
+
482
+ def get_recent_commits(self, max_count: int = 20) -> List[Dict]:
483
+ """
484
+ Get recent commits in the repository
485
+
486
+ Args:
487
+ max_count: Maximum number of commits to return
488
+
489
+ Returns:
490
+ List of recent commits with summary information
491
+ """
492
+ commits = []
493
+
494
+ for commit in self.repo.iter_commits(max_count=max_count):
495
+ # Try to get stats, but handle errors for initial/incomplete commits
496
+ try:
497
+ files_changed = len(commit.stats.files)
498
+ except Exception:
499
+ # Can't get stats (e.g., initial commit, shallow clone)
500
+ files_changed = 0
501
+
502
+ commits.append(
503
+ {
504
+ "sha": commit.hexsha[:8],
505
+ "full_sha": commit.hexsha,
506
+ "author": str(commit.author),
507
+ "date": commit.committed_datetime.isoformat(),
508
+ "message": commit.summary,
509
+ "files_changed": files_changed,
510
+ }
511
+ )
512
+
513
+ return commits
514
+
515
+ def get_commit_details(self, commit_sha: str) -> Optional[Dict]:
516
+ """
517
+ Get detailed information about a specific commit
518
+
519
+ Args:
520
+ commit_sha: Commit SHA (can be short or full)
521
+
522
+ Returns:
523
+ Detailed commit information or None if not found:
524
+ - sha: Short SHA
525
+ - full_sha: Full SHA
526
+ - author: Author name
527
+ - author_email: Author email
528
+ - date: Commit date
529
+ - message: Full commit message
530
+ - files_changed: List of files modified
531
+ - insertions: Number of lines inserted
532
+ - deletions: Number of lines deleted
533
+ """
534
+ try:
535
+ commit = self.repo.commit(commit_sha)
536
+
537
+ # Try to get stats, but handle errors for initial/incomplete commits
538
+ try:
539
+ files_changed = list(commit.stats.files.keys())
540
+ insertions = commit.stats.total["insertions"]
541
+ deletions = commit.stats.total["deletions"]
542
+ except Exception:
543
+ # Can't get stats (e.g., initial commit, shallow clone)
544
+ files_changed = []
545
+ insertions = 0
546
+ deletions = 0
547
+
548
+ return {
549
+ "sha": commit.hexsha[:8],
550
+ "full_sha": commit.hexsha,
551
+ "author": str(commit.author),
552
+ "author_email": commit.author.email,
553
+ "date": commit.committed_datetime.isoformat(),
554
+ "message": commit.message.strip(),
555
+ "files_changed": files_changed,
556
+ "insertions": insertions,
557
+ "deletions": deletions,
558
+ }
559
+ except Exception as e:
560
+ print(f"Error getting commit {commit_sha}: {e}")
561
+ return None
562
+
563
+ def search_commits(self, query: str, max_results: int = 10) -> List[Dict]:
564
+ """
565
+ Search commit messages for a query string
566
+
567
+ Args:
568
+ query: Search term to find in commit messages
569
+ max_results: Maximum results to return
570
+
571
+ Returns:
572
+ List of matching commits
573
+ """
574
+ results = []
575
+ query_lower = query.lower()
576
+
577
+ # Search through the last 500 commits
578
+ for commit in self.repo.iter_commits(max_count=500):
579
+ if query_lower in str(commit.message).lower():
580
+ results.append(
581
+ {
582
+ "sha": commit.hexsha[:8],
583
+ "full_sha": commit.hexsha,
584
+ "author": str(commit.author),
585
+ "date": commit.committed_datetime.isoformat(),
586
+ "message": commit.summary,
587
+ }
588
+ )
589
+
590
+ if len(results) >= max_results:
591
+ break
592
+
593
+ return results
594
+
595
+
596
+ def main():
597
+ """Test git helper functions"""
598
+ import sys
599
+
600
+ if len(sys.argv) < 2:
601
+ print("Usage: python -m cicada.git_helper /path/to/repo")
602
+ print("\nExample:")
603
+ print(" python -m cicada.git_helper .")
604
+ return
605
+
606
+ repo_path = sys.argv[1]
607
+
608
+ try:
609
+ helper = GitHelper(repo_path)
610
+
611
+ print("=" * 60)
612
+ print("Git Helper Test")
613
+ print("=" * 60)
614
+
615
+ print("\nšŸ“‹ Recent commits (last 5):")
616
+ for commit in helper.get_recent_commits(5):
617
+ print(f" {commit['sha']} - {commit['message']}")
618
+ print(f" by {commit['author']} ({commit['files_changed']} files)")
619
+
620
+ print("\nšŸ” Searching for 'README' in commits:")
621
+ for commit in helper.search_commits("README", 3):
622
+ print(f" {commit['sha']} - {commit['message']}")
623
+
624
+ # Try to get history for a known file
625
+ print("\nšŸ“ Testing file history:")
626
+ test_files = ["README.md", "pyproject.toml", "cicada/mcp_server.py"]
627
+ for test_file in test_files:
628
+ history = helper.get_file_history(test_file, max_commits=3)
629
+ if history:
630
+ print(f"\n {test_file} (last 3 commits):")
631
+ for commit in history:
632
+ print(f" {commit['sha']} - {commit['summary']}")
633
+ break
634
+
635
+ print("\nāœ… Git helper is working correctly!")
636
+
637
+ except git.InvalidGitRepositoryError:
638
+ print(f"āŒ Error: {repo_path} is not a git repository")
639
+ sys.exit(1)
640
+ except Exception as e:
641
+ print(f"āŒ Error: {e}")
642
+ sys.exit(1)
643
+
644
+
645
+ if __name__ == "__main__":
646
+ main()