none-shall-parse 0.6.2__py3-none-any.whl
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.
- none_shall_parse/__init__.py +139 -0
- none_shall_parse/dates.py +801 -0
- none_shall_parse/imeis.py +211 -0
- none_shall_parse/lists.py +48 -0
- none_shall_parse/parse.py +196 -0
- none_shall_parse/strings.py +267 -0
- none_shall_parse/types.py +41 -0
- none_shall_parse-0.6.2.dist-info/METADATA +66 -0
- none_shall_parse-0.6.2.dist-info/RECORD +10 -0
- none_shall_parse-0.6.2.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import hashlib
|
|
3
|
+
import itertools
|
|
4
|
+
import random
|
|
5
|
+
import re
|
|
6
|
+
import secrets
|
|
7
|
+
import string
|
|
8
|
+
import unicodedata
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
_control_chars = "".join(
|
|
12
|
+
map(chr, itertools.chain(range(0x00, 0x20), range(0x7F, 0xA0)))
|
|
13
|
+
)
|
|
14
|
+
_re_control_char = re.compile("[%s]" % re.escape(_control_chars))
|
|
15
|
+
_re_combine_whitespace = re.compile(r"\s+")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def slugify(value: object, allow_unicode: bool = False) -> str:
|
|
19
|
+
"""
|
|
20
|
+
Maps directly to Django's slugify function.
|
|
21
|
+
Convert to ASCII if 'allow_unicode' is False. Convert spaces or repeated
|
|
22
|
+
dashes to single dashes. Remove characters that aren't alphanumerics,
|
|
23
|
+
underscores, or hyphens. Convert to lowercase. Also strip leading and
|
|
24
|
+
trailing whitespace, dashes, and underscores.
|
|
25
|
+
"""
|
|
26
|
+
value = str(value)
|
|
27
|
+
if allow_unicode:
|
|
28
|
+
value = unicodedata.normalize("NFKC", value)
|
|
29
|
+
else:
|
|
30
|
+
value = (
|
|
31
|
+
unicodedata.normalize("NFKD", value)
|
|
32
|
+
.encode("ascii", "ignore")
|
|
33
|
+
.decode("ascii")
|
|
34
|
+
)
|
|
35
|
+
value = re.sub(r"[^\w\s-]", "", value.lower())
|
|
36
|
+
return re.sub(r"[-\s]+", "-", value).strip("-_")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def random_16():
|
|
40
|
+
return "".join(random.choices(string.ascii_letters + string.digits, k=16))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def to_human_string(s: Any) -> tuple[Any | str, bool]:
|
|
44
|
+
"""
|
|
45
|
+
Cleans up a string by removing extra whitespace and control characters.
|
|
46
|
+
|
|
47
|
+
Removes unnecessary whitespace and control characters from the input string.
|
|
48
|
+
This function is designed to validate and clean user-provided string input
|
|
49
|
+
while preserving input that does not require modification. It returns the
|
|
50
|
+
cleaned string along with a boolean indicating whether changes were made
|
|
51
|
+
to the original string.
|
|
52
|
+
|
|
53
|
+
Parameters:
|
|
54
|
+
s : any
|
|
55
|
+
The input value to clean and validate. If it is not a string, it is
|
|
56
|
+
returned unchanged with a modification flag of False.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
tuple
|
|
60
|
+
A tuple where the first element is the cleaned string (or the original
|
|
61
|
+
input if it is not a string), and the second element is a boolean
|
|
62
|
+
indicating whether the string was modified.
|
|
63
|
+
"""
|
|
64
|
+
if not isinstance(s, str):
|
|
65
|
+
return s, False
|
|
66
|
+
|
|
67
|
+
c = _re_combine_whitespace.sub(" ", s).strip()
|
|
68
|
+
clean_string = _re_control_char.sub("", c)
|
|
69
|
+
if clean_string == s:
|
|
70
|
+
return s, False
|
|
71
|
+
return clean_string, True
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def is_quoted_string(s: str, strip: bool = False) -> tuple[bool, str]:
|
|
75
|
+
"""
|
|
76
|
+
Checks if a given string is enclosed in quotes and optionally strips the quotes.
|
|
77
|
+
|
|
78
|
+
The function determines whether a given string starts and ends with matching quotes,
|
|
79
|
+
either single quotes (') or double quotes ("). If the string is quoted and
|
|
80
|
+
the `strip` parameter is set to True, it removes the enclosing quotes and returns
|
|
81
|
+
the unquoted string.
|
|
82
|
+
|
|
83
|
+
Parameters:
|
|
84
|
+
s : str
|
|
85
|
+
The input string to check and possibly process.
|
|
86
|
+
strip : bool, optional
|
|
87
|
+
Indicates whether to remove the enclosing quotes if the string is quoted.
|
|
88
|
+
Defaults to False.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
tuple[bool, str]
|
|
92
|
+
A tuple where the first element is a boolean indicating whether the string
|
|
93
|
+
is quoted, and the second element is the original string or the stripped
|
|
94
|
+
version if `strip` is True.
|
|
95
|
+
"""
|
|
96
|
+
is_quoted = False
|
|
97
|
+
result = s
|
|
98
|
+
if not isinstance(s, str):
|
|
99
|
+
return is_quoted, result
|
|
100
|
+
|
|
101
|
+
if s[0] == s[-1]:
|
|
102
|
+
if s[0] in ['"', "'"]:
|
|
103
|
+
is_quoted = True
|
|
104
|
+
if strip:
|
|
105
|
+
if s[0] == "'":
|
|
106
|
+
result = s.strip("'")
|
|
107
|
+
elif s[0] == '"':
|
|
108
|
+
result = s.strip('"')
|
|
109
|
+
return is_quoted, result
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def is_numeric_string(s: str, convert: bool = False) -> tuple[bool, str | int | float]:
|
|
113
|
+
"""
|
|
114
|
+
Checks if the given string represents a numeric value and optionally converts it.
|
|
115
|
+
|
|
116
|
+
This function determines if the provided string represents a numeric value.
|
|
117
|
+
If the input is numeric and the `convert` flag is set to True, it returns
|
|
118
|
+
the numeric value converted to either an integer (if the float represents
|
|
119
|
+
an integer) or a float. If the input is not numeric, it returns the original
|
|
120
|
+
input string.
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
----------
|
|
124
|
+
s : str
|
|
125
|
+
The input string to check.
|
|
126
|
+
convert : bool, optional
|
|
127
|
+
A flag indicating whether to convert the numeric string to a numeric
|
|
128
|
+
type (default is False).
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
tuple
|
|
133
|
+
A tuple containing a boolean and the result:
|
|
134
|
+
- A boolean indicating whether the input string is numeric or not.
|
|
135
|
+
- The numeric value if `convert` is True and the string is numeric;
|
|
136
|
+
otherwise, the original string.
|
|
137
|
+
"""
|
|
138
|
+
is_numeric = False
|
|
139
|
+
result = s
|
|
140
|
+
f = None
|
|
141
|
+
if not isinstance(s, str):
|
|
142
|
+
return is_numeric, result
|
|
143
|
+
try:
|
|
144
|
+
f = float(s)
|
|
145
|
+
is_numeric = True
|
|
146
|
+
except ValueError:
|
|
147
|
+
is_numeric = False
|
|
148
|
+
|
|
149
|
+
if is_numeric and convert:
|
|
150
|
+
result = int(f) if f.is_integer() else f
|
|
151
|
+
|
|
152
|
+
return is_numeric, result
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def custom_slug(s: str) -> str:
|
|
156
|
+
# Remove all non-word characters (everything except numbers and letters)
|
|
157
|
+
s = re.sub(r"[^\w\s]", "", s)
|
|
158
|
+
|
|
159
|
+
# Replace all runs of whitespace with a single dash
|
|
160
|
+
s = re.sub(r"\s+", "_", s)
|
|
161
|
+
|
|
162
|
+
return s
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def b64_encode(s: str | bytes) -> str:
|
|
166
|
+
"""
|
|
167
|
+
Encodes a string or bytes into its Base64 representation.
|
|
168
|
+
|
|
169
|
+
This function takes an input, either a string or bytes, and encodes it
|
|
170
|
+
into its Base64 representation. If the input is a string, it is first
|
|
171
|
+
encoded into bytes using UTF-8 encoding. The resulting Base64 encoded
|
|
172
|
+
value is returned as a string.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
s: The input to encode, which can be either a string or bytes.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
The Base64 encoded representation of the input as a string.
|
|
179
|
+
"""
|
|
180
|
+
if isinstance(s, str):
|
|
181
|
+
s = s.encode("utf-8")
|
|
182
|
+
return base64.b64encode(s).decode("utf-8")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def b64_decode(s: str) -> bytes:
|
|
186
|
+
"""
|
|
187
|
+
Decodes a Base64 encoded string to its original binary format.
|
|
188
|
+
|
|
189
|
+
This function takes a Base64 encoded string and decodes it to its
|
|
190
|
+
original bytes form. Base64 encoding may omit padding characters, so
|
|
191
|
+
the function ensures the input is properly padded before decoding.
|
|
192
|
+
|
|
193
|
+
Parameters:
|
|
194
|
+
s: str
|
|
195
|
+
A Base64 encoded string that needs to be decoded.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
bytes
|
|
199
|
+
The decoded binary data.
|
|
200
|
+
|
|
201
|
+
Raises:
|
|
202
|
+
ValueError
|
|
203
|
+
If the input string contains invalid Base64 characters.
|
|
204
|
+
"""
|
|
205
|
+
pad = "=" * (-len(s) % 4)
|
|
206
|
+
return base64.b64decode(s + pad)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def calc_hash(*args: Any) -> str:
|
|
210
|
+
"""
|
|
211
|
+
Calculate and return a SHA-1 hash for the given arguments.
|
|
212
|
+
|
|
213
|
+
This function joins the provided arguments into a single string, encodes it
|
|
214
|
+
using UTF-16, and calculates the SHA-1 hash of the resulting bytes.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
*args: A variable number of arguments to include in the hash.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
str: The computed SHA-1 hash as a hexadecimal string.
|
|
221
|
+
"""
|
|
222
|
+
s = "_".join(map(str, args))
|
|
223
|
+
return hashlib.sha1(s.encode("utf-16")).hexdigest()
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def generate_random_password(n: int = 10) -> str:
|
|
227
|
+
"""
|
|
228
|
+
Generates a random password meeting specific criteria for complexity. The
|
|
229
|
+
function ensures the password contains at least one lowercase letter, one
|
|
230
|
+
uppercase letter, and at least three numeric digits. The length of the
|
|
231
|
+
password can be customized using the 'n' parameter.
|
|
232
|
+
|
|
233
|
+
Parameters:
|
|
234
|
+
n (int): Length of the password to be generated. Default is 10.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
str: A randomly generated password that meets the specified criteria.
|
|
238
|
+
"""
|
|
239
|
+
alphabet = string.ascii_letters + string.digits
|
|
240
|
+
while True:
|
|
241
|
+
password = "".join(secrets.choice(alphabet) for i in range(n))
|
|
242
|
+
if (
|
|
243
|
+
any(c.islower() for c in password)
|
|
244
|
+
and any(c.isupper() for c in password)
|
|
245
|
+
and sum(c.isdigit() for c in password) >= 3
|
|
246
|
+
):
|
|
247
|
+
break
|
|
248
|
+
return password
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def generate_crypto_password(n: int = 32) -> str:
|
|
252
|
+
"""
|
|
253
|
+
Generates a cryptographically secure password string.
|
|
254
|
+
|
|
255
|
+
This function uses the `secrets` module to generate a cryptographically
|
|
256
|
+
secure random string with a specified length. The default length of
|
|
257
|
+
the password is 32 characters.
|
|
258
|
+
|
|
259
|
+
Parameters:
|
|
260
|
+
n: int, optional
|
|
261
|
+
Length of the password string. Defaults to 32.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
str
|
|
265
|
+
A cryptographically secure randomly generated password string.
|
|
266
|
+
"""
|
|
267
|
+
return secrets.token_urlsafe(n)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from datetime import datetime, date
|
|
2
|
+
from typing import Protocol, Sequence, Tuple, Union, TypeVar
|
|
3
|
+
|
|
4
|
+
from pendulum import DateTime, Date
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class StringLike(Protocol):
|
|
8
|
+
"""
|
|
9
|
+
Protocol that defines the expected behavior for string-like objects.
|
|
10
|
+
|
|
11
|
+
This protocol specifies the methods and properties that an object
|
|
12
|
+
must implement to be considered string-like. It defines basic
|
|
13
|
+
string operations such as getting the string representation and
|
|
14
|
+
length, string concatenation, containment checks, and several
|
|
15
|
+
commonly used string manipulation methods. Objects adhering to
|
|
16
|
+
this protocol can mimic the behavior of standard Python strings.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __str__(self) -> str: ...
|
|
20
|
+
def __len__(self) -> int: ...
|
|
21
|
+
def __add__(self, other: str) -> str: ...
|
|
22
|
+
def __contains__(self, item: str) -> bool: ...
|
|
23
|
+
|
|
24
|
+
# Most commonly used string methods
|
|
25
|
+
def upper(self) -> str: ...
|
|
26
|
+
def lower(self) -> str: ...
|
|
27
|
+
def strip(self) -> str: ...
|
|
28
|
+
def startswith(self, prefix: str) -> bool: ...
|
|
29
|
+
def endswith(self, suffix: str) -> bool: ...
|
|
30
|
+
def replace(self, old: str, new: str) -> str: ...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
ChoicesType = Sequence[Tuple[Union[int, str], StringLike]]
|
|
34
|
+
|
|
35
|
+
DateLike = Union[Date, date]
|
|
36
|
+
|
|
37
|
+
DateTimeLike = Union[DateTime, datetime]
|
|
38
|
+
|
|
39
|
+
DateTimeOrDateLike = Union[DateTimeLike, DateLike]
|
|
40
|
+
|
|
41
|
+
T = TypeVar("T")
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: none-shall-parse
|
|
3
|
+
Version: 0.6.2
|
|
4
|
+
Summary: Trinity Shared Python utilities.
|
|
5
|
+
Author: Andries Niemandt, Jan Badenhorst
|
|
6
|
+
Author-email: Andries Niemandt <andries.niemandt@trintel.co.za>, Jan Badenhorst <jan@trintel.co.za>
|
|
7
|
+
License: MIT
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Dist: pendulum
|
|
12
|
+
Requires-Dist: pytest>=8.0.0 ; extra == 'dev'
|
|
13
|
+
Requires-Dist: ruff>=0.12.3 ; extra == 'dev'
|
|
14
|
+
Requires-Dist: isort ; extra == 'dev'
|
|
15
|
+
Requires-Dist: flake8 ; extra == 'dev'
|
|
16
|
+
Requires-Dist: uv-build ; extra == 'dev'
|
|
17
|
+
Requires-Python: >=3.12
|
|
18
|
+
Project-URL: Homepage, https://github.com/trinity-telecomms/tpu
|
|
19
|
+
Project-URL: Issues, https://github.com/trinity-telecomms/tpu/issues
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# none-shall-parse
|
|
24
|
+
|
|
25
|
+
A collection of shared utilities for Trinity projects.
|
|
26
|
+
|
|
27
|
+
Originally intended to be parsing utilities only, this grew to include
|
|
28
|
+
other useful functions.
|
|
29
|
+
|
|
30
|
+
Named for its author Andries Niemandt — whose surname loosely
|
|
31
|
+
translates to "none". Combined this with our parsing intentions
|
|
32
|
+
to create a name which nods to the Black Knight in Monty Python's Holy Grail.
|
|
33
|
+
https://www.youtube.com/watch?v=zKhEw7nD9C4
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
Using `uv`:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uv add none-shall-parse
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Using `pip` with `uv`:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
uv pip install none-shall-parse
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Using `pip`:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install none-shall-parse
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Development Quick Start
|
|
56
|
+
|
|
57
|
+
#### To build and publish to pypi:
|
|
58
|
+
|
|
59
|
+
Update the version in the `pyproject.toml` file, then:
|
|
60
|
+
```bash
|
|
61
|
+
uv sync --upgrade --all-extras --all-groups
|
|
62
|
+
pytest
|
|
63
|
+
rm -rf dist/ build/ *.egg-info/
|
|
64
|
+
uv build
|
|
65
|
+
uv publish
|
|
66
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
none_shall_parse/__init__.py,sha256=ekNMvJaRzR-SOv2bQAOuH86ImPHXGpIj2NbeCO45GOc,3897
|
|
2
|
+
none_shall_parse/dates.py,sha256=lbdfw6DCq5EvmUIHliZK8yMkouqDNvY0jNWMWrTt9Zk,28216
|
|
3
|
+
none_shall_parse/imeis.py,sha256=20pONoUhLKomZxAJegqSSjG72hZjYs60r8IcaRt-15M,6770
|
|
4
|
+
none_shall_parse/lists.py,sha256=DhbiElDBYTVss1ivDytCCwbUKpKpaCIy4orv8KOK-4M,1765
|
|
5
|
+
none_shall_parse/parse.py,sha256=77bXZAtwFksRwuZ9Ax0lPxEjFpyjkQBqRa5mBc1WkF4,6843
|
|
6
|
+
none_shall_parse/strings.py,sha256=F7491CJAHJjL7vdEGwoH_4S6PjaovYUS_yzVGJ-bYIE,8463
|
|
7
|
+
none_shall_parse/types.py,sha256=WAgILMtW2_fm9MBpUuQvq68yXYBNd3rnSoQk70ibOd4,1320
|
|
8
|
+
none_shall_parse-0.6.2.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
9
|
+
none_shall_parse-0.6.2.dist-info/METADATA,sha256=a9CfMCC9wVb8gw4NoI964u9y2S_FibwMOAzpwPqGreo,1701
|
|
10
|
+
none_shall_parse-0.6.2.dist-info/RECORD,,
|