python-backpack 2.0.1__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.1 → python_backpack-2.0.3}/.github/workflows/codecov_tests.yml +17 -8
- {python_backpack-2.0.1 → python_backpack-2.0.3}/.gitignore +1 -0
- python_backpack-2.0.3/PKG-INFO +168 -0
- {python_backpack-2.0.1 → python_backpack-2.0.3}/README.md +17 -15
- {python_backpack-2.0.1 → python_backpack-2.0.3}/backpack/cache.py +28 -6
- {python_backpack-2.0.1 → python_backpack-2.0.3}/backpack/file_utils.py +26 -18
- {python_backpack-2.0.1 → python_backpack-2.0.3}/backpack/folder_utils.py +17 -7
- {python_backpack-2.0.1 → python_backpack-2.0.3}/backpack/json_metadata.py +5 -2
- {python_backpack-2.0.1 → python_backpack-2.0.3}/backpack/json_user_settings.py +5 -5
- {python_backpack-2.0.1 → python_backpack-2.0.3}/backpack/json_utils.py +7 -7
- {python_backpack-2.0.1 → python_backpack-2.0.3}/backpack/strings.py +9 -15
- {python_backpack-2.0.1 → python_backpack-2.0.3}/backpack/test_utils.py +10 -4
- {python_backpack-2.0.1 → python_backpack-2.0.3}/backpack/version.py +6 -4
- python_backpack-2.0.3/pyproject.toml +52 -0
- {python_backpack-2.0.1 → 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.1 → 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.1 → python_backpack-2.0.3}/tests/test_jsonmd.py +29 -29
- {python_backpack-2.0.1 → python_backpack-2.0.3}/tests/test_misc.py +15 -7
- {python_backpack-2.0.1 → python_backpack-2.0.3}/tests/test_strings.py +10 -7
- {python_backpack-2.0.1 → 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.1/PKG-INFO +0 -7
- python_backpack-2.0.1/pyproject.toml +0 -29
- python_backpack-2.0.1/tests/test_file_utils.py +0 -71
- python_backpack-2.0.1/tests/test_folder_utils.py +0 -105
- python_backpack-2.0.1/tests/test_json_utils.py +0 -46
- python_backpack-2.0.1/uv.lock +0 -295
- {python_backpack-2.0.1 → python_backpack-2.0.3}/LICENSE +0 -0
- {python_backpack-2.0.1 → python_backpack-2.0.3}/__init__.py +0 -0
- {python_backpack-2.0.1 → python_backpack-2.0.3}/backpack/__init__.py +0 -0
- {python_backpack-2.0.1 → python_backpack-2.0.3}/backpack/custom_errors.py +0 -0
- {python_backpack-2.0.1 → python_backpack-2.0.3}/backpack/logger.py +0 -0
- {python_backpack-2.0.1 → python_backpack-2.0.3}/backpack/patterns.py +0 -0
- {python_backpack-2.0.1 → python_backpack-2.0.3}/backpack.code-workspace +0 -0
- {python_backpack-2.0.1 → python_backpack-2.0.3}/codecov.yml +0 -0
- {python_backpack-2.0.1 → python_backpack-2.0.3}/coverage.bat +0 -0
- {python_backpack-2.0.1 → python_backpack-2.0.3}/tests/__init__.py +0 -0
- {python_backpack-2.0.1 → python_backpack-2.0.3}/tests/test_cache.py +0 -0
- {python_backpack-2.0.1 → python_backpack-2.0.3}/tests/test_errors.py +0 -0
- {python_backpack-2.0.1 → python_backpack-2.0.3}/tests/test_files/edited.txt +0 -0
- {python_backpack-2.0.1 → python_backpack-2.0.3}/tests/test_files/edited_remove.txt +0 -0
- {python_backpack-2.0.1 → python_backpack-2.0.3}/tests/test_files/locked_file.txt +0 -0
- {python_backpack-2.0.1 → python_backpack-2.0.3}/tests/test_files/origin.txt +0 -0
- {python_backpack-2.0.1 → python_backpack-2.0.3}/tests/test_files/origin_remove.txt +0 -0
- {python_backpack-2.0.1 → python_backpack-2.0.3}/tests/test_files/unlocked_file.txt +0 -0
- {python_backpack-2.0.1 → python_backpack-2.0.3}/tests/test_json/data.json +0 -0
- {python_backpack-2.0.1 → python_backpack-2.0.3}/tests/test_json/data_broken.json +0 -0
- {python_backpack-2.0.1 → 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
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-backpack
|
|
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
|
|
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
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
[](https://pypi.python.org/pypi/python-backpack/)
|
|
24
|
+
[](https://badge.fury.io/py/python-backpack)
|
|
25
|
+
[](https://badge.fury.io/gh/MaxRocamora%2Fpython-backpack)
|
|
26
|
+
[](https://codecov.io/gh/MaxRocamora/python-backpack)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Python-Backpack
|
|
30
|
+
|
|
31
|
+
Python-Backpack is a lightweight utility collection for common scripting tasks:
|
|
32
|
+
|
|
33
|
+
- JSON load/save helpers with validation
|
|
34
|
+
- user settings persistence under the OS user directory
|
|
35
|
+
- metadata export/import to JSON files
|
|
36
|
+
- file and folder operations
|
|
37
|
+
- string normalization and case conversion
|
|
38
|
+
- cache decorator with expiration support
|
|
39
|
+
- custom exceptions, logging helper, singleton pattern, and testing helpers
|
|
40
|
+
|
|
41
|
+
## Compatibility
|
|
42
|
+
|
|
43
|
+
- Python 3.11+
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install python-backpack
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Package API Reference
|
|
52
|
+
|
|
53
|
+
### Cache (`backpack.cache`)
|
|
54
|
+
|
|
55
|
+
- `timed_lru_cache(seconds: int, maxsize: int = 128)`
|
|
56
|
+
- `functools.lru_cache` decorator with expiration time.
|
|
57
|
+
- Wrapped calls support `force_clear=True` and `show_log=True`.
|
|
58
|
+
|
|
59
|
+
### Custom Errors (`backpack.custom_errors`)
|
|
60
|
+
|
|
61
|
+
- `EnvironmentVariableNotFoundError(var_name: str)`
|
|
62
|
+
- Raised when a required environment variable is missing.
|
|
63
|
+
- `ApplicationNotFoundError(app_name: str)`
|
|
64
|
+
- Raised when a required application is not found.
|
|
65
|
+
|
|
66
|
+
### File Utils (`backpack.file_utils`)
|
|
67
|
+
|
|
68
|
+
- `replace_strings_in_file(ascii_file: str, strings: Sequence[str], new_string: str) -> None`
|
|
69
|
+
- Replaces multiple string occurrences in a text file.
|
|
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.
|
|
72
|
+
- `file_is_writeable(filepath: str) -> bool`
|
|
73
|
+
- Checks whether a file can be opened for read/write.
|
|
74
|
+
- `get_version_from_filename(filename: str) -> str`
|
|
75
|
+
- Extracts a numeric version token from a filename.
|
|
76
|
+
|
|
77
|
+
### Folder Utils (`backpack.folder_utils`)
|
|
78
|
+
|
|
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`
|
|
82
|
+
- Creates multiple folders.
|
|
83
|
+
- `create_folder(path: str, force_empty: bool = False, verbose: bool = True) -> bool`
|
|
84
|
+
- Creates a folder and optionally clears it if it already exists.
|
|
85
|
+
- `remove_files_in_dir(path: str) -> None`
|
|
86
|
+
- Removes all files and subdirectories inside a directory.
|
|
87
|
+
- `recursive_dir_copy(source_path: str, target_path: str) -> None`
|
|
88
|
+
- Recursively copies files and subfolders from source to target.
|
|
89
|
+
|
|
90
|
+
### JSON Utils (`backpack.json_utils`)
|
|
91
|
+
|
|
92
|
+
- `json_load(json_file: str) -> dict`
|
|
93
|
+
- Loads JSON data from file with validation and error handling.
|
|
94
|
+
- `json_save(data: object, json_file: str) -> bool`
|
|
95
|
+
- Saves a JSON-serializable value as UTF-8 and returns whether it succeeded.
|
|
96
|
+
|
|
97
|
+
### JSON Metadata (`backpack.json_metadata`)
|
|
98
|
+
|
|
99
|
+
- `JsonMetaFile(name: str, path: str)`
|
|
100
|
+
- Manages a metadata JSON file with package/system/time information.
|
|
101
|
+
- Main public methods:
|
|
102
|
+
- `has_file() -> bool`
|
|
103
|
+
- `load() -> None`
|
|
104
|
+
- `insert(key: str, value: Any) -> None`
|
|
105
|
+
- `remove(key: str) -> None`
|
|
106
|
+
- `save() -> None`
|
|
107
|
+
- `load_as_class() -> type`
|
|
108
|
+
- `insert_class(_class: type) -> None`
|
|
109
|
+
|
|
110
|
+
### JSON User Settings (`backpack.json_user_settings`)
|
|
111
|
+
|
|
112
|
+
- `JsonUserSettings(folder: str, name: str)`
|
|
113
|
+
- Saves and loads JSON settings in the current user's home directory.
|
|
114
|
+
- Main public methods:
|
|
115
|
+
- `save_settings(data: dict | None = None) -> bool`
|
|
116
|
+
- `load_settings() -> dict | bool`
|
|
117
|
+
|
|
118
|
+
### Logger (`backpack.logger`)
|
|
119
|
+
|
|
120
|
+
- `get_logger(name: str) -> logging.Logger`
|
|
121
|
+
- Returns a configured logger with stream handler.
|
|
122
|
+
|
|
123
|
+
### Patterns (`backpack.patterns`)
|
|
124
|
+
|
|
125
|
+
- `Singleton`
|
|
126
|
+
- Base class implementing singleton behavior via `__new__`.
|
|
127
|
+
|
|
128
|
+
### Strings (`backpack.strings`)
|
|
129
|
+
|
|
130
|
+
- `normalize_input_string(input_string: str, under_spaces: bool = True, under_hyphen: bool = True, replacer: str = '_') -> str`
|
|
131
|
+
- Keeps alphanumeric/space/hyphen characters and normalizes separators.
|
|
132
|
+
- `begin_or_end_with_numbers(input_string: str) -> bool`
|
|
133
|
+
- Checks whether first or last character is numeric.
|
|
134
|
+
- `begin_with_number(input_string: str) -> bool`
|
|
135
|
+
- Checks whether the first character is numeric.
|
|
136
|
+
- `has_numbers(input_string: str) -> bool`
|
|
137
|
+
- Checks whether any character is numeric.
|
|
138
|
+
- `camelcase_to_snakecase(input_string: str) -> str`
|
|
139
|
+
- Converts CamelCase to snake_case.
|
|
140
|
+
- Handles acronym prefixes, for example `HTTPServer -> http_server`.
|
|
141
|
+
|
|
142
|
+
### Test Utils (`backpack.test_utils`)
|
|
143
|
+
|
|
144
|
+
- `random_string(length: int = 10) -> str`
|
|
145
|
+
- Generates a random lowercase string.
|
|
146
|
+
- `time_function_decorator(method: Callable[P, R]) -> Callable[P, R]`
|
|
147
|
+
- Decorator that logs execution time while preserving function metadata and return values.
|
|
148
|
+
|
|
149
|
+
## Quick Example
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from backpack.cache import timed_lru_cache
|
|
153
|
+
from backpack.strings import camelcase_to_snakecase, normalize_input_string
|
|
154
|
+
from backpack.json_utils import json_save, json_load
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@timed_lru_cache(seconds=60)
|
|
158
|
+
def expensive_call():
|
|
159
|
+
return {'ok': True}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
value = camelcase_to_snakecase('HTTPServer')
|
|
163
|
+
clean = normalize_input_string('Blade Runner-2049')
|
|
164
|
+
|
|
165
|
+
json_save({'value': value, 'clean': clean}, 'data.json')
|
|
166
|
+
data = json_load('data.json')
|
|
167
|
+
```
|
|
168
|
+
|
|
@@ -43,32 +43,34 @@ 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
|
+
- `get_version_from_filename(filename: str) -> str`
|
|
53
|
+
- Extracts a numeric version token from a filename.
|
|
52
54
|
|
|
53
55
|
### Folder Utils (`backpack.folder_utils`)
|
|
54
56
|
|
|
55
|
-
- `browse_folder(folder: str) -> bool`
|
|
56
|
-
- Opens a folder in Windows Explorer.
|
|
57
|
-
- `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`
|
|
58
60
|
- Creates multiple folders.
|
|
59
|
-
- `create_folder(path: str, force_empty: bool = False, verbose: bool = True)`
|
|
61
|
+
- `create_folder(path: str, force_empty: bool = False, verbose: bool = True) -> bool`
|
|
60
62
|
- Creates a folder and optionally clears it if it already exists.
|
|
61
|
-
- `remove_files_in_dir(path: str)`
|
|
63
|
+
- `remove_files_in_dir(path: str) -> None`
|
|
62
64
|
- Removes all files and subdirectories inside a directory.
|
|
63
|
-
- `recursive_dir_copy(source_path: str, target_path: str)`
|
|
65
|
+
- `recursive_dir_copy(source_path: str, target_path: str) -> None`
|
|
64
66
|
- Recursively copies files and subfolders from source to target.
|
|
65
67
|
|
|
66
68
|
### JSON Utils (`backpack.json_utils`)
|
|
67
69
|
|
|
68
70
|
- `json_load(json_file: str) -> dict`
|
|
69
71
|
- Loads JSON data from file with validation and error handling.
|
|
70
|
-
- `json_save(data:
|
|
71
|
-
- 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.
|
|
72
74
|
|
|
73
75
|
### JSON Metadata (`backpack.json_metadata`)
|
|
74
76
|
|
|
@@ -88,7 +90,7 @@ pip install python-backpack
|
|
|
88
90
|
- `JsonUserSettings(folder: str, name: str)`
|
|
89
91
|
- Saves and loads JSON settings in the current user's home directory.
|
|
90
92
|
- Main public methods:
|
|
91
|
-
- `save_settings(data: dict | None = None) -> bool
|
|
93
|
+
- `save_settings(data: dict | None = None) -> bool`
|
|
92
94
|
- `load_settings() -> dict | bool`
|
|
93
95
|
|
|
94
96
|
### Logger (`backpack.logger`)
|
|
@@ -119,8 +121,8 @@ pip install python-backpack
|
|
|
119
121
|
|
|
120
122
|
- `random_string(length: int = 10) -> str`
|
|
121
123
|
- Generates a random lowercase string.
|
|
122
|
-
- `time_function_decorator(method:
|
|
123
|
-
- 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.
|
|
124
126
|
|
|
125
127
|
## Quick Example
|
|
126
128
|
|
|
@@ -132,7 +134,7 @@ from backpack.json_utils import json_save, json_load
|
|
|
132
134
|
|
|
133
135
|
@timed_lru_cache(seconds=60)
|
|
134
136
|
def expensive_call():
|
|
135
|
-
|
|
137
|
+
return {'ok': True}
|
|
136
138
|
|
|
137
139
|
|
|
138
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:
|