Skip to content

Commit

Permalink
Add some pipx operations & facts
Browse files Browse the repository at this point in the history
  • Loading branch information
maisim committed Aug 29, 2024
1 parent 134e7b2 commit 7d9c9ec
Show file tree
Hide file tree
Showing 9 changed files with 284 additions and 0 deletions.
81 changes: 81 additions & 0 deletions pyinfra/facts/pipx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import re

from pyinfra.api import FactBase

from .util.packaging import parse_packages


# TODO: move to an utils file
def parse_environment(output):
environment_REGEX = r"^(?P<key>[A-Z_]+)=(?P<value>.*)$"
environment_variables = {}

for line in output:
matches = re.match(environment_REGEX, line)

if matches:
environment_variables[matches.group("key")] = matches.group("value")

return environment_variables


pipx_REGEX = r"^([a-zA-Z0-9_\-\+\.]+)\s+([0-9\.]+[a-z0-9\-]*)$"


class PipxPackages(FactBase):
"""
Returns a dict of installed pipx packages:
.. code:: python
{
"package_name": ["version"],
}
"""

default = dict
pipx_command = "pipx"

def requires_command(self, pipx=None):
return pipx or self.pipx_command

def command(self, pipx=None):
pipx = pipx or self.pipx_command
return f"{pipx} list --short"

def process(self, output):
return parse_packages(pipx_REGEX, output)


class PipxEnvironment(FactBase):
"""
Returns a dict of pipx environment variables:
.. code:: python
{
"PIPX_HOME": "/home/doodba/.local/pipx",
"PIPX_BIN_DIR": "/home/doodba/.local/bin",
"PIPX_SHARED_LIBS": "/home/doodba/.local/pipx/shared",
"PIPX_LOCAL_VENVS": "/home/doodba/.local/pipx/venvs",
"PIPX_LOG_DIR": "/home/doodba/.local/pipx/logs",
"PIPX_TRASH_DIR": "/home/doodba/.local/pipx/.trash",
"PIPX_VENV_CACHEDIR": "/home/doodba/.local/pipx/.cache",
}
"""

default = dict
pipx_command = "pipx"

def requires_command(self):
return self.pipx_command

def command(self, pipx=None):
if pipx:
self.pipx_command = pipx

pipx = self.pipx_command
return f"{pipx} environment"

def process(self, output):
return parse_environment(output)
98 changes: 98 additions & 0 deletions pyinfra/operations/pipx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""
Manage pipx (python) applications.
"""

from pyinfra import host
from pyinfra.api import operation
from pyinfra.facts.server import Path
from pyinfra.facts.pipx import PipxPackages, PipxEnvironment

from .util.packaging import ensure_packages


@operation
def packages(
packages=None,
present=True,
latest=False,
requirements=None,
pipx="pipx",
extra_install_args=None,
):
"""
Install/remove/update pipx packages.
+ packages: list of packages to ensure
+ present: whether the packages should be installed
+ latest: whether to upgrade packages without a specified version
+ pipx: name or path of the pipx binary to use
+ extra_install_args: additional arguments to the pipx install command
Versions:
Package versions can be pinned like pipx: ``<pkg>==<version>``.
**Example:**
.. code:: python
pipx.packages(
name="Install ",
packages=["pyinfra"],
)
"""

# We should always use the --force flag because
# if install it's called, that meen the version missmatch
# so we need the --force flag to get the good version installed
install_command = [pipx, "install", "--force"]
if extra_install_args:
install_command.append(extra_install_args)
install_command = " ".join(install_command)

uninstall_command = " ".join([pipx, "uninstall"])
upgrade_command = " ".join([pipx, "upgrade"])

# Handle passed in packages
if packages:
current_packages = host.get_fact(PipxPackages, pipx=pipx)

# pipx support only one package name at a time
for package in packages:
yield from ensure_packages(
host,
[package],
current_packages,
present,
install_command=install_command,
uninstall_command=uninstall_command,
upgrade_command=upgrade_command,
version_join="==",
latest=latest,
)


@operation
def upgrade_all(pipx="pipx"):
"""
Upgrade all pipx packages.
"""

yield f"{pipx} upgrade-all"


@operation
def ensure_path(pipx="pipx"):
"""
Ensure pipx bin dir is in the PATH.
"""

# Fetch the current user's PATH
path = host.get_fact(Path)
# Fetch the pipx environment variables
pipx_env = host.get_fact(PipxEnvironment)

# If the pipx bin dir is already in the user's PATH, we're done
if pipx_env["PIPX_BIN_DIR"] in path.split(":"):
return
else:
yield f"{pipx} ensurepath"
12 changes: 12 additions & 0 deletions tests/facts/pipx.PipxEnvironment/packages.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"command": "pipx environment",
"requires_command": "pipx",
"output": [
"copier 9.0.1",
"invoke 2.2.0"
],
"fact": {
"copier": ["9.0.1"],
"invoke": ["2.2.0"]
}
}
22 changes: 22 additions & 0 deletions tests/facts/pipx.PipxPackages/packages.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"command": "pipx environment",
"requires_command": "pipx",
"output": [
"PIPX_HOME=/home/doodba/.local/pipx",
"PIPX_BIN_DIR=/home/doodba/.local/bin",
"PIPX_SHARED_LIBS=/home/doodba/.local/pipx/shared",
"PIPX_LOCAL_VENVS=/home/doodba/.local/pipx/venvs",
"PIPX_LOG_DIR=/home/doodba/.local/pipx/logs",
"PIPX_TRASH_DIR=/home/doodba/.local/pipx/.trash",
"PIPX_VENV_CACHEDIR=/home/doodba/.local/pipx/.cache"
],
"fact": {
"PIPX_HOME": "/home/doodba/.local/pipx",
"PIPX_BIN_DIR": "/home/doodba/.local/bin",
"PIPX_SHARED_LIBS": "/home/doodba/.local/pipx/shared",
"PIPX_LOCAL_VENVS": "/home/doodba/.local/pipx/venvs",
"PIPX_LOG_DIR": "/home/doodba/.local/pipx/logs",
"PIPX_TRASH_DIR": "/home/doodba/.local/pipx/.trash",
"PIPX_VENV_CACHEDIR": "/home/doodba/.local/pipx/.cache"
}
}
19 changes: 19 additions & 0 deletions tests/operations/pipx.ensure_path/ensure_path.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"args": [["copier==9.0.1", "invoke"]],
"facts": {
"pipx.PipxEnvironment": {
"PIPX_HOME": "/home/doodba/.local/pipx",
"PIPX_BIN_DIR": "/home/doodba/.local/bin",
"PIPX_SHARED_LIBS": "/home/doodba/.local/pipx/shared",
"PIPX_LOCAL_VENVS": "/home/doodba/.local/pipx/venvs",
"PIPX_LOG_DIR": "/home/doodba/.local/pipx/logs",
"PIPX_TRASH_DIR": "/home/doodba/.local/pipx/.trash",
"PIPX_VENV_CACHEDIR": "/home/doodba/.local/pipx/.cache"
},
"server.Path": "/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games"

},
"commands": [
"pipx ensurepath"
]
}
10 changes: 10 additions & 0 deletions tests/operations/pipx.packages/add_packages.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"args": ["copier==9.0.1", "invoke"],
"facts": {
"pipx.PipxPackages": {}
},
"commands": [
"pipx install --force copier==9.0.1",
"pipx install --force invoke"
]
}
17 changes: 17 additions & 0 deletions tests/operations/pipx.packages/install_extra_args.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"args": [["pyinfra==1.1", "pytask", "test==1.1"]],
"kwargs": {
"extra_install_args": "--user"
},
"facts": {
"pip.PipPackages": {
"pip=pip": {
"pyinfra": ["1.0"],
"test": ["1.1"]
}
}
},
"commands": [
"pip install --user pyinfra==1.1 pytask"
]
}
18 changes: 18 additions & 0 deletions tests/operations/pipx.packages/remove_packages.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"args": [["elasticquery==1.2", "pyinfra", "test==1.1"]],
"kwargs": {
"present": false
},
"facts": {
"pip.PipPackages": {
"pip=pip": {
"elasticquery": ["1.0"],
"pyinfra": [""],
"test": ["1.1"]
}
}
},
"commands": [
"pip uninstall --yes pyinfra test==1.1"
]
}
7 changes: 7 additions & 0 deletions tests/operations/pipx.upgrade_all/upgrade_all.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"args": [],
"facts": {},
"commands": [
"pipx upgrade-all"
]
}

0 comments on commit 7d9c9ec

Please sign in to comment.