absfuyu 5.6.1__py3-none-any.whl → 6.1.2__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.
Potentially problematic release.
This version of absfuyu might be problematic. Click here for more details.
- absfuyu/__init__.py +5 -3
- absfuyu/__main__.py +2 -2
- absfuyu/cli/__init__.py +13 -2
- absfuyu/cli/audio_group.py +98 -0
- absfuyu/cli/color.py +2 -2
- absfuyu/cli/config_group.py +2 -2
- absfuyu/cli/do_group.py +2 -2
- absfuyu/cli/game_group.py +20 -2
- absfuyu/cli/tool_group.py +68 -4
- absfuyu/config/__init__.py +3 -3
- absfuyu/core/__init__.py +10 -6
- absfuyu/core/baseclass.py +104 -34
- absfuyu/core/baseclass2.py +43 -2
- absfuyu/core/decorator.py +2 -2
- absfuyu/core/docstring.py +4 -2
- absfuyu/core/dummy_cli.py +3 -3
- absfuyu/core/dummy_func.py +2 -2
- absfuyu/dxt/__init__.py +2 -2
- absfuyu/dxt/base_type.py +93 -0
- absfuyu/dxt/dictext.py +188 -6
- absfuyu/dxt/dxt_support.py +2 -2
- absfuyu/dxt/intext.py +72 -4
- absfuyu/dxt/listext.py +495 -23
- absfuyu/dxt/strext.py +2 -2
- absfuyu/extra/__init__.py +2 -2
- absfuyu/extra/audio/__init__.py +8 -0
- absfuyu/extra/audio/_util.py +57 -0
- absfuyu/extra/audio/convert.py +192 -0
- absfuyu/extra/audio/lossless.py +281 -0
- absfuyu/extra/beautiful.py +2 -2
- absfuyu/extra/da/__init__.py +39 -3
- absfuyu/extra/da/dadf.py +436 -29
- absfuyu/extra/da/dadf_base.py +2 -2
- absfuyu/extra/da/df_func.py +89 -5
- absfuyu/extra/da/mplt.py +2 -2
- absfuyu/extra/ggapi/__init__.py +8 -0
- absfuyu/extra/ggapi/gdrive.py +223 -0
- absfuyu/extra/ggapi/glicense.py +148 -0
- absfuyu/extra/ggapi/glicense_df.py +186 -0
- absfuyu/extra/ggapi/gsheet.py +88 -0
- absfuyu/extra/img/__init__.py +30 -0
- absfuyu/extra/img/converter.py +402 -0
- absfuyu/extra/img/dup_check.py +291 -0
- absfuyu/extra/pdf.py +4 -6
- absfuyu/extra/rclone.py +253 -0
- absfuyu/extra/xml.py +90 -0
- absfuyu/fun/__init__.py +2 -20
- absfuyu/fun/rubik.py +2 -2
- absfuyu/fun/tarot.py +2 -2
- absfuyu/game/__init__.py +2 -2
- absfuyu/game/game_stat.py +2 -2
- absfuyu/game/schulte.py +78 -0
- absfuyu/game/sudoku.py +2 -2
- absfuyu/game/tictactoe.py +2 -2
- absfuyu/game/wordle.py +6 -4
- absfuyu/general/__init__.py +2 -2
- absfuyu/general/content.py +2 -2
- absfuyu/general/human.py +2 -2
- absfuyu/general/resrel.py +213 -0
- absfuyu/general/shape.py +3 -8
- absfuyu/general/tax.py +344 -0
- absfuyu/logger.py +806 -59
- absfuyu/numbers/__init__.py +13 -0
- absfuyu/numbers/number_to_word.py +321 -0
- absfuyu/numbers/shorten_number.py +303 -0
- absfuyu/numbers/time_duration.py +217 -0
- absfuyu/pkg_data/__init__.py +2 -2
- absfuyu/pkg_data/deprecated.py +2 -2
- absfuyu/pkg_data/logo.py +1462 -0
- absfuyu/sort.py +4 -4
- absfuyu/tools/__init__.py +2 -2
- absfuyu/tools/checksum.py +119 -4
- absfuyu/tools/converter.py +2 -2
- absfuyu/tools/generator.py +24 -7
- absfuyu/tools/inspector.py +2 -2
- absfuyu/tools/keygen.py +2 -2
- absfuyu/tools/obfuscator.py +2 -2
- absfuyu/tools/passwordlib.py +2 -2
- absfuyu/tools/shutdownizer.py +3 -8
- absfuyu/tools/sw.py +213 -10
- absfuyu/tools/web.py +10 -13
- absfuyu/typings.py +5 -8
- absfuyu/util/__init__.py +31 -2
- absfuyu/util/api.py +7 -4
- absfuyu/util/cli.py +119 -0
- absfuyu/util/gui.py +91 -0
- absfuyu/util/json_method.py +2 -2
- absfuyu/util/lunar.py +2 -2
- absfuyu/util/package.py +124 -0
- absfuyu/util/path.py +313 -4
- absfuyu/util/performance.py +2 -2
- absfuyu/util/shorten_number.py +206 -13
- absfuyu/util/text_table.py +2 -2
- absfuyu/util/zipped.py +2 -2
- absfuyu/version.py +22 -19
- {absfuyu-5.6.1.dist-info → absfuyu-6.1.2.dist-info}/METADATA +37 -8
- absfuyu-6.1.2.dist-info/RECORD +105 -0
- {absfuyu-5.6.1.dist-info → absfuyu-6.1.2.dist-info}/WHEEL +1 -1
- absfuyu/extra/data_analysis.py +0 -21
- absfuyu-5.6.1.dist-info/RECORD +0 -79
- {absfuyu-5.6.1.dist-info → absfuyu-6.1.2.dist-info}/entry_points.txt +0 -0
- {absfuyu-5.6.1.dist-info → absfuyu-6.1.2.dist-info}/licenses/LICENSE +0 -0
absfuyu/typings.py
CHANGED
|
@@ -3,8 +3,8 @@ Absfuyu: Core
|
|
|
3
3
|
-------------
|
|
4
4
|
Pre-defined typing
|
|
5
5
|
|
|
6
|
-
Version:
|
|
7
|
-
Date updated: 12/
|
|
6
|
+
Version: 6.1.1
|
|
7
|
+
Date updated: 30/12/2025 (dd/mm/yyyy)
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
# Module Package
|
|
@@ -33,21 +33,18 @@ __all__ = [
|
|
|
33
33
|
from collections.abc import Callable
|
|
34
34
|
from typing import Any, ParamSpec, Protocol, TypeVar, overload
|
|
35
35
|
|
|
36
|
-
try:
|
|
37
|
-
from typing import override # type: ignore
|
|
38
|
-
except ImportError:
|
|
39
|
-
from absfuyu.core.decorator import dummy_decorator as override
|
|
40
|
-
|
|
41
|
-
|
|
42
36
|
# Type
|
|
43
37
|
# ---------------------------------------------------------------------------
|
|
44
38
|
# Types where neither is possible are invariant
|
|
39
|
+
# Type safety must be strict
|
|
45
40
|
T = TypeVar("T") # Type invariant
|
|
46
41
|
# Type variables that are covariant can be substituted
|
|
47
42
|
# with a more specific type without causing errors
|
|
43
|
+
# Safe to treat a Box[int] as a Box[object]
|
|
48
44
|
T_co = TypeVar("T_co", covariant=True) # Type covariant
|
|
49
45
|
# Type variables that are contravariant can be substituted
|
|
50
46
|
# with a more general type without causing errors
|
|
47
|
+
# Safe to use a Box[object] where a Box[int] is needed
|
|
51
48
|
T_contra = TypeVar("T_contra", contravariant=True) # Type contravariant
|
|
52
49
|
|
|
53
50
|
KT = TypeVar("KT")
|
absfuyu/util/__init__.py
CHANGED
|
@@ -3,8 +3,8 @@ Absufyu: Utilities
|
|
|
3
3
|
------------------
|
|
4
4
|
Some random utilities
|
|
5
5
|
|
|
6
|
-
Version:
|
|
7
|
-
Date updated: 12/
|
|
6
|
+
Version: 6.1.1
|
|
7
|
+
Date updated: 30/12/2025 (dd/mm/yyyy)
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
# Module Package
|
|
@@ -25,6 +25,7 @@ __all__ = [
|
|
|
25
25
|
# Library
|
|
26
26
|
# ---------------------------------------------------------------------------
|
|
27
27
|
import pkgutil
|
|
28
|
+
import subprocess
|
|
28
29
|
from datetime import datetime
|
|
29
30
|
from string import printable
|
|
30
31
|
|
|
@@ -257,3 +258,31 @@ def convert_to_raw_unicode(text: str, partial: bool = True) -> str:
|
|
|
257
258
|
character if character in character_set and partial else _convert(character)
|
|
258
259
|
for character in text
|
|
259
260
|
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@versionadded("5.9.0")
|
|
264
|
+
def is_command_available(cmd: list[str] | str, err_msg: str = "") -> None:
|
|
265
|
+
"""
|
|
266
|
+
Checks if the desired command available
|
|
267
|
+
|
|
268
|
+
Parameters
|
|
269
|
+
----------
|
|
270
|
+
cmd : list[str] | str
|
|
271
|
+
Command to check
|
|
272
|
+
|
|
273
|
+
err_msg : str, optional
|
|
274
|
+
Custom error message, by default ""
|
|
275
|
+
|
|
276
|
+
Raises
|
|
277
|
+
------
|
|
278
|
+
ValueError
|
|
279
|
+
When command is unvailable
|
|
280
|
+
"""
|
|
281
|
+
try:
|
|
282
|
+
subprocess.run(
|
|
283
|
+
cmd,
|
|
284
|
+
stdout=subprocess.DEVNULL,
|
|
285
|
+
stderr=subprocess.DEVNULL,
|
|
286
|
+
)
|
|
287
|
+
except Exception:
|
|
288
|
+
raise ValueError(err_msg)
|
absfuyu/util/api.py
CHANGED
|
@@ -3,8 +3,8 @@ Absufyu: API
|
|
|
3
3
|
------------
|
|
4
4
|
Fetch data stuff
|
|
5
5
|
|
|
6
|
-
Version:
|
|
7
|
-
Date updated: 12/
|
|
6
|
+
Version: 6.1.1
|
|
7
|
+
Date updated: 30/12/2025 (dd/mm/yyyy)
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
# Module level
|
|
@@ -23,12 +23,15 @@ import subprocess
|
|
|
23
23
|
from pathlib import Path
|
|
24
24
|
from typing import NamedTuple
|
|
25
25
|
|
|
26
|
-
import requests
|
|
27
|
-
|
|
28
26
|
from absfuyu.core.baseclass import BaseClass
|
|
29
27
|
from absfuyu.core.docstring import versionadded, versionchanged
|
|
30
28
|
from absfuyu.logger import logger
|
|
31
29
|
|
|
30
|
+
try:
|
|
31
|
+
import requests
|
|
32
|
+
except ImportError:
|
|
33
|
+
raise ImportError("Please install requests package")
|
|
34
|
+
|
|
32
35
|
|
|
33
36
|
# Function
|
|
34
37
|
# ---------------------------------------------------------------------------
|
absfuyu/util/cli.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Absfuyu: CLI
|
|
3
|
+
------------
|
|
4
|
+
Custom Argument Parser
|
|
5
|
+
|
|
6
|
+
Version: 6.1.1
|
|
7
|
+
Date updated: 30/12/2025 (dd/mm/yyyy)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# Module level
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
__all__ = ["FuyuArgumentParser"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Library
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
import logging
|
|
18
|
+
import sys
|
|
19
|
+
from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Class
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
class FuyuArgumentParser(ArgumentParser):
|
|
27
|
+
"""
|
|
28
|
+
Usage:
|
|
29
|
+
------
|
|
30
|
+
>>> import sys
|
|
31
|
+
>>> args = FuyuArgumentParser()
|
|
32
|
+
>>> args.start()
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
prog: str | None = None,
|
|
38
|
+
description: str | None = None,
|
|
39
|
+
epilog: str | None = None,
|
|
40
|
+
version: str = "0.0.1",
|
|
41
|
+
formatter_class: Any = ArgumentDefaultsHelpFormatter,
|
|
42
|
+
prefix_chars: str = "-",
|
|
43
|
+
argument_default: Any = None,
|
|
44
|
+
conflict_handler: str = "error",
|
|
45
|
+
add_help: bool = True,
|
|
46
|
+
allow_abbrev: bool = True,
|
|
47
|
+
exit_on_error: bool = True,
|
|
48
|
+
) -> None:
|
|
49
|
+
# Desc
|
|
50
|
+
if description is None:
|
|
51
|
+
description = f"Absfuyu CLI {version}"
|
|
52
|
+
|
|
53
|
+
# Default
|
|
54
|
+
self._default_args = ["--help"]
|
|
55
|
+
|
|
56
|
+
# Super
|
|
57
|
+
super().__init__(
|
|
58
|
+
prog=prog,
|
|
59
|
+
description=description,
|
|
60
|
+
epilog=epilog,
|
|
61
|
+
formatter_class=formatter_class,
|
|
62
|
+
prefix_chars=prefix_chars,
|
|
63
|
+
argument_default=argument_default,
|
|
64
|
+
conflict_handler=conflict_handler,
|
|
65
|
+
add_help=add_help,
|
|
66
|
+
allow_abbrev=allow_abbrev,
|
|
67
|
+
exit_on_error=exit_on_error,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Add version
|
|
71
|
+
self.add_argument("-v", "--version", action="version", version=f"%(prog)s {version}")
|
|
72
|
+
|
|
73
|
+
# Add log level
|
|
74
|
+
_ll_val = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
|
75
|
+
self.add_argument(
|
|
76
|
+
"-ll",
|
|
77
|
+
"--log-level",
|
|
78
|
+
metavar="LOG_LEVEL",
|
|
79
|
+
dest="log_level",
|
|
80
|
+
choices=_ll_val,
|
|
81
|
+
default="ERROR",
|
|
82
|
+
help=f"Log level: {', '.join(_ll_val)}",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def _fuyu_set_log_level(self, log_level: str = "ERROR") -> None:
|
|
86
|
+
log_levels = {
|
|
87
|
+
"DEBUG": logging.DEBUG,
|
|
88
|
+
"INFO": logging.INFO,
|
|
89
|
+
"WARNING": logging.WARNING,
|
|
90
|
+
"ERROR": logging.ERROR,
|
|
91
|
+
"CRITICAL": logging.CRITICAL,
|
|
92
|
+
}
|
|
93
|
+
logging.basicConfig(
|
|
94
|
+
level=log_levels[log_level],
|
|
95
|
+
format="[%(asctime)s] [%(module)s] [%(name)s] [%(funcName)s] [%(levelname)-s] %(message)s",
|
|
96
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def start(self, default: Callable[[], Any] | None = None):
|
|
100
|
+
"""
|
|
101
|
+
Quick start for argument parser
|
|
102
|
+
|
|
103
|
+
Parameters
|
|
104
|
+
----------
|
|
105
|
+
default : Callable[[], Any] | None, optional
|
|
106
|
+
| Default callable to run, by default ``None``
|
|
107
|
+
| Shows help on default behavior (default=None)
|
|
108
|
+
|
|
109
|
+
Returns
|
|
110
|
+
-------
|
|
111
|
+
Namespace
|
|
112
|
+
Namespace
|
|
113
|
+
"""
|
|
114
|
+
if default is None:
|
|
115
|
+
args = self.parse_args(args=None if sys.argv[1:] else self._default_args)
|
|
116
|
+
else:
|
|
117
|
+
args = self.parse_args(args=None if sys.argv[1:] else default())
|
|
118
|
+
self._fuyu_set_log_level(getattr(args, "log_level", "ERROR"))
|
|
119
|
+
return args
|
absfuyu/util/gui.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Absfuyu: GUI
|
|
3
|
+
------------
|
|
4
|
+
Custom tkinter GUI
|
|
5
|
+
|
|
6
|
+
Version: 6.1.1
|
|
7
|
+
Date updated: 30/12/2025 (dd/mm/yyyy)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# Module level
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
__all__ = ["CustomTkinterApp"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Library
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
import tkinter as tk
|
|
18
|
+
|
|
19
|
+
from absfuyu.pkg_data.logo import AbsfuyuLogo
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Class
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
class CustomTkinterApp(tk.Tk):
|
|
25
|
+
|
|
26
|
+
def __init__(self, title: str | None = None, size: tuple[int, int] | None = None) -> None:
|
|
27
|
+
"""
|
|
28
|
+
Custom Tkinter GUI
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
title : str | None, optional
|
|
33
|
+
Title of the app, by default None
|
|
34
|
+
|
|
35
|
+
size : tuple[int, int] | None, optional
|
|
36
|
+
Size of the app (width, height), by default None
|
|
37
|
+
"""
|
|
38
|
+
super().__init__()
|
|
39
|
+
|
|
40
|
+
# Set custom icon
|
|
41
|
+
self.iconphoto(True, tk.PhotoImage(data=AbsfuyuLogo.SHORT))
|
|
42
|
+
|
|
43
|
+
# Title
|
|
44
|
+
self.title(title)
|
|
45
|
+
|
|
46
|
+
# Set size
|
|
47
|
+
self._absfuyu_set_width_height(size)
|
|
48
|
+
|
|
49
|
+
# @versionadded("5.10.0")
|
|
50
|
+
def _absfuyu_set_width_height(self, size: tuple[int, int] | None = None) -> None:
|
|
51
|
+
"""
|
|
52
|
+
Set width and height for the app.
|
|
53
|
+
|
|
54
|
+
Parameters
|
|
55
|
+
----------
|
|
56
|
+
size : tuple[int, int] | None, optional
|
|
57
|
+
Size of the app (width, height), by default ``None``
|
|
58
|
+
(420 x 250)
|
|
59
|
+
"""
|
|
60
|
+
# Set GUI appears in center
|
|
61
|
+
if size is None:
|
|
62
|
+
_width = 420
|
|
63
|
+
_height = 250
|
|
64
|
+
else:
|
|
65
|
+
_width, _height = size
|
|
66
|
+
|
|
67
|
+
_width_screen = self.winfo_screenwidth()
|
|
68
|
+
_width_screen_offset = 0.0052 # x offset mutiplier
|
|
69
|
+
_x_offset = int(_width_screen * _width_screen_offset)
|
|
70
|
+
|
|
71
|
+
_height_screen = self.winfo_screenheight()
|
|
72
|
+
_height_screen_offset = 0.0926 # y offset mutiplier
|
|
73
|
+
_y_offset = int(_height_screen * _height_screen_offset)
|
|
74
|
+
|
|
75
|
+
if _width > _width_screen:
|
|
76
|
+
_width = int(_width_screen * (1 - 0.001))
|
|
77
|
+
# _x_offset = 0
|
|
78
|
+
|
|
79
|
+
if _height > (_height_screen - _y_offset):
|
|
80
|
+
_height = int(_height_screen * (1 - 0.001))
|
|
81
|
+
_y_offset = 0
|
|
82
|
+
|
|
83
|
+
_x = (_width_screen / 2) - (_width / 2) - _x_offset
|
|
84
|
+
_y = (_height_screen / 2) - (_height / 2) - _y_offset
|
|
85
|
+
|
|
86
|
+
self.geometry(f"{_width}x{_height}+{int(_x)}+{int(_y)}")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
app = CustomTkinterApp("absfuyu")
|
|
91
|
+
app.mainloop()
|
absfuyu/util/json_method.py
CHANGED
absfuyu/util/lunar.py
CHANGED
absfuyu/util/package.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Absfuyu: Package
|
|
3
|
+
----------------
|
|
4
|
+
Package related
|
|
5
|
+
|
|
6
|
+
Version: 6.1.1
|
|
7
|
+
Date updated: 30/12/2025 (dd/mm/yyyy)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# Module level
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
__all__ = ["PackageManager"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Library
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
import ensurepip
|
|
18
|
+
import importlib
|
|
19
|
+
import importlib.metadata
|
|
20
|
+
import importlib.util
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
from absfuyu.core.baseclass import BaseClass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Class
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
class PackageManager(BaseClass):
|
|
30
|
+
"""
|
|
31
|
+
Utility class for checking, installing, and importing Python packages safely.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
package: str,
|
|
37
|
+
import_name: str | None = None,
|
|
38
|
+
auto_upgrade: bool = False,
|
|
39
|
+
auto_bootstrap: bool = True,
|
|
40
|
+
) -> None:
|
|
41
|
+
"""
|
|
42
|
+
A configurable package manager utility that can check, install, and import Python packages safely.
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
package : str
|
|
47
|
+
The pip package name (e.g. "absfuyu", "random").
|
|
48
|
+
|
|
49
|
+
import_name : str | None
|
|
50
|
+
The importable module name if different from the package name (e.g. "PIL" for "pillow").
|
|
51
|
+
|
|
52
|
+
auto_upgrade : bool, by default ``False``
|
|
53
|
+
Whether to automatically upgrade packages during installation.
|
|
54
|
+
|
|
55
|
+
auto_bootstrap : bool, by default ``True``
|
|
56
|
+
Whether to automatically bootstrap pip using ensurepip if it's missing.
|
|
57
|
+
"""
|
|
58
|
+
self.package = package
|
|
59
|
+
self.import_name = import_name
|
|
60
|
+
self.auto_upgrade = auto_upgrade
|
|
61
|
+
self.auto_bootstrap = auto_bootstrap
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def version(self) -> str | None:
|
|
65
|
+
"""
|
|
66
|
+
Version of package if available
|
|
67
|
+
|
|
68
|
+
Returns
|
|
69
|
+
-------
|
|
70
|
+
str
|
|
71
|
+
Version of package
|
|
72
|
+
|
|
73
|
+
None
|
|
74
|
+
When package is not available
|
|
75
|
+
"""
|
|
76
|
+
try:
|
|
77
|
+
return importlib.metadata.version(self.package or self.import_name)
|
|
78
|
+
except importlib.metadata.PackageNotFoundError:
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
def _run_pip_install(self, package: str) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Internal helper to safely run pip install, bootstrapping pip if needed.
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
import pip # noqa: F401
|
|
87
|
+
except ImportError:
|
|
88
|
+
if self.auto_bootstrap:
|
|
89
|
+
ensurepip.bootstrap()
|
|
90
|
+
else:
|
|
91
|
+
raise RuntimeError("pip not found and auto_bootstrap is disabled.")
|
|
92
|
+
|
|
93
|
+
cmd = [sys.executable, "-m", "pip", "install", package]
|
|
94
|
+
if self.auto_upgrade:
|
|
95
|
+
cmd.append("--upgrade")
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
subprocess.check_call(cmd)
|
|
99
|
+
except subprocess.CalledProcessError as e:
|
|
100
|
+
raise RuntimeError(f"Failed to install {package}: {e}")
|
|
101
|
+
|
|
102
|
+
def ensure_installed(self) -> None:
|
|
103
|
+
"""
|
|
104
|
+
Ensure a Python package is installed. Installs (and optionally upgrades) it if missing.
|
|
105
|
+
"""
|
|
106
|
+
module_name = self.import_name or self.package
|
|
107
|
+
|
|
108
|
+
if importlib.util.find_spec(module_name) is None:
|
|
109
|
+
self._run_pip_install(self.package)
|
|
110
|
+
|
|
111
|
+
def ensure_import(self):
|
|
112
|
+
"""
|
|
113
|
+
Ensure a Python package is importable, installing it if necessary.
|
|
114
|
+
Returns the imported module.
|
|
115
|
+
"""
|
|
116
|
+
module_name = self.import_name or self.package
|
|
117
|
+
|
|
118
|
+
if importlib.util.find_spec(module_name) is not None:
|
|
119
|
+
return importlib.import_module(module_name)
|
|
120
|
+
|
|
121
|
+
self.logger.info(f"Installing missing package: {self.package} ...")
|
|
122
|
+
|
|
123
|
+
self._run_pip_install(self.package)
|
|
124
|
+
return importlib.import_module(module_name)
|