pyazrico 0.1.0__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.
@@ -0,0 +1,14 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .venv/
7
+ venv/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ .coverage
12
+ htmlcov/
13
+ .idea/
14
+ .vscode/
pyazrico-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Azrideus
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.5
2
+ Name: pyazrico
3
+ Version: 0.1.0
4
+ Summary: Shared utilities used across Azrico Python packages.
5
+ Author-email: Azrideus <gebraengineer@gmail.com>
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Keywords: helpers,utilities
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.10
16
+ Provides-Extra: dev
17
+ Requires-Dist: build>=1.2; extra == 'dev'
18
+ Requires-Dist: mypy>=1.10; extra == 'dev'
19
+ Requires-Dist: pytest-cov>=5; extra == 'dev'
20
+ Requires-Dist: pytest>=8; extra == 'dev'
21
+ Requires-Dist: ruff>=0.6; extra == 'dev'
22
+ Requires-Dist: twine>=5; extra == 'dev'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # pyazrico
26
+
27
+ Shared utilities used across my Python packages.
28
+
29
+ Installed as `pyazrico`, imported as `azrico`.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install pyazrico
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ```python
40
+ from azrico import str_utils
41
+ ```
42
+
43
+ ## Modules
44
+
45
+ | Module | Description |
46
+ | ----------------- | ----------------- |
47
+ | `azrico.str_utils` | String utilities |
48
+
49
+ ## Development
50
+
51
+ ```bash
52
+ python -m venv .venv
53
+ .venv\Scripts\activate # Windows
54
+ # source .venv/bin/activate # macOS / Linux
55
+ pip install -e ".[dev]"
56
+
57
+ pytest # run tests
58
+ ruff check . # lint
59
+ mypy # type-check
60
+ ```
61
+
62
+ ## Releasing to PyPI
63
+
64
+ 1. Bump `__version__` in `src/azrico/__init__.py`.
65
+ 2. Build and upload:
66
+
67
+ ```bash
68
+ rm -rf dist
69
+ python -m build
70
+ twine check dist/*
71
+ twine upload dist/* # or --repository testpypi first
72
+ ```
73
+
74
+ ## License
75
+
76
+ MIT
@@ -0,0 +1,52 @@
1
+ # pyazrico
2
+
3
+ Shared utilities used across my Python packages.
4
+
5
+ Installed as `pyazrico`, imported as `azrico`.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install pyazrico
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```python
16
+ from azrico import str_utils
17
+ ```
18
+
19
+ ## Modules
20
+
21
+ | Module | Description |
22
+ | ----------------- | ----------------- |
23
+ | `azrico.str_utils` | String utilities |
24
+
25
+ ## Development
26
+
27
+ ```bash
28
+ python -m venv .venv
29
+ .venv\Scripts\activate # Windows
30
+ # source .venv/bin/activate # macOS / Linux
31
+ pip install -e ".[dev]"
32
+
33
+ pytest # run tests
34
+ ruff check . # lint
35
+ mypy # type-check
36
+ ```
37
+
38
+ ## Releasing to PyPI
39
+
40
+ 1. Bump `__version__` in `src/azrico/__init__.py`.
41
+ 2. Build and upload:
42
+
43
+ ```bash
44
+ rm -rf dist
45
+ python -m build
46
+ twine check dist/*
47
+ twine upload dist/* # or --repository testpypi first
48
+ ```
49
+
50
+ ## License
51
+
52
+ MIT
@@ -0,0 +1,62 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.26"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "pyazrico"
7
+ dynamic = ["version"]
8
+ description = "Shared utilities used across Azrico Python packages."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Azrideus", email = "gebraengineer@gmail.com" }]
14
+ keywords = ["utilities", "helpers"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Typing :: Typed",
22
+ ]
23
+ dependencies = []
24
+
25
+ [project.optional-dependencies]
26
+ dev = [
27
+ "pytest>=8",
28
+ "pytest-cov>=5",
29
+ "ruff>=0.6",
30
+ "mypy>=1.10",
31
+ "build>=1.2",
32
+ "twine>=5",
33
+ ]
34
+
35
+ # [project.urls]
36
+ # Homepage = "https://github.com/<user>/pyazrico"
37
+ # Issues = "https://github.com/<user>/pyazrico/issues"
38
+
39
+ [tool.hatch.version]
40
+ path = "src/azrico/__init__.py"
41
+
42
+ [tool.hatch.build.targets.wheel]
43
+ packages = ["src/azrico"]
44
+
45
+ [tool.hatch.build.targets.sdist]
46
+ include = ["src/azrico", "tests", "README.md", "LICENSE"]
47
+
48
+ [tool.pytest.ini_options]
49
+ testpaths = ["tests"]
50
+ addopts = "-ra"
51
+
52
+ [tool.ruff]
53
+ line-length = 100
54
+ target-version = "py310"
55
+
56
+ [tool.ruff.lint]
57
+ select = ["E", "F", "W", "I", "B", "UP"]
58
+
59
+ [tool.mypy]
60
+ python_version = "3.10"
61
+ strict = true
62
+ files = ["src"]
@@ -0,0 +1,41 @@
1
+ import re
2
+ from collections.abc import Iterable
3
+
4
+
5
+ class NameMatcher:
6
+ """Exact match, then a unique prefix ("mon"), then a unique per-part prefix ("g-1", "g1" -> "guard-1").
7
+
8
+ Parts are runs of letters or digits; a letter part matches by prefix, a number part only in full.
9
+ """
10
+
11
+ @staticmethod
12
+ def match(query: str, names: Iterable[str]) -> list[str]:
13
+ """The single resolved name, or every candidate when ambiguous, or [] when nothing matches."""
14
+ query = query.casefold()
15
+ words = NameMatcher._parts(query)
16
+ exact: list[str] = []
17
+ prefix: list[str] = []
18
+ parts: list[str] = []
19
+ for name in names:
20
+ key = name.casefold()
21
+ key_words = NameMatcher._parts(key)
22
+ if key == query:
23
+ exact.append(name)
24
+ if key.startswith(query):
25
+ prefix.append(name)
26
+ if len(words) == len(key_words) and all(
27
+ map(NameMatcher._part_matches, key_words, words)
28
+ ):
29
+ parts.append(name)
30
+ for found in (exact, prefix, parts):
31
+ if len(found) == 1:
32
+ return found
33
+ return sorted(set(prefix + parts))
34
+
35
+ @staticmethod
36
+ def _parts(name: str) -> list[str]:
37
+ return re.findall(r"[^\W\d_]+|\d+", name)
38
+
39
+ @staticmethod
40
+ def _part_matches(key_word: str, word: str) -> bool:
41
+ return key_word == word if word.isdigit() else key_word.startswith(word)
@@ -0,0 +1,8 @@
1
+ """Azrico: shared utilities used across Azrico Python packages."""
2
+
3
+ from .classes import CapacityDict
4
+ from .NameMatcher import NameMatcher
5
+
6
+ __version__ = "0.1.0"
7
+
8
+ __all__ = ["CapacityDict", "NameMatcher", "__version__"]
@@ -0,0 +1,17 @@
1
+ from collections import OrderedDict
2
+
3
+
4
+ class CapacityDict(OrderedDict):
5
+ """
6
+ Dict with a maximum capacity. When the capacity is exceeded, the oldest item is removed.
7
+ """
8
+
9
+ def __init__(self, capacity: int, *args, **kwargs):
10
+ self.capacity = capacity
11
+ super().__init__(*args, **kwargs)
12
+
13
+ def __setitem__(self, key, value):
14
+ # If the key is new and we are at capacity, evict the oldest item
15
+ if key not in self and len(self) >= self.capacity:
16
+ self.popitem(last=False) # last=False pops the FIFO (oldest) item
17
+ super().__setitem__(key, value)
@@ -0,0 +1,3 @@
1
+ from .CapacityDict import CapacityDict
2
+
3
+ __all__ = ["CapacityDict"]
@@ -0,0 +1,35 @@
1
+ """File utilities."""
2
+
3
+ import glob
4
+ import hashlib
5
+ import os
6
+
7
+ # Defaults for `file_list_hashed`.
8
+ SKIP_EXTS = frozenset({".pyc", ".pyo", ".tmp", ".log"})
9
+ SKIP_DIR_NAMES = frozenset({".git", "__pycache__", "node_modules", ".venv", "venv"})
10
+
11
+
12
+ def hash_file(path) -> str:
13
+ """SHA-256 of a file's contents, streamed in 1 MiB blocks."""
14
+ digest = hashlib.sha256()
15
+ with open(path, "rb") as f:
16
+ for block in iter(lambda: f.read(1024 * 1024), b""):
17
+ digest.update(block)
18
+ return digest.hexdigest()
19
+
20
+
21
+ def file_list_hashed(
22
+ requested_dir, *, skip_exts=SKIP_EXTS, skip_dir_names=SKIP_DIR_NAMES
23
+ ) -> dict[str, str]:
24
+ """Map every file under `requested_dir` (recursive) to its content hash, skipping
25
+ `skip_dir_names` directories and `skip_exts` file types."""
26
+ files = {}
27
+ for path in glob.glob(f"{requested_dir}/**/*", recursive=True):
28
+ if os.path.isdir(path):
29
+ continue
30
+ if any(part in skip_dir_names for part in path.split(os.sep)):
31
+ continue
32
+ if os.path.splitext(path)[1].lower() in skip_exts:
33
+ continue
34
+ files[path] = hash_file(path)
35
+ return files
@@ -0,0 +1,23 @@
1
+ def parse_bool(value) -> bool:
2
+ """Parse common string boolean values, falling back to normal truthiness."""
3
+ if isinstance(value, str):
4
+ return value.strip().lower() in (
5
+ "1",
6
+ "true",
7
+ "on",
8
+ "ok",
9
+ "yes",
10
+ "y",
11
+ "ye",
12
+ "yep",
13
+ "yeah",
14
+ "continue",
15
+ "go",
16
+ "good",
17
+ "sure",
18
+ "okay",
19
+ "proceed",
20
+ "confirm",
21
+ "confirmed",
22
+ )
23
+ return bool(value)
File without changes
@@ -0,0 +1,91 @@
1
+ """String utilities."""
2
+
3
+ import json
4
+ import re
5
+
6
+
7
+ def str_loose_match(a: str, b: str) -> bool:
8
+ """Case-insensitive equality after stripping all whitespace and dash and underscores."""
9
+ bad_chars = re.compile(r"[\s\-_]+")
10
+ a = bad_chars.sub("", a or "").casefold()
11
+ b = bad_chars.sub("", b or "").casefold()
12
+ return a == b
13
+
14
+
15
+ def str_loose_in(value: str, items) -> bool:
16
+ """`value in items`, compared with `str_loose_match`."""
17
+ return any(str_loose_match(value, item) for item in items or ())
18
+
19
+
20
+ def slugify(text: str, maxlen: int = 48) -> str:
21
+ """Lowercase [a-z0-9-] slug, truncated at a word boundary (no mid-word cut).
22
+ Empty / all-punctuation input yields "default"."""
23
+ return m_sanitize_text(text, replacement="-", maxlen=maxlen, default="default")
24
+
25
+
26
+ def m_sanitize_text(
27
+ value: str,
28
+ *,
29
+ keep: str = "",
30
+ replacement: str = "",
31
+ lowercase: bool = True,
32
+ maxlen: int = 0,
33
+ default: str = "",
34
+ ) -> str:
35
+ """Keep only [A-Za-z0-9] plus the punctuation listed in `keep`.
36
+
37
+ Every run of other characters becomes `replacement`, which is then trimmed
38
+ off both ends. `keep` is "_-" for slugs, "._-" for filenames, "" (the
39
+ default) for alphanumeric-only. `maxlen` truncates at the last
40
+ `replacement` boundary so a word is never cut in half, and `default` is
41
+ returned when nothing survives.
42
+ """
43
+ s = re.sub(f"[^A-Za-z0-9{re.escape(keep)}]+", replacement, (value or "").strip())
44
+ if lowercase:
45
+ s = s.lower()
46
+ if replacement:
47
+ s = s.strip(replacement)
48
+ if maxlen and len(s) > maxlen:
49
+ cut = s[:maxlen]
50
+ if replacement in cut[maxlen // 2 :]:
51
+ cut = cut[: cut.rfind(replacement)]
52
+ s = cut.strip(replacement)
53
+ elif maxlen:
54
+ s = s[:maxlen]
55
+ return s or default
56
+
57
+
58
+ def basic_str_array(v, whitespace_seperated: bool = False, unique: bool = False) -> list[str]:
59
+ """
60
+ converts input to a list
61
+ split by , \n | or \\, and strips whitespace from each element.
62
+ unique=True drops duplicates, keeping first-seen order.
63
+ A bracketed string is read as an array: "[1, 2]" and '["1", "2"]' -> ["1", "2"]
64
+ """
65
+ if isinstance(v, str) and v.strip().startswith("[") and v.strip().endswith("]"):
66
+ try:
67
+ v = json.loads(v)
68
+ except ValueError:
69
+ v = [x.strip().strip("\"'") for x in v.strip()[1:-1].split(",")]
70
+ if isinstance(v, str):
71
+ has_comma = "," in v
72
+ v = v.replace("\\", ",").replace("\n", ",").replace("|", ",")
73
+ # a comma seperated string should not be considred whitespace seperated.
74
+ if not has_comma and whitespace_seperated:
75
+ v = re.sub(r"\s+", ",", v)
76
+ v = v.split(",")
77
+ if not isinstance(v, list):
78
+ return []
79
+ items = [str(x).strip() for x in v if str(x).strip()]
80
+ return list(dict.fromkeys(items)) if unique else items
81
+
82
+
83
+ def normalize_extensions(extensions) -> list[str]:
84
+ """normalize file extensions to a lowercase dotted form
85
+ `pdf`, `.PDF`, `*.pdf` -> `.pdf`. Duplicates are dropped, order is kept.
86
+ """
87
+ return list(
88
+ dict.fromkeys(
89
+ "." + ext.lower().lstrip("*.").strip() for ext in (extensions or []) if ext.strip(" *.")
90
+ )
91
+ )
@@ -0,0 +1,5 @@
1
+ from azrico import str_utils
2
+
3
+
4
+ def test_module_imports() -> None:
5
+ assert str_utils is not None