pymarklet 0.1.0__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.
- pymarklet/__init__.py +6 -0
- pymarklet/cli.py +35 -0
- pymarklet/generator.py +180 -0
- pymarklet-0.1.0.dist-info/METADATA +147 -0
- pymarklet-0.1.0.dist-info/RECORD +8 -0
- pymarklet-0.1.0.dist-info/WHEEL +4 -0
- pymarklet-0.1.0.dist-info/entry_points.txt +3 -0
- pymarklet-0.1.0.dist-info/licenses/LICENSE +21 -0
pymarklet/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Create browser bookmarklets that run Python with Pyodide."""
|
|
2
|
+
|
|
3
|
+
from .generator import build_bookmarklet, declared_pypi_packages, imported_package_modules
|
|
4
|
+
|
|
5
|
+
__all__ = ["build_bookmarklet", "declared_pypi_packages", "imported_package_modules"]
|
|
6
|
+
__version__ = "0.1.0"
|
pymarklet/cli.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Command-line interface for PyMarklet."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .generator import generate_bookmarklet
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
13
|
+
"""Build the command-line argument parser."""
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
description="Generate a Pyodide-powered browser bookmarklet from a Python script.",
|
|
16
|
+
)
|
|
17
|
+
parser.add_argument("python_file", type=Path, help="UTF-8 Python source file to embed")
|
|
18
|
+
parser.add_argument(
|
|
19
|
+
"-o",
|
|
20
|
+
"--output",
|
|
21
|
+
type=Path,
|
|
22
|
+
default=Path("bookmarklet.js"),
|
|
23
|
+
help="destination for the bookmarklet URL (default: bookmarklet.js)",
|
|
24
|
+
)
|
|
25
|
+
return parser
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
29
|
+
"""Run the PyMarklet CLI and return a process exit status."""
|
|
30
|
+
args = build_parser().parse_args(argv)
|
|
31
|
+
return 0 if generate_bookmarklet(args.python_file, args.output) else 1
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
raise SystemExit(main())
|
pymarklet/generator.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from urllib.parse import quote
|
|
8
|
+
|
|
9
|
+
PYODIDE_VERSION = "0.27.5"
|
|
10
|
+
PYODIDE_BASE_URL = f"https://cdn.jsdelivr.net/pyodide/v{PYODIDE_VERSION}/full/"
|
|
11
|
+
PYODIDE_SCRIPT_URL = f"{PYODIDE_BASE_URL}pyodide.js"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Final optimized, pre-minified JavaScript layout shell
|
|
15
|
+
JAVASCRIPT_TEMPLATE = r"""(() => {
|
|
16
|
+
const cId = "py-bookmarklet-container";
|
|
17
|
+
if (document.getElementById(cId)) return;
|
|
18
|
+
const c = document.createElement("section");
|
|
19
|
+
c.id = cId;
|
|
20
|
+
c.style.cssText = "position:fixed;top:10px;right:10px;width:400px;height:500px;z-index:2147483647;display:flex;flex-direction:column;overflow:hidden;box-sizing:border-box;border:1px solid #333;border-radius:8px;background:#1e1e1e;color:#f4f4f4;font-family:monospace;box-shadow:0 4px 15px rgba(0,0,0,.3)";
|
|
21
|
+
const h = document.createElement("header");
|
|
22
|
+
h.style.cssText = "display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:#2d2d2d;border-bottom:1px solid #333;cursor:grab;user-select:none;touch-action:none";
|
|
23
|
+
const s = document.createElement("span");
|
|
24
|
+
s.textContent = "Loading Pyodide...";
|
|
25
|
+
s.style.cssText = "overflow:hidden;color:#6f6;font-size:12px;font-weight:bold;text-overflow:ellipsis;white-space:nowrap";
|
|
26
|
+
const cb = document.createElement("button");
|
|
27
|
+
cb.type = "button"; cb.textContent = "×"; cb.setAttribute("aria-label", "Close");
|
|
28
|
+
cb.style.cssText = "margin-left:12px;border:0;background:transparent;color:#f66;cursor:pointer;font-size:20px;font-weight:bold;line-height:1";
|
|
29
|
+
const ct = document.createElement("div");
|
|
30
|
+
ct.style.cssText = "flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden";
|
|
31
|
+
const cv = document.createElement("canvas");
|
|
32
|
+
cv.id = "canvas"; cv.tabIndex = 0; cv.style.cssText = "display:block;max-width:100%;max-height:70%;margin:0 auto;background:#000;image-rendering:pixelated;outline:none";
|
|
33
|
+
cv.addEventListener("pointerdown", () => cv.focus());
|
|
34
|
+
const o = document.createElement("pre");
|
|
35
|
+
o.style.cssText = "flex:1;min-height:0;overflow:auto;margin:0;padding:15px;box-sizing:border-box;white-space:pre-wrap;overflow-wrap:anywhere";
|
|
36
|
+
h.append(s, cb); ct.append(cv, o); c.append(h, ct); document.body.appendChild(c);
|
|
37
|
+
const setStatus = (msg, isErr = false) => { s.textContent = msg; s.style.color = isErr ? "#f66" : "#6f6"; };
|
|
38
|
+
const log = (val) => { o.textContent += `${String(val)}\n`; o.scrollTop = o.scrollHeight; };
|
|
39
|
+
cb.addEventListener("click", () => c.remove());
|
|
40
|
+
let dx = 0, dy = 0;
|
|
41
|
+
h.addEventListener("pointerdown", (e) => {
|
|
42
|
+
if (e.target === cb) return;
|
|
43
|
+
const b = c.getBoundingClientRect();
|
|
44
|
+
dx = e.clientX - b.left; dy = e.clientY - b.top;
|
|
45
|
+
h.setPointerCapture(e.pointerId); h.style.cursor = "grabbing";
|
|
46
|
+
});
|
|
47
|
+
h.addEventListener("pointermove", (e) => {
|
|
48
|
+
if (!h.hasPointerCapture(e.pointerId)) return;
|
|
49
|
+
const ml = Math.max(0, window.innerWidth - c.offsetWidth);
|
|
50
|
+
const mt = Math.max(0, window.innerHeight - c.offsetHeight);
|
|
51
|
+
const left = Math.min(ml, Math.max(0, e.clientX - dx));
|
|
52
|
+
const top = Math.min(mt, Math.max(0, e.clientY - dy));
|
|
53
|
+
c.style.left = `${left}px`; c.style.top = `${top}px`; c.style.right = "auto";
|
|
54
|
+
});
|
|
55
|
+
const stop = (e) => { if (h.hasPointerCapture(e.pointerId)) h.releasePointerCapture(e.pointerId); h.style.cursor = "grab"; };
|
|
56
|
+
h.addEventListener("pointerup", stop); h.addEventListener("pointercancel", stop);
|
|
57
|
+
|
|
58
|
+
__DATA_SPLICING_PLACEHOLDER__
|
|
59
|
+
|
|
60
|
+
async function runPython() {
|
|
61
|
+
try {
|
|
62
|
+
setStatus("Starting Python...");
|
|
63
|
+
const pyodide = await globalThis.loadPyodide({ indexURL: "__PYODIDE_BASE_URL__", stdout: log, stderr: log });
|
|
64
|
+
pyodide._api._skip_unwind_fatal_error = true;
|
|
65
|
+
pyodide.canvas.setCanvas2D(cv);
|
|
66
|
+
|
|
67
|
+
setStatus("Resolving Pyodide imports...");
|
|
68
|
+
await pyodide.loadPackagesFromImports(pyCode);
|
|
69
|
+
|
|
70
|
+
const installPyPiPackages = async (packages) => {
|
|
71
|
+
if (!packages.length) return;
|
|
72
|
+
setStatus(`Installing PyPI packages: ${packages.join(", ")}...`);
|
|
73
|
+
await pyodide.loadPackage("micropip");
|
|
74
|
+
const micropip = pyodide.pyimport("micropip");
|
|
75
|
+
try {
|
|
76
|
+
await micropip.install(packages);
|
|
77
|
+
} finally {
|
|
78
|
+
micropip.destroy();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// The optional first-line # PKGS: manifest specifies PyPI distribution
|
|
83
|
+
// names, covering cases such as `scikit-learn` -> `sklearn`.
|
|
84
|
+
await installPyPiPackages(declaredPackages);
|
|
85
|
+
|
|
86
|
+
// Check after both resolvers so conventional import names still install
|
|
87
|
+
// automatically when a script does not declare a distribution.
|
|
88
|
+
const pypiPackages = packageModules.filter((moduleName) => !pyodide.runPython(
|
|
89
|
+
`import importlib.util\nimportlib.util.find_spec(${JSON.stringify(moduleName)}) is not None`,
|
|
90
|
+
));
|
|
91
|
+
await installPyPiPackages(pypiPackages);
|
|
92
|
+
|
|
93
|
+
setStatus("Starting Python...");
|
|
94
|
+
await pyodide.runPythonAsync(pyCode);
|
|
95
|
+
setStatus("Execution finished.");
|
|
96
|
+
} catch (err) {
|
|
97
|
+
setStatus("Python execution failed.", true);
|
|
98
|
+
log(err instanceof Error ? err.stack || err.message : err);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function loadPyodideScript() {
|
|
102
|
+
if (typeof globalThis.loadPyodide === "function") { void runPython(); return; }
|
|
103
|
+
const scriptNode = document.createElement("script"); scriptNode.src = "__PYODIDE_SCRIPT_URL__"; scriptNode.async = true;
|
|
104
|
+
scriptNode.onload = () => void runPython();
|
|
105
|
+
scriptNode.onerror = () => setStatus("Could not load Pyodide (possibly blocked by CSP).", true);
|
|
106
|
+
document.body.appendChild(scriptNode);
|
|
107
|
+
}
|
|
108
|
+
loadPyodideScript();
|
|
109
|
+
})();"""
|
|
110
|
+
|
|
111
|
+
def clean_python_code(code: str) -> str:
|
|
112
|
+
"""Preserve the user's program exactly; JSON encoding handles URL embedding."""
|
|
113
|
+
return code
|
|
114
|
+
|
|
115
|
+
def declared_pypi_packages(python_code: str) -> list[str]:
|
|
116
|
+
"""Read comma-separated PyPI requirements from a first-line # PKGS: header."""
|
|
117
|
+
first_line = python_code.split("\n", 1)[0].lstrip("\ufeff")
|
|
118
|
+
match = re.fullmatch(r"\s*#\s*PKGS\s*:\s*(.*)\s*", first_line, flags=re.IGNORECASE)
|
|
119
|
+
if not match or not match.group(1):
|
|
120
|
+
return []
|
|
121
|
+
|
|
122
|
+
packages = [package.strip() for package in match.group(1).split(",")]
|
|
123
|
+
return list(dict.fromkeys(package for package in packages if package))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def imported_package_modules(python_code: str) -> list[str]:
|
|
127
|
+
"""Return third-party top-level modules imported by *python_code*."""
|
|
128
|
+
try:
|
|
129
|
+
tree = ast.parse(python_code)
|
|
130
|
+
except SyntaxError:
|
|
131
|
+
# Let Pyodide report the original syntax error when the bookmarklet runs.
|
|
132
|
+
return []
|
|
133
|
+
|
|
134
|
+
imported_modules: list[str] = []
|
|
135
|
+
for node in ast.walk(tree):
|
|
136
|
+
if isinstance(node, ast.Import):
|
|
137
|
+
imported_modules.extend(alias.name.split(".", 1)[0] for alias in node.names)
|
|
138
|
+
elif isinstance(node, ast.ImportFrom) and node.level == 0 and node.module:
|
|
139
|
+
imported_modules.append(node.module.split(".", 1)[0])
|
|
140
|
+
|
|
141
|
+
return list(dict.fromkeys(imported_modules))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def build_bookmarklet(python_code: str) -> str:
|
|
145
|
+
cleaned_py = clean_python_code(python_code)
|
|
146
|
+
package_modules = imported_package_modules(cleaned_py)
|
|
147
|
+
declared_packages = declared_pypi_packages(cleaned_py)
|
|
148
|
+
|
|
149
|
+
# Inject variables securely as valid JS.
|
|
150
|
+
js_variables = (
|
|
151
|
+
f"const pyCode = {json.dumps(cleaned_py)};\n"
|
|
152
|
+
f"const declaredPackages = {json.dumps(declared_packages)};\n"
|
|
153
|
+
f"const packageModules = {json.dumps(package_modules)};"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
payload = JAVASCRIPT_TEMPLATE.replace('__DATA_SPLICING_PLACEHOLDER__', js_variables)
|
|
157
|
+
payload = payload.replace("__PYODIDE_BASE_URL__", PYODIDE_BASE_URL)
|
|
158
|
+
payload = payload.replace("__PYODIDE_SCRIPT_URL__", PYODIDE_SCRIPT_URL)
|
|
159
|
+
|
|
160
|
+
# Keep line breaks: collapsing them can make a `//` comment consume the
|
|
161
|
+
# remaining JavaScript. `quote` encodes them safely for a bookmarklet URL.
|
|
162
|
+
return f"javascript:{quote(payload, safe='')}"
|
|
163
|
+
|
|
164
|
+
def generate_bookmarklet(python_file_path: Path, output_path: Path) -> bool:
|
|
165
|
+
"""Generate a bookmarklet from *python_file_path* and write it to *output_path*."""
|
|
166
|
+
try:
|
|
167
|
+
python_code = python_file_path.read_text(encoding="utf-8")
|
|
168
|
+
except (OSError, UnicodeError) as error:
|
|
169
|
+
print(f"Error reading {python_file_path}: {error}")
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
bookmarklet = build_bookmarklet(python_code)
|
|
173
|
+
try:
|
|
174
|
+
output_path.write_text(bookmarklet, encoding="utf-8")
|
|
175
|
+
except OSError as error:
|
|
176
|
+
print(f"Error writing {output_path}: {error}")
|
|
177
|
+
return False
|
|
178
|
+
|
|
179
|
+
print(f"Bookmarklet written to {output_path} ({len(bookmarklet)} characters).")
|
|
180
|
+
return True
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pymarklet
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generate browser bookmarklets that run Python with Pyodide.
|
|
5
|
+
Keywords: bookmarklet,browser,pyodide,python,webassembly
|
|
6
|
+
Author: PyMarklet contributors
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Web Environment
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
20
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Project-URL: Homepage, https://github.com/Block120/PyMarklet
|
|
23
|
+
Project-URL: Repository, https://github.com/Block120/PyMarklet
|
|
24
|
+
Project-URL: Issues, https://github.com/Block120/PyMarklet/issues
|
|
25
|
+
Project-URL: Changelog, https://github.com/Block120/PyMarklet/releases
|
|
26
|
+
Provides-Extra: test
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# PyMarklet
|
|
30
|
+
|
|
31
|
+
[](https://github.com/Block120/PyMarklet/actions/workflows/ci.yml)
|
|
32
|
+
[](https://pypi.org/project/pymarklet/)
|
|
33
|
+
[](https://pypi.org/project/pymarklet/)
|
|
34
|
+
|
|
35
|
+
Generate a browser bookmarklet that runs a Python script with [Pyodide](https://pyodide.org/). The generated bookmarklet adds a draggable output panel to the current page, loads Python in WebAssembly, and runs your embedded script in the browser.
|
|
36
|
+
|
|
37
|
+
> **Experimental:** PyMarklet executes code in the context of the web page where the bookmarklet is activated. Read the [Security and privacy](#security-and-privacy) section before sharing or running a bookmarklet.
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
python -m pip install pymarklet
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
PyMarklet requires Python 3.11 or later to generate bookmarklets. The resulting bookmarklet runs Python in a supported modern browser; it does not use your local Python environment at runtime.
|
|
46
|
+
|
|
47
|
+
## Quick start
|
|
48
|
+
|
|
49
|
+
Create a Python script:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
# hello.py
|
|
53
|
+
print("Hello from Python in your browser!")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Generate a bookmarklet URL:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
pymarklet hello.py -o hello-bookmarklet.txt
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
1. Open `hello-bookmarklet.txt` and copy its complete contents.
|
|
63
|
+
2. Create a browser bookmark named **PyMarklet: hello**.
|
|
64
|
+
3. Paste the copied `javascript:` URL into the bookmark's URL/location field.
|
|
65
|
+
4. Visit a webpage and activate the bookmark.
|
|
66
|
+
|
|
67
|
+
A panel appears in the upper-right corner with status and Python output. Drag its header to move it, click **×** to remove it, and click the canvas before sending keyboard input to graphical applications.
|
|
68
|
+
|
|
69
|
+
> Some browsers block pasting `javascript:` URLs into the address bar. Paste the URL into a bookmark's URL field instead.
|
|
70
|
+
|
|
71
|
+
## Command line
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
pymarklet [-h] [-o OUTPUT] python_file
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
| Argument | Description |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| `python_file` | UTF-8 Python source file to embed. |
|
|
80
|
+
| `-o`, `--output` | Destination file for the bookmarklet URL. Defaults to `bookmarklet.js` in the current directory. |
|
|
81
|
+
|
|
82
|
+
The command returns a nonzero status if it cannot read the source or write the output file.
|
|
83
|
+
|
|
84
|
+
## Dependencies
|
|
85
|
+
|
|
86
|
+
PyMarklet determines dependencies in two stages when the bookmarklet runs:
|
|
87
|
+
|
|
88
|
+
1. It asks Pyodide to load packages it recognizes from normal `import` statements.
|
|
89
|
+
2. For missing imports, it attempts to install a package with the same name using `micropip`.
|
|
90
|
+
|
|
91
|
+
When a PyPI distribution name differs from its import name, declare it in a **first-line** manifest:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
# PKGS: python-dateutil, more-itertools
|
|
95
|
+
from dateutil import parser
|
|
96
|
+
from more_itertools import chunked
|
|
97
|
+
|
|
98
|
+
print(parser.isoparse("2026-10-10T12:34:56Z"))
|
|
99
|
+
print(list(chunked(range(6), 2)))
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The manifest is comma-separated, case-insensitive (`# pkgs:` also works), trims duplicates, and accepts normal PyPI requirement specifiers such as `requests>=2.32`. Use it for every dependency whose import name is not its distribution name—for example, `scikit-learn` / `sklearn`.
|
|
103
|
+
|
|
104
|
+
## How it works
|
|
105
|
+
|
|
106
|
+
The generated URL embeds your source, creates a small page overlay, and loads **Pyodide 0.27.5** from jsDelivr. Standard output and errors appear in the overlay. The canvas is available to Pyodide-compatible graphical libraries such as Pygame.
|
|
107
|
+
|
|
108
|
+
The source in [`examples/integration_stress_test.py`](examples/integration_stress_test.py) and the page in [`examples/sandbox.html`](examples/sandbox.html) are a manual integration example:
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
pymarklet examples/integration_stress_test.py
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Open `examples/sandbox.html` in a browser, then use the generated bookmarklet on that page.
|
|
115
|
+
|
|
116
|
+
## Limitations
|
|
117
|
+
|
|
118
|
+
- **Network access is required.** The browser fetches Pyodide from jsDelivr and may fetch packages from PyPI-compatible hosts.
|
|
119
|
+
- **Content Security Policy (CSP) can prevent execution.** Sites may block the injected Pyodide script, package downloads, or required browser APIs.
|
|
120
|
+
- **Only Pyodide-compatible packages work.** Native packages without compatible WebAssembly wheels cannot be installed.
|
|
121
|
+
- **Import detection is static and best-effort.** Dynamic imports and relative imports are not detected; use `# PKGS:` for dependencies that need explicit installation.
|
|
122
|
+
- **Bookmark URLs have practical length limits.** The loader is already several thousand characters long, and the URL grows with your script. Keep scripts small.
|
|
123
|
+
- Closing the overlay removes its UI but does not forcibly cancel Python work already scheduled in the page.
|
|
124
|
+
|
|
125
|
+
## Security and privacy
|
|
126
|
+
|
|
127
|
+
- A bookmarklet is executable code. Only run bookmarklets you trust.
|
|
128
|
+
- Your Python source is embedded in the bookmarklet URL. **Never include secrets, credentials, private endpoints, or sensitive data.** Anyone with the URL can extract the source.
|
|
129
|
+
- The bookmarklet executes with the permissions and network environment of the page where you activate it. Review site policies and do not use it on pages where injected code is inappropriate.
|
|
130
|
+
- Dependencies are downloaded and executed in the browser. Pin or constrain declared requirements where reproducibility matters.
|
|
131
|
+
- PyMarklet relies on jsDelivr to serve its pinned Pyodide runtime. Ad blockers, offline mode, proxies, or supply-chain policy may block it.
|
|
132
|
+
|
|
133
|
+
See [`SECURITY.md`](SECURITY.md) for vulnerability reporting.
|
|
134
|
+
|
|
135
|
+
## Development
|
|
136
|
+
|
|
137
|
+
```sh
|
|
138
|
+
uv sync
|
|
139
|
+
uv run python -m unittest discover -s tests -v
|
|
140
|
+
uv build
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Contributions are welcome; see [`CONTRIBUTING.md`](CONTRIBUTING.md). This project is licensed under the [MIT License](LICENSE).
|
|
144
|
+
|
|
145
|
+
## Publishing checklist
|
|
146
|
+
|
|
147
|
+
Before the first public release, confirm the `pymarklet` name is available on PyPI and configure PyPI Trusted Publishing for this repository and the `pypi` GitHub environment.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
pymarklet/__init__.py,sha256=L1JChUeGKvKgb3RcUq--UGvWzwq2Iyh_M8F-W0deTmU,265
|
|
2
|
+
pymarklet/cli.py,sha256=FaCiDMbLvkdLVJIDHUnhuxTl0XznpPwiiYO9wEDz2wY,1047
|
|
3
|
+
pymarklet/generator.py,sha256=0LFwOGnRM8dBvGR7uF0w0D5dHSQ_51CFR3Zzz0hEKvk,8374
|
|
4
|
+
pymarklet-0.1.0.dist-info/licenses/LICENSE,sha256=CjOdOot5xuiS9QoMisbF2iNRVseoHihq8SAdwIdK6TE,1079
|
|
5
|
+
pymarklet-0.1.0.dist-info/WHEEL,sha256=xTCBFIZIIyRSxyowOv5Wqg_jFU6KsdIffuulBB09Ph8,80
|
|
6
|
+
pymarklet-0.1.0.dist-info/entry_points.txt,sha256=yu0HNdKBY0K5Xuzc3WVMlWqgSVZNdOlW9Pxx7qD3HXk,50
|
|
7
|
+
pymarklet-0.1.0.dist-info/METADATA,sha256=kXwIdSMLmGO1-W1uKfCIG56SmHwkIFUfhSP5TASCxng,6949
|
|
8
|
+
pymarklet-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PyMarklet contributors
|
|
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.
|