pysealer 0.6.0__cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.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.
@@ -0,0 +1,175 @@
1
+ """
2
+ GitHub secrets integration for pysealer.
3
+
4
+ This module provides functionality to automatically upload the pysealer public key
5
+ to GitHub repository secrets when `pysealer init` is run.
6
+ """
7
+
8
+ import logging
9
+ import os
10
+ import re
11
+ from pathlib import Path
12
+ from typing import Optional, Tuple
13
+
14
+ import git
15
+ from github import Github, GithubException
16
+ from nacl import encoding, public
17
+
18
+ # Suppress verbose GitHub API logging
19
+ logging.getLogger("github").setLevel(logging.CRITICAL)
20
+ logging.getLogger("urllib3").setLevel(logging.CRITICAL)
21
+
22
+
23
+ def get_repo_info() -> Tuple[str, str]:
24
+ """
25
+ Extract GitHub owner and repository name from git remote URL.
26
+
27
+ Supports both SSH and HTTPS formats:
28
+ - SSH: git@github.com:owner/repo.git
29
+ - HTTPS: https://github.com/owner/repo.git
30
+
31
+ Returns:
32
+ Tuple[str, str]: (owner, repo_name)
33
+
34
+ Raises:
35
+ ValueError: If not in a git repository or remote URL is not from GitHub
36
+ RuntimeError: If unable to parse the remote URL
37
+ """
38
+ try:
39
+ # Get the git repository from current directory
40
+ repo = git.Repo(search_parent_directories=True)
41
+
42
+ # Try to get the origin remote
43
+ if 'origin' not in repo.remotes:
44
+ raise ValueError("No 'origin' remote found in git repository")
45
+
46
+ remote_url = repo.remotes.origin.url
47
+
48
+ # Parse SSH format: git@github.com:owner/repo.git
49
+ ssh_pattern = r'git@github\.com:([^/]+)/(.+?)(?:\.git)?$'
50
+ ssh_match = re.match(ssh_pattern, remote_url)
51
+ if ssh_match:
52
+ owner, repo_name = ssh_match.groups()
53
+ return owner, repo_name
54
+
55
+ # Parse HTTPS format: https://github.com/owner/repo.git
56
+ https_pattern = r'https://github\.com/([^/]+)/(.+?)(?:\.git)?$'
57
+ https_match = re.match(https_pattern, remote_url)
58
+ if https_match:
59
+ owner, repo_name = https_match.groups()
60
+ return owner, repo_name
61
+
62
+ raise RuntimeError(f"Could not parse GitHub repository from remote URL: {remote_url}")
63
+
64
+ except git.InvalidGitRepositoryError:
65
+ raise ValueError("Not in a git repository. Please run this command from within a git repository.")
66
+ except git.GitCommandError as e:
67
+ raise RuntimeError(f"Git error: {e}")
68
+
69
+
70
+ def encrypt_secret(public_key: str, secret_value: str) -> str:
71
+ """
72
+ Encrypt a secret using GitHub's public key.
73
+
74
+ GitHub requires secrets to be encrypted using the repository's public key
75
+ before they can be uploaded via the API.
76
+
77
+ Args:
78
+ public_key: The repository's public key (base64 encoded)
79
+ secret_value: The secret value to encrypt
80
+
81
+ Returns:
82
+ str: Base64 encoded encrypted secret
83
+ """
84
+ # Convert the public key from base64
85
+ public_key_bytes = public_key.encode("utf-8")
86
+ public_key_obj = public.PublicKey(public_key_bytes, encoding.Base64Encoder())
87
+
88
+ # Encrypt the secret using sealed box
89
+ sealed_box = public.SealedBox(public_key_obj)
90
+ encrypted = sealed_box.encrypt(secret_value.encode("utf-8"))
91
+
92
+ # Return base64 encoded encrypted secret
93
+ return encoding.Base64Encoder().encode(encrypted).decode("utf-8")
94
+
95
+
96
+ def add_secret_to_github(token: str, owner: str, repo_name: str, secret_name: str, secret_value: str) -> None:
97
+ """
98
+ Add or update a secret in GitHub repository.
99
+
100
+ Args:
101
+ token: GitHub personal access token (needs 'repo' scope)
102
+ owner: GitHub repository owner (user or organization)
103
+ repo_name: Repository name
104
+ secret_name: Name of the secret to create/update
105
+ secret_value: Value of the secret (will be encrypted before upload)
106
+
107
+ Raises:
108
+ GithubException: If GitHub API request fails
109
+ Exception: For other errors during the process
110
+ """
111
+ try:
112
+ # Initialize GitHub client
113
+ g = Github(token)
114
+
115
+ # Get the repository
116
+ repo = g.get_repo(f"{owner}/{repo_name}")
117
+
118
+ # Get the repository's public key for encrypting secrets
119
+ public_key = repo.get_public_key()
120
+
121
+ # Encrypt the secret
122
+ encrypted_value = encrypt_secret(public_key.key, secret_value)
123
+
124
+ # Create or update the secret (actions secrets are at repository level)
125
+ repo.create_secret(secret_name, encrypted_value, secret_type="actions")
126
+
127
+ except GithubException as e:
128
+ if e.status == 401:
129
+ raise Exception("Authentication failed. Please check your GitHub token.")
130
+ elif e.status == 403:
131
+ raise Exception("Permission denied. Your token needs 'repo' scope to manage secrets.")
132
+ elif e.status == 404:
133
+ raise Exception(f"Repository '{owner}/{repo_name}' not found or you don't have access.")
134
+ else:
135
+ raise Exception(f"GitHub API error: {e.data.get('message', str(e))}")
136
+
137
+
138
+ def setup_github_secrets(public_key: str, github_token: Optional[str] = None) -> Tuple[bool, str]:
139
+ """
140
+ Main function to orchestrate GitHub secrets setup.
141
+
142
+ This function:
143
+ 1. Gets GitHub token from parameter or environment variable
144
+ 2. Extracts repository info from git remote
145
+ 3. Uploads the public key to GitHub secrets
146
+
147
+ Args:
148
+ public_key: The pysealer public key to upload
149
+ github_token: Optional GitHub token. If None, uses GITHUB_TOKEN env var
150
+
151
+ Returns:
152
+ Tuple[bool, str]: (success, message)
153
+ - success: True if secret was uploaded successfully
154
+ - message: Success or error message
155
+ """
156
+ # Get GitHub token
157
+ token = github_token or os.getenv("GITHUB_TOKEN")
158
+ if not token:
159
+ return False, "No GitHub token provided. Use --github-token or set GITHUB_TOKEN environment variable."
160
+
161
+ try:
162
+ # Get repository information
163
+ owner, repo_name = get_repo_info()
164
+
165
+ # Upload the secret
166
+ add_secret_to_github(token, owner, repo_name, "PYSEALER_PUBLIC_KEY", public_key)
167
+
168
+ return True, f"Successfully added PYSEALER_PUBLIC_KEY to {owner}/{repo_name}"
169
+
170
+ except ValueError as e:
171
+ return False, f"Repository detection failed: {e}"
172
+ except RuntimeError as e:
173
+ return False, f"Git error: {e}"
174
+ except Exception as e:
175
+ return False, f"Failed to upload secret: {e}"
@@ -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,171 @@
1
+ Metadata-Version: 2.4
2
+ Name: pysealer
3
+ Version: 0.6.0
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: pygithub>=2.1.1
21
+ Requires-Dist: pynacl>=1.5.0
22
+ Requires-Dist: gitpython>=3.1.0
23
+ Requires-Dist: pytest>=7.0.0 ; extra == 'test'
24
+ Requires-Dist: pytest-cov>=4.0.0 ; extra == 'test'
25
+ Requires-Dist: pytest-asyncio>=0.21.0 ; extra == 'test'
26
+ Provides-Extra: test
27
+ License-File: LICENSE
28
+ Summary: Cryptographically sign Python functions and classes for defense-in-depth security
29
+ Keywords: rust,python,decorator,cryptography
30
+ Author: Aidan Dyga
31
+ License: MIT
32
+ Requires-Python: >=3.8
33
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
34
+ Project-URL: Issues, https://github.com/MCP-Security-Research/pysealer/issues
35
+ Project-URL: Repository, https://github.com/MCP-Security-Research/pysealer
36
+
37
+ # pysealer
38
+
39
+ Cryptographically sign Python functions and classes for defense-in-depth security
40
+
41
+ > 💡 **code version controls code**
42
+
43
+ - 🦀 Built with the [maturin build system](https://www.maturin.rs/) for easy Rust-Python packaging
44
+ - 🔗 [PyO3](https://pyo3.rs/v0.27.1/index.html) bindings for seamless Python-Rust integration
45
+ - 🔏 [Ed25519](https://docs.rs/ed25519-dalek/latest/ed25519_dalek/) signatures to ensure code integrity and authorship
46
+ - 🖥️ [Typer](https://typer.tiangolo.com/) for a clean and user-friendly command line interface
47
+
48
+ 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.
49
+
50
+ ## Table of Contents
51
+
52
+ 1. [Getting Started](#getting-started)
53
+ 2. [Usage](#usage)
54
+ 3. [How It Works](#how-it-works)
55
+ 4. [Model Context Protocol (MCP) Security Use Cases](#model-context-protocol-mcp-security-use-cases)
56
+ 5. [Contributing](#contributing)
57
+ 6. [License](#license)
58
+
59
+ ## Getting Started
60
+
61
+ ```shell
62
+ pip install pysealer
63
+ # or
64
+ uv pip install pysealer
65
+ ```
66
+
67
+ ## Usage
68
+
69
+ ```shell
70
+ pysealer init [ENV_FILE] # Initialize the pysealer tool by generating and saving keys to an ENV_FILE (default: .env)
71
+ pysealer decorate <file.py>... # Add cryptographic decorators to all functions/classes in one or more .py files
72
+ pysealer check <file.py>... # Verify the integrity and validity of pysealer decorators in one or more .py files
73
+ pysealer remove <file.py>... # Remove all pysealer decorators from one or more .py files
74
+ pysealer --help # Show all available commands and options
75
+ ```
76
+
77
+ ## How It Works
78
+
79
+ Pysealer works by automatically injecting cryptographic decorators into your Python functions and classes. Here's how the process works:
80
+
81
+ ### Step-by-Step Example
82
+
83
+ Suppose you have a file `fibonacci.py`:
84
+
85
+ ```python
86
+ def fibonacci(n):
87
+ if n <= 0:
88
+ return 0
89
+ elif n == 1:
90
+ return 1
91
+ else:
92
+ return fibonacci(n-1) + fibonacci(n-2)
93
+ ```
94
+
95
+ #### 1. Decorate the file
96
+
97
+ ```shell
98
+ pysealer decorate examples/fibonacci.py
99
+
100
+ Successfully added decorators to 1 file:
101
+ ✓ /path/to/examples/fibonacci.py
102
+ ```
103
+
104
+ ```python
105
+ @pysealer._GnCLaWr9B6TD524JZ3v1CENXmo5Drwfgvc9arVagbghQ6hMH4Aqc8whs3Tf57pkTjsAVNDybviW9XG5Eu3JSP6T()
106
+ def fibonacci(n):
107
+ if n <= 0:
108
+ return 0
109
+ elif n == 1:
110
+ return 1
111
+ else:
112
+ return fibonacci(n-1) + fibonacci(n-2)
113
+ ```
114
+
115
+ #### 2. Check integrity
116
+
117
+ ```shell
118
+ pysealer check examples/fibonacci.py
119
+
120
+ All decorators are valid in 1 file:
121
+ ✓ /path/to/examples/fibonacci.py: 1 decorators valid
122
+ ```
123
+
124
+ #### 3. Tamper with the code (change return 0 to return 42)
125
+
126
+ ```python
127
+ @pysealer._GnCLaWr9B6TD524JZ3v1CENXmo5Drwfgvc9arVagbghQ6hMH4Aqc8whs3Tf57pkTjsAVNDybviW9XG5Eu3JSP6T()
128
+ def fibonacci(n):
129
+ if n <= 0:
130
+ return 42
131
+ elif n == 1:
132
+ return 1
133
+ else:
134
+ return fibonacci(n-1) + fibonacci(n-2)
135
+ ```
136
+
137
+ #### 4. Check again
138
+
139
+ ```shell
140
+ pysealer check examples/fibonacci.py
141
+
142
+ 1/1 decorators failed verification across 1 file:
143
+ ✗ /path/to/examples/fibonacci.py: 1/1 decorators failed
144
+ ```
145
+
146
+ ## Model Context Protocol (MCP) Security Use Cases
147
+
148
+ 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.
149
+
150
+ - **Detect Version Control Changes**
151
+ - Automatically detect unauthorized code modifications through cryptographic signatures
152
+ - Each function's decorator contains a signature based on its code and docstring
153
+ - Any mismatch between code and signature is immediately flagged
154
+
155
+ - **Defense-in-Depth for Source Control**
156
+ - Add an additional security layer to version control systems
157
+ - Complement existing security measures with cryptographic verification
158
+ - Reduce risk through multiple layers of protection
159
+
160
+ ## Contributing
161
+
162
+ **🙌 Contributions are welcome!**
163
+
164
+ 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.
165
+
166
+ All ideas and contributions are appreciated—thanks for helping make pysealer better!
167
+
168
+ ## License
169
+
170
+ Pysealer is licensed under the MIT License. See [LICENSE](LICENSE) for details.
171
+
@@ -0,0 +1,15 @@
1
+ pysealer/__init__.py,sha256=qxCa8932UYgSwwcWv1TP6uRWxhSn3QPeot3ItEkFQCw,944
2
+ pysealer/_pysealer.cpython-314t-arm-linux-gnueabihf.so,sha256=gkDFYnx5EkWhvUGiLCCpQX2vBVX02IuZg__MQoSGLlI,683780
3
+ pysealer/add_decorators.py,sha256=QkjZw7ckLd06RIg77nhscgzXk8SyLdJ2kNyQ0BTjS4g,9049
4
+ pysealer/check_decorators.py,sha256=_fEsd94qN_ypMOePF75QcL--KrIXFQunyXXAZIeexTM,7401
5
+ pysealer/cli.py,sha256=Ysk3b8q17YboAq5mxi3uwB1t4WPF2KcRJNeyQkJ05fs,16490
6
+ pysealer/dummy_decorators.py,sha256=9ORNnMZ6lT5e1gU4Dt9lifAEDfjJ1wCXGiOUfMiqP-w,2807
7
+ pysealer/git_diff.py,sha256=hgFhjoE5Yom-LF31BzwtO5jmohlW4FfWvfp8BDZgeNY,6912
8
+ pysealer/github_secrets.py,sha256=0Hms2VEewZl2Dv1G1eVUBPLouiy8mcZFrxlLftUOpFg,6331
9
+ pysealer/remove_decorators.py,sha256=v1VpxFid8kqqoj-NuWSWWzCoYUwl1XVYyGdTE8BLbeI,3276
10
+ pysealer/setup.py,sha256=M8wgv4GeVbbC_t0a8PBD5Hn6piqGTFIUTlo1ds6JtAc,4206
11
+ pysealer-0.6.0.dist-info/METADATA,sha256=Re0ucu5klXyfnolk1K0evoZ1zGAUd0uQ2Jz334XHmqg,6229
12
+ pysealer-0.6.0.dist-info/WHEEL,sha256=QOjFC7sblOj_sFAUv-rKZxS-TdD55Wj4pH_OsgCJnhg,149
13
+ pysealer-0.6.0.dist-info/entry_points.txt,sha256=DUDRyFGp10a8FMaLUFE6X94kCD2dciDY66ISXuUySmA,45
14
+ pysealer-0.6.0.dist-info/licenses/LICENSE,sha256=FtcYsMyXNwAqNhOMxR3TwptCUnwb5coRK_UQyWGLJnc,1067
15
+ pysealer-0.6.0.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: cp314-cp314t-manylinux_2_17_armv7l
5
+ Tag: cp314-cp314t-manylinux2014_armv7l
@@ -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.