Skip to content

Commit a1d91d5

Browse files
committed
Add first step for a python ynh-dev
1 parent 0e77bd1 commit a1d91d5

File tree

6 files changed

+618
-0
lines changed

6 files changed

+618
-0
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# Python stuff
2+
__pycache__/
3+
.*_cache/
4+
uv.lock
5+
.python-version
6+
17
# Apps
28
*_ynh
39

pyproject.toml

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
[project]
2+
name = "ynh-dev"
3+
version = "2.0"
4+
description = "Yunohost dev environment manager"
5+
readme = "README.md"
6+
authors = [
7+
{name = "YunoHost", email = "yunohost@yunohost.org"}
8+
]
9+
requires-python = ">=3.11"
10+
11+
dependencies = [
12+
"pyinotify",
13+
]
14+
15+
16+
[project.scripts]
17+
ynh-dev = "ynh-dev:main"
18+
19+
[dependency-groups]
20+
lint = [
21+
"ruff",
22+
"mypy>=1.18",
23+
]
24+
25+
[tool.uv]
26+
package = false
27+
28+
[tool.ruff]
29+
line-length = 120
30+
31+
[tool.ruff.lint]
32+
select = [
33+
"YTT", # flake8-2020
34+
"ANN", # flake8-annotations
35+
"BLE", # flake8-blind-except
36+
"B", # flake8-bugbear
37+
"A", # flake8-builtins
38+
"C4", # flake8-comprehensions
39+
"DTZ", # flake8-datetimez
40+
"ISC", # flake8-implicit-str-concat
41+
"ICN", # flake8-import-conventions
42+
# "LOG", # flake8-logging
43+
# "G", # flake8-logging-format
44+
"PIE", # flake8-pie
45+
"Q", # flake8-quotes
46+
"RET", # flake8-return
47+
"SIM", # flake8-simplify
48+
"SLOT", # flake8-slots
49+
"PTH", # flake8-use-pathlib
50+
"FLY", # flynt
51+
"I", # isort
52+
"N", # pep8-naming
53+
"PERF", # Perflint
54+
"E", # pycodestyle
55+
"W", # pycodestyle
56+
"F", # Pyflakes
57+
"PL", # Pylint
58+
"UP", # pyupgrade
59+
"FURB", # refurb
60+
"RUF", # Ruff-specific rules
61+
"TRY", # tryceratops
62+
# "D",
63+
]
64+
ignore = [
65+
"ANN401", # any-type
66+
"COM812", # missing-trailing-comma
67+
"D203", # incorrect-blank-line-before-class
68+
"PLR0911", # too-many-return-statements
69+
"PLR0912", # too-many-branches
70+
"PLR0913", # too-many-arguments
71+
"PLR0915", # too-many-statements
72+
"PLR2004", # magic-value-comparison
73+
"UP009", # utf8-encoding-declaration
74+
"TRY003", # raise-vanilla-args
75+
"D100", # undocumented-public-module
76+
"D104", # undocumented-public-package
77+
"D105", # undocumented-magic-method
78+
"D200", # unnecessary-multiline-docstring
79+
"D212", # multi-line-summary-first-line
80+
"D401", # non-imperative-mood
81+
]

ynh-dev.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env python3
2+
3+
import os
4+
5+
6+
def main() -> None:
7+
if "container" in os.environ:
8+
from ynh_dev.ynh_dev_guest import main_container # noqa: PLC0415
9+
main_container()
10+
else:
11+
from ynh_dev.ynh_dev_host import main_host # noqa: PLC0415
12+
main_host()
13+
14+
15+
if __name__ == "__main__":
16+
main()

ynh_dev/libincus.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
#!/usr/bin/env python3
2+
3+
import json
4+
import logging
5+
import os
6+
import platform
7+
import shutil
8+
import subprocess
9+
from pathlib import Path
10+
from typing import Any
11+
12+
13+
class Incus:
14+
def __init__(self) -> None:
15+
pass
16+
17+
def arch(self) -> str:
18+
plat = platform.machine()
19+
if plat in ["x86_64", "amd64"]:
20+
return "amd64"
21+
if plat in ["arm64", "aarch64"]:
22+
return "arm64"
23+
if plat in ["armhf"]:
24+
return "armhf"
25+
raise RuntimeError(f"Unknown platform {plat}!")
26+
27+
def _run(self, *args: str, interactive: bool = False, **kwargs: Any) -> str:
28+
command = ["incus", *args]
29+
if interactive:
30+
subprocess.run(command, **kwargs, stdin=subprocess.PIPE, capture_output=False, check=True)
31+
result = ""
32+
else:
33+
result = subprocess.check_output(command, **kwargs).decode("utf-8")
34+
return result
35+
36+
def _run_logged_prefixed(self, *args: str, prefix: str = "", **kwargs: Any) -> None:
37+
command = ["incus", *args]
38+
39+
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **kwargs)
40+
assert process.stdout
41+
with process.stdout:
42+
for line in iter(process.stdout.readline, b""): # b'\n'-separated lines
43+
linestr = line if isinstance(line, str) else line.decode("utf-8")
44+
logging.debug("%s%s", prefix, linestr.rstrip("\n"))
45+
exitcode = process.wait() # 0 means success
46+
if exitcode:
47+
raise RuntimeError(f"Could not run {' '.join(command)}")
48+
49+
def instance_stopped(self, name: str) -> bool:
50+
assert self.instance_exists(name)
51+
res = json.loads(self._run("info", name))
52+
return str(res["Status"]) == "STOPPED"
53+
54+
def instance_exists(self, name: str) -> bool:
55+
res = json.loads(self._run("list", "-f", "json"))
56+
instance_names = [instance["name"] for instance in res]
57+
return name in instance_names
58+
59+
def instance_start(self, name: str) -> None:
60+
self._run("start", name)
61+
62+
def instance_stop(self, name: str) -> None:
63+
self._run("stop", name)
64+
65+
def instance_delete(self, name: str) -> None:
66+
self._run("delete", name)
67+
68+
def launch(self, image_name: str, instance_name: str, *args: str) -> None:
69+
self._run("launch", image_name, instance_name, *args)
70+
71+
def push_file(self, instance_name: str, file: Path, target: str) -> None:
72+
self._run("file", "push", str(file), f"{instance_name}{target}")
73+
os.sync()
74+
75+
def execute(self, instance_name: str, *args: str, exec_: bool = False, cwd: str | None = None) -> None:
76+
cwd_args = ["--cwd", cwd] if cwd else []
77+
incus_args: list[str] = ["exec", instance_name, *cwd_args, "--", *args]
78+
if exec_:
79+
incus = shutil.which("incus")
80+
assert incus
81+
os.execv(incus, ["incus", *incus_args])
82+
else:
83+
self._run_logged_prefixed(*incus_args, prefix=" In container |\t")
84+
85+
def publish(self, instance_name: str, image_alias: str, properties: dict[str, str]) -> None:
86+
properties_list = [f"{key}={value}" for key, value in properties.items()]
87+
self._run("publish", instance_name, "--alias", image_alias, *properties_list)
88+
89+
def image_export(self, image_alias: str, image_target: str, target_dir: Path) -> None:
90+
self._run("image", "export", image_alias, image_target, cwd=target_dir)
91+
92+
def image_exists(self, alias: str) -> bool:
93+
res = json.loads(self._run("image", "list", "-f", "json"))
94+
image_aliases = [alias["name"] for image in res for alias in image["aliases"]]
95+
return alias in image_aliases
96+
97+
def image_alias_exists(self, alias: str) -> bool:
98+
res = json.loads(self._run("image", "alias", "list", "-f", "json"))
99+
aliases = [alias["name"] for alias in res]
100+
return alias in aliases
101+
102+
def image_delete(self, alias: str) -> None:
103+
self._run("image", "delete", alias)
104+
105+
def image_download(self, alias: str) -> None:
106+
if self.image_alias_exists(alias):
107+
self.image_delete(alias)
108+
self._run("image", "copy", alias, "local:", "--copy-aliases", "--auto-update", interactive=True)
109+
110+
def remotes(self) -> dict[str, dict[str, str]]:
111+
return json.loads(self._run("remote", "list", "-f", "json"))
112+
113+
def remote_add(self, name: str, url: str, public: bool, protocol: str) -> None:
114+
self._run(
115+
"remote",
116+
"add",
117+
name,
118+
url,
119+
"--protocol",
120+
protocol,
121+
*(["--public"] if public else []),
122+
)

0 commit comments

Comments
 (0)