python-backpack 2.0.2__tar.gz → 2.0.3__tar.gz
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.
- {python_backpack-2.0.2 → python_backpack-2.0.3}/.github/workflows/codecov_tests.yml +17 -8
- {python_backpack-2.0.2 → python_backpack-2.0.3}/.gitignore +1 -0
- {python_backpack-2.0.2 → python_backpack-2.0.3}/PKG-INFO +31 -18
- {python_backpack-2.0.2 → python_backpack-2.0.3}/README.md +15 -15
- {python_backpack-2.0.2 → python_backpack-2.0.3}/backpack/cache.py +28 -6
- {python_backpack-2.0.2 → python_backpack-2.0.3}/backpack/file_utils.py +26 -18
- {python_backpack-2.0.2 → python_backpack-2.0.3}/backpack/folder_utils.py +17 -7
- {python_backpack-2.0.2 → python_backpack-2.0.3}/backpack/json_metadata.py +5 -2
- {python_backpack-2.0.2 → python_backpack-2.0.3}/backpack/json_user_settings.py +5 -5
- {python_backpack-2.0.2 → python_backpack-2.0.3}/backpack/json_utils.py +7 -7
- {python_backpack-2.0.2 → python_backpack-2.0.3}/backpack/strings.py +9 -15
- {python_backpack-2.0.2 → python_backpack-2.0.3}/backpack/test_utils.py +10 -4
- {python_backpack-2.0.2 → python_backpack-2.0.3}/backpack/version.py +5 -4
- python_backpack-2.0.3/pyproject.toml +52 -0
- {python_backpack-2.0.2 → python_backpack-2.0.3}/ruff.toml +4 -2
- python_backpack-2.0.3/tests/test_file_utils.py +86 -0
- python_backpack-2.0.3/tests/test_folder_utils.py +107 -0
- {python_backpack-2.0.2 → python_backpack-2.0.3}/tests/test_json_user_settings.py +21 -44
- python_backpack-2.0.3/tests/test_json_utils.py +35 -0
- {python_backpack-2.0.2 → python_backpack-2.0.3}/tests/test_jsonmd.py +29 -29
- {python_backpack-2.0.2 → python_backpack-2.0.3}/tests/test_misc.py +15 -7
- {python_backpack-2.0.2 → python_backpack-2.0.3}/tests/test_strings.py +10 -7
- {python_backpack-2.0.2 → python_backpack-2.0.3}/tests/test_test_utils.py +12 -14
- python_backpack-2.0.3/uv.lock +312 -0
- python_backpack-2.0.2/.vscode/settings.json +0 -9
- python_backpack-2.0.2/pyproject.toml +0 -30
- python_backpack-2.0.2/tests/test_file_utils.py +0 -71
- python_backpack-2.0.2/tests/test_folder_utils.py +0 -105
- python_backpack-2.0.2/tests/test_json_utils.py +0 -46
- python_backpack-2.0.2/uv.lock +0 -295
- {python_backpack-2.0.2 → python_backpack-2.0.3}/LICENSE +0 -0
- {python_backpack-2.0.2 → python_backpack-2.0.3}/__init__.py +0 -0
- {python_backpack-2.0.2 → python_backpack-2.0.3}/backpack/__init__.py +0 -0
- {python_backpack-2.0.2 → python_backpack-2.0.3}/backpack/custom_errors.py +0 -0
- {python_backpack-2.0.2 → python_backpack-2.0.3}/backpack/logger.py +0 -0
- {python_backpack-2.0.2 → python_backpack-2.0.3}/backpack/patterns.py +0 -0
- {python_backpack-2.0.2 → python_backpack-2.0.3}/backpack.code-workspace +0 -0
- {python_backpack-2.0.2 → python_backpack-2.0.3}/codecov.yml +0 -0
- {python_backpack-2.0.2 → python_backpack-2.0.3}/coverage.bat +0 -0
- {python_backpack-2.0.2 → python_backpack-2.0.3}/tests/__init__.py +0 -0
- {python_backpack-2.0.2 → python_backpack-2.0.3}/tests/test_cache.py +0 -0
- {python_backpack-2.0.2 → python_backpack-2.0.3}/tests/test_errors.py +0 -0
- {python_backpack-2.0.2 → python_backpack-2.0.3}/tests/test_files/edited.txt +0 -0
- {python_backpack-2.0.2 → python_backpack-2.0.3}/tests/test_files/edited_remove.txt +0 -0
- {python_backpack-2.0.2 → python_backpack-2.0.3}/tests/test_files/locked_file.txt +0 -0
- {python_backpack-2.0.2 → python_backpack-2.0.3}/tests/test_files/origin.txt +0 -0
- {python_backpack-2.0.2 → python_backpack-2.0.3}/tests/test_files/origin_remove.txt +0 -0
- {python_backpack-2.0.2 → python_backpack-2.0.3}/tests/test_files/unlocked_file.txt +0 -0
- {python_backpack-2.0.2 → python_backpack-2.0.3}/tests/test_json/data.json +0 -0
- {python_backpack-2.0.2 → python_backpack-2.0.3}/tests/test_json/data_broken.json +0 -0
- {python_backpack-2.0.2 → python_backpack-2.0.3}/tests/test_json/save/data_saved.json +0 -0
|
@@ -9,7 +9,7 @@ jobs:
|
|
|
9
9
|
runs-on: ubuntu-latest
|
|
10
10
|
strategy:
|
|
11
11
|
matrix:
|
|
12
|
-
python-version: ['3.11']
|
|
12
|
+
python-version: ['3.11', '3.12', '3.13', '3.14']
|
|
13
13
|
|
|
14
14
|
steps:
|
|
15
15
|
- uses: actions/checkout@v6
|
|
@@ -19,20 +19,29 @@ jobs:
|
|
|
19
19
|
with:
|
|
20
20
|
python-version: ${{ matrix.python-version }}
|
|
21
21
|
|
|
22
|
-
- name: Add path to PYTHONPATH
|
|
23
|
-
run: |
|
|
24
|
-
echo "PYTHONPATH=$GITHUB_WORKSPACE" >> $GITHUB_ENV
|
|
25
|
-
|
|
26
22
|
- name: Install dependencies
|
|
27
23
|
run: |
|
|
28
24
|
python -m pip install --upgrade pip
|
|
29
25
|
pip install uv
|
|
30
26
|
uv sync --dev
|
|
31
27
|
|
|
32
|
-
- name: Run
|
|
28
|
+
- name: Run Ruff
|
|
29
|
+
if: ${{ matrix.python-version == '3.11' }}
|
|
30
|
+
run: uv run ruff check .
|
|
31
|
+
|
|
32
|
+
- name: Run Pyright
|
|
33
|
+
if: ${{ matrix.python-version == '3.11' }}
|
|
34
|
+
run: uv run pyright backpack tests
|
|
35
|
+
|
|
36
|
+
- name: Run tests
|
|
37
|
+
if: ${{ matrix.python-version != '3.11' }}
|
|
38
|
+
run: uv run pytest
|
|
39
|
+
|
|
40
|
+
- name: Run tests with branch coverage
|
|
41
|
+
if: ${{ matrix.python-version == '3.11' }}
|
|
33
42
|
run: |
|
|
34
|
-
uv run coverage run --source=backpack -m pytest
|
|
35
|
-
uv run coverage report -m --fail-under=
|
|
43
|
+
uv run coverage run --branch --source=backpack -m pytest
|
|
44
|
+
uv run coverage report -m --fail-under=99
|
|
36
45
|
uv run coverage xml
|
|
37
46
|
|
|
38
47
|
- name: Upload coverage.xml
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-backpack
|
|
3
|
-
Version: 2.0.
|
|
4
|
-
Summary:
|
|
5
|
-
|
|
3
|
+
Version: 2.0.3
|
|
4
|
+
Summary: Lightweight utilities for JSON, filesystem operations, strings, caching, and scripting.
|
|
5
|
+
Project-URL: Homepage, https://github.com/MaxRocamora/python-backpack
|
|
6
|
+
Project-URL: Repository, https://github.com/MaxRocamora/python-backpack.git
|
|
7
|
+
Author-email: Maximiliano Rocamora <maxirocamora@gmail.com>
|
|
8
|
+
License-Expression: GPL-3.0-only
|
|
6
9
|
License-File: LICENSE
|
|
10
|
+
Keywords: cache,filesystem,json,strings,utilities
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Topic :: Utilities
|
|
7
20
|
Requires-Python: >=3.11
|
|
8
21
|
Description-Content-Type: text/markdown
|
|
9
22
|
|
|
@@ -52,10 +65,10 @@ pip install python-backpack
|
|
|
52
65
|
|
|
53
66
|
### File Utils (`backpack.file_utils`)
|
|
54
67
|
|
|
55
|
-
- `replace_strings_in_file(ascii_file: str, strings:
|
|
68
|
+
- `replace_strings_in_file(ascii_file: str, strings: Sequence[str], new_string: str) -> None`
|
|
56
69
|
- Replaces multiple string occurrences in a text file.
|
|
57
|
-
- `remove_line_from_file(ascii_file: str, strings:
|
|
58
|
-
- Removes exact matching
|
|
70
|
+
- `remove_line_from_file(ascii_file: str, strings: Sequence[str], verbose: bool = False) -> None`
|
|
71
|
+
- Removes every exact matching line while preserving retained line endings.
|
|
59
72
|
- `file_is_writeable(filepath: str) -> bool`
|
|
60
73
|
- Checks whether a file can be opened for read/write.
|
|
61
74
|
- `get_version_from_filename(filename: str) -> str`
|
|
@@ -63,23 +76,23 @@ pip install python-backpack
|
|
|
63
76
|
|
|
64
77
|
### Folder Utils (`backpack.folder_utils`)
|
|
65
78
|
|
|
66
|
-
- `browse_folder(folder: str) -> bool`
|
|
67
|
-
- Opens a folder in Windows Explorer.
|
|
68
|
-
- `create_folders(folders:
|
|
79
|
+
- `browse_folder(folder: str | None) -> bool`
|
|
80
|
+
- Opens a valid folder in Windows Explorer and returns `False` if it cannot be opened.
|
|
81
|
+
- `create_folders(folders: Sequence[str], force_empty: bool = False, verbose: bool = False) -> None`
|
|
69
82
|
- Creates multiple folders.
|
|
70
|
-
- `create_folder(path: str, force_empty: bool = False, verbose: bool = True)`
|
|
83
|
+
- `create_folder(path: str, force_empty: bool = False, verbose: bool = True) -> bool`
|
|
71
84
|
- Creates a folder and optionally clears it if it already exists.
|
|
72
|
-
- `remove_files_in_dir(path: str)`
|
|
85
|
+
- `remove_files_in_dir(path: str) -> None`
|
|
73
86
|
- Removes all files and subdirectories inside a directory.
|
|
74
|
-
- `recursive_dir_copy(source_path: str, target_path: str)`
|
|
87
|
+
- `recursive_dir_copy(source_path: str, target_path: str) -> None`
|
|
75
88
|
- Recursively copies files and subfolders from source to target.
|
|
76
89
|
|
|
77
90
|
### JSON Utils (`backpack.json_utils`)
|
|
78
91
|
|
|
79
92
|
- `json_load(json_file: str) -> dict`
|
|
80
93
|
- Loads JSON data from file with validation and error handling.
|
|
81
|
-
- `json_save(data:
|
|
82
|
-
- Saves a
|
|
94
|
+
- `json_save(data: object, json_file: str) -> bool`
|
|
95
|
+
- Saves a JSON-serializable value as UTF-8 and returns whether it succeeded.
|
|
83
96
|
|
|
84
97
|
### JSON Metadata (`backpack.json_metadata`)
|
|
85
98
|
|
|
@@ -99,7 +112,7 @@ pip install python-backpack
|
|
|
99
112
|
- `JsonUserSettings(folder: str, name: str)`
|
|
100
113
|
- Saves and loads JSON settings in the current user's home directory.
|
|
101
114
|
- Main public methods:
|
|
102
|
-
- `save_settings(data: dict | None = None) -> bool
|
|
115
|
+
- `save_settings(data: dict | None = None) -> bool`
|
|
103
116
|
- `load_settings() -> dict | bool`
|
|
104
117
|
|
|
105
118
|
### Logger (`backpack.logger`)
|
|
@@ -130,8 +143,8 @@ pip install python-backpack
|
|
|
130
143
|
|
|
131
144
|
- `random_string(length: int = 10) -> str`
|
|
132
145
|
- Generates a random lowercase string.
|
|
133
|
-
- `time_function_decorator(method:
|
|
134
|
-
- Decorator that logs execution time.
|
|
146
|
+
- `time_function_decorator(method: Callable[P, R]) -> Callable[P, R]`
|
|
147
|
+
- Decorator that logs execution time while preserving function metadata and return values.
|
|
135
148
|
|
|
136
149
|
## Quick Example
|
|
137
150
|
|
|
@@ -143,7 +156,7 @@ from backpack.json_utils import json_save, json_load
|
|
|
143
156
|
|
|
144
157
|
@timed_lru_cache(seconds=60)
|
|
145
158
|
def expensive_call():
|
|
146
|
-
|
|
159
|
+
return {'ok': True}
|
|
147
160
|
|
|
148
161
|
|
|
149
162
|
value = camelcase_to_snakecase('HTTPServer')
|
|
@@ -43,10 +43,10 @@ pip install python-backpack
|
|
|
43
43
|
|
|
44
44
|
### File Utils (`backpack.file_utils`)
|
|
45
45
|
|
|
46
|
-
- `replace_strings_in_file(ascii_file: str, strings:
|
|
46
|
+
- `replace_strings_in_file(ascii_file: str, strings: Sequence[str], new_string: str) -> None`
|
|
47
47
|
- Replaces multiple string occurrences in a text file.
|
|
48
|
-
- `remove_line_from_file(ascii_file: str, strings:
|
|
49
|
-
- Removes exact matching
|
|
48
|
+
- `remove_line_from_file(ascii_file: str, strings: Sequence[str], verbose: bool = False) -> None`
|
|
49
|
+
- Removes every exact matching line while preserving retained line endings.
|
|
50
50
|
- `file_is_writeable(filepath: str) -> bool`
|
|
51
51
|
- Checks whether a file can be opened for read/write.
|
|
52
52
|
- `get_version_from_filename(filename: str) -> str`
|
|
@@ -54,23 +54,23 @@ pip install python-backpack
|
|
|
54
54
|
|
|
55
55
|
### Folder Utils (`backpack.folder_utils`)
|
|
56
56
|
|
|
57
|
-
- `browse_folder(folder: str) -> bool`
|
|
58
|
-
- Opens a folder in Windows Explorer.
|
|
59
|
-
- `create_folders(folders:
|
|
57
|
+
- `browse_folder(folder: str | None) -> bool`
|
|
58
|
+
- Opens a valid folder in Windows Explorer and returns `False` if it cannot be opened.
|
|
59
|
+
- `create_folders(folders: Sequence[str], force_empty: bool = False, verbose: bool = False) -> None`
|
|
60
60
|
- Creates multiple folders.
|
|
61
|
-
- `create_folder(path: str, force_empty: bool = False, verbose: bool = True)`
|
|
61
|
+
- `create_folder(path: str, force_empty: bool = False, verbose: bool = True) -> bool`
|
|
62
62
|
- Creates a folder and optionally clears it if it already exists.
|
|
63
|
-
- `remove_files_in_dir(path: str)`
|
|
63
|
+
- `remove_files_in_dir(path: str) -> None`
|
|
64
64
|
- Removes all files and subdirectories inside a directory.
|
|
65
|
-
- `recursive_dir_copy(source_path: str, target_path: str)`
|
|
65
|
+
- `recursive_dir_copy(source_path: str, target_path: str) -> None`
|
|
66
66
|
- Recursively copies files and subfolders from source to target.
|
|
67
67
|
|
|
68
68
|
### JSON Utils (`backpack.json_utils`)
|
|
69
69
|
|
|
70
70
|
- `json_load(json_file: str) -> dict`
|
|
71
71
|
- Loads JSON data from file with validation and error handling.
|
|
72
|
-
- `json_save(data:
|
|
73
|
-
- Saves a
|
|
72
|
+
- `json_save(data: object, json_file: str) -> bool`
|
|
73
|
+
- Saves a JSON-serializable value as UTF-8 and returns whether it succeeded.
|
|
74
74
|
|
|
75
75
|
### JSON Metadata (`backpack.json_metadata`)
|
|
76
76
|
|
|
@@ -90,7 +90,7 @@ pip install python-backpack
|
|
|
90
90
|
- `JsonUserSettings(folder: str, name: str)`
|
|
91
91
|
- Saves and loads JSON settings in the current user's home directory.
|
|
92
92
|
- Main public methods:
|
|
93
|
-
- `save_settings(data: dict | None = None) -> bool
|
|
93
|
+
- `save_settings(data: dict | None = None) -> bool`
|
|
94
94
|
- `load_settings() -> dict | bool`
|
|
95
95
|
|
|
96
96
|
### Logger (`backpack.logger`)
|
|
@@ -121,8 +121,8 @@ pip install python-backpack
|
|
|
121
121
|
|
|
122
122
|
- `random_string(length: int = 10) -> str`
|
|
123
123
|
- Generates a random lowercase string.
|
|
124
|
-
- `time_function_decorator(method:
|
|
125
|
-
- Decorator that logs execution time.
|
|
124
|
+
- `time_function_decorator(method: Callable[P, R]) -> Callable[P, R]`
|
|
125
|
+
- Decorator that logs execution time while preserving function metadata and return values.
|
|
126
126
|
|
|
127
127
|
## Quick Example
|
|
128
128
|
|
|
@@ -134,7 +134,7 @@ from backpack.json_utils import json_save, json_load
|
|
|
134
134
|
|
|
135
135
|
@timed_lru_cache(seconds=60)
|
|
136
136
|
def expensive_call():
|
|
137
|
-
|
|
137
|
+
return {'ok': True}
|
|
138
138
|
|
|
139
139
|
|
|
140
140
|
value = camelcase_to_snakecase('HTTPServer')
|
|
@@ -5,16 +5,33 @@
|
|
|
5
5
|
# ----------------------------------------------------------------------------------------
|
|
6
6
|
from datetime import datetime, timedelta, timezone
|
|
7
7
|
from functools import lru_cache, wraps
|
|
8
|
-
from typing import Any, Callable, TypeVar, cast
|
|
8
|
+
from typing import Any, Callable, Protocol, TypeVar, cast
|
|
9
9
|
|
|
10
10
|
from backpack.logger import get_logger
|
|
11
11
|
|
|
12
12
|
log = get_logger('Python Backpack - Cache')
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
R = TypeVar('R', covariant=True)
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
|
|
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]]:
|
|
18
35
|
"""Lru_cache with expiration time.
|
|
19
36
|
|
|
20
37
|
Args:
|
|
@@ -39,13 +56,18 @@ def timed_lru_cache(seconds: int, maxsize: int = 128) -> Callable[[F], F]:
|
|
|
39
56
|
|
|
40
57
|
"""
|
|
41
58
|
|
|
42
|
-
def wrapper_cache(func:
|
|
59
|
+
def wrapper_cache(func: Callable[..., R]) -> TimedCachedCallable[R]:
|
|
43
60
|
cached_func = cast(Any, lru_cache(maxsize=maxsize)(func))
|
|
44
61
|
cached_func.lifetime = timedelta(seconds=seconds)
|
|
45
62
|
cached_func.expiration = datetime.now(timezone.utc) + cached_func.lifetime
|
|
46
63
|
|
|
47
64
|
@wraps(func)
|
|
48
|
-
def wrapped_func(
|
|
65
|
+
def wrapped_func(
|
|
66
|
+
*args: Any,
|
|
67
|
+
force_clear: bool = False,
|
|
68
|
+
show_log: bool = False,
|
|
69
|
+
**kwargs: Any,
|
|
70
|
+
) -> R:
|
|
49
71
|
"""Wrapper function for lru_cache with expiration time.
|
|
50
72
|
|
|
51
73
|
Args:
|
|
@@ -66,6 +88,6 @@ def timed_lru_cache(seconds: int, maxsize: int = 128) -> Callable[[F], F]:
|
|
|
66
88
|
|
|
67
89
|
return cached_func(*args, **kwargs)
|
|
68
90
|
|
|
69
|
-
return cast(
|
|
91
|
+
return cast(TimedCachedCallable[R], wrapped_func)
|
|
70
92
|
|
|
71
93
|
return wrapper_cache
|
|
@@ -4,14 +4,18 @@
|
|
|
4
4
|
# https://github.com/MaxRocamora/python-backpack
|
|
5
5
|
# ----------------------------------------------------------------------------------------
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
from collections.abc import Sequence
|
|
8
8
|
|
|
9
9
|
from backpack.logger import get_logger
|
|
10
10
|
|
|
11
11
|
log = get_logger('Python Backpack - FileUtils')
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
def replace_strings_in_file(
|
|
14
|
+
def replace_strings_in_file(
|
|
15
|
+
ascii_file: str,
|
|
16
|
+
strings: Sequence[str],
|
|
17
|
+
new_string: str,
|
|
18
|
+
) -> None:
|
|
15
19
|
"""Opens ascii file and replaces all occurrences from strings into new_string.
|
|
16
20
|
|
|
17
21
|
In this class we use a full path to avoid use of os.dirname, which
|
|
@@ -28,7 +32,7 @@ def replace_strings_in_file(ascii_file: str, strings: list, new_string: str) ->
|
|
|
28
32
|
|
|
29
33
|
log.info(f'Replacing Strings, Opening File: {ascii_file}')
|
|
30
34
|
|
|
31
|
-
with open(ascii_file) as f:
|
|
35
|
+
with open(ascii_file, newline='') as f:
|
|
32
36
|
file_data = f.read()
|
|
33
37
|
for i in strings:
|
|
34
38
|
log.info('Finding: %s', i)
|
|
@@ -36,13 +40,16 @@ def replace_strings_in_file(ascii_file: str, strings: list, new_string: str) ->
|
|
|
36
40
|
log.info('-' * 50)
|
|
37
41
|
file_data = file_data.replace(i, new_string)
|
|
38
42
|
|
|
39
|
-
with open(ascii_file, 'w') as f:
|
|
43
|
+
with open(ascii_file, 'w', newline='') as f:
|
|
40
44
|
f.write(file_data)
|
|
41
|
-
f.close()
|
|
42
45
|
log.info(f'Closing File: {ascii_file}')
|
|
43
46
|
|
|
44
47
|
|
|
45
|
-
def remove_line_from_file(
|
|
48
|
+
def remove_line_from_file(
|
|
49
|
+
ascii_file: str,
|
|
50
|
+
strings: Sequence[str],
|
|
51
|
+
verbose: bool = False,
|
|
52
|
+
) -> None:
|
|
46
53
|
"""Removes given lines from ascii file.
|
|
47
54
|
|
|
48
55
|
Args:
|
|
@@ -51,21 +58,22 @@ def remove_line_from_file(ascii_file: str, strings: list, verbose: bool = False)
|
|
|
51
58
|
verbose: (bool) if true, prints removed lines
|
|
52
59
|
"""
|
|
53
60
|
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
retained_lines = []
|
|
62
|
+
with open(ascii_file, newline='') as f:
|
|
63
|
+
file_content = f.readlines()
|
|
56
64
|
|
|
57
65
|
for line in file_content:
|
|
66
|
+
value = line.rstrip('\r\n')
|
|
58
67
|
if verbose:
|
|
59
|
-
log.info(f'Checking line: {
|
|
60
|
-
|
|
61
|
-
if
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
with open(ascii_file, 'w') as f:
|
|
67
|
-
|
|
68
|
-
f.write(contents)
|
|
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)
|
|
69
77
|
|
|
70
78
|
|
|
71
79
|
def file_is_writeable(filepath: str) -> bool:
|
|
@@ -7,27 +7,37 @@
|
|
|
7
7
|
import os
|
|
8
8
|
import shutil
|
|
9
9
|
import subprocess
|
|
10
|
+
from collections.abc import Sequence
|
|
10
11
|
|
|
11
12
|
from backpack.logger import get_logger
|
|
12
13
|
|
|
13
14
|
log = get_logger('Python Backpack - FolderUtils')
|
|
14
15
|
|
|
15
16
|
|
|
16
|
-
def browse_folder(folder: str) -> bool:
|
|
17
|
+
def browse_folder(folder: str | None) -> bool:
|
|
17
18
|
"""Open windows explorer on folder.
|
|
18
19
|
|
|
19
20
|
Args:
|
|
20
21
|
folder: (string path) folder to open
|
|
21
22
|
"""
|
|
22
23
|
if folder and os.path.isdir(folder):
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
try:
|
|
25
|
+
subprocess.Popen(['explorer', os.path.abspath(folder)])
|
|
26
|
+
return True
|
|
27
|
+
except OSError as error:
|
|
28
|
+
log.warning(f'Unable to open folder {folder}')
|
|
29
|
+
log.error(str(error))
|
|
30
|
+
return False
|
|
25
31
|
|
|
26
32
|
log.warning(f'Unable to open folder {folder}')
|
|
27
33
|
return False
|
|
28
34
|
|
|
29
35
|
|
|
30
|
-
def create_folders(
|
|
36
|
+
def create_folders(
|
|
37
|
+
folders: Sequence[str],
|
|
38
|
+
force_empty: bool = False,
|
|
39
|
+
verbose: bool = False,
|
|
40
|
+
) -> None:
|
|
31
41
|
"""Creates multiple folders on disc.
|
|
32
42
|
|
|
33
43
|
Args:
|
|
@@ -39,7 +49,7 @@ def create_folders(folders: list, force_empty: bool = False, verbose: bool = Fal
|
|
|
39
49
|
create_folder(folder, force_empty=force_empty, verbose=verbose)
|
|
40
50
|
|
|
41
51
|
|
|
42
|
-
def create_folder(path: str, force_empty: bool = False, verbose: bool = True):
|
|
52
|
+
def create_folder(path: str, force_empty: bool = False, verbose: bool = True) -> bool:
|
|
43
53
|
"""Creates a folder.
|
|
44
54
|
|
|
45
55
|
Args:
|
|
@@ -63,7 +73,7 @@ def create_folder(path: str, force_empty: bool = False, verbose: bool = True):
|
|
|
63
73
|
return True
|
|
64
74
|
|
|
65
75
|
|
|
66
|
-
def remove_files_in_dir(path: str):
|
|
76
|
+
def remove_files_in_dir(path: str) -> None:
|
|
67
77
|
"""Clears all content in given directory."""
|
|
68
78
|
for root, dirs, files in os.walk(path):
|
|
69
79
|
for f in files:
|
|
@@ -72,7 +82,7 @@ def remove_files_in_dir(path: str):
|
|
|
72
82
|
shutil.rmtree(os.path.join(root, d))
|
|
73
83
|
|
|
74
84
|
|
|
75
|
-
def recursive_dir_copy(source_path: str, target_path: str):
|
|
85
|
+
def recursive_dir_copy(source_path: str, target_path: str) -> None:
|
|
76
86
|
"""Copy all files src dir to dest dir, including sub-directories.
|
|
77
87
|
|
|
78
88
|
Args:
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# Maximiliano Rocamora / maxirocamora@gmail.com
|
|
4
4
|
# https://github.com/MaxRocamora/python-backpack
|
|
5
5
|
# ----------------------------------------------------------------------------------------
|
|
6
|
+
import getpass
|
|
6
7
|
import inspect
|
|
7
8
|
import os
|
|
8
9
|
import platform
|
|
@@ -97,7 +98,9 @@ class JsonMetaFile:
|
|
|
97
98
|
attributes = {}
|
|
98
99
|
for name in dir(_class):
|
|
99
100
|
value = getattr(_class, name)
|
|
100
|
-
if not name.startswith('__') and not
|
|
101
|
+
if not name.startswith('__') and not (
|
|
102
|
+
inspect.isroutine(value) or inspect.isdatadescriptor(value)
|
|
103
|
+
):
|
|
101
104
|
attributes[name] = value
|
|
102
105
|
|
|
103
106
|
self._data = attributes
|
|
@@ -113,7 +116,7 @@ class JsonMetaFile:
|
|
|
113
116
|
'app': os.path.basename(sys.executable),
|
|
114
117
|
'PC': str(platform.node()),
|
|
115
118
|
'python_version': sys.version,
|
|
116
|
-
'User':
|
|
119
|
+
'User': getpass.getuser(),
|
|
117
120
|
'time': self._current_time_metadata(),
|
|
118
121
|
}
|
|
119
122
|
|
|
@@ -27,7 +27,7 @@ class JsonUserSettings:
|
|
|
27
27
|
folder (str): name of sub folder inside user path. Defaults to 'json_settings'.
|
|
28
28
|
name (str): name used for the json file. Defaults to 'user_data'.
|
|
29
29
|
"""
|
|
30
|
-
self.
|
|
30
|
+
self.filename = name
|
|
31
31
|
self.folder = folder
|
|
32
32
|
self._user_data = {}
|
|
33
33
|
self._verify_path()
|
|
@@ -35,7 +35,7 @@ class JsonUserSettings:
|
|
|
35
35
|
@property
|
|
36
36
|
def filepath(self) -> str:
|
|
37
37
|
"""Returns user filepath."""
|
|
38
|
-
path = os.path.join(self.os_user_folder, self.folder, f'{self.
|
|
38
|
+
path = os.path.join(self.os_user_folder, self.folder, f'{self.filename}.json')
|
|
39
39
|
return os.path.abspath(path)
|
|
40
40
|
|
|
41
41
|
@property
|
|
@@ -60,14 +60,14 @@ class JsonUserSettings:
|
|
|
60
60
|
|
|
61
61
|
return True
|
|
62
62
|
|
|
63
|
-
def save_settings(self, data: dict | None = None) -> bool
|
|
63
|
+
def save_settings(self, data: dict | None = None) -> bool:
|
|
64
64
|
"""Saves a dictionary into a json file (os user path).
|
|
65
65
|
|
|
66
66
|
Args:
|
|
67
|
-
data (
|
|
67
|
+
data (dict): info dictionary to save, if not provided,
|
|
68
68
|
saves instead local self.user_data property
|
|
69
69
|
Returns:
|
|
70
|
-
bool
|
|
70
|
+
bool: True if the file was saved, otherwise False.
|
|
71
71
|
"""
|
|
72
72
|
if data is None:
|
|
73
73
|
data = self.user_data
|
|
@@ -18,32 +18,32 @@ def json_load(json_file: str) -> dict:
|
|
|
18
18
|
if not os.path.exists(json_file):
|
|
19
19
|
raise OSError(f'json_load: File not found: {json_file}.')
|
|
20
20
|
|
|
21
|
-
with open(json_file) as json_file_opened:
|
|
21
|
+
with open(json_file, encoding='utf-8') as json_file_opened:
|
|
22
22
|
try:
|
|
23
23
|
value = json.load(json_file_opened)
|
|
24
24
|
except ValueError as e:
|
|
25
|
-
json_file_opened.close()
|
|
26
25
|
raise OSError(f'{json_file} \n JSON File issue: {str(e)}') from e
|
|
27
26
|
|
|
28
27
|
return value
|
|
29
28
|
|
|
30
29
|
|
|
31
|
-
def json_save(data:
|
|
30
|
+
def json_save(data: object, json_file: str) -> bool:
|
|
32
31
|
"""Saves a dictionary into a json file.
|
|
33
32
|
|
|
34
33
|
Args:
|
|
35
|
-
data:
|
|
34
|
+
data: value to serialize as JSON
|
|
36
35
|
json_file: (string filepath) json file to save data.
|
|
37
36
|
|
|
38
37
|
Returns:
|
|
39
38
|
bool (True if success)
|
|
40
39
|
"""
|
|
41
40
|
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
parent_dir = os.path.dirname(json_file)
|
|
42
|
+
if parent_dir and not os.path.exists(parent_dir):
|
|
43
|
+
os.makedirs(parent_dir, exist_ok=True)
|
|
44
44
|
|
|
45
45
|
try:
|
|
46
|
-
with open(json_file, 'w') as f:
|
|
46
|
+
with open(json_file, 'w', encoding='utf-8') as f:
|
|
47
47
|
json.dump(data, f, sort_keys=True, indent=4)
|
|
48
48
|
return True
|
|
49
49
|
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
# https://github.com/MaxRocamora/python-backpack
|
|
5
5
|
# ----------------------------------------------------------------------------------------
|
|
6
6
|
|
|
7
|
-
import contextlib
|
|
8
7
|
import re
|
|
9
8
|
|
|
10
9
|
|
|
@@ -29,30 +28,25 @@ def normalize_input_string(
|
|
|
29
28
|
str: reformatted string
|
|
30
29
|
"""
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
regex =
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
# replacing " " for replacer
|
|
38
|
-
if under_spaces and char == ' ':
|
|
39
|
-
regex = regex.replace(' ', replacer)
|
|
40
|
-
|
|
41
|
-
# replacing "-" for replacer
|
|
42
|
-
if under_hyphen and char == '-':
|
|
43
|
-
regex = regex.replace('-', replacer)
|
|
31
|
+
regex = re.sub(r'[^A-Za-z0-9 _-]+', '', input_string)
|
|
32
|
+
if under_spaces:
|
|
33
|
+
regex = regex.replace(' ', replacer)
|
|
34
|
+
if under_hyphen:
|
|
35
|
+
regex = regex.replace('-', replacer)
|
|
44
36
|
|
|
45
37
|
return regex
|
|
46
38
|
|
|
47
39
|
|
|
48
40
|
def begin_or_end_with_numbers(input_string: str) -> bool:
|
|
49
41
|
"""Returns true if the input string begins or ends with number."""
|
|
50
|
-
return
|
|
42
|
+
return bool(input_string) and (
|
|
43
|
+
_char_is_number(input_string[0]) or _char_is_number(input_string[-1])
|
|
44
|
+
)
|
|
51
45
|
|
|
52
46
|
|
|
53
47
|
def begin_with_number(input_string: str) -> bool:
|
|
54
48
|
"""Returns true if the input string begins with numbers."""
|
|
55
|
-
return _char_is_number(input_string[0])
|
|
49
|
+
return bool(input_string) and _char_is_number(input_string[0])
|
|
56
50
|
|
|
57
51
|
|
|
58
52
|
def has_numbers(input_string: str) -> bool:
|
|
@@ -6,11 +6,16 @@
|
|
|
6
6
|
import random
|
|
7
7
|
import string
|
|
8
8
|
import time
|
|
9
|
+
from functools import wraps
|
|
10
|
+
from typing import Callable, ParamSpec, TypeVar
|
|
9
11
|
|
|
10
12
|
from backpack.logger import get_logger
|
|
11
13
|
|
|
12
14
|
log = get_logger('Python Backpack - TestUtils')
|
|
13
15
|
|
|
16
|
+
P = ParamSpec('P')
|
|
17
|
+
R = TypeVar('R')
|
|
18
|
+
|
|
14
19
|
|
|
15
20
|
def random_string(length: int = 10) -> str:
|
|
16
21
|
"""Generates a random string of fixed length.
|
|
@@ -26,13 +31,14 @@ def random_string(length: int = 10) -> str:
|
|
|
26
31
|
return ''.join(random.choice(letters) for _ in range(length))
|
|
27
32
|
|
|
28
33
|
|
|
29
|
-
def time_function_decorator(method:
|
|
34
|
+
def time_function_decorator(method: Callable[P, R]) -> Callable[P, R]:
|
|
30
35
|
"""Decorator to measure methods execution time."""
|
|
31
36
|
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
@wraps(method)
|
|
38
|
+
def timed(*args: P.args, **kw: P.kwargs) -> R:
|
|
39
|
+
ts = time.perf_counter()
|
|
34
40
|
result = method(*args, **kw)
|
|
35
|
-
te = time.
|
|
41
|
+
te = time.perf_counter()
|
|
36
42
|
|
|
37
43
|
message = f'{method.__name__!r} {(te - ts) * 1000:2.2f} ms'
|
|
38
44
|
log.info(message)
|
|
@@ -11,12 +11,13 @@
|
|
|
11
11
|
# 1.1.4 07/2025 - Moved from pipenv to poetry for dependency management.toml
|
|
12
12
|
# 2.0.1 05/2026 - Remove tox workflow and setup.py, standardize uv + coverage tooling
|
|
13
13
|
# 2.0.2 05/2026 - Add PyPI long description metadata and release polish
|
|
14
|
+
# 2.0.3 06/2026 - Add type hints to backpack.cache, backpack.folder_utils, backpack.json_user_settings, backpack.json_utils, and backpack.logger, refactor backpack.cache to use functools.lru_cache, and add tests for backpack.cache and backpack.folder_utils.
|
|
14
15
|
# ----------------------------------------------------------------------------------------
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
VERSION_MINOR = 0
|
|
18
|
-
VERSION_PATCH = 2
|
|
17
|
+
__version__ = '2.0.3'
|
|
19
18
|
|
|
20
|
-
|
|
19
|
+
VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH = map(int, __version__.split('.'))
|
|
20
|
+
|
|
21
|
+
version = __version__
|
|
21
22
|
|
|
22
23
|
app_name = 'python-backpack'
|