kisa-utils 0.39.1__py3-none-any.whl → 0.40.0__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.
kisa_utils/__init__.py CHANGED
@@ -18,3 +18,4 @@ from kisa_utils import permissions
18
18
  from kisa_utils import functionUtils
19
19
  from kisa_utils import servers
20
20
  from kisa_utils import enqueue
21
+ from kisa_utils import dataStructures
@@ -0,0 +1,89 @@
1
+ import copy
2
+ import pprint
3
+
4
+ class KDict(dict):
5
+ '''
6
+ A dict subclass that allows attribute access (obj.key)
7
+ and recursively converts nested dicts, lists, tuples, and sets.
8
+ '''
9
+
10
+ def __init__(self, *args, **kwargs):
11
+ super().__init__(*args, **kwargs)
12
+ self._convert_nested()
13
+
14
+ def __getattr__(self, name):
15
+ try:
16
+ return self[name]
17
+ except KeyError:
18
+ raise AttributeError(f"`KDict` object has no attribute '{name}'")
19
+
20
+ def __setattr__(self, name, value):
21
+ self[name] = value
22
+
23
+ def __delattr__(self, name):
24
+ try:
25
+ del self[name]
26
+ except KeyError:
27
+ raise AttributeError(f"`KDict` object has no attribute '{name}'")
28
+
29
+ @staticmethod
30
+ def _wrap(value):
31
+ '''Recursively wrap any dicts inside nested structures.'''
32
+ if isinstance(value, dict) and not isinstance(value, KDict):
33
+ return KDict(value)
34
+ elif isinstance(value, list):
35
+ return [KDict._wrap(v) for v in value]
36
+ elif isinstance(value, tuple):
37
+ return tuple(KDict._wrap(v) for v in value)
38
+ elif isinstance(value, set):
39
+ return {KDict._wrap(v) for v in value}
40
+ else:
41
+ return value
42
+
43
+ def _convert_nested(self):
44
+ '''Convert all nested dicts/lists/tuples/sets in self.'''
45
+ for k, v in list(self.items()):
46
+ super().__setitem__(k, self._wrap(v))
47
+
48
+ def copy(self):
49
+ return KDict(copy.deepcopy(self))
50
+
51
+ def update(self, *args, **kwargs):
52
+ result = super().update(*args, **kwargs)
53
+ self._convert_nested()
54
+ return result
55
+
56
+ @classmethod
57
+ def fromkeys(cls, seq, value=None):
58
+ return cls({k: value for k in seq})
59
+
60
+ def __getitem__(self, key):
61
+ value = super().__getitem__(key)
62
+ wrapped = self._wrap(value)
63
+ if wrapped is not value:
64
+ super().__setitem__(key, wrapped)
65
+ return wrapped
66
+
67
+ def toDict(self):
68
+ '''
69
+ convert KDict to dict
70
+ '''
71
+ def unwrap(v):
72
+ if isinstance(v, KDict):
73
+ return {k: unwrap(x) for k, x in v.items()}
74
+ if isinstance(v, list):
75
+ return [unwrap(x) for x in v]
76
+ if isinstance(v, tuple):
77
+ return tuple(unwrap(x) for x in v)
78
+ if isinstance(v, set):
79
+ return {unwrap(x) for x in v}
80
+ return v
81
+ return unwrap(self)
82
+
83
+ def __repr__(self):
84
+ return repr(dict(self))
85
+
86
+ # map the KDict type to pprint's dict pretty-printer
87
+ try:
88
+ pprint.PrettyPrinter._dispatch[KDict.__repr__] = pprint.PrettyPrinter._pprint_dict
89
+ except Exception: pass
kisa_utils/db.py CHANGED
@@ -14,6 +14,7 @@ from .response import Error, Ok, Response
14
14
  from .functionUtils import enforceRequirements, Definition
15
15
  from .structures.utils import Value
16
16
  from . import structures
17
+ from .dataStructures import KDict
17
18
 
18
19
  if sqlite3.sqlite_version_info[1]<38:
19
20
  sys.exit(f'we need sqlite3 v3.38.0+ to run this program. current version::{sqlite3.sqlite_version}')
@@ -395,7 +396,7 @@ class Api:
395
396
 
396
397
  return _data
397
398
 
