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.
Files changed (45) hide show
  1. abxpkg/__init__.py +195 -0
  2. abxpkg/admin.py +45 -0
  3. abxpkg/apps.py +6 -0
  4. abxpkg/base_types.py +312 -0
  5. abxpkg/binary.py +530 -0
  6. abxpkg/binprovider.py +2121 -0
  7. abxpkg/binprovider_ansible.py +422 -0
  8. abxpkg/binprovider_apt.py +320 -0
  9. abxpkg/binprovider_bash.py +284 -0
  10. abxpkg/binprovider_brew.py +672 -0
  11. abxpkg/binprovider_bun.py +445 -0
  12. abxpkg/binprovider_cargo.py +258 -0
  13. abxpkg/binprovider_chromewebstore.py +255 -0
  14. abxpkg/binprovider_deno.py +295 -0
  15. abxpkg/binprovider_docker.py +324 -0
  16. abxpkg/binprovider_gem.py +261 -0
  17. abxpkg/binprovider_goget.py +273 -0
  18. abxpkg/binprovider_nix.py +245 -0
  19. abxpkg/binprovider_npm.py +691 -0
  20. abxpkg/binprovider_pip.py +646 -0
  21. abxpkg/binprovider_playwright.py +595 -0
  22. abxpkg/binprovider_pnpm.py +506 -0
  23. abxpkg/binprovider_puppeteer.py +662 -0
  24. abxpkg/binprovider_pyinfra.py +309 -0
  25. abxpkg/binprovider_uv.py +604 -0
  26. abxpkg/binprovider_yarn.py +533 -0
  27. abxpkg/cli.py +1289 -0
  28. abxpkg/config.py +196 -0
  29. abxpkg/exceptions.py +88 -0
  30. abxpkg/js/base/config.json +108 -0
  31. abxpkg/js/base/utils.js +402 -0
  32. abxpkg/js/chrome/chrome_utils.js +4147 -0
  33. abxpkg/js/chrome/config.json +248 -0
  34. abxpkg/logging.py +307 -0
  35. abxpkg/models.py +27 -0
  36. abxpkg/semver.py +145 -0
  37. abxpkg/settings.py +38 -0
  38. abxpkg/shallowbinary.py +5 -0
  39. abxpkg/tests.py +1 -0
  40. abxpkg/views.py +103 -0
  41. abxpkg-1.10.1.dist-info/METADATA +1325 -0
  42. abxpkg-1.10.1.dist-info/RECORD +45 -0
  43. abxpkg-1.10.1.dist-info/WHEEL +4 -0
  44. abxpkg-1.10.1.dist-info/entry_points.txt +3 -0
  45. 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
@@ -0,0 +1,6 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class AbxPkgConfig(AppConfig):
5
+ default_auto_field = "django.db.models.BigAutoField"
6
+ name = "abxpkg"
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)]