stdl 0.6.7__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.
stdl/import_lazy.py ADDED
@@ -0,0 +1,123 @@
1
+ import importlib
2
+ import sys
3
+ import time
4
+ from typing import Any
5
+
6
+
7
+ class LazyImport:
8
+ """A lazy import that loads the actual object on first access."""
9
+
10
+ def __init__(
11
+ self,
12
+ module_name: str,
13
+ attr_name: str | None = None,
14
+ verbose: bool = False,
15
+ ):
16
+ self.module_name = module_name
17
+ self.attr_name = attr_name
18
+ self.verbose = verbose
19
+
20
+ self._cached_value: Any = None
21
+ self._loaded = False
22
+
23
+ def _load(self) -> Any:
24
+ if not self._loaded:
25
+ if self.verbose:
26
+ start_time = time.time()
27
+
28
+ module_already_loaded = self.module_name in sys.modules
29
+ try:
30
+ module = importlib.import_module(self.module_name)
31
+ except ImportError as e:
32
+ if self.attr_name:
33
+ raise ImportError(
34
+ f"Cannot import '{self.attr_name}' from '{self.module_name}': {e}"
35
+ )
36
+ else:
37
+ raise ImportError(f"Cannot import module '{self.module_name}': {e}")
38
+
39
+ if self.attr_name:
40
+ try:
41
+ self._cached_value = getattr(module, self.attr_name)
42
+ except AttributeError:
43
+ raise ImportError(
44
+ f"Module '{self.module_name}' has no attribute '{self.attr_name}'"
45
+ )
46
+ else:
47
+ self._cached_value = module
48
+
49
+ if self.verbose and not module_already_loaded:
50
+ end_time = time.time()
51
+ import_time = end_time - start_time
52
+ import_target = (
53
+ f"{self.module_name}.{self.attr_name}" if self.attr_name else self.module_name
54
+ )
55
+
56
+ print(
57
+ f'importing "{import_target}" took {import_time:.3f}s',
58
+ file=sys.stderr,
59
+ )
60
+
61
+ self._loaded = True
62
+
63
+ return self._cached_value
64
+
65
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
66
+ return self._load()(*args, **kwargs)
67
+
68
+ def __getattr__(self, name: str) -> Any:
69
+ return getattr(self._load(), name)
70
+
71
+ def __setattr__(self, name: str, value: Any) -> None:
72
+ if name in ("module_name", "attr_name", "verbose", "_cached_value", "_loaded"):
73
+ super().__setattr__(name, value)
74
+ else:
75
+ setattr(self._load(), name, value)
76
+
77
+ def __repr__(self) -> str:
78
+ if self._loaded:
79
+ return repr(self._cached_value)
80
+ return f"<LazyImport: {self.module_name}{('.' + self.attr_name) if self.attr_name else ''}>"
81
+
82
+
83
+ def import_lazy(
84
+ module: str,
85
+ names: list[str] | None = None,
86
+ alias: str | None = None,
87
+ verbose: bool = False,
88
+ ) -> None:
89
+ """
90
+ Lazy import a module.
91
+
92
+ Args:
93
+ module (str): Module name to import
94
+ names (list[str] | None): List of specific names to import from the module. If None, imports the module itself.
95
+ alias (str | None): Alias to use for the imported module (only valid when names is None)
96
+ verbose (bool): If True, print timing information when imports actually happen
97
+
98
+ Example:
99
+ ```python
100
+ >>> import_lazy("module", ["name_1", "name_2"]) # from module import name_1, name_2
101
+ >>> import_lazy("module") # import module
102
+ >>> import_lazy("module", alias="mod") # import module as mod
103
+ >>> import_lazy("package.submodule", ["name_1"]) # from package.submodule import name_1
104
+ ```
105
+ """
106
+ frame = sys._getframe(1)
107
+ caller_globals = frame.f_globals
108
+
109
+ if names is None:
110
+ lazy_import = LazyImport(module, verbose=verbose)
111
+ if alias:
112
+ caller_globals[alias] = lazy_import
113
+ else:
114
+ caller_globals[module.split(".")[-1]] = lazy_import
115
+ else:
116
+ if alias:
117
+ raise ValueError("Cannot use alias with named imports")
118
+ for name in names:
119
+ lazy_import = LazyImport(module, name, verbose=verbose)
120
+ caller_globals[name] = lazy_import
121
+
122
+
123
+ __all__ = ["import_lazy", "LazyImport"]
stdl/log.py ADDED
@@ -0,0 +1,142 @@
1
+ from collections.abc import Callable
2
+ from shutil import get_terminal_size
3
+ from typing import Any, ClassVar
4
+
5
+ from stdl.st import ForegroundColor, colored
6
+
7
+ IGNORE_BREAKS = False
8
+
9
+
10
+ def color_tag(text: str, color: str) -> str:
11
+ return f"<{color}>{text}</{color}>"
12
+
13
+
14
+ class LoguruFormatter:
15
+ time = color_tag("{time:YYYY-MM-DD HH:mm:ss}", "green")
16
+ level = color_tag("{level: <8}", "level")
17
+ msg = color_tag("{message:<24}", "level")
18
+ name = color_tag("{name}", "light-blue")
19
+ func = color_tag("{function}", "light-blue")
20
+ lineno = color_tag("{line}", "light-blue")
21
+ extra_key_skips: ClassVar[list[str]] = ["title"]
22
+ extra_key_name_color: ForegroundColor = "white"
23
+
24
+ def format(self, record: dict[str, Any]) -> str:
25
+ """
26
+ Example:
27
+ ```python
28
+ >>> import sys
29
+ >>> from loguru import logger
30
+ >>> logger.remove()
31
+ >>> logger.add(sys.stdout, level="DEBUG", format=loguru_formater)
32
+ ```
33
+ """
34
+ extras = ""
35
+ if len(record["extra"]):
36
+ for key in record["extra"].keys():
37
+ if key in self.extra_key_skips:
38
+ continue
39
+ extras = (
40
+ extras
41
+ + colored(key, self.extra_key_name_color)
42
+ + "="
43
+ + "{extra["
44
+ + key
45
+ + "]}, "
46
+ )
47
+ extras = extras[:-2]
48
+
49
+ if title := record["extra"].get("title"):
50
+ return f"{self.time} | {self.level} | [ {title} ] {self.name}:{self.func}:{self.lineno} - {self.msg} {extras}\n"
51
+ return f"{self.time} | {self.level} | {self.name}:{self.func}:{self.lineno} - {self.msg} {extras}\n"
52
+
53
+
54
+ class SimpleLoguruFormatter(LoguruFormatter):
55
+ def format(self, record: dict[str, Any]) -> str:
56
+ extras = ""
57
+ if len(record["extra"]):
58
+ for key in record["extra"].keys():
59
+ if key in self.extra_key_skips:
60
+ continue
61
+ extras = (
62
+ extras
63
+ + colored(key, self.extra_key_name_color)
64
+ + "="
65
+ + "{extra["
66
+ + key
67
+ + "]}, "
68
+ )
69
+ extras = extras[:-2]
70
+
71
+ return f"{self.level} - {self.msg} {extras}\n"
72
+
73
+
74
+ loguru_formater = LoguruFormatter().format
75
+ simple_loguru_formater = SimpleLoguruFormatter().format
76
+
77
+
78
+ def get_logging_config(
79
+ format: str | None = None,
80
+ level: str = "INFO",
81
+ filename: str | None = None,
82
+ backup_count: int = 0,
83
+ disable_existing: bool = False,
84
+ console_handler: str = "logging.StreamHandler",
85
+ file_handler: str = "logging.handlers.TimedRotatingFileHandler",
86
+ format_style: str = "{",
87
+ ) -> dict[str, Any]:
88
+ format = (
89
+ format
90
+ or "{asctime} | {levelname:<8s} | [ {name} ] {module}.{funcName}:{lineno} - {message}"
91
+ )
92
+
93
+ config = {
94
+ "version": 1,
95
+ "disable_existing_loggers": disable_existing,
96
+ "formatters": {
97
+ "standard": {
98
+ "format": format,
99
+ "datefmt": "%Y-%m-%d %H:%M:%S",
100
+ "style": format_style,
101
+ }
102
+ },
103
+ "handlers": {
104
+ "console": {
105
+ "class": console_handler,
106
+ "level": level,
107
+ "formatter": "standard",
108
+ "stream": "ext://sys.stdout",
109
+ },
110
+ },
111
+ "root": {"level": level, "handlers": ["console"]},
112
+ }
113
+ if filename:
114
+ config["handlers"]["file"] = {
115
+ "class": file_handler,
116
+ "level": level,
117
+ "formatter": "standard",
118
+ "backupCount": backup_count,
119
+ "filename": filename,
120
+ }
121
+ config["root"]["handlers"].append("file")
122
+
123
+ return config
124
+
125
+
126
+ def br(
127
+ c: str = "_",
128
+ length: int | None = None,
129
+ handler: Callable[[str], None] = print,
130
+ *,
131
+ newline: bool = False,
132
+ ) -> None:
133
+ if IGNORE_BREAKS:
134
+ return
135
+ length = length or get_terminal_size().columns
136
+ line = c * length
137
+ if newline:
138
+ line += "\n"
139
+ handler(line)
140
+
141
+
142
+ __all__ = ["loguru_formater", "br", "get_logging_config", "LoguruFormatter"]
stdl/lst.py ADDED
@@ -0,0 +1,166 @@
1
+ from collections import Counter
2
+ from collections.abc import Sequence
3
+ from typing import Any, TypeVar
4
+
5
+ T = TypeVar("T")
6
+
7
+
8
+ def chunks(l: list[T], size: int) -> list[list[T]]:
9
+ """
10
+ Splits a list into chunks of a specified size.
11
+
12
+ Args:
13
+ l (list[T]): list to be split
14
+ size (int): Size of each chunk.
15
+
16
+ Returns:
17
+ list[list[T]] : a list containing the chunks.
18
+
19
+ Raises:
20
+ ValueError : if the size is less than 1 or greater than the length of the list
21
+ """
22
+ if size < 1 or size > len(l):
23
+ raise ValueError(size)
24
+ return [l[i : i + size] for i in range(0, len(l), size)]
25
+
26
+
27
+ def split(l: list[T], parts: int) -> list[list[T]]:
28
+ """
29
+ Splits a list into a specified number of parts
30
+
31
+ Args:
32
+ l (list[T]): List to be split
33
+ parts (int): Number of parts to split the list into.
34
+
35
+ Returns:
36
+ list[list[T]] : a list containing the split parts.
37
+
38
+ Raises:
39
+ ValueError : if the parts is less than 1 or greater than the length of the list.
40
+ """
41
+ if parts < 1 or parts > len(l):
42
+ raise ValueError(parts)
43
+
44
+ def inner(l: list[T], parts: int) -> list[list[T]]:
45
+ length = len(l)
46
+ si = length // parts
47
+ if length - si > 0:
48
+ return [l[:si], *inner(l[si:], parts - 1)]
49
+ else:
50
+ return [l]
51
+
52
+ return inner(l, parts)
53
+
54
+
55
+ def unique(l: Sequence[T]) -> list[T]:
56
+ """
57
+ Filter out all non-unique values from a list.
58
+
59
+ Args:
60
+ l (Sequence[T]): The list to filter.
61
+
62
+ Returns:
63
+ list[T]: A list of only unique values.
64
+ """
65
+ return [i for i, count in Counter(l).items() if count == 1]
66
+
67
+
68
+ def non_unique(l: Sequence[T]) -> list[T]:
69
+ """
70
+ Filter out all unique values from a list.
71
+
72
+ Args:
73
+ l (Sequence[T]): The list to filter.
74
+
75
+ Returns:
76
+ list[T]: A list of only non-unique values.
77
+ """
78
+ return [i for i, count in Counter(l).items() if count > 1]
79
+
80
+
81
+ def every_nth(l: list[T], n: int) -> list[T]:
82
+ """
83
+ Return every nth element from a list
84
+ Args:
85
+ l (list[T]): The list to extract elements from.
86
+ n (int): The index to extract the element at
87
+ Returns:
88
+ list[T]: A list of every nth element
89
+ """
90
+ return l[n - 1 :: n]
91
+
92
+
93
+ def occurrences(l: Sequence[T], val: Any) -> int:
94
+ """
95
+ Count the number of occurrences of a value in a list
96
+ Args:
97
+ l (Sequence[T]): The list to count values in.
98
+ val (Any): The value to count
99
+ Returns:
100
+ int: Number of occurrences
101
+ """
102
+ return len([i for i in l if i == val and type(i) is type(val)])
103
+
104
+
105
+ def nodups(l: Sequence[T]) -> list[T]:
106
+ """Remove duplicates from a list and maintain the order"""
107
+ result: list[T] = []
108
+ for i in l:
109
+ if i not in result:
110
+ result.append(i)
111
+ return result
112
+
113
+
114
+ def replace_sublists(l: list[Any], sublist: list[Any], replacement: list[Any]) -> list[Any]:
115
+ """
116
+ Replaces all occurrences of sublist `sublist` with `replacement` in `main`.
117
+
118
+ Args:
119
+ l: The list in which to search for sublists.
120
+ sublist: The sublist to replace.
121
+ replacement: The sublist to replace `sublist` with.
122
+
123
+ Example:
124
+ >>> replace_all_sublist([1, 2, 3, 2, 3], [2, 3], [5])
125
+ [1, 5, 5]
126
+ """
127
+ result = []
128
+ i = 0
129
+ len_old = len(sublist)
130
+ while i <= len(l) - len_old:
131
+ if l[i : i + len_old] == sublist:
132
+ result.extend(replacement)
133
+ i += len_old
134
+ else:
135
+ result.append(l[i])
136
+ i += 1
137
+ result.extend(l[i:])
138
+ return result
139
+
140
+
141
+ def contains_sublist(l: list[Any], sublist: list[Any]) -> bool:
142
+ """
143
+ Checks if list contains a sublist.
144
+
145
+ Args:
146
+ l: The list to search.
147
+ sublist: The sublist to search for.
148
+
149
+ """
150
+ len_b = len(sublist)
151
+ for i in range(len(l) - len_b + 1):
152
+ if l[i : i + len_b] == sublist:
153
+ return True
154
+ return False
155
+
156
+
157
+ __all__ = [
158
+ "unique",
159
+ "non_unique",
160
+ "every_nth",
161
+ "occurrences",
162
+ "chunks",
163
+ "split",
164
+ "replace_sublists",
165
+ "contains_sublist",
166
+ ]
stdl/net.py ADDED
@@ -0,0 +1,80 @@
1
+ import os
2
+ import warnings
3
+ from http.client import HTTPMessage
4
+ from urllib.request import urlopen, urlretrieve
5
+
6
+ try:
7
+ from tqdm import tqdm
8
+ except ImportError: # pragma: no cover - optional dependency
9
+ tqdm = None
10
+
11
+ from stdl.fs import bytes_readable, readable_size_to_bytes
12
+
13
+ if tqdm is not None:
14
+
15
+ class ProgressBarTQDM(tqdm):
16
+ def update_to(self, b: int = 1, bsize: int = 1, tsize: int | None = None) -> None:
17
+ if tsize is not None:
18
+ self.total = tsize
19
+ self.update(b * bsize - self.n)
20
+ else:
21
+ ProgressBarTQDM = None
22
+
23
+
24
+ class DownloadSizeExceededError(Exception):
25
+ def __init__(self, filesize: int, maxsize: int):
26
+ self.filesize = filesize
27
+ self.maxsize = maxsize
28
+
29
+ def __str__(self) -> str:
30
+ return f"File size {self.filesize} ({bytes_readable(self.filesize)}) exceeds maximum size limit of {self.maxsize} bytes ({bytes_readable(self.maxsize)})"
31
+
32
+
33
+ def download(
34
+ url: str,
35
+ path: str,
36
+ *,
37
+ maxsize: int | str | None = None,
38
+ progressbar: bool = False,
39
+ overwrite: bool = False,
40
+ ) -> tuple[str, HTTPMessage]:
41
+ """
42
+ Download a file
43
+
44
+ Args:
45
+ url (str): File URL
46
+ path (str): Save path
47
+ maxsize (int | str | None, optional): Maximum file size in bytes or human readable format.
48
+ progressbar (bool, optional): Display progress bar in console.
49
+ overwrite (bool, optional): Overwrite destination path if it already exists.
50
+
51
+ Raises:
52
+ FileExistsError: if path already exists and overwrite is set to False
53
+ DownloadSizeExceededError: if file size exceeds maxsize
54
+ """
55
+ if maxsize is not None:
56
+ if isinstance(maxsize, str):
57
+ maxsize = readable_size_to_bytes(maxsize)
58
+ filesize = int(urlopen(url).headers.get("Content-Length", 0))
59
+ if filesize > maxsize:
60
+ raise DownloadSizeExceededError(filesize, maxsize)
61
+
62
+ if os.path.exists(path) and not overwrite:
63
+ raise FileExistsError(path)
64
+
65
+ if progressbar and ProgressBarTQDM is not None:
66
+ with ProgressBarTQDM(unit="B", unit_scale=True, unit_divisor=1024) as t:
67
+ r = urlretrieve(url, path, reporthook=t.update_to, data=None)
68
+ t.total = t.n
69
+ else:
70
+ if progressbar and ProgressBarTQDM is None:
71
+ warnings.warn(
72
+ "progressbar=True requested but tqdm is not installed; downloading without progress bar.",
73
+ RuntimeWarning,
74
+ stacklevel=2,
75
+ )
76
+ r = urlretrieve(url, path)
77
+ return r
78
+
79
+
80
+ __all__ = ["download"]