398
- def __decodeOutgoingData(self, data:list[tuple]) -> list[tuple]:
399
+ def __decodeOutgoingData(self, data:list[tuple], dictType:dict|KDict = dict) -> list[tuple]:
399
400
  '''
400
401
  attempt to decode json objects to python list/dict where applicable.
401
402
  Args:
@@ -408,6 +409,8 @@ class Api:
408
409
  if value and isinstance(value,str) and (value[0] in '[{'):
409
410
  try:
410
411
  value = storage.decodeJSON(value)
412
+ if dictType==KDict:
413
+ value = KDict._wrap(value)
411
414
  except:
412
415
  pass
413
416
 
@@ -613,7 +616,8 @@ class Api:
613
616
  table:str, columns:list, condition:str, conditionData:list,
614
617
  limit:int=MAX_FETCH_ITEMS, returnDicts:bool=False,
615
618
  returnNamespaces:bool=False, parseJson:bool=False,
616
- returnGenerator:bool=False
619
+ returnGenerator:bool=False,
620
+ useKDicts:bool=True,
617
621
  ) -> list|Response:
618
622
  '''
619
623
  attempt to fetch from the database
@@ -624,11 +628,12 @@ class Api:
624
628
  condition(str): a string indicating the SQL condition for the fetch eg `userId=? and dateCreated<?`. all values a represented with the `?` placeholder
625
629
  conditionData(list): a list containing the values for each `?` placeholder in the condition
626
630
  limit(int): number indicating the maximum number of results to fetch
627
- returnDicts(bool): if `True`, we shall return a list of dictionaries as opposed to a list of tuples
631
+ returnDicts(bool): if `True`, we shall return a list of dictionaries(KDict if `useKDicts`=`True`) as opposed to a list of tuples
628
632
  returnNamespaces(bool): if `True`, we shall return a list of named-tuples as opposed to a list of tuples
629
633
  ** if both `returnDicts` and `returnNamespaces` are set, `returnDicts` is effected
630
634
  parseJson(bool):if `True`, we shall parse json objects to python lists and dictionaries where possible
631
635
  returnGenerator(bool): if True, a generator will be returned instead of the list of tuple|dict|SimpleNamespace. this is especially recommended for large data
636
+ useKDicts(bool): if True, all dicts returned are `KDicts` ie dicts that support the dot notation, just like JS objects
632
637
  '''
633
638
  if not (limit>0 and limit<=MAX_FETCH_ITEMS):
634
639
  err = f'please set a limit on the returned rows. maximum should be {MAX_FETCH_ITEMS}'
@@ -666,18 +671,23 @@ class Api:
666
671
  except Exception as e:
667
672
  return Error(str(e))
668
673
 
674
+ dictType = KDict if useKDicts else dict
675
+
669
676
  fetchedData = self.cursor
670
677
  if parseJson and not returnGenerator:
671
- fetchedData = self.__decodeOutgoingData(fetchedData)
678
+ fetchedData = self.__decodeOutgoingData(fetchedData, dictType=dictType)
672
679
 
673
680
  cols = [_[0] for _ in self.cursor.description]
674
681
 
675
682
  if not (returnDicts or returnNamespaces):
676
- returnData = [_ for _ in fetchedData] if not returnGenerator else self.__createFetchResultsGenerator(fetchedData, cols, parseJson, 'plain')
683
+ if fetchedData == self.cursor:
684
+ fetchedData = [_ for _ in fetchedData]
685
+
686
+ returnData = fetchedData if not returnGenerator else self.__createFetchResultsGenerator(fetchedData, cols, parseJson, 'plain')
677
687
  return returnData if not self.__returnKISAResponse else Ok(returnData)
678
688
 
679
689
  if returnDicts:
680
- returnData = [dict(zip(cols,_)) for _ in fetchedData] if not returnGenerator else self.__createFetchResultsGenerator(fetchedData, cols, parseJson, 'dicts')
690
+ returnData = [dictType(zip(cols,_)) for _ in fetchedData] if not returnGenerator else self.__createFetchResultsGenerator(fetchedData, cols, parseJson, 'dicts')
681
691
  return returnData if not self.__returnKISAResponse else Ok(returnData)
682
692
 
683
693
  # namepsaces...
kisa_utils/response.py CHANGED
@@ -1,4 +1,4 @@
1
- from kisa_utils.storage import encodeJSON
1
+ import json as _j; encodeJSON = _j.JSONEncoder().encode
2
2
  from typing import Any
3
3
  import inspect
4
4
 
@@ -57,10 +57,16 @@ class Response(tuple):
57
57
  'status': self.__status,
58
58
  'log': self.__log,
59
59
  'data': self.__data,
60
- }.get(index, self)) == self:
61
- raise ValueError(f'invalid accessor given `{index}`')
60
+ }.get(index, self)) != self:
61
+ return value
62
+
63
+ if not isinstance(self.__data, dict) or (index not in self.__data):
64
+ return self.__data
65
+
66
+ return self.__data[index]
62
67
 
63
- return value
68
+ def __getattr__(self, attr):
69
+ return self[attr]
64
70
 
65
71
  def __iter__(self):
66
72
  return iter([self[0], self[1]])
@@ -137,4 +143,9 @@ def Error(log:str) -> Response:
137
143
  Args:
138
144
  log(str): the log to pass to the Response object
139
145
  '''
