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.
Files changed (47) hide show
  1. python_backpack-2.0.1/.github/workflows/codecov_tests.yml +51 -0
  2. python_backpack-2.0.1/.gitignore +131 -0
  3. python_backpack-2.0.1/PKG-INFO +7 -0
  4. python_backpack-2.0.1/README.md +144 -0
  5. python_backpack-2.0.1/backpack/__init__.py +0 -0
  6. {python_backpack-1.1.4 → python_backpack-2.0.1}/backpack/cache.py +14 -11
  7. {python_backpack-1.1.4 → python_backpack-2.0.1}/backpack/file_utils.py +41 -0
  8. {python_backpack-1.1.4 → python_backpack-2.0.1}/backpack/json_user_settings.py +7 -4
  9. {python_backpack-1.1.4 → python_backpack-2.0.1}/backpack/json_utils.py +1 -1
  10. {python_backpack-1.1.4 → python_backpack-2.0.1}/backpack/strings.py +32 -17
  11. {python_backpack-1.1.4 → python_backpack-2.0.1}/backpack/test_utils.py +1 -1
  12. {python_backpack-1.1.4 → python_backpack-2.0.1}/backpack/version.py +4 -3
  13. python_backpack-2.0.1/backpack.code-workspace +63 -0
  14. python_backpack-2.0.1/codecov.yml +2 -0
  15. python_backpack-2.0.1/coverage.bat +9 -0
  16. python_backpack-2.0.1/pyproject.toml +29 -0
  17. python_backpack-2.0.1/ruff.toml +26 -0
  18. python_backpack-2.0.1/tests/__init__.py +0 -0
  19. python_backpack-2.0.1/tests/test_cache.py +20 -0
  20. python_backpack-2.0.1/tests/test_errors.py +47 -0
  21. python_backpack-2.0.1/tests/test_file_utils.py +71 -0
  22. python_backpack-2.0.1/tests/test_files/edited.txt +9 -0
  23. python_backpack-2.0.1/tests/test_files/edited_remove.txt +5 -0
  24. python_backpack-2.0.1/tests/test_files/locked_file.txt +1 -0
  25. python_backpack-2.0.1/tests/test_files/origin.txt +9 -0
  26. python_backpack-2.0.1/tests/test_files/origin_remove.txt +8 -0
  27. python_backpack-2.0.1/tests/test_files/unlocked_file.txt +0 -0
  28. python_backpack-2.0.1/tests/test_folder_utils.py +105 -0
  29. python_backpack-2.0.1/tests/test_json/data.json +13 -0
  30. python_backpack-2.0.1/tests/test_json/data_broken.json +13 -0
  31. python_backpack-2.0.1/tests/test_json/save/data_saved.json +0 -0
  32. python_backpack-2.0.1/tests/test_json_user_settings.py +90 -0
  33. python_backpack-2.0.1/tests/test_json_utils.py +46 -0
  34. python_backpack-2.0.1/tests/test_jsonmd.py +88 -0
  35. python_backpack-2.0.1/tests/test_misc.py +47 -0
  36. python_backpack-2.0.1/tests/test_strings.py +125 -0
  37. python_backpack-2.0.1/tests/test_test_utils.py +35 -0
  38. python_backpack-2.0.1/uv.lock +295 -0
  39. python_backpack-1.1.4/PKG-INFO +0 -12
  40. python_backpack-1.1.4/pyproject.toml +0 -23
  41. {python_backpack-1.1.4 → python_backpack-2.0.1}/LICENSE +0 -0
  42. {python_backpack-1.1.4/backpack → python_backpack-2.0.1}/__init__.py +0 -0
  43. {python_backpack-1.1.4 → python_backpack-2.0.1}/backpack/custom_errors.py +0 -0
  44. {python_backpack-1.1.4 → python_backpack-2.0.1}/backpack/folder_utils.py +0 -0
  45. {python_backpack-1.1.4 → python_backpack-2.0.1}/backpack/json_metadata.py +0 -0
  46. {python_backpack-1.1.4 → python_backpack-2.0.1}/backpack/logger.py +0 -0
  47. {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
+ [![PyPI Supported Python Versions](https://img.shields.io/pypi/pyversions/python-backpack.svg?style=flat-square&logo=appveyor)](https://pypi.python.org/pypi/python-backpack/)
2
+ [![PyPI version](https://badge.fury.io/py/python-backpack.svg?style=flat-square&logo=appveyor)](https://badge.fury.io/py/python-backpack)
3
+ [![GitHub version](https://badge.fury.io/gh/MaxRocamora%2Fpython-backpack.svg?style=flat-square&logo=appveyor)](https://badge.fury.io/gh/MaxRocamora%2Fpython-backpack)
4
+ [![codecov](https://codecov.io/gh/MaxRocamora/python-backpack/graph/badge.svg?token=6D1xwYdXW2)](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
- def timed_lru_cache(seconds: int, maxsize: int = 128):
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
- func = lru_cache(maxsize=maxsize)(func)
41
- func.lifetime = timedelta(seconds=seconds)
42
- func.expiration = datetime.now(timezone.utc) + func.lifetime
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) >= func.expiration:
60
+ if force_clear or datetime.now(timezone.utc) >= cached_func.expiration:
58
61
  if show_log:
59
- log.debug(f'Cache cleared for {func.__name__}')
62
+ log.debug(f'Cache cleared for {cached_func.__name__}')
60
63
 
61
- func.cache_clear()
62
- func.expiration = datetime.now(timezone.utc) + func.lifetime
64
+ cached_func.cache_clear()
65
+ cached_func.expiration = datetime.now(timezone.utc) + cached_func.lifetime
63
66
 
64
- return func(*args, **kwargs)
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=False) -> bool:
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) : info dictionary to save, if set to False,
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 not data:
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 reformat_input_string(
12
- input_string: str, under_spaces: bool = True, under_hyphen: bool = True
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() and index == 0:
101
- snake_str += char.lower()
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
- snake_str += char
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
@@ -12,7 +12,7 @@ from backpack.logger import get_logger
12
12
  log = get_logger('Python Backpack - TestUtils')
13
13
 
14
14
 
15
- def random_string(length: str = 10) -> str:
15
+ def random_string(length: int = 10) -> str:
16
16
  """Generates a random string of fixed length.
17
17
 
18
18
  Args:
@@ -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 = 1
15
- VERSION_MINOR = 1
16
- VERSION_PATCH = 4
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,2 @@
1
+ ignore:
2
+ - "*/tests/*"
@@ -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