pycontrol-install 0.1.0a1__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.
- pycontrol_install/__init__.py +5 -0
- pycontrol_install/cli.py +334 -0
- pycontrol_install/shortcuts.py +116 -0
- pycontrol_install-0.1.0a1.dist-info/METADATA +122 -0
- pycontrol_install-0.1.0a1.dist-info/RECORD +8 -0
- pycontrol_install-0.1.0a1.dist-info/WHEEL +4 -0
- pycontrol_install-0.1.0a1.dist-info/entry_points.txt +2 -0
- pycontrol_install-0.1.0a1.dist-info/licenses/LICENSE.txt +8 -0
pycontrol_install/cli.py
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""Command-line installer for the global pyControl app and its workspaces.
|
|
2
|
+
|
|
3
|
+
``pycontrol-install`` installs ``pycontrol-gui`` once per machine with
|
|
4
|
+
``uv tool install``, scaffolds plain-data workspace folders, and remembers the
|
|
5
|
+
workspace so the globally installed ``pycontrol-gui`` command opens it on the
|
|
6
|
+
next launch.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import os
|
|
13
|
+
import shlex
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
import tomllib
|
|
18
|
+
from collections.abc import Sequence
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Protocol
|
|
22
|
+
|
|
23
|
+
from pycontrol_install.shortcuts import create_shortcut
|
|
24
|
+
|
|
25
|
+
GUI_PACKAGE = "pycontrol-gui"
|
|
26
|
+
CORE_PACKAGE = "pycontrol-core"
|
|
27
|
+
_INTEGRATION_EXTRAS = ("notion", "s3", "slack")
|
|
28
|
+
_PRERELEASE_CHOICES = ("allow", "disallow", "if-necessary", "explicit", "if-necessary-or-explicit")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class BootstrapError(RuntimeError):
|
|
32
|
+
"""Expected bootstrap failure suitable for a one-line CLI error."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CommandRunner(Protocol):
|
|
36
|
+
"""Small abstraction for testing command construction without invoking uv."""
|
|
37
|
+
|
|
38
|
+
def has_command(self, name: str) -> bool: ...
|
|
39
|
+
|
|
40
|
+
def run(self, cwd: Path, command: Sequence[str]) -> None: ...
|
|
41
|
+
|
|
42
|
+
def run_capture(self, cwd: Path, command: Sequence[str]) -> str: ...
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class BootstrapConfig:
|
|
47
|
+
"""Parsed bootstrap options."""
|
|
48
|
+
|
|
49
|
+
target: Path
|
|
50
|
+
launch: bool
|
|
51
|
+
force: bool
|
|
52
|
+
python: str
|
|
53
|
+
prerelease: str
|
|
54
|
+
index_url: str | None
|
|
55
|
+
extra_index_url: str | None
|
|
56
|
+
integration_extras: tuple[str, ...]
|
|
57
|
+
update_gui: bool
|
|
58
|
+
shortcut: bool | None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class SubprocessRunner:
|
|
62
|
+
"""Run commands on the local machine."""
|
|
63
|
+
|
|
64
|
+
def has_command(self, name: str) -> bool:
|
|
65
|
+
return shutil.which(name) is not None
|
|
66
|
+
|
|
67
|
+
def run(self, cwd: Path, command: Sequence[str]) -> None:
|
|
68
|
+
print(f"Running in {cwd}: {shlex.join(command)}")
|
|
69
|
+
try:
|
|
70
|
+
subprocess.run(command, cwd=cwd, check=True)
|
|
71
|
+
except FileNotFoundError as exc:
|
|
72
|
+
raise BootstrapError(f"command not found: {command[0]}") from exc
|
|
73
|
+
except subprocess.CalledProcessError as exc:
|
|
74
|
+
raise BootstrapError(f"command failed with exit code {exc.returncode}: {shlex.join(command)}") from exc
|
|
75
|
+
|
|
76
|
+
def run_capture(self, cwd: Path, command: Sequence[str]) -> str:
|
|
77
|
+
try:
|
|
78
|
+
completed = subprocess.run(command, cwd=cwd, check=True, capture_output=True, text=True)
|
|
79
|
+
except FileNotFoundError as exc:
|
|
80
|
+
raise BootstrapError(f"command not found: {command[0]}") from exc
|
|
81
|
+
except subprocess.CalledProcessError as exc:
|
|
82
|
+
raise BootstrapError(f"command failed with exit code {exc.returncode}: {shlex.join(command)}") from exc
|
|
83
|
+
return completed.stdout.strip()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
87
|
+
parser = argparse.ArgumentParser(
|
|
88
|
+
prog="pycontrol-install",
|
|
89
|
+
description="Install the pyControl GUI globally with uv and create a workspace data folder.",
|
|
90
|
+
)
|
|
91
|
+
parser.add_argument(
|
|
92
|
+
"path",
|
|
93
|
+
nargs="?",
|
|
94
|
+
default=".",
|
|
95
|
+
help="Workspace path to create or initialize (default: current directory).",
|
|
96
|
+
)
|
|
97
|
+
parser.add_argument(
|
|
98
|
+
"--launch",
|
|
99
|
+
action="store_true",
|
|
100
|
+
help="Launch the pyControl GUI after the workspace is ready.",
|
|
101
|
+
)
|
|
102
|
+
parser.add_argument(
|
|
103
|
+
"--force",
|
|
104
|
+
action="store_true",
|
|
105
|
+
help="Pass --force to workspace scaffolding so differing bundled files are overwritten.",
|
|
106
|
+
)
|
|
107
|
+
parser.add_argument(
|
|
108
|
+
"--update-gui",
|
|
109
|
+
action="store_true",
|
|
110
|
+
help="Reinstall the newest pyControl GUI (preserving installed integration extras) and exit.",
|
|
111
|
+
)
|
|
112
|
+
parser.add_argument(
|
|
113
|
+
"--shortcut",
|
|
114
|
+
action=argparse.BooleanOptionalAction,
|
|
115
|
+
default=None,
|
|
116
|
+
help="Create (or skip) an OS shortcut for the pyControl GUI without prompting.",
|
|
117
|
+
)
|
|
118
|
+
parser.add_argument(
|
|
119
|
+
"--python",
|
|
120
|
+
default="3.12",
|
|
121
|
+
help="Python version request for the global tool environment (default: 3.12).",
|
|
122
|
+
)
|
|
123
|
+
parser.add_argument(
|
|
124
|
+
"--prerelease",
|
|
125
|
+
choices=_PRERELEASE_CHOICES,
|
|
126
|
+
default="if-necessary-or-explicit",
|
|
127
|
+
help=(
|
|
128
|
+
"uv prerelease resolution strategy (default: if-necessary-or-explicit, which selects pyControl "
|
|
129
|
+
"alphas because no stable release exists yet while keeping third-party dependencies stable)."
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
parser.add_argument(
|
|
133
|
+
"--index-url",
|
|
134
|
+
default=None,
|
|
135
|
+
help="Package index URL for uv tool install, or PYCONTROL_INSTALL_INDEX_URL.",
|
|
136
|
+
)
|
|
137
|
+
parser.add_argument(
|
|
138
|
+
"--extra-index-url",
|
|
139
|
+
default=None,
|
|
140
|
+
help="Extra package index URL for uv tool install, or PYCONTROL_INSTALL_EXTRA_INDEX_URL.",
|
|
141
|
+
)
|
|
142
|
+
parser.add_argument(
|
|
143
|
+
"--with-slack",
|
|
144
|
+
action="store_true",
|
|
145
|
+
help="Install Slack notification dependencies into the global tool environment.",
|
|
146
|
+
)
|
|
147
|
+
parser.add_argument(
|
|
148
|
+
"--with-notion",
|
|
149
|
+
action="store_true",
|
|
150
|
+
help="Install Notion helper dependencies into the global tool environment.",
|
|
151
|
+
)
|
|
152
|
+
parser.add_argument(
|
|
153
|
+
"--with-s3",
|
|
154
|
+
action="store_true",
|
|
155
|
+
help="Install S3 export helper dependencies into the global tool environment.",
|
|
156
|
+
)
|
|
157
|
+
parser.add_argument(
|
|
158
|
+
"--with-integrations",
|
|
159
|
+
action="store_true",
|
|
160
|
+
help="Install all optional integration helper dependencies into the global tool environment.",
|
|
161
|
+
)
|
|
162
|
+
return parser
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def config_from_args(args: argparse.Namespace) -> BootstrapConfig:
|
|
166
|
+
selected = set(_INTEGRATION_EXTRAS if args.with_integrations else ())
|
|
167
|
+
if args.with_notion:
|
|
168
|
+
selected.add("notion")
|
|
169
|
+
if args.with_s3:
|
|
170
|
+
selected.add("s3")
|
|
171
|
+
if args.with_slack:
|
|
172
|
+
selected.add("slack")
|
|
173
|
+
|
|
174
|
+
return BootstrapConfig(
|
|
175
|
+
target=Path(args.path),
|
|
176
|
+
launch=bool(args.launch),
|
|
177
|
+
force=bool(args.force),
|
|
178
|
+
python=str(args.python),
|
|
179
|
+
prerelease=str(args.prerelease),
|
|
180
|
+
index_url=args.index_url or os.environ.get("PYCONTROL_INSTALL_INDEX_URL") or None,
|
|
181
|
+
extra_index_url=args.extra_index_url or os.environ.get("PYCONTROL_INSTALL_EXTRA_INDEX_URL") or None,
|
|
182
|
+
integration_extras=tuple(extra for extra in _INTEGRATION_EXTRAS if extra in selected),
|
|
183
|
+
update_gui=bool(args.update_gui),
|
|
184
|
+
shortcut=args.shortcut,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def bootstrap(config: BootstrapConfig, runner: CommandRunner | None = None) -> Path | None:
|
|
189
|
+
"""Install the GUI globally, then scaffold and remember the workspace.
|
|
190
|
+
|
|
191
|
+
Returns the workspace path, or ``None`` for ``--update-gui`` runs.
|
|
192
|
+
"""
|
|
193
|
+
runner = runner or SubprocessRunner()
|
|
194
|
+
if not runner.has_command("uv"):
|
|
195
|
+
raise BootstrapError(
|
|
196
|
+
"uv is required because pycontrol-install uses uv; install it from "
|
|
197
|
+
"https://docs.astral.sh/uv/getting-started/installation/"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
cwd = Path.cwd()
|
|
201
|
+
receipt = _receipt_path(runner, cwd)
|
|
202
|
+
|
|
203
|
+
if config.update_gui:
|
|
204
|
+
if receipt is None or not receipt.is_file():
|
|
205
|
+
raise BootstrapError(
|
|
206
|
+
f"{GUI_PACKAGE} is not installed as a uv tool yet; run pycontrol-install without --update-gui first."
|
|
207
|
+
)
|
|
208
|
+
extras = tuple(sorted({*_extras_from_receipt(receipt), *config.integration_extras}))
|
|
209
|
+
runner.run(cwd, _install_command(config, extras=extras, reinstall=True))
|
|
210
|
+
print(f"{GUI_PACKAGE} reinstalled at the newest version.")
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
reinstall = receipt is not None and receipt.is_file()
|
|
214
|
+
runner.run(cwd, _install_command(config, extras=config.integration_extras, reinstall=reinstall))
|
|
215
|
+
|
|
216
|
+
bin_dir = Path(runner.run_capture(cwd, ["uv", "tool", "dir", "--bin"]))
|
|
217
|
+
target = _prepare_target(config.target)
|
|
218
|
+
|
|
219
|
+
workspace_init = [str(bin_dir / "pycontrol"), "workspace", "init", str(target)]
|
|
220
|
+
if config.force:
|
|
221
|
+
workspace_init.append("--force")
|
|
222
|
+
runner.run(target, workspace_init)
|
|
223
|
+
|
|
224
|
+
runner.run(target, [str(bin_dir / GUI_PACKAGE), "--set-workspace", str(target)])
|
|
225
|
+
|
|
226
|
+
if _shortcut_wanted(config.shortcut):
|
|
227
|
+
_create_shortcut_nonfatal(bin_dir)
|
|
228
|
+
|
|
229
|
+
if config.launch:
|
|
230
|
+
runner.run(target, [str(bin_dir / GUI_PACKAGE), str(target)])
|
|
231
|
+
|
|
232
|
+
return target
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
236
|
+
parser = build_parser()
|
|
237
|
+
args = parser.parse_args(argv)
|
|
238
|
+
try:
|
|
239
|
+
workspace = bootstrap(config_from_args(args))
|
|
240
|
+
except BootstrapError as exc:
|
|
241
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
242
|
+
return 2
|
|
243
|
+
|
|
244
|
+
if workspace is not None:
|
|
245
|
+
print(f"Workspace ready: {workspace}")
|
|
246
|
+
if not args.launch:
|
|
247
|
+
print(f"Launch anytime with: {GUI_PACKAGE}")
|
|
248
|
+
print(f"If the {GUI_PACKAGE} command is not found in new terminals, run: uv tool update-shell")
|
|
249
|
+
return 0
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _install_command(config: BootstrapConfig, *, extras: tuple[str, ...], reinstall: bool) -> list[str]:
|
|
253
|
+
# --refresh: resolve against a revalidated index so a release published
|
|
254
|
+
# moments ago is picked up instead of a cached older version.
|
|
255
|
+
# --force: claim the pycontrol/pycontrol-gui executables even when a stale
|
|
256
|
+
# shim or an older tool install already provides them.
|
|
257
|
+
command = [
|
|
258
|
+
"uv",
|
|
259
|
+
"tool",
|
|
260
|
+
"install",
|
|
261
|
+
"--refresh",
|
|
262
|
+
"--force",
|
|
263
|
+
"--python",
|
|
264
|
+
config.python,
|
|
265
|
+
"--prerelease",
|
|
266
|
+
config.prerelease,
|
|
267
|
+
]
|
|
268
|
+
if config.index_url:
|
|
269
|
+
command.extend(["--index-url", config.index_url])
|
|
270
|
+
if config.extra_index_url:
|
|
271
|
+
command.extend(["--extra-index-url", config.extra_index_url])
|
|
272
|
+
if extras:
|
|
273
|
+
command.extend(["--with", f"{CORE_PACKAGE}[{','.join(sorted(extras))}]"])
|
|
274
|
+
command.extend(["--with-executables-from", CORE_PACKAGE])
|
|
275
|
+
if reinstall:
|
|
276
|
+
command.append("--reinstall")
|
|
277
|
+
command.append(GUI_PACKAGE)
|
|
278
|
+
return command
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _receipt_path(runner: CommandRunner, cwd: Path) -> Path | None:
|
|
282
|
+
try:
|
|
283
|
+
tool_dir = runner.run_capture(cwd, ["uv", "tool", "dir"])
|
|
284
|
+
except BootstrapError:
|
|
285
|
+
return None
|
|
286
|
+
if not tool_dir:
|
|
287
|
+
return None
|
|
288
|
+
return Path(tool_dir) / GUI_PACKAGE / "uv-receipt.toml"
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _extras_from_receipt(receipt: Path) -> tuple[str, ...]:
|
|
292
|
+
"""Recover ``pycontrol-core`` extras from the uv tool receipt so an update
|
|
293
|
+
reinstall keeps previously chosen integrations."""
|
|
294
|
+
try:
|
|
295
|
+
data = tomllib.loads(receipt.read_text(encoding="utf-8"))
|
|
296
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
297
|
+
return ()
|
|
298
|
+
requirements = data.get("tool", {}).get("requirements", [])
|
|
299
|
+
for requirement in requirements:
|
|
300
|
+
if isinstance(requirement, dict) and requirement.get("name") == CORE_PACKAGE:
|
|
301
|
+
extras = requirement.get("extras", [])
|
|
302
|
+
return tuple(sorted(str(extra) for extra in extras if str(extra) in _INTEGRATION_EXTRAS))
|
|
303
|
+
return ()
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _prepare_target(raw_target: Path) -> Path:
|
|
307
|
+
target = raw_target.expanduser().resolve()
|
|
308
|
+
if target.exists() and not target.is_dir():
|
|
309
|
+
raise BootstrapError(f"target exists and is not a directory: {target}")
|
|
310
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
311
|
+
return target
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _shortcut_wanted(shortcut: bool | None) -> bool:
|
|
315
|
+
if shortcut is not None:
|
|
316
|
+
return shortcut
|
|
317
|
+
if not sys.stdin.isatty():
|
|
318
|
+
return False
|
|
319
|
+
answer = input("Create an OS shortcut for the pyControl GUI? [Y/n] ").strip().lower()
|
|
320
|
+
return answer in ("", "y", "yes")
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _create_shortcut_nonfatal(bin_dir: Path) -> None:
|
|
324
|
+
try:
|
|
325
|
+
created = create_shortcut(bin_dir)
|
|
326
|
+
except Exception as exc: # noqa: BLE001 - shortcuts are a nicety, never fatal.
|
|
327
|
+
print(f"warning: could not create a shortcut: {exc}", file=sys.stderr)
|
|
328
|
+
return
|
|
329
|
+
if created is not None:
|
|
330
|
+
print(f"Shortcut created: {created}")
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
if __name__ == "__main__": # pragma: no cover
|
|
334
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Optional OS shortcuts pointing at the globally installed ``pycontrol-gui``.
|
|
2
|
+
|
|
3
|
+
All variants target the stable uv tool shim (``<uv tool bin dir>/pycontrol-gui``),
|
|
4
|
+
which survives upgrades, so shortcuts never need regeneration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import plistlib
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
APP_NAME = "pyControl GUI"
|
|
15
|
+
_BUNDLE_IDENTIFIER = "org.pycontrol.gui-launcher"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def create_shortcut(bin_dir: Path, home: Path | None = None, platform: str | None = None) -> Path | None:
|
|
19
|
+
"""Create a double-clickable shortcut for the current OS.
|
|
20
|
+
|
|
21
|
+
Returns the created shortcut path, or ``None`` when the platform has no
|
|
22
|
+
shortcut support. Raises on failure; callers treat that as non-fatal.
|
|
23
|
+
"""
|
|
24
|
+
home = home or Path.home()
|
|
25
|
+
platform = platform or sys.platform
|
|
26
|
+
if platform == "darwin":
|
|
27
|
+
return create_macos_app_bundle(bin_dir, home)
|
|
28
|
+
if platform == "win32":
|
|
29
|
+
return create_windows_start_menu_shortcut(bin_dir)
|
|
30
|
+
if platform.startswith("linux"):
|
|
31
|
+
return create_linux_desktop_entry(bin_dir, home)
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def create_macos_app_bundle(bin_dir: Path, home: Path) -> Path:
|
|
36
|
+
"""Minimal ~/Applications app bundle wrapping the pycontrol-gui shim."""
|
|
37
|
+
bundle = home / "Applications" / f"{APP_NAME}.app"
|
|
38
|
+
macos_dir = bundle / "Contents" / "MacOS"
|
|
39
|
+
macos_dir.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
|
|
41
|
+
info = {
|
|
42
|
+
"CFBundleName": APP_NAME,
|
|
43
|
+
"CFBundleDisplayName": APP_NAME,
|
|
44
|
+
"CFBundleIdentifier": _BUNDLE_IDENTIFIER,
|
|
45
|
+
"CFBundlePackageType": "APPL",
|
|
46
|
+
"CFBundleExecutable": "pycontrol-gui-launcher",
|
|
47
|
+
}
|
|
48
|
+
with (bundle / "Contents" / "Info.plist").open("wb") as stream:
|
|
49
|
+
plistlib.dump(info, stream)
|
|
50
|
+
|
|
51
|
+
launcher = macos_dir / "pycontrol-gui-launcher"
|
|
52
|
+
launcher.write_text(macos_launcher_script(bin_dir), encoding="utf-8", newline="\n")
|
|
53
|
+
launcher.chmod(launcher.stat().st_mode | 0o111)
|
|
54
|
+
return bundle
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def macos_launcher_script(bin_dir: Path) -> str:
|
|
58
|
+
return f'#!/bin/sh\nexec "{bin_dir / "pycontrol-gui"}" "$@"\n'
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def create_windows_start_menu_shortcut(bin_dir: Path) -> Path:
|
|
62
|
+
"""Start-menu .lnk via the WScript.Shell COM object (no extra dependencies)."""
|
|
63
|
+
completed = subprocess.run(
|
|
64
|
+
["powershell", "-NoProfile", "-Command", windows_shortcut_command(bin_dir)],
|
|
65
|
+
check=True,
|
|
66
|
+
capture_output=True,
|
|
67
|
+
text=True,
|
|
68
|
+
)
|
|
69
|
+
link_path = completed.stdout.strip().splitlines()[-1] if completed.stdout.strip() else ""
|
|
70
|
+
if not link_path:
|
|
71
|
+
raise RuntimeError("PowerShell did not report the created shortcut path")
|
|
72
|
+
return Path(link_path)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def windows_shortcut_command(bin_dir: Path) -> str:
|
|
76
|
+
target = bin_dir / "pycontrol-gui.exe"
|
|
77
|
+
return (
|
|
78
|
+
"$programs = [Environment]::GetFolderPath('Programs'); "
|
|
79
|
+
f"$link = Join-Path $programs '{APP_NAME}.lnk'; "
|
|
80
|
+
"$shell = New-Object -ComObject WScript.Shell; "
|
|
81
|
+
"$shortcut = $shell.CreateShortcut($link); "
|
|
82
|
+
f"$shortcut.TargetPath = '{target}'; "
|
|
83
|
+
"$shortcut.Save(); "
|
|
84
|
+
"Write-Output $link"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def create_linux_desktop_entry(bin_dir: Path, home: Path) -> Path:
|
|
89
|
+
applications = home / ".local" / "share" / "applications"
|
|
90
|
+
applications.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
entry = applications / "pycontrol-gui.desktop"
|
|
92
|
+
entry.write_text(linux_desktop_entry(bin_dir), encoding="utf-8", newline="\n")
|
|
93
|
+
return entry
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def linux_desktop_entry(bin_dir: Path) -> str:
|
|
97
|
+
return (
|
|
98
|
+
"[Desktop Entry]\n"
|
|
99
|
+
"Type=Application\n"
|
|
100
|
+
f"Name={APP_NAME}\n"
|
|
101
|
+
f'Exec="{bin_dir / "pycontrol-gui"}"\n'
|
|
102
|
+
"Terminal=false\n"
|
|
103
|
+
"Categories=Science;\n"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
__all__ = [
|
|
108
|
+
"APP_NAME",
|
|
109
|
+
"create_linux_desktop_entry",
|
|
110
|
+
"create_macos_app_bundle",
|
|
111
|
+
"create_shortcut",
|
|
112
|
+
"create_windows_start_menu_shortcut",
|
|
113
|
+
"linux_desktop_entry",
|
|
114
|
+
"macos_launcher_script",
|
|
115
|
+
"windows_shortcut_command",
|
|
116
|
+
]
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pycontrol-install
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: uvx installer for the global pyControl GUI and its workspaces.
|
|
5
|
+
Project-URL: Homepage, https://github.com/karpova-lab/pycontrol-install
|
|
6
|
+
Author: pyControl contributors
|
|
7
|
+
License: GPL-3.0-or-later
|
|
8
|
+
License-File: LICENSE.txt
|
|
9
|
+
Keywords: behaviour,neuroscience,pycontrol,uv
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# pycontrol-install
|
|
20
|
+
|
|
21
|
+
`pycontrol-install` is a small `uvx` installer for pyControl. It installs
|
|
22
|
+
`pycontrol-gui` (and the `pycontrol` CLI) **once per machine** with
|
|
23
|
+
`uv tool install`, scaffolds plain-data workspace folders, and remembers the
|
|
24
|
+
workspace so the global `pycontrol-gui` command opens it on the next launch.
|
|
25
|
+
|
|
26
|
+
Install `uv`, then install pyControl and create a workspace:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
uvx pycontrol-install my-workspace --launch
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This:
|
|
33
|
+
|
|
34
|
+
1. runs `uv tool install pycontrol-gui` into a self-contained, uv-managed
|
|
35
|
+
environment, putting `pycontrol-gui` and `pycontrol` on your PATH,
|
|
36
|
+
2. scaffolds the `my-workspace` data folder (no `pyproject.toml`, `.venv`, or
|
|
37
|
+
lockfile — workspaces are pure data),
|
|
38
|
+
3. remembers it as the GUI's current workspace,
|
|
39
|
+
4. offers to create an OS shortcut (macOS `~/Applications`, Windows Start
|
|
40
|
+
menu, Linux application menu), and
|
|
41
|
+
5. launches the GUI (`--launch`).
|
|
42
|
+
|
|
43
|
+
After setup, launch the GUI from anywhere with `pycontrol-gui`.
|
|
44
|
+
|
|
45
|
+
To initialize the current directory instead:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
uvx pycontrol-install . --launch
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This package intentionally uses `uv`. Users who prefer pip can still install
|
|
52
|
+
and run pyControl manually from a Python 3.11+ virtual environment; see the
|
|
53
|
+
`pycontrol-core` getting-started guide for the pip/venv workflow.
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pycontrol-install [path] [--launch] [--force] [--shortcut | --no-shortcut] [--update-gui]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`path` defaults to the current directory. Relative paths are resolved from the
|
|
62
|
+
directory where the command is run. Re-running `pycontrol-install` is safe: the
|
|
63
|
+
global install is refreshed (`--reinstall`) and workspace scaffolding is a safe
|
|
64
|
+
merge.
|
|
65
|
+
|
|
66
|
+
Examples:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pycontrol-install
|
|
70
|
+
pycontrol-install my-workspace --launch
|
|
71
|
+
pycontrol-install ~/pycontrol-workspaces/ws1 --with-slack
|
|
72
|
+
pycontrol-install my-workspace --no-shortcut
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Updating the GUI
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
uvx pycontrol-install --update-gui
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Reinstalls the newest `pycontrol-gui` against a freshly refreshed package
|
|
82
|
+
index, preserving integration extras recorded in the uv tool receipt.
|
|
83
|
+
(`uv tool upgrade pycontrol-gui` also works; `--update-gui` additionally
|
|
84
|
+
guarantees the index is revalidated and the pyControl resolution strategy is
|
|
85
|
+
applied.)
|
|
86
|
+
|
|
87
|
+
## Integration helpers
|
|
88
|
+
|
|
89
|
+
Optional integration dependencies are installed into the global tool
|
|
90
|
+
environment:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
pycontrol-install my-workspace --with-slack
|
|
94
|
+
pycontrol-install my-workspace --with-notion
|
|
95
|
+
pycontrol-install my-workspace --with-s3
|
|
96
|
+
pycontrol-install my-workspace --with-integrations
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
To add an integration to an existing install without touching any workspace,
|
|
100
|
+
combine it with `--update-gui`:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
uvx pycontrol-install --update-gui --with-slack
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Release smoke tests
|
|
107
|
+
|
|
108
|
+
Package indexes can be overridden:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
pycontrol-install test-workspace \
|
|
112
|
+
--index-url https://test.pypi.org/simple/ \
|
|
113
|
+
--extra-index-url https://pypi.org/simple/
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Development
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
uv sync --group dev
|
|
120
|
+
uv run pytest
|
|
121
|
+
uv run ruff check src tests
|
|
122
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
pycontrol_install/__init__.py,sha256=cAUQjIarvq-QPQrG7Zxt6AWRdXVXG2jKz34jpBZbAXg,86
|
|
2
|
+
pycontrol_install/cli.py,sha256=pPDPKXU86kv0R6-JOPX0i-1knrroxLhostjr1OB8QS0,11787
|
|
3
|
+
pycontrol_install/shortcuts.py,sha256=RqWUBtPbdXt33c-kZRH67U3mk-gx1PoVDN8GiC1B67A,3925
|
|
4
|
+
pycontrol_install-0.1.0a1.dist-info/METADATA,sha256=SfTF2leLzJaGa2ZGOTy3XapMtLJZF_8z5QoldgROVwk,3686
|
|
5
|
+
pycontrol_install-0.1.0a1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
6
|
+
pycontrol_install-0.1.0a1.dist-info/entry_points.txt,sha256=Ijc_DZaKwhox1CJLjnD30GnM57_Eu7mduHQ473GL0V0,65
|
|
7
|
+
pycontrol_install-0.1.0a1.dist-info/licenses/LICENSE.txt,sha256=eOoHjIIzecTuMO3xRRPSEAWfIBYJquFE5X7iS3NJYNk,1453
|
|
8
|
+
pycontrol_install-0.1.0a1.dist-info/RECORD,,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Copyright © 2026 Howard Hughes Medical Institute
|
|
2
|
+
|
|
3
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
|
4
|
+
|
|
5
|
+
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
6
|
+
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
7
|
+
Neither the name of HHMI nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
|
8
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|