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.
Files changed (47) hide show
  1. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/PKG-INFO +1 -1
  2. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/changes.md +9 -0
  3. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/_metadata.json +1 -1
  4. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/converters/excelHelpers.py +18 -2
  5. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/converters/numericConverter.py +13 -5
  6. kisspy_python-1.2.0/kisspy/decorators/methodParameters.py +97 -0
  7. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/decorators/singleton.py +9 -5
  8. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/decorators/thrdSafeSync.py +12 -7
  9. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/exceptions.py +4 -0
  10. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/metaclasses/singleton.py +6 -5
  11. kisspy_python-1.2.0/kisspy/xtdPy/dicts.py +90 -0
  12. kisspy_python-1.2.0/kisspy/xtdPy/dt/timeFormatting.py +26 -0
  13. kisspy_python-1.2.0/kisspy/xtdPy/lists.py +84 -0
  14. kisspy_python-1.2.0/kisspy/xtdPy/paths/directories.py +68 -0
  15. kisspy_python-1.2.0/kisspy/xtdPy/paths/files.py +72 -0
  16. kisspy_python-1.2.0/kisspy/xtdPy/strings/base64data.py +54 -0
  17. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy_python.egg-info/PKG-INFO +1 -1
  18. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy_python.egg-info/SOURCES.txt +1 -0
  19. kisspy_python-1.0.1/kisspy/xtdPy/dicts.py +0 -63
  20. kisspy_python-1.0.1/kisspy/xtdPy/dt/timeFormatting.py +0 -21
  21. kisspy_python-1.0.1/kisspy/xtdPy/lists.py +0 -95
  22. kisspy_python-1.0.1/kisspy/xtdPy/paths/directories.py +0 -43
  23. kisspy_python-1.0.1/kisspy/xtdPy/paths/files.py +0 -48
  24. kisspy_python-1.0.1/kisspy/xtdPy/strings/base64data.py +0 -32
  25. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/MANIFEST.in +0 -0
  26. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/__init__.py +0 -0
  27. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/converters/__init__.py +0 -0
  28. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/decorators/__init__.py +0 -0
  29. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/decorators/pidFile.py +0 -0
  30. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/metaclasses/__init__.py +0 -0
  31. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/xtdPy/__init__.py +0 -0
  32. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/xtdPy/dt/__init__.py +0 -0
  33. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/xtdPy/json.py +0 -0
  34. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/xtdPy/paths/__init__.py +0 -0
  35. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/xtdPy/paths/_common.py +0 -0
  36. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/xtdPy/strings/__init__.py +0 -0
  37. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/xtdPy/strings/textExtensions.py +0 -0
  38. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy/xtdPy/strings/textNormalizer.py +0 -0
  39. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy_python.egg-info/dependency_links.txt +0 -0
  40. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy_python.egg-info/requires.txt +0 -0
  41. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/kisspy_python.egg-info/top_level.txt +0 -0
  42. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/license.md +0 -0
  43. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/pyproject.toml +0 -0
  44. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/readme.md +0 -0
  45. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/requirements/prod.txt +0 -0
  46. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/setup.cfg +0 -0
  47. {kisspy_python-1.0.1 → kisspy_python-1.2.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kisspy-python
3
- Version: 1.0.1
3
+ Version: 1.2.0
4
4
  Summary: Simple Python Scripting Helpers
5
5
  Home-page: https://github.com/joemarchionna/kisspy
6
6
  Author: Joe Marchionna
@@ -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
@@ -1,7 +1,7 @@
1
1
 
2
2
  {
3
3
  "name": "kisspy-python",
4
- "version": "1.0.1",
4
+ "version": "1.2.0",
5
5
  "author": "Joe Marchionna",
6
6
  "author_email": "joemarchionna@gmail.com",
7
7
  "description": "Simple Python Scripting Helpers",
@@ -13,7 +13,15 @@ def _divmod_excel(n):
13
13
 
14
14
 
15
15
  def toExcelCol(columnNum: int) -> str:
16
- """converts a 1-based column number to Excel column name, ie: 27 -> AA"""
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
- """converts an Excel column name to a 1-based integer, ie: AA -> 27"""
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
- return args[0]
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\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
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;\n
4
- usage:\n
3
+ class decorator to create a single instance of a class
5
4
 
6
- @singleton\n
7
- class MyClass(<baseClass>):\n
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\n
7
- example:\n
8
- ```python
9
- @thrdSafeSync
10
- def oneAtATime():
11
- file IO process...
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;\n
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))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kisspy-python
3
- Version: 1.0.1
3
+ Version: 1.2.0
4
4
  Summary: Simple Python Scripting Helpers
5
5
  Home-page: https://github.com/joemarchionna/kisspy
6
6
  Author: Joe Marchionna
@@ -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