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 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,2 @@
1
+ from kisspy.converters.numericConverter import toNumeric, raiseValExc, returnNone, returnTxt
2
+ from kisspy.converters.excelHelpers import toExcelCol, fromExcelCol
@@ -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]
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ kisspy