kisspy-python 1.0.1__tar.gz → 1.2.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.
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/PKG-INFO +1 -1
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/changes.md +9 -0
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/_metadata.json +1 -1
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/converters/excelHelpers.py +18 -2
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/converters/numericConverter.py +13 -5
- kisspy_python-1.2.0/kisspy/decorators/methodParameters.py +97 -0
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/decorators/singleton.py +9 -5
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/decorators/thrdSafeSync.py +12 -7
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/exceptions.py +4 -0
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/metaclasses/singleton.py +6 -5
- kisspy_python-1.2.0/kisspy/xtdPy/dicts.py +90 -0
- kisspy_python-1.2.0/kisspy/xtdPy/dt/timeFormatting.py +26 -0
- kisspy_python-1.2.0/kisspy/xtdPy/lists.py +84 -0
- kisspy_python-1.2.0/kisspy/xtdPy/paths/directories.py +68 -0
- kisspy_python-1.2.0/kisspy/xtdPy/paths/files.py +72 -0
- kisspy_python-1.2.0/kisspy/xtdPy/strings/base64data.py +54 -0
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy_python.egg-info/PKG-INFO +1 -1
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy_python.egg-info/SOURCES.txt +1 -0
- kisspy_python-1.0.1/kisspy/xtdPy/dicts.py +0 -63
- kisspy_python-1.0.1/kisspy/xtdPy/dt/timeFormatting.py +0 -21
- kisspy_python-1.0.1/kisspy/xtdPy/lists.py +0 -95
- kisspy_python-1.0.1/kisspy/xtdPy/paths/directories.py +0 -43
- kisspy_python-1.0.1/kisspy/xtdPy/paths/files.py +0 -48
- kisspy_python-1.0.1/kisspy/xtdPy/strings/base64data.py +0 -32
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/MANIFEST.in +0 -0
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/__init__.py +0 -0
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/converters/__init__.py +0 -0
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/decorators/__init__.py +0 -0
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/decorators/pidFile.py +0 -0
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/metaclasses/__init__.py +0 -0
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/xtdPy/__init__.py +0 -0
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/xtdPy/dt/__init__.py +0 -0
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/xtdPy/json.py +0 -0
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/xtdPy/paths/__init__.py +0 -0
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/xtdPy/paths/_common.py +0 -0
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/xtdPy/strings/__init__.py +0 -0
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/xtdPy/strings/textExtensions.py +0 -0
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/xtdPy/strings/textNormalizer.py +0 -0
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy_python.egg-info/dependency_links.txt +0 -0
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy_python.egg-info/requires.txt +0 -0
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy_python.egg-info/top_level.txt +0 -0
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/license.md +0 -0
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/pyproject.toml +0 -0
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/readme.md +0 -0
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/requirements/prod.txt +0 -0
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/setup.cfg +0 -0
- {kisspy_python-1.0.1 → kisspy_python-1.2.0}/setup.py +0 -0
|
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
none
|
|
9
9
|
|
|
10
|
+
## [1.2.0] - 2026 05 07
|
|
11
|
+
### Added
|
|
12
|
+
- methodParameter decorators
|
|
13
|
+
|
|
14
|
+
## [1.1.0] - 2026 04 07
|
|
15
|
+
### Changed
|
|
16
|
+
- Doc Strings
|
|
17
|
+
- minor edits to methods
|
|
18
|
+
|
|
10
19
|
## [1.0.0] - 2026 03 18
|
|
11
20
|
### Added
|
|
12
21
|
- Release Published
|
|
@@ -13,7 +13,15 @@ def _divmod_excel(n):
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def toExcelCol(columnNum: int) -> str:
|
|
16
|
-
"""
|
|
16
|
+
"""
|
|
17
|
+
converts a 1-based column number to Excel-ish column name, ie: 1 -> A; 27 -> AA
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
columnNum (int): the column number
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
str: the column name
|
|
24
|
+
"""
|
|
17
25
|
chars = []
|
|
18
26
|
while columnNum > 0:
|
|
19
27
|
columnNum, d = _divmod_excel(columnNum)
|
|
@@ -22,5 +30,13 @@ def toExcelCol(columnNum: int) -> str:
|
|
|
22
30
|
|
|
23
31
|
|
|
24
32
|
def fromExcelCol(columnName: str) -> int:
|
|
25
|
-
"""
|
|
33
|
+
"""
|
|
34
|
+
converts an Excel column name to a 1-based integer, ie: A -> 1; AA -> 27
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
columnName (str): the column name
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
int: the column number
|
|
41
|
+
"""
|
|
26
42
|
return reduce(lambda r, x: r * 26 + x + 1, map(string.ascii_uppercase.index, columnName), 0)
|
|
@@ -12,15 +12,21 @@ def returnNone(*args, **kwargs) -> None:
|
|
|
12
12
|
|
|
13
13
|
def returnTxt(*args, **kwargs) -> None:
|
|
14
14
|
"""returns the first argument, does nothing else"""
|
|
15
|
-
|
|
15
|
+
if args:
|
|
16
|
+
return args[0]
|
|
17
|
+
return ""
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
def toNumeric(val, onFail=returnNone) -> float | int | None:
|
|
19
21
|
"""
|
|
20
|
-
returns a float or int, by converting the provided val
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
returns a float or int, by converting the provided val to a float and if possible, an int
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
val (_type_): the value to attempt to convert
|
|
26
|
+
onFail (callable, optional): on error or failure, this is called with the value as the first arg / parameter and the exception as the second parameter. Defaults to returnNone
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
float | int | None: resulting value
|
|
24
30
|
"""
|
|
25
31
|
try:
|
|
26
32
|
if isinstance(val, str):
|
|
@@ -29,4 +35,6 @@ def toNumeric(val, onFail=returnNone) -> float | int | None:
|
|
|
29
35
|
return int(val)
|
|
30
36
|
return val
|
|
31
37
|
except ValueError as verr:
|
|
38
|
+
if not onFail:
|
|
39
|
+
return None
|
|
32
40
|
return onFail(val, verr)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import inspect
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _raiseValueErrorIfErrors(errors: list[str], frmt: str):
|
|
6
|
+
if not errors:
|
|
7
|
+
return
|
|
8
|
+
raise ValueError(frmt.format(", ".join(errors)))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def parametersNotNone(paramNames: list[str]):
|
|
12
|
+
"""
|
|
13
|
+
validates that the parameters that are listed are not literally 'None'
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
paramNames (list[str]): the decorated function parameter names to validate
|
|
17
|
+
|
|
18
|
+
Raises:
|
|
19
|
+
ValueError
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def decorator(func):
|
|
23
|
+
@functools.wraps(func)
|
|
24
|
+
def wrapper(*args, **kwargs):
|
|
25
|
+
sig = inspect.signature(func)
|
|
26
|
+
bound = sig.bind(*args, **kwargs)
|
|
27
|
+
bound.apply_defaults()
|
|
28
|
+
errors = []
|
|
29
|
+
for pn in paramNames:
|
|
30
|
+
if pn in bound.arguments:
|
|
31
|
+
val = bound.arguments[pn]
|
|
32
|
+
if val is None:
|
|
33
|
+
errors.append(pn)
|
|
34
|
+
_raiseValueErrorIfErrors(errors, "Parameter(s) '{}' Cannot Be 'None'")
|
|
35
|
+
return func(*bound.args, **bound.kwargs)
|
|
36
|
+
|
|
37
|
+
return wrapper
|
|
38
|
+
|
|
39
|
+
return decorator
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def parametersHaveTruthyValue(paramNames: list[str]):
|
|
43
|
+
"""
|
|
44
|
+
validates that the parameters that are listed all have 'truthy' values
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
paramNames (list[str]): the decorated function parameter names to validate
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
ValueError
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def decorator(func):
|
|
54
|
+
@functools.wraps(func)
|
|
55
|
+
def wrapper(*args, **kwargs):
|
|
56
|
+
sig = inspect.signature(func)
|
|
57
|
+
bound = sig.bind(*args, **kwargs)
|
|
58
|
+
bound.apply_defaults()
|
|
59
|
+
errors = []
|
|
60
|
+
for pn in paramNames:
|
|
61
|
+
if pn in bound.arguments:
|
|
62
|
+
val = bound.arguments[pn]
|
|
63
|
+
if not val:
|
|
64
|
+
errors.append(pn)
|
|
65
|
+
_raiseValueErrorIfErrors(errors, "Parameter(s) '{}' Are Not A Truthy Value")
|
|
66
|
+
return func(*bound.args, **bound.kwargs)
|
|
67
|
+
|
|
68
|
+
return wrapper
|
|
69
|
+
|
|
70
|
+
return decorator
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def parameterLimit(paramName: str, lo: float | int, hi: float | int):
|
|
74
|
+
"""
|
|
75
|
+
validates that the listed parameter's value is within the hi and lo value specified
|
|
76
|
+
and corrects the value keeping it within the range specified
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
paramName (str): the decorated function parameter name to validate
|
|
80
|
+
lo (float | int): lower value limit
|
|
81
|
+
hi (float | int): upper value limit
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def decorator(func):
|
|
85
|
+
@functools.wraps(func)
|
|
86
|
+
def wrapper(*args, **kwargs):
|
|
87
|
+
sig = inspect.signature(func)
|
|
88
|
+
bound = sig.bind(*args, **kwargs)
|
|
89
|
+
bound.apply_defaults()
|
|
90
|
+
if paramName in bound.arguments:
|
|
91
|
+
val = bound.arguments[paramName]
|
|
92
|
+
bound.arguments[paramName] = max(lo, min(hi, val))
|
|
93
|
+
return func(*bound.args, **bound.kwargs)
|
|
94
|
+
|
|
95
|
+
return wrapper
|
|
96
|
+
|
|
97
|
+
return decorator
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
def singleton(_class):
|
|
2
2
|
"""
|
|
3
|
-
class decorator to create a single instance of a class
|
|
4
|
-
usage:\n
|
|
3
|
+
class decorator to create a single instance of a class
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
pass
|
|
5
|
+
Args:
|
|
6
|
+
_class (_type_): the class to decorate
|
|
9
7
|
|
|
8
|
+
Example:
|
|
9
|
+
````python
|
|
10
|
+
@singleton
|
|
11
|
+
class MyClass(<baseClass>):
|
|
12
|
+
pass
|
|
13
|
+
````
|
|
10
14
|
"""
|
|
11
15
|
_instances = {}
|
|
12
16
|
|
|
@@ -3,13 +3,18 @@ import threading
|
|
|
3
3
|
|
|
4
4
|
def thrdSafeSync(func):
|
|
5
5
|
"""
|
|
6
|
-
decorator that makes the function thread safe
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
decorator that makes the function thread safe
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
func (_type_): the method to decorate
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
|
|
13
|
+
````python
|
|
14
|
+
@thrdSafeSync
|
|
15
|
+
def oneAtATime():
|
|
16
|
+
file IO process...
|
|
17
|
+
````
|
|
13
18
|
"""
|
|
14
19
|
lock = threading.Lock() # A lock for this specific function
|
|
15
20
|
|
|
@@ -11,6 +11,9 @@ class TooManyRecordsException(KisspyException):
|
|
|
11
11
|
MSG_FORMAT = "{} {} Returned, {} Were Expected"
|
|
12
12
|
|
|
13
13
|
def __init__(self, *args: object, numberRecords: int = None, recordType: str = "Records", numberExpected: int = 1) -> None:
|
|
14
|
+
self.numberRecords = numberRecords
|
|
15
|
+
self.numberExpected = numberExpected
|
|
16
|
+
self.recordType = recordType
|
|
14
17
|
if numberRecords and recordType:
|
|
15
18
|
msg = TooManyRecordsException.MSG_FORMAT.format(numberRecords, recordType, numberExpected)
|
|
16
19
|
super().__init__(msg, *args)
|
|
@@ -22,6 +25,7 @@ class ZeroRecordsException(KisspyException):
|
|
|
22
25
|
MSG_FORMAT = "Zero (0) {} Returned, > 0 Were Expected"
|
|
23
26
|
|
|
24
27
|
def __init__(self, *args, recordType: str = "Records"):
|
|
28
|
+
self.recordType = recordType
|
|
25
29
|
if args:
|
|
26
30
|
super().__init__(*args)
|
|
27
31
|
else:
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
class Singleton(type):
|
|
2
2
|
"""
|
|
3
|
-
meta-class to create a single instance of a class
|
|
4
|
-
usage:\n
|
|
5
|
-
|
|
6
|
-
class MyClass(<baseClass>, metaclass=Singleton):\n
|
|
7
|
-
pass
|
|
3
|
+
meta-class to create a single instance of a class
|
|
8
4
|
|
|
5
|
+
Example:
|
|
6
|
+
````python
|
|
7
|
+
class MyClass(<baseClass>, metaclass=Singleton):\n
|
|
8
|
+
pass
|
|
9
|
+
````
|
|
9
10
|
"""
|
|
10
11
|
|
|
11
12
|
_instances = {}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def getValueOfPath(data: dict, keyPath: str, defaultValue=None):
|
|
5
|
+
"""
|
|
6
|
+
returns the value at the key path provided of a multi-level dict
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
data (dict): the dict to evaluate, ie: {'animals':{'dogs':{'whippet':'fast'}}}
|
|
10
|
+
keyPath (str): key 'path' to work down using '/' as separators, ie: 'animals/dogs/whippet' returns 'fast'
|
|
11
|
+
defaultValue (_type_, optional): default value returned if the path is not found. Defaults to None
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
_type_: value of the final key of the path
|
|
15
|
+
"""
|
|
16
|
+
mKeys = keyPath.split("/")
|
|
17
|
+
dval = dict(data)
|
|
18
|
+
for k in mKeys:
|
|
19
|
+
dval = dval.get(k, None)
|
|
20
|
+
if dval is None:
|
|
21
|
+
dval = defaultValue
|
|
22
|
+
break
|
|
23
|
+
return dval
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def setValueOfPath(data: dict, keyPath: str, value, createTree: bool = True) -> bool:
|
|
27
|
+
"""
|
|
28
|
+
sets the value provided to the last key in the path
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
data (dict): the dict to evaluate
|
|
32
|
+
keyPath (str): key 'path' to work down using '/' as separators
|
|
33
|
+
value (_type_): value to set the last key of the path to
|
|
34
|
+
createTree (bool, optional): if True, it creates a dict if any key in the path does not exist, otherwise does not set the value and exits. Defaults to True
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
bool: True if the value was set, False if it did not set the value
|
|
38
|
+
"""
|
|
39
|
+
if not keyPath:
|
|
40
|
+
return False
|
|
41
|
+
pKeys = keyPath.split("/")
|
|
42
|
+
dval = data
|
|
43
|
+
for i, k in enumerate(pKeys):
|
|
44
|
+
if not isinstance(dval, dict):
|
|
45
|
+
return False
|
|
46
|
+
if (i + 1) < len(pKeys):
|
|
47
|
+
if (k not in dval) and createTree:
|
|
48
|
+
dval[k] = {}
|
|
49
|
+
dval = dval.get(k, None)
|
|
50
|
+
else:
|
|
51
|
+
dval[k] = value
|
|
52
|
+
return True
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def deepCopy(data: dict | list) -> dict | list:
|
|
57
|
+
"""
|
|
58
|
+
returns a true deep copy of the original data object by serializing to JSON and back again
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
data (dict|list): JSON-isable object
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
dict|list: copy of the input data
|
|
65
|
+
"""
|
|
66
|
+
jStr = json.dumps(data)
|
|
67
|
+
return json.loads(jStr)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def addToDictIfExists(destination: dict, fieldName: str, fieldValue, normalizeTxtTo: str = None) -> bool:
|
|
71
|
+
"""
|
|
72
|
+
adds the fieldvalue assigned to the fieldname key in the destination dictionary, if the fieldvalue is truthy
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
destination (dict): the dict to add the field to
|
|
76
|
+
fieldName (str): the field or key name
|
|
77
|
+
fieldValue (_type_): the field or key value
|
|
78
|
+
normalizeTxtTo (str, optional): name of the method on the object to call to normalize, ie: 'lower'. Defaults to None
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
bool: True if the value was set, False if it did not set the value
|
|
82
|
+
"""
|
|
83
|
+
if not fieldValue:
|
|
84
|
+
return False
|
|
85
|
+
if isinstance(fieldValue, str) and normalizeTxtTo:
|
|
86
|
+
mthd = getattr(fieldValue, normalizeTxtTo)
|
|
87
|
+
if callable(mthd):
|
|
88
|
+
fieldValue = mthd()
|
|
89
|
+
destination[fieldName] = fieldValue
|
|
90
|
+
return True
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
|
|
3
|
+
SORTABLE_DT_FRMT = "%Y%m%d.%H%M%S"
|
|
4
|
+
SORTABLE_DT_TZA_FRMT = "%Y%m%d.%H%M%S%z"
|
|
5
|
+
PYLOG_DT_FRMT = "%Y-%m-%d %H:%M:%S,%f"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def getSortableNow(prefix: str = None, suffix: str = None, frmt: str = SORTABLE_DT_FRMT) -> str:
|
|
9
|
+
"""
|
|
10
|
+
returns the date and time as a sortable string separated by a period ie: 'YYYYmmdd.HHMMSS' or the format provided
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
prefix (str, optional): text added before sortable date/time. Defaults to None
|
|
14
|
+
suffix (str, optional): text added before sortable date/time. Defaults to None
|
|
15
|
+
frmt (str, optional): datatime output format. Defaults to SORTABLE_DT_FRMT
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
str: the current datetime formatted as specified
|
|
19
|
+
"""
|
|
20
|
+
val = ""
|
|
21
|
+
if prefix:
|
|
22
|
+
val = prefix
|
|
23
|
+
val = val + datetime.datetime.now().strftime(frmt)
|
|
24
|
+
if suffix:
|
|
25
|
+
val = val + suffix
|
|
26
|
+
return val
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from kisspy.exceptions import TooManyRecordsException
|
|
2
|
+
|
|
3
|
+
NO_VALUE = "__no_value__"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def binRecordsOnKey(elements: list[dict], key: str, noKeyOrValue: str = NO_VALUE) -> list[list[dict]]:
|
|
7
|
+
"""
|
|
8
|
+
returns a list within a list where the internal lists are binned
|
|
9
|
+
based on the values of the key in the original list
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
elements (list[dict]): original list of objects
|
|
13
|
+
key (str): key to evaluate value of when binning objects
|
|
14
|
+
noKeyOrValue (str, optional): if set will bin records with missing or null values together. to not bin missing or null values set to 'None'. Defaults to NO_VALUE
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
list[list[dict]]: binned records based on value
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
````python
|
|
21
|
+
i = [{'a':1}, {'a':2}, {'a':2}, {'a':1}, {'a':4}, {'a':1}, {'a':5} ]
|
|
22
|
+
z = binRecordsOnKey(i, 'a')
|
|
23
|
+
z = [
|
|
24
|
+
[{'a':1}, {'a':1}, {'a':1}],
|
|
25
|
+
[{'a':2}, {'a':2}],
|
|
26
|
+
[{'a':4}],
|
|
27
|
+
[{'a':5}]
|
|
28
|
+
]
|
|
29
|
+
````
|
|
30
|
+
"""
|
|
31
|
+
uniqueKeyValues = []
|
|
32
|
+
for e in elements:
|
|
33
|
+
kv = e.get(key, noKeyOrValue)
|
|
34
|
+
if kv and (kv not in uniqueKeyValues):
|
|
35
|
+
uniqueKeyValues.append(kv)
|
|
36
|
+
dividedLists = []
|
|
37
|
+
for ukv in uniqueKeyValues:
|
|
38
|
+
dividedLists.append([x for x in elements if x.get(key, noKeyOrValue) == ukv])
|
|
39
|
+
return dividedLists
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def appendUnique(lst: list, obj) -> bool:
|
|
43
|
+
"""
|
|
44
|
+
appends the object provided to the list provided IF the object is not in the list already
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
lst (list): the list to possibly append the value to
|
|
48
|
+
obj (_type_): the object to append to the list
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
bool: True if the object was appended to the list, otherwise False
|
|
52
|
+
"""
|
|
53
|
+
if obj in lst:
|
|
54
|
+
return False
|
|
55
|
+
lst.append(obj)
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def thrwMultiRcdExc(*args, **kwargs):
|
|
60
|
+
raise TooManyRecordsException(numberRecords=len(args[0]))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def assertOne(records: list, onZeroRcds=None, onMultiRcds=thrwMultiRcdExc) -> dict | list:
|
|
64
|
+
"""
|
|
65
|
+
if the list has only one record, returns the only record\n
|
|
66
|
+
NOTE: the callables are passed the records list as an arg
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
records (list): the list to be evaluated
|
|
70
|
+
onZeroRcds (callable, optional): if the list is empty or None, returns the result of the callable if provided. Defaults to None
|
|
71
|
+
onMultiRcds (callable, optional): if the length of the list is greater than one, returns the result of the callable if provided. Defaults to raising a TooManyRecordsException
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
dict | list: single record object or None
|
|
75
|
+
"""
|
|
76
|
+
if not records:
|
|
77
|
+
if onZeroRcds:
|
|
78
|
+
return onZeroRcds(records)
|
|
79
|
+
return None
|
|
80
|
+
if len(records) > 1:
|
|
81
|
+
if onMultiRcds:
|
|
82
|
+
return onMultiRcds(records)
|
|
83
|
+
return records
|
|
84
|
+
return records[0]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from kisspy.xtdPy.paths._common import frmtPath
|
|
2
|
+
from typing import Tuple
|
|
3
|
+
import pathlib
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _createDir(directory: str) -> bool:
|
|
8
|
+
if os.path.exists(directory):
|
|
9
|
+
return False
|
|
10
|
+
pathlib.Path(directory).mkdir(parents=True, exist_ok=True)
|
|
11
|
+
return True
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def splitPath(filename: str, convertToFwdSlsh: bool = True, endInSeparator: bool = True) -> Tuple[str, str]:
|
|
15
|
+
"""
|
|
16
|
+
splits the filename to the path and basename - same as os.path.split except converts to forward slashes and endings if specified
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
filename (str): path to split
|
|
20
|
+
convertToFwdSlsh (bool, optional): converts any backslash to forward slash if True. Defaults to True
|
|
21
|
+
endInSeparator (bool, optional): ends the directory path with a slash if specified. Defaults to True
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Tuple[str, str]: directory path, basename
|
|
25
|
+
"""
|
|
26
|
+
d, f = os.path.split(filename)
|
|
27
|
+
d = frmtPath(d, convertToFwdSlsh, endInSeparator)
|
|
28
|
+
return d, f
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def createDir(fqFilename: str, directory: str = None, convertToFwdSlsh: bool = True, endInSeparator: bool = True) -> str:
|
|
32
|
+
"""
|
|
33
|
+
creates the parent directories if they do not exist
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
fqFilename (str): fully-qualified filename with path - set to None if providing just a directory parameter
|
|
37
|
+
directory (str, optional): directory to create. Defaults to None
|
|
38
|
+
convertToFwdSlsh (bool, optional): converts any backslash to forward slash if True. Defaults to True
|
|
39
|
+
endInSeparator (bool, optional): ends the directory path with a slash if specified. Defaults to True
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
str: directory
|
|
43
|
+
"""
|
|
44
|
+
if fqFilename:
|
|
45
|
+
directory, f = splitPath(fqFilename, convertToFwdSlsh, endInSeparator)
|
|
46
|
+
else:
|
|
47
|
+
directory = frmtPath(directory, convertToFwdSlsh, endInSeparator)
|
|
48
|
+
_createDir(directory)
|
|
49
|
+
return directory
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def expandUser(path: str, expandChar: str = "~", convertToFwdSlsh: bool = True, endInSeparator: bool = False) -> str:
|
|
53
|
+
"""
|
|
54
|
+
expands the path to a fully qualified path using the expandChar as a signal to do so
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
path (str): _description_
|
|
58
|
+
expandChar (str, optional): representative char for the user directory. Defaults to "~"
|
|
59
|
+
convertToFwdSlsh (bool, optional): converts any backslash to forward slash if True. Defaults to True
|
|
60
|
+
endInSeparator (bool, optional): ends the directory path with a slash if specified. Defaults to False
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
str: path
|
|
64
|
+
"""
|
|
65
|
+
if not path or not path.startswith(expandChar):
|
|
66
|
+
return path
|
|
67
|
+
homeDir = frmtPath(str(pathlib.Path.home()), convertToFwdSlsh, endInSeparator)
|
|
68
|
+
return path.replace(expandChar, homeDir)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from kisspy.xtdPy.paths.directories import expandUser
|
|
2
|
+
from kisspy.xtdPy.json import loadData
|
|
3
|
+
from typing import Tuple
|
|
4
|
+
import datetime
|
|
5
|
+
import pathlib
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
_logger = logging.getLogger(__name__)
|
|
10
|
+
_logger.addHandler(logging.NullHandler())
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def lastModifiedDT(fqFilename: str) -> datetime.datetime:
|
|
14
|
+
"""
|
|
15
|
+
returns a datetime object for the last modified datetime of the file specified
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
fqFilename (str): the path to the file
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
datetime.datetime: last modified datetime
|
|
22
|
+
"""
|
|
23
|
+
try:
|
|
24
|
+
pth = pathlib.Path(fqFilename)
|
|
25
|
+
return datetime.datetime.fromtimestamp(pth.stat().st_mtime)
|
|
26
|
+
except:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _getFn(directory: str, fn: str) -> str:
|
|
31
|
+
"""returns a string as the filepath"""
|
|
32
|
+
return "{}/{}".format(expandUser(directory), fn)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def searchForFile(filename: str, directories: list[str]) -> str:
|
|
36
|
+
"""
|
|
37
|
+
searches for a file in the locations provided and returns the file path;\n
|
|
38
|
+
will expand to the user's home folder if a path starts with ~
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
filename (str): the file name to search for
|
|
42
|
+
directories (list[str]): a list of possible locations, ie directories
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
str: the file path to the existing (found) file
|
|
46
|
+
"""
|
|
47
|
+
for fn in [_getFn(x, filename) for x in directories]:
|
|
48
|
+
_logger.debug("Searching For File '{}'".format(fn))
|
|
49
|
+
if os.path.exists(fn) and os.path.isfile(fn):
|
|
50
|
+
return fn
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def searchAndload(filename: str, directories: list[str], defaultData: dict | list = None) -> Tuple[str, dict]:
|
|
55
|
+
"""
|
|
56
|
+
searches for a file in the locations provided; if a file is found, it \n
|
|
57
|
+
loads the file with json and returns the file's content;\n
|
|
58
|
+
will expand to the user's home folder if a path starts with ~
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
filename (str): the file name to search for
|
|
62
|
+
directories (list[str]): a list of possible locations, ie directories
|
|
63
|
+
defaultData (dict|list, optional): default data to return if no file is present. Defaults to None
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Tuple[str, dict]: a tuple: found file name and file content
|
|
67
|
+
"""
|
|
68
|
+
fn = searchForFile(filename, directories)
|
|
69
|
+
if fn:
|
|
70
|
+
_logger.debug("Found File '{}'".format(fn))
|
|
71
|
+
return fn, loadData(fn, defaultData)
|
|
72
|
+
return None, defaultData
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def fromFile(filename: str) -> str:
|
|
5
|
+
"""
|
|
6
|
+
returns a string representing the file contents in Base64
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
filename (str): the filename of the file to evaluate / convert
|
|
10
|
+
|
|
11
|
+
Returns:
|
|
12
|
+
str: the Base64 representation of the file contents
|
|
13
|
+
"""
|
|
14
|
+
with open(filename, "rb") as image_file:
|
|
15
|
+
data = base64.b64encode(image_file.read()).decode("utf-8")
|
|
16
|
+
return data
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def fromBytes(bytes: bytes) -> str:
|
|
20
|
+
"""
|
|
21
|
+
returns a string representing the bytes in Base64
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
bytes (bytes): bytes from file or stream
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
str: the Base64 representation of the bytes
|
|
28
|
+
"""
|
|
29
|
+
return base64.b64encode(bytes).decode("utf-8")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def toBytes(base64str: str) -> bytes:
|
|
33
|
+
"""
|
|
34
|
+
writes Base64 string back to original binary encoding
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
base64str (str): the Base64 encoded string
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
bytes: binary encoded bytes
|
|
41
|
+
"""
|
|
42
|
+
return base64.b64decode(base64str)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def toFile(filename: str, base64str: str):
|
|
46
|
+
"""
|
|
47
|
+
writes Base64 string back to original binary encoded file
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
filename (str): the filename to save the bytes to
|
|
51
|
+
base64str (str): the original Base64 encoded string
|
|
52
|
+
"""
|
|
53
|
+
with open(filename, "wb") as writer:
|
|
54
|
+
writer.write(base64.b64decode(base64str))
|
|
@@ -11,6 +11,7 @@ kisspy/converters/__init__.py
|
|
|
11
11
|
kisspy/converters/excelHelpers.py
|
|
12
12
|
kisspy/converters/numericConverter.py
|
|
13
13
|
kisspy/decorators/__init__.py
|
|
14
|
+
kisspy/decorators/methodParameters.py
|
|
14
15
|
kisspy/decorators/pidFile.py
|
|
15
16
|
kisspy/decorators/singleton.py
|
|
16
17
|
kisspy/decorators/thrdSafeSync.py
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def getValueOfPath(data: dict, keyPath: str, defaultValue=None):
|
|
5
|
-
"""
|
|
6
|
-
returns the value at the key path provided;\n
|
|
7
|
-
data: dict, the dictionary to evaluate, ie: {'animals':{'dogs':{'whippet':'fast'}}};\n
|
|
8
|
-
keyPath: str, key 'tree' to work down using '/' as separators, ie: 'animals/dogs/whippet' returns 'fast';\n
|
|
9
|
-
defaultValue: any, default value returned if the path is not found
|
|
10
|
-
"""
|
|
11
|
-
mKeys = keyPath.split("/")
|
|
12
|
-
dval = dict(data)
|
|
13
|
-
for k in mKeys:
|
|
14
|
-
dval = dval.get(k, None)
|
|
15
|
-
if dval is None:
|
|
16
|
-
dval = defaultValue
|
|
17
|
-
break
|
|
18
|
-
return dval
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def setValueOfPath(data: dict, keyPath: str, value, createTree: bool = True) -> bool:
|
|
22
|
-
"""
|
|
23
|
-
sets the value provided to the last key in the path\n
|
|
24
|
-
returns true if the value was able to be set, false if any of the tree was not a dict and therefore unable to be set\n
|
|
25
|
-
if createTree is true, it creates a dict if any key in the path does not exist, otherwise does not set the value and exits
|
|
26
|
-
"""
|
|
27
|
-
if not keyPath:
|
|
28
|
-
return False
|
|
29
|
-
pKeys = keyPath.split("/")
|
|
30
|
-
dval = data
|
|
31
|
-
for i, k in enumerate(pKeys):
|
|
32
|
-
if not isinstance(dval, dict):
|
|
33
|
-
return False
|
|
34
|
-
if (i + 1) < len(pKeys):
|
|
35
|
-
if (k not in dval) and createTree:
|
|
36
|
-
dval[k] = {}
|
|
37
|
-
dval = dval.get(k, None)
|
|
38
|
-
else:
|
|
39
|
-
dval[k] = value
|
|
40
|
-
return True
|
|
41
|
-
return False
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def deepCopy(data):
|
|
45
|
-
"""
|
|
46
|
-
returns a true deep copy of the original data object by serializing to JSON and back again
|
|
47
|
-
"""
|
|
48
|
-
jStr = json.dumps(data)
|
|
49
|
-
return json.loads(jStr)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def addToDictIfExists(destination: dict, fieldName: str, fieldValue, normalizeTxtTo: str = None):
|
|
53
|
-
"""
|
|
54
|
-
adds the fieldvalue assigned to the fieldname key in the destination dictionary, if the fieldvalue is not 'None'\n
|
|
55
|
-
if normalizeTxtTo is a callable function, it will call that function when setting, ie: 'lower'
|
|
56
|
-
"""
|
|
57
|
-
if fieldValue:
|
|
58
|
-
if isinstance(fieldValue, str) and normalizeTxtTo:
|
|
59
|
-
mthd = getattr(fieldValue, normalizeTxtTo)
|
|
60
|
-
if callable(mthd):
|
|
61
|
-
fieldValue = mthd()
|
|
62
|
-
destination[fieldName] = fieldValue
|
|
63
|
-
return destination
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import datetime
|
|
2
|
-
|
|
3
|
-
SORTABLE_DT_FRMT = "%Y%m%d.%H%M%S"
|
|
4
|
-
SORTABLE_DT_TZA_FRMT = "%Y%m%d.%H%M%S%z"
|
|
5
|
-
PYLOG_DT_FRMT = "%Y-%m-%d %H:%M:%S,%f"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def getSortableNow(prefix=None, suffix=None, frmt: str = SORTABLE_DT_FRMT):
|
|
9
|
-
"""
|
|
10
|
-
returns the date and time as a sortable string separated by a period ie: 'YYYYmmdd.HHMMSS' or the format provided;\n
|
|
11
|
-
prefix: str added before sortable date/time;\n
|
|
12
|
-
suffix: str added after sortable date/time;\n
|
|
13
|
-
frmt: str, format to output datetime as
|
|
14
|
-
"""
|
|
15
|
-
val = ""
|
|
16
|
-
if prefix:
|
|
17
|
-
val = prefix
|
|
18
|
-
val = val + datetime.datetime.now().strftime(frmt)
|
|
19
|
-
if suffix:
|
|
20
|
-
val = val + suffix
|
|
21
|
-
return val
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
from kisspy.exceptions import TooManyRecordsException
|
|
2
|
-
|
|
3
|
-
NO_VALUE = "__no_value__"
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def binRecordsOnKey(elements: list[dict], key: str, noKeyOrValue: str = NO_VALUE) -> list[list[dict]]:
|
|
7
|
-
"""
|
|
8
|
-
returns a list within a list where the internal lists are binned based on the values of the key in the original list\n
|
|
9
|
-
will bin missing or null values together; to not bin null or missing key records, set noKeyOrValue = None\n
|
|
10
|
-
ie:\n
|
|
11
|
-
i = [{'a':1},{'a':2},{'a':2},{'a':1},{'a':4},{'a':1},{'a':5}]\n
|
|
12
|
-
z = binRecordsOnKey(i, 'a')\n
|
|
13
|
-
z is [ \n
|
|
14
|
-
[{'a':1},{'a':1},{'a':1}], \n
|
|
15
|
-
[{'a':2},{'a':2}], \n
|
|
16
|
-
[{'a':4}], \n
|
|
17
|
-
[{'a':5}] \n
|
|
18
|
-
]
|
|
19
|
-
"""
|
|
20
|
-
uniqueKeyValues = []
|
|
21
|
-
for e in elements:
|
|
22
|
-
kv = e.get(key, noKeyOrValue)
|
|
23
|
-
if kv and (kv not in uniqueKeyValues):
|
|
24
|
-
uniqueKeyValues.append(kv)
|
|
25
|
-
dividedLists = []
|
|
26
|
-
for ukv in uniqueKeyValues:
|
|
27
|
-
dividedLists.append([x for x in elements if x.get(key, noKeyOrValue) == ukv])
|
|
28
|
-
return dividedLists
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def appendUnique(lst: list, obj) -> bool:
|
|
32
|
-
"""
|
|
33
|
-
appends the object provided to the list provided IF the object is not in the list already\n
|
|
34
|
-
lst: list, the list to append the value to\n
|
|
35
|
-
obj: object, the object to append to the list\n
|
|
36
|
-
returns True if the object was appended to the list, otherwise False
|
|
37
|
-
"""
|
|
38
|
-
if obj in lst:
|
|
39
|
-
return False
|
|
40
|
-
lst.append(obj)
|
|
41
|
-
return True
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def thrwMultiRcdExc(*args, **kwargs):
|
|
45
|
-
raise TooManyRecordsException(numberRecords=len(args[0]))
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def assertOne(records: list, onZeroRcds=None, onMultiRcds=thrwMultiRcdExc) -> dict | list:
|
|
49
|
-
"""
|
|
50
|
-
if the list has only one record, returns the only record\n
|
|
51
|
-
if the list is empty or None, returns the result of the\n
|
|
52
|
-
onZeroRcds callable if provided else None\n
|
|
53
|
-
if the length of the list is greater than one, returns the\n
|
|
54
|
-
result of the onMultiRcds callable if provided else the original\n
|
|
55
|
-
list; default action is raise an TooManyRecordsException\n
|
|
56
|
-
the callables are passed the records list as an arg
|
|
57
|
-
"""
|
|
58
|
-
if not records:
|
|
59
|
-
if onZeroRcds:
|
|
60
|
-
return onZeroRcds(records)
|
|
61
|
-
return None
|
|
62
|
-
if len(records) > 1:
|
|
63
|
-
if onMultiRcds:
|
|
64
|
-
return onMultiRcds(records)
|
|
65
|
-
return records
|
|
66
|
-
return records[0]
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
# def _cleanValue(value, caseSensitive=False):
|
|
70
|
-
# if not isinstance(value, str):
|
|
71
|
-
# return None
|
|
72
|
-
# value = value.strip()
|
|
73
|
-
# if not caseSensitive:
|
|
74
|
-
# value = value.lower()
|
|
75
|
-
# return value
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
# def getExactMatches(
|
|
79
|
-
# records: list[dict], keyName: str, compareTo: str, caseSensitive=False, onCleanValue=_cleanValue
|
|
80
|
-
# ) -> list[dict]:
|
|
81
|
-
# """
|
|
82
|
-
# returns a list of records where the keyName's value matches the compareTo value\n
|
|
83
|
-
# recordList: list, the list to evaluate\n
|
|
84
|
-
# keyName: str, the dict key of each record to compare compareTo to\n
|
|
85
|
-
# compareTo: str, the value to compare to\n
|
|
86
|
-
# caseSensitive: bool, if False (default), comparison is done case-insensitive and vice-versa\n
|
|
87
|
-
# onCleanValue: callable, method to call to clean the value
|
|
88
|
-
# """
|
|
89
|
-
# exactMatches = []
|
|
90
|
-
# compareTo = onCleanValue(compareTo, caseSensitive)
|
|
91
|
-
# for r in records:
|
|
92
|
-
# v = onCleanValue(r.get(keyName, None), caseSensitive)
|
|
93
|
-
# if v and (v == compareTo):
|
|
94
|
-
# exactMatches.append(r)
|
|
95
|
-
# return exactMatches
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
from kisspy.xtdPy.paths._common import frmtPath
|
|
2
|
-
from typing import Tuple
|
|
3
|
-
import pathlib
|
|
4
|
-
import os
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def _createDir(directory: str) -> bool:
|
|
8
|
-
if os.path.exists(directory):
|
|
9
|
-
return False
|
|
10
|
-
pathlib.Path(directory).mkdir(parents=True, exist_ok=True)
|
|
11
|
-
return True
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def splitPath(filename: str, convertToFwdSlsh: bool = True, endInSeparator: bool = True) -> Tuple[str, str]:
|
|
15
|
-
d, f = os.path.split(filename)
|
|
16
|
-
d = frmtPath(d, convertToFwdSlsh, endInSeparator)
|
|
17
|
-
return d, f
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def createDir(fqFilename: str, directory: str = None, convertToFwdSlsh: bool = True, endInSeparator: bool = True) -> str:
|
|
21
|
-
"""
|
|
22
|
-
creates the parent directories if they do not exist;\n
|
|
23
|
-
takes either filename (including directories) or directory parameter\n
|
|
24
|
-
returns the directory
|
|
25
|
-
"""
|
|
26
|
-
if fqFilename:
|
|
27
|
-
directory, f = splitPath(fqFilename, convertToFwdSlsh, endInSeparator)
|
|
28
|
-
else:
|
|
29
|
-
directory = frmtPath(directory, convertToFwdSlsh, endInSeparator)
|
|
30
|
-
_createDir(directory)
|
|
31
|
-
return directory
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def expandUser(path: str, expandChar: str = "~", convertToFwdSlsh: bool = True, endInSeparator: bool = False) -> str:
|
|
35
|
-
"""
|
|
36
|
-
expands the path to a fully qualified path using the expandChar as a signal to do so\n
|
|
37
|
-
replaces back slashes to forward slashes if convertToFwdSlsh is true\n
|
|
38
|
-
returns the path as a simple string
|
|
39
|
-
"""
|
|
40
|
-
if not path or not path.startswith(expandChar):
|
|
41
|
-
return path
|
|
42
|
-
homeDir = frmtPath(str(pathlib.Path.home()), convertToFwdSlsh, endInSeparator)
|
|
43
|
-
return path.replace(expandChar, homeDir)
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
from kisspy.xtdPy.paths.directories import expandUser
|
|
2
|
-
from kisspy.xtdPy.json import loadData
|
|
3
|
-
from typing import Tuple
|
|
4
|
-
import datetime
|
|
5
|
-
import pathlib
|
|
6
|
-
import logging
|
|
7
|
-
import os
|
|
8
|
-
|
|
9
|
-
_logger = logging.getLogger(__name__)
|
|
10
|
-
_logger.addHandler(logging.NullHandler())
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def lastModifiedDT(fqFilename) -> datetime.datetime:
|
|
14
|
-
"""returns a datetime object for the file specified, or None if it fails"""
|
|
15
|
-
try:
|
|
16
|
-
pth = pathlib.Path(fqFilename)
|
|
17
|
-
return datetime.datetime.fromtimestamp(pth.stat().st_mtime)
|
|
18
|
-
except:
|
|
19
|
-
return None
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def _getFn(directory: str, fn: str) -> str:
|
|
23
|
-
"""returns a string as the filepath"""
|
|
24
|
-
return "{}/{}".format(expandUser(directory), fn)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def searchForFile(filename: str, locations: list[str]) -> str:
|
|
28
|
-
"""
|
|
29
|
-
searches for a file in the locations provided and returns the fn\n
|
|
30
|
-
locations can include the home directory designation '~'
|
|
31
|
-
"""
|
|
32
|
-
for fn in [_getFn(x, filename) for x in locations]:
|
|
33
|
-
_logger.debug("Searching For File '{}'".format(fn))
|
|
34
|
-
if os.path.exists(fn) and os.path.isfile(fn):
|
|
35
|
-
return fn
|
|
36
|
-
return None
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def searchAndload(filename: str, directories: list[str], defaultData=None) -> Tuple[str, dict]:
|
|
40
|
-
"""
|
|
41
|
-
searches for a file in the format location/appName/filename and returns the fn and loaded json content\n
|
|
42
|
-
directories can include the home directory designation '~'
|
|
43
|
-
"""
|
|
44
|
-
fn = searchForFile(filename, directories)
|
|
45
|
-
if fn:
|
|
46
|
-
_logger.debug("Found File '{}'".format(fn))
|
|
47
|
-
return fn, loadData(fn, defaultData)
|
|
48
|
-
return None, defaultData
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import base64
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def fromFile(filename):
|
|
5
|
-
"""
|
|
6
|
-
returns a string representing the file contents in Base64
|
|
7
|
-
"""
|
|
8
|
-
with open(filename, "rb") as image_file:
|
|
9
|
-
data = base64.b64encode(image_file.read()).decode("utf-8")
|
|
10
|
-
return data
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def fromBytes(bytes):
|
|
14
|
-
"""
|
|
15
|
-
returns a string representing the bytes in Base64
|
|
16
|
-
"""
|
|
17
|
-
return base64.b64encode(bytes).decode("utf-8")
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def toBytes(base64str):
|
|
21
|
-
"""
|
|
22
|
-
writes Base64 string back to original binary encoding
|
|
23
|
-
"""
|
|
24
|
-
return base64.b64decode(base64str)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def toFile(filename, base64str):
|
|
28
|
-
"""
|
|
29
|
-
writes Base64 string back to original binary encoded file
|
|
30
|
-
"""
|
|
31
|
-
with open(filename, "wb") as writer:
|
|
32
|
-
writer.write(base64.b64decode(base64str))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|