python-backpack 1.1.4__tar.gz → 2.0.1__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/.github/workflows/codecov_tests.yml +51 -0
- python_backpack-2.0.1/.gitignore +131 -0
- python_backpack-2.0.1/PKG-INFO +7 -0
- python_backpack-2.0.1/README.md +144 -0
- python_backpack-2.0.1/backpack/__init__.py +0 -0
- {python_backpack-1.1.4 → python_backpack-2.0.1}/backpack/cache.py +14 -11
- {python_backpack-1.1.4 → python_backpack-2.0.1}/backpack/file_utils.py +41 -0
- {python_backpack-1.1.4 → python_backpack-2.0.1}/backpack/json_user_settings.py +7 -4
- {python_backpack-1.1.4 → python_backpack-2.0.1}/backpack/json_utils.py +1 -1
- {python_backpack-1.1.4 → python_backpack-2.0.1}/backpack/strings.py +32 -17
- {python_backpack-1.1.4 → python_backpack-2.0.1}/backpack/test_utils.py +1 -1
- {python_backpack-1.1.4 → python_backpack-2.0.1}/backpack/version.py +4 -3
- python_backpack-2.0.1/backpack.code-workspace +63 -0
- python_backpack-2.0.1/codecov.yml +2 -0
- python_backpack-2.0.1/coverage.bat +9 -0
- python_backpack-2.0.1/pyproject.toml +29 -0
- python_backpack-2.0.1/ruff.toml +26 -0
- python_backpack-2.0.1/tests/__init__.py +0 -0
- python_backpack-2.0.1/tests/test_cache.py +20 -0
- python_backpack-2.0.1/tests/test_errors.py +47 -0
- python_backpack-2.0.1/tests/test_file_utils.py +71 -0
- python_backpack-2.0.1/tests/test_files/edited.txt +9 -0
- python_backpack-2.0.1/tests/test_files/edited_remove.txt +5 -0
- python_backpack-2.0.1/tests/test_files/locked_file.txt +1 -0
- python_backpack-2.0.1/tests/test_files/origin.txt +9 -0
- python_backpack-2.0.1/tests/test_files/origin_remove.txt +8 -0
- python_backpack-2.0.1/tests/test_files/unlocked_file.txt +0 -0
- python_backpack-2.0.1/tests/test_folder_utils.py +105 -0
- python_backpack-2.0.1/tests/test_json/data.json +13 -0
- python_backpack-2.0.1/tests/test_json/data_broken.json +13 -0
- python_backpack-2.0.1/tests/test_json/save/data_saved.json +0 -0
- python_backpack-2.0.1/tests/test_json_user_settings.py +90 -0
- python_backpack-2.0.1/tests/test_json_utils.py +46 -0
- python_backpack-2.0.1/tests/test_jsonmd.py +88 -0
- python_backpack-2.0.1/tests/test_misc.py +47 -0
- python_backpack-2.0.1/tests/test_strings.py +125 -0
- python_backpack-2.0.1/tests/test_test_utils.py +35 -0
- python_backpack-2.0.1/uv.lock +295 -0
- python_backpack-1.1.4/PKG-INFO +0 -12
- python_backpack-1.1.4/pyproject.toml +0 -23
- {python_backpack-1.1.4 → python_backpack-2.0.1}/LICENSE +0 -0
- {python_backpack-1.1.4/backpack → python_backpack-2.0.1}/__init__.py +0 -0
- {python_backpack-1.1.4 → python_backpack-2.0.1}/backpack/custom_errors.py +0 -0
- {python_backpack-1.1.4 → python_backpack-2.0.1}/backpack/folder_utils.py +0 -0
- {python_backpack-1.1.4 → python_backpack-2.0.1}/backpack/json_metadata.py +0 -0
- {python_backpack-1.1.4 → python_backpack-2.0.1}/backpack/logger.py +0 -0
- {python_backpack-1.1.4 → python_backpack-2.0.1}/backpack/patterns.py +0 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: Python Tests & Coverage
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
- push
|
|
5
|
+
- pull_request
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
strategy:
|
|
11
|
+
matrix:
|
|
12
|
+
python-version: ['3.11']
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v6
|
|
16
|
+
|
|
17
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
18
|
+
uses: actions/setup-python@v6
|
|
19
|
+
with:
|
|
20
|
+
python-version: ${{ matrix.python-version }}
|
|
21
|
+
|
|
22
|
+
- name: Add path to PYTHONPATH
|
|
23
|
+
run: |
|
|
24
|
+
echo "PYTHONPATH=$GITHUB_WORKSPACE" >> $GITHUB_ENV
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: |
|
|
28
|
+
python -m pip install --upgrade pip
|
|
29
|
+
pip install uv
|
|
30
|
+
uv sync --dev
|
|
31
|
+
|
|
32
|
+
- name: Run tests with coverage
|
|
33
|
+
run: |
|
|
34
|
+
uv run coverage run --source=backpack -m pytest
|
|
35
|
+
uv run coverage report -m --fail-under=70
|
|
36
|
+
uv run coverage xml
|
|
37
|
+
|
|
38
|
+
- name: Upload coverage.xml
|
|
39
|
+
if: ${{ matrix.python-version == '3.11' }}
|
|
40
|
+
uses: actions/upload-artifact@v7
|
|
41
|
+
with:
|
|
42
|
+
name: upload coverage
|
|
43
|
+
path: ${{ github.workspace }}/coverage.xml
|
|
44
|
+
if-no-files-found: warn
|
|
45
|
+
|
|
46
|
+
# upload coverage.xml to codecov
|
|
47
|
+
- name: Upload coverage.xml to codecov
|
|
48
|
+
if: ${{ matrix.python-version == '3.11' }}
|
|
49
|
+
uses: codecov/codecov-action@v6
|
|
50
|
+
with:
|
|
51
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
pip-wheel-metadata/
|
|
24
|
+
share/python-wheels/
|
|
25
|
+
*.egg-info/
|
|
26
|
+
.installed.cfg
|
|
27
|
+
*.egg
|
|
28
|
+
MANIFEST
|
|
29
|
+
|
|
30
|
+
# PyInstaller
|
|
31
|
+
# Usually these files are written by a python script from a template
|
|
32
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
33
|
+
*.manifest
|
|
34
|
+
*.spec
|
|
35
|
+
|
|
36
|
+
# Installer logs
|
|
37
|
+
pip-log.txt
|
|
38
|
+
pip-delete-this-directory.txt
|
|
39
|
+
|
|
40
|
+
# Unit test / coverage reports
|
|
41
|
+
htmlcov/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py,cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
|
|
53
|
+
# Translations
|
|
54
|
+
*.mo
|
|
55
|
+
*.pot
|
|
56
|
+
|
|
57
|
+
# Django stuff:
|
|
58
|
+
*.log
|
|
59
|
+
local_settings.py
|
|
60
|
+
db.sqlite3
|
|
61
|
+
db.sqlite3-journal
|
|
62
|
+
|
|
63
|
+
# Flask stuff:
|
|
64
|
+
instance/
|
|
65
|
+
.webassets-cache
|
|
66
|
+
|
|
67
|
+
# Scrapy stuff:
|
|
68
|
+
.scrapy
|
|
69
|
+
|
|
70
|
+
# Sphinx documentation
|
|
71
|
+
docs/_build/
|
|
72
|
+
|
|
73
|
+
# PyBuilder
|
|
74
|
+
target/
|
|
75
|
+
|
|
76
|
+
# Jupyter Notebook
|
|
77
|
+
.ipynb_checkpoints
|
|
78
|
+
|
|
79
|
+
# IPython
|
|
80
|
+
profile_default/
|
|
81
|
+
ipython_config.py
|
|
82
|
+
|
|
83
|
+
# pyenv
|
|
84
|
+
.python-version
|
|
85
|
+
|
|
86
|
+
# pipenv
|
|
87
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
88
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
89
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
90
|
+
# install all needed dependencies.
|
|
91
|
+
#Pipfile.lock
|
|
92
|
+
|
|
93
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
|
94
|
+
__pypackages__/
|
|
95
|
+
|
|
96
|
+
# Celery stuff
|
|
97
|
+
celerybeat-schedule
|
|
98
|
+
celerybeat.pid
|
|
99
|
+
|
|
100
|
+
# SageMath parsed files
|
|
101
|
+
*.sage.py
|
|
102
|
+
|
|
103
|
+
# Environments
|
|
104
|
+
.env
|
|
105
|
+
.venv
|
|
106
|
+
env/
|
|
107
|
+
venv/
|
|
108
|
+
ENV/
|
|
109
|
+
env.bak/
|
|
110
|
+
venv.bak/
|
|
111
|
+
|
|
112
|
+
# Spyder project settings
|
|
113
|
+
.spyderproject
|
|
114
|
+
.spyproject
|
|
115
|
+
|
|
116
|
+
# Rope project settings
|
|
117
|
+
.ropeproject
|
|
118
|
+
|
|
119
|
+
# mkdocs documentation
|
|
120
|
+
/site
|
|
121
|
+
|
|
122
|
+
# mypy
|
|
123
|
+
.mypy_cache/
|
|
124
|
+
.dmypy.json
|
|
125
|
+
dmypy.json
|
|
126
|
+
|
|
127
|
+
# Pyre type checker
|
|
128
|
+
.pyre/
|
|
129
|
+
|
|
130
|
+
# test generated files
|
|
131
|
+
tests/test_json/MD_test.json
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-backpack
|
|
3
|
+
Version: 2.0.1
|
|
4
|
+
Summary: A collection of personal scripts for json, File/Folder Operations, String Validation, Custom Errors, Cache and stuff.
|
|
5
|
+
Author: Maximiliano Rocamora
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.11
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
[](https://pypi.python.org/pypi/python-backpack/)
|
|
2
|
+
[](https://badge.fury.io/py/python-backpack)
|
|
3
|
+
[](https://badge.fury.io/gh/MaxRocamora%2Fpython-backpack)
|
|
4
|
+
[](https://codecov.io/gh/MaxRocamora/python-backpack)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Python-Backpack
|
|
8
|
+
|
|
9
|
+
Python-Backpack is a lightweight utility collection for common scripting tasks:
|
|
10
|
+
|
|
11
|
+
- JSON load/save helpers with validation
|
|
12
|
+
- user settings persistence under the OS user directory
|
|
13
|
+
- metadata export/import to JSON files
|
|
14
|
+
- file and folder operations
|
|
15
|
+
- string normalization and case conversion
|
|
16
|
+
- cache decorator with expiration support
|
|
17
|
+
- custom exceptions, logging helper, singleton pattern, and testing helpers
|
|
18
|
+
|
|
19
|
+
## Compatibility
|
|
20
|
+
|
|
21
|
+
- Python 3.11+
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install python-backpack
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Package API Reference
|
|
30
|
+
|
|
31
|
+
### Cache (`backpack.cache`)
|
|
32
|
+
|
|
33
|
+
- `timed_lru_cache(seconds: int, maxsize: int = 128)`
|
|
34
|
+
- `functools.lru_cache` decorator with expiration time.
|
|
35
|
+
- Wrapped calls support `force_clear=True` and `show_log=True`.
|
|
36
|
+
|
|
37
|
+
### Custom Errors (`backpack.custom_errors`)
|
|
38
|
+
|
|
39
|
+
- `EnvironmentVariableNotFoundError(var_name: str)`
|
|
40
|
+
- Raised when a required environment variable is missing.
|
|
41
|
+
- `ApplicationNotFoundError(app_name: str)`
|
|
42
|
+
- Raised when a required application is not found.
|
|
43
|
+
|
|
44
|
+
### File Utils (`backpack.file_utils`)
|
|
45
|
+
|
|
46
|
+
- `replace_strings_in_file(ascii_file: str, strings: list, new_string: str) -> None`
|
|
47
|
+
- Replaces multiple string occurrences in a text file.
|
|
48
|
+
- `remove_line_from_file(ascii_file: str, strings: list, verbose: bool = False) -> None`
|
|
49
|
+
- Removes exact matching lines from a text file.
|
|
50
|
+
- `file_is_writeable(filepath: str) -> bool`
|
|
51
|
+
- Checks whether a file can be opened for read/write.
|
|
52
|
+
|
|
53
|
+
### Folder Utils (`backpack.folder_utils`)
|
|
54
|
+
|
|
55
|
+
- `browse_folder(folder: str) -> bool`
|
|
56
|
+
- Opens a folder in Windows Explorer.
|
|
57
|
+
- `create_folders(folders: list, force_empty: bool = False, verbose: bool = False)`
|
|
58
|
+
- Creates multiple folders.
|
|
59
|
+
- `create_folder(path: str, force_empty: bool = False, verbose: bool = True)`
|
|
60
|
+
- Creates a folder and optionally clears it if it already exists.
|
|
61
|
+
- `remove_files_in_dir(path: str)`
|
|
62
|
+
- Removes all files and subdirectories inside a directory.
|
|
63
|
+
- `recursive_dir_copy(source_path: str, target_path: str)`
|
|
64
|
+
- Recursively copies files and subfolders from source to target.
|
|
65
|
+
|
|
66
|
+
### JSON Utils (`backpack.json_utils`)
|
|
67
|
+
|
|
68
|
+
- `json_load(json_file: str) -> dict`
|
|
69
|
+
- Loads JSON data from file with validation and error handling.
|
|
70
|
+
- `json_save(data: dict, json_file: str) -> bool`
|
|
71
|
+
- Saves a dictionary to JSON file.
|
|
72
|
+
|
|
73
|
+
### JSON Metadata (`backpack.json_metadata`)
|
|
74
|
+
|
|
75
|
+
- `JsonMetaFile(name: str, path: str)`
|
|
76
|
+
- Manages a metadata JSON file with package/system/time information.
|
|
77
|
+
- Main public methods:
|
|
78
|
+
- `has_file() -> bool`
|
|
79
|
+
- `load() -> None`
|
|
80
|
+
- `insert(key: str, value: Any) -> None`
|
|
81
|
+
- `remove(key: str) -> None`
|
|
82
|
+
- `save() -> None`
|
|
83
|
+
- `load_as_class() -> type`
|
|
84
|
+
- `insert_class(_class: type) -> None`
|
|
85
|
+
|
|
86
|
+
### JSON User Settings (`backpack.json_user_settings`)
|
|
87
|
+
|
|
88
|
+
- `JsonUserSettings(folder: str, name: str)`
|
|
89
|
+
- Saves and loads JSON settings in the current user's home directory.
|
|
90
|
+
- Main public methods:
|
|
91
|
+
- `save_settings(data: dict | None = None) -> bool | None`
|
|
92
|
+
- `load_settings() -> dict | bool`
|
|
93
|
+
|
|
94
|
+
### Logger (`backpack.logger`)
|
|
95
|
+
|
|
96
|
+
- `get_logger(name: str) -> logging.Logger`
|
|
97
|
+
- Returns a configured logger with stream handler.
|
|
98
|
+
|
|
99
|
+
### Patterns (`backpack.patterns`)
|
|
100
|
+
|
|
101
|
+
- `Singleton`
|
|
102
|
+
- Base class implementing singleton behavior via `__new__`.
|
|
103
|
+
|
|
104
|
+
### Strings (`backpack.strings`)
|
|
105
|
+
|
|
106
|
+
- `normalize_input_string(input_string: str, under_spaces: bool = True, under_hyphen: bool = True, replacer: str = '_') -> str`
|
|
107
|
+
- Keeps alphanumeric/space/hyphen characters and normalizes separators.
|
|
108
|
+
- `begin_or_end_with_numbers(input_string: str) -> bool`
|
|
109
|
+
- Checks whether first or last character is numeric.
|
|
110
|
+
- `begin_with_number(input_string: str) -> bool`
|
|
111
|
+
- Checks whether the first character is numeric.
|
|
112
|
+
- `has_numbers(input_string: str) -> bool`
|
|
113
|
+
- Checks whether any character is numeric.
|
|
114
|
+
- `camelcase_to_snakecase(input_string: str) -> str`
|
|
115
|
+
- Converts CamelCase to snake_case.
|
|
116
|
+
- Handles acronym prefixes, for example `HTTPServer -> http_server`.
|
|
117
|
+
|
|
118
|
+
### Test Utils (`backpack.test_utils`)
|
|
119
|
+
|
|
120
|
+
- `random_string(length: int = 10) -> str`
|
|
121
|
+
- Generates a random lowercase string.
|
|
122
|
+
- `time_function_decorator(method: type)`
|
|
123
|
+
- Decorator that logs execution time.
|
|
124
|
+
|
|
125
|
+
## Quick Example
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from backpack.cache import timed_lru_cache
|
|
129
|
+
from backpack.strings import camelcase_to_snakecase, normalize_input_string
|
|
130
|
+
from backpack.json_utils import json_save, json_load
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@timed_lru_cache(seconds=60)
|
|
134
|
+
def expensive_call():
|
|
135
|
+
return {'ok': True}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
value = camelcase_to_snakecase('HTTPServer')
|
|
139
|
+
clean = normalize_input_string('Blade Runner-2049')
|
|
140
|
+
|
|
141
|
+
json_save({'value': value, 'clean': clean}, 'data.json')
|
|
142
|
+
data = json_load('data.json')
|
|
143
|
+
```
|
|
144
|
+
|
|
File without changes
|
|
@@ -5,13 +5,16 @@
|
|
|
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
9
|
|
|
9
10
|
from backpack.logger import get_logger
|
|
10
11
|
|
|
11
12
|
log = get_logger('Python Backpack - Cache')
|
|
12
13
|
|
|
14
|
+
F = TypeVar('F', bound=Callable[..., Any])
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
|
|
17
|
+
def timed_lru_cache(seconds: int, maxsize: int = 128) -> Callable[[F], F]:
|
|
15
18
|
"""Lru_cache with expiration time.
|
|
16
19
|
|
|
17
20
|
Args:
|
|
@@ -36,10 +39,10 @@ def timed_lru_cache(seconds: int, maxsize: int = 128):
|
|
|
36
39
|
|
|
37
40
|
"""
|
|
38
41
|
|
|
39
|
-
def wrapper_cache(func):
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
def wrapper_cache(func: F) -> F:
|
|
43
|
+
cached_func = cast(Any, lru_cache(maxsize=maxsize)(func))
|
|
44
|
+
cached_func.lifetime = timedelta(seconds=seconds)
|
|
45
|
+
cached_func.expiration = datetime.now(timezone.utc) + cached_func.lifetime
|
|
43
46
|
|
|
44
47
|
@wraps(func)
|
|
45
48
|
def wrapped_func(*args, force_clear: bool = False, show_log: bool = False, **kwargs):
|
|
@@ -54,15 +57,15 @@ def timed_lru_cache(seconds: int, maxsize: int = 128):
|
|
|
54
57
|
function result
|
|
55
58
|
"""
|
|
56
59
|
|
|
57
|
-
if force_clear or datetime.now(timezone.utc) >=
|
|
60
|
+
if force_clear or datetime.now(timezone.utc) >= cached_func.expiration:
|
|
58
61
|
if show_log:
|
|
59
|
-
log.debug(f'Cache cleared for {
|
|
62
|
+
log.debug(f'Cache cleared for {cached_func.__name__}')
|
|
60
63
|
|
|
61
|
-
|
|
62
|
-
|
|
64
|
+
cached_func.cache_clear()
|
|
65
|
+
cached_func.expiration = datetime.now(timezone.utc) + cached_func.lifetime
|
|
63
66
|
|
|
64
|
-
return
|
|
67
|
+
return cached_func(*args, **kwargs)
|
|
65
68
|
|
|
66
|
-
return wrapped_func
|
|
69
|
+
return cast(F, wrapped_func)
|
|
67
70
|
|
|
68
71
|
return wrapper_cache
|
|
@@ -84,3 +84,44 @@ def file_is_writeable(filepath: str) -> bool:
|
|
|
84
84
|
log.info(x.strerror)
|
|
85
85
|
|
|
86
86
|
return False
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_version_from_filename(filename: str) -> str:
|
|
90
|
+
"""Extracts version from filename.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
filename: (str) file name to extract version from
|
|
94
|
+
Returns:
|
|
95
|
+
str : version extracted from filename
|
|
96
|
+
|
|
97
|
+
Examples:
|
|
98
|
+
get_version_from_filename('myfile_23.txt') -> '23'
|
|
99
|
+
get_version_from_filename('myfile-9.txt') -> '9'
|
|
100
|
+
get_version_from_filename('myfile.130.txt') -> '130'
|
|
101
|
+
get_version_from_filename('myfile_v1002.txt') -> '1002'
|
|
102
|
+
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
filename_no_ext = filename.rsplit('.', 1)[0]
|
|
106
|
+
|
|
107
|
+
# guess is the separator is an underscore, dash, dot or v, and the version is the last part before the extension
|
|
108
|
+
separators = ['_', '-', '.', 'v']
|
|
109
|
+
if not any(sep in filename_no_ext for sep in separators):
|
|
110
|
+
log.info(f'No separator found in filename: {filename_no_ext}. Using fallback extraction.')
|
|
111
|
+
else:
|
|
112
|
+
for sep in separators:
|
|
113
|
+
if sep in filename_no_ext:
|
|
114
|
+
parts = filename_no_ext.split(sep)
|
|
115
|
+
version_part = parts[-1].split('.')[0] # Get the last part before the extension
|
|
116
|
+
if version_part.replace('.', '').isdigit(): # Check if it's a valid version number
|
|
117
|
+
log.info(
|
|
118
|
+
f'Extracted version: {version_part} from filename: {filename} using separator: {sep}'
|
|
119
|
+
)
|
|
120
|
+
return version_part
|
|
121
|
+
|
|
122
|
+
# Fallback: extract from the entire filename
|
|
123
|
+
version = filename_no_ext.lstrip('v')
|
|
124
|
+
version = version.replace('_', '.').replace('-', '.')
|
|
125
|
+
version = version if version.replace('.', '').isdigit() else '0'
|
|
126
|
+
log.info(f'Extracted version: {version} from filename: {filename}')
|
|
127
|
+
return version
|
|
@@ -9,6 +9,7 @@ us.save(someDict)
|
|
|
9
9
|
data = us.load()
|
|
10
10
|
|
|
11
11
|
"""
|
|
12
|
+
|
|
12
13
|
# ----------------------------------------------------------------------------------------
|
|
13
14
|
import os
|
|
14
15
|
|
|
@@ -59,14 +60,16 @@ class JsonUserSettings:
|
|
|
59
60
|
|
|
60
61
|
return True
|
|
61
62
|
|
|
62
|
-
def save_settings(self, data=
|
|
63
|
+
def save_settings(self, data: dict | None = None) -> bool | None:
|
|
63
64
|
"""Saves a dictionary into a json file (os user path).
|
|
64
65
|
|
|
65
66
|
Args:
|
|
66
|
-
data (dictionary)
|
|
67
|
+
data (dictionary): info dictionary to save, if not provided,
|
|
67
68
|
saves instead local self.user_data property
|
|
69
|
+
Returns:
|
|
70
|
+
bool | None: True if file was saved, False if error, None if no data to save.
|
|
68
71
|
"""
|
|
69
|
-
if
|
|
72
|
+
if data is None:
|
|
70
73
|
data = self.user_data
|
|
71
74
|
|
|
72
75
|
r = json_save(data, self.filepath)
|
|
@@ -74,7 +77,7 @@ class JsonUserSettings:
|
|
|
74
77
|
log.info('json settings file saved! [%s]', self.filepath)
|
|
75
78
|
return r
|
|
76
79
|
|
|
77
|
-
def load_settings(self) -> dict:
|
|
80
|
+
def load_settings(self) -> dict | bool:
|
|
78
81
|
"""Load json file from path and returns its contents."""
|
|
79
82
|
try:
|
|
80
83
|
return json_load(self.filepath)
|
|
@@ -40,7 +40,7 @@ def json_save(data: dict, json_file: str) -> bool:
|
|
|
40
40
|
"""
|
|
41
41
|
|
|
42
42
|
if not os.path.exists(os.path.dirname(json_file)):
|
|
43
|
-
os.makedirs(os.path.dirname(json_file))
|
|
43
|
+
os.makedirs(os.path.dirname(json_file), exist_ok=True)
|
|
44
44
|
|
|
45
45
|
try:
|
|
46
46
|
with open(json_file, 'w') as f:
|
|
@@ -8,8 +8,11 @@ import contextlib
|
|
|
8
8
|
import re
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
def
|
|
12
|
-
input_string: str,
|
|
11
|
+
def normalize_input_string(
|
|
12
|
+
input_string: str,
|
|
13
|
+
under_spaces: bool = True,
|
|
14
|
+
under_hyphen: bool = True,
|
|
15
|
+
replacer: str = '_',
|
|
13
16
|
) -> str:
|
|
14
17
|
"""Reformat the user input string.
|
|
15
18
|
|
|
@@ -20,6 +23,7 @@ def reformat_input_string(
|
|
|
20
23
|
input_string (str): input string to reformat
|
|
21
24
|
under_spaces (bool, optional): replaces spaces. Defaults to True.
|
|
22
25
|
under_hyphen (bool, optional): replaces hyphens. Defaults to True.
|
|
26
|
+
replacer (str, optional): char to replace spaces and hyphens. Defaults to '_'.
|
|
23
27
|
|
|
24
28
|
Returns:
|
|
25
29
|
str: reformatted string
|
|
@@ -30,13 +34,13 @@ def reformat_input_string(
|
|
|
30
34
|
regex = re.sub('[^A-Za-z0-9 _-]+', '', input_string)
|
|
31
35
|
|
|
32
36
|
for char in regex:
|
|
33
|
-
# replacing " " for
|
|
37
|
+
# replacing " " for replacer
|
|
34
38
|
if under_spaces and char == ' ':
|
|
35
|
-
regex = regex.replace(' ',
|
|
39
|
+
regex = regex.replace(' ', replacer)
|
|
36
40
|
|
|
37
|
-
# replacing "-" for
|
|
41
|
+
# replacing "-" for replacer
|
|
38
42
|
if under_hyphen and char == '-':
|
|
39
|
-
regex = regex.replace('-',
|
|
43
|
+
regex = regex.replace('-', replacer)
|
|
40
44
|
|
|
41
45
|
return regex
|
|
42
46
|
|
|
@@ -85,6 +89,7 @@ def camelcase_to_snakecase(input_string: str) -> str:
|
|
|
85
89
|
Examples:
|
|
86
90
|
OneDayCaseChar > one_day_case_char
|
|
87
91
|
oneCamelCaseChar > one_camel_case_char
|
|
92
|
+
HTTPServer > http_server
|
|
88
93
|
_TwoCamel_CaseChar > _two_camel_case_char
|
|
89
94
|
_LeadingUnderscore_case_ > _leading_underscore_case_
|
|
90
95
|
"""
|
|
@@ -97,18 +102,28 @@ def camelcase_to_snakecase(input_string: str) -> str:
|
|
|
97
102
|
snake_str += char
|
|
98
103
|
continue
|
|
99
104
|
|
|
100
|
-
if char.isupper()
|
|
101
|
-
snake_str += char
|
|
102
|
-
continue
|
|
103
|
-
|
|
104
|
-
if char.isupper():
|
|
105
|
-
if input_string[index - 1] != '_':
|
|
106
|
-
snake_str += '_' + char.lower()
|
|
107
|
-
else:
|
|
108
|
-
snake_str += char.lower()
|
|
109
|
-
|
|
105
|
+
if not char.isupper():
|
|
106
|
+
snake_str += char
|
|
110
107
|
continue
|
|
111
108
|
|
|
112
|
-
|
|
109
|
+
prev_char = input_string[index - 1] if index > 0 else ''
|
|
110
|
+
next_char = input_string[index + 1] if index + 1 < len(input_string) else ''
|
|
111
|
+
|
|
112
|
+
# Add underscore only at real word boundaries.
|
|
113
|
+
# This keeps acronyms together: HTTPServer -> http_server.
|
|
114
|
+
should_add_underscore = (
|
|
115
|
+
index > 0
|
|
116
|
+
and prev_char != '_'
|
|
117
|
+
and (
|
|
118
|
+
prev_char.islower()
|
|
119
|
+
or prev_char.isdigit()
|
|
120
|
+
or (prev_char.isupper() and next_char.islower())
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if should_add_underscore:
|
|
125
|
+
snake_str += '_'
|
|
126
|
+
|
|
127
|
+
snake_str += char.lower()
|
|
113
128
|
|
|
114
129
|
return snake_str
|
|
@@ -9,11 +9,12 @@
|
|
|
9
9
|
# 1.1.2 07/2025 - Update actions versions, remove unused badge from README
|
|
10
10
|
# 1.1.3 07/2025 - Improve test class names and docstrings for clarity, ruff formatting
|
|
11
11
|
# 1.1.4 07/2025 - Moved from pipenv to poetry for dependency management.toml
|
|
12
|
+
# 2.0.1 05/2026 - Remove tox workflow and setup.py, standardize uv + coverage tooling
|
|
12
13
|
# ----------------------------------------------------------------------------------------
|
|
13
14
|
|
|
14
|
-
VERSION_MAJOR =
|
|
15
|
-
VERSION_MINOR =
|
|
16
|
-
VERSION_PATCH =
|
|
15
|
+
VERSION_MAJOR = 2
|
|
16
|
+
VERSION_MINOR = 0
|
|
17
|
+
VERSION_PATCH = 1
|
|
17
18
|
|
|
18
19
|
version = f'{VERSION_MAJOR}.{VERSION_MINOR}.{VERSION_PATCH}'
|
|
19
20
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"folders": [
|
|
3
|
+
{
|
|
4
|
+
"path": "."
|
|
5
|
+
}
|
|
6
|
+
],
|
|
7
|
+
"settings": {
|
|
8
|
+
"python.analysis.completeFunctionParens": false,
|
|
9
|
+
"python.analysis.autoImportCompletions": false,
|
|
10
|
+
"editor.formatOnSave": true,
|
|
11
|
+
"editor.formatOnPaste": false,
|
|
12
|
+
"editor.codeActionsOnSave": {
|
|
13
|
+
"source.organizeImports": "never"
|
|
14
|
+
},
|
|
15
|
+
"editor.rulers": [
|
|
16
|
+
90
|
|
17
|
+
],
|
|
18
|
+
"editor.defaultFormatter": "charliermarsh.ruff",
|
|
19
|
+
"editor.wordWrap": "wordWrapColumn",
|
|
20
|
+
"editor.wordWrapColumn": 120,
|
|
21
|
+
"workbench.colorTheme": "Monokai Pro (Filter Spectrum)",
|
|
22
|
+
"workbench.colorCustomizations": {
|
|
23
|
+
"titleBar.activeBackground": "#f36edf",
|
|
24
|
+
"titleBar.activeForeground": "#0c0b0b"
|
|
25
|
+
},
|
|
26
|
+
"python.analysis.extraPaths": [],
|
|
27
|
+
"python.testing.unittestArgs": [
|
|
28
|
+
"-v",
|
|
29
|
+
"-s",
|
|
30
|
+
".",
|
|
31
|
+
"-p",
|
|
32
|
+
"*test*.py"
|
|
33
|
+
],
|
|
34
|
+
"python.testing.pytestEnabled": false,
|
|
35
|
+
"python.testing.unittestEnabled": true,
|
|
36
|
+
"terminal.integrated.fontSize": 12,
|
|
37
|
+
"cSpell.words": [
|
|
38
|
+
"autopep",
|
|
39
|
+
"camelcase",
|
|
40
|
+
"charliermarsh",
|
|
41
|
+
"codecov",
|
|
42
|
+
"docstrings",
|
|
43
|
+
"ftime",
|
|
44
|
+
"jsonfile",
|
|
45
|
+
"levelname",
|
|
46
|
+
"Maximiliano",
|
|
47
|
+
"Monokai",
|
|
48
|
+
"numpy",
|
|
49
|
+
"Parens",
|
|
50
|
+
"pipenv",
|
|
51
|
+
"pycodestyle",
|
|
52
|
+
"pydocstyle",
|
|
53
|
+
"pylint",
|
|
54
|
+
"pypi",
|
|
55
|
+
"pytest",
|
|
56
|
+
"PYTHONPATH",
|
|
57
|
+
"Rocamora",
|
|
58
|
+
"runn",
|
|
59
|
+
"snakecase",
|
|
60
|
+
"strerror"
|
|
61
|
+
],
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
REM Run tests with coverage and generate reports.
|
|
3
|
+
uv run coverage run --source=backpack -m pytest
|
|
4
|
+
if errorlevel 1 exit /b %errorlevel%
|
|
5
|
+
uv run coverage report -m --fail-under=70
|
|
6
|
+
if errorlevel 1 exit /b %errorlevel%
|
|
7
|
+
uv run coverage html
|
|
8
|
+
if errorlevel 1 exit /b %errorlevel%
|
|
9
|
+
uv run coverage xml
|