sysetup 1.4.3__tar.gz → 1.4.4__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.
Files changed (50) hide show
  1. {sysetup-1.4.3/src/sysetup.egg-info → sysetup-1.4.4}/PKG-INFO +7 -9
  2. {sysetup-1.4.3 → sysetup-1.4.4}/README.md +1 -1
  3. {sysetup-1.4.3 → sysetup-1.4.4}/pyproject.toml +12 -23
  4. {sysetup-1.4.3 → sysetup-1.4.4}/src/sysetup/context/__init__.py +0 -1
  5. {sysetup-1.4.3 → sysetup-1.4.4}/src/sysetup/context/context.py +5 -5
  6. {sysetup-1.4.3 → sysetup-1.4.4}/src/sysetup/context/options.py +0 -3
  7. sysetup-1.4.4/src/sysetup/context/system.py +15 -0
  8. sysetup-1.4.4/src/sysetup/main/files.py +89 -0
  9. sysetup-1.4.4/src/sysetup/main/linux/installations.py +41 -0
  10. sysetup-1.4.4/src/sysetup/main/linux/packages.py +52 -0
  11. sysetup-1.4.4/src/sysetup/main/linux/setup.py +35 -0
  12. sysetup-1.4.4/src/sysetup/main/main.py +32 -0
  13. sysetup-1.4.4/src/sysetup/main/packages.py +37 -0
  14. {sysetup-1.4.3 → sysetup-1.4.4}/src/sysetup/models/path.py +1 -3
  15. sysetup-1.4.4/src/sysetup/utils/__init__.py +2 -0
  16. {sysetup-1.4.3 → sysetup-1.4.4}/src/sysetup/utils/bitwarden.py +12 -14
  17. sysetup-1.4.4/src/sysetup/utils/download.py +18 -0
  18. {sysetup-1.4.3 → sysetup-1.4.4/src/sysetup.egg-info}/PKG-INFO +7 -9
  19. {sysetup-1.4.3 → sysetup-1.4.4}/src/sysetup.egg-info/SOURCES.txt +7 -14
  20. sysetup-1.4.4/src/sysetup.egg-info/requires.txt +10 -0
  21. sysetup-1.4.3/bin/pw +0 -10
  22. sysetup-1.4.3/bin/pw-askpass +0 -4
  23. sysetup-1.4.3/src/sysetup/context/action.py +0 -8
  24. sysetup-1.4.3/src/sysetup/context/installations.py +0 -5
  25. sysetup-1.4.3/src/sysetup/main/files/assets.py +0 -47
  26. sysetup-1.4.3/src/sysetup/main/files/permissions.py +0 -33
  27. sysetup-1.4.3/src/sysetup/main/files/settings.py +0 -42
  28. sysetup-1.4.3/src/sysetup/main/files/setup.py +0 -8
  29. sysetup-1.4.3/src/sysetup/main/installations.py +0 -99
  30. sysetup-1.4.3/src/sysetup/main/main.py +0 -27
  31. sysetup-1.4.3/src/sysetup/main/packages.py +0 -87
  32. sysetup-1.4.3/src/sysetup/utils/__init__.py +0 -4
  33. sysetup-1.4.3/src/sysetup/utils/download.py +0 -36
  34. sysetup-1.4.3/src/sysetup.egg-info/requires.txt +0 -12
  35. sysetup-1.4.3/tests/test_background.py +0 -54
  36. sysetup-1.4.3/tests/test_cli_entry_point.py +0 -12
  37. sysetup-1.4.3/tests/test_main.py +0 -12
  38. {sysetup-1.4.3 → sysetup-1.4.4}/LICENSE +0 -0
  39. {sysetup-1.4.3 → sysetup-1.4.4}/setup.cfg +0 -0
  40. {sysetup-1.4.3 → sysetup-1.4.4}/src/sysetup/__init__.py +0 -0
  41. {sysetup-1.4.3 → sysetup-1.4.4}/src/sysetup/cli/__init__.py +0 -0
  42. {sysetup-1.4.3 → sysetup-1.4.4}/src/sysetup/cli/entry_point.py +0 -0
  43. {sysetup-1.4.3 → sysetup-1.4.4}/src/sysetup/context/secrets_.py +0 -0
  44. {sysetup-1.4.3 → sysetup-1.4.4}/src/sysetup/main/__init__.py +0 -0
  45. {sysetup-1.4.3/src/sysetup/main/files → sysetup-1.4.4/src/sysetup/main/linux}/__init__.py +0 -0
  46. {sysetup-1.4.3 → sysetup-1.4.4}/src/sysetup/models/__init__.py +0 -0
  47. {sysetup-1.4.3 → sysetup-1.4.4}/src/sysetup/py.typed +0 -0
  48. {sysetup-1.4.3 → sysetup-1.4.4}/src/sysetup.egg-info/dependency_links.txt +0 -0
  49. {sysetup-1.4.3 → sysetup-1.4.4}/src/sysetup.egg-info/entry_points.txt +0 -0
  50. {sysetup-1.4.3 → sysetup-1.4.4}/src/sysetup.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sysetup
3
- Version: 1.4.3
3
+ Version: 1.4.4
4
4
  Summary: Personal system setup
5
5
  Author-email: Quinten Roets <qdr2104@columbia.edu>
6
6
  License-Expression: MIT
@@ -8,24 +8,22 @@ Project-URL: Source Code, https://github.com/quintenroets/sysetup
8
8
  Requires-Python: >=3.10
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
- Requires-Dist: backupmaster<3,>=2.0.2
12
- Requires-Dist: dbus-next<1,>=0.2.3
13
- Requires-Dist: package-utils[context]<1,>=0.6.1
14
- Requires-Dist: powercli<1,>=0.3.1
11
+ Requires-Dist: backupmaster<3,>=2.0.5
12
+ Requires-Dist: package-utils[context]<1,>=0.8.3
13
+ Requires-Dist: powercli<1,>=0.3.7
15
14
  Requires-Dist: requests<3,>=2.32.3
16
- Requires-Dist: superpathlib<3,>=2.0.4
15
+ Requires-Dist: superpathlib<3,>=2.0.11
17
16
  Provides-Extra: dev
18
17
  Requires-Dist: dirhash<1,>=0.5.0; extra == "dev"
