python-script-launcher 0.0.10__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.
- python_script_launcher/__init__.py +49 -0
- python_script_launcher/__main__.py +191 -0
- python_script_launcher/app.py +677 -0
- python_script_launcher/client.py +99 -0
- python_script_launcher/make_exe.py +189 -0
- python_script_launcher/scripts/file_status.py +34 -0
- python_script_launcher/scripts/hello.py +17 -0
- python_script_launcher/scripts/multi.py +24 -0
- python_script_launcher/scripts/test_modules.py +24 -0
- python_script_launcher/static/index.html +147 -0
- python_script_launcher/tasks.py +189 -0
- python_script_launcher-0.0.10.dist-info/METADATA +181 -0
- python_script_launcher-0.0.10.dist-info/RECORD +16 -0
- python_script_launcher-0.0.10.dist-info/WHEEL +5 -0
- python_script_launcher-0.0.10.dist-info/entry_points.txt +2 -0
- python_script_launcher-0.0.10.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Python App Launcher - Package Python scripts into standalone exe.
|
|
2
|
+
|
|
3
|
+
This package is normally built by publish.py from the top-level source files
|
|
4
|
+
(app.py, client.py, tasks.py, __init__.py, __main__.py). Importing this
|
|
5
|
+
package makes the sibling `app` / `client` / `launcher` modules available under
|
|
6
|
+
their bare names, so `from app import ...` works both from the repo root and
|
|
7
|
+
from an installed wheel.
|
|
8
|
+
"""
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
# Make sibling modules (app.py, client.py, tasks.py) importable by bare
|
|
14
|
+
# name, matching how they are used from the repo root and inside a frozen exe.
|
|
15
|
+
_here = os.path.dirname(os.path.abspath(__file__))
|
|
16
|
+
if _here not in sys.path:
|
|
17
|
+
sys.path.insert(0, _here)
|
|
18
|
+
|
|
19
|
+
# When installed from a wheel, the version comes from package metadata.
|
|
20
|
+
# When running from the source repo, fall back to the VERSION file at the
|
|
21
|
+
# project root (which publish.py maintains).
|
|
22
|
+
try:
|
|
23
|
+
from importlib.metadata import version as _pkg_version, PackageNotFoundError
|
|
24
|
+
try:
|
|
25
|
+
__version__ = _pkg_version("python-script-launcher")
|
|
26
|
+
except PackageNotFoundError:
|
|
27
|
+
_version_file = Path(_here).parent / "VERSION"
|
|
28
|
+
__version__ = _version_file.read_text(encoding="utf-8").strip() if _version_file.exists() else "0.0.0"
|
|
29
|
+
except Exception:
|
|
30
|
+
__version__ = "0.0.0"
|
|
31
|
+
|
|
32
|
+
from app import create_app, scan_scripts, find_python, ScriptInfo, ParamInfo # noqa: E402
|
|
33
|
+
from client import start_desktop # noqa: E402
|
|
34
|
+
from tasks import task, run_cli, collect_tasks, TaskSpec, TaskParam # noqa: E402
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"create_app",
|
|
38
|
+
"scan_scripts",
|
|
39
|
+
"find_python",
|
|
40
|
+
"start_desktop",
|
|
41
|
+
"ScriptInfo",
|
|
42
|
+
"ParamInfo",
|
|
43
|
+
"task",
|
|
44
|
+
"run_cli",
|
|
45
|
+
"collect_tasks",
|
|
46
|
+
"TaskSpec",
|
|
47
|
+
"TaskParam",
|
|
48
|
+
"__version__",
|
|
49
|
+
]
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python App Launcher CLI.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python -m python_script_launcher scan --scripts <dir>
|
|
6
|
+
python -m python_script_launcher run --scripts <dir> [--port <port>]
|
|
7
|
+
python -m python_script_launcher build --scripts <dir>
|
|
8
|
+
[--output <dir>] [--name <exe>]
|
|
9
|
+
[--mode web|client|both]
|
|
10
|
+
[--onefile] [--clean]
|
|
11
|
+
|
|
12
|
+
`build` reuses make_exe.build_exe: it stages the launcher runtime + your
|
|
13
|
+
scripts into a temporary project directory and invokes PyInstaller with
|
|
14
|
+
`--collect-all` for every package installed in the current environment.
|
|
15
|
+
"""
|
|
16
|
+
import argparse
|
|
17
|
+
import shutil
|
|
18
|
+
import sys
|
|
19
|
+
import tempfile
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
PACKAGE_DIR = Path(__file__).parent.resolve()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _die(msg: str, code: int = 1):
|
|
27
|
+
print(f"[ERROR] {msg}")
|
|
28
|
+
sys.exit(code)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def cmd_scan(args):
|
|
32
|
+
from app import scan_scripts
|
|
33
|
+
root = Path(args.scripts).resolve()
|
|
34
|
+
if not root.exists():
|
|
35
|
+
_die(f"Directory not found: {root}")
|
|
36
|
+
scripts = scan_scripts(root)
|
|
37
|
+
if not scripts:
|
|
38
|
+
_die(f"No scripts with main() found in: {root}")
|
|
39
|
+
print(f"\n Scripts in: {root}")
|
|
40
|
+
print(f" Found: {len(scripts)}\n")
|
|
41
|
+
for name, info in scripts.items():
|
|
42
|
+
params = ", ".join(p.name for p in info.params)
|
|
43
|
+
print(f" - {name}({params})")
|
|
44
|
+
first_line = info.docstring.split("\n")[0]
|
|
45
|
+
if first_line:
|
|
46
|
+
print(f" {first_line}")
|
|
47
|
+
return scripts
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def cmd_run(args):
|
|
51
|
+
from app import create_app, scan_scripts, is_port_used
|
|
52
|
+
import uvicorn
|
|
53
|
+
root = Path(args.scripts).resolve()
|
|
54
|
+
if not root.exists():
|
|
55
|
+
_die(f"Directory not found: {root}")
|
|
56
|
+
scripts = scan_scripts(root)
|
|
57
|
+
print(f"\n Found {len(scripts)} scripts in {root}")
|
|
58
|
+
port = args.port
|
|
59
|
+
if is_port_used(port):
|
|
60
|
+
_die(f"Port {port} already in use")
|
|
61
|
+
app = create_app(root=root)
|
|
62
|
+
print(f" http://localhost:{port}\n")
|
|
63
|
+
uvicorn.run(app, host="0.0.0.0", port=port)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
LAUNCHER_FILES = ["app.py", "client.py", "tasks.py"]
|
|
67
|
+
STATIC_DIR = "static"
|
|
68
|
+
SHIM_PKG = "python_script_launcher"
|
|
69
|
+
SHIM_INIT = (
|
|
70
|
+
'"'*3 + "Compatibility shim generated by launcher build.\n\n"
|
|
71
|
+
"Allows user scripts to write ``from python_script_launcher import task``\n"
|
|
72
|
+
"even after being packaged into a standalone exe. All names are forwarded\n"
|
|
73
|
+
"from the top-level ``tasks`` module that ships alongside the launcher.\n"
|
|
74
|
+
+ '"'*3 + "\n"
|
|
75
|
+
"from tasks import * # noqa: F401,F403\n"
|
|
76
|
+
"from tasks import task, run_cli, collect_tasks, TaskSpec, TaskParam # noqa: F401\n"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _stage_project(scripts_src: Path) -> Path:
|
|
81
|
+
"""Create a temp directory that mimics the launcher repo layout, so
|
|
82
|
+
make_exe.build_exe() can operate on it unchanged.
|
|
83
|
+
"""
|
|
84
|
+
tmp = Path(tempfile.mkdtemp(prefix="launcher-build-"))
|
|
85
|
+
for name in LAUNCHER_FILES:
|
|
86
|
+
src = PACKAGE_DIR / name
|
|
87
|
+
if src.exists():
|
|
88
|
+
shutil.copy2(src, tmp / name)
|
|
89
|
+
static_src = PACKAGE_DIR / STATIC_DIR
|
|
90
|
+
if static_src.exists():
|
|
91
|
+
shutil.copytree(static_src, tmp / STATIC_DIR)
|
|
92
|
+
# Generate a lightweight `python_script_launcher` package shim so user
|
|
93
|
+
# scripts using `from python_script_launcher import task` keep working
|
|
94
|
+
# inside the frozen exe. The exe ships `tasks.py` at its root, and the
|
|
95
|
+
# shim just re-exports its public names.
|
|
96
|
+
shim_dir = tmp / SHIM_PKG
|
|
97
|
+
shim_dir.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
(shim_dir / "__init__.py").write_text(SHIM_INIT, encoding="utf-8", newline="\n")
|
|
99
|
+
# Copy user scripts into <tmp>/scripts so both scan_scripts and
|
|
100
|
+
# PyInstaller's --add-data pick them up.
|
|
101
|
+
shutil.copytree(scripts_src, tmp / "scripts", dirs_exist_ok=True)
|
|
102
|
+
return tmp
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def cmd_build(args):
|
|
106
|
+
from app import scan_scripts
|
|
107
|
+
from make_exe import build_exe
|
|
108
|
+
|
|
109
|
+
scripts_dir = Path(args.scripts).resolve()
|
|
110
|
+
if not scripts_dir.exists():
|
|
111
|
+
_die(f"Directory not found: {scripts_dir}")
|
|
112
|
+
|
|
113
|
+
scripts = scan_scripts(scripts_dir)
|
|
114
|
+
if not scripts:
|
|
115
|
+
_die(f"No scripts with main() found in: {scripts_dir}")
|
|
116
|
+
|
|
117
|
+
output_dir = Path(args.output).resolve() if args.output else Path.cwd() / "dist"
|
|
118
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
119
|
+
app_name = args.name or scripts_dir.name
|
|
120
|
+
mode = args.mode
|
|
121
|
+
|
|
122
|
+
print("=" * 60)
|
|
123
|
+
print(f" Building : {app_name}")
|
|
124
|
+
print(f" Mode : {mode}")
|
|
125
|
+
print(f" Scripts : {len(scripts)} ({', '.join(scripts)})")
|
|
126
|
+
print(f" Output : {output_dir}")
|
|
127
|
+
print("=" * 60)
|
|
128
|
+
|
|
129
|
+
stage = _stage_project(scripts_dir)
|
|
130
|
+
try:
|
|
131
|
+
targets = []
|
|
132
|
+
if mode in ("web", "both"):
|
|
133
|
+
targets.append(("app.py", app_name, False))
|
|
134
|
+
if mode in ("client", "both"):
|
|
135
|
+
client_name = f"{app_name}Desktop" if mode == "both" else app_name
|
|
136
|
+
targets.append(("client.py", client_name, True))
|
|
137
|
+
|
|
138
|
+
for entry, name, noconsole in targets:
|
|
139
|
+
build_exe(
|
|
140
|
+
entry=entry,
|
|
141
|
+
name=name,
|
|
142
|
+
project=stage,
|
|
143
|
+
dist=output_dir,
|
|
144
|
+
static=stage / STATIC_DIR,
|
|
145
|
+
noconsole=noconsole,
|
|
146
|
+
onefile=args.onefile,
|
|
147
|
+
clean=args.clean,
|
|
148
|
+
)
|
|
149
|
+
finally:
|
|
150
|
+
shutil.rmtree(stage, ignore_errors=True)
|
|
151
|
+
|
|
152
|
+
print("=" * 60)
|
|
153
|
+
print(f" Done. Artifacts in {output_dir}")
|
|
154
|
+
print("=" * 60)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def main():
|
|
158
|
+
parser = argparse.ArgumentParser(
|
|
159
|
+
prog="python -m python_script_launcher",
|
|
160
|
+
description="Package Python scripts into standalone exe",
|
|
161
|
+
)
|
|
162
|
+
sub = parser.add_subparsers(dest="command")
|
|
163
|
+
|
|
164
|
+
p_scan = sub.add_parser("scan", help="Scan scripts directory")
|
|
165
|
+
p_scan.add_argument("--scripts", "-s", required=True)
|
|
166
|
+
|
|
167
|
+
p_run = sub.add_parser("run", help="Run launcher (dev mode)")
|
|
168
|
+
p_run.add_argument("--scripts", "-s", required=True)
|
|
169
|
+
p_run.add_argument("--port", "-p", type=int, default=8765)
|
|
170
|
+
|
|
171
|
+
p_build = sub.add_parser("build", help="Build exe")
|
|
172
|
+
p_build.add_argument("--scripts", "-s", required=True, help="Scripts directory")
|
|
173
|
+
p_build.add_argument("--output", "-o", default=None, help="Output directory")
|
|
174
|
+
p_build.add_argument("--name", "-n", default=None, help="App name")
|
|
175
|
+
p_build.add_argument("--mode", "-m", default="both",
|
|
176
|
+
choices=["web", "client", "both"],
|
|
177
|
+
help="Build mode: web / client / both (default: both)")
|
|
178
|
+
p_build.add_argument("--onefile", action="store_true",
|
|
179
|
+
help="Single-file exe (slower cold start)")
|
|
180
|
+
p_build.add_argument("--clean", action="store_true", help="Drop build cache")
|
|
181
|
+
|
|
182
|
+
args = parser.parse_args()
|
|
183
|
+
handler = {"scan": cmd_scan, "run": cmd_run, "build": cmd_build}.get(args.command)
|
|
184
|
+
if handler is None:
|
|
185
|
+
parser.print_help()
|
|
186
|
+
return
|
|
187
|
+
handler(args)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
if __name__ == "__main__":
|
|
191
|
+
main()
|