pyship 0.1.6__py3-none-any.whl → 0.3.1__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.
- pyship/__init__.py +2 -1
- pyship/__main__.py +0 -1
- pyship/_version_.py +1 -1
- pyship/app_info.py +12 -27
- pyship/clip.py +18 -104
- pyship/cloud.py +2 -1
- pyship/constants.py +7 -0
- pyship/create_launcher.py +48 -81
- pyship/download.py +18 -2
- pyship/launcher/__init__.py +1 -1
- pyship/launcher/launcher_standalone.py +309 -0
- pyship/launcher/metadata.py +5 -0
- pyship/launcher_stub.py +198 -0
- pyship/nsis.py +12 -5
- pyship/pyship.py +11 -9
- pyship/pyship_custom_print.py +0 -1
- pyship/pyship_get_icon.py +3 -1
- pyship/uv_util.py +157 -0
- pyship-0.3.1.dist-info/METADATA +81 -0
- pyship-0.3.1.dist-info/RECORD +34 -0
- {pyship-0.1.6.dist-info → pyship-0.3.1.dist-info}/WHEEL +1 -1
- pyship/atomic_zip.py +0 -41
- pyship/get-pip.py +0 -22713
- pyship/launcher/launcher.py +0 -224
- pyship/patch/__init__.py +0 -0
- pyship/patch/pyship_patch.py +0 -33
- pyship-0.1.6.dist-info/METADATA +0 -47
- pyship-0.1.6.dist-info/RECORD +0 -36
- {pyship-0.1.6.dist-info → pyship-0.3.1.dist-info/licenses}/LICENSE +0 -0
- {pyship-0.1.6.dist-info → pyship-0.3.1.dist-info}/top_level.txt +0 -0
pyship/uv_util.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
import shutil
|
|
3
|
+
import subprocess
|
|
4
|
+
import zipfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from typeguard import typechecked
|
|
8
|
+
from balsa import get_logger
|
|
9
|
+
|
|
10
|
+
from pyship import __application_name__, pyship_print
|
|
11
|
+
|
|
12
|
+
log = get_logger(__application_name__)
|
|
13
|
+
|
|
14
|
+
UV_GITHUB_RELEASE_URL = "https://github.com/astral-sh/uv/releases/latest/download"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@typechecked
|
|
18
|
+
def find_or_bootstrap_uv(cache_dir: Path) -> Path:
|
|
19
|
+
"""
|
|
20
|
+
Find uv on PATH, in cache, or download the standalone binary.
|
|
21
|
+
:param cache_dir: directory to cache the uv binary
|
|
22
|
+
:return: path to the uv executable
|
|
23
|
+
"""
|
|
24
|
+
# check PATH first
|
|
25
|
+
uv_on_path = shutil.which("uv")
|
|
26
|
+
if uv_on_path is not None:
|
|
27
|
+
uv_path = Path(uv_on_path)
|
|
28
|
+
log.info(f"found uv on PATH: {uv_path}")
|
|
29
|
+
return uv_path
|
|
30
|
+
|
|
31
|
+
# check cache
|
|
32
|
+
uv_cache_dir = Path(cache_dir, "uv")
|
|
33
|
+
uv_exe = Path(uv_cache_dir, "uv.exe")
|
|
34
|
+
if uv_exe.exists():
|
|
35
|
+
log.info(f"found cached uv: {uv_exe}")
|
|
36
|
+
return uv_exe
|
|
37
|
+
|
|
38
|
+
# download uv standalone binary
|
|
39
|
+
pyship_print("downloading uv...")
|
|
40
|
+
uv_cache_dir.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
|
|
42
|
+
machine = platform.machine().lower()
|
|
43
|
+
if machine in ("amd64", "x86_64"):
|
|
44
|
+
arch = "x86_64"
|
|
45
|
+
elif machine in ("arm64", "aarch64"):
|
|
46
|
+
arch = "aarch64"
|
|
47
|
+
else:
|
|
48
|
+
arch = machine
|
|
49
|
+
|
|
50
|
+
zip_name = f"uv-{arch}-pc-windows-msvc.zip"
|
|
51
|
+
zip_url = f"{UV_GITHUB_RELEASE_URL}/{zip_name}"
|
|
52
|
+
zip_path = Path(uv_cache_dir, zip_name)
|
|
53
|
+
|
|
54
|
+
import requests
|
|
55
|
+
|
|
56
|
+
response = requests.get(zip_url, stream=True)
|
|
57
|
+
response.raise_for_status()
|
|
58
|
+
with open(zip_path, "wb") as f:
|
|
59
|
+
shutil.copyfileobj(response.raw, f)
|
|
60
|
+
|
|
61
|
+
with zipfile.ZipFile(zip_path) as zf:
|
|
62
|
+
zf.extractall(uv_cache_dir)
|
|
63
|
+
|
|
64
|
+
# the zip extracts into a subdirectory; find uv.exe
|
|
65
|
+
if not uv_exe.exists():
|
|
66
|
+
for candidate in uv_cache_dir.rglob("uv.exe"):
|
|
67
|
+
shutil.move(str(candidate), str(uv_exe))
|
|
68
|
+
break
|
|
69
|
+
|
|
70
|
+
if not uv_exe.exists():
|
|
71
|
+
raise FileNotFoundError(f"could not find uv.exe after extracting {zip_path}")
|
|
72
|
+
|
|
73
|
+
log.info(f"downloaded uv to {uv_exe}")
|
|
74
|
+
return uv_exe
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@typechecked
|
|
78
|
+
def uv_python_install(uv_path: Path, python_version: str) -> Path:
|
|
79
|
+
"""
|
|
80
|
+
Install a Python version via uv and return its path.
|
|
81
|
+
:param uv_path: path to uv executable
|
|
82
|
+
:param python_version: Python version string (e.g. "3.12.4")
|
|
83
|
+
:return: path to the installed Python interpreter
|
|
84
|
+
"""
|
|
85
|
+
pyship_print(f"installing Python {python_version} via uv...")
|
|
86
|
+
subprocess.run([str(uv_path), "python", "install", python_version], check=True, capture_output=True, text=True)
|
|
87
|
+
|
|
88
|
+
result = subprocess.run([str(uv_path), "python", "find", python_version], check=True, capture_output=True, text=True)
|
|
89
|
+
python_path = Path(result.stdout.strip())
|
|
90
|
+
log.info(f"uv python find {python_version} -> {python_path}")
|
|
91
|
+
return python_path
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@typechecked
|
|
95
|
+
def uv_venv_create(uv_path: Path, venv_dir: Path, python_version: str) -> None:
|
|
96
|
+
"""
|
|
97
|
+
Create a relocatable venv using uv.
|
|
98
|
+
:param uv_path: path to uv executable
|
|
99
|
+
:param venv_dir: destination directory for the venv
|
|
100
|
+
:param python_version: Python version string
|
|
101
|
+
"""
|
|
102
|
+
pyship_print(f"creating relocatable venv at {venv_dir}...")
|
|
103
|
+
cmd = [str(uv_path), "venv", "--relocatable", "--python", python_version, str(venv_dir)]
|
|
104
|
+
log.info(f"uv venv cmd: {cmd}")
|
|
105
|
+
subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@typechecked
|
|
109
|
+
def uv_pip_install(uv_path: Path, target_python: Path, packages: list, find_links: list, upgrade: bool = True) -> None:
|
|
110
|
+
"""
|
|
111
|
+
Install packages into a venv using uv pip.
|
|
112
|
+
:param uv_path: path to uv executable
|
|
113
|
+
:param target_python: path to the Python interpreter in the target venv
|
|
114
|
+
:param packages: list of package names or paths to install
|
|
115
|
+
:param find_links: list of directories for --find-links
|
|
116
|
+
:param upgrade: whether to pass -U flag
|
|
117
|
+
"""
|
|
118
|
+
cmd = [str(uv_path), "pip", "install", "--python", str(target_python)]
|
|
119
|
+
if upgrade:
|
|
120
|
+
cmd.append("-U")
|
|
121
|
+
cmd.extend(packages)
|
|
122
|
+
for link in find_links:
|
|
123
|
+
link_path = Path(link)
|
|
124
|
+
if link_path.exists():
|
|
125
|
+
cmd.extend(["-f", str(link_path)])
|
|
126
|
+
log.info(f"uv pip install cmd: {cmd}")
|
|
127
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
128
|
+
log.info(result.stdout)
|
|
129
|
+
if result.stderr:
|
|
130
|
+
log.info(result.stderr)
|
|
131
|
+
if result.returncode != 0:
|
|
132
|
+
log.error(f"uv pip install failed (exit {result.returncode}): {result.stderr}")
|
|
133
|
+
result.check_returncode()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@typechecked
|
|
137
|
+
def uv_build(uv_path: Path, project_dir: Path, output_dir: Path) -> Path:
|
|
138
|
+
"""
|
|
139
|
+
Build a wheel using uv build.
|
|
140
|
+
:param uv_path: path to uv executable
|
|
141
|
+
:param project_dir: project directory containing pyproject.toml
|
|
142
|
+
:param output_dir: directory for the built wheel
|
|
143
|
+
:return: path to the built wheel
|
|
144
|
+
"""
|
|
145
|
+
pyship_print(f"building wheel via uv in {project_dir}...")
|
|
146
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
147
|
+
cmd = [str(uv_path), "build", "--wheel", "--out-dir", str(output_dir)]
|
|
148
|
+
log.info(f"uv build cmd: {cmd}")
|
|
149
|
+
result = subprocess.run(cmd, cwd=str(project_dir), check=True, capture_output=True, text=True)
|
|
150
|
+
log.info(result.stdout)
|
|
151
|
+
|
|
152
|
+
# find the wheel that was just built
|
|
153
|
+
wheels = list(output_dir.glob("*.whl"))
|
|
154
|
+
if len(wheels) == 0:
|
|
155
|
+
raise FileNotFoundError(f"no wheel found in {output_dir} after uv build")
|
|
156
|
+
# return the most recently modified wheel
|
|
157
|
+
return sorted(wheels, key=lambda p: p.stat().st_mtime)[-1]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyship
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: freezer, installer and updater for Python applications
|
|
5
|
+
Home-page: https://github.com/jamesabel/pyship
|
|
6
|
+
Download-URL: https://github.com/jamesabel/pyship
|
|
7
|
+
Author: abel
|
|
8
|
+
Author-email: j@abel.co
|
|
9
|
+
License: MIT License
|
|
10
|
+
Keywords: freezer,installer,ship
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: setuptools
|
|
14
|
+
Requires-Dist: wheel
|
|
15
|
+
Requires-Dist: ismain
|
|
16
|
+
Requires-Dist: balsa
|
|
17
|
+
Requires-Dist: requests
|
|
18
|
+
Requires-Dist: attrs
|
|
19
|
+
Requires-Dist: typeguard
|
|
20
|
+
Requires-Dist: toml
|
|
21
|
+
Requires-Dist: semver
|
|
22
|
+
Requires-Dist: python-dateutil
|
|
23
|
+
Requires-Dist: wheel-inspect
|
|
24
|
+
Requires-Dist: boto3
|
|
25
|
+
Requires-Dist: awsimple
|
|
26
|
+
Requires-Dist: platformdirs
|
|
27
|
+
Requires-Dist: pyshipupdate
|
|
28
|
+
Dynamic: author
|
|
29
|
+
Dynamic: author-email
|
|
30
|
+
Dynamic: description
|
|
31
|
+
Dynamic: description-content-type
|
|
32
|
+
Dynamic: download-url
|
|
33
|
+
Dynamic: home-page
|
|
34
|
+
Dynamic: keywords
|
|
35
|
+
Dynamic: license
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
Dynamic: requires-dist
|
|
38
|
+
Dynamic: summary
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# PyShip
|
|
42
|
+
|
|
43
|
+
[](https://github.com/jamesabel/pyship/actions/workflows/ci.yml)
|
|
44
|
+
[](https://codecov.io/gh/jamesabel/pyship)
|
|
45
|
+
|
|
46
|
+
Enables shipping a python application to end users.
|
|
47
|
+
|
|
48
|
+
## PyShip's Major Features
|
|
49
|
+
|
|
50
|
+
* Freeze practically any Python application
|
|
51
|
+
* Creates an installer
|
|
52
|
+
* Uploads application installer and updates to the cloud
|
|
53
|
+
* Automatic application updating in the background (no user intervention)
|
|
54
|
+
* OS native application (e.g. .exe for Windows)
|
|
55
|
+
* Run on OS startup option
|
|
56
|
+
|
|
57
|
+
## Documentation and Examples
|
|
58
|
+
|
|
59
|
+
[Learn PyShip By Example](https://github.com/jamesabel/pyshipexample)
|
|
60
|
+
|
|
61
|
+
[Short video on pyship given at Pyninsula](https://abelpublic.s3.us-west-2.amazonaws.com/pyship_pyninsula_10_2020.mkv)
|
|
62
|
+
|
|
63
|
+
## Testing
|
|
64
|
+
|
|
65
|
+
Run tests with:
|
|
66
|
+
|
|
67
|
+
```batch
|
|
68
|
+
venv\Scripts\python.exe -m pytest test_pyship/ -v
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Environment Variables
|
|
72
|
+
|
|
73
|
+
| Variable | Description | Default |
|
|
74
|
+
|----------|-------------|---------|
|
|
75
|
+
| `AWSIMPLE_USE_MOTO_MOCK` | Set to `0` to use real AWS instead of [moto](https://github.com/getmoto/moto) mock. Required for `test_f_update` which tests cross-process S3 updates. Requires valid AWS credentials configured. | `1` (use moto) |
|
|
76
|
+
| `MAKE_NSIS_PATH` | Path to the NSIS `makensis.exe` executable. | `C:\Program Files (x86)\NSIS\makensis.exe` |
|
|
77
|
+
|
|
78
|
+
### Test Modes
|
|
79
|
+
|
|
80
|
+
- **Default (moto mock)**: All tests run with mocked AWS S3. No credentials needed. `test_f_update` is skipped.
|
|
81
|
+
- **Real AWS** (`AWSIMPLE_USE_MOTO_MOCK=0`): All tests run against real AWS S3. `test_f_update` runs and tests cross-process updates.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
pyship/__init__.py,sha256=5lL2td91Xm31z8x4XcTZW591rO3hvFzXpAtTiKdt064,1438
|
|
2
|
+
pyship/__main__.py,sha256=KtAdk7BCWhXbhywlFB8P4HbvypVkqJXbxgMW3XVD-To,84
|
|
3
|
+
pyship/_version_.py,sha256=oP5tZarLvBvusPvGrBXb6FMKD55dVUQDZ9LYYFZi4A0,389
|
|
4
|
+
pyship/app_info.py,sha256=kxWMOhksgPijODj76LUkHuxKzPeHNWMN2Dv4D9PfAp4,6383
|
|
5
|
+
pyship/arguments.py,sha256=p4cA8FwbNSZqg4XI6LsYit1QN7TpAWWZH2xrgLf-f-g,1882
|
|
6
|
+
pyship/clip.py,sha256=N6es31zy0l3kakf1Enxt2fqkcMQoaUSXMlcCqhHJdWI,3700
|
|
7
|
+
pyship/cloud.py,sha256=k8m-254Akpp3NALeP0pJzFs21_2qZmmK0GHfSC-Z0sw,1282
|
|
8
|
+
pyship/constants.py,sha256=Y-yA_MMwL572UbZqmhILrJL6NH9xK3q5jELJR6atw7I,243
|
|
9
|
+
pyship/create_launcher.py,sha256=HxfOFC9zJf5yhTm9XtCaR49Jn9-SNkpzWgHSLyhnu6k,3794
|
|
10
|
+
pyship/download.py,sha256=QHbdVwxLNBToMEC0tOF2ZquDpoydRu0-cj7LOERr5Q4,2460
|
|
11
|
+
pyship/launcher_stub.py,sha256=iezFuelAps8_tmjMZdb7G6MkccWp9a2wFqtIxgLRICk,6677
|
|
12
|
+
pyship/logging.py,sha256=cIB7487tN8TsKeDHwwzjA30rHRW5Ci5GA5uPbaEXaY0,966
|
|
13
|
+
pyship/main.py,sha256=pALpy8ooWsKkUpWUtGWO567I9u1fbIFWdBU0dnWm2K4,866
|
|
14
|
+
pyship/nsis.py,sha256=xPW1f10CEKfDgvu4jd7e-tJqUfgm3AbAPC1E5rmLzO4,13066
|
|
15
|
+
pyship/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
pyship/pyship.ico,sha256=9oiMtmA7NiBmxM0NmMJQU5jXmnbEWCdqTaO3XvJ7ozY,103500
|
|
17
|
+
pyship/pyship.py,sha256=3kmYP_UNF4oexNlvLqTrVqE3s0-5rAgHsETaQp69LV0,5682
|
|
18
|
+
pyship/pyship_custom_print.py,sha256=FrVy3XCZg8sex4a-aKtghqz1BlfO2DGgcpsdviQcssw,628
|
|
19
|
+
pyship/pyship_exceptions.py,sha256=teUOh3FuRUU9hyv7L4owACQZPWNdwpYTEz6l-Ao35nw,959
|
|
20
|
+
pyship/pyship_get_icon.py,sha256=BQp_xYl8CXt2e7nDB0llSxr0hbP7to2fkQqPeu8WpIg,2050
|
|
21
|
+
pyship/pyship_path.py,sha256=RpU2n7pWXTr0501S7U8KxWCpBc8jOqKPuaX0X6IWPI8,262
|
|
22
|
+
pyship/pyship_subprocess.py,sha256=hH4MW_j2zLJLDmLDkkl2JWupXrvSemAeCPAEtPdSGyk,3212
|
|
23
|
+
pyship/uv_util.py,sha256=dcPtE0XYeQ3KFLMGA0qr_FhuTi1hprlWddFBa5hssNM,5662
|
|
24
|
+
pyship/launcher/__init__.py,sha256=OujoBeud36d3e8V6QzCWe5A-ilkaMeC5AmnoE72u62U,256
|
|
25
|
+
pyship/launcher/__main__.py,sha256=KhFSf1RU5Cj15y0c4FlzvIvDpWWi9NKYy0gzJ-w7KQE,115
|
|
26
|
+
pyship/launcher/hash.py,sha256=THEh8G-StEJeh5tH24RnAQpYB0Zv1nnBwqOoStN6_NY,1153
|
|
27
|
+
pyship/launcher/launcher_standalone.py,sha256=1HIXieq17IQFuldBAkmo7tjpsjADQVw7R8zXXQpEMXs,10654
|
|
28
|
+
pyship/launcher/metadata.py,sha256=PnOkYLQf6NYvPiZWhf0LAUjmFWzURKWriRQJSy3T-x4,1969
|
|
29
|
+
pyship/launcher/restart_monitor.py,sha256=jfzchLxGuC_xUNdPAr849FhAdozJ6BX9M-gqLd-gGIA,952
|
|
30
|
+
pyship-0.3.1.dist-info/licenses/LICENSE,sha256=wJpPh6rC_YhbTZ7ZIy1VjrqeLdJ8Pz0ppx2MUi7s8Dg,1088
|
|
31
|
+
pyship-0.3.1.dist-info/METADATA,sha256=9q0ykIxrTYTZevR0Q1pNkEvnqIErD8wSmXh6-Uk0Rwc,2703
|
|
32
|
+
pyship-0.3.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
33
|
+
pyship-0.3.1.dist-info/top_level.txt,sha256=mlozeft-UInVAceSajzNvtkn2vPa8El9j0RulsTVtlU,7
|
|
34
|
+
pyship-0.3.1.dist-info/RECORD,,
|
pyship/atomic_zip.py
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
from tempfile import mkdtemp
|
|
3
|
-
import zipfile
|
|
4
|
-
import os
|
|
5
|
-
|
|
6
|
-
from typeguard import typechecked
|
|
7
|
-
from balsa import get_logger
|
|
8
|
-
|
|
9
|
-
from pyshipupdate import mkdirs, rmdir
|
|
10
|
-
from pyship import __application_name__
|
|
11
|
-
|
|
12
|
-
log = get_logger(__application_name__)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@typechecked
|
|
16
|
-
def atomic_unzip(zip_file_path: Path, destination_dir: Path) -> bool:
|
|
17
|
-
"""
|
|
18
|
-
Unzip a zip file to a directory in a safe, atomic way. This avoids the issue where if an unzip fails in the middle it can potentially leave a directory with partially unzipped contents.
|
|
19
|
-
In other words, atomic_unzip is all or nothing.
|
|
20
|
-
:param zip_file_path: path to zip file to unzip
|
|
21
|
-
:param destination_dir: destination dir for unzipped contents
|
|
22
|
-
:return: True on success, False on failure
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
log.info(f'unzipping "{zip_file_path}" to "{destination_dir}"')
|
|
26
|
-
success_flag = False
|
|
27
|
-
temp_dir = mkdtemp(dir=destination_dir.parent)
|
|
28
|
-
try:
|
|
29
|
-
mkdirs(temp_dir, True)
|
|
30
|
-
|
|
31
|
-
zip_ref = zipfile.ZipFile(zip_file_path, "r")
|
|
32
|
-
zip_ref.extractall(temp_dir)
|
|
33
|
-
zip_ref.close()
|
|
34
|
-
|
|
35
|
-
rmdir(destination_dir) # in case dest already exists
|
|
36
|
-
os.rename(temp_dir, str(destination_dir))
|
|
37
|
-
success_flag = True
|
|
38
|
-
except IOError as e:
|
|
39
|
-
log.info(f"{zip_file_path=} {destination_dir=} {e}")
|
|
40
|
-
|
|
41
|
-
return success_flag
|