python-backpack 2.0.3__py3-none-any.whl → 2.0.5__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.
backpack/cache.py CHANGED
@@ -1,93 +1,93 @@
1
- # ----------------------------------------------------------------------------------------
2
- # Python-Backpack - Pattern Utilities
3
- # Maximiliano Rocamora / maxirocamora@gmail.com
4
- # https://github.com/MaxRocamora/python-backpack
5
- # ----------------------------------------------------------------------------------------
6
- from datetime import datetime, timedelta, timezone
7
- from functools import lru_cache, wraps
8
- from typing import Any, Callable, Protocol, TypeVar, cast
9
-
10
- from backpack.logger import get_logger
11
-
12
- log = get_logger('Python Backpack - Cache')
13
-
14
- R = TypeVar('R', covariant=True)
15
-
16
-
17
- class TimedCachedCallable(Protocol[R]):
18
- """Callable returned by timed_lru_cache with cache control kwargs."""
19
-
20
- def __call__(
21
- self,
22
- *args: Any,
23
- force_clear: bool = False,
24
- show_log: bool = False,
25
- **kwargs: Any,
26
- ) -> R:
27
- """Call the cached function, optionally forcing a cache clear first."""
28
- ...
29
-
30
-
31
- def timed_lru_cache(
32
- seconds: int,
33
- maxsize: int = 128,
34
- ) -> Callable[[Callable[..., R]], TimedCachedCallable[R]]:
35
- """Lru_cache with expiration time.
36
-
37
- Args:
38
- seconds (int): expiration time in seconds
39
- maxsize (int): maxsize for lru_cache
40
- Note:
41
- Wrapped function can be forced to clear cache with: force_clear=True
42
- Wrapped function can show log on clear with: show_log=True
43
- Returns:
44
- function result
45
-
46
- # * Usage:
47
-
48
- # * Add the decorator to your function
49
-
50
- @timed_lru_cache(seconds=60)
51
- def my_function():
52
- return 'Hello World'
53
-
54
- # * to clear the cache, use force_clear=True on the function call
55
- my_function(force_clear=True)
56
-
57
- """
58
-
59
- def wrapper_cache(func: Callable[..., R]) -> TimedCachedCallable[R]:
60
- cached_func = cast(Any, lru_cache(maxsize=maxsize)(func))
61
- cached_func.lifetime = timedelta(seconds=seconds)
62
- cached_func.expiration = datetime.now(timezone.utc) + cached_func.lifetime
63
-
64
- @wraps(func)
65
- def wrapped_func(
66
- *args: Any,
67
- force_clear: bool = False,
68
- show_log: bool = False,
69
- **kwargs: Any,
70
- ) -> R:
71
- """Wrapper function for lru_cache with expiration time.
72
-
73
- Args:
74
- *args: function arguments
75
- force_clear (bool): forces a clear cache
76
- show_log (bool): show log on clear
77
- **kwargs: function keyword arguments
78
- Returns:
79
- function result
80
- """
81
-
82
- if force_clear or datetime.now(timezone.utc) >= cached_func.expiration:
83
- if show_log:
84
- log.debug(f'Cache cleared for {cached_func.__name__}')
85
-
86
- cached_func.cache_clear()
87
- cached_func.expiration = datetime.now(timezone.utc) + cached_func.lifetime
88
-
89
- return cached_func(*args, **kwargs)
90
-
91
- return cast(TimedCachedCallable[R], wrapped_func)
92
-
93
- return wrapper_cache
1
+ # ----------------------------------------------------------------------------------------
2
+ # Python-Backpack - Pattern Utilities
3
+ # Maximiliano Rocamora / maxirocamora@gmail.com
4
+ # https://github.com/MaxRocamora/python-backpack
5
+ # ----------------------------------------------------------------------------------------
6
+ from datetime import datetime, timedelta, timezone
7
+ from functools import lru_cache, wraps
8
+ from typing import Any, Callable, Protocol, TypeVar, cast
9
+
10
+ from backpack.logger import get_logger
11
+
12
+ log = get_logger('Python Backpack - Cache')
13
+
14
+ R = TypeVar('R', covariant=True)
15
+
16
+
17
+ class TimedCachedCallable(Protocol[R]):
18
+ """Callable returned by timed_lru_cache with cache control kwargs."""
19
+
20
+ def __call__(
21
+ self,
22
+ *args: Any,
23
+ force_clear: bool = False,
24
+ show_log: bool = False,
25
+ **kwargs: Any,
26
+ ) -> R:
27
+ """Call the cached function, optionally forcing a cache clear first."""
28
+ ...
29
+
30
+
31
+ def timed_lru_cache(
32
+ seconds: int,
33
+ maxsize: int = 128,
34
+ ) -> Callable[[Callable[..., R]], TimedCachedCallable[R]]:
35
+ """Lru_cache with expiration time.
36
+
37
+ Args:
38
+ seconds (int): expiration time in seconds
39
+ maxsize (int): maxsize for lru_cache
40
+ Note:
41
+ Wrapped function can be forced to clear cache with: force_clear=True
42
+ Wrapped function can show log on clear with: show_log=True
43
+ Returns:
44
+ function result
45
+
46
+ # * Usage:
47
+
48
+ # * Add the decorator to your function
49
+
50
+ @timed_lru_cache(seconds=60)
51
+ def my_function():
52
+ return 'Hello World'
53
+
54
+ # * to clear the cache, use force_clear=True on the function call
55
+ my_function(force_clear=True)
56
+
57
+ """
58
+
59
+ def wrapper_cache(func: Callable[..., R]) -> TimedCachedCallable[R]:
60
+ cached_func = cast(Any, lru_cache(maxsize=maxsize)(func))
61
+ cached_func.lifetime = timedelta(seconds=seconds)
62
+ cached_func.expiration = datetime.now(timezone.utc) + cached_func.lifetime
63
+
64
+ @wraps(func)
65
+ def wrapped_func(
66
+ *args: Any,
67
+ force_clear: bool = False,
68
+ show_log: bool = False,
69
+ **kwargs: Any,
70
+ ) -> R:
71
+ """Wrapper function for lru_cache with expiration time.
72
+
73
+ Args:
74
+ *args: function arguments
75
+ force_clear (bool): forces a clear cache
76
+ show_log (bool): show log on clear
77
+ **kwargs: function keyword arguments
78
+ Returns:
79
+ function result
80
+ """
81
+
82
+ if force_clear or datetime.now(timezone.utc) >= cached_func.expiration:
83
+ if show_log:
84
+ log.debug(f'Cache cleared for {cached_func.__name__}')
85
+
86
+ cached_func.cache_clear()
87
+ cached_func.expiration = datetime.now(timezone.utc) + cached_func.lifetime
88
+
89
+ return cached_func(*args, **kwargs)
90
+
91
+ return cast(TimedCachedCallable[R], wrapped_func)
92
+
93
+ return wrapper_cache
backpack/custom_errors.py CHANGED
@@ -1,37 +1,37 @@
1
- # ----------------------------------------------------------------------------------------
2
- # Python-Backpack - Custom Exceptions
3
- # Maximiliano Rocamora / maxirocamora@gmail.com
4
- # https://github.com/MaxRocamora/python-backpack
5
- # ----------------------------------------------------------------------------------------
6
-
7
-
8
- class EnvironmentVariableNotFoundError(Exception):
9
- def __init__(self, var_name: str) -> None:
10
- """Error Raised when a required environment variable is missing from os.
11
-
12
- Args:
13
- var_name (str): name of the required variable missing
14
- """
15
- self.var_name = var_name
16
- self.message = f'Required Environment Variable [{var_name}] not found.'
17
- super().__init__(self.message)
18
-
19
- def __str__(self):
20
- """Return the error message."""
21
- return self.message
22
-
23
-
24
- class ApplicationNotFoundError(Exception):
25
- def __init__(self, app_name: str) -> None:
26
- """Error Raised when an Application Name required is not found.
27
-
28
- Args:
29
- app_name (str): name of the required application missing
30
- """
31
- self.app_name = app_name
32
- self.message = f'Application ({app_name}) not found.'
33
- super().__init__(self.message)
34
-
35
- def __str__(self):
36
- """Return the error message."""
37
- return self.message
1
+ # ----------------------------------------------------------------------------------------
2
+ # Python-Backpack - Custom Exceptions
3
+ # Maximiliano Rocamora / maxirocamora@gmail.com
4
+ # https://github.com/MaxRocamora/python-backpack
5
+ # ----------------------------------------------------------------------------------------
6
+
7
+
8
+ class EnvironmentVariableNotFoundError(Exception):
9
+ def __init__(self, var_name: str) -> None:
10
+ """Error Raised when a required environment variable is missing from os.
11
+
12
+ Args:
13
+ var_name (str): name of the required variable missing
14
+ """
15
+ self.var_name = var_name
16
+ self.message = f'Required Environment Variable [{var_name}] not found.'
17
+ super().__init__(self.message)
18
+
19
+ def __str__(self):
20
+ """Return the error message."""
21
+ return self.message
22
+
23
+
24
+ class ApplicationNotFoundError(Exception):
25
+ def __init__(self, app_name: str) -> None:
26
+ """Error Raised when an Application Name required is not found.
27
+
28
+ Args:
29
+ app_name (str): name of the required application missing
30
+ """
31
+ self.app_name = app_name
32
+ self.message = f'Application ({app_name}) not found.'
33
+ super().__init__(self.message)
34
+
35
+ def __str__(self):
36
+ """Return the error message."""
37
+ return self.message
backpack/file_utils.py CHANGED
@@ -1,135 +1,140 @@
1
- # ----------------------------------------------------------------------------------------
2
- # Python-Backpack - File Utilities - Ascii Files Manipulation
3
- # Maximiliano Rocamora / maxirocamora@gmail.com
4
- # https://github.com/MaxRocamora/python-backpack
5
- # ----------------------------------------------------------------------------------------
6
-
7
- from collections.abc import Sequence
8
-
9
- from backpack.logger import get_logger
10
-
11
- log = get_logger('Python Backpack - FileUtils')
12
-
13
-
14
- def replace_strings_in_file(
15
- ascii_file: str,
16
- strings: Sequence[str],
17
- new_string: str,
18
- ) -> None:
19
- """Opens ascii file and replaces all occurrences from strings into new_string.
20
-
21
- In this class we use a full path to avoid use of os.dirname, which
22
- causes string encode problems.
23
-
24
- Args:
25
- ascii_file: (fullpath string) file to open
26
- strings: (list) strings to replace
27
- new_string: (string) new string or path to set
28
- """
29
-
30
- # force forward slashes
31
- new_string = new_string.replace('\\', '/')
32
-
33
- log.info(f'Replacing Strings, Opening File: {ascii_file}')
34
-
35
- with open(ascii_file, newline='') as f:
36
- file_data = f.read()
37
- for i in strings:
38
- log.info('Finding: %s', i)
39
- log.info('Replacing for: %s', new_string)
40
- log.info('-' * 50)
41
- file_data = file_data.replace(i, new_string)
42
-
43
- with open(ascii_file, 'w', newline='') as f:
44
- f.write(file_data)
45
- log.info(f'Closing File: {ascii_file}')
46
-
47
-
48
- def remove_line_from_file(
49
- ascii_file: str,
50
- strings: Sequence[str],
51
- verbose: bool = False,
52
- ) -> None:
53
- """Removes given lines from ascii file.
54
-
55
- Args:
56
- ascii_file: (string path) ASCII file to process
57
- strings: (list) match lines to remove
58
- verbose: (bool) if true, prints removed lines
59
- """
60
-
61
- retained_lines = []
62
- with open(ascii_file, newline='') as f:
63
- file_content = f.readlines()
64
-
65
- for line in file_content:
66
- value = line.rstrip('\r\n')
67
- if verbose:
68
- log.info(f'Checking line: {value}')
69
- if value in strings:
70
- if verbose:
71
- log.info(f'Removing line: {value}')
72
- else:
73
- retained_lines.append(line)
74
-
75
- with open(ascii_file, 'w', newline='') as f:
76
- f.writelines(retained_lines)
77
-
78
-
79
- def file_is_writeable(filepath: str) -> bool:
80
- """Checks if a file is locked and can be overwritten.
81
-
82
- Args:
83
- filepath: (str) file to check
84
- Returns:
85
- bool : true if file is writeable
86
- """
87
- try:
88
- with open(filepath, 'r+') as _:
89
- return True
90
- except OSError as x:
91
- log.warning(f'{filepath} is locked ')
92
- log.info(x.strerror)
93
-
94
- return False
95
-
96
-
97
- def get_version_from_filename(filename: str) -> str:
98
- """Extracts version from filename.
99
-
100
- Args:
101
- filename: (str) file name to extract version from
102
- Returns:
103
- str : version extracted from filename
104
-
105
- Examples:
106
- get_version_from_filename('myfile_23.txt') -> '23'
107
- get_version_from_filename('myfile-9.txt') -> '9'
108
- get_version_from_filename('myfile.130.txt') -> '130'
109
- get_version_from_filename('myfile_v1002.txt') -> '1002'
110
-
111
- """
112
-
113
- filename_no_ext = filename.rsplit('.', 1)[0]
114
-
115
- # guess is the separator is an underscore, dash, dot or v, and the version is the last part before the extension
116
- separators = ['_', '-', '.', 'v']
117
- if not any(sep in filename_no_ext for sep in separators):
118
- log.info(f'No separator found in filename: {filename_no_ext}. Using fallback extraction.')
119
- else:
120
- for sep in separators:
121
- if sep in filename_no_ext:
122
- parts = filename_no_ext.split(sep)
123
- version_part = parts[-1].split('.')[0] # Get the last part before the extension
124
- if version_part.replace('.', '').isdigit(): # Check if it's a valid version number
125
- log.info(
126
- f'Extracted version: {version_part} from filename: {filename} using separator: {sep}'
127
- )
128
- return version_part
129
-
130
- # Fallback: extract from the entire filename
131
- version = filename_no_ext.lstrip('v')
132
- version = version.replace('_', '.').replace('-', '.')
133
- version = version if version.replace('.', '').isdigit() else '0'
134
- log.info(f'Extracted version: {version} from filename: {filename}')
135
- return version
1
+ # ----------------------------------------------------------------------------------------
2
+ # Python-Backpack - File Utilities - Ascii Files Manipulation
3
+ # Maximiliano Rocamora / maxirocamora@gmail.com
4
+ # https://github.com/MaxRocamora/python-backpack
5
+ # ----------------------------------------------------------------------------------------
6
+
7
+ import re
8
+ from collections.abc import Sequence
9
+
10
+ from backpack.logger import get_logger
11
+
12
+ log = get_logger('Python Backpack - FileUtils')
13
+
14
+
15
+ def replace_strings_in_file(
16
+ ascii_file: str,
17
+ strings: Sequence[str],
18
+ new_string: str,
19
+ ) -> None:
20
+ """Opens ascii file and replaces all occurrences from strings into new_string.
21
+
22
+ In this class we use a full path to avoid use of os.dirname, which
23
+ causes string encode problems.
24
+
25
+ Args:
26
+ ascii_file: (fullpath string) file to open
27
+ strings: (list) strings to replace
28
+ new_string: (string) new string or path to set
29
+ """
30
+
31
+ # force forward slashes
32
+ new_string = new_string.replace('\\', '/')
33
+
34
+ log.info(f'Replacing Strings, Opening File: {ascii_file}')
35
+
36
+ with open(ascii_file, newline='') as f:
37
+ file_data = f.read()
38
+ for i in strings:
39
+ log.info('Finding: %s', i)
40
+ log.info('Replacing for: %s', new_string)
41
+ log.info('-' * 50)
42
+ file_data = file_data.replace(i, new_string)
43
+
44
+ with open(ascii_file, 'w', newline='') as f:
45
+ f.write(file_data)
46
+ log.info(f'Closing File: {ascii_file}')
47
+
48
+
49
+ def remove_line_from_file(
50
+ ascii_file: str,
51
+ strings: Sequence[str],
52
+ verbose: bool = False,
53
+ ) -> None:
54
+ """Removes given lines from ascii file.
55
+
56
+ Args:
57
+ ascii_file: (string path) ASCII file to process
58
+ strings: (list) match lines to remove
59
+ verbose: (bool) if true, prints removed lines
60
+ """
61
+
62
+ retained_lines = []
63
+ with open(ascii_file, newline='') as f:
64
+ file_content = f.readlines()
65
+
66
+ for line in file_content:
67
+ value = line.rstrip('\r\n')
68
+ if verbose:
69
+ log.info(f'Checking line: {value}')
70
+ if value in strings:
71
+ if verbose:
72
+ log.info(f'Removing line: {value}')
73
+ else:
74
+ retained_lines.append(line)
75
+
76
+ with open(ascii_file, 'w', newline='') as f:
77
+ f.writelines(retained_lines)
78
+
79
+
80
+ def file_is_writeable(filepath: str) -> bool:
81
+ """Checks if a file is locked and can be overwritten.
82
+
83
+ Args:
84
+ filepath: (str) file to check
85
+ Returns:
86
+ bool : true if file is writeable
87
+ """
88
+ try:
89
+ with open(filepath, 'r+') as _:
90
+ return True
91
+ except OSError as x:
92
+ log.warning(f'{filepath} is locked ')
93
+ log.info(x.strerror)
94
+
95
+ return False
96
+
97
+
98
+ def get_version_from_filename(filename: str, separator: str | None = None) -> str:
99
+ """Extracts version from filename.
100
+
101
+ Args:
102
+ filename: (str) file name to extract version from
103
+ separator: delimiter that must precede the version; when omitted, use supported delimiters
104
+ Returns:
105
+ str : version extracted from filename
106
+
107
+ Examples:
108
+ get_version_from_filename('myfile_23.txt') -> '23'
109
+ get_version_from_filename('myfile-9.txt') -> '9'
110
+ get_version_from_filename('myfile.130.txt') -> '130'
111
+ get_version_from_filename('myfile_v1002.txt') -> '1002'
112
+ get_version_from_filename('XD_shot_0000.0003.ma') -> '0003'
113
+ get_version_from_filename('ANM_shot_0010.0003.ma') -> '0003'
114
+
115
+ """
116
+
117
+ filename_no_ext = filename.rsplit('.', 1)[0]
118
+
119
+ if separator is not None:
120
+ if not separator:
121
+ raise ValueError('separator must not be empty')
122
+
123
+ version = filename_no_ext.rsplit(separator, 1)[-1]
124
+ if version.isdigit():
125
+ log.info(
126
+ f'Extracted version: {version} from filename: {filename} using separator: {separator}'
127
+ )
128
+ return version
129
+
130
+ log.info(f'No numeric version found in filename: {filename} using separator: {separator}')
131
+ return '0'
132
+
133
+ match = re.search(r'(?:^|[_.-])v?(\d+)$', filename_no_ext)
134
+ if match:
135
+ version = match.group(1)
136
+ log.info(f'Extracted version: {version} from filename: {filename}')
137
+ return version
138
+
139
+ log.info(f'No numeric version found in filename: {filename}')
140
+ return '0'