python-script-launcher 0.0.10__tar.gz
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-0.0.10/PKG-INFO +181 -0
- python_script_launcher-0.0.10/README.md +153 -0
- python_script_launcher-0.0.10/pyproject.toml +44 -0
- python_script_launcher-0.0.10/python_script_launcher/__init__.py +49 -0
- python_script_launcher-0.0.10/python_script_launcher/__main__.py +191 -0
- python_script_launcher-0.0.10/python_script_launcher/app.py +677 -0
- python_script_launcher-0.0.10/python_script_launcher/client.py +99 -0
- python_script_launcher-0.0.10/python_script_launcher/make_exe.py +189 -0
- python_script_launcher-0.0.10/python_script_launcher/scripts/file_status.py +34 -0
- python_script_launcher-0.0.10/python_script_launcher/scripts/hello.py +17 -0
- python_script_launcher-0.0.10/python_script_launcher/scripts/multi.py +24 -0
- python_script_launcher-0.0.10/python_script_launcher/scripts/test_modules.py +24 -0
- python_script_launcher-0.0.10/python_script_launcher/static/index.html +147 -0
- python_script_launcher-0.0.10/python_script_launcher/tasks.py +189 -0
- python_script_launcher-0.0.10/python_script_launcher.egg-info/PKG-INFO +181 -0
- python_script_launcher-0.0.10/python_script_launcher.egg-info/SOURCES.txt +19 -0
- python_script_launcher-0.0.10/python_script_launcher.egg-info/dependency_links.txt +1 -0
- python_script_launcher-0.0.10/python_script_launcher.egg-info/entry_points.txt +2 -0
- python_script_launcher-0.0.10/python_script_launcher.egg-info/requires.txt +16 -0
- python_script_launcher-0.0.10/python_script_launcher.egg-info/top_level.txt +1 -0
- python_script_launcher-0.0.10/setup.cfg +4 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-script-launcher
|
|
3
|
+
Version: 0.0.10
|
|
4
|
+
Summary: Package Python scripts into standalone exe (Web + desktop client).
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Keywords: pyinstaller,packaging,exe,launcher,scripts
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: fastapi>=0.100.0
|
|
16
|
+
Requires-Dist: python-multipart>=0.0.32
|
|
17
|
+
Requires-Dist: uvicorn[standard]>=0.20.0
|
|
18
|
+
Requires-Dist: requests>=2.34.2
|
|
19
|
+
Provides-Extra: desktop
|
|
20
|
+
Requires-Dist: pywebview>=4.0; extra == "desktop"
|
|
21
|
+
Provides-Extra: build
|
|
22
|
+
Requires-Dist: pyinstaller>=6.0; extra == "build"
|
|
23
|
+
Requires-Dist: build>=1.0; extra == "build"
|
|
24
|
+
Provides-Extra: all
|
|
25
|
+
Requires-Dist: pywebview>=4.0; extra == "all"
|
|
26
|
+
Requires-Dist: pyinstaller>=6.0; extra == "all"
|
|
27
|
+
Requires-Dist: build>=1.0; extra == "all"
|
|
28
|
+
|
|
29
|
+
# Python App Launcher
|
|
30
|
+
|
|
31
|
+
一键把任意 Python 脚本目录变成独立 Web/桌面 exe:自动扫描脚本、生成参数表单、实时流式输出,支持一份脚本注册多个可运行任务。
|
|
32
|
+
|
|
33
|
+
## 特性
|
|
34
|
+
|
|
35
|
+
- **自动扫描**:递归识别 `scripts/` 下的入口,支持 `@task` 装饰器与传统 `def main()`。
|
|
36
|
+
- **多任务脚本**:一个 `.py` 文件可注册多个 `@task`,无需手写 `if __name__ == "__main__"`。
|
|
37
|
+
- **参数表单**:根据函数签名自动生成 UI(`str/int/float/bool` + 默认值 + 文件上传)。
|
|
38
|
+
- **实时输出**:进程内 `runpy` / `importlib` 执行 + SSE 行流,冷启动毫秒级。
|
|
39
|
+
- **一键打包**:`launcher build` 调用 PyInstaller,产出无需 Python 环境的 `.exe`。
|
|
40
|
+
- **双形态发布**:`publish.py` 同步产出 wheel + sdist + `onedir/onefile` exe + release zip。
|
|
41
|
+
|
|
42
|
+
## 安装
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# 通过 uv(推荐)
|
|
46
|
+
uv sync
|
|
47
|
+
|
|
48
|
+
# 或安装已发布的 wheel(含打包依赖)
|
|
49
|
+
pip install "python-script-launcher[build]"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 用户脚本写法
|
|
53
|
+
|
|
54
|
+
推荐使用 `@task` 装饰器,一个文件可暴露多个可调用任务:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
# my_tools/demo.py
|
|
58
|
+
from python_script_launcher import task # 或 from tasks import task
|
|
59
|
+
|
|
60
|
+
@task
|
|
61
|
+
def hi(name: str = "world"):
|
|
62
|
+
"""打个招呼。"""
|
|
63
|
+
print(f"hi, {name}!")
|
|
64
|
+
|
|
65
|
+
@task(name="add", desc="加两个整数")
|
|
66
|
+
def add_(a: int, b: int = 1):
|
|
67
|
+
print(a + b)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
依然兼容旧式 `def main(...) + __main__` 脚本:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
def main(name: str = "world"):
|
|
74
|
+
print(f"hello, {name}!")
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__":
|
|
77
|
+
import argparse
|
|
78
|
+
p = argparse.ArgumentParser()
|
|
79
|
+
p.add_argument("--name", default="world")
|
|
80
|
+
main(**vars(p.parse_args()))
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**约定**
|
|
84
|
+
|
|
85
|
+
| 特性 | 说明 |
|
|
86
|
+
|------|------|
|
|
87
|
+
| 入口 | `@task` 装饰函数,或顶层 `def main(...)` |
|
|
88
|
+
| 参数类型 | 通过类型注解推导(`str/int/float/bool`),无注解按 `str` 处理 |
|
|
89
|
+
| 必填/可选 | 有默认值 → 可选;无默认值 → 必填 |
|
|
90
|
+
| 文件参数 | 参数名含 `file` / `path` / `upload` 时启用上传控件 |
|
|
91
|
+
| 说明文字 | 函数 docstring 显示在 UI 上 |
|
|
92
|
+
|
|
93
|
+
## CLI
|
|
94
|
+
|
|
95
|
+
安装后可直接使用 `launcher` 入口(或 `python -m python_script_launcher`):
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# 扫描并列出脚本
|
|
99
|
+
launcher scan --scripts D:\my_tools
|
|
100
|
+
|
|
101
|
+
# 开发模式启动 Web UI(http://localhost:8765)
|
|
102
|
+
launcher run --scripts D:\my_tools --port 8765
|
|
103
|
+
|
|
104
|
+
# 打包成 exe(onedir 默认,冷启动快)
|
|
105
|
+
launcher build --scripts D:\my_tools --name MyTools --mode both
|
|
106
|
+
|
|
107
|
+
# 单文件 exe(体积大、启动稍慢)
|
|
108
|
+
launcher build --scripts D:\my_tools --onefile --clean
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`build` 参数:
|
|
112
|
+
|
|
113
|
+
| 参数 | 说明 | 默认 |
|
|
114
|
+
|------|------|------|
|
|
115
|
+
| `--scripts, -s` | 脚本目录 | 必填 |
|
|
116
|
+
| `--output, -o` | 产物目录 | `./dist` |
|
|
117
|
+
| `--name, -n` | 应用名 | 脚本目录名 |
|
|
118
|
+
| `--mode, -m` | `web` / `client` / `both` | `both` |
|
|
119
|
+
| `--onefile` | 单文件 exe | `false` |
|
|
120
|
+
| `--clean` | 丢弃 `build/` 缓存重打 | `false` |
|
|
121
|
+
|
|
122
|
+
## 作为库调用
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from python_script_launcher import create_app, scan_scripts, start_desktop
|
|
126
|
+
from pathlib import Path
|
|
127
|
+
|
|
128
|
+
# 扫描
|
|
129
|
+
scripts = scan_scripts(Path(r"D:\\my_tools"))
|
|
130
|
+
|
|
131
|
+
# 启动 Web
|
|
132
|
+
app = create_app(root=r"D:\\my_tools") # ASGI app, 用 uvicorn.run 起
|
|
133
|
+
|
|
134
|
+
# 启动桌面
|
|
135
|
+
start_desktop(root=r"D:\\my_tools", title="我的工具", port=8765)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## 发布 / 版本管理
|
|
139
|
+
|
|
140
|
+
`publish.py` 是唯一的发布入口,从根目录源码 stage 出一份 `python_script_launcher/` 布局并调用 `python -m build`:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# 只出 wheel + sdist
|
|
144
|
+
python publish.py --wheel
|
|
145
|
+
|
|
146
|
+
# 只出 exe(onedir,PythonAppLauncher / PythonAppDesktop)
|
|
147
|
+
python publish.py --exe
|
|
148
|
+
|
|
149
|
+
# 完整发布:wheel + exe + zip
|
|
150
|
+
python publish.py --wheel --exe --zip
|
|
151
|
+
|
|
152
|
+
# 升版本号(写入 VERSION 后再构建)
|
|
153
|
+
python publish.py --version 1.2.0 --wheel --exe
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## 打包原理速览
|
|
157
|
+
|
|
158
|
+
- `make_exe.py` 用 `importlib.metadata.distributions()` 拿到当前解释器里所有第三方包,通过 `--collect-all` 全量塞进 exe,避开脆弱的 import 扫描。
|
|
159
|
+
- `__main__.py::_stage_project()` 会把 `app.py / client.py / tasks.py / static/` + 用户脚本复制到临时目录,并合成 `python_script_launcher/` 包 shim(转发 `tasks`),让打包后的 exe 里用户脚本 `from python_script_launcher import task` 依然可用。
|
|
160
|
+
- 打包后 `app.py` 在 frozen 模式下直接进程内跑用户脚本(`runpy.run_path` 或 `@task` importlib),不再拉起子 Python,冷启动 20-60 ms。
|
|
161
|
+
|
|
162
|
+
## 目录结构
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
pyRunner/
|
|
166
|
+
├─ app.py # FastAPI 后端 + 脚本扫描 / SSE 执行
|
|
167
|
+
├─ client.py # pywebview 桌面壳
|
|
168
|
+
├─ tasks.py # @task 装饰器 + run_cli()
|
|
169
|
+
├─ make_exe.py # PyInstaller 封装(build_exe 可复用)
|
|
170
|
+
├─ __init__.py # wheel 包入口 + 版本号
|
|
171
|
+
├─ __main__.py # launcher CLI (scan/run/build)
|
|
172
|
+
├─ publish.py # wheel + exe + zip 发布器
|
|
173
|
+
├─ static/ # Web UI(单页)
|
|
174
|
+
├─ scripts/ # 示例脚本
|
|
175
|
+
├─ pyproject.toml # 本地 dev 依赖(wheel 由 publish.py 生成)
|
|
176
|
+
└─ VERSION # 版本号(publish.py 读写)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
MIT
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Python App Launcher
|
|
2
|
+
|
|
3
|
+
一键把任意 Python 脚本目录变成独立 Web/桌面 exe:自动扫描脚本、生成参数表单、实时流式输出,支持一份脚本注册多个可运行任务。
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
- **自动扫描**:递归识别 `scripts/` 下的入口,支持 `@task` 装饰器与传统 `def main()`。
|
|
8
|
+
- **多任务脚本**:一个 `.py` 文件可注册多个 `@task`,无需手写 `if __name__ == "__main__"`。
|
|
9
|
+
- **参数表单**:根据函数签名自动生成 UI(`str/int/float/bool` + 默认值 + 文件上传)。
|
|
10
|
+
- **实时输出**:进程内 `runpy` / `importlib` 执行 + SSE 行流,冷启动毫秒级。
|
|
11
|
+
- **一键打包**:`launcher build` 调用 PyInstaller,产出无需 Python 环境的 `.exe`。
|
|
12
|
+
- **双形态发布**:`publish.py` 同步产出 wheel + sdist + `onedir/onefile` exe + release zip。
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# 通过 uv(推荐)
|
|
18
|
+
uv sync
|
|
19
|
+
|
|
20
|
+
# 或安装已发布的 wheel(含打包依赖)
|
|
21
|
+
pip install "python-script-launcher[build]"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 用户脚本写法
|
|
25
|
+
|
|
26
|
+
推荐使用 `@task` 装饰器,一个文件可暴露多个可调用任务:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
# my_tools/demo.py
|
|
30
|
+
from python_script_launcher import task # 或 from tasks import task
|
|
31
|
+
|
|
32
|
+
@task
|
|
33
|
+
def hi(name: str = "world"):
|
|
34
|
+
"""打个招呼。"""
|
|
35
|
+
print(f"hi, {name}!")
|
|
36
|
+
|
|
37
|
+
@task(name="add", desc="加两个整数")
|
|
38
|
+
def add_(a: int, b: int = 1):
|
|
39
|
+
print(a + b)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
依然兼容旧式 `def main(...) + __main__` 脚本:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
def main(name: str = "world"):
|
|
46
|
+
print(f"hello, {name}!")
|
|
47
|
+
|
|
48
|
+
if __name__ == "__main__":
|
|
49
|
+
import argparse
|
|
50
|
+
p = argparse.ArgumentParser()
|
|
51
|
+
p.add_argument("--name", default="world")
|
|
52
|
+
main(**vars(p.parse_args()))
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**约定**
|
|
56
|
+
|
|
57
|
+
| 特性 | 说明 |
|
|
58
|
+
|------|------|
|
|
59
|
+
| 入口 | `@task` 装饰函数,或顶层 `def main(...)` |
|
|
60
|
+
| 参数类型 | 通过类型注解推导(`str/int/float/bool`),无注解按 `str` 处理 |
|
|
61
|
+
| 必填/可选 | 有默认值 → 可选;无默认值 → 必填 |
|
|
62
|
+
| 文件参数 | 参数名含 `file` / `path` / `upload` 时启用上传控件 |
|
|
63
|
+
| 说明文字 | 函数 docstring 显示在 UI 上 |
|
|
64
|
+
|
|
65
|
+
## CLI
|
|
66
|
+
|
|
67
|
+
安装后可直接使用 `launcher` 入口(或 `python -m python_script_launcher`):
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# 扫描并列出脚本
|
|
71
|
+
launcher scan --scripts D:\my_tools
|
|
72
|
+
|
|
73
|
+
# 开发模式启动 Web UI(http://localhost:8765)
|
|
74
|
+
launcher run --scripts D:\my_tools --port 8765
|
|
75
|
+
|
|
76
|
+
# 打包成 exe(onedir 默认,冷启动快)
|
|
77
|
+
launcher build --scripts D:\my_tools --name MyTools --mode both
|
|
78
|
+
|
|
79
|
+
# 单文件 exe(体积大、启动稍慢)
|
|
80
|
+
launcher build --scripts D:\my_tools --onefile --clean
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`build` 参数:
|
|
84
|
+
|
|
85
|
+
| 参数 | 说明 | 默认 |
|
|
86
|
+
|------|------|------|
|
|
87
|
+
| `--scripts, -s` | 脚本目录 | 必填 |
|
|
88
|
+
| `--output, -o` | 产物目录 | `./dist` |
|
|
89
|
+
| `--name, -n` | 应用名 | 脚本目录名 |
|
|
90
|
+
| `--mode, -m` | `web` / `client` / `both` | `both` |
|
|
91
|
+
| `--onefile` | 单文件 exe | `false` |
|
|
92
|
+
| `--clean` | 丢弃 `build/` 缓存重打 | `false` |
|
|
93
|
+
|
|
94
|
+
## 作为库调用
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from python_script_launcher import create_app, scan_scripts, start_desktop
|
|
98
|
+
from pathlib import Path
|
|
99
|
+
|
|
100
|
+
# 扫描
|
|
101
|
+
scripts = scan_scripts(Path(r"D:\\my_tools"))
|
|
102
|
+
|
|
103
|
+
# 启动 Web
|
|
104
|
+
app = create_app(root=r"D:\\my_tools") # ASGI app, 用 uvicorn.run 起
|
|
105
|
+
|
|
106
|
+
# 启动桌面
|
|
107
|
+
start_desktop(root=r"D:\\my_tools", title="我的工具", port=8765)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## 发布 / 版本管理
|
|
111
|
+
|
|
112
|
+
`publish.py` 是唯一的发布入口,从根目录源码 stage 出一份 `python_script_launcher/` 布局并调用 `python -m build`:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# 只出 wheel + sdist
|
|
116
|
+
python publish.py --wheel
|
|
117
|
+
|
|
118
|
+
# 只出 exe(onedir,PythonAppLauncher / PythonAppDesktop)
|
|
119
|
+
python publish.py --exe
|
|
120
|
+
|
|
121
|
+
# 完整发布:wheel + exe + zip
|
|
122
|
+
python publish.py --wheel --exe --zip
|
|
123
|
+
|
|
124
|
+
# 升版本号(写入 VERSION 后再构建)
|
|
125
|
+
python publish.py --version 1.2.0 --wheel --exe
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## 打包原理速览
|
|
129
|
+
|
|
130
|
+
- `make_exe.py` 用 `importlib.metadata.distributions()` 拿到当前解释器里所有第三方包,通过 `--collect-all` 全量塞进 exe,避开脆弱的 import 扫描。
|
|
131
|
+
- `__main__.py::_stage_project()` 会把 `app.py / client.py / tasks.py / static/` + 用户脚本复制到临时目录,并合成 `python_script_launcher/` 包 shim(转发 `tasks`),让打包后的 exe 里用户脚本 `from python_script_launcher import task` 依然可用。
|
|
132
|
+
- 打包后 `app.py` 在 frozen 模式下直接进程内跑用户脚本(`runpy.run_path` 或 `@task` importlib),不再拉起子 Python,冷启动 20-60 ms。
|
|
133
|
+
|
|
134
|
+
## 目录结构
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
pyRunner/
|
|
138
|
+
├─ app.py # FastAPI 后端 + 脚本扫描 / SSE 执行
|
|
139
|
+
├─ client.py # pywebview 桌面壳
|
|
140
|
+
├─ tasks.py # @task 装饰器 + run_cli()
|
|
141
|
+
├─ make_exe.py # PyInstaller 封装(build_exe 可复用)
|
|
142
|
+
├─ __init__.py # wheel 包入口 + 版本号
|
|
143
|
+
├─ __main__.py # launcher CLI (scan/run/build)
|
|
144
|
+
├─ publish.py # wheel + exe + zip 发布器
|
|
145
|
+
├─ static/ # Web UI(单页)
|
|
146
|
+
├─ scripts/ # 示例脚本
|
|
147
|
+
├─ pyproject.toml # 本地 dev 依赖(wheel 由 publish.py 生成)
|
|
148
|
+
└─ VERSION # 版本号(publish.py 读写)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
MIT
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "python-script-launcher"
|
|
7
|
+
version = "0.0.10"
|
|
8
|
+
description = "Package Python scripts into standalone exe (Web + desktop client)."
|
|
9
|
+
license = "MIT"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
keywords = ["pyinstaller", "packaging", "exe", "launcher", "scripts"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Topic :: Software Development :: Build Tools",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"fastapi>=0.100.0",
|
|
23
|
+
"python-multipart>=0.0.32",
|
|
24
|
+
"uvicorn[standard]>=0.20.0",
|
|
25
|
+
"requests>=2.34.2",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
desktop = ["pywebview>=4.0"]
|
|
30
|
+
build = ["pyinstaller>=6.0", "build>=1.0"]
|
|
31
|
+
all = ["pywebview>=4.0", "pyinstaller>=6.0", "build>=1.0"]
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
launcher = "python_script_launcher.__main__:main"
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.packages.find]
|
|
37
|
+
include = ["python_script_launcher", "python_script_launcher.*"]
|
|
38
|
+
|
|
39
|
+
[tool.setuptools.package-data]
|
|
40
|
+
"python_script_launcher" = [
|
|
41
|
+
"app.py", "client.py", "make_exe.py", "tasks.py",
|
|
42
|
+
"static/**/*",
|
|
43
|
+
"scripts/**/*.py",
|
|
44
|
+
]
|
|
@@ -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()
|