kisspy-python 1.0.1__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.
- kisspy/__init__.py +7 -0
- kisspy/_metadata.json +21 -0
- kisspy/converters/__init__.py +2 -0
- kisspy/converters/excelHelpers.py +26 -0
- kisspy/converters/numericConverter.py +32 -0
- kisspy/decorators/__init__.py +0 -0
- kisspy/decorators/pidFile.py +32 -0
- kisspy/decorators/singleton.py +18 -0
- kisspy/decorators/thrdSafeSync.py +20 -0
- kisspy/exceptions.py +29 -0
- kisspy/metaclasses/__init__.py +0 -0
- kisspy/metaclasses/singleton.py +16 -0
- kisspy/xtdPy/__init__.py +0 -0
- kisspy/xtdPy/dicts.py +63 -0
- kisspy/xtdPy/dt/__init__.py +0 -0
- kisspy/xtdPy/dt/timeFormatting.py +21 -0
- kisspy/xtdPy/json.py +80 -0
- kisspy/xtdPy/lists.py +95 -0
- kisspy/xtdPy/paths/__init__.py +0 -0
- kisspy/xtdPy/paths/_common.py +20 -0
- kisspy/xtdPy/paths/directories.py +43 -0
- kisspy/xtdPy/paths/files.py +48 -0
- kisspy/xtdPy/strings/__init__.py +0 -0
- kisspy/xtdPy/strings/base64data.py +32 -0
- kisspy/xtdPy/strings/textExtensions.py +41 -0
- kisspy/xtdPy/strings/textNormalizer.py +103 -0
- kisspy_python-1.0.1.dist-info/METADATA +251 -0
- kisspy_python-1.0.1.dist-info/RECORD +30 -0
- kisspy_python-1.0.1.dist-info/WHEEL +5 -0
- kisspy_python-1.0.1.dist-info/top_level.txt +1 -0
kisspy/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from kisspy.converters.numericConverter import toNumeric, raiseValExc, returnNone, returnTxt
|
|
2
|
+
from kisspy.xtdPy.dt.timeFormatting import getSortableNow, SORTABLE_DT_FRMT, SORTABLE_DT_TZA_FRMT
|
|
3
|
+
from kisspy.xtdPy.paths.directories import splitPath, expandUser, createDir
|
|
4
|
+
from kisspy.xtdPy.strings.textExtensions import randomString
|
|
5
|
+
from kisspy.xtdPy.dicts import getValueOfPath, setValueOfPath, addToDictIfExists
|
|
6
|
+
from kisspy.xtdPy.lists import appendUnique, assertOne
|
|
7
|
+
from kisspy.xtdPy.json import dumpData, loadData, createAndReturnDefault
|
kisspy/_metadata.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
|
|
2
|
+
{
|
|
3
|
+
"name": "kisspy-python",
|
|
4
|
+
"version": "1.0.1",
|
|
5
|
+
"author": "Joe Marchionna",
|
|
6
|
+
"author_email": "joemarchionna@gmail.com",
|
|
7
|
+
"description": "Simple Python Scripting Helpers",
|
|
8
|
+
"requirements": "requirements/prod.txt",
|
|
9
|
+
"url": "https://github.com/joemarchionna/kisspy",
|
|
10
|
+
"packageData":
|
|
11
|
+
{
|
|
12
|
+
"kisspy": [
|
|
13
|
+
"*.json"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"classifiers": [
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Intended Audience :: Developers"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from functools import reduce
|
|
2
|
+
import string
|
|
3
|
+
|
|
4
|
+
# https://stackoverflow.com/questions/48983939/convert-a-number-to-excel-s-base-26
|
|
5
|
+
# this works for SBS plates as well
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _divmod_excel(n):
|
|
9
|
+
a, b = divmod(n, 26)
|
|
10
|
+
if b == 0:
|
|
11
|
+
return a - 1, b + 26
|
|
12
|
+
return a, b
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def toExcelCol(columnNum: int) -> str:
|
|
16
|
+
"""converts a 1-based column number to Excel column name, ie: 27 -> AA"""
|
|
17
|
+
chars = []
|
|
18
|
+
while columnNum > 0:
|
|
19
|
+
columnNum, d = _divmod_excel(columnNum)
|
|
20
|
+
chars.append(string.ascii_uppercase[d - 1])
|
|
21
|
+
return "".join(reversed(chars))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def fromExcelCol(columnName: str) -> int:
|
|
25
|
+
"""converts an Excel column name to a 1-based integer, ie: AA -> 27"""
|
|
26
|
+
return reduce(lambda r, x: r * 26 + x + 1, map(string.ascii_uppercase.index, columnName), 0)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
def raiseValExc(*args, **kwargs):
|
|
2
|
+
"""raises a ValueError with the first argument displayed in single quotes as part of the error message"""
|
|
3
|
+
msg = "The Value '{}' Could Not Be Converted To A Numeric Value".format(args[0])
|
|
4
|
+
args[1].add_note(msg)
|
|
5
|
+
raise args[1]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def returnNone(*args, **kwargs) -> None:
|
|
9
|
+
"""returns Python None variable regardless of arguments passed"""
|
|
10
|
+
return None
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def returnTxt(*args, **kwargs) -> None:
|
|
14
|
+
"""returns the first argument, does nothing else"""
|
|
15
|
+
return args[0]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def toNumeric(val, onFail=returnNone) -> float | int | None:
|
|
19
|
+
"""
|
|
20
|
+
returns a float or int, by converting the provided val\n
|
|
21
|
+
\t to a float and if possible, an int\n
|
|
22
|
+
on error, it calls onFail providing val as the first parameter\n
|
|
23
|
+
\t by default, returns None
|
|
24
|
+
"""
|
|
25
|
+
try:
|
|
26
|
+
if isinstance(val, str):
|
|
27
|
+
val = float(val)
|
|
28
|
+
if int(val) == val:
|
|
29
|
+
return int(val)
|
|
30
|
+
return val
|
|
31
|
+
except ValueError as verr:
|
|
32
|
+
return onFail(val, verr)
|
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from kisspy.exceptions import PidFilePresentException
|
|
2
|
+
from functools import wraps
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
PID_FN = "wkdir/appPID.txt"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def pidFile(filename: str = PID_FN):
|
|
9
|
+
"""
|
|
10
|
+
will allow method decorated to run only if the pid file specified is not present\n
|
|
11
|
+
if filename is set to None this will have no effect (will allow multiple instances)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def pidFileDecorator(func):
|
|
15
|
+
@wraps(func)
|
|
16
|
+
def wrapper(*args, **kwargs):
|
|
17
|
+
try:
|
|
18
|
+
if filename:
|
|
19
|
+
if os.path.exists(filename):
|
|
20
|
+
raise PidFilePresentException("Method Already Running!")
|
|
21
|
+
with open(filename, "w") as wtr:
|
|
22
|
+
wtr.write("{}\n".format(os.getpid()))
|
|
23
|
+
results = func(*args, **kwargs)
|
|
24
|
+
if filename:
|
|
25
|
+
os.remove(filename)
|
|
26
|
+
return results
|
|
27
|
+
except PidFilePresentException as pidErr:
|
|
28
|
+
print("{}: {}".format(PidFilePresentException.__name__, pidErr))
|
|
29
|
+
|
|
30
|
+
return wrapper
|
|
31
|
+
|
|
32
|
+
return pidFileDecorator
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
def singleton(_class):
|
|
2
|
+
"""
|
|
3
|
+
class decorator to create a single instance of a class;\n
|
|
4
|
+
usage:\n
|
|
5
|
+
|
|
6
|
+
@singleton\n
|
|
7
|
+
class MyClass(<baseClass>):\n
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
_instances = {}
|
|
12
|
+
|
|
13
|
+
def _getinstance(*args, **kwargs):
|
|
14
|
+
if _class not in _instances:
|
|
15
|
+
_instances[_class] = _class(*args, **kwargs)
|
|
16
|
+
return _instances[_class]
|
|
17
|
+
|
|
18
|
+
return _getinstance
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def thrdSafeSync(func):
|
|
5
|
+
"""
|
|
6
|
+
decorator that makes the function thread safe\n
|
|
7
|
+
example:\n
|
|
8
|
+
```python
|
|
9
|
+
@thrdSafeSync
|
|
10
|
+
def oneAtATime():
|
|
11
|
+
file IO process...
|
|
12
|
+
```
|
|
13
|
+
"""
|
|
14
|
+
lock = threading.Lock() # A lock for this specific function
|
|
15
|
+
|
|
16
|
+
def wrapper(*args, **kwargs):
|
|
17
|
+
with lock: # Acquire and release the lock using 'with' statement
|
|
18
|
+
return func(*args, **kwargs)
|
|
19
|
+
|
|
20
|
+
return wrapper
|
kisspy/exceptions.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
class KisspyException(Exception):
|
|
2
|
+
def toStrWithTyp(self) -> str:
|
|
3
|
+
return "{}: {}".format(type(self).__name__, self)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PidFilePresentException(KisspyException):
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TooManyRecordsException(KisspyException):
|
|
11
|
+
MSG_FORMAT = "{} {} Returned, {} Were Expected"
|
|
12
|
+
|
|
13
|
+
def __init__(self, *args: object, numberRecords: int = None, recordType: str = "Records", numberExpected: int = 1) -> None:
|
|
14
|
+
if numberRecords and recordType:
|
|
15
|
+
msg = TooManyRecordsException.MSG_FORMAT.format(numberRecords, recordType, numberExpected)
|
|
16
|
+
super().__init__(msg, *args)
|
|
17
|
+
else:
|
|
18
|
+
super().__init__(*args)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ZeroRecordsException(KisspyException):
|
|
22
|
+
MSG_FORMAT = "Zero (0) {} Returned, > 0 Were Expected"
|
|
23
|
+
|
|
24
|
+
def __init__(self, *args, recordType: str = "Records"):
|
|
25
|
+
if args:
|
|
26
|
+
super().__init__(*args)
|
|
27
|
+
else:
|
|
28
|
+
msg = ZeroRecordsException.MSG_FORMAT.format(recordType)
|
|
29
|
+
super().__init__(msg, *args)
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class Singleton(type):
|
|
2
|
+
"""
|
|
3
|
+
meta-class to create a single instance of a class;\n
|
|
4
|
+
usage:\n
|
|
5
|
+
|
|
6
|
+
class MyClass(<baseClass>, metaclass=Singleton):\n
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
_instances = {}
|
|
12
|
+
|
|
13
|
+
def __call__(cls, *args, **kwargs):
|
|
14
|
+
if cls not in cls._instances:
|
|
15
|
+
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
|
|
16
|
+
return cls._instances[cls]
|
kisspy/xtdPy/__init__.py
ADDED
|
File without changes
|
kisspy/xtdPy/dicts.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
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
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
kisspy/xtdPy/json.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import pathlib
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
_logger = logging.getLogger(__name__)
|
|
7
|
+
_logger.addHandler(logging.NullHandler())
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def returnNone(*args, **kwargs):
|
|
11
|
+
_logger.debug("No Data Provided, Returning None (Not Saving)")
|
|
12
|
+
return None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def dumpData(filename: str, data: dict | list, onNoData=returnNone) -> str:
|
|
16
|
+
"""
|
|
17
|
+
dumps the data to the filename specified, creating parent directories if neccessary
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
filename (str): filename, including path
|
|
21
|
+
data (dict | list): the data to dump to JSON
|
|
22
|
+
onNoData (callable, optional): a method expecting the filename and data as args, in that order. Defaults to returnNone
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
str: filename
|
|
26
|
+
"""
|
|
27
|
+
if not data:
|
|
28
|
+
return onNoData(filename, data)
|
|
29
|
+
pathlib.Path(os.path.split(filename)[0]).mkdir(parents=True, exist_ok=True)
|
|
30
|
+
with open(filename, "w") as wtr:
|
|
31
|
+
json.dump(data, wtr, indent=4)
|
|
32
|
+
return filename
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def returnDefault(*args, **kwargs) -> dict:
|
|
36
|
+
"""
|
|
37
|
+
returns the JSON data\n
|
|
38
|
+
expects the filename and data as args, in that order
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
dict | list: JSON data
|
|
42
|
+
"""
|
|
43
|
+
_logger.debug("'{}' Does Not Exist, Returning Default Data".format(args[0]))
|
|
44
|
+
return args[1]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def createAndReturnDefault(*args, **kwargs) -> dict:
|
|
48
|
+
"""
|
|
49
|
+
saves the JSON data to the path provided and returns the data\n
|
|
50
|
+
expects the filename and data as args, in that order
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
dict | list: JSON data
|
|
54
|
+
"""
|
|
55
|
+
_logger.debug("'{}' Did Not Exist, Saved And Returning Default Data".format(args[0]))
|
|
56
|
+
dumpData(args[0], args[1])
|
|
57
|
+
return args[1]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def loadData(filename: str, defaultData=None, onNotExist=returnDefault) -> dict | list:
|
|
61
|
+
"""
|
|
62
|
+
loads json data from the file specified
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
filename (str): filename
|
|
66
|
+
defaultData (dict | list, optional): if the filename is not present, this value is passed to the onNotExist callable. Defaults to None
|
|
67
|
+
onNotExist (callable, optional): a method expecting the filename and default data as args, in that order. Defaults to returnDefault
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
dict | list: the JSON data in the file or defaultData
|
|
71
|
+
"""
|
|
72
|
+
if not filename:
|
|
73
|
+
_logger.debug("No Filename Provided, Returning Default Data")
|
|
74
|
+
return defaultData
|
|
75
|
+
if not os.path.exists(filename):
|
|
76
|
+
if onNotExist:
|
|
77
|
+
return onNotExist(filename, defaultData)
|
|
78
|
+
return defaultData
|
|
79
|
+
with open(filename) as rdr:
|
|
80
|
+
return json.load(rdr)
|
kisspy/xtdPy/lists.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
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
|
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
def addEndingSeparator(path: str, sep: str = "/") -> str:
|
|
2
|
+
"""ensure the path ends in the separator specified"""
|
|
3
|
+
if path.endswith(sep):
|
|
4
|
+
return path
|
|
5
|
+
return path + sep
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def removeEndingSeparator(path: str, sep: str = "/") -> str:
|
|
9
|
+
"""ensure the path does not end in the separator specified"""
|
|
10
|
+
if not path.endswith(sep):
|
|
11
|
+
return path
|
|
12
|
+
return path[: (len(path) - 1)]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def frmtPath(path: str, convertToFwdSlsh: bool, endInSeparator: bool) -> str:
|
|
16
|
+
if convertToFwdSlsh:
|
|
17
|
+
path = path.replace("\\", "/")
|
|
18
|
+
if endInSeparator:
|
|
19
|
+
path = addEndingSeparator(path)
|
|
20
|
+
return path
|
|
@@ -0,0 +1,43 @@
|
|
|
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)
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
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))
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def randomString(size=5) -> str:
|
|
6
|
+
"""
|
|
7
|
+
returns a random string of length size
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
size (int, optional): length of the string returned. Defaults to 5
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
str: random string
|
|
14
|
+
"""
|
|
15
|
+
return str(os.urandom(size).hex())[:size]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def replaceChars(source: str, remove: str, replaceWith: str = ""):
|
|
19
|
+
"""
|
|
20
|
+
cleans and returns a string by replacing / removing chars;\n
|
|
21
|
+
source: str, the source string to clean;\n
|
|
22
|
+
remove: str, string containing any chars to remove from the source string;\n
|
|
23
|
+
replaceWith: str, the string to put in place of any char found, empty (remove) or not empty (replace)
|
|
24
|
+
"""
|
|
25
|
+
removeRe = remove.replace("[", r"\[").replace("]", r"\]")
|
|
26
|
+
removeRe = "[{}]".format(removeRe)
|
|
27
|
+
result = re.sub(removeRe, replaceWith, source)
|
|
28
|
+
return result
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def cleanAndNormalize(source: str, remove=" %:,[]<>*?", replaceWith: str = "_"):
|
|
32
|
+
"""
|
|
33
|
+
cleans a string, works great for a filename
|
|
34
|
+
"""
|
|
35
|
+
cfn = replaceChars(source=source, remove=remove, replaceWith=replaceWith)
|
|
36
|
+
if replaceWith:
|
|
37
|
+
endsPattern = "(^" + replaceWith + ")|(" + replaceWith + "$)"
|
|
38
|
+
cfn = re.sub(endsPattern, "", cfn)
|
|
39
|
+
multiPattern = "([" + replaceWith + "]{2})"
|
|
40
|
+
cfn = re.sub(multiPattern, replaceWith, cfn)
|
|
41
|
+
return cfn
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import unidecode
|
|
2
|
+
import logging
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
SPACE_BEFORE = r"(["
|
|
6
|
+
SPACE_AFTER = r")];,"
|
|
7
|
+
NO_SPACE_BEFORE = r")]/\-;,"
|
|
8
|
+
NO_SPACE_AFTER = r"([/\-"
|
|
9
|
+
|
|
10
|
+
_logger = logging.getLogger(__name__)
|
|
11
|
+
_logger.addHandler(logging.NullHandler())
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _createSeq(chars: str, addSpace: bool, before: bool):
|
|
15
|
+
d = {}
|
|
16
|
+
for c in chars:
|
|
17
|
+
if addSpace:
|
|
18
|
+
d[c] = " {}".format(c) if before else "{} ".format(c)
|
|
19
|
+
else:
|
|
20
|
+
k = " {}".format(c) if before else "{} ".format(c)
|
|
21
|
+
d[k] = c
|
|
22
|
+
return d
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _removeKeys(s: str, sequence: dict):
|
|
26
|
+
for k in sequence.keys():
|
|
27
|
+
s = s.replace(str(k), sequence[k])
|
|
28
|
+
return s
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _normalize(txt: str, preferredSeq: dict, removeSeq: dict, toAscii: bool, removeUnprintable: bool, preserveMultispace: bool) -> str:
|
|
32
|
+
if removeUnprintable:
|
|
33
|
+
txt = "".join(c for c in txt if c.isprintable())
|
|
34
|
+
if toAscii:
|
|
35
|
+
txt = unidecode.unidecode(txt)
|
|
36
|
+
txt = _removeKeys(txt, preferredSeq)
|
|
37
|
+
if not preserveMultispace:
|
|
38
|
+
txt = re.sub(r"\s+", " ", txt)
|
|
39
|
+
txt = _removeKeys(txt, removeSeq)
|
|
40
|
+
return txt.strip()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def normalizeTxt(
|
|
44
|
+
txt: str,
|
|
45
|
+
spaceBefore: str = SPACE_BEFORE,
|
|
46
|
+
spaceAfter: str = SPACE_AFTER,
|
|
47
|
+
noSpaceBefore: str = NO_SPACE_BEFORE,
|
|
48
|
+
noSpaceAfter: str = NO_SPACE_AFTER,
|
|
49
|
+
toAscii: bool = True,
|
|
50
|
+
removeUnprintable: bool = True,
|
|
51
|
+
preserveMultispace: bool = False,
|
|
52
|
+
) -> str:
|
|
53
|
+
"""
|
|
54
|
+
normalizes string s;\n
|
|
55
|
+
'β-NS' --> 'b-NS'\n
|
|
56
|
+
'Some Text(Example)For Display' --> 'Some Text (Example) For Display'
|
|
57
|
+
"""
|
|
58
|
+
preferredSequences = _createSeq(spaceBefore, True, True)
|
|
59
|
+
preferredSequences.update(_createSeq(spaceAfter, True, False))
|
|
60
|
+
removeSequences = _createSeq(noSpaceBefore, False, True)
|
|
61
|
+
removeSequences.update(_createSeq(noSpaceAfter, False, False))
|
|
62
|
+
ns = _normalize(txt, preferredSequences, removeSequences, toAscii, removeUnprintable, preserveMultispace)
|
|
63
|
+
_logger.debug("Text '{}' Normalized To '{}'".format(txt, ns))
|
|
64
|
+
return ns
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TextNormalizer(object):
|
|
68
|
+
"""provides an object that persists the settings and log debug statements if needed"""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
spaceBefore: str = SPACE_BEFORE,
|
|
73
|
+
spaceAfter: str = SPACE_AFTER,
|
|
74
|
+
noSpaceBefore: str = NO_SPACE_BEFORE,
|
|
75
|
+
noSpaceAfter: str = NO_SPACE_AFTER,
|
|
76
|
+
toAscii: bool = True,
|
|
77
|
+
removeUnprintable: bool = True,
|
|
78
|
+
preserveMultispace: bool = False,
|
|
79
|
+
) -> None:
|
|
80
|
+
self.toAscii = toAscii
|
|
81
|
+
self.removeUnprintable = removeUnprintable
|
|
82
|
+
self.preserveMultiSpace = preserveMultispace
|
|
83
|
+
self.preferredSequences = _createSeq(spaceBefore, True, True)
|
|
84
|
+
self.preferredSequences.update(_createSeq(spaceAfter, True, False))
|
|
85
|
+
self.removeSequences = _createSeq(noSpaceBefore, False, True)
|
|
86
|
+
self.removeSequences.update(_createSeq(noSpaceAfter, False, False))
|
|
87
|
+
|
|
88
|
+
def normalize(self, s: str):
|
|
89
|
+
"""
|
|
90
|
+
normalizes string s;\n
|
|
91
|
+
'β-NS' --> 'b-NS'\n
|
|
92
|
+
'Some Text(Example)For Display' --> 'Some Text (Example) For Display'
|
|
93
|
+
"""
|
|
94
|
+
ns = _normalize(
|
|
95
|
+
s,
|
|
96
|
+
self.preferredSequences,
|
|
97
|
+
self.removeSequences,
|
|
98
|
+
self.toAscii,
|
|
99
|
+
self.removeUnprintable,
|
|
100
|
+
self.preserveMultiSpace,
|
|
101
|
+
)
|
|
102
|
+
_logger.debug("Text '{}' Normalized To '{}'".format(s, ns))
|
|
103
|
+
return ns
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kisspy-python
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Simple Python Scripting Helpers
|
|
5
|
+
Home-page: https://github.com/joemarchionna/kisspy
|
|
6
|
+
Author: Joe Marchionna
|
|
7
|
+
Author-email: joemarchionna@gmail.com
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) Joe Marchionna
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
|
|
30
|
+
Classifier: Programming Language :: Python :: 3
|
|
31
|
+
Classifier: Operating System :: OS Independent
|
|
32
|
+
Classifier: Intended Audience :: Developers
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
Requires-Dist: unidecode
|
|
35
|
+
Dynamic: author
|
|
36
|
+
Dynamic: author-email
|
|
37
|
+
Dynamic: classifier
|
|
38
|
+
Dynamic: description
|
|
39
|
+
Dynamic: description-content-type
|
|
40
|
+
Dynamic: home-page
|
|
41
|
+
Dynamic: license
|
|
42
|
+
Dynamic: requires-dist
|
|
43
|
+
Dynamic: summary
|
|
44
|
+
|
|
45
|
+
# kisspy - Simple Python Scripting Helpers
|
|
46
|
+
|
|
47
|
+
This is a collection of simple helper methods and classes that I have found useful for data processing
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
The package offers simple methods that I found I was reusing frequently.
|
|
52
|
+
|
|
53
|
+
### Converters
|
|
54
|
+
|
|
55
|
+
Numeric Converter
|
|
56
|
+
|
|
57
|
+
````python
|
|
58
|
+
from kisspy import toNumeric, returnTxt,raiseValExc
|
|
59
|
+
|
|
60
|
+
def rtnMsg(*args):
|
|
61
|
+
return "Can't Convert '{}' To A Number".format(args[0])
|
|
62
|
+
|
|
63
|
+
alphaVal = "55"
|
|
64
|
+
numVal = toNumeric(alphaVal)
|
|
65
|
+
print(isinstance(numVal, int))
|
|
66
|
+
|
|
67
|
+
alphaVal = "88 ft"
|
|
68
|
+
numVal = toNumeric(alphaVal)
|
|
69
|
+
print(numVal)
|
|
70
|
+
|
|
71
|
+
numVal = toNumeric(alphaVal, onFail=returnTxt)
|
|
72
|
+
print(numVal)
|
|
73
|
+
|
|
74
|
+
numVal = toNumeric(alphaVal, onFail=rtnMsg)
|
|
75
|
+
print(numVal)
|
|
76
|
+
|
|
77
|
+
numVal = toNumeric(alphaVal, onFail=raiseValExc)
|
|
78
|
+
|
|
79
|
+
>> True
|
|
80
|
+
>> None
|
|
81
|
+
>> 88 ft
|
|
82
|
+
>> Can't Convert '88 ft' To A Number
|
|
83
|
+
>> Traceback (most recent call last):
|
|
84
|
+
>> File "C:\Files\code\python\projects\kisspy\examples\converters.py", line 26, in <module>
|
|
85
|
+
>> numVal = toNumeric(alphaVal, onFail=raiseValExc)
|
|
86
|
+
>> File "C:\Files\code\python\projects\kisspy\kisspy\converters\numericConverter.py", line 32, in toNumeric
|
|
87
|
+
>> return onFail(val,verr)
|
|
88
|
+
>> File "C:\Files\code\python\projects\kisspy\kisspy\converters\numericConverter.py", line 5, in raiseValExc
|
|
89
|
+
>> raise args[1]
|
|
90
|
+
>> File "C:\Files\code\python\projects\kisspy\kisspy\converters\numericConverter.py", line 27, in toNumeric
|
|
91
|
+
>> val = float(val)
|
|
92
|
+
>> ValueError: could not convert string to float: '88 ft'
|
|
93
|
+
>> The Value '88 ft' Could Not Be Converted To A Numeric Value
|
|
94
|
+
````
|
|
95
|
+
|
|
96
|
+
Spreadsheet Converters
|
|
97
|
+
|
|
98
|
+
````python
|
|
99
|
+
from kisspy.converters.excelHelpers import fromExcelCol, toExcelCol
|
|
100
|
+
|
|
101
|
+
col = "C"
|
|
102
|
+
print(fromExcelCol(col))
|
|
103
|
+
|
|
104
|
+
col = "AF"
|
|
105
|
+
print(fromExcelCol(col))
|
|
106
|
+
|
|
107
|
+
num = 2
|
|
108
|
+
print(toExcelCol(num))
|
|
109
|
+
|
|
110
|
+
num = 33
|
|
111
|
+
print(toExcelCol(num))
|
|
112
|
+
|
|
113
|
+
>> 3
|
|
114
|
+
>> 32
|
|
115
|
+
>> B
|
|
116
|
+
>> AG
|
|
117
|
+
````
|
|
118
|
+
|
|
119
|
+
The elements in decorators and metaclasses modules speak for themselves.
|
|
120
|
+
|
|
121
|
+
There are a number of Python "extensions" that do stuff you would have wanted anyways:
|
|
122
|
+
|
|
123
|
+
Unique Appending To Lists:
|
|
124
|
+
|
|
125
|
+
````python
|
|
126
|
+
from kisspy import appendUnique
|
|
127
|
+
|
|
128
|
+
vl = [2, 4, 6, 8]
|
|
129
|
+
if appendUnique(vl, 4):
|
|
130
|
+
print("Added 4 Again: {}".format(vl))
|
|
131
|
+
|
|
132
|
+
if appendUnique(vl, 10):
|
|
133
|
+
print("Added 10: {}".format(vl))
|
|
134
|
+
|
|
135
|
+
>> Added 10: [2, 4, 6, 8, 10]
|
|
136
|
+
````
|
|
137
|
+
|
|
138
|
+
Asserting One Record: Returns the one record, with options:
|
|
139
|
+
````python
|
|
140
|
+
from kisspy import assertOne
|
|
141
|
+
from kisspy.exceptions import TooManyRecordsException, ZeroRecordsException
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def thrwZeroRecordsEx(*args, **kwargs):
|
|
145
|
+
raise ZeroRecordsException()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
vl = [2, 4, 6, 8]
|
|
149
|
+
print(assertOne([]))
|
|
150
|
+
print(assertOne([44]))
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
oneValue = assertOne(vl)
|
|
154
|
+
except TooManyRecordsException as err:
|
|
155
|
+
print(err.toStrWithTyp())
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
print(assertOne([], onZeroRcds=thrwZeroRecordsEx))
|
|
159
|
+
except ZeroRecordsException as err:
|
|
160
|
+
print(err.toStrWithTyp())
|
|
161
|
+
|
|
162
|
+
>> None
|
|
163
|
+
>> 44
|
|
164
|
+
>> TooManyRecordsException: 4 Records Returned, 1 Were Expected
|
|
165
|
+
>> ZeroRecordsException: Zero (0) Records Returned, > 0 Were Expected
|
|
166
|
+
````
|
|
167
|
+
|
|
168
|
+
Nested Dicts:
|
|
169
|
+
|
|
170
|
+
````python
|
|
171
|
+
from kisspy import setValueOfPath, getValueOfPath
|
|
172
|
+
|
|
173
|
+
vd = {"animals": {"dog": {"whippet": "fast", "lab": "slower"}}}
|
|
174
|
+
|
|
175
|
+
wv = getValueOfPath(vd, keyPath="animals/dog/lab")
|
|
176
|
+
print(wv)
|
|
177
|
+
|
|
178
|
+
cv = getValueOfPath(vd, keyPath="animals/cat/tabby")
|
|
179
|
+
print(cv)
|
|
180
|
+
|
|
181
|
+
setValueOfPath(vd, "animals/dog/golden", "silly")
|
|
182
|
+
print(vd)
|
|
183
|
+
|
|
184
|
+
>> slower
|
|
185
|
+
>> None
|
|
186
|
+
>> {'animals': {'dog': {'whippet': 'fast', 'lab': 'slower', 'golden': 'silly'}}}
|
|
187
|
+
````
|
|
188
|
+
|
|
189
|
+
## Installation
|
|
190
|
+
|
|
191
|
+
### Using In Projects
|
|
192
|
+
|
|
193
|
+
Installation:
|
|
194
|
+
|
|
195
|
+
````bash
|
|
196
|
+
pip install kisspy-python
|
|
197
|
+
````
|
|
198
|
+
|
|
199
|
+
### Cloning For Development
|
|
200
|
+
|
|
201
|
+
<p>Set up a virtual environment. Once an environment is set up, run the command below to:
|
|
202
|
+
|
|
203
|
+
* validate the environment variable
|
|
204
|
+
* activate the environment (if not already activated)
|
|
205
|
+
* install all of the necessary packages into the local environment
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
pip install -U -r requirements/dev.txt
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
<p>The dev.txt file includes:
|
|
212
|
+
|
|
213
|
+
* BLACK, a code formatter, see notes at the bottom of this file for details
|
|
214
|
+
|
|
215
|
+
## Dependancies
|
|
216
|
+
|
|
217
|
+
This library depends on the following projects:
|
|
218
|
+
|
|
219
|
+
* unidecode
|
|
220
|
+
|
|
221
|
+
## Tests
|
|
222
|
+
|
|
223
|
+
To run tests:
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
python -m unittest discover -s tests/
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Code Formatting
|
|
230
|
+
|
|
231
|
+
Code formatting is done using BLACK. BLACK allows almost no customization to how code is formatted with the exception of line length, which has been set to 119 characters.
|
|
232
|
+
|
|
233
|
+
Use the following to bulk format files:
|
|
234
|
+
|
|
235
|
+
````bash
|
|
236
|
+
black . -l 144
|
|
237
|
+
````
|
|
238
|
+
|
|
239
|
+
## Creating A New Release
|
|
240
|
+
|
|
241
|
+
Please do the following when making a new release, most are documented above:
|
|
242
|
+
|
|
243
|
+
1. Run tests
|
|
244
|
+
1. Code format
|
|
245
|
+
1. Be sure to update the change log and _metadata.json with version and notes
|
|
246
|
+
1. git add, commit, and push changes
|
|
247
|
+
1. run the following code to generate a wheel:
|
|
248
|
+
|
|
249
|
+
````bash
|
|
250
|
+
python -m build
|
|
251
|
+
````
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
kisspy/__init__.py,sha256=1AByCkDjKpxKhk318T0xXUbCrf3lOcaSW2FWu6LEKUs,537
|
|
2
|
+
kisspy/_metadata.json,sha256=AGuqG0M992j9GSzMq5eXJJ_7gnRtFIdnG1s4Z-7dE_Y,535
|
|
3
|
+
kisspy/exceptions.py,sha256=3w0Q_cGgiQnjztuWseeXf-IgC6PIQ_3It7YU_wKG21s,996
|
|
4
|
+
kisspy/converters/__init__.py,sha256=h9Jt5wflyMowJqxXoTZzR6rn_l7osVwzjZSD-vnKSsw,161
|
|
5
|
+
kisspy/converters/excelHelpers.py,sha256=gfSg83RBddY-PaAJsJqYFG9Rv__nvSrJHIQwBOf3t3w,783
|
|
6
|
+
kisspy/converters/numericConverter.py,sha256=c7Mbrumy8-h5V_ItZn9S6RNMmck-AiONVCl6rQQb7DM,1014
|
|
7
|
+
kisspy/decorators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
kisspy/decorators/pidFile.py,sha256=05-HuipSi_bwc2K-i_-yAz-idVn6CtMrH3ueOTWmSt0,1070
|
|
9
|
+
kisspy/decorators/singleton.py,sha256=WjXBv8vrPkpgDPIBWcZuwqprd_tCr5-4fL_-mYUv1nE,393
|
|
10
|
+
kisspy/decorators/thrdSafeSync.py,sha256=fQO0qMh3FoZYeCbp9MXrxHWoC-q5nF5Cb4LAuagHFVU,450
|
|
11
|
+
kisspy/metaclasses/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
kisspy/metaclasses/singleton.py,sha256=J_5WzlumxQEWhGDqFM3slBe1Fqa382HBMOp31045O1w,396
|
|
13
|
+
kisspy/xtdPy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
kisspy/xtdPy/dicts.py,sha256=74gL_dLZUMjAhtcVrM_VMUpS1q4SMgCviN7gL4FL-34,2233
|
|
15
|
+
kisspy/xtdPy/json.py,sha256=1PgaUvuSG0g02ojS3t0e0gArcq6QeFRKPKFgg7qctqY,2488
|
|
16
|
+
kisspy/xtdPy/lists.py,sha256=rwxuGbha_Fmkn9eRmP28n-Bb13iHrL8mxw8JmdZ5ggU,3372
|
|
17
|
+
kisspy/xtdPy/dt/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
+
kisspy/xtdPy/dt/timeFormatting.py,sha256=SeuImL28ml91mDj2UhiJLIjb-Ul1iiZzkKoqhkgZ1u4,653
|
|
19
|
+
kisspy/xtdPy/paths/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
+
kisspy/xtdPy/paths/_common.py,sha256=TA49EZStx0XOiQAdf3WdDj7MXof-gQsmzY7DtvnpEYY,623
|
|
21
|
+
kisspy/xtdPy/paths/directories.py,sha256=UBaYy_XYfK7c6CLpyOriBo7Y2oa6MgOQ1VV4Bl7SHPk,1561
|
|
22
|
+
kisspy/xtdPy/paths/files.py,sha256=1bF72IrF5B4m0KPmOhS-IXwRfn9oX_ESYXAMe_VtYrg,1570
|
|
23
|
+
kisspy/xtdPy/strings/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
|
+
kisspy/xtdPy/strings/base64data.py,sha256=41je_wjZRt2x70Ksx-vf3lV8CwBOPCqDUtHcEQcP1Z8,734
|
|
25
|
+
kisspy/xtdPy/strings/textExtensions.py,sha256=uG_L5fB3jPKUrGkbcrf-llpp6apH1xXg5SRb38O4Uk8,1319
|
|
26
|
+
kisspy/xtdPy/strings/textNormalizer.py,sha256=X1HWzqBt0-eIY9g1XTezL1eaY30ssl3eJcWIcFQLiI8,3356
|
|
27
|
+
kisspy_python-1.0.1.dist-info/METADATA,sha256=7OBk8QBbiAZMN86B9rBkUKF-ivQaSAUweux7wCbHqgQ,6705
|
|
28
|
+
kisspy_python-1.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
29
|
+
kisspy_python-1.0.1.dist-info/top_level.txt,sha256=MlVmNAYxhXMcmWvxuxk1NMsr-v_ir8Ri51GwxAVJz6s,7
|
|
30
|
+
kisspy_python-1.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
kisspy
|