Skip to content

Commit b81423f

Browse files
authored
Add pipx operations & facts
1 parent f52ac49 commit b81423f

File tree

9 files changed

+268
-0
lines changed

9 files changed

+268
-0
lines changed

pyinfra/facts/pipx.py

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import re
2+
3+
from pyinfra.api import FactBase
4+
5+
from .util.packaging import parse_packages
6+
7+
8+
# TODO: move to an utils file
9+
def parse_environment(output):
10+
environment_REGEX = r"^(?P<key>[A-Z_]+)=(?P<value>.*)$"
11+
environment_variables = {}
12+
13+
for line in output:
14+
matches = re.match(environment_REGEX, line)
15+
16+
if matches:
17+
environment_variables[matches.group("key")] = matches.group("value")
18+
19+
return environment_variables
20+
21+
22+
PIPX_REGEX = r"^([a-zA-Z0-9_\-\+\.]+)\s+([0-9\.]+[a-z0-9\-]*)$"
23+
24+
25+
class PipxPackages(FactBase):
26+
"""
27+
Returns a dict of installed pipx packages:
28+
29+
.. code:: python
30+
31+
{
32+
"package_name": ["version"],
33+
}
34+
"""
35+
36+
default = dict
37+
38+
def requires_command(self) -> str:
39+
return "pipx"
40+
41+
def command(self) -> str:
42+
return "pipx list --short"
43+
44+
def process(self, output):
45+
return parse_packages(PIPX_REGEX, output)
46+
47+
48+
class PipxEnvironment(FactBase):
49+
"""
50+
Returns a dict of pipx environment variables:
51+
52+
.. code:: python
53+
54+
{
55+
"PIPX_HOME": "/home/doodba/.local/pipx",
56+
"PIPX_BIN_DIR": "/home/doodba/.local/bin",
57+
"PIPX_SHARED_LIBS": "/home/doodba/.local/pipx/shared",
58+
"PIPX_LOCAL_VENVS": "/home/doodba/.local/pipx/venvs",
59+
"PIPX_LOG_DIR": "/home/doodba/.local/pipx/logs",
60+
"PIPX_TRASH_DIR": "/home/doodba/.local/pipx/.trash",
61+
"PIPX_VENV_CACHEDIR": "/home/doodba/.local/pipx/.cache",
62+
}
63+
"""
64+
65+
default = dict
66+
67+
def requires_command(self) -> str:
68+
return "pipx"
69+
70+
def command(self) -> str:
71+
return "pipx environment"
72+
73+
def process(self, output):
74+
return parse_environment(output)

pyinfra/operations/pipx.py

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""
2+
Manage pipx (python) applications.
3+
"""
4+
5+
from pyinfra import host
6+
from pyinfra.api import operation
7+
from pyinfra.facts.pipx import PipxEnvironment, PipxPackages
8+
from pyinfra.facts.server import Path
9+
10+
from .util.packaging import ensure_packages
11+
12+
13+
@operation()
14+
def packages(
15+
packages=None,
16+
present=True,
17+
latest=False,
18+
extra_args=None,
19+
):
20+
"""
21+
Install/remove/update pipx packages.
22+
23+
+ packages: list of packages to ensure
24+
+ present: whether the packages should be installed
25+
+ latest: whether to upgrade packages without a specified version
26+
+ extra_args: additional arguments to the pipx command
27+
28+
Versions:
29+
Package versions can be pinned like pip: ``<pkg>==<version>``.
30+
31+
**Example:**
32+
33+
.. code:: python
34+
35+
pipx.packages(
36+
name="Install ",
37+
packages=["pyinfra"],
38+
)
39+
"""
40+
41+
prep_install_command = ["pipx", "install"]
42+
43+
if extra_args:
44+
prep_install_command.append(extra_args)
45+
install_command = " ".join(prep_install_command)
46+
47+
uninstall_command = "pipx uninstall"
48+
upgrade_command = "pipx upgrade"
49+
50+
current_packages = host.get_fact(PipxPackages)
51+
52+
# pipx support only one package name at a time
53+
for package in packages:
54+
yield from ensure_packages(
55+
host,
56+
[package],
57+
current_packages,
58+
present,
59+
install_command=install_command,
60+
uninstall_command=uninstall_command,
61+
upgrade_command=upgrade_command,
62+
version_join="==",
63+
latest=latest,
64+
)
65+
66+
67+
@operation()
68+
def upgrade_all():
69+
"""
70+
Upgrade all pipx packages.
71+
"""
72+
yield "pipx upgrade-all"
73+
74+
75+
@operation()
76+
def ensure_path():
77+
"""
78+
Ensure pipx bin dir is in the PATH.
79+
"""
80+
81+
# Fetch the current user's PATH
82+
path = host.get_fact(Path)
83+
# Fetch the pipx environment variables
84+
pipx_env = host.get_fact(PipxEnvironment)
85+
86+
# If the pipx bin dir is already in the user's PATH, we're done
87+
if "PIPX_BIN_DIR" in pipx_env and pipx_env["PIPX_BIN_DIR"] in path.split(":"):
88+
host.noop("pipx bin dir is already in the PATH")
89+
else:
90+
yield "pipx ensurepath"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"command": "pipx environment",
3+
"requires_command": "pipx",
4+
"output": [
5+
"PIPX_HOME=/home/doodba/.local/pipx",
6+
"PIPX_BIN_DIR=/home/doodba/.local/bin",
7+
"PIPX_SHARED_LIBS=/home/doodba/.local/pipx/shared",
8+
"PIPX_LOCAL_VENVS=/home/doodba/.local/pipx/venvs",
9+
"PIPX_LOG_DIR=/home/doodba/.local/pipx/logs",
10+
"PIPX_TRASH_DIR=/home/doodba/.local/pipx/.trash",
11+
"PIPX_VENV_CACHEDIR=/home/doodba/.local/pipx/.cache"
12+
],
13+
"fact": {
14+
"PIPX_HOME": "/home/doodba/.local/pipx",
15+
"PIPX_BIN_DIR": "/home/doodba/.local/bin",
16+
"PIPX_SHARED_LIBS": "/home/doodba/.local/pipx/shared",
17+
"PIPX_LOCAL_VENVS": "/home/doodba/.local/pipx/venvs",
18+
"PIPX_LOG_DIR": "/home/doodba/.local/pipx/logs",
19+
"PIPX_TRASH_DIR": "/home/doodba/.local/pipx/.trash",
20+
"PIPX_VENV_CACHEDIR": "/home/doodba/.local/pipx/.cache"
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"command": "pipx list --short",
3+
"requires_command": "pipx",
4+
"output": [
5+
"copier 9.0.1",
6+
"invoke 2.2.0"
7+
],
8+
"fact": {
9+
"copier": ["9.0.1"],
10+
"invoke": ["2.2.0"]
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"args": [],
3+
"facts": {
4+
"pipx.PipxEnvironment": {
5+
"PIPX_HOME": "/home/bob/.local/pipx",
6+
"PIPX_BIN_DIR": "/home/bob/.local/bin",
7+
"PIPX_SHARED_LIBS": "/home/bob/.local/pipx/shared",
8+
"PIPX_LOCAL_VENVS": "/home/bob/.local/pipx/venvs",
9+
"PIPX_LOG_DIR": "/home/bob/.local/pipx/logs",
10+
"PIPX_TRASH_DIR": "/home/bob/.local/pipx/.trash",
11+
"PIPX_VENV_CACHEDIR": "/home/bob/.local/pipx/.cache"
12+
},
13+
"server.Path": "/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games"
14+
15+
},
16+
"commands": [
17+
"pipx ensurepath"
18+
]
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"args": [["copier==0.9.1", "invoke", "ensurepath"]],
3+
"facts": {
4+
"pipx.PipxPackages": {"ensurepath": ["0.1.1"]}
5+
},
6+
"commands": [
7+
"pipx install copier==0.9.1",
8+
"pipx install invoke"
9+
]
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"args": [["pyinfra==1.2", "pytask", "test==1.1"]],
3+
"kwargs": {
4+
"extra_args": "--index-url https://pypi.org/"
5+
},
6+
"facts": {
7+
"pipx.PipxPackages": {
8+
"pyinfra": ["1.0"],
9+
"test": ["1.1"]
10+
}
11+
},
12+
"commands": [
13+
"pipx install --index-url https://pypi.org/ pyinfra==1.2",
14+
"pipx install --index-url https://pypi.org/ pytask"
15+
]
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"args": [["invoke", "copier"]],
3+
"kwargs": {
4+
"present": false
5+
},
6+
"facts": {
7+
"pipx.PipxPackages": {
8+
"copier": ["9.0.1"],
9+
"invoke": ["2.2.0"]
10+
}
11+
},
12+
"commands": [
13+
"pipx uninstall invoke",
14+
"pipx uninstall copier"
15+
]
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"args": [],
3+
"facts": {},
4+
"commands": [
5+
"pipx upgrade-all"
6+
],
7+
"idempotent": false,
8+
"disable_idempotent_warning_reason": "package upgrades are always executed"
9+
}

0 commit comments

Comments
 (0)