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/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
+ [![CI](https://github.com/jamesabel/pyship/actions/workflows/ci.yml/badge.svg)](https://github.com/jamesabel/pyship/actions/workflows/ci.yml)
44
+ [![codecov](https://codecov.io/gh/jamesabel/pyship/branch/main/graph/badge.svg)](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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.40.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
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