abxpkg 1.10.1__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.
- abxpkg/__init__.py +195 -0
- abxpkg/admin.py +45 -0
- abxpkg/apps.py +6 -0
- abxpkg/base_types.py +312 -0
- abxpkg/binary.py +530 -0
- abxpkg/binprovider.py +2121 -0
- abxpkg/binprovider_ansible.py +422 -0
- abxpkg/binprovider_apt.py +320 -0
- abxpkg/binprovider_bash.py +284 -0
- abxpkg/binprovider_brew.py +672 -0
- abxpkg/binprovider_bun.py +445 -0
- abxpkg/binprovider_cargo.py +258 -0
- abxpkg/binprovider_chromewebstore.py +255 -0
- abxpkg/binprovider_deno.py +295 -0
- abxpkg/binprovider_docker.py +324 -0
- abxpkg/binprovider_gem.py +261 -0
- abxpkg/binprovider_goget.py +273 -0
- abxpkg/binprovider_nix.py +245 -0
- abxpkg/binprovider_npm.py +691 -0
- abxpkg/binprovider_pip.py +646 -0
- abxpkg/binprovider_playwright.py +595 -0
- abxpkg/binprovider_pnpm.py +506 -0
- abxpkg/binprovider_puppeteer.py +662 -0
- abxpkg/binprovider_pyinfra.py +309 -0
- abxpkg/binprovider_uv.py +604 -0
- abxpkg/binprovider_yarn.py +533 -0
- abxpkg/cli.py +1289 -0
- abxpkg/config.py +196 -0
- abxpkg/exceptions.py +88 -0
- abxpkg/js/base/config.json +108 -0
- abxpkg/js/base/utils.js +402 -0
- abxpkg/js/chrome/chrome_utils.js +4147 -0
- abxpkg/js/chrome/config.json +248 -0
- abxpkg/logging.py +307 -0
- abxpkg/models.py +27 -0
- abxpkg/semver.py +145 -0
- abxpkg/settings.py +38 -0
- abxpkg/shallowbinary.py +5 -0
- abxpkg/tests.py +1 -0
- abxpkg/views.py +103 -0
- abxpkg-1.10.1.dist-info/METADATA +1325 -0
- abxpkg-1.10.1.dist-info/RECORD +45 -0
- abxpkg-1.10.1.dist-info/WHEEL +4 -0
- abxpkg-1.10.1.dist-info/entry_points.txt +3 -0
- abxpkg-1.10.1.dist-info/licenses/LICENSE +21 -0
abxpkg/__init__.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
__package__ = "abxpkg"
|
|
2
|
+
|
|
3
|
+
from .base_types import (
|
|
4
|
+
BinName,
|
|
5
|
+
InstallArgs,
|
|
6
|
+
PATHStr,
|
|
7
|
+
HostBinPath,
|
|
8
|
+
HostExistsPath,
|
|
9
|
+
BinDirPath,
|
|
10
|
+
BinProviderName,
|
|
11
|
+
bin_name,
|
|
12
|
+
bin_abspath,
|
|
13
|
+
bin_abspaths,
|
|
14
|
+
func_takes_args_or_kwargs,
|
|
15
|
+
)
|
|
16
|
+
from .semver import SemVer, bin_version
|
|
17
|
+
from .shallowbinary import ShallowBinary
|
|
18
|
+
from .logging import (
|
|
19
|
+
logger,
|
|
20
|
+
get_logger,
|
|
21
|
+
configure_logging,
|
|
22
|
+
configure_rich_logging,
|
|
23
|
+
RICH_INSTALLED,
|
|
24
|
+
)
|
|
25
|
+
from .exceptions import (
|
|
26
|
+
ABXPkgError,
|
|
27
|
+
BinaryOperationError,
|
|
28
|
+
BinaryInstallError,
|
|
29
|
+
BinaryLoadError,
|
|
30
|
+
BinaryUpdateError,
|
|
31
|
+
BinaryUninstallError,
|
|
32
|
+
)
|
|
33
|
+
from .binprovider import (
|
|
34
|
+
BinProvider,
|
|
35
|
+
EnvProvider,
|
|
36
|
+
OPERATING_SYSTEM,
|
|
37
|
+
DEFAULT_PATH,
|
|
38
|
+
DEFAULT_ENV_PATH,
|
|
39
|
+
PYTHON_BIN_DIR,
|
|
40
|
+
BinProviderOverrides,
|
|
41
|
+
BinaryOverrides,
|
|
42
|
+
ProviderFuncReturnValue,
|
|
43
|
+
HandlerType,
|
|
44
|
+
HandlerValue,
|
|
45
|
+
HandlerDict,
|
|
46
|
+
HandlerReturnValue,
|
|
47
|
+
)
|
|
48
|
+
from .binary import Binary
|
|
49
|
+
|
|
50
|
+
from .binprovider_apt import AptProvider
|
|
51
|
+
from .binprovider_brew import BrewProvider
|
|
52
|
+
from .binprovider_cargo import CargoProvider
|
|
53
|
+
from .binprovider_gem import GemProvider
|
|
54
|
+
from .binprovider_goget import GoGetProvider
|
|
55
|
+
from .binprovider_nix import NixProvider
|
|
56
|
+
from .binprovider_docker import DockerProvider
|
|
57
|
+
from .binprovider_pip import PipProvider
|
|
58
|
+
from .binprovider_uv import UvProvider
|
|
59
|
+
from .binprovider_npm import NpmProvider
|
|
60
|
+
from .binprovider_pnpm import PnpmProvider
|
|
61
|
+
from .binprovider_yarn import YarnProvider
|
|
62
|
+
from .binprovider_bun import BunProvider
|
|
63
|
+
from .binprovider_deno import DenoProvider
|
|
64
|
+
from .binprovider_ansible import AnsibleProvider
|
|
65
|
+
from .binprovider_pyinfra import PyinfraProvider
|
|
66
|
+
from .binprovider_chromewebstore import ChromeWebstoreProvider
|
|
67
|
+
from .binprovider_puppeteer import PuppeteerProvider
|
|
68
|
+
from .binprovider_playwright import PlaywrightProvider
|
|
69
|
+
from .binprovider_bash import BashProvider
|
|
70
|
+
|
|
71
|
+
ALL_PROVIDERS = [
|
|
72
|
+
EnvProvider,
|
|
73
|
+
UvProvider,
|
|
74
|
+
PipProvider,
|
|
75
|
+
PnpmProvider,
|
|
76
|
+
NpmProvider,
|
|
77
|
+
YarnProvider,
|
|
78
|
+
BunProvider,
|
|
79
|
+
BrewProvider,
|
|
80
|
+
GemProvider,
|
|
81
|
+
GoGetProvider,
|
|
82
|
+
CargoProvider,
|
|
83
|
+
PlaywrightProvider,
|
|
84
|
+
PuppeteerProvider,
|
|
85
|
+
AptProvider,
|
|
86
|
+
NixProvider,
|
|
87
|
+
DockerProvider,
|
|
88
|
+
DenoProvider,
|
|
89
|
+
AnsibleProvider,
|
|
90
|
+
PyinfraProvider,
|
|
91
|
+
ChromeWebstoreProvider,
|
|
92
|
+
BashProvider,
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _provider_class(provider: type[BinProvider] | BinProvider) -> type[BinProvider]:
|
|
97
|
+
return provider if isinstance(provider, type) else type(provider)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
ALL_PROVIDER_NAMES = [
|
|
101
|
+
_provider_class(provider).model_fields["name"].default for provider in ALL_PROVIDERS
|
|
102
|
+
] # pip, apt, brew, etc.
|
|
103
|
+
ALL_PROVIDER_CLASS_NAMES = [
|
|
104
|
+
_provider_class(provider).__name__ for provider in ALL_PROVIDERS
|
|
105
|
+
] # PipProvider, AptProvider, BrewProvider, etc.
|
|
106
|
+
|
|
107
|
+
# Lazy provider singletons: maps provider name -> class
|
|
108
|
+
# e.g. 'apt' -> AptProvider, 'pip' -> PipProvider, 'env' -> EnvProvider
|
|
109
|
+
_PROVIDER_CLASS_BY_NAME = {
|
|
110
|
+
_provider_class(provider).model_fields["name"].default: _provider_class(provider)
|
|
111
|
+
for provider in ALL_PROVIDERS
|
|
112
|
+
}
|
|
113
|
+
_provider_singletons: dict = {}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def __getattr__(name: str):
|
|
117
|
+
if name in _PROVIDER_CLASS_BY_NAME:
|
|
118
|
+
if name not in _provider_singletons:
|
|
119
|
+
_provider_singletons[name] = _PROVIDER_CLASS_BY_NAME[name]()
|
|
120
|
+
return _provider_singletons[name]
|
|
121
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
__all__ = [
|
|
125
|
+
# Main types
|
|
126
|
+
"BinProvider",
|
|
127
|
+
"Binary",
|
|
128
|
+
"SemVer",
|
|
129
|
+
"ShallowBinary",
|
|
130
|
+
"logger",
|
|
131
|
+
"get_logger",
|
|
132
|
+
"configure_logging",
|
|
133
|
+
"configure_rich_logging",
|
|
134
|
+
"RICH_INSTALLED",
|
|
135
|
+
# Exceptions
|
|
136
|
+
"ABXPkgError",
|
|
137
|
+
"BinaryOperationError",
|
|
138
|
+
"BinaryInstallError",
|
|
139
|
+
"BinaryLoadError",
|
|
140
|
+
"BinaryUpdateError",
|
|
141
|
+
"BinaryUninstallError",
|
|
142
|
+
# Helper Types
|
|
143
|
+
"BinName",
|
|
144
|
+
"InstallArgs",
|
|
145
|
+
"PATHStr",
|
|
146
|
+
"BinDirPath",
|
|
147
|
+
"HostBinPath",
|
|
148
|
+
"HostExistsPath",
|
|
149
|
+
"BinProviderName",
|
|
150
|
+
# Override types
|
|
151
|
+
"BinProviderOverrides",
|
|
152
|
+
"BinaryOverrides",
|
|
153
|
+
"ProviderFuncReturnValue",
|
|
154
|
+
"HandlerType",
|
|
155
|
+
"HandlerValue",
|
|
156
|
+
"HandlerDict",
|
|
157
|
+
"HandlerReturnValue",
|
|
158
|
+
# Validator Functions
|
|
159
|
+
"bin_version",
|
|
160
|
+
"bin_name",
|
|
161
|
+
"bin_abspath",
|
|
162
|
+
"bin_abspaths",
|
|
163
|
+
"func_takes_args_or_kwargs",
|
|
164
|
+
# Globals
|
|
165
|
+
"OPERATING_SYSTEM",
|
|
166
|
+
"DEFAULT_PATH",
|
|
167
|
+
"DEFAULT_ENV_PATH",
|
|
168
|
+
"PYTHON_BIN_DIR",
|
|
169
|
+
# BinProviders (classes)
|
|
170
|
+
"EnvProvider",
|
|
171
|
+
"AptProvider",
|
|
172
|
+
"BrewProvider",
|
|
173
|
+
"CargoProvider",
|
|
174
|
+
"GemProvider",
|
|
175
|
+
"GoGetProvider",
|
|
176
|
+
"NixProvider",
|
|
177
|
+
"DockerProvider",
|
|
178
|
+
"PipProvider",
|
|
179
|
+
"UvProvider",
|
|
180
|
+
"NpmProvider",
|
|
181
|
+
"PnpmProvider",
|
|
182
|
+
"YarnProvider",
|
|
183
|
+
"BunProvider",
|
|
184
|
+
"DenoProvider",
|
|
185
|
+
"AnsibleProvider",
|
|
186
|
+
"PyinfraProvider",
|
|
187
|
+
"ChromeWebstoreProvider",
|
|
188
|
+
"PuppeteerProvider",
|
|
189
|
+
"PlaywrightProvider",
|
|
190
|
+
"BashProvider",
|
|
191
|
+
# Note: provider singleton names (apt, pip, brew, etc.) are intentionally
|
|
192
|
+
# excluded from __all__ so that `from abxpkg import *` does not eagerly
|
|
193
|
+
# instantiate every provider. Use explicit imports instead:
|
|
194
|
+
# from abxpkg import apt, pip, brew, npm, pnpm, yarn, bun, deno
|
|
195
|
+
]
|
abxpkg/admin.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from types import MethodType
|
|
2
|
+
|
|
3
|
+
from django.contrib import admin
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def register_admin_views(admin_site: admin.AdminSite):
|
|
7
|
+
"""register the django-admin-data-views defined in settings.ADMIN_DATA_VIEWS"""
|
|
8
|
+
|
|
9
|
+
from admin_data_views.admin import (
|
|
10
|
+
get_app_list,
|
|
11
|
+
admin_data_index_view,
|
|
12
|
+
get_admin_data_urls,
|
|
13
|
+
get_urls,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
setattr(admin_site, "get_app_list", MethodType(get_app_list, admin_site))
|
|
17
|
+
setattr(
|
|
18
|
+
admin_site,
|
|
19
|
+
"admin_data_index_view",
|
|
20
|
+
MethodType(admin_data_index_view, admin_site),
|
|
21
|
+
)
|
|
22
|
+
setattr(
|
|
23
|
+
admin_site,
|
|
24
|
+
"get_admin_data_urls",
|
|
25
|
+
MethodType(get_admin_data_urls, admin_site),
|
|
26
|
+
)
|
|
27
|
+
setattr(
|
|
28
|
+
admin_site,
|
|
29
|
+
"get_urls",
|
|
30
|
+
MethodType(get_urls(admin_site.get_urls), admin_site),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return admin_site
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
register_admin_views(admin.site)
|
|
37
|
+
|
|
38
|
+
# if you've implemented a custom admin site, you should call this function on your site
|
|
39
|
+
|
|
40
|
+
# class YourSiteAdmin(admin.AdminSite):
|
|
41
|
+
# ...
|
|
42
|
+
#
|
|
43
|
+
# custom_site = YourSiteAdmin()
|
|
44
|
+
#
|
|
45
|
+
# register_admin_views(custom_site)
|
abxpkg/apps.py
ADDED
abxpkg/base_types.py
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
__package__ = "abxpkg"
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Annotated
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
|
|
11
|
+
from platformdirs import user_config_path
|
|
12
|
+
|
|
13
|
+
from pydantic import TypeAdapter, AfterValidator, BeforeValidator, ValidationError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Read once at import time. When set, providers that opt into
|
|
17
|
+
# ``abxpkg_install_root_default("<provider>")`` default their
|
|
18
|
+
# ``install_root`` to ``ABXPKG_LIB_DIR / <provider name>`` (e.g.
|
|
19
|
+
# ``<lib>/npm``, ``<lib>/pip``, ``<lib>/gem``). Per-provider
|
|
20
|
+
# ``ABXPKG_<NAME>_ROOT`` env vars override this for their own
|
|
21
|
+
# provider; explicit constructor kwargs override both.
|
|
22
|
+
DEFAULT_LIB_DIR: Path = user_config_path("abx") / "lib"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def abxpkg_install_root_default(provider_name: str) -> Path | None:
|
|
26
|
+
"""Resolve a provider's default install root from env vars.
|
|
27
|
+
|
|
28
|
+
Precedence (most specific wins):
|
|
29
|
+
1. ``ABXPKG_<PROVIDER>_ROOT`` (e.g. ``ABXPKG_NPM_ROOT``)
|
|
30
|
+
2. ``ABXPKG_LIB_DIR / <provider_name>``
|
|
31
|
+
3. ``None`` — caller's built-in default applies.
|
|
32
|
+
|
|
33
|
+
Explicit constructor kwargs (``install_root=`` or the
|
|
34
|
+
provider-specific alias) always override whatever this function
|
|
35
|
+
returns.
|
|
36
|
+
"""
|
|
37
|
+
specific = os.environ.get(f"ABXPKG_{provider_name.upper()}_ROOT", "").strip()
|
|
38
|
+
if specific:
|
|
39
|
+
return Path(specific).expanduser().resolve()
|
|
40
|
+
# Read from os.environ directly (not the cached module-level
|
|
41
|
+
# ABXPKG_LIB_DIR) because the CLI sets it at runtime via --lib.
|
|
42
|
+
lib_dir = os.environ.get("ABXPKG_LIB_DIR", "").strip()
|
|
43
|
+
if lib_dir:
|
|
44
|
+
return Path(lib_dir).expanduser().resolve() / provider_name
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def validate_binprovider_name(name: str) -> str:
|
|
49
|
+
assert 1 < len(name) < 16, (
|
|
50
|
+
"BinProvider names must be between 1 and 16 characters long"
|
|
51
|
+
)
|
|
52
|
+
assert name.replace("_", "").isalnum(), (
|
|
53
|
+
"BinProvider names can only contain a-Z0-9 and underscores"
|
|
54
|
+
)
|
|
55
|
+
assert name[0].isalpha(), "BinProvider names must start with a letter"
|
|
56
|
+
return name
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
BinProviderName = Annotated[str, AfterValidator(validate_binprovider_name)]
|
|
60
|
+
# in practice this is essentially BinProviderName: Literal['env', 'pip', 'apt', 'brew', 'npm', 'vendor']
|
|
61
|
+
# but because users can create their own BinProviders we can't restrict it to a preset list of literal names
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def validate_bin_dir(path: Path) -> Path:
|
|
65
|
+
path = path.expanduser().absolute()
|
|
66
|
+
assert path.resolve()
|
|
67
|
+
assert os.path.isdir(path) and os.access(path, os.R_OK), (
|
|
68
|
+
f"path entries to add to $PATH must be absolute paths to directories {dir}"
|
|
69
|
+
)
|
|
70
|
+
return path
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
BinDirPath = Annotated[Path, AfterValidator(validate_bin_dir)]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def validate_PATH(PATH: str | list[str]) -> str:
|
|
77
|
+
paths = PATH.split(":") if isinstance(PATH, str) else list(PATH)
|
|
78
|
+
assert all(Path(bin_dir) for bin_dir in paths)
|
|
79
|
+
return ":".join(paths).strip(":")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
PATHStr = Annotated[str, BeforeValidator(validate_PATH)]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def func_takes_args_or_kwargs(lambda_func: Callable[..., Any]) -> bool:
|
|
86
|
+
"""returns True if a lambda func takes args/kwargs of any kind, otherwise false if it's pure/argless"""
|
|
87
|
+
signature = inspect.signature(lambda_func)
|
|
88
|
+
return any(
|
|
89
|
+
parameter.kind
|
|
90
|
+
in (
|
|
91
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
|
92
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
93
|
+
inspect.Parameter.VAR_POSITIONAL,
|
|
94
|
+
inspect.Parameter.VAR_KEYWORD,
|
|
95
|
+
)
|
|
96
|
+
for parameter in signature.parameters.values()
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# @validate_call
|
|
101
|
+
def bin_name(bin_path_or_name: str | Path) -> str:
|
|
102
|
+
"""
|
|
103
|
+
- wget -> wget
|
|
104
|
+
- /usr/bin/wget -> wget
|
|
105
|
+
- ~/bin/wget -> wget
|
|
106
|
+
- ~/.local/bin/wget -> wget
|
|
107
|
+
- @postlight/parser -> @postlight/parser
|
|
108
|
+
- @postlight/parser@2.2.3 -> @postlight/parser
|
|
109
|
+
- yt-dlp==2024.05.09 -> yt-dlp
|
|
110
|
+
- postlight/parser^2.2.3 -> postlight/parser
|
|
111
|
+
- @postlight/parser@^2.2.3 -> @postlight/parser
|
|
112
|
+
"""
|
|
113
|
+
str_bin_name = (
|
|
114
|
+
str(bin_path_or_name)
|
|
115
|
+
.split("^", 1)[0]
|
|
116
|
+
.split("=", 1)[0]
|
|
117
|
+
.split(">", 1)[0]
|
|
118
|
+
.split("<", 1)[0]
|
|
119
|
+
)
|
|
120
|
+
if str_bin_name.startswith("@"):
|
|
121
|
+
# @postlight/parser@^2.2.3 -> @postlight/parser
|
|
122
|
+
str_bin_name = "@" + str_bin_name[1:].split("@", 1)[0]
|
|
123
|
+
else:
|
|
124
|
+
str_bin_name = str_bin_name.split("@", 1)[0]
|
|
125
|
+
|
|
126
|
+
assert len(str_bin_name) > 0, "Binary names must be non-empty"
|
|
127
|
+
name = (
|
|
128
|
+
Path(str_bin_name).name if str_bin_name[0] in (".", "/", "~") else str_bin_name
|
|
129
|
+
)
|
|
130
|
+
assert 1 <= len(name) < 64, "Binary names must be between 1 and 63 characters long"
|
|
131
|
+
assert (
|
|
132
|
+
name.replace("-", "")
|
|
133
|
+
.replace("_", "")
|
|
134
|
+
.replace(".", "")
|
|
135
|
+
.replace(" ", "")
|
|
136
|
+
.replace("@", "")
|
|
137
|
+
.replace("/", "")
|
|
138
|
+
.isalnum()
|
|
139
|
+
), f"Binary name can only contain a-Z0-9-_.@/ and spaces: {name}"
|
|
140
|
+
assert name.replace("@", "")[0].isalpha(), (
|
|
141
|
+
"Binary names must start with a letter or @"
|
|
142
|
+
)
|
|
143
|
+
# print('PARSING BIN NAME', bin_path_or_name, '->', name)
|
|
144
|
+
return name
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
BinName = Annotated[str, AfterValidator(bin_name)]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# @validate_call
|
|
151
|
+
def path_is_file(path: Path | str) -> Path:
|
|
152
|
+
path = Path(path) if isinstance(path, str) else path
|
|
153
|
+
assert os.path.isfile(path) and os.access(path, os.R_OK), (
|
|
154
|
+
f"Path is not a file or we dont have permission to read it: {path}"
|
|
155
|
+
)
|
|
156
|
+
return path
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
HostExistsPath = Annotated[Path, AfterValidator(path_is_file)]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# @validate_call
|
|
163
|
+
def path_is_executable(path: HostExistsPath) -> HostExistsPath:
|
|
164
|
+
assert os.path.isfile(path) and os.access(path, os.X_OK), (
|
|
165
|
+
f"Path is not executable (fix by running chmod +x {path})"
|
|
166
|
+
)
|
|
167
|
+
return path
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# @validate_call
|
|
171
|
+
def path_is_script(path: HostExistsPath) -> HostExistsPath:
|
|
172
|
+
SCRIPT_EXTENSIONS = (".py", ".js", ".sh")
|
|
173
|
+
assert path.suffix.lower() in SCRIPT_EXTENSIONS, (
|
|
174
|
+
"Path is not a script (does not end in {})".format(", ".join(SCRIPT_EXTENSIONS))
|
|
175
|
+
)
|
|
176
|
+
return path
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
HostExecutablePath = Annotated[HostExistsPath, AfterValidator(path_is_executable)]
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# @validate_call
|
|
183
|
+
def path_is_abspath(path: Path) -> Path:
|
|
184
|
+
path = path.expanduser().absolute() # resolve ~/ -> /home/<username/ and ../../
|
|
185
|
+
assert (
|
|
186
|
+
path.resolve()
|
|
187
|
+
) # make sure symlinks can be resolved, but dont return resolved link
|
|
188
|
+
return path
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
HostAbsPath = Annotated[HostExistsPath, AfterValidator(path_is_abspath)]
|
|
192
|
+
HostBinPath = Annotated[
|
|
193
|
+
HostExistsPath,
|
|
194
|
+
AfterValidator(path_is_abspath),
|
|
195
|
+
] # removed: AfterValidator(path_is_executable)
|
|
196
|
+
# not all bins need to be executable to be bins, some are scripts
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# @validate_call
|
|
200
|
+
def bin_abspath(
|
|
201
|
+
bin_path_or_name: str | BinName | Path,
|
|
202
|
+
PATH: PATHStr | None = None,
|
|
203
|
+
) -> HostBinPath | None:
|
|
204
|
+
assert bin_path_or_name
|
|
205
|
+
if PATH is None:
|
|
206
|
+
PATH = os.environ.get("PATH", "/bin")
|
|
207
|
+
if PATH:
|
|
208
|
+
PATH = str(PATH)
|
|
209
|
+
else:
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
if str(bin_path_or_name).startswith("/"):
|
|
213
|
+
# already a path, get its absolute form
|
|
214
|
+
abspath = Path(bin_path_or_name).expanduser().absolute()
|
|
215
|
+
else:
|
|
216
|
+
# not a path yet, get path using shutil.which
|
|
217
|
+
binpath = shutil.which(bin_path_or_name, mode=os.X_OK, path=PATH)
|
|
218
|
+
# print(bin_path_or_name, PATH.split(':'), binpath, 'GOPINGNGN')
|
|
219
|
+
if not binpath:
|
|
220
|
+
# some bins dont show up with shutil.which (e.g. django-admin.py)
|
|
221
|
+
for path in PATH.split(":"):
|
|
222
|
+
bin_dir = Path(path)
|
|
223
|
+
# print('BIN_DIR', bin_dir, bin_dir.is_dir())
|
|
224
|
+
if not (os.path.isdir(bin_dir) and os.access(bin_dir, os.R_OK)):
|
|
225
|
+
# raise Exception(f'Found invalid dir in $PATH: {bin_dir}')
|
|
226
|
+
continue
|
|
227
|
+
bin_file = bin_dir / bin_path_or_name
|
|
228
|
+
# print(bin_file, path, bin_file.exists(), bin_file.is_file(), bin_file.is_symlink())
|
|
229
|
+
if os.path.isfile(bin_file) and os.access(bin_file, os.R_OK):
|
|
230
|
+
return bin_file
|
|
231
|
+
|
|
232
|
+
return None
|
|
233
|
+
# print(binpath, PATH)
|
|
234
|
+
if str(Path(binpath).parent) not in PATH:
|
|
235
|
+
# print('WARNING, found bin but not in PATH', binpath, PATH)
|
|
236
|
+
# found bin but it was outside our search $PATH
|
|
237
|
+
return None
|
|
238
|
+
abspath = Path(binpath).expanduser().absolute()
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
return TypeAdapter(HostBinPath).validate_python(abspath)
|
|
242
|
+
except ValidationError:
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# @validate_call
|
|
247
|
+
def bin_abspaths(
|
|
248
|
+
bin_path_or_name: BinName | Path,
|
|
249
|
+
PATH: PATHStr | None = None,
|
|
250
|
+
) -> list[HostBinPath]:
|
|
251
|
+
assert bin_path_or_name
|
|
252
|
+
|
|
253
|
+
PATH = PATH or os.environ.get("PATH", "/bin")
|
|
254
|
+
abspaths = []
|
|
255
|
+
|
|
256
|
+
if str(bin_path_or_name).startswith("/"):
|
|
257
|
+
# already a path, get its absolute form
|
|
258
|
+
abspaths.append(Path(bin_path_or_name).expanduser().absolute())
|
|
259
|
+
else:
|
|
260
|
+
# not a path yet, get path using shutil.which
|
|
261
|
+
for path in PATH.split(":"):
|
|
262
|
+
binpath = shutil.which(bin_path_or_name, mode=os.X_OK, path=path)
|
|
263
|
+
if binpath and str(Path(binpath).parent) in PATH:
|
|
264
|
+
abspaths.append(binpath)
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
return TypeAdapter(list[HostBinPath]).validate_python(abspaths)
|
|
268
|
+
except ValidationError:
|
|
269
|
+
return []
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
################## Types ##############################################
|
|
273
|
+
|
|
274
|
+
UNKNOWN_SHA256 = "unknown"
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def is_valid_sha256(sha256: str) -> str:
|
|
278
|
+
if sha256 == UNKNOWN_SHA256:
|
|
279
|
+
return sha256
|
|
280
|
+
assert len(sha256) == 64
|
|
281
|
+
assert sha256.isalnum()
|
|
282
|
+
return sha256
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
Sha256 = Annotated[str, AfterValidator(is_valid_sha256)]
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def is_valid_install_args(
|
|
289
|
+
install_args: list[str] | tuple[str, ...] | str,
|
|
290
|
+
) -> tuple[str, ...]:
|
|
291
|
+
"""Make sure a string is a valid install string for a package manager, e.g. ['yt-dlp', 'ffmpeg']"""
|
|
292
|
+
if isinstance(install_args, str):
|
|
293
|
+
install_args = [install_args]
|
|
294
|
+
assert install_args
|
|
295
|
+
assert all(len(arg) for arg in install_args)
|
|
296
|
+
return tuple(install_args)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def is_name_of_method_on_self(method_name: str) -> str:
|
|
300
|
+
assert (
|
|
301
|
+
method_name.startswith("self.")
|
|
302
|
+
and method_name.replace(".", "").replace("_", "").isalnum()
|
|
303
|
+
)
|
|
304
|
+
return method_name
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
InstallArgs = Annotated[
|
|
308
|
+
tuple[str, ...] | list[str],
|
|
309
|
+
AfterValidator(is_valid_install_args),
|
|
310
|
+
]
|
|
311
|
+
|
|
312
|
+
SelfMethodName = Annotated[str, AfterValidator(is_name_of_method_on_self)]
|