pysealer 0.6.0__pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.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.
pysealer/__init__.py ADDED
@@ -0,0 +1,24 @@
1
+ """Pysealer package entry point
2
+
3
+ This package serves as a bridge between the Python command line interface and the
4
+ underlying Rust implementation (compiled as _pysealer module). It exposes the
5
+ core functionality for adding version control decorators to Python functions.
6
+
7
+ This module also dynamically provides decorator placeholders (e.g. @pysealer._<sig>)
8
+ so that decorated functions remain importable.
9
+ """
10
+
11
+ # Define the rust to python module version and functions
12
+ from ._pysealer import generate_keypair, generate_signature, verify_signature
13
+
14
+ __version__ = "0.6.0"
15
+ __all__ = ["generate_keypair", "generate_signature", "verify_signature"]
16
+
17
+ # Ensure dummy decorators are registered on import
18
+ from . import dummy_decorators
19
+
20
+ # Allow dynamic decorator resolution for @pyseal._<sig>()
21
+ def __getattr__(name):
22
+ if name.startswith("_"):
23
+ return dummy_decorators._dummy_decorator
24
+ raise AttributeError(f"module 'pysealer' has no attribute '{name}'")
@@ -0,0 +1,228 @@
1
+ """Automatically add cryptographic decorators to all functions and classes in a python file."""
2
+
3
+ import ast
4
+ import copy
5
+ from pathlib import Path
6
+ from pysealer import generate_signature
7
+ from .setup import get_private_key
8
+
9
+ def add_decorators(file_path: str) -> tuple[str, bool]:
10
+ """
11
+ Parse a Python file, add decorators to all functions and classes, and return the modified code.
12
+
13
+ Args:
14
+ file_path: Path to the Python file to process
15
+
16
+ Returns:
17
+ Tuple of (modified Python source code as a string, whether any decorators were added)
18
+ """
19
+ # Read the entire file content into a string
20
+ with open(file_path, 'r') as f:
21
+ content = f.read()
22
+
23
+ # Split content into lines for manipulation
24
+ lines = content.split('\n')
25
+
26
+ # Parse the Python source code into an Abstract Syntax Tree (AST)
27
+ tree = ast.parse(content)
28
+
29
+ # First pass: Remove existing pysealer decorators
30
+ lines_to_remove = set()
31
+ for node in ast.walk(tree):
32
+ if type(node).__name__ in ("FunctionDef", "AsyncFunctionDef", "ClassDef"):
33
+ if hasattr(node, 'decorator_list'):
34
+ for decorator in node.decorator_list:
35
+ is_pysealer_decorator = False
36
+
37
+ if isinstance(decorator, ast.Name):
38
+ if decorator.id.startswith("pysealer"):
39
+ is_pysealer_decorator = True
40
+ elif isinstance(decorator, ast.Attribute):
41
+ if isinstance(decorator.value, ast.Name) and decorator.value.id == "pysealer":
42
+ is_pysealer_decorator = True
43
+ elif isinstance(decorator, ast.Call):
44
+ func = decorator.func
45
+ if isinstance(func, ast.Attribute):
46
+ if isinstance(func.value, ast.Name) and func.value.id == "pysealer":
47
+ is_pysealer_decorator = True
48
+ elif isinstance(func, ast.Name) and func.id.startswith("pysealer"):
49
+ is_pysealer_decorator = True
50
+
51
+ if is_pysealer_decorator:
52
+ # Mark this line for removal (convert to 0-indexed)
53
+ lines_to_remove.add(decorator.lineno - 1)
54
+
55
+ # Remove the marked lines (in reverse order to preserve indices)
56
+ for line_idx in sorted(lines_to_remove, reverse=True):
57
+ del lines[line_idx]
58
+
59
+ # Re-parse the content after removing decorators to get updated line numbers
60
+ modified_content = '\n'.join(lines)
61
+ tree = ast.parse(modified_content)
62
+
63
+ # Build parent map for all nodes
64
+ parent_map = {}
65
+ for parent in ast.walk(tree):
66
+ for child in ast.iter_child_nodes(parent):
67
+ parent_map[child] = parent
68
+
69
+ decorators_to_add = []
70
+
71
+ for node in ast.walk(tree):
72
+ node_type = type(node).__name__
73
+
74
+ # Only decorate:
75
+ # - Top-level functions (not inside a class)
76
+ # - Top-level classes
77
+ if node_type in ("FunctionDef", "AsyncFunctionDef"):
78
+ parent = parent_map.get(node)
79
+ if isinstance(parent, ast.ClassDef):
80
+ continue # skip methods inside classes
81
+ elif node_type == "ClassDef":
82
+ pass # always decorate classes
83
+ else:
84
+ continue
85
+
86
+ # Extract the complete source code of this function/class for hashing
87
+ # Use original source to preserve formatting (quotes, spacing, etc.)
88
+ start_line = node.lineno - 1
89
+ end_line = node.end_lineno if hasattr(node, 'end_lineno') and node.end_lineno else node.lineno
90
+
91
+ # Get the source lines for this node
92
+ source_lines = lines[start_line:end_line]
93
+
94
+ # Filter out pysealer decorator lines
95
+ filtered_lines = []
96
+ for line in source_lines:
97
+ stripped = line.strip()
98
+ # Skip lines that are pysealer decorators
99
+ if stripped.startswith('@pysealer.') or stripped.startswith('@pysealer'):
100
+ continue
101
+ filtered_lines.append(line)
102
+
103
+ function_source = '\n'.join(filtered_lines)
104
+
105
+ try:
106
+ private_key = get_private_key()
107
+ except (FileNotFoundError, ValueError) as e:
108
+ raise RuntimeError(f"Cannot add decorators: {e}. Please run 'pysealer init' first.")
109
+
110
+ try:
111
+ signature = generate_signature(function_source, private_key)
112
+ except Exception as e:
113
+ raise RuntimeError(f"Failed to generate signature: {e}")
114
+
115
+ decorator_line = node.lineno - 1
116
+ if hasattr(node, 'decorator_list') and node.decorator_list:
117
+ decorator_line = node.decorator_list[0].lineno - 1
118
+
119
+ decorators_to_add.append((decorator_line, node.col_offset, signature))
120
+
121
+ # If no decorators to add, return original content
122
+ if not decorators_to_add:
123
+ return content, False
124
+
125
+ # Sort in reverse order to add from bottom to top (preserves line numbers)
126
+ decorators_to_add.sort(reverse=True)
127
+
128
+ # Add decorators to the lines first
129
+ for line_idx, col_offset, signature in decorators_to_add:
130
+ indent = ' ' * col_offset
131
+ decorator_line = f"{indent}@pysealer._{signature}()"
132
+ lines.insert(line_idx, decorator_line)
133
+
134
+ # Now add 'import pysealer' at the top if not present
135
+ has_import_pysealer = any(
136
+ line.strip() == 'import pysealer' or line.strip().startswith('import pysealer') or line.strip().startswith('from pysealer')
137
+ for line in lines
138
+ )
139
+ if not has_import_pysealer:
140
+ # Find the import block
141
+ import_indices = [i for i, line in enumerate(lines) if line.strip().startswith('import ') or line.strip().startswith('from ')]
142
+ if import_indices:
143
+ # Insert after the last import in the block
144
+ last_import = import_indices[-1]
145
+ lines.insert(last_import + 1, 'import pysealer')
146
+ else:
147
+ # No import block found, insert after shebang/docstring/comments
148
+ insert_at = 0
149
+ if lines and lines[0].startswith('#!'):
150
+ insert_at = 1
151
+ # Skip module-level docstrings and blank lines
152
+ while insert_at < len(lines):
153
+ line = lines[insert_at].strip()
154
+ if line == '':
155
+ insert_at += 1
156
+ elif line.startswith('"""') or line.startswith("'''"):
157
+ # Handle multi-line docstrings
158
+ quote = '"""' if line.startswith('"""') else "'''"
159
+ # Check if docstring ends on same line
160
+ if line.count(quote) >= 2:
161
+ insert_at += 1
162
+ else:
163
+ # Multi-line docstring
164
+ insert_at += 1
165
+ while insert_at < len(lines) and quote not in lines[insert_at]:
166
+ insert_at += 1
167
+ if insert_at < len(lines):
168
+ insert_at += 1
169
+ elif line.startswith('#'):
170
+ # Skip comments
171
+ insert_at += 1
172
+ else:
173
+ # Found first non-blank, non-comment, non-docstring line
174
+ break
175
+
176
+ lines.insert(insert_at, 'import pysealer')
177
+ # Add blank line after import if the next line isn't blank
178
+ if insert_at + 1 < len(lines) and lines[insert_at + 1].strip() != '':
179
+ lines.insert(insert_at + 1, '')
180
+
181
+ # Join lines back together
182
+ modified_code = '\n'.join(lines)
183
+
184
+ return modified_code, True
185
+
186
+
187
+ def add_decorators_to_folder(folder_path: str) -> list[str]:
188
+ """
189
+ Add decorators to all Python files in a folder.
190
+
191
+ Args:
192
+ folder_path: Path to the folder containing Python files
193
+
194
+ Returns:
195
+ List of file paths where decorators were successfully added
196
+ """
197
+ folder = Path(folder_path)
198
+
199
+ if not folder.exists():
200
+ raise FileNotFoundError(f"Folder '{folder_path}' does not exist.")
201
+
202
+ if not folder.is_dir():
203
+ raise NotADirectoryError(f"'{folder_path}' is not a directory.")
204
+
205
+ # Find all Python files in the folder (recursive)
206
+ python_files = list(folder.rglob('*.py'))
207
+
208
+ if not python_files:
209
+ raise ValueError(f"No Python files found in '{folder_path}'.")
210
+
211
+ decorated_files = []
212
+ errors = []
213
+
214
+ for py_file in python_files:
215
+ try:
216
+ modified_code, has_changes = add_decorators(str(py_file))
217
+ if has_changes:
218
+ with open(py_file, 'w') as f:
219
+ f.write(modified_code)
220
+ decorated_files.append(str(py_file))
221
+ except Exception as e:
222
+ errors.append((str(py_file), str(e)))
223
+
224
+ if errors:
225
+ error_msg = "\n".join([f" - {file}: {error}" for file, error in errors])
226
+ raise RuntimeError(f"Failed to decorate some files:\n{error_msg}")
227
+
228
+ return decorated_files
@@ -0,0 +1,186 @@
1
+ """Automatically verify cryptographic decorators for all functions and classes in a python file."""
2
+
3
+ import ast
4
+ import copy
5
+ from pathlib import Path
6
+ from typing import Dict, List, Tuple, Optional
7
+ from pysealer import verify_signature
8
+ from .setup import get_public_key
9
+ from .git_diff import get_function_diff, is_git_available
10
+
11
+
12
+ def check_decorators(file_path: str) -> Dict[str, dict]:
13
+ """
14
+ Parse a Python file and verify all pysealer cryptographic decorators.
15
+
16
+ This function checks that each function/class with a pysealer decorator has a valid
17
+ signature that matches the current source code of that function/class.
18
+
19
+ Args:
20
+ file_path: Path to the Python file to verify
21
+
22
+ Returns:
23
+ Dictionary mapping function/class names to their verification results:
24
+ {
25
+ "function_name": {
26
+ "valid": bool, # Whether signature is valid
27
+ "signature": str, # The signature found in decorator
28
+ "message": str, # Success or error message
29
+ "has_decorator": bool, # Whether function has pysealer decorator
30
+ "line_start": int, # Starting line number
31
+ "line_end": int, # Ending line number
32
+ "source": str, # Function source code
33
+ "diff": List[Tuple] # Git diff if validation failed
34
+ }
35
+ }
36
+ """
37
+ # Read the file content
38
+ with open(file_path, 'r') as f:
39
+ content = f.read()
40
+
41
+ # Parse the Python source code into an AST
42
+ tree = ast.parse(content)
43
+
44
+ # Get the public key for verification
45
+ try:
46
+ public_key = get_public_key()
47
+ except (FileNotFoundError, ValueError) as e:
48
+ raise RuntimeError(f"Cannot verify decorators: {e}. Please run 'pysealer init' first.")
49
+
50
+ # Dictionary to store results
51
+ results = {}
52
+
53
+ # Iterate through each node in the AST
54
+ for node in ast.walk(tree):
55
+ node_type = type(node).__name__
56
+
57
+ # Check if this node is a function or class definition
58
+ if node_type in ("FunctionDef", "AsyncFunctionDef", "ClassDef"):
59
+ name = node.name
60
+
61
+ # Look for pysealer decorator
62
+ signature_from_decorator = None
63
+ has_pysealer_decorator = False
64
+
65
+ if hasattr(node, 'decorator_list'):
66
+ for decorator in node.decorator_list:
67
+ # Check if decorator is a Call node (e.g., @pysealer._<signature>())
68
+ if isinstance(decorator, ast.Call):
69
+ func = decorator.func
70
+ # Check if it's pysealer._<signature>
71
+ if isinstance(func, ast.Attribute):
72
+ if isinstance(func.value, ast.Name) and func.value.id == "pysealer":
73
+ attr_name = func.attr
74
+ # Extract signature (remove leading underscore)
75
+ if attr_name.startswith('_'):
76
+ signature_from_decorator = attr_name[1:]
77
+ has_pysealer_decorator = True
78
+ break
79
+
80
+ # Initialize result for this function/class
81
+ result = {
82
+ "has_decorator": has_pysealer_decorator,
83
+ "valid": False,
84
+ "signature": signature_from_decorator,
85
+ "message": "",
86
+ "line_start": node.lineno,
87
+ "line_end": node.end_lineno if hasattr(node, 'end_lineno') and node.end_lineno else node.lineno,
88
+ "source": "",
89
+ "diff": None
90
+ }
91
+
92
+ if not has_pysealer_decorator:
93
+ result["message"] = "No pysealer decorator found"
94
+ results[name] = result
95
+ continue
96
+
97
+ # Extract the source code without pysealer decorators for verification
98
+ # Use original source to preserve formatting (quotes, spacing, etc.)
99
+ content_lines = content.split('\n')
100
+ start_line = node.lineno - 1
101
+ end_line = node.end_lineno if hasattr(node, 'end_lineno') and node.end_lineno else node.lineno
102
+
103
+ # Get the source lines for this node
104
+ source_lines = content_lines[start_line:end_line]
105
+
106
+ # Filter out pysealer decorator lines
107
+ filtered_lines = []
108
+ for line in source_lines:
109
+ stripped = line.strip()
110
+ # Skip lines that are pysealer decorators
111
+ if stripped.startswith('@pysealer.') or stripped.startswith('@pysealer'):
112
+ continue
113
+ filtered_lines.append(line)
114
+
115
+ function_source = '\n'.join(filtered_lines)
116
+
117
+ # Store the source code
118
+ result["source"] = function_source
119
+
120
+ # Verify the signature
121
+ try:
122
+ is_valid = verify_signature(function_source, signature_from_decorator, public_key)
123
+
124
+ result["valid"] = is_valid
125
+ if is_valid:
126
+ result["message"] = "✓ Signature valid - code has not been tampered with"
127
+ else:
128
+ result["message"] = "✗ Signature invalid - code may have been modified"
129
+
130
+ # Try to get git diff for failed validation (only if git is available)
131
+ if is_git_available():
132
+ try:
133
+ diff = get_function_diff(
134
+ file_path,
135
+ name,
136
+ function_source,
137
+ node.lineno
138
+ )
139
+ if diff:
140
+ result["diff"] = diff
141
+ except Exception:
142
+ # If git diff fails, just continue without it
143
+ pass
144
+
145
+ except Exception as e:
146
+ result["message"] = f"✗ Error verifying signature: {e}"
147
+
148
+ results[name] = result
149
+
150
+ return results
151
+
152
+
153
+ def check_decorators_in_folder(folder_path: str) -> Dict[str, Dict[str, dict]]:
154
+ """
155
+ Check decorators in all Python files in a folder.
156
+
157
+ Args:
158
+ folder_path: Path to the folder containing Python files
159
+
160
+ Returns:
161
+ Dictionary mapping file paths to their verification results
162
+ """
163
+ folder = Path(folder_path)
164
+
165
+ if not folder.exists():
166
+ raise FileNotFoundError(f"Folder '{folder_path}' does not exist.")
167
+
168
+ if not folder.is_dir():
169
+ raise NotADirectoryError(f"'{folder_path}' is not a directory.")
170
+
171
+ # Find all Python files in the folder (recursive)
172
+ python_files = list(folder.rglob('*.py'))
173
+
174
+ if not python_files:
175
+ raise ValueError(f"No Python files found in '{folder_path}'.")
176
+
177
+ all_results = {}
178
+
179
+ for py_file in python_files:
180
+ try:
181
+ results = check_decorators(str(py_file))
182
+ all_results[str(py_file)] = results
183
+ except Exception as e:
184
+ all_results[str(py_file)] = {"error": str(e)}
185
+
186
+ return all_results