140
- return Response(False, log=log)
146
+ return Response(False, log=log)
147
+
148
+ if __name__=='__main__':
149
+ r = Ok(74); print(r['xx'], r.xx, r['xy'], r.data, r)
150
+ r = Ok({'xx':50}); print(r['xx'], r.xx, r['xy'], r['data'], r)
151
+ r = Ok({'xx':{'a':13}}); print(r['xx']['a'], r.xx['a'], r['data'], r)
kisa_utils/storage.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import os, glob, json
2
2
  import inspect
3
+ from kisa_utils.response import Response, Error, Ok
3
4
 
4
5
  encodeJSON = json.JSONEncoder().encode
5
6
  decodeJSON = json.JSONDecoder().decode
@@ -22,7 +23,7 @@ class Path:
22
23
  return os.path.isfile(path)
23
24
 
24
25
  @staticmethod
25
- def createDirectory(path:str) -> dict[str,bool|str]:
26
+ def createDirectory(path:str) -> Response:
26
27
  '''
27
28
  attempt to create a directory provided in `path`.
28
29
  Args:
@@ -31,159 +32,91 @@ class Path:
31
32
  Note:
32
33
  if any intermediate directories dont exist, they will be created as well
33
34
 
34
- Returns:
35
- the standard dict in the form
36
- ```
37
- {
38
- 'status': bool,
39
- 'log': str
40
- }
41
- ```
42
35
  '''
43
- reply = {'status':False, 'log':''}
44
36
  if os.system(f"mkdir -p '{path}' 2> /dev/null"):
45
- reply['log'] = 'failed to create directory. please review your permissions'
46
- return reply
37
+ return Error('failed to create directory. please review your permissions')
47
38
 
48
- reply['status'] = True
49
- return reply
39
+ return Ok()
50
40
 
51
41
  @staticmethod
52
- def createShortcut(source:str, destination:str) -> dict[str,bool|str]:
42
+ def createShortcut(source:str, destination:str) -> Response:
53
43
  '''
54
44
  attempt to create a shortcut.
55
45
  Args:
56
46
  source(str): the path we want to create a shortcut to
57
47
  destination(str): the path to the shortcut
58
-
59
- Returns:
60
- the standard dict in the form of
61
- ```
62
- {
63
- 'status': bool,
64
- 'log': str
65
- }
66
- ```
67
48
  '''
68
- reply = {'status':False, 'log':''}
69
49
 
70
50
  if not Path.exists(source):
71
- reply['log'] = 'the source path does not exist'
72
- return reply
51
+ return Error('the source path does not exist')
73
52
 
74
53
  if Path.exists(destination):
75
- reply['log'] = 'the destination path already exists'
76
- return reply
54
+ return Error('the destination path already exists')
77
55
 
78
56
  if os.system(f"ln -s -T '{source}' '{destination}' 2> /dev/null"):
79
- reply['log'] = 'failed to create the shortcut. please confirm that all necessary paths are valid and exist'
80
- return reply
57
+ return Error('failed to create the shortcut. please confirm that all necessary paths are valid and exist')
81
58
 
82
- reply['status'] = True
83
- return reply
59
+ return Ok()
84
60
 
85
61
  @staticmethod
86
- def delete(path:str) -> dict[str,bool|str]:
62
+ def delete(path:str) -> Response:
87
63
  '''
88
64
  attempt to delete `path`.
89
65
 
90
66
  Note:
91
67
  if the path is a directory, it will NOT be deleted if its empty
92
-
93
- Returns:
94
- the standard dict in the form
95
- ```
96
- {
97
- 'status': bool,
98
- 'log': str
99
- }
100
- ```
101
68
  '''
102
- reply = {'status':False, 'log':''}
103
69
 
104
70
  if not Path.exists(path):
105
- reply['log'] = 'path does not exist'
106
- return reply
71
+ return Error('path does not exist')
107
72
 
108
73
  if Path.isDirectory(path) and os.listdir(path):
109
- reply['log'] = 'can not delete a non-empty directory'
110
- return reply
74
+ return Error('can not delete a non-empty directory')
111
75
 
112
76
  if Path.isFile(path):
113
77
  if os.system(f"rm '{path}' 2> /dev/null"):
114
- reply['log'] = 'failed to delete the file. please ensure you have the right permissions'
115
- return reply
78
+ return Error('failed to delete the file. please ensure you have the right permissions')
116
79
  else:
117
80
  if os.system(f"rmdir '{path}' 2> /dev/null"):
118
- reply['log'] = 'failed to delete the directory. please ensure you have the right permissions'
119
- return reply
81
+ return Error('failed to delete the directory. please ensure you have the right permissions')
120
82
 
121
- reply['status'] = True
122
- return reply
83
+ return Ok()
123
84
 
124
85
  @staticmethod
125
- def copy(source:str, destination:str) -> dict[str,bool|str]:
86
+ def copy(source:str, destination:str) -> Response:
126
87
  '''
127
88
  attempt to copy a file/directory
128
89
  Args:
129
90
  source(str): the path we want to copy
130
91
  destination(str): the path to the new copy of the file/directory
131
-
132
- Returns:
133
- the standard dict in the form
134
- ```
135
- {
136
- 'status': bool,
137
- 'log': str
138
- }
139
- ```
140
92
  '''
141
- reply = {'status':False, 'log':''}
142
93
 
143
94
  if not Path.exists(source):
144
- reply['log'] = 'the source path does not exist'
145
- return reply
95
+ return Error('the source path does not exist')
146
96
 
147
97
  if Path.exists(destination):
148
- reply['log'] = 'the destination path already exists'
149
- return reply
98
+ return Error('the destination path already exists')
150
99
 
151
100
  if os.system(f"cp -rfHpu '{source}' '{destination}' 2> /dev/null"):
152
- reply['log'] = 'failed to copy the source. please confirm that all necessary paths are valid and exist'
153
- return reply
101
+ return Error('failed to copy the source. please confirm that all necessary paths are valid and exist')
154
102
 
155
- reply['status'] = True
156
- return reply
103
+ return Ok()
157
104
 
158
105
  @staticmethod
159
- def listDirectory(path:str) -> dict[str,bool|str|list[str]]:
106
+ def listDirectory(path:str) -> Response:
160
107
  '''
161
108
  attempt to list the contents of a directory
162
109
  Args:
163
110
  path(str): the path to the directory we want to list
164
-
165
- Returns:
166
- the standard dict in the form
167
- ```
168
- {
169
- 'status': bool,
170
- 'log': str,
171
- 'contents': list[str] # list of ABSOLUTE paths
172
- }
173
- ```
174
111
  '''
175
- reply = {'status':False, 'log':'', 'contents':[]}
176
112
 
177
113
  if not Path.isDirectory(path):
178
- reply['log'] = 'the path given is not a directory'
179
- return reply
114
+ return Error('the path given is not a directory')
180
115
 
181
116
  path += '/*'
182
117
  contents = [os.path.abspath(f) for f in glob.glob(path)]
183
118
 
184
- reply['contents'] = contents
185
- reply['status'] = True
186
- return reply
119
+ return Ok(contents)
187
120
 
188
121
  @staticmethod
189
122
  def getMyAbsolutePath() -> str:
@@ -206,7 +139,7 @@ class Path:
206
139
  return os.path.abspath(caller_file)
207
140
 
208
141
  @staticmethod
209
- def directoryBackAt(path:str, howFarBack:int=0) -> dict[str,bool|str]:
142
+ def directoryBackAt(path:str, howFarBack:int=0) -> Response:
210
143
  '''
211
144
  get directory thats `howFarBack` steps from `path`
212
145
  Args:
@@ -223,36 +156,30 @@ class Path:
223
156
 
224
157
  if `howFarBack` = 4, then final directory = '/a/b'
225
158
  ```
226
-
227
- Returns:
228
- dict in form
229
- ```
230
- {
231
- 'status': bool,
232
- 'log': str,
233
- 'directory': str
234
- }
235
- ```
236
159
  '''
237
- reply = {'status':False, 'log':'', 'directory':''}
238
160
 
239
161
  if not Path.exists(path):
240
- reply['log'] = 'could not find the given path'
241
- return reply
162
+ return Error('could not find the given path')
242
163
 
243
164
  if howFarBack<0:
244
- reply['log'] = 'invalid value given for `howFarBack`'
245
- return reply
165
+ return Error('invalid value given for `howFarBack`')
246
166
 
247
167
  parent = os.path.dirname(path)
248
168
 
249
169
  if howFarBack >= parent.count('/'):
250
- reply['log'] = 'value of `howFarBack` goes beyond the root directory `/`'
251
- return reply
170
+ return Error('value of `howFarBack` goes beyond the root directory `/`')
171
+
172
+ return Ok('/'.join(parent.split('/')[:-howFarBack]) if howFarBack else parent)
173
+
174
+ @staticmethod
175
+ def getAbsolutePath(path:str) -> str:
176
+ '''
177
+ get absolute path to file
178
+ Args:
179
+ path(str): path, relative or otherwise whose absolute path we need
180
+ '''
181
+ return os.path.abspath(path)
252
182
 
253
- reply['directory'] = '/'.join(parent.split('/')[:-howFarBack]) if howFarBack else parent
254
- reply['status'] = True
255
- return reply
256
183
 
257
184
  if __name__=='__main__':
258
- pass
185
+ print(Path.getAbsolutePath('.'))
@@ -7,6 +7,7 @@ from typing import Any, get_args, get_origin
7
7
  from types import UnionType
8
8
  from kisa_utils.structures.utils import Value
9
9
  from kisa_utils.response import Response, Ok, Error
10
+ from kisa_utils.dataStructures import KDict
10
11
 
11
12
  def validate(instance:Any, structure:Any, path:str='$') -> dict:
12
13
  '''
@@ -53,11 +54,13 @@ def validate(instance:Any, structure:Any, path:str='$') -> dict:
53
54
  result['status'] = True
54
55
  return result
55
56
  else:
56
- if not isinstance(instance,type(structure)):
57
+ # checking in both directions as dict and KDict can fail when you check
58
+ # `isinstance(dict, KDict)` but will pass when the arguments switch positions
59
+ if (not isinstance(instance,type(structure))) and (not isinstance(structure, type(instance))):
57
60
  result['log'] = f'E02: types not similar:: {path}, expected {str(type(structure))[7:-1]} but got {str(type(instance))[7:-1]}'
58
61
  return result
59
62
 
60
- if isinstance(structure,dict):
63
+ if isinstance(structure,(dict, KDict)):
61
64
  _path = path
62
65
  for key in structure:
63
66
  path = f'{_path}->{key}'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: kisa-utils
3
- Version: 0.39.1
3
+ Version: 0.40.0
4
4
  Summary: Utility functions and modules for KISA Developers
5
5
  Author: Tom Bukenya
6
6
  Author-email: glayn2bukman@gmail.com
@@ -1,9 +1,10 @@
1
- kisa_utils/__init__.py,sha256=iXjGTol3UxJkh_cyISYEAY_yWC5ZlNvIRXFuuiHx76Q,621
1
+ kisa_utils/__init__.py,sha256=z2vCa2O3W1vOFhW4K0_hkpyEuy_H05WUTzdCPHldCyI,659
2
2
  kisa_utils/cache.py,sha256=4Ue5G3QhHSQAmIfQKYgWKWjNL4rA4wLLd_RdBLb2ABY,7345
3
3
  kisa_utils/codes.py,sha256=PV_S53Skggf4XetOdYoIKtEmM8cpN5wZwUlxje70WZY,904
4
4
  kisa_utils/config.py,sha256=NfluzGKTh66qfNtC-Ae0zNb1XzMTgU2Me9Vi82R9c1E,2285
5
+ kisa_utils/dataStructures.py,sha256=ZgLpttJ66jfpU1NWzLDD1Czqxzj6sWereffgTQWhlV8,2679
5
6
  kisa_utils/dates.py,sha256=zxe4n0PdKReZjK5ZkvnCZtJ55lk5oqu9oS8VX_nLozw,13966
6
- kisa_utils/db.py,sha256=tVNMV-hc-oPxR4iRSX-d82Gi4fQwI5Cga1eP8SLvHFs,49202
7
+ kisa_utils/db.py,sha256=oUpqpew3a1a69Ow1xSydfB1S_QhiUrCtba_7dbK0XBs,49715
7
8
  kisa_utils/encryption.py,sha256=nFzNpzWV_D9uSEq4FsgCnlS7FQtqWP9fvM_81rsfcLo,4218
8
9
  kisa_utils/enqueue.py,sha256=VIliaMvw4MUdOqts0dXdZCYNxs-QrOVjIRAR3scGrRM,11786
9
10
  kisa_utils/figures.py,sha256=pYIpQzu1OXRSsY1d98GhgPifnIRmgl-r7S32ai-Ms0c,3731
@@ -11,9 +12,9 @@ kisa_utils/functionUtils.py,sha256=PlXjnmU1uJWNdISlJJ3SCgavTsgNBoebaa9dtWSFhRA,6
11
12
  kisa_utils/log.py,sha256=0TYdxcIBts026RCSuVIQBcZ-CW1ES7n3M1nEIjmeLTM,2295
12
13
  kisa_utils/queues.py,sha256=9QqPtDujw6tbWk7uUiXrsd0rVBTIkzeQw9b45l5Fo3k,6502
13
14
  kisa_utils/remote.py,sha256=0RDrfC4RUW4m6JLziC0_EXJYqzWp38Rw8NDroJ0MuqI,2149
14
- kisa_utils/response.py,sha256=FusfDTBn7gOMqt34XkiUf4d1lWn42_c1n_61wJoPKYU,4182
15
+ kisa_utils/response.py,sha256=asETUBkeF5OlSTwa-coa7lZDCKmQlHCmHf6eaZFl8CU,4560
15
16
  kisa_utils/standardize.py,sha256=nt-uzHQFoKxGscD_MpDYXw65Teg3724whAqa6Kh_zhE,2231
16
- kisa_utils/storage.py,sha256=8-NgrY6BpIo-B9IJNTd-TqMB0m9BY1PuQgZ9WL1z8SM,7683
17
+ kisa_utils/storage.py,sha256=waHLmrf19QxvEQZlWlC2qQvEYFFhDeJHc7MMNbcmj7c,5823
17
18
  kisa_utils/threads.py,sha256=qQqsf64YHMyLpboq5AEXKxYqf3iXUhxiJe6Ymg-vlxI,12840
18
19
  kisa_utils/token.py,sha256=Y2qglWYWpmHxoXBh-TH0r1as0uPV5LLqMNcunLvM4vM,7850
19
20
  kisa_utils/permissions/__config__.py,sha256=i3ELkOydDnjKx2ozQTxLZdZ8DXSeUncnl2kRxANjFmM,613
@@ -22,8 +23,8 @@ kisa_utils/servers/__init__.py,sha256=lPqDyGTrFo0qwPZ2WA9Xtcpc5D8AIU4huqgFx1iZf6
22
23
  kisa_utils/servers/flask.py,sha256=XZYY1pWnP1mSvaS5Uv8G3EFJV5BJBQtU2gDbO8suvLc,40422
23
24
  kisa_utils/structures/__init__.py,sha256=JBU1j3A42jQ62ALKnsS1Hav9YXcYwjDw1wQJtohXPbU,83
24
25
  kisa_utils/structures/utils.py,sha256=665rXIapGwFqejizeJwy3DryeskCQOdgP25BCdLkGvk,2898
25
- kisa_utils/structures/validator.py,sha256=Y4UmB4TH7N-GkK22EV1WOsPWjTeqxVWLTentl1keZD4,4053
26
- kisa_utils-0.39.1.dist-info/METADATA,sha256=_lCvaoHUhJMTWCKO93XOLW-uqssorWIkxc9wB5Ifr7E,477
27
- kisa_utils-0.39.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
28
- kisa_utils-0.39.1.dist-info/top_level.txt,sha256=URxY4sRuqmirOxWtztpVmPoGQdksEMYO6hmYsEDGz2Y,75
29
- kisa_utils-0.39.1.dist-info/RECORD,,
26
+ kisa_utils/structures/validator.py,sha256=JhD9jcfbjTwBr_7OfuNaJd_cYr7wR2emFhsCEo5MCHQ,4323
27
+ kisa_utils-0.40.0.dist-info/METADATA,sha256=OxeJF4Wb_Yk5x5spw-v8bdERToTAICfPzHPs9ntEyhg,477
28
+ kisa_utils-0.40.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
29
+ kisa_utils-0.40.0.dist-info/top_level.txt,sha256=URxY4sRuqmirOxWtztpVmPoGQdksEMYO6hmYsEDGz2Y,75
30
+ kisa_utils-0.40.0.dist-info/RECORD,,