19
18
  Requires-Dist: types-requests<3,>=2.32.0.20250306; extra == "dev"
20
- Requires-Dist: package-dev-tools<1,>=0.7.1; extra == "dev"
21
- Requires-Dist: package-dev-utils<1,>=0.1.6; extra == "dev"
19
+ Requires-Dist: package-dev-tools<1,>=0.8.0; extra == "dev"
22
20
  Dynamic: license-file
23
21
 
24
22
  # Sysetup
25
23
  [![PyPI version](https://badge.fury.io/py/sysetup.svg)](https://badge.fury.io/py/sysetup)
26
24
  ![PyPI downloads](https://img.shields.io/pypi/dm/sysetup)
27
25
  ![Python version](https://img.shields.io/badge/python-3.10+-brightgreen)
28
- ![Operating system](https://img.shields.io/badge/os-linux-brightgreen)
26
+ ![Operating system](https://img.shields.io/badge/os-linux%20%7c%20macOS-brightgreen)
29
27
  ![Coverage](https://img.shields.io/badge/coverage-66%25-brightgreen)
30
28
  ## [Plasma](https://kde.org/plasma-desktop/) 6 required
31
29
 
@@ -2,7 +2,7 @@
2
2
  [![PyPI version](https://badge.fury.io/py/sysetup.svg)](https://badge.fury.io/py/sysetup)
3
3
  ![PyPI downloads](https://img.shields.io/pypi/dm/sysetup)
4
4
  ![Python version](https://img.shields.io/badge/python-3.10+-brightgreen)
5
- ![Operating system](https://img.shields.io/badge/os-linux-brightgreen)
5
+ ![Operating system](https://img.shields.io/badge/os-linux%20%7c%20macOS-brightgreen)
6
6
  ![Coverage](https://img.shields.io/badge/coverage-66%25-brightgreen)
7
7
  ## [Plasma](https://kde.org/plasma-desktop/) 6 required
8
8
 
@@ -1,26 +1,24 @@
1
1
  [project]
2
2
  name = "sysetup"
3
- version = "1.4.3"
3
+ version = "1.4.4"
4
4
  description = "Personal system setup"
5
5
  authors = [{name = "Quinten Roets", email = "qdr2104@columbia.edu"}]
6
6
  license = "MIT"
7
7
  readme = "README.md"
8
8
  requires-python = ">=3.10"
9
9
  dependencies = [
10
- "backupmaster >=2.0.2, <3",
11
- "dbus-next >=0.2.3, <1",
12
- "package-utils[context] >=0.6.1, <1",
13
- "powercli >=0.3.1, <1",
10
+ "backupmaster >=2.0.5, <3",
11
+ "package-utils[context] >=0.8.3, <1",
12
+ "powercli >=0.3.7, <1",
14
13
  "requests >=2.32.3, <3",
15
- "superpathlib >=2.0.4, <3",
14
+ "superpathlib >=2.0.11, <3",
16
15
  ]
17
16
 
18
17
  [project.optional-dependencies]
19
18
  dev = [
20
19
  "dirhash >=0.5.0, <1",
21
20
  "types-requests >=2.32.0.20250306, <3",
22
- "package-dev-tools >=0.7.1, <1",
23
- "package-dev-utils >=0.1.6, <1",
21
+ "package-dev-tools >=0.8.0, <1",
24
22
  ]
25
23
 
26
24
  [project.urls]
@@ -34,22 +32,10 @@ exportcrontab = "sysetup.main.files.assets:move_crontab"
34
32
  requires = ["setuptools"]
35
33
  build-backend = "setuptools.build_meta"
36
34
 
37
- [tool.coverage.run]
38
- command_line = "-m pytest tests"
39
-
40
- [tool.coverage.report]
41
- precision = 4
42
- fail_under = 60
43
-
44
35
  [tool.mypy]
45
36
  strict = true
46
37
  no_implicit_reexport = false
47
38
 
48
- [tool.pytest.ini_options]
49
- pythonpath = [
50
- "src", ".",
51
- ]
52
-
53
39
  [tool.ruff]
54
40
  fix = true
55
41
 
@@ -60,13 +46,16 @@ ignore = [
60
46
  "D", # docstrings
61
47
  "G004", # logging f-string
62
48
  "S101", # assert used
49
+ "D1", # missing docstrings
50
+ "D200", # one-line docstring
51
+ "D203", # conflicts with D211
52
+ "D205", # blank line between summary and description
53
+ "D212", # conflicts with D213
54
+ "D401", # imperative first line
63
55
  ]
64
56
 
65
57
  [tool.ruff.lint.per-file-ignores]
66
58
  "__init__.py" = ["F401"]
67
59
 
68
- [tool.setuptools]
69
- script-files = ["bin/pw", "bin/pw-askpass"]
70
-
71
60
  [tool.setuptools.package-data]
72
61
  sysetup = ["assets/scripts/update_wallpaper.js", "py.typed"]
@@ -1,2 +1 @@
1
- from .action import Action
2
1
  from .context import Context, context
@@ -1,11 +1,11 @@
1
- import os
2
1
  from functools import cached_property
2
+ from pathlib import Path
3
3
 
4
- from package_utils.context import Context as Context_
4
+ from package_utils.context.context import Context as Context_
5
5
 
6
- from .installations import is_installed
7
6
  from .options import Options
8
7
  from .secrets_ import Secrets
8
+ from .system import is_installed
9
9
 
10
10
 
11
11
  class Context(Context_[Options, None, Secrets]):
@@ -19,8 +19,8 @@ class Context(Context_[Options, None, Secrets]):
19
19
  return self.package_manager == "apt-get"
20
20
 
21
21
  @cached_property
22
- def is_running_in_test(self) -> bool:
23
- return "DISPLAY" not in os.environ
22
+ def is_running_in_container(self) -> bool:
23
+ return Path("/.dockerenv").exists()
24
24
 
25
25
 
26
26
  context = Context(Options, Secrets=Secrets)
@@ -3,11 +3,8 @@ from typing import Annotated
3
3
 
4
4
  import typer
5
5
 
6
- from .action import Action
7
-
8
6
 
9
7
  @dataclass
10
8
  class Options:
11
9
  bitwarden_password: Annotated[str, typer.Option()] = ""
12
10
  bitwarden_email: Annotated[str, typer.Option()] = "quinten.roets@gmail.com"
13
- action: Annotated[Action, typer.Argument(help="The part to setup")] = Action.all
@@ -0,0 +1,15 @@
1
+ import platform
2
+
3
+ import cli
4
+
5
+
6
+ def is_mac() -> bool:
7
+ return platform.system() == "Darwin"
8
+
9
+
10
+ def is_linux() -> bool:
11
+ return platform.system() == "Linux"
12
+
13
+
14
+ def is_installed(package: str) -> bool:
15
+ return cli.completes_successfully("which", package)
@@ -0,0 +1,89 @@
1
+ import stat
2
+
3
+ import cli
4
+
5
+ from sysetup.context import context
6
+ from sysetup.context.system import is_linux, is_mac
7
+ from sysetup.models import Path
8
+ from sysetup.utils import download_directory, ensure_downloaded
9
+
10
+ from .packages import install
11
+
12
+
13
+ def setup() -> None:
14
+ configure_git()
15
+ configure_ssh()
16
+ remove_clutter()
17
+ install_custom_certificate()
18
+
19
+
20
+ def configure_git() -> None:
21
+ directory = Path.HOME / ".config" / "git" / "hooks"
22
+ download_directory(directory)
23
+ for path in directory.iterdir():
24
+ cli.run("chmod +x", path)
25
+
26
+
27
+ def configure_ssh() -> None:
28
+ directory = Path.HOME / ".ssh"
29
+ download_directory(directory)
30
+ if is_linux():
31
+ remove_macos_ssh_options(directory / "config")
32
+ for path in directory.glob("id_*"):
33
+ if path.suffix != ".pub":
34
+ check_permissions(path)
35
+
36
+
37
+ def remove_macos_ssh_options(config: Path) -> None:
38
+ lines = (line for line in config.lines if "UseKeychain" not in line)
39
+ config.lines = list(lines)
40
+
41
+
42
+ def check_permissions(path: Path) -> None:
43
+ permissions = path.stat().st_mode
44
+ other_users_can_read = permissions & (stat.S_IRGRP | stat.S_IROTH)
45
+ if other_users_can_read:
46
+ path.chmod(0o600)
47
+
48
+
49
+ def remove_clutter() -> None:
50
+ names = (
51
+ "Desktop",
52
+ "Downloads",
53
+ "Music",
54
+ "Pictures",
55
+ "Public",
56
+ "Templates",
57
+ "Videos",
58
+ )
59
+ for name in names:
60
+ path = Path.HOME / name
61
+ path.rmtree(missing_ok=True)
62
+
63
+ root = Path("/") if is_linux() else Path("/") / "opt" / "homebrew"
64
+ nginx_path = root / "etc" / "nginx" / "sites-enabled" / "default"
65
+ if nginx_path.exists():
66
+ cli.run("rm", nginx_path, root=True)
67
+
68
+
69
+ def install_custom_certificate() -> None:
70
+ certificate_file = Path.assets / "certificates" / "certificate.crt"
71
+ ensure_downloaded(certificate_file)
72
+ if is_mac():
73
+ keychain = "/Library/Keychains/System.keychain"
74
+ command = (
75
+ f"security add-trusted-cert -d -r trustRoot "
76
+ f"-k {keychain} {certificate_file}"
77
+ )
78
+ else:
79
+ install(["libnss3-tools"])
80
+ certificate_directory = Path.HOME / ".pki" / "nssdb"
81
+ if not certificate_directory.exists():
82
+ certificate_directory.mkdir(parents=True)
83
+ cli.run(f"certutil -d sql:{certificate_directory} -N --empty-password")
84
+ command = (
85
+ f"certutil -d sql:{certificate_directory} "
86
+ f'-A -t "C,," -n "QCA" -i {certificate_file}'
87
+ )
88
+ if not context.is_running_in_container:
89
+ cli.run(command, root=is_mac())
@@ -0,0 +1,41 @@
1
+ import cli
2
+
3
+ from sysetup.context.system import is_installed
4
+ from sysetup.main.packages import install
5
+ from sysetup.models import Path
6
+
7
+
8
+ def setup() -> None:
9
+ install_repository("keyd", "rvaiya/keyd")
10
+ install_repository("ydotool", "ReimuNotMoe/ydotool")
11
+ enable_service("ssh")
12
+ install_language_support()
13
+
14
+
15
+ def install_language_support() -> None:
16
+ try:
17
+ packages = cli.capture_output("check-language-support")
18
+ except FileNotFoundError:
19
+ pass
20
+ else:
21
+ if packages:
22
+ install(packages)
23
+
24
+
25
+ def install_repository(name: str, repository: str) -> None:
26
+ if not is_installed(name):
27
+ if name == "ydotool":
28
+ cli.run("apt-get install scdoc", root=True)
29
+ url = f"https://github.com/{repository}"
30
+ with Path.tempdir() as directory:
31
+ cli.run("git clone", url, directory)
32
+ if (directory / "CMakeLists.txt").exists():
33
+ cli.run("apt-get install -y cmake", root=True)
34
+ cli.run("cmake .", cwd=directory)
35
+ cli.run_commands("make", "sudo make install", cwd=directory)
36
+ enable_service(name)
37
+
38
+
39
+ def enable_service(name: str) -> None:
40
+ if Path("/run/systemd/system").exists():
41
+ cli.run(f"systemctl enable --now {name}", root=True)
@@ -0,0 +1,52 @@
1
+ import cli
2
+
3
+ from sysetup.context import context
4
+ from sysetup.context.system import is_installed
5
+ from sysetup.main.packages import install_packages
6
+ from sysetup.models import Path
7
+ from sysetup.utils import bitwarden_client
8
+
9
+
10
+ def setup() -> None:
11
+ enable_sudo()
12
+ update_package_manager()
13
+ install_packages()
14
+ cleanup_after_install()
15
+
16
+
17
+ def enable_sudo() -> None:
18
+ password = bitwarden_client().fetch_secret("Laptop")
19
+ cli.run("sudo -S true", input=password)
20
+
21
+
22
+ def update_package_manager() -> None:
23
+ if context.apt_is_installed:
24
+ update_apt()
25
+ else:
26
+ cli.run("pacman -Syy", root=True)
27
+
28
+
29
+ def update_apt() -> None:
30
+ value = (
31
+ "ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true"
32
+ )
33
+ agree_eula_command = f'echo "{value}" | sudo debconf-set-selections'
34
+ commands = "sudo apt-get update", agree_eula_command
35
+ cli.run_commands_in_shell(*commands)
36
+ if not Path("/snap").exists():
37
+ cli.run("ln -s /var/lib/snapd/snap /snap", root=True)
38
+
39
+
40
+ def cleanup_after_install() -> None:
41
+ if context.apt_is_installed:
42
+ cli.run("sudo apt-get autoremove -y")
43
+ cli.run("tlp start", root=True)
44
+ if is_installed("qdbus"):
45
+ commands = "rm /usr/bin/qdbus", "ln -s /usr/lib/qt6/bin/qdbus /usr/bin/qdbus"
46
+ cli.run_commands(*commands, root=True)
47
+ delete = "apt purge -y" if context.apt_is_installed else "pacman -R --noconfirm"
48
+ commands = (
49
+ "auto-cpufreq --install",
50
+ f"{delete} firefox",
51
+ )
52
+ cli.run_commands(*commands, check=False, root=True)
@@ -0,0 +1,35 @@
1
+ import cli
2
+
3
+ from sysetup.context import context
4
+ from sysetup.context.system import is_installed
5
+ from sysetup.models import Path
6
+ from sysetup.utils import ensure_downloaded
7
+
8
+ from . import installations, packages
9
+
10
+
11
+ def setup() -> None:
12
+ set_background()
13
+ packages.setup()
14
+ install_crontab()
15
+ installations.setup()
16
+
17
+
18
+ def set_background() -> None: # pragma: nocover
19
+ path = (
20
+ Path.HOME / ".local" / "share" / "wallpapers" / "Qwallpapers" / "background.jpg"
21
+ )
22
+ ensure_downloaded(path)
23
+ script = Path.update_wallpaper_script.text
24
+ script = script.replace("__wallpaper_uri__", path.as_uri())
25
+ command = (
26
+ "qdbus org.kde.plasmashell /PlasmaShell org.kde.PlasmaShell.evaluateScript"
27
+ )
28
+ if not context.is_running_in_container and is_installed("qdbus"):
29
+ cli.run(command, script)
30
+
31
+
32
+ def install_crontab() -> None:
33
+ path = Path.assets / "crontab" / "crontab"
34
+ ensure_downloaded(path)
35
+ cli.run("crontab -", input=path.text)
@@ -0,0 +1,32 @@
1
+ import cli
2
+
3
+ from sysetup.context import context
4
+ from sysetup.context.system import is_linux, is_mac
5
+ from sysetup.models import Path
6
+ from sysetup.utils import bitwarden_client
7
+
8
+ from . import files, linux, packages
9
+
10
+
11
+ def main() -> None:
12
+ """
13
+ Personal system setup.
14
+ """
15
+ files.setup()
16
+ if is_linux():
17
+ linux.setup()
18
+ elif is_mac():
19
+ packages.install_packages()
20
+ install_personal_git_repositories()
21
+ if not context.is_running_in_container:
22
+ flags = ("--include-browser",) if is_linux() else ()
23
+ cli.run("backup pull --no-confirm-push", *flags)
24
+
25
+
26
+ def install_personal_git_repositories() -> None:
27
+ github_token = bitwarden_client().fetch_secret("GitHub Token")
28
+ base_url = f"https://{github_token}@github.com/quintenroets"
29
+ if not Path.extensions.exists():
30
+ command = f"git clone {base_url}/extensions.git"
31
+ cli.run(command, Path.extensions)
32
+ cli.run(f"uv pip install git+{base_url}/system.git")
@@ -0,0 +1,37 @@
1
+ import shlex
2
+ from collections.abc import Iterable
3
+
4
+ import cli
5
+
6
+ from sysetup.context import context
7
+ from sysetup.context.system import is_linux
8
+ from sysetup.models import Path
9
+ from sysetup.utils import download_directory
10
+
11
+
12
+ def install_packages() -> None:
13
+ download_directory(Path.packages)
14
+ installations = (
15
+ {"packages": None, "snap": "snap install"}
16
+ if is_linux()
17
+ else {"brew": "brew install"}
18
+ )
19
+ for name, command in installations.items():
20
+ path = (Path.packages / name).with_suffix(".yaml")
21
+ packages: list[str] = path.yaml
22
+ install(packages, install_command=command)
23
+ if is_linux() and not context.apt_is_installed:
24
+ commands = "sudo pacman -S --noconfirm base-devel", "uv pip install wheel"
25
+ cli.run_commands(*commands)
26
+
27
+
28
+ def install(packages: Iterable[str], install_command: str | None = None) -> None:
29
+ if install_command is None:
30
+ install_command = (
31
+ "apt-get install -y"
32
+ if context.apt_is_installed
33
+ else "pacman -S --noconfirm"
34
+ )
35
+ for package in packages:
36
+ args = shlex.split(package)
37
+ cli.run(install_command, *args, root=is_linux(), check=False)
@@ -1,11 +1,9 @@
1
- from typing import TypeVar, cast
1
+ from typing import cast
2
2
 
3
3
  import superpathlib
4
4
  from simple_classproperty import classproperty
5
5
  from typing_extensions import Self
6
6
 
7
- T = TypeVar("T", bound="Path")
8
-
9
7
 
10
8
  class Path(superpathlib.Path):
11
9
  @classmethod
@@ -0,0 +1,2 @@
1
+ from .bitwarden import bitwarden_client
2
+ from .download import download_directory, ensure_downloaded
@@ -1,8 +1,9 @@
1
1
  import io
2
2
  import json
3
+ import sys
3
4
  import zipfile
4
5
  from dataclasses import dataclass
5
- from functools import cached_property
6
+ from functools import cache, cached_property
6
7
  from typing import cast
7
8
 
8
9
  import cli
@@ -17,12 +18,12 @@ from sysetup.models import Path
17
18
  class Client:
18
19
  password: str
19
20
  email: str
20
- download_url: str = "https://bitwarden.com/download/?app=cli&platform=linux"
21
21
 
22
22
  def fetch_secret(self, name: str) -> str:
23
23
  command = "./bw list items --session", self.session_token, "--search", name
24
24
  response = cli.capture_output(*command)
25
- secret = json.loads(response)[0]["notes"]
25
+ item = json.loads(response)[0]
26
+ secret = item.get("notes") or item["login"]["password"]
26
27
  return cast("str", secret)
27
28
 
28
29
  @cached_property
@@ -40,20 +41,17 @@ class Client:
40
41
  return cli.capture_output(*command)
41
42
 
42
43
  def download_cli(self) -> None:
43
- response = requests.get(self.download_url, timeout=10).content
44
+ platform = "macos" if sys.platform == "darwin" else "linux"
45
+ download_url = f"https://bitwarden.com/download/?app=cli&platform={platform}"
46
+ response = requests.get(download_url, timeout=10).content
44
47
  zip_bytes = io.BytesIO(response)
45
48
  with zipfile.ZipFile(zip_bytes, "r") as zip_file:
46
49
  zip_file.extractall()
47
50
  Path("bw").chmod(0o755)
48
51
 
49
52
 
50
- @dataclass
51
- class Bitwarden:
52
- @cached_property
53
- def client(self) -> Client:
54
- password = context.options.bitwarden_password
55
- password = password or Prompt.ask("Bitwarden password", password=True)
56
- return Client(password=password, email=context.options.bitwarden_email)
57
-
58
-
59
- bitwarden = Bitwarden()
53
+ @cache
54
+ def bitwarden_client() -> Client:
55
+ password = context.options.bitwarden_password
56
+ password = password or Prompt.ask("Bitwarden password", password=True)
57
+ return Client(password=password, email=context.options.bitwarden_email)
@@ -0,0 +1,18 @@
1
+ import os
2
+
3
+ from backup.syncer.builder import create_syncer
4
+
5
+ from sysetup.models import Path
6
+
7
+ from .bitwarden import bitwarden_client
8
+
9
+
10
+ def ensure_downloaded(path: Path) -> None:
11
+ if not path.exists():
12
+ download_directory(path.parent)
13
+
14
+
15
+ def download_directory(directory: Path) -> None:
16
+ if "RCLONE" not in os.environ:
17
+ os.environ["RCLONE"] = bitwarden_client().fetch_secret("Rclone")
18
+ create_syncer(directory=directory).capture_pull()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sysetup
3
- Version: 1.4.3
3
+ Version: 1.4.4
4
4
  Summary: Personal system setup
5
5
  Author-email: Quinten Roets <qdr2104@columbia.edu>
6
6
  License-Expression: MIT
@@ -8,24 +8,22 @@ Project-URL: Source Code, https://github.com/quintenroets/sysetup
8
8
  Requires-Python: >=3.10
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
- Requires-Dist: backupmaster<3,>=2.0.2
12
- Requires-Dist: dbus-next<1,>=0.2.3
13
- Requires-Dist: package-utils[context]<1,>=0.6.1
14
- Requires-Dist: powercli<1,>=0.3.1
11
+ Requires-Dist: backupmaster<3,>=2.0.5
12
+ Requires-Dist: package-utils[context]<1,>=0.8.3
13
+ Requires-Dist: powercli<1,>=0.3.7
15
14
  Requires-Dist: requests<3,>=2.32.3
16
- Requires-Dist: superpathlib<3,>=2.0.4
15
+ Requires-Dist: superpathlib<3,>=2.0.11
17
16
  Provides-Extra: dev
18
17
  Requires-Dist: dirhash<1,>=0.5.0; extra == "dev"
19
18
  Requires-Dist: types-requests<3,>=2.32.0.20250306; extra == "dev"
20
- Requires-Dist: package-dev-tools<1,>=0.7.1; extra == "dev"
21
- Requires-Dist: package-dev-utils<1,>=0.1.6; extra == "dev"
19
+ Requires-Dist: package-dev-tools<1,>=0.8.0; extra == "dev"
22
20
  Dynamic: license-file
23
21
 
24
22
  # Sysetup
25
23
  [![PyPI version](https://badge.fury.io/py/sysetup.svg)](https://badge.fury.io/py/sysetup)
26
24
  ![PyPI downloads](https://img.shields.io/pypi/dm/sysetup)
27
25
  ![Python version](https://img.shields.io/badge/python-3.10+-brightgreen)
28
- ![Operating system](https://img.shields.io/badge/os-linux-brightgreen)
26
+ ![Operating system](https://img.shields.io/badge/os-linux%20%7c%20macOS-brightgreen)
29
27
  ![Coverage](https://img.shields.io/badge/coverage-66%25-brightgreen)
30
28
  ## [Plasma](https://kde.org/plasma-desktop/) 6 required
31
29
 
@@ -1,8 +1,6 @@
1
1
  LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
- bin/pw
5
- bin/pw-askpass
6
4
  src/sysetup/__init__.py
7
5
  src/sysetup/py.typed
8
6
  src/sysetup.egg-info/PKG-INFO
@@ -14,25 +12,20 @@ src/sysetup.egg-info/top_level.txt
14
12
  src/sysetup/cli/__init__.py
15
13
  src/sysetup/cli/entry_point.py
16
14
  src/sysetup/context/__init__.py
17
- src/sysetup/context/action.py
18
15
  src/sysetup/context/context.py
19
- src/sysetup/context/installations.py
20
16
  src/sysetup/context/options.py
21
17
  src/sysetup/context/secrets_.py
18
+ src/sysetup/context/system.py
22
19
  src/sysetup/main/__init__.py
23
- src/sysetup/main/installations.py
20
+ src/sysetup/main/files.py
24
21
  src/sysetup/main/main.py
25
22
  src/sysetup/main/packages.py
26
- src/sysetup/main/files/__init__.py
27
- src/sysetup/main/files/assets.py
28
- src/sysetup/main/files/permissions.py
29
- src/sysetup/main/files/settings.py
30
- src/sysetup/main/files/setup.py
23
+ src/sysetup/main/linux/__init__.py
24
+ src/sysetup/main/linux/installations.py
25
+ src/sysetup/main/linux/packages.py
26
+ src/sysetup/main/linux/setup.py
31
27
  src/sysetup/models/__init__.py
32
28
  src/sysetup/models/path.py
33
29
  src/sysetup/utils/__init__.py
34
30
  src/sysetup/utils/bitwarden.py
35
- src/sysetup/utils/download.py
36
- tests/test_background.py
37
- tests/test_cli_entry_point.py
38
- tests/test_main.py
31
+ src/sysetup/utils/download.py
@@ -0,0 +1,10 @@
1
+ backupmaster<3,>=2.0.5
2
+ package-utils[context]<1,>=0.8.3
3
+ powercli<1,>=0.3.7
4
+ requests<3,>=2.32.3
5
+ superpathlib<3,>=2.0.11
6
+
7
+ [dev]
8
+ dirhash<1,>=0.5.0
9
+ types-requests<3,>=2.32.0.20250306
10
+ package-dev-tools<1,>=0.8.0
sysetup-1.4.3/bin/pw DELETED
@@ -1,10 +0,0 @@
1
- #! /bin/bash
2
-
3
- password_name=${1:-Login}
4
-
5
- if command -v ksshaskpass >/dev/null 2>&1; then
6
- ksshaskpass -- "Enter passphrase for $password_name: "
7
- else
8
- security find-generic-password -s "$password_name" -a "autopass" -w
9
- # to add: security add-generic-password -s "$password_name" -a "autopass" -U -w
10
- fi
@@ -1,4 +0,0 @@
1
- #! /bin/bash
2
- # separate script because input message needs to be ignored
3
-
4
- pw
@@ -1,8 +0,0 @@
1
- from enum import Enum
2
-
3
-
4
- class Action(str, Enum):
5
- all = "all"
6
- files = "files"
7
- packages = "packages"
8
- install = "install"
@@ -1,5 +0,0 @@
1
- import cli
2
-
3
-
4
- def is_installed(package: str) -> bool:
5
- return cli.completes_successfully("which", package)
@@ -1,47 +0,0 @@
1
- import cli
2
- from backup.syncer import Syncer
3
- from backup.syncer.sync_configs import SyncConfigs
4
-
5
- from sysetup.models import Path
6
- from sysetup.utils import download_directory
7
-
8
-
9
- def setup() -> None:
10
- directories = (
11
- Path.assets,
12
- Path.HOME / ".local" / "share" / "kwalletd",
13
- Path.assets.parent / "backup",
14
- )
15
- for directory in directories:
16
- download_directory(directory)
17
- move_crontab()
18
- move_setup_files()
19
-
20
-
21
- def move_crontab() -> None:
22
- src = Path.assets / "crontab" / "crontab"
23
- cli.run("crontab -", input=src.text)
24
-
25
-
26
- def move_setup_files() -> None:
27
- setup_files_root = Path.assets / "files"
28
- setup_files = []
29
- archived_setup_files = []
30
- for path in setup_files_root.rglob("*"):
31
- if path.is_file():
32
- if path.archive_format is None:
33
- setup_files.append(path)
34
- else:
35
- archived_setup_files.append(path)
36
-
37
- config = SyncConfigs.cache.with_paths(setup_files)
38
- if setup_files:
39
- Syncer(config).capture_pull()
40
- for path in archived_setup_files:
41
- dest = (config.source / path.relative_to(setup_files_root)).parent
42
- if dest.is_root and not dest.exists():
43
- cli.run("mkdir -p", dest, root=True)
44
- else:
45
- dest.create_parent()
46
- with cli.status(f"Unpacking {path.relative_to(setup_files_root)}"):
47
- cli.capture_output("unzip -o", path, "-d", dest, root=dest.is_root)
@@ -1,33 +0,0 @@
1
- import stat
2
-
3
- import cli
4
-
5
- from sysetup.models import Path
6
- from sysetup.utils import download_directory
7
-
8
-
9
- def setup() -> None:
10
- set_git_permissions()
11
- set_ssh_permissions()
12
-
13
-
14
- def set_git_permissions() -> None:
15
- git_hooks_folder = Path.HOME / ".config" / "git" / "hooks"
16
- download_directory(git_hooks_folder)
17
- for path in git_hooks_folder.iterdir():
18
- cli.run("chmod +x", path)
19
-
20
-
21
- def set_ssh_permissions() -> None:
22
- directory = Path.HOME / ".ssh"
23
- download_directory(directory)
24
- for path in directory.glob("id_*"):
25
- if path.suffix != ".pub":
26
- check_permissions(path)
27
-
28
-
29
- def check_permissions(path: Path) -> None:
30
- permissions = path.stat().st_mode
31
- other_users_can_read = permissions & (stat.S_IRGRP | stat.S_IROTH)
32
- if other_users_can_read:
33
- path.chmod(33152)
@@ -1,42 +0,0 @@
1
- import cli
2
-
3
- from sysetup.context import context
4
- from sysetup.models import Path
5
- from sysetup.utils import download_directory, is_installed
6
-
7
-
8
- def remove_clutter() -> None:
9
- names = (
10
- "Desktop",
11
- "Downloads",
12
- "Music",
13
- "Pictures",
14
- "Public",
15
- "Templates",
16
- "Videos",
17
- )
18
- for name in names:
19
- path = Path.HOME / name
20
- path.rmtree(missing_ok=True)
21
-
22
- nginx_path = Path("/") / "etc" / "nginx" / "sites-enabled" / "default"
23
- if nginx_path.exists():
24
- cli.run("rm", nginx_path, root=True)
25
-
26
-
27
- def set_background() -> None: # pragma: nocover
28
- wallpaper_path = (
29
- Path.HOME / ".local" / "share" / "wallpapers" / "Qwallpapers" / "background.jpg"
30
- )
31
- download_directory(wallpaper_path.parent)
32
- script = Path.update_wallpaper_script.text
33
- script = script.replace("__wallpaper_uri__", wallpaper_path.as_uri())
34
- run_kde_script(script)
35
-
36
-
37
- def run_kde_script(script: str) -> None: # pragma: nocover
38
- command = (
39
- "qdbus org.kde.plasmashell /PlasmaShell org.kde.PlasmaShell.evaluateScript"
40
- )
41
- if not context.is_running_in_test and is_installed("qdbus"):
42
- cli.run(command, script)
@@ -1,8 +0,0 @@
1
- from sysetup.main.files import assets, permissions, settings
2
-
3
-
4
- def setup() -> None:
5
- permissions.setup()
6
- assets.setup()
7
- settings.set_background()
8
- settings.remove_clutter()
@@ -1,99 +0,0 @@
1
- import cli
2
-
3
- from sysetup.context import context
4
- from sysetup.models import Path
5
- from sysetup.utils import bitwarden, is_installed
6
-
7
- from .packages import install
8
-
9
-
10
- def setup() -> None:
11
- install_chromium()
12
- install_keyd()
13
- install_ydotool()
14
- enable_service("ssh")
15
- install_language_support()
16
- install_personal_git_repositories()
17
-
18
-
19
- def install_personal_git_repositories() -> None:
20
- github_token = bitwarden.client.fetch_secret("GitHub")
21
- base_url = f"https://{github_token}@github.com/quintenroets"
22
- if not Path.extensions.exists():
23
- command = f"git clone {base_url}/extensions.git"
24
- cli.run(command, Path.extensions)
25
- cli.run(f"uv pip install git+{base_url}/system.git")
26
-
27
-
28
- def install_language_support() -> None:
29
- try:
30
- packages = cli.capture_output("check-language-support")
31
- except FileNotFoundError:
32
- pass
33
- else:
34
- if packages:
35
- install(packages)
36
-
37
-
38
- def install_chromium() -> None:
39
- if not is_installed("chromium-browser"):
40
- _install_chromium()
41
-
42
-
43
- def _install_chromium() -> None:
44
- release_name = cli.capture_output_lines("lsb_release -sc")[-1]
45
- repo_url = f"https://freeshell.de/phd/chromium/{release_name}"
46
- key = "869689FE09306074"
47
- keyring = "/usr/share/keyrings/phd-chromium.gpg"
48
- sources_file = "/etc/apt/sources.list.d/phd-chromium.list"
49
- commands = (
50
- f"gpg --keyserver keyserver.ubuntu.com --recv-keys {key}",
51
- f"gpg --export {key} | sudo gpg --dearmor -o {keyring}",
52
- f'echo "deb [signed-by={keyring}] {repo_url} /" | sudo tee {sources_file}',
53
- "apt-get update",
54
- "apt-get install -y chromium",
55
- )
56
- check = not context.is_running_in_test
57
- cli.run_commands(*commands, shell=True, root=True, check=check) # noqa: S604
58
- install_custom_certificate()
59
-
60
-
61
- def install_custom_certificate() -> None:
62
- install(["libnss3-tools"])
63
- certificate_directory = Path.HOME / ".pki" / "nssdb"
64
- certificate_file = Path.assets / "certificates" / "certificate.crt"
65
- command = (
66
- f"certutil -d sql:{certificate_directory} "
67
- f'-A -t "C,," -n "QCA" -i {certificate_file}'
68
- )
69
- if not context.is_running_in_test:
70
- cli.run(command)
71
-
72
-
73
- def install_keyd() -> None:
74
- install_repository("keyd", "rvaiya/keyd")
75
- enable_service("keyd")
76
-
77
-
78
- def enable_service(name: str) -> None:
79
- if not context.is_running_in_test:
80
- command = f"systemctl enable --now {name}"
81
- cli.run(command, root=True)
82
-
83
-
84
- def install_repository(name: str, repository: str) -> None:
85
- if not is_installed(name):
86
- url = f"https://github.com/{repository}"
87
- with Path.tempdir() as directory:
88
- cli.run("git clone", url, directory)
89
- if (directory / "CMakeLists.txt").exists():
90
- cli.run("apt-get install -y cmake", root=True)
91
- cli.run("cmake .", cwd=directory)
92
- cli.run_commands("make", "sudo make install", cwd=directory)
93
-
94
-
95
- def install_ydotool() -> None:
96
- if not is_installed("ydotool"):
97
- cli.run("apt-get install scdoc", root=True)
98
- install_repository("ydotool", "ReimuNotMoe/ydotool")
99
- enable_service("ydotoold")
@@ -1,27 +0,0 @@
1
- import cli
2
-
3
- from sysetup.context import Action, context
4
-
5
- from . import files, installations, packages
6
-
7
-
8
- def main() -> None:
9
- """
10
- Personal system setup.
11
- """
12
- action_mapper = {
13
- Action.all.value: setup,
14
- Action.packages.value: packages.setup,
15
- Action.files.value: files.setup,
16
- Action.install.value: installations.setup,
17
- }
18
- action = action_mapper[context.options.action.value]
19
- action()
20
-
21
-
22
- def setup() -> None:
23
- packages.setup()
24
- files.setup()
25
- installations.setup()
26
- if not context.is_running_in_test:
27
- cli.run("backup pull --include-browser --no-confirm-push")
@@ -1,87 +0,0 @@
1
- import platform
2
- import shlex
3
- import warnings
4
- from collections.abc import Iterable
5
-
6
- import cli
7
-
8
- from sysetup.context import context
9
- from sysetup.models import Path
10
- from sysetup.utils import bitwarden, download_directory, is_installed
11
-
12
-
13
- def setup() -> None:
14
- enable_sudo()
15
- update_package_manager()
16
- install_packages()
17
- cleanup_after_install()
18
-
19
-
20
- def enable_sudo() -> None:
21
- password = bitwarden.client.fetch_secret("Laptop")
22
- cli.run("sudo -S true", input=password) # activate sudo without askpass
23
-
24
-
25
- def install_packages() -> None:
26
- download_directory(Path.packages)
27
- installations = {"packages": None, "snap": "snap install"}
28
- for name, command in installations.items():
29
- path = (Path.packages / name).with_suffix(".yaml")
30
- packages: list[str] = path.yaml
31
- install(packages, install_command=command)
32
-
33
- if not context.apt_is_installed:
34
- commands = "sudo pacman -S --noconfirm base-devel", "uv pip install wheel"
35
- cli.run_commands(*commands)
36
-
37
-
38
- def cleanup_after_install() -> None:
39
- if context.apt_is_installed:
40
- cli.run("sudo apt-get autoremove -y")
41
- cli.run("tlp start", root=True)
42
- if is_installed("qdbus"):
43
- commands = "rm /usr/bin/qdbus", "ln -s /usr/lib/qt6/bin/qdbus /usr/bin/qdbus"
44
- cli.run_commands(*commands, root=True)
45
- delete = "apt purge -y" if context.apt_is_installed else "pacman -R --noconfirm"
46
- commands = (
47
- "auto-cpufreq --install", # Fails on VM
48
- f"{delete} firefox", # fails if firefox not installed
49
- )
50
- cli.run_commands(*commands, check=False, root=True)
51
-
52
-
53
- def update_package_manager() -> None:
54
- if context.apt_is_installed:
55
- update_apt()
56
- else:
57
- cli.run("pacman -Syy", root=True)
58
-
59
-
60
- def update_apt() -> None:
61
- value = (
62
- "ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true"
63
- )
64
- agree_eula_command = f'echo "{value}" | sudo debconf-set-selections'
65
- commands = "sudo apt-get update", agree_eula_command
66
- cli.run_commands_in_shell(*commands)
67
- if not Path("/snap").exists():
68
- cli.run("ln -s /var/lib/snapd/snap /snap", root=True)
69
-
70
-
71
- def install(packages: Iterable[str], install_command: str | None = None) -> None:
72
- if install_command is None:
73
- install_command = (
74
- "apt-get install -y"
75
- if context.apt_is_installed
76
- else "pacman -S --noconfirm"
77
- )
78
-
79
- is_linux = platform.system() == "Linux"
80
- if not is_linux:
81
- message = "Required packages can only be installed on Linux"
82
- warnings.warn(message, stacklevel=2)
83
- return
84
-
85
- for package in packages:
86
- args = shlex.split(package)
87
- cli.run(install_command, *args, root=True, check=False)
@@ -1,4 +0,0 @@
1
- from sysetup.context.installations import is_installed
2
-
3
- from .bitwarden import bitwarden
4
- from .download import download_directory, download_file
@@ -1,36 +0,0 @@
1
- import os
2
-
3
- import cli
4
- from backup.context import context as backup_context
5
- from backup.syncer import Syncer
6
- from backup.syncer.sync_configs import select_sync_config
7
-
8
- from sysetup.models import Path
9
-
10
- from .bitwarden import bitwarden
11
-
12
-
13
- def download_directory(path: Path) -> None:
14
- download_item(directory=path)
15
-
16
-
17
- def download_file(path: Path) -> None:
18
- download_item(path)
19
-
20
-
21
- def download_item(path: Path | None = None, directory: Path | None = None) -> None:
22
- check_authenticated()
23
- location = path or directory
24
- assert location is not None
25
- config = select_sync_config(location)
26
- config.path = path
27
- config.directory = directory
28
- Syncer(config).capture_pull()
29
-
30
-
31
- def check_authenticated() -> None:
32
- try:
33
- assert backup_context.secrets.rclone
34
- except cli.models.CalledProcessError:
35
- os.environ["RCLONE"] = "dummy"
36
- backup_context.secrets.rclone = bitwarden.client.fetch_secret("Rclone")
@@ -1,12 +0,0 @@
1
- backupmaster<3,>=2.0.2
2
- dbus-next<1,>=0.2.3
3
- package-utils[context]<1,>=0.6.1
4
- powercli<1,>=0.3.1
5
- requests<3,>=2.32.3
6
- superpathlib<3,>=2.0.4
7
-
8
- [dev]
9
- dirhash<1,>=0.5.0
10
- types-requests<3,>=2.32.0.20250306
11
- package-dev-tools<1,>=0.7.1
12
- package-dev-utils<1,>=0.1.6
@@ -1,54 +0,0 @@
1
- import os
2
- from collections.abc import Callable, Iterator
3
-
4
- import pytest
5
- from backup.utils import setup
6
-
7
- from sysetup.main.files.settings import set_background
8
- from sysetup.models import Path
9
-
10
- plasma_config_path = Path.HOME / ".config" / "plasma-org.kde.plasma.desktop-appletsrc"
11
-
12
-
13
- @pytest.fixture
14
- def restore(path: Path) -> Callable[[Path], Iterator[None]]:
15
- def _restore(restored_path: Path) -> Iterator[None]:
16
- exists = restored_path.exists()
17
- if exists:
18
- restored_path.copy_to(path, include_properties=False)
19
- yield
20
- if exists:
21
- path.rename(restored_path, exist_ok=True)
22
-
23
- return _restore
24
-
25
-
26
- @pytest.fixture
27
- def restore_and_check(
28
- restore: Callable[[Path], Iterator[None]],
29
- ) -> Callable[[Path], Iterator[None]]:
30
- setup.check_setup()
31
-
32
- def _restore_and_check(restored_path: Path) -> Iterator[None]:
33
- content_hash = restored_path.content_hash
34
- yield from restore(restored_path)
35
- assert restored_path.content_hash == content_hash
36
-
37
- return _restore_and_check
38
-
39
-
40
- @pytest.fixture
41
- def restore_config_path(
42
- restore_and_check: Callable[[Path], Iterator[None]],
43
- ) -> Iterator[None]:
44
- yield from restore_and_check(plasma_config_path)
45
-
46
-
47
- def test_wallpaper(
48
- restore_config_path: Callable[[Path], Iterator[None]], # noqa: ARG001
49
- ) -> None:
50
- # "org.kde.PlasmaShell.evaluateScript missing in GITHUB_ACTIONS"
51
- # still run existing test case to maximize code under coverage
52
- if "GITHUB_ACTIONS" not in os.environ:
53
- set_background()
54
- assert "Qwallpapers" in plasma_config_path.text
@@ -1,12 +0,0 @@
1
- from unittest.mock import MagicMock, patch
2
-
3
- from package_dev_utils.tests.args import no_cli_args
4
-
5
- from sysetup import cli
6
-
7
-
8
- @no_cli_args
9
- @patch("sysetup.main.main.setup")
10
- def test_entry_point(mocked_setup: MagicMock) -> None:
11
- cli.entry_point()
12
- mocked_setup.assert_called_once()
@@ -1,12 +0,0 @@
1
- from unittest.mock import MagicMock, patch
2
-
3
- from package_dev_utils.tests.args import no_cli_args
4
-
5
- from sysetup.main.main import main
6
-
7
-
8
- @no_cli_args
9
- @patch("sysetup.main.main.setup")
10
- def test_main(mocked_setup: MagicMock) -> None:
11
- main()
12
- mocked_setup.assert_called_once()
File without changes
File without changes
File without changes
File without changes