pysealer 0.1.4__pp39-pypy39_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.1.4"
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,215 @@
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) -> str:
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
+ Modified Python source code as a string
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
+ # Dynamically add 'import pysealer' grouped with other imports if not present
27
+ has_import_pysealer = any(
28
+ line.strip() == 'import pysealer' or line.strip().startswith('import pysealer') or line.strip().startswith('from pysealer')
29
+ for line in lines
30
+ )
31
+ if not has_import_pysealer:
32
+ # Find the import block
33
+ import_indices = [i for i, line in enumerate(lines) if line.strip().startswith('import ') or line.strip().startswith('from ')]
34
+ if import_indices:
35
+ # Insert after the last import in the block
36
+ last_import = import_indices[-1]
37
+ lines.insert(last_import + 1, 'import pysealer')
38
+ else:
39
+ # No import block found, insert after shebang/docstring/comments as before
40
+ insert_at = 0
41
+ if lines and lines[0].startswith('#!'):
42
+ insert_at = 1
43
+ while insert_at < len(lines) and (lines[insert_at].strip() == '' or lines[insert_at].strip().startswith('"""') or lines[insert_at].strip().startswith("''")):
44
+ if lines[insert_at].strip().startswith('"""') or lines[insert_at].strip().startswith("''"):
45
+ quote = lines[insert_at].strip()[:3]
46
+ insert_at += 1
47
+ while insert_at < len(lines) and not lines[insert_at].strip().endswith(quote):
48
+ insert_at += 1
49
+ if insert_at < len(lines):
50
+ insert_at += 1
51
+ else:
52
+ insert_at += 1
53
+ lines.insert(insert_at, 'import pysealer')
54
+
55
+ # Parse the Python source code into an Abstract Syntax Tree (AST)
56
+ content = '\n'.join(lines)
57
+ tree = ast.parse(content)
58
+
59
+ # First pass: Remove existing pysealer decorators
60
+ lines_to_remove = set()
61
+ for node in ast.walk(tree):
62
+ if type(node).__name__ in ("FunctionDef", "AsyncFunctionDef", "ClassDef"):
63
+ if hasattr(node, 'decorator_list'):
64
+ for decorator in node.decorator_list:
65
+ is_pysealer_decorator = False
66
+
67
+ if isinstance(decorator, ast.Name):
68
+ if decorator.id.startswith("pysealer"):
69
+ is_pysealer_decorator = True
70
+ elif isinstance(decorator, ast.Attribute):
71
+ if isinstance(decorator.value, ast.Name) and decorator.value.id == "pysealer":
72
+ is_pysealer_decorator = True
73
+ elif isinstance(decorator, ast.Call):
74
+ func = decorator.func
75
+ if isinstance(func, ast.Attribute):
76
+ if isinstance(func.value, ast.Name) and func.value.id == "pysealer":
77
+ is_pysealer_decorator = True
78
+ elif isinstance(func, ast.Name) and func.id.startswith("pysealer"):
79
+ is_pysealer_decorator = True
80
+
81
+ if is_pysealer_decorator:
82
+ # Mark this line for removal (convert to 0-indexed)
83
+ lines_to_remove.add(decorator.lineno - 1)
84
+
85
+ # Remove the marked lines (in reverse order to preserve indices)
86
+ for line_idx in sorted(lines_to_remove, reverse=True):
87
+ del lines[line_idx]
88
+
89
+ # Re-parse the content after removing decorators to get updated line numbers
90
+ modified_content = '\n'.join(lines)
91
+ tree = ast.parse(modified_content)
92
+
93
+ # Build parent map for all nodes
94
+ parent_map = {}
95
+ for parent in ast.walk(tree):
96
+ for child in ast.iter_child_nodes(parent):
97
+ parent_map[child] = parent
98
+
99
+ decorators_to_add = []
100
+
101
+ for node in ast.walk(tree):
102
+ node_type = type(node).__name__
103
+
104
+ # Only decorate:
105
+ # - Top-level functions (not inside a class)
106
+ # - Top-level classes
107
+ if node_type in ("FunctionDef", "AsyncFunctionDef"):
108
+ parent = parent_map.get(node)
109
+ if isinstance(parent, ast.ClassDef):
110
+ continue # skip methods inside classes
111
+ elif node_type == "ClassDef":
112
+ pass # always decorate classes
113
+ else:
114
+ continue
115
+
116
+ # Extract the complete source code of this function/class for hashing
117
+ node_clone = copy.deepcopy(node)
118
+
119
+ # Filter out pysealer decorators
120
+ if hasattr(node_clone, 'decorator_list'):
121
+ filtered_decorators = []
122
+ for decorator in node_clone.decorator_list:
123
+ should_keep = True
124
+ if isinstance(decorator, ast.Name):
125
+ if decorator.id.startswith("pysealer"):
126
+ should_keep = False
127
+ elif isinstance(decorator, ast.Attribute):
128
+ if isinstance(decorator.value, ast.Name) and decorator.value.id == "pysealer":
129
+ should_keep = False
130
+ elif isinstance(decorator, ast.Call):
131
+ func = decorator.func
132
+ if isinstance(func, ast.Attribute):
133
+ if isinstance(func.value, ast.Name) and func.value.id == "pysealer":
134
+ should_keep = False
135
+ elif isinstance(func, ast.Name) and func.id.startswith("pysealer"):
136
+ should_keep = False
137
+ if should_keep:
138
+ filtered_decorators.append(decorator)
139
+ node_clone.decorator_list = filtered_decorators
140
+
141
+ module_wrapper = ast.Module(body=[node_clone], type_ignores=[])
142
+ function_source = ast.unparse(module_wrapper)
143
+
144
+ try:
145
+ private_key = get_private_key()
146
+ except (FileNotFoundError, ValueError) as e:
147
+ raise RuntimeError(f"Cannot add decorators: {e}. Please run 'pysealer init' first.")
148
+
149
+ try:
150
+ signature = generate_signature(function_source, private_key)
151
+ except Exception as e:
152
+ raise RuntimeError(f"Failed to generate signature: {e}")
153
+
154
+ decorator_line = node.lineno - 1
155
+ if hasattr(node, 'decorator_list') and node.decorator_list:
156
+ decorator_line = node.decorator_list[0].lineno - 1
157
+
158
+ decorators_to_add.append((decorator_line, node.col_offset, signature))
159
+
160
+ # Sort in reverse order to add from bottom to top (preserves line numbers)
161
+ decorators_to_add.sort(reverse=True)
162
+
163
+ # Add decorators to the lines
164
+ for line_idx, col_offset, signature in decorators_to_add:
165
+ indent = ' ' * col_offset
166
+ decorator_line = f"{indent}@pysealer._{signature}()"
167
+ lines.insert(line_idx, decorator_line)
168
+
169
+ # Join lines back together
170
+ modified_code = '\n'.join(lines)
171
+
172
+ return modified_code
173
+
174
+
175
+ def add_decorators_to_folder(folder_path: str) -> list[str]:
176
+ """
177
+ Add decorators to all Python files in a folder.
178
+
179
+ Args:
180
+ folder_path: Path to the folder containing Python files
181
+
182
+ Returns:
183
+ List of file paths that were successfully decorated
184
+ """
185
+ folder = Path(folder_path)
186
+
187
+ if not folder.exists():
188
+ raise FileNotFoundError(f"Folder '{folder_path}' does not exist.")
189
+
190
+ if not folder.is_dir():
191
+ raise NotADirectoryError(f"'{folder_path}' is not a directory.")
192
+
193
+ # Find all Python files in the folder (non-recursive)
194
+ python_files = list(folder.glob('*.py'))
195
+
196
+ if not python_files:
197
+ raise ValueError(f"No Python files found in '{folder_path}'.")
198
+
199
+ decorated_files = []
200
+ errors = []
201
+
202
+ for py_file in python_files:
203
+ try:
204
+ modified_code = add_decorators(str(py_file))
205
+ with open(py_file, 'w') as f:
206
+ f.write(modified_code)
207
+ decorated_files.append(str(py_file))
208
+ except Exception as e:
209
+ errors.append((str(py_file), str(e)))
210
+
211
+ if errors:
212
+ error_msg = "\n".join([f" - {file}: {error}" for file, error in errors])
213
+ raise RuntimeError(f"Failed to decorate some files:\n{error_msg}")
214
+
215
+ return decorated_files
@@ -0,0 +1,179 @@
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
7
+ from pysealer import verify_signature
8
+ from .setup import get_public_key
9
+
10
+
11
+ def check_decorators(file_path: str) -> Dict[str, dict]:
12
+ """
13
+ Parse a Python file and verify all pysealer cryptographic decorators.
14
+
15
+ This function checks that each function/class with a pysealer decorator has a valid
16
+ signature that matches the current source code of that function/class.
17
+
18
+ Args:
19
+ file_path: Path to the Python file to verify
20
+
21
+ Returns:
22
+ Dictionary mapping function/class names to their verification results:
23
+ {
24
+ "function_name": {
25
+ "valid": bool, # Whether signature is valid
26
+ "signature": str, # The signature found in decorator
27
+ "message": str, # Success or error message
28
+ "has_decorator": bool # Whether function has pysealer decorator
29
+ }
30
+ }
31
+ """
32
+ # Read the file content
33
+ with open(file_path, 'r') as f:
34
+ content = f.read()
35
+
36
+ # Parse the Python source code into an AST
37
+ tree = ast.parse(content)
38
+
39
+ # Get the public key for verification
40
+ try:
41
+ public_key = get_public_key()
42
+ except (FileNotFoundError, ValueError) as e:
43
+ raise RuntimeError(f"Cannot verify decorators: {e}. Please run 'pysealer init' first.")
44
+
45
+ # Dictionary to store results
46
+ results = {}
47
+
48
+ # Iterate through each node in the AST
49
+ for node in ast.walk(tree):
50
+ node_type = type(node).__name__
51
+
52
+ # Check if this node is a function or class definition
53
+ if node_type in ("FunctionDef", "AsyncFunctionDef", "ClassDef"):
54
+ name = node.name
55
+
56
+ # Look for pysealer decorator
57
+ signature_from_decorator = None
58
+ has_pysealer_decorator = False
59
+
60
+ if hasattr(node, 'decorator_list'):
61
+ for decorator in node.decorator_list:
62
+ # Check if decorator is a Call node (e.g., @pysealer._<signature>())
63
+ if isinstance(decorator, ast.Call):
64
+ func = decorator.func
65
+ # Check if it's pysealer._<signature>
66
+ if isinstance(func, ast.Attribute):
67
+ if isinstance(func.value, ast.Name) and func.value.id == "pysealer":
68
+ attr_name = func.attr
69
+ # Extract signature (remove leading underscore)
70
+ if attr_name.startswith('_'):
71
+ signature_from_decorator = attr_name[1:]
72
+ has_pysealer_decorator = True
73
+ break
74
+
75
+ # Initialize result for this function/class
76
+ result = {
77
+ "has_decorator": has_pysealer_decorator,
78
+ "valid": False,
79
+ "signature": signature_from_decorator,
80
+ "message": ""
81
+ }
82
+
83
+ if not has_pysealer_decorator:
84
+ result["message"] = "No pysealer decorator found"
85
+ results[name] = result
86
+ continue
87
+
88
+ # Extract the source code without pysealer decorators for verification
89
+ node_clone = copy.deepcopy(node)
90
+
91
+ # Filter out pysealer decorators from the clone
92
+ if hasattr(node_clone, 'decorator_list'):
93
+ filtered_decorators = []
94
+
95
+ for decorator in node_clone.decorator_list:
96
+ should_keep = True
97
+
98
+ # Check if decorator is a simple Name node starting with "pysealer"
99
+ if isinstance(decorator, ast.Name):
100
+ if decorator.id.startswith("pysealer"):
101
+ should_keep = False
102
+
103
+ # Check if decorator is an Attribute node (e.g., pysealer.something)
104
+ elif isinstance(decorator, ast.Attribute):
105
+ if isinstance(decorator.value, ast.Name) and decorator.value.id == "pysealer":
106
+ should_keep = False
107
+
108
+ # Check if decorator is a Call node
109
+ elif isinstance(decorator, ast.Call):
110
+ func = decorator.func
111
+ # Check if call is to pysealer.something()
112
+ if isinstance(func, ast.Attribute):
113
+ if isinstance(func.value, ast.Name) and func.value.id == "pysealer":
114
+ should_keep = False
115
+ # Check if call is to pysealer_something()
116
+ elif isinstance(func, ast.Name) and func.id.startswith("pysealer"):
117
+ should_keep = False
118
+
119
+ if should_keep:
120
+ filtered_decorators.append(decorator)
121
+
122
+ node_clone.decorator_list = filtered_decorators
123
+
124
+ # Convert the filtered node back to source code
125
+ module_wrapper = ast.Module(body=[node_clone], type_ignores=[])
126
+ function_source = ast.unparse(module_wrapper)
127
+
128
+ # Verify the signature
129
+ try:
130
+ is_valid = verify_signature(function_source, signature_from_decorator, public_key)
131
+
132
+ result["valid"] = is_valid
133
+ if is_valid:
134
+ result["message"] = "✓ Signature valid - code has not been tampered with"
135
+ else:
136
+ result["message"] = "✗ Signature invalid - code may have been modified"
137
+
138
+ except Exception as e:
139
+ result["message"] = f"✗ Error verifying signature: {e}"
140
+
141
+ results[name] = result
142
+
143
+ return results
144
+
145
+
146
+ def check_decorators_in_folder(folder_path: str) -> Dict[str, Dict[str, dict]]:
147
+ """
148
+ Check decorators in all Python files in a folder.
149
+
150
+ Args:
151
+ folder_path: Path to the folder containing Python files
152
+
153
+ Returns:
154
+ Dictionary mapping file paths to their verification results
155
+ """
156
+ folder = Path(folder_path)
157
+
158
+ if not folder.exists():
159
+ raise FileNotFoundError(f"Folder '{folder_path}' does not exist.")
160
+
161
+ if not folder.is_dir():
162
+ raise NotADirectoryError(f"'{folder_path}' is not a directory.")
163
+
164
+ # Find all Python files in the folder (non-recursive)
165
+ python_files = list(folder.glob('*.py'))
166
+
167
+ if not python_files:
168
+ raise ValueError(f"No Python files found in '{folder_path}'.")
169
+
170
+ all_results = {}
171
+
172
+ for py_file in python_files:
173
+ try:
174
+ results = check_decorators(str(py_file))
175
+ all_results[str(py_file)] = results
176
+ except Exception as e:
177
+ all_results[str(py_file)] = {"error": str(e)}
178
+
179
+ return all_results
pysealer/cli.py ADDED
@@ -0,0 +1,284 @@
1
+ """
2
+ Command-line interface for pysealer.
3
+
4
+ Commands:
5
+ - init: Initialize pysealer with a new keypair and .env file.
6
+ - lock: Add pysealer decorators to all functions and classes in a Python file.
7
+ - check: Check the integrity and validity of pysealer decorators in a Python file.
8
+ - remove: Remove all pysealer decorators from a Python file.
9
+
10
+ Use `pysealer --help` to see available options and command details.
11
+ Use `pysealer --version` to see the current version of pysealer installed.
12
+ """
13
+
14
+ from pathlib import Path
15
+
16
+ import typer
17
+ from typing_extensions import Annotated
18
+
19
+ from . import __version__
20
+ from .setup import setup_keypair
21
+ from .add_decorators import add_decorators, add_decorators_to_folder
22
+ from .check_decorators import check_decorators, check_decorators_in_folder
23
+ from .remove_decorators import remove_decorators, remove_decorators_from_folder
24
+
25
+ app = typer.Typer(
26
+ name="pysealer",
27
+ help="Version control your Python functions and classes with cryptographic decorators",
28
+ no_args_is_help=True,
29
+ )
30
+
31
+
32
+ def version_callback(value: bool):
33
+ """Helper function to display version information."""
34
+ if value:
35
+ typer.echo(f"pysealer {__version__}")
36
+ raise typer.Exit()
37
+
38
+
39
+ @app.callback()
40
+ def version(
41
+ version: Annotated[
42
+ bool,
43
+ typer.Option("--version", help="Report the current version of pysealer installed.", callback=version_callback, is_eager=True)
44
+ ] = False
45
+ ):
46
+ """Report the current version of pysealer installed."""
47
+ pass
48
+
49
+
50
+ @app.command()
51
+ def init(
52
+ env_file: Annotated[
53
+ str,
54
+ typer.Argument(help="Path to the .env file")
55
+ ] = ".env"
56
+ ):
57
+ """Initialize pysealer with an .env file."""
58
+ try:
59
+ env_path = Path(env_file)
60
+
61
+ # Generate and store keypair (will raise error if keys already exist)
62
+ setup_keypair(env_path)
63
+ typer.echo(typer.style("Successfully initialized pysealer!", fg=typer.colors.BLUE, bold=True))
64
+ typer.echo(f"🔑 Keypair generated and stored in {env_path}")
65
+ typer.echo("⚠️ Keep your .env file secure and add it to .gitignore!")
66
+
67
+ except Exception as e:
68
+ typer.echo(typer.style(f"Error during initialization: {e}", fg=typer.colors.RED, bold=True), err=True)
69
+ raise typer.Exit(code=1)
70
+
71
+
72
+ @app.command()
73
+ def lock(
74
+ file_path: Annotated[
75
+ str,
76
+ typer.Argument(help="Path to the Python file or folder to decorate")
77
+ ]
78
+ ):
79
+ """Add decorators to all functions and classes in a Python file or all Python files in a folder."""
80
+ path = Path(file_path)
81
+
82
+ # Validate path exists
83
+ if not path.exists():
84
+ typer.echo(typer.style(f"Error: Path '{path}' does not exist.", fg=typer.colors.RED, bold=True), err=True)
85
+ raise typer.Exit(code=1)
86
+
87
+ try:
88
+ # Handle folder path
89
+ if path.is_dir():
90
+ resolved_path = str(path.resolve())
91
+ decorated_files = add_decorators_to_folder(resolved_path)
92
+
93
+ typer.echo(typer.style(f"Successfully added decorators to {len(decorated_files)} files:", fg=typer.colors.BLUE, bold=True))
94
+ for file in decorated_files:
95
+ typer.echo(f" {typer.style('✓', fg=typer.colors.GREEN)} {file}")
96
+
97
+ # Handle file path
98
+ else:
99
+ # Validate it's a Python file
100
+ if not path.suffix == '.py':
101
+ typer.echo(typer.style(f"Error: File '{path}' is not a Python file.", fg=typer.colors.RED, bold=True), err=True)
102
+ raise typer.Exit(code=1)
103
+
104
+ # Add decorators to all functions and classes in the file
105
+ resolved_path = str(path.resolve())
106
+ modified_code = add_decorators(resolved_path)
107
+
108
+ # Write the modified code back to the file
109
+ with open(resolved_path, 'w') as f:
110
+ f.write(modified_code)
111
+
112
+ typer.echo(typer.style(f"Successfully added decorators to 1 file:", fg=typer.colors.BLUE, bold=True))
113
+ typer.echo(f" {typer.style('✓', fg=typer.colors.GREEN)} {resolved_path}")
114
+
115
+ except (RuntimeError, FileNotFoundError, NotADirectoryError, ValueError) as e:
116
+ typer.echo(typer.style(f"Error: {e}", fg=typer.colors.RED, bold=True), err=True)
117
+ raise typer.Exit(code=1)
118
+ except Exception as e:
119
+ typer.echo(typer.style(f"Unexpected error while locking file: {e}", fg=typer.colors.RED, bold=True), err=True)
120
+ raise typer.Exit(code=1)
121
+
122
+
123
+ @app.command()
124
+ def check(
125
+ file_path: Annotated[
126
+ str,
127
+ typer.Argument(help="Path to the Python file or folder to check")
128
+ ]
129
+ ):
130
+ """Check the integrity of decorators in a Python file or all Python files in a folder."""
131
+ path = Path(file_path)
132
+
133
+ # Validate path exists
134
+ if not path.exists():
135
+ typer.echo(typer.style(f"Error: Path '{path}' does not exist.", fg=typer.colors.RED, bold=True), err=True)
136
+ raise typer.Exit(code=1)
137
+
138
+ try:
139
+ # Handle folder path
140
+ if path.is_dir():
141
+ resolved_path = str(path.resolve())
142
+ all_results = check_decorators_in_folder(resolved_path)
143
+
144
+ total_decorated = 0
145
+ total_valid = 0
146
+ total_files = 0
147
+ files_with_issues = []
148
+
149
+ for file_path, results in all_results.items():
150
+ # Skip files with errors
151
+ if "error" in results:
152
+ typer.echo(typer.style(f"✗ {file_path}: {results['error']}", fg=typer.colors.RED))
153
+ files_with_issues.append(file_path)
154
+ continue
155
+
156
+ total_files += 1
157
+ decorated_count = sum(1 for r in results.values() if r["has_decorator"])
158
+ valid_count = sum(1 for r in results.values() if r["valid"])
159
+
160
+ total_decorated += decorated_count
161
+ total_valid += valid_count
162
+
163
+ # Track files with validation failures
164
+ if decorated_count > 0 and valid_count < decorated_count:
165
+ files_with_issues.append(file_path)
166
+
167
+ # Summary header
168
+ if total_decorated == 0:
169
+ typer.echo("⚠️ No pysealer decorators found in any files.")
170
+ elif total_valid == total_decorated:
171
+ typer.echo(typer.style(f"All decorators are valid in {total_files} files:", fg=typer.colors.BLUE, bold=True))
172
+ else:
173
+ typer.echo(typer.style(f"{total_decorated - total_valid}/{total_decorated} decorators failed verification across {total_files} files:", fg=typer.colors.BLUE, bold=True), err=True)
174
+
175
+ # File-by-file details
176
+ for file_path, results in all_results.items():
177
+ if "error" in results:
178
+ continue
179
+
180
+ decorated_count = sum(1 for r in results.values() if r["has_decorator"])
181
+ valid_count = sum(1 for r in results.values() if r["valid"])
182
+
183
+ if decorated_count > 0:
184
+ if valid_count == decorated_count:
185
+ typer.echo(f" {typer.style('✓', fg=typer.colors.GREEN)} {file_path}: {typer.style(f'{decorated_count} decorators valid', fg=typer.colors.GREEN)}")
186
+ else:
187
+ typer.echo(f" {typer.style('✗', fg=typer.colors.RED)} {file_path}: {typer.style(f'{decorated_count - valid_count}/{decorated_count} decorators failed', fg=typer.colors.RED)}")
188
+
189
+ # Exit with error if there were failures
190
+ if total_decorated > 0 and total_valid < total_decorated:
191
+ raise typer.Exit(code=1)
192
+
193
+ # Handle file path
194
+ else:
195
+ # Validate it's a Python file
196
+ if not path.suffix == '.py':
197
+ typer.echo(typer.style(f"Error: File '{path}' is not a Python file.", fg=typer.colors.RED, bold=True), err=True)
198
+ raise typer.Exit(code=1)
199
+
200
+ # Check all decorators in the file
201
+ resolved_path = str(path.resolve())
202
+ results = check_decorators(resolved_path)
203
+
204
+ # Return success if all decorated functions are valid
205
+ decorated_count = sum(1 for r in results.values() if r["has_decorator"])
206
+ valid_count = sum(1 for r in results.values() if r["valid"])
207
+
208
+ if decorated_count == 0:
209
+ typer.echo("⚠️ No pysealer decorators found in this file.")
210
+ elif valid_count == decorated_count:
211
+ typer.echo(typer.style("All decorators are valid in 1 file:", fg=typer.colors.BLUE, bold=True))
212
+ typer.echo(f"{typer.style('✓', fg=typer.colors.GREEN)} {resolved_path}: {typer.style(f'{decorated_count} decorators valid', fg=typer.colors.GREEN)}")
213
+ else:
214
+ typer.echo(typer.style(f"{decorated_count - valid_count}/{decorated_count} decorators failed verification across 1 file:", fg=typer.colors.BLUE, bold=True), err=True)
215
+ typer.echo(f" {typer.style('✗', fg=typer.colors.RED)} {resolved_path}: {typer.style(f'{decorated_count - valid_count}/{decorated_count} decorators failed', fg=typer.colors.RED)}")
216
+ raise typer.Exit(code=1)
217
+
218
+ except (FileNotFoundError, NotADirectoryError, ValueError) as e:
219
+ typer.echo(typer.style(f"Error: {e}", fg=typer.colors.RED, bold=True), err=True)
220
+ raise typer.Exit(code=1)
221
+
222
+
223
+ @app.command()
224
+ def remove(
225
+ file_path: Annotated[
226
+ str,
227
+ typer.Argument(help="Path to the Python file or folder to remove pysealer decorators from")
228
+ ]
229
+ ):
230
+ """Remove pysealer decorators from all functions and classes in a Python file or all Python files in a folder."""
231
+ path = Path(file_path)
232
+
233
+ # Validate path exists
234
+ if not path.exists():
235
+ typer.echo(typer.style(f"Error: Path '{path}' does not exist.", fg=typer.colors.RED, bold=True), err=True)
236
+ raise typer.Exit(code=1)
237
+
238
+ try:
239
+ # Handle folder path
240
+ if path.is_dir():
241
+ resolved_path = str(path.resolve())
242
+ modified_files = remove_decorators_from_folder(resolved_path)
243
+
244
+ if modified_files:
245
+ typer.echo(typer.style(f"Successfully removed decorators from {len(modified_files)} files:", fg=typer.colors.BLUE, bold=True))
246
+ for file in modified_files:
247
+ typer.echo(f" {typer.style('✓', fg=typer.colors.GREEN)} {file}")
248
+ else:
249
+ typer.echo("⚠️ No pysealer decorators found in any files.")
250
+
251
+ # Handle file path
252
+ else:
253
+ # Validate it's a Python file
254
+ if not path.suffix == '.py':
255
+ typer.echo(typer.style(f"Error: File '{path}' is not a Python file.", fg=typer.colors.RED, bold=True), err=True)
256
+ raise typer.Exit(code=1)
257
+
258
+ resolved_path = str(path.resolve())
259
+ modified_code, found = remove_decorators(resolved_path)
260
+
261
+ with open(resolved_path, 'w') as f:
262
+ f.write(modified_code)
263
+
264
+ if found:
265
+ typer.echo(typer.style(f"Successfully removed decorators from 1 file:", fg=typer.colors.BLUE, bold=True))
266
+ typer.echo(f" {typer.style('✓', fg=typer.colors.GREEN)} {resolved_path}")
267
+ else:
268
+ typer.echo(f"⚠️ No pysealer decorators found in {resolved_path}")
269
+
270
+ except (FileNotFoundError, NotADirectoryError, ValueError) as e:
271
+ typer.echo(typer.style(f"Error: {e}", fg=typer.colors.RED, bold=True), err=True)
272
+ raise typer.Exit(code=1)
273
+ except Exception as e:
274
+ typer.echo(typer.style(f"Unexpected error while removing decorators: {e}", fg=typer.colors.RED, bold=True), err=True)
275
+ raise typer.Exit(code=1)
276
+
277
+
278
+ def main():
279
+ """Main CLI entry point."""
280
+ app()
281
+
282
+
283
+ if __name__ == '__main__':
284
+ main()
@@ -0,0 +1,83 @@
1
+ """Defines dummy decorators for all decorators found in the target file."""
2
+
3
+ import os
4
+ import ast
5
+ import inspect
6
+
7
+
8
+ def _dummy_decorator(func=None, *args, **kwargs):
9
+ """
10
+ A no-op (dummy) decorator that can be used in place of any decorator.
11
+
12
+ Handles both @deco and @deco(...) usages. If used as @deco, it returns the function unchanged.
13
+ If used as @deco(...), it returns a wrapper that returns the function unchanged.
14
+
15
+ Args:
16
+ func (callable, optional): The function to decorate, or None if called with arguments.
17
+ *args: Positional arguments (ignored).
18
+ **kwargs: Keyword arguments (ignored).
19
+
20
+ Returns:
21
+ callable: The original function, unchanged.
22
+ """
23
+ if callable(func) and not args and not kwargs:
24
+ return func
25
+ def wrapper(f):
26
+ return f
27
+ return wrapper
28
+
29
+ def _discover_decorators(file_path):
30
+ """
31
+ Yield all decorator names used in the given Python file.
32
+
33
+ This function parses the specified Python file and walks its AST to find all decorator names
34
+ used on functions, async functions, and classes. It handles decorators used as @deco, @deco(...),
35
+ and @obj.deco or @obj.deco(...).
36
+
37
+ Args:
38
+ file_path (str): Path to the Python file to scan.
39
+
40
+ Yields:
41
+ str: The name of each decorator found.
42
+ """
43
+ if not os.path.exists(file_path):
44
+ return
45
+ with open(file_path, "r") as f:
46
+ src = f.read()
47
+ try:
48
+ tree = ast.parse(src)
49
+ except Exception:
50
+ return
51
+ for node in ast.walk(tree):
52
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
53
+ for deco in node.decorator_list:
54
+ # Handles @deco or @deco(...)
55
+ if isinstance(deco, ast.Name):
56
+ yield deco.id
57
+ elif isinstance(deco, ast.Attribute):
58
+ yield deco.attr
59
+ elif isinstance(deco.func, ast.Name):
60
+ yield deco.func.id
61
+ elif isinstance(deco.func, ast.Attribute):
62
+ yield deco.func.attr
63
+
64
+ def _get_caller_file():
65
+ """
66
+ Find the filename of the module that imported this module at the top level.
67
+
68
+ Returns:
69
+ str or None: The filename of the caller module, or None if not found.
70
+ """
71
+ stack = inspect.stack()
72
+ for frame in stack:
73
+ if frame.function == "<module>" and frame.filename != __file__:
74
+ return frame.filename
75
+
76
+ # Main logic: define dummy decorators for all found in the caller file
77
+ _CALLER_FILE = _get_caller_file()
78
+ if _CALLER_FILE:
79
+ _seen = set()
80
+ for deco_name in _discover_decorators(_CALLER_FILE):
81
+ if deco_name and deco_name not in globals() and deco_name not in _seen:
82
+ globals()[deco_name] = _dummy_decorator
83
+ _seen.add(deco_name)
@@ -0,0 +1,88 @@
1
+ """Remove cryptographic pysealer decorators from all functions and classes in a Python file."""
2
+
3
+ import ast
4
+ from typing import List, Tuple, Dict
5
+ from pathlib import Path
6
+
7
+ def remove_decorators(file_path: str) -> Tuple[str, bool]:
8
+ """
9
+ Parse a Python file, remove all @pysealer.* decorators from functions and classes, and return the modified code.
10
+
11
+ Args:
12
+ file_path: Path to the Python file to process
13
+ Returns:
14
+ Modified Python source code as a string
15
+ """
16
+ with open(file_path, 'r') as f:
17
+ content = f.read()
18
+
19
+ tree = ast.parse(content)
20
+ lines = content.split('\n')
21
+ lines_to_remove = set()
22
+
23
+ for node in ast.walk(tree):
24
+ if type(node).__name__ in ("FunctionDef", "AsyncFunctionDef", "ClassDef"):
25
+ if hasattr(node, 'decorator_list'):
26
+ for decorator in node.decorator_list:
27
+ is_pysealer_decorator = False
28
+ if isinstance(decorator, ast.Name):
29
+ if decorator.id.startswith("pysealer"):
30
+ is_pysealer_decorator = True
31
+ elif isinstance(decorator, ast.Attribute):
32
+ if isinstance(decorator.value, ast.Name) and decorator.value.id == "pysealer":
33
+ is_pysealer_decorator = True
34
+ elif isinstance(decorator, ast.Call):
35
+ func = decorator.func
36
+ if isinstance(func, ast.Attribute):
37
+ if isinstance(func.value, ast.Name) and func.value.id == "pysealer":
38
+ is_pysealer_decorator = True
39
+ elif isinstance(func, ast.Name) and func.id.startswith("pysealer"):
40
+ is_pysealer_decorator = True
41
+ if is_pysealer_decorator:
42
+ lines_to_remove.add(decorator.lineno - 1)
43
+
44
+ found = len(lines_to_remove) > 0
45
+ for line_idx in sorted(lines_to_remove, reverse=True):
46
+ del lines[line_idx]
47
+
48
+ modified_code = '\n'.join(lines)
49
+ return modified_code, found
50
+
51
+
52
+ def remove_decorators_from_folder(folder_path: str) -> List[str]:
53
+ """
54
+ Remove pysealer decorators from all Python files in a folder (recursively).
55
+
56
+ Args:
57
+ folder_path: Path to the folder to process
58
+ Returns:
59
+ List of file paths where decorators were removed
60
+ """
61
+ folder = Path(folder_path)
62
+
63
+ if not folder.is_dir():
64
+ raise NotADirectoryError(f"'{folder_path}' is not a directory")
65
+
66
+ # Find all Python files recursively
67
+ python_files = list(folder.rglob("*.py"))
68
+
69
+ if not python_files:
70
+ raise FileNotFoundError(f"No Python files found in '{folder_path}'")
71
+
72
+ files_modified = []
73
+
74
+ for py_file in python_files:
75
+ try:
76
+ file_path = str(py_file.resolve())
77
+ modified_code, found = remove_decorators(file_path)
78
+
79
+ if found:
80
+ # Write the modified code back to the file
81
+ with open(file_path, 'w') as f:
82
+ f.write(modified_code)
83
+ files_modified.append(file_path)
84
+ except Exception as e:
85
+ # Skip files that can't be processed
86
+ continue
87
+
88
+ return files_modified
pysealer/setup.py ADDED
@@ -0,0 +1,137 @@
1
+ """Setup the storage of the pysealer keypair in a .env file."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Optional
6
+ from dotenv import load_dotenv, set_key
7
+ from pysealer import generate_keypair
8
+
9
+
10
+ def _find_env_file() -> Path:
11
+ """
12
+ Search for .env file starting from current directory and walking up to parent directories.
13
+ Also checks PYSEALER_ENV_PATH environment variable.
14
+
15
+ Returns:
16
+ Path: Path to the .env file
17
+
18
+ Raises:
19
+ FileNotFoundError: If no .env file is found
20
+ """
21
+ # First check if PYSEALER_ENV_PATH environment variable is set
22
+ env_path_var = os.getenv("PYSEALER_ENV_PATH")
23
+ if env_path_var:
24
+ env_path = Path(env_path_var)
25
+ if env_path.exists():
26
+ return env_path
27
+
28
+ # Start from current working directory and search upward
29
+ current = Path.cwd()
30
+
31
+ # Check current directory and all parent directories up to root
32
+ for parent in [current] + list(current.parents):
33
+ env_file = parent / '.env'
34
+ if env_file.exists():
35
+ return env_file
36
+
37
+ # If not found, return the default location (current directory)
38
+ # This will be used in error messages
39
+ return Path.cwd() / '.env'
40
+
41
+ def setup_keypair(env_path: Optional[str | Path] = None):
42
+ """
43
+ Generate and store keypair securely.
44
+
45
+ Args:
46
+ env_path: Optional path to .env file. If None, creates in current directory.
47
+ """
48
+ # Determine .env location
49
+ if env_path is None:
50
+ env_path = Path.cwd() / '.env'
51
+ else:
52
+ env_path = Path(env_path)
53
+
54
+ # Check if keys already exist
55
+ if env_path.exists():
56
+ load_dotenv(env_path)
57
+ existing_private = os.getenv("PYSEALER_PRIVATE_KEY")
58
+ existing_public = os.getenv("PYSEALER_PUBLIC_KEY")
59
+
60
+ if existing_private or existing_public:
61
+ raise ValueError(f"Keys already exist in {env_path} Cannot overwrite existing keys.")
62
+
63
+ # Create .env if it doesn't exist
64
+ env_path.touch(exist_ok=True)
65
+
66
+ # Generate keypair using the Rust function
67
+ private_key_hex, public_key_hex = generate_keypair()
68
+
69
+ # Store keys in .env file
70
+ set_key(str(env_path), "PYSEALER_PRIVATE_KEY", private_key_hex)
71
+ set_key(str(env_path), "PYSEALER_PUBLIC_KEY", public_key_hex)
72
+
73
+ return private_key_hex, public_key_hex
74
+
75
+
76
+ def get_public_key(env_path: Optional[str | Path] = None) -> str:
77
+ """
78
+ Retrieve the public key from the .env file.
79
+
80
+ Args:
81
+ env_path: Optional path to .env file. If None, searches from current directory upward.
82
+
83
+ Returns:
84
+ str: The public key hex string, or None if not found.
85
+ """
86
+ # Determine .env location
87
+ if env_path is None:
88
+ env_path = _find_env_file()
89
+ else:
90
+ env_path = Path(env_path)
91
+
92
+ # Check if .env exists
93
+ if not env_path.exists():
94
+ raise FileNotFoundError(f"No .env file found at {env_path}. Run setup_keypair() first.")
95
+
96
+ # Load environment variables from .env
97
+ load_dotenv(env_path)
98
+
99
+ # Get public key
100
+ public_key = os.getenv("PYSEALER_PUBLIC_KEY")
101
+
102
+ if public_key is None:
103
+ raise ValueError(f"PYSEALER_PUBLIC_KEY not found in {env_path}. Run setup_keypair() first.")
104
+
105
+ return public_key
106
+
107
+
108
+ def get_private_key(env_path: Optional[str | Path] = None) -> str:
109
+ """
110
+ Retrieve the private key from the .env file.
111
+
112
+ Args:
113
+ env_path: Optional path to .env file. If None, searches from current directory upward.
114
+
115
+ Returns:
116
+ str: The private key hex string, or None if not found.
117
+ """
118
+ # Determine .env location
119
+ if env_path is None:
120
+ env_path = _find_env_file()
121
+ else:
122
+ env_path = Path(env_path)
123
+
124
+ # Check if .env exists
125
+ if not env_path.exists():
126
+ raise FileNotFoundError(f"No .env file found at {env_path}. Run setup_keypair() first.")
127
+
128
+ # Load environment variables from .env
129
+ load_dotenv(env_path)
130
+
131
+ # Get private key
132
+ private_key = os.getenv("PYSEALER_PRIVATE_KEY")
133
+
134
+ if private_key is None:
135
+ raise ValueError(f"PYSEALER_PRIVATE_KEY not found in {env_path}. Run setup_keypair() first.")
136
+
137
+ return private_key
@@ -0,0 +1,168 @@
1
+ Metadata-Version: 2.4
2
+ Name: pysealer
3
+ Version: 0.1.4
4
+ Classifier: Development Status :: 3 - Alpha
5
+ Classifier: Intended Audience :: Developers
6
+ Classifier: License :: OSI Approved :: MIT License
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.8
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Rust
14
+ Classifier: Programming Language :: Python :: Implementation :: CPython
15
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
16
+ Classifier: Topic :: Security
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Dist: python-dotenv>=1.0.0
19
+ Requires-Dist: typer>=0.9.0
20
+ Requires-Dist: pytest>=7.0.0 ; extra == 'test'
21
+ Requires-Dist: pytest-cov>=4.0.0 ; extra == 'test'
22
+ Requires-Dist: pytest-asyncio>=0.21.0 ; extra == 'test'
23
+ Provides-Extra: test
24
+ License-File: LICENSE
25
+ Summary: Cryptographically sign Python functions and classes for defense-in-depth security
26
+ Keywords: rust,python,decorator,cryptography
27
+ Author: Aidan Dyga
28
+ License: MIT
29
+ Requires-Python: >=3.8
30
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
31
+ Project-URL: Issues, https://github.com/MCP-Security-Research/pysealer/issues
32
+ Project-URL: Repository, https://github.com/MCP-Security-Research/pysealer
33
+
34
+ # pysealer
35
+
36
+ Cryptographically sign Python functions and classes for defense-in-depth security
37
+
38
+ > 💡 **code version controls code**
39
+
40
+ - 🦀 Built with the [maturin build system](https://www.maturin.rs/) for easy Rust-Python packaging
41
+ - 🔗 [PyO3](https://pyo3.rs/v0.27.1/index.html) bindings for seamless Python-Rust integration
42
+ - 🔏 [Ed25519](https://docs.rs/ed25519-dalek/latest/ed25519_dalek/) signatures to ensure code integrity and authorship
43
+ - 🖥️ [Typer](https://typer.tiangolo.com/) for a clean and user-friendly command line interface
44
+
45
+ Pysealer helps you maintain code integrity by automatically adding cryptographic signatures to your Python functions and classes. Each function or class receives a unique decorator containing a cryptographic signature that verifies both authorship and integrity, making it easy to detect unauthorized code modifications.
46
+
47
+ ## Table of Contents
48
+
49
+ 1. [Getting Started](#getting-started)
50
+ 2. [Usage](#usage)
51
+ 3. [How It Works](#how-it-works)
52
+ 4. [Model Context Protocol (MCP) Security Use Cases](#model-context-protocol-mcp-security-use-cases)
53
+ 5. [Contributing](#contributing)
54
+ 6. [License](#license)
55
+
56
+ ## Getting Started
57
+
58
+ ```shell
59
+ pip install pysealer
60
+ # or
61
+ uv pip install pysealer
62
+ ```
63
+
64
+ ## Usage
65
+
66
+ ```shell
67
+ pysealer init [ENV_FILE] # Initialize the pysealer tool by generating and saving keys to an ENV_FILE (default: .env)
68
+ pysealer decorate <file.py>... # Add cryptographic decorators to all functions/classes in one or more .py files
69
+ pysealer check <file.py>... # Verify the integrity and validity of pysealer decorators in one or more .py files
70
+ pysealer remove <file.py>... # Remove all pysealer decorators from one or more .py files
71
+ pysealer --help # Show all available commands and options
72
+ ```
73
+
74
+ ## How It Works
75
+
76
+ Pysealer works by automatically injecting cryptographic decorators into your Python functions and classes. Here's how the process works:
77
+
78
+ ### Step-by-Step Example
79
+
80
+ Suppose you have a file `fibonacci.py`:
81
+
82
+ ```python
83
+ def fibonacci(n):
84
+ if n <= 0:
85
+ return 0
86
+ elif n == 1:
87
+ return 1
88
+ else:
89
+ return fibonacci(n-1) + fibonacci(n-2)
90
+ ```
91
+
92
+ #### 1. Decorate the file
93
+
94
+ ```shell
95
+ pysealer decorate examples/fibonacci.py
96
+
97
+ Successfully added decorators to 1 file:
98
+ ✓ /path/to/examples/fibonacci.py
99
+ ```
100
+
101
+ ```python
102
+ @pysealer._GnCLaWr9B6TD524JZ3v1CENXmo5Drwfgvc9arVagbghQ6hMH4Aqc8whs3Tf57pkTjsAVNDybviW9XG5Eu3JSP6T()
103
+ def fibonacci(n):
104
+ if n <= 0:
105
+ return 0
106
+ elif n == 1:
107
+ return 1
108
+ else:
109
+ return fibonacci(n-1) + fibonacci(n-2)
110
+ ```
111
+
112
+ #### 2. Check integrity
113
+
114
+ ```shell
115
+ pysealer check examples/fibonacci.py
116
+
117
+ All decorators are valid in 1 file:
118
+ ✓ /path/to/examples/fibonacci.py: 1 decorators valid
119
+ ```
120
+
121
+ #### 3. Tamper with the code (change return 0 to return 42)
122
+
123
+ ```python
124
+ @pysealer._GnCLaWr9B6TD524JZ3v1CENXmo5Drwfgvc9arVagbghQ6hMH4Aqc8whs3Tf57pkTjsAVNDybviW9XG5Eu3JSP6T()
125
+ def fibonacci(n):
126
+ if n <= 0:
127
+ return 42
128
+ elif n == 1:
129
+ return 1
130
+ else:
131
+ return fibonacci(n-1) + fibonacci(n-2)
132
+ ```
133
+
134
+ #### 4. Check again
135
+
136
+ ```shell
137
+ pysealer check examples/fibonacci.py
138
+
139
+ 1/1 decorators failed verification across 1 file:
140
+ ✗ /path/to/examples/fibonacci.py: 1/1 decorators failed
141
+ ```
142
+
143
+ ## Model Context Protocol (MCP) Security Use Cases
144
+
145
+ One use case of Pysealer is to protect MCP servers from upstream attacks by cryptographically signing tool functions and their docstrings. Since LLMs rely on docstrings to understand tool behavior, attackers can inject malicious instructions or create fake tools that mimic legitimate ones. Pysealer's signatures ensure tool authenticity and detect tampering because any modification to code or docstrings breaks the signature and flags compromised tools.
146
+
147
+ - **Detect Version Control Changes**
148
+ - Automatically detect unauthorized code modifications through cryptographic signatures
149
+ - Each function's decorator contains a signature based on its code and docstring
150
+ - Any mismatch between code and signature is immediately flagged
151
+
152
+ - **Defense-in-Depth for Source Control**
153
+ - Add an additional security layer to version control systems
154
+ - Complement existing security measures with cryptographic verification
155
+ - Reduce risk through multiple layers of protection
156
+
157
+ ## Contributing
158
+
159
+ **🙌 Contributions are welcome!**
160
+
161
+ If you have suggestions, bug reports, or want to help improve Pysealer, feel free to open an [issue](https://github.com/MCP-Security-Research/pysealer/issues) or submit a pull request.
162
+
163
+ All ideas and contributions are appreciated—thanks for helping make pysealer better!
164
+
165
+ ## License
166
+
167
+ Pysealer is licensed under the MIT License. See [LICENSE](LICENSE) for details.
168
+
@@ -0,0 +1,13 @@
1
+ pysealer/__init__.py,sha256=_qr-hT8yCojoPgSYfjZkvp93h6tIL8PD5T0BzeXFay4,944
2
+ pysealer/_pysealer.pypy39-pp73-aarch64-linux-gnu.so,sha256=X_tgEY8Zi6wXqU__bwpK9OTUfio4mv0ey-7HxbghoXc,673712
3
+ pysealer/add_decorators.py,sha256=-Jq1no002PfFnYeYVQsIGptzZpqVpoopox7RVV5QbtU,8750
4
+ pysealer/check_decorators.py,sha256=LdcVBWS_PbuqJvXNBkOYSaPRiDOMvAlA5SZhjkxGGk8,7203
5
+ pysealer/cli.py,sha256=X03gHpITt6mS5POFUNsHVt8mMZEaGcN5YkDsHaNz_9g,12040
6
+ pysealer/dummy_decorators.py,sha256=9ORNnMZ6lT5e1gU4Dt9lifAEDfjJ1wCXGiOUfMiqP-w,2807
7
+ pysealer/remove_decorators.py,sha256=v1VpxFid8kqqoj-NuWSWWzCoYUwl1XVYyGdTE8BLbeI,3276
8
+ pysealer/setup.py,sha256=M8wgv4GeVbbC_t0a8PBD5Hn6piqGTFIUTlo1ds6JtAc,4206
9
+ pysealer-0.1.4.dist-info/METADATA,sha256=VZGBUzePSeKdoH29A65AOlGSnRqD_pL89EfGySUjGIE,6137
10
+ pysealer-0.1.4.dist-info/WHEEL,sha256=qzwzn5_kd3G5istIUlu4zh1R72c65tXbcz-afAAujws,159
11
+ pysealer-0.1.4.dist-info/entry_points.txt,sha256=DUDRyFGp10a8FMaLUFE6X94kCD2dciDY66ISXuUySmA,45
12
+ pysealer-0.1.4.dist-info/licenses/LICENSE,sha256=FtcYsMyXNwAqNhOMxR3TwptCUnwb5coRK_UQyWGLJnc,1067
13
+ pysealer-0.1.4.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.11.5)
3
+ Root-Is-Purelib: false
4
+ Tag: pp39-pypy39_pp73-manylinux_2_17_aarch64
5
+ Tag: pp39-pypy39_pp73-manylinux2014_aarch64
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pysealer=pysealer.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Aidan Dyga